quot;Surface Up Synced: {surfaceUp}"); #if CINEMACHINE if (vcam != null) { vcam.transform.up = surfaceUp; // Level camera to surface } #endif } } public void UpdateSurfaceUp(Vector3 newUp) { surfaceUp = newUp; #if CINEMACHINE if (vcam != null) { vcam.transform.up = newUp; } #endif } } ``` ## SurfaceWalker.cs ### (Upgraded for New Input System) ```csharp using UnityEngine; using UnityEngine.InputSystem; // Now fully used // Reference to the generated Input Actions class (place in same folder or adjust namespace) public partial class SurfaceWalker : MonoBehaviour { [Header("Detection")] public float detectionDistance = 1.5f; // Fallback ray length public LayerMask groundMask = 1; // Ground/wall/ceiling layer public float orientationSpeed = 8f; // Lerp speed for normal/rotation public float groundedTimeout = 0.2f; // Max air time before fallback gravity public float gravityStrength = 20f; // Pull strength [Header("Movement")] public float turnSpeed = 180f; // Degrees/sec for left/right rotation (tune: 90=slow, 360=snappy) public bool useStrafing = false; // Toggle: True = A/D strafes (sidestep), False = A/D turns (yaw) public float walkSpeed = 5f; public float jumpForce = 10f; public float climbBoost = 2f; // Extra push on steep surfaces (>60° tilt) public Transform cameraTransform; // Main Camera for relative input [Header("Input System")] public PlayerInputActions inputActions; // Assign your Input Actions asset here [Header("Debug Viz")] public bool showGizmos = true; private Rigidbody rb; private Vector3 surfaceNormal = Vector3.up; // Current "up" public Vector3 SurfaceNormal => surfaceNormal; private Vector3 gravityDir = Vector3.down; // Opposite of up public bool isGrounded; // Inspector-visible // Collision tracking private Vector3 avgContactNormal = Vector3.up; private float lastContactTime; // Fallback ray caches private Vector3 debugRayOrigin; private Vector3 debugRayDir; private float debugCastDist; // Input smoothing (optional: mimics old smoothed Vertical) private Vector2 smoothedInput; private float inputSmoothing = 10f; // Lerp speed for vertical smoothing void Awake() { // Auto-generate/load if not assigned (for convenience) if (inputActions == null) { inputActions = new PlayerInputActions(); } } void Start() { rb = GetComponent<Rigidbody>(); if (rb == null) { Debug.LogError("No Rigidbody!"); return; } rb.useGravity = false; rb.constraints = RigidbodyConstraints.FreezeRotationX | RigidbodyConstraints.FreezeRotationZ; // Enable input actions inputActions.Player.Enable(); } void OnDestroy() { if (inputActions != null) { inputActions.Player.Disable(); } } void FixedUpdate() { UpdateGrounded(); // Check timeout DetectSurface(); // Collision or fallback ray ApplyGravity(); if (isGrounded) { OrientToSurface(); HandleMovement(); } } void UpdateGrounded() { isGrounded = (Time.time - lastContactTime) < groundedTimeout; } void DetectSurface() { if (avgContactNormal != Vector3.zero) // Recent collision { surfaceNormal = Vector3.Lerp(surfaceNormal, avgContactNormal, Time.fixedDeltaTime * orientationSpeed); // At end of if (bufferedNormal) or fallback hit block GetComponent<CameraAdapter>()?.UpdateSurfaceUp(surfaceNormal); gravityDir = -surfaceNormal; Debug.Log(quot;Contact Blend | AvgNormal: {avgContactNormal} | New Up: {surfaceNormal}"); avgContactNormal = Vector3.zero; // Reset for next frame return; } // Fallback ray if no recent contact (air/jump) float skinWidth = 0.02f; Vector3 rayOrigin = transform.position + surfaceNormal * skinWidth; Vector3 rayDir = -surfaceNormal; float castDist = detectionDistance; debugRayOrigin = rayOrigin; debugRayDir = rayDir; debugCastDist = castDist; if (Physics.Raycast(rayOrigin, rayDir, out RaycastHit hit, castDist, groundMask)) { surfaceNormal = Vector3.Lerp(surfaceNormal, hit.normal, Time.fixedDeltaTime * orientationSpeed); gravityDir = -surfaceNormal; Debug.Log(quot;Fallback HIT! Normal: {hit.normal} | Dist: {hit.distance}"); } else { Debug.Log("Air! Blending to global up."); surfaceNormal = Vector3.Lerp(surfaceNormal, Vector3.up, Time.fixedDeltaTime * orientationSpeed * 0.5f); gravityDir = Vector3.down; } } void ApplyGravity() { rb.AddForce(gravityDir * gravityStrength * rb.mass, ForceMode.Acceleration); } void OrientToSurface() { // Up-only alignment: Preserve forward, just tilt to surface Quaternion upRotation = Quaternion.FromToRotation(transform.up, surfaceNormal); transform.rotation = Quaternion.Slerp(transform.rotation, upRotation * transform.rotation, Time.fixedDeltaTime * orientationSpeed); } void HandleMovement() { // Read inputs (polled in FixedUpdate for physics sync) Vector2 rawInput = inputActions.Player.Move.ReadValue<Vector2>(); bool modifierPressed = inputActions.Player.StrafeModifier.IsPressed(); bool jumpPressed = inputActions.Player.Jump.triggered; // Optional: Smooth vertical (Y) to mimic old Input.GetAxis("Vertical") smoothedInput = Vector2.Lerp(smoothedInput, rawInput, Time.fixedDeltaTime * inputSmoothing); float horizontalRaw = smoothedInput.x; // Keep X raw-ish (lerp is light) float vertical = smoothedInput.y; // Smoothed Y Vector2 input = new Vector2(horizontalRaw, vertical); if (input.magnitude > 0.1f) { Debug.Log(quot;Input: {input} | Up: {surfaceNormal} | Grounded: {isGrounded} | Mode: {(useStrafing ? "Always Strafe" : (modifierPressed ? "StrafeModifier Strafe" : "Turn"))}"); Vector3 playerForward = Vector3.ProjectOnPlane(transform.forward, surfaceNormal).normalized; Vector3 moveDir = playerForward * input.y; // Base: Forward/back // Determine mode: Always strafe OR StrafeModifier pressed bool strafingThisFrame = useStrafing || modifierPressed; // Horizontal input: Strafe or Turn if (Mathf.Abs(horizontalRaw) > 0.1f) { if (strafingThisFrame) { // Strafe: Move sideways relative to player forward (no rotation) Vector3 playerRight = Vector3.Cross(surfaceNormal, playerForward).normalized; moveDir += playerRight * horizontalRaw; // Add left/right Debug.Log(quot;Strafe | Right Dir: {playerRight}"); // NEW: Clamp angular velocity during strafe (prevent drift/circling) Vector3 angularNormal = Vector3.Project(rb.angularVelocity, surfaceNormal); rb.angularVelocity = angularNormal * 0.9f; // Damp all but up-spin (set to 0 for full stop: angularNormal * 0f) } else { // Turn: Yaw around surface normal float yaw = horizontalRaw * turnSpeed * Time.fixedDeltaTime; transform.RotateAround(transform.position, surfaceNormal, yaw); Debug.Log(quot;Yaw Turn: {yaw:F2}° around {surfaceNormal}"); } } // Normalize if combined (forward + strafe) if (moveDir.magnitude > 1f) moveDir = moveDir.normalized; // Apply velocity Vector3 currentVel = rb.linearVelocity; Vector3 normalComp = Vector3.Project(currentVel, surfaceNormal); Vector3 tangentialComp = currentVel - normalComp; Vector3 desiredTangential = moveDir * walkSpeed; Vector3 newTangential = Vector3.Lerp(tangentialComp, desiredTangential, Time.fixedDeltaTime * orientationSpeed); rb.linearVelocity = normalComp + newTangential; // Climb boost on steep surfaces float tiltAngle = Vector3.Angle(surfaceNormal, Vector3.up); if (tiltAngle > 60f) { rb.AddForce(moveDir * climbBoost * rb.mass, ForceMode.Acceleration); Debug.Log(quot;Climb Boost! Tilt: {tiltAngle:F1}° | Extra: {climbBoost}"); } Debug.Log(quot;Vel: {rb.linearVelocity} | MoveDir: {moveDir}"); } else if (isGrounded) { // Damp tangential for stop Vector3 currentVel = rb.linearVelocity; Vector3 normalComp = Vector3.Project(currentVel, surfaceNormal); Vector3 tangentialComp = currentVel - normalComp; rb.linearVelocity = normalComp + tangentialComp * 0.9f; // Explicit angular stop (damp rotation if no horizontal input) rb.angularVelocity = Vector3.Project(rb.angularVelocity, surfaceNormal) * 0.5f; // Preserve only up-spin (rare), damp rest } // Jump (use .triggered for GetKeyDown-like behavior; checks per-frame) if (jumpPressed && isGrounded) { rb.AddForce(surfaceNormal * jumpForce, ForceMode.Impulse); Debug.Log(quot;Jump along: {surfaceNormal}"); } } void OnCollisionStay(Collision collision) { if (((1 << collision.gameObject.layer) & groundMask) == 0) return; // Not ground lastContactTime = Time.time; // Update grounded timer Vector3 totalNormal = Vector3.zero; int contactCount = collision.contactCount; for (int i = 0; i < contactCount; i++) { totalNormal += collision.GetContact(i).normal; } avgContactNormal = totalNormal.normalized; Debug.Log(quot;Contacts: {contactCount} | Avg Normal: {avgContactNormal} | Collider: {collision.gameObject.name}"); } void OnDrawGizmos() { if (!Application.isPlaying || !showGizmos) return; // Fallback ray (blue if air, green if hit) Gizmos.color = isGrounded ? Color.green : Color.blue; Gizmos.DrawRay(debugRayOrigin, debugRayDir * debugCastDist); // Current up (yellow) Gizmos.color = Color.yellow; Gizmos.DrawLine(transform.position, transform.position + surfaceNormal); // Contact normal (if recent, red) if (avgContactNormal != Vector3.zero) { Gizmos.color = Color.red; Gizmos.DrawLine(transform.position, transform.position + avgContactNormal); } } } ``` This upgrade keeps the original behavior intact while making inputs more modern, extensible, and controller-friendly. If you need further tweaks (e.g., full event callbacks or mouse look), let me know!