This is an upgrade to the previous project here: [[Surface-Walking with an Omnidirectional Player Movement]] ## Setup Instructions for New Input System To upgrade your Unity project to the New Input System (which is the default in Unity 6), follow these steps. This assumes you're starting from a project using the legacy Input Manager. The upgrade focuses on `SurfaceWalker.cs` (which handles all inputs); `CameraAdapter.cs` remains unchanged as it has no input logic. 1. **Install/Enable the Input System Package** (if not already): - Open **Window > Package Manager**. - Search for "Input System" and install/enable it (it's built-in for Unity 2023+ but confirm). - Unity will prompt to restart the editor and switch from the old Input Manager. **Accept this**—it will auto-generate a "NewControls" Input Action Asset if none exists, but we'll customize it. - In **Edit > Project Settings > Player > Active Input Handling**, set to **Input System Package (New)** or **Both** (for fallback during testing). Restart Unity after changing. 2. **Create or Customize the Input Action Asset**: - In Project window, right-click > **Create > Input Actions**. Name it `PlayerInputActions`. - Double-click to open the Input Actions editor. - Create an **Action Map** named `Player`. - Add these **Actions** under the `Player` map (right-click > Add Action): | Action Name | Action Type | Control Type | Interactions | Bindings | |------------------|-------------|--------------|--------------|----------| | **Move** | Value | Vector2 | (none) | - Add **2D Vector Composite**<br>- Up: W key / Up Arrow / Gamepad Left Stick Up<br>- Down: S key / Down Arrow / Gamepad Left Stick Down<br>- Left: A key / Left Arrow / Gamepad Left Stick Left<br>- Right: D key / Right Arrow / Gamepad Left Stick Right<br>- (Optional: Add Mouse Delta for look, but not needed here) | | **StrafeModifier** | Button | (none) | Press | - Right Mouse Button (to match old "Fire3")<br>- (Optional: Add Left Shift as alt binding) | | **Jump** | Button | (none) | Press | - Space key<br>- (Optional: Gamepad South Button / A button) | - Save the asset (Ctrl+S). - **Generate C# Class**: In the Input Actions editor, check **Generate C# Class** and **Update** (or via menu: Actions > Generate C# Class). This creates a `PlayerInputActions.cs` script in your Assets folder—**do not edit this manually**; it's auto-generated. 3. **Add PlayerInput Component** (Optional but Recommended): - Select your Player GameObject. - Add Component > **Player Input**. - Assign your `PlayerInputActions` asset to the **Actions** field. - Set **Behavior** to **Invoke Unity Events** (for callbacks) or leave as **Send Messages** (default). - This auto-enables the actions on Start, but we'll handle it manually in script for more control. 4. **Script Integration**: - Replace your `SurfaceWalker.cs` with the upgraded version below. It now uses the generated `PlayerInputActions` class. - No changes to `CameraAdapter.cs`. - In the Inspector for your Player prefab/GameObject: - Drag the `PlayerInputActions` asset to the new **Input Actions** field in `SurfaceWalker`. - Test: Play the scene. Movement should feel similar (WASD for forward/back/strafe-or-turn, Right Mouse for strafe modifier if `useStrafing` is false, Space to jump). Debug logs will confirm inputs. - **Smoothing Note**: Old `Input.GetAxis("Vertical")` was smoothed; new system uses raw values by default. If you want smoothing, add a simple lerp in code (example commented in script). 5. **Troubleshooting**: - **Inputs not responding?** Ensure actions are enabled (check in script's `Start()`). Remap bindings in the Input Actions editor if needed. - **Legacy Fallback**: If using "Both" in Project Settings, old inputs still work—test by commenting out new code temporarily. - **Gamepad Support**: Already included in bindings; test with a controller. - **Builds**: New Input System works cross-platform, but generate the C# class before building. - If you have multiple action maps or complex setups, consider using `PlayerInput` callbacks instead of polling (e.g., `OnMove` event for `Move` action). ## Upgraded Scripts ### CameraAdapter.cs (Unchanged) ```csharp using UnityEngine; #if CINEMACHINE // Conditional compile—skips if package missing using Cinemachine; #endif public class CameraAdapter : MonoBehaviour { public Vector3 surfaceUp { get; private set; } = Vector3.up; [Header("Cinemachine Setup (Optional)")] #if CINEMACHINE public CinemachineVirtualCamera vcam; // Assign your Vcam #endif private SurfaceWalker walker; void Start() { walker = GetComponent<SurfaceWalker>(); if (walker == null) { Debug.LogWarning("SurfaceWalker missing—CameraAdapter won't sync normals!"); } #if CINEMACHINE if (vcam == null) { Debug.Log("Drag CinemachineVirtualCamera to 'vcam' field for auto-leveling."); } #else Debug.Log("Cinemachine not detected—camera leveling skipped. Install via Package Manager?"); #endif } void Update() { if (walker != null) { surfaceUp = walker.SurfaceNormal; // Now uses the public property (capital S) // Log for debug (remove later) if (surfaceUp != Vector3.up) Debug.Log(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!