# 6. Worked Example — Pushable Prop + Character Interaction Part of [[Physics 3D]]. Previous: [[5. Forces FixedUpdate and Motion Modes]]. Next: [[7. Physics Pitfalls]]. **Goal:** A simple dynamic character controller (force-based capsule) that can shove a crate, with correct layers, a Physics Material on the crate, and a NonAlloc ground check. > [!note] Verification status > Authored against Unity 6000.5 Manual/API citations in prior parts. **Not playtested in the Editor for this write-up.** ## 6.1 Scene setup 1. New scene; Plane with MeshCollider (static). 2. Layers: create `Player`, `Prop`, `Ground` (assign Plane → Ground). 3. **Edit > Project Settings > Physics** Layer Collision Matrix: - Player ↔ Ground, Prop - Prop ↔ Ground, Player, Prop - Player ✗ Player (optional) 4. Create **Physics Material** `PropFriction` (dynamic/static friction ~0.4, bounciness 0). 5. Cube `Crate`: - Scale (1,1,1), Layer `Prop` - BoxCollider + Rigidbody (mass 20, Interpolate, Discrete) - Assign `PropFriction` 6. Capsule `Player`: - Layer `Player` - CapsuleCollider + Rigidbody (mass 80, Interpolate, freeze rotation XZ) - Child camera or use Scene view follow ## 6.2 Scripts ### Player push controller ```csharp using UnityEngine; using UnityEngine.InputSystem; [RequireComponent(typeof(Rigidbody))] [RequireComponent(typeof(CapsuleCollider))] public class PushPlayerController : MonoBehaviour { [Header("Move")] [SerializeField] float acceleration = 40f; [SerializeField] float maxSpeed = 6f; [SerializeField] float jumpImpulse = 7f; [Header("Ground")] [SerializeField] LayerMask groundMask; [SerializeField] float groundProbe = 0.3f; [Header("Input")] [SerializeField] InputActionReference move; [SerializeField] InputActionReference jump; Rigidbody _rb; CapsuleCollider _capsule; readonly RaycastHit[] _hits = new RaycastHit[4]; Vector2 _moveInput; bool _jumpQueued; void Awake() { _rb = GetComponent<Rigidbody>(); _capsule = GetComponent<CapsuleCollider>(); _rb.constraints = RigidbodyConstraints.FreezeRotationX | RigidbodyConstraints.FreezeRotationZ; _rb.interpolation = RigidbodyInterpolation.Interpolate; } void OnEnable() { move.action.Enable(); jump.action.Enable(); jump.action.performed += OnJump; } void OnDisable() { jump.action.performed -= OnJump; move.action.Disable(); jump.action.Disable(); } void OnJump(InputAction.CallbackContext ctx) => _jumpQueued = true; void Update() { _moveInput = move.action.ReadValue<Vector2>(); } void FixedUpdate() { bool grounded = IsGrounded(); Vector3 wish = new Vector3(_moveInput.x, 0f, _moveInput.y); if (wish.sqrMagnitude > 1f) wish.Normalize(); Vector3 v = _rb.linearVelocity; Vector3 horizontal = new Vector3(v.x, 0f, v.z); Vector3 desired = wish * maxSpeed; Vector3 delta = desired - horizontal; Vector3 accel = Vector3.ClampMagnitude(delta, acceleration * Time.fixedDeltaTime); _rb.AddForce(new Vector3(accel.x, 0f, accel.z) / Time.fixedDeltaTime, ForceMode.Acceleration); if (_jumpQueued && grounded) _rb.AddForce(Vector3.up * jumpImpulse, ForceMode.VelocityChange); _jumpQueued = false; } bool IsGrounded() { Vector3 origin = transform.TransformPoint(_capsule.center); float radius = _capsule.radius * 0.95f; float cast = (_capsule.height * 0.5f) - radius + groundProbe; int count = Physics.SphereCastNonAlloc(origin, radius, Vector3.down, _hits, cast, groundMask, QueryTriggerInteraction.Ignore); return count > 0; } void OnCollisionEnter(Collision collision) { if (collision.rigidbody == null) return; // Optional: amplify shove on props lighter than player if (collision.rigidbody.mass < _rb.mass && collision.gameObject.layer == LayerMask.NameToLayer("Prop")) { Vector3 push = _rb.linearVelocity; push.y = 0f; collision.rigidbody.AddForce(push.normalized * 2f, ForceMode.Impulse); } } } ``` > [!warning] Code not compiled in Unity > Not compiled in Editor. Wire Input Action References from an Input Actions asset ([[Input System]]). ### Optional prop sleep watchdog ```csharp using UnityEngine; [RequireComponent(typeof(Rigidbody))] public class PropSleepWatchdog : MonoBehaviour { Rigidbody _rb; void Awake() => _rb = GetComponent<Rigidbody>(); void OnCollisionEnter(Collision collision) { if (_rb.IsSleeping()) _rb.WakeUp(); } } ``` > [!warning] Code not compiled in Unity > Not compiled in Editor for this write-up. ## 6.3 Tuning checklist | Knob | If wrong | |---|---| | Crate mass >> player | Feels immovable — raise player accel or lower mass | | Friction too high | Crate sticks; lower dynamic friction | | Player not Interpolating | Camera jitter | | Prop Discrete + fast hit | Rare tunnel — Speculative on prop or player | | Missing layer collision | Walk through crate | Feel targets belong in a short design note ([[Level Up! (Review)]]). ## 6.4 Extensions - Enemy shove reactions → [[Enemy AI State Machine]] - Sensing before engaging crates as cover → [[Organic Enemy Sensing]] - Networked crates → authority rules in [[Multiplayer with Netcode for GameObjects]] ## Next [[7. Physics Pitfalls]] — tunneling, scale, CCD cost, sleep traps.