# 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.