# Tutorial: first character controller

Drive a capsule with collide-and-slide instead of a dynamic rigid body. Author a physics actor with `is_character_controller`, set desired velocity each tick via `hi.world().setVelocity`, and read grounded state with `hi.world().physicsState`.

Deep reference: [Physics](/docs/systems/physics.md). Prerequisites: [First physics](/docs/tutorials/first-physics.md), [First input action](/docs/tutorials/first-input.md).

## Zig, Kawa, or both?

| Approach | Author CCT (scene/editor) | Drive desired velocity / grounded |
|---|---|---|
| **Zig-only** | Yes | Yes — `w.setVelocity` / `w.physicsState` in Zig `update` |
| **Zig + Kawa** | Yes | Zig owns locomotion; Kawa can handle `on_collision`, messages, camera, etc. on the same actor |
| **Kawa-only locomotion** | Yes (flag on the actor) | Yes — `Physics.update(actor, { linear_velocity = wish })` and `Physics.state(actor).grounded`. Do **not** use `Actor.set_position` as a substitute (teleports, skips collide-and-slide) |

Authoring the capsule controller is language-agnostic. **Moving** it each frame is `setVelocity` / `Physics.update` — never a position write.

## What you get

| Piece | Role |
|---|---|
| Capsule collider | Shape for the controller (`collider_radius` + `collider_half_height`) |
| Kinematic body | Owned by the CCT; not a dynamic solver body |
| Desired `linear_velocity` | World-space **intent** only (m/s) via `setVelocity` — never bake platform velocity in |
| Ride | Tenkai adds ground surface velocity while grounded (elevators). Jump inherits one frame automatically |
| `PhysicsStateView` | `grounded` / `hit_ceiling` / `ground_normal` / `platform_velocity` (read-only; do not re-add to `setVelocity`) |

Do **not** use forces or `body_type: Dynamic` for player locomotion. Keep dynamics for props and debris.

---

## 1. Entity with physics

```zig
const hi = @import("hikari_game");

// Use the PlayerData definition from step 3.
const PlayerLogic = hi.defineComponent(.{
    .name = "player_logic",
    .storage = .embedded,
    .data = PlayerData,
});

pub const PlayerEntity = hi.defineActor(.{
    .archetype = "player",
    .components = .{ hi.ComponentRender, hi.ComponentPhysics, PlayerLogic },
});
```

Export `defineActor` under project `src/` like any other actor ([First entity](/docs/tutorials/first-entity.md)). The sample game ships `player` in the messaging scene.

---

## 2. Author the controller (scene or editor)

### Scene JSON

```json
{
  "id": "player_01",
  "archetype": "player",
  "transform": {
    "position": [0, 1.2, 0],
    "rotation_euler": [0, 0, 0],
    "scale": [1, 1, 1]
  },
  "components": {
    "render": {
      "mesh": "asset://./models/capsule",
      "material": "asset://./materials/capsule"
    },
    "physics": {
      "is_character_controller": true,
      "collider_shape": "Capsule",
      "collider_radius": 0.35,
      "collider_half_height": 0.5,
      "max_slope_deg": 45,
      "step_offset": 0.35
    }
  }
}
```

Match sample `messaging_player` in `scenes/messaging.json` (capsule visual + CCT block).

Enabling `is_character_controller` forces **Capsule** + **Kinematic**. Capsule axis is body local +Y (upright). Render mesh can be any visual; collider size is independent.

| Field | Meaning | Default |
|---|---|---|
| `collider_radius` | Capsule radius | `0.5` if omitted |
| `collider_half_height` | Half-length of the cylindrical segment (excluding caps) | `0.5` |
| `max_slope_deg` | Steepest walkable surface | `45` |
| `step_offset` | Max step height the controller will climb | `0.35` |

### Editor

Select the actor → **Physics** → enable **Character**, tune radius / half height / max slope / step offset, Save.

---

## 3. Drive desired velocity from Zig

Desired velocity is **not** “current rigid-body velocity you integrate yourself.” Each physics tick the engine calls the capsule move with that vector as desired velocity, then writes grounded flags back onto the component (read next frame via `physicsState`).

Add this definition to the same file as step 1. Keep the `hi` import only once. Runtime vertical velocity lives in `state` so it is not saved as an authored property.

```zig
// Condensed from sample player_entity.zig (camera-yaw move + move_up jump).
const PlayerData = struct {
    move_speed: f32 = 6.0,
    jump_speed: f32 = 7.0,
    state: struct { vertical_velocity: f32 = 0 } = .{},
    gravity: f32 = 20.0,

    pub fn update(self: *@This(), actor: hi.ActorContext, ctx: *const hi.TickContext) void {
        const id = actor.id;
        const w = hi.world();
        const state = w.physicsState(id);
        if (!state.is_character_controller) return;

        var local_x: f32 = 0;
        var local_z: f32 = 0;
        if (w.actionHeld("move_forward")) local_z += 1;
        if (w.actionHeld("move_back")) local_z -= 1;
        if (w.actionHeld("move_right")) local_x += 1;
        if (w.actionHeld("move_left")) local_x -= 1;

        // Camera-relative XZ via primary camera yaw (see docs/systems/coordinate-space.md).
        const cam = w.primaryCamera();
        const yaw = if (cam.isValid() and w.isAlive(cam)) w.rotationEuler(cam)[1] else 0;
        const basis = hi.cameraYawBasis(yaw);
        var wish = basis.forward * @as(@Vector(3, f32), @splat(local_z)) +
            basis.right * @as(@Vector(3, f32), @splat(local_x));
        wish[1] = 0;

        const len_sq = wish[0] * wish[0] + wish[2] * wish[2];
        if (len_sq > 1e-6) {
            const inv = 1.0 / @sqrt(len_sq);
            wish[0] *= inv * self.move_speed;
            wish[2] *= inv * self.move_speed;
        }

        if (state.grounded) {
            self.state.vertical_velocity = 0;
            // Sample binds jump to move_up; add a dedicated "jump" action when ready.
            if (w.actionPressed("move_up")) {
                self.state.vertical_velocity = self.jump_speed;
            }
        } else {
            self.state.vertical_velocity -= self.gravity * ctx.dt;
        }

        wish[1] = self.state.vertical_velocity;
        w.setVelocity(id, wish);
    }
};
```

Sample `input_actions.json` already binds `move_forward` / `move_back` / `move_left` / `move_right` / `move_up`. Full sample: `player_entity.zig` (also faces walk direction). Prefer `w.primaryCamera()` over `w.find("camera")` — scene ids vary (`messaging_camera_main` in the messaging scene).

**Grounded timing:** `physicsState(...).grounded` reflects the **previous** physics move. That is the usual pattern for jump buffering / coyote-time decisions in `update`. Kawa reads the same flag as `Physics.state(self).grounded`.

---

## 4. Optional: spawn at runtime

`SpawnDesc` can initialize the complete physics block before `awake`, including a
character controller. For a loose pickup / prop spawn, see [First runtime spawn](/docs/tutorials/first-runtime-spawn.md):

```zig
const w = hi.world();
const id = try w.spawn(&.{
    .archetype = "cube",
    .id = "pickup_runtime",
    .transform = .{ .position = .{ 0, 2, 0 } },
    .layer = .loose,
});
_ = id;
```

---

## Verify

1. Place a static floor (box or plane) and the player above it (messaging ships `messaging_player`).
2. Play — walk with move actions; character should slide along walls and stand on the floor (`physicsState.grounded == true` while resting).
3. Raise a small step under `step_offset` — walking into it should climb; taller ledges should block.
4. Stop — Edit restores authored transforms (play-time CCT pose does not write back).

## Common mistakes

| Mistake | Fix |
|---|---|
| Dynamic body + CCT flag | Use the Character checkbox / `is_character_controller`; body becomes kinematic |
| Box collider “player” | CCT is capsule-only |
| Expecting gravity from the solver | Apply gravity (and jump) yourself into desired velocity `.y` |
| Reading velocity as post-slide | Treat `setVelocity` as the wish vector; use `physicsState.grounded` for support |

## Next

- [First physics](/docs/tutorials/first-physics.md) — floors, props, triggers around the player
- [First input action](/docs/tutorials/first-input.md) — bind move / jump
- [First messages](/docs/tutorials/first-messaging.md) — react to pads / pickups via collisions
