# Building a Player Controller — Step by Step

Each stage below is a **complete, runnable script**. Test each one in-game before moving to the next — that's the point of building it incrementally instead of pasting the final version.

---

## Stage 1 — Basic Movement (no jump, no ground check, no drag)

Goal: get the player moving with WASD, relative to an `orientation` Transform (usually your camera).

```csharp
using UnityEngine;
using UnityEngine.InputSystem;

public class DabouzaMovement : MonoBehaviour
{
    [Header("Movement")]
    public float moveSpeed;

    [Header("References")]
    public Transform orientation;

    float horizontalInput;
    float verticalInput;

    Vector3 moveDirection;

    Rigidbody rb;

    private void Start()
    {
        rb = GetComponent<Rigidbody>();
        rb.freezeRotation = true; // stop the Rigidbody from tipping over from physics collisions
    }

    private void Update()
    {
        MyInput();
    }

    private void FixedUpdate() // physics updates should live here, not in Update()
    {
        MovePlayer();
    }

    private void MyInput()
    {
        horizontalInput = 0f;
        verticalInput = 0f;

        if (Keyboard.current == null) return;

        if (Keyboard.current[Key.D].isPressed) horizontalInput = 1f;
        if (Keyboard.current[Key.A].isPressed) horizontalInput = -1f;
        if (Keyboard.current[Key.W].isPressed) verticalInput = 1f;
        if (Keyboard.current[Key.S].isPressed) verticalInput = -1f;
    }

    private void MovePlayer()
    {
        // combine forward/back and left/right based on where "orientation" is facing
        moveDirection = orientation.forward * verticalInput + orientation.right * horizontalInput;

        rb.AddForce(moveDirection.normalized * moveSpeed * 10f, ForceMode.Force);
    }
}
```

**Setup checklist for this stage:**
- Player GameObject needs a `Rigidbody` and a collider (Capsule Collider is standard).
- `orientation` must be assigned in the Inspector — drag in your camera's Transform, or a dedicated empty child object that only rotates on the Y axis (this matters later so movement doesn't tilt with camera pitch).
- `moveSpeed` — try something like `5`.

**Why `FixedUpdate()` and not `Update()`?**
`FixedUpdate()` runs on a fixed timestep (default every 0.02s = 50 times/sec), independent of framerate. Physics forces (`AddForce`, `Rigidbody.velocity`) should always be applied here — applying physics forces in `Update()` (which runs at variable framerate) causes inconsistent movement speed across different machines/framerates.

**Test:** Press Play. WASD should move the capsule around. If nothing moves, check: Rigidbody attached? `orientation` assigned? `moveSpeed` non-zero?

---

## Stage 2 — Add Jumping (teaching Raycast for ground detection)

Now we add a jump — but a jump needs to know *"is the player currently touching the ground?"* first, otherwise you could jump infinitely in mid-air. That's what `Physics.Raycast` is for.

### What is a Raycast?
Think of it as firing an invisible laser beam from a point, in a direction, for some distance. If that beam hits something on the way, `Physics.Raycast` returns `true` and tells you what it hit.

```csharp
Physics.Raycast(origin, direction, distance, layerMask)
```

- **origin** — where the ray starts (a `Vector3` position)
- **direction** — which way it points (a `Vector3`, usually normalized, e.g. `Vector3.down`)
- **distance** — how far the ray travels before giving up
- **layerMask** — *(optional but important)* restricts what the ray is allowed to hit. Without it, the ray could "detect ground" by hitting the player's own collider or an unrelated object like a floating coin.

For our ground check, we fire a ray straight down from the player's center, just far enough to reach slightly below their feet.

```csharp
grounded = Physics.Raycast(transform.position, Vector3.down, playerHeight * 0.5f + 0.3f, whatIsGround);
```

- `transform.position` — ray starts at the player's center
- `Vector3.down` — pointing straight down
- `playerHeight * 0.5f + 0.3f` — half the player's height gets the ray from the center down to the feet, `+0.3f` adds a small buffer so it still detects ground a moment before/after actual contact (prevents jitter)
- `whatIsGround` — a `LayerMask` you set in the Inspector, so this ray *only* counts hits against objects on your "Ground" layer

### Adding jump on top of Stage 1:

```csharp
using UnityEngine;
using UnityEngine.InputSystem;

public class DabouzaMovement : MonoBehaviour
{
    [Header("Movement")]
    public float moveSpeed;

    [Header("Jumping")]
    public float jumpForce;
    public float jumpCooldown;
    bool readyToJump = true;
    public Key jumpKey = Key.Space;

    [Header("Ground Check")]
    public float playerHeight;
    public LayerMask whatIsGround;
    bool grounded;

    [Header("References")]
    public Transform orientation;

    float horizontalInput;
    float verticalInput;

    Vector3 moveDirection;

    Rigidbody rb;

    private void Start()
    {
        rb = GetComponent<Rigidbody>();
        rb.freezeRotation = true;
    }

    private void Update()
    {
        // ground check — do this in Update() so it's checked every frame, as early as possible
        grounded = Physics.Raycast(transform.position, Vector3.down, playerHeight * 0.5f + 0.3f, whatIsGround);

        MyInput();
    }

    private void FixedUpdate()
    {
        MovePlayer();
    }

    private void MyInput()
    {
        horizontalInput = 0f;
        verticalInput = 0f;

        if (Keyboard.current == null) return;

        if (Keyboard.current[Key.D].isPressed) horizontalInput = 1f;
        if (Keyboard.current[Key.A].isPressed) horizontalInput = -1f;
        if (Keyboard.current[Key.W].isPressed) verticalInput = 1f;
        if (Keyboard.current[Key.S].isPressed) verticalInput = -1f;

        if (Keyboard.current[jumpKey].wasPressedThisFrame && readyToJump && grounded)
        {
            readyToJump = false;
            Jump();
            Invoke(nameof(ResetJump), jumpCooldown);
        }
    }

    private void MovePlayer()
    {
        moveDirection = orientation.forward * verticalInput + orientation.right * horizontalInput;
        rb.AddForce(moveDirection.normalized * moveSpeed * 10f, ForceMode.Force);
    }

    private void Jump()
    {
        // zero out existing vertical velocity so jump height is consistent
        // whether the player was falling, standing still, or already moving up slightly
        rb.velocity = new Vector3(rb.velocity.x, 0f, rb.velocity.z);
        rb.AddForce(transform.up * jumpForce, ForceMode.Impulse);
    }

    private void ResetJump()
    {
        readyToJump = true;
    }
}
```

**New setup steps for this stage:**
- Create a Layer called `Ground` (Inspector → top-right Layer dropdown → Add Layer), assign it to your floor object(s).
- Set `whatIsGround` in the Inspector to that `Ground` layer.
- Set `playerHeight` to roughly match your capsule collider's height (e.g. `2`).
- Set `jumpForce` (try `8`) and `jumpCooldown` (try `0.25`).

**Why `ForceMode.Impulse` for jump but `ForceMode.Force` for movement?**
- `ForceMode.Force` applies continuously over time — right for sustained movement, scaled by mass and `FixedUpdate`'s timestep.
- `ForceMode.Impulse` applies instantly, all at once — right for a jump, which is a single instantaneous push rather than something sustained.

**Why the jump cooldown + `readyToJump` flag?**
Without it, if the player holds Space, `wasPressedThisFrame` only fires once per press anyway — but the cooldown exists so gameplay-wise you can't spam jump immediately after landing, giving the jump a deliberate cadence. `Invoke(nameof(ResetJump), jumpCooldown)` schedules `ResetJump()` to run after `jumpCooldown` seconds, flipping `readyToJump` back to `true`.

**Test:** Press Space while grounded — should jump. Try spamming Space in the air — shouldn't double-jump. Walk off a ledge — `grounded` should become `false` (you can verify with a `Debug.Log(grounded)` temporarily, or watch it live in the Inspector since it's a visible field... actually it's not `public`, so temporarily add `[SerializeField]` above `bool grounded;` if you want to watch it tick in the Inspector).

---

## Stage 3 — Drag (air control vs ground control)

Right now, the player never slows down on their own — Rigidbody `drag` is what creates friction-like deceleration. We want **high drag on the ground** (so the player stops quickly when you let go of keys — feels responsive) and **low/no drag in the air** (so jumps have realistic momentum and aren't awkwardly slowed mid-air).

Add this to `Update()`, right after the ground check:

```csharp
[Header("Movement")]
public float moveSpeed;
public float groundDrag;   // <-- new field
```

```csharp
private void Update()
{
    grounded = Physics.Raycast(transform.position, Vector3.down, playerHeight * 0.5f + 0.3f, whatIsGround);

    MyInput();

    // apply drag based on ground state
    if (grounded)
        rb.drag = groundDrag;
    else
        rb.drag = 0;
}
```

**Setup:** set `groundDrag` to something like `5`.

**Test:** Move, then release keys while grounded — you should stop quickly. Jump and release keys mid-air — you should keep drifting with momentum instead of stopping abruptly.

---

## Stage 4 — Full Combined Script

This is everything above merged together — Stage 1 (movement) + Stage 2 (jump/raycast) + Stage 3 (drag):

```csharp
using UnityEngine;
using UnityEngine.InputSystem;

public class DabouzaMovement : MonoBehaviour
{
    [Header("Movement")]
    public float moveSpeed;
    public float groundDrag;

    [Header("Jumping")]
    public float jumpForce;
    public float jumpCooldown;
    bool readyToJump = true;
    public Key jumpKey = Key.Space;

    [Header("Ground Check")]
    public float playerHeight;
    public LayerMask whatIsGround;
    bool grounded;

    [Header("References")]
    public Transform orientation;

    float horizontalInput;
    float verticalInput;

    Vector3 moveDirection;

    Rigidbody rb;

    private void Start()
    {
        rb = GetComponent<Rigidbody>();
        rb.freezeRotation = true;
    }

    private void Update()
    {
        grounded = Physics.Raycast(transform.position, Vector3.down, playerHeight * 0.5f + 0.3f, whatIsGround);

        MyInput();

        rb.drag = grounded ? groundDrag : 0f;
    }

    private void FixedUpdate()
    {
        MovePlayer();
    }

    private void MyInput()
    {
        horizontalInput = 0f;
        verticalInput = 0f;

        if (Keyboard.current == null) return;

        if (Keyboard.current[Key.D].isPressed) horizontalInput = 1f;
        if (Keyboard.current[Key.A].isPressed) horizontalInput = -1f;
        if (Keyboard.current[Key.W].isPressed) verticalInput = 1f;
        if (Keyboard.current[Key.S].isPressed) verticalInput = -1f;

        if (Keyboard.current[jumpKey].wasPressedThisFrame && readyToJump && grounded)
        {
            readyToJump = false;
            Jump();
            Invoke(nameof(ResetJump), jumpCooldown);
        }
    }

    private void MovePlayer()
    {
        moveDirection = orientation.forward * verticalInput + orientation.right * horizontalInput;
        rb.AddForce(moveDirection.normalized * moveSpeed * 10f, ForceMode.Force);
    }

    private void Jump()
    {
        rb.velocity = new Vector3(rb.velocity.x, 0f, rb.velocity.z);
        rb.AddForce(transform.up * jumpForce, ForceMode.Impulse);
    }

    private void ResetJump()
    {
        readyToJump = true;
    }
}
```

---

## Where to go next (optional future stages)
- **Air control multiplier** — reduce movement force while `!grounded` so players can't fully steer mid-air like on the ground.
- **Speed cap** — clamp `rb.velocity` magnitude on the XZ plane so `moveSpeed` is an actual ceiling, not just an input force.
- **Slope handling** — raycast/`SphereCast` to detect slope angle so the player doesn't slide down or launch off ramps unexpectedly.
- **Crouch/sprint** — modify `moveSpeed` and collider height based on key state.

Each of these is its own small, testable addition — same pattern as above: add one field, one behavior, test, move on.
