Skip to content
hikari
RenderingEditorAIToolchainDocumentation
GitHub
hikari

© 2026 Flying Rat Studio.
All rights reserved.

Explore the engineDocumentationContributorsLicenseBack to top
Documentation / Tutorials
Browse docs
Overview
Tutorials16
OverviewFirst game projectFirst entityAuthored user_data and the inspectorFirst mesh and materialFirst physics body and triggerFirst character controllerFirst session servicesFirst UIFirst input actionFirst messagesPlay, Edit, and scenesFirst runtime spawnFirst motionFirst skeletal animationAssets in Play (soft refs and hot reload)Dynamic editor recompile
Guides16
OverviewDevelopment guideGameplay APIChoosing component storageBuild and packagingProject filePluginsHikari Plugin APIData-driven content, JSON, and pathsScripting with KawaShader authoringTime of dayUser interfaceUI layoutUI widgetsMigration from Unity / UnrealActor and component lifecycle
Systems28
OverviewArchitectureApplication lifecycleFrontends and driversPlatforms and supportSession services and cross-scene stateScenes and gameplayActor communication (hi.actors)Game-facing refsSave / replication wire versionCoordinate space and camera conventionsRenderingRenderer architecture mapFrame governorGPU particlesVisual ZonesVolumetric mediaInputAudioPhysicsMotion KitTemporal KitAssets and ShinraPrefabsAsset residencyAsset formats (Shinra pipeline)UI and editorEditor asset hot reloadEditor Project Selector
Language reference2
OverviewAkari language referenceKawa language reference
Engine overview
Start exploring
  • No matching sections. Try fewer words or another topic.
NavigateEnter Openesc Close
Overview
Tutorials16
OverviewFirst game projectFirst entityAuthored user_data and the inspectorFirst mesh and materialFirst physics body and triggerFirst character controllerFirst session servicesFirst UIFirst input actionFirst messagesPlay, Edit, and scenesFirst runtime spawnFirst motionFirst skeletal animationAssets in Play (soft refs and hot reload)Dynamic editor recompile
Guides16
OverviewDevelopment guideGameplay APIChoosing component storageBuild and packagingProject filePluginsHikari Plugin APIData-driven content, JSON, and pathsScripting with KawaShader authoringTime of dayUser interfaceUI layoutUI widgetsMigration from Unity / UnrealActor and component lifecycle
Systems28
OverviewArchitectureApplication lifecycleFrontends and driversPlatforms and supportSession services and cross-scene stateScenes and gameplayActor communication (hi.actors)Game-facing refsSave / replication wire versionCoordinate space and camera conventionsRenderingRenderer architecture mapFrame governorGPU particlesVisual ZonesVolumetric mediaInputAudioPhysicsMotion KitTemporal KitAssets and ShinraPrefabsAsset residencyAsset formats (Shinra pipeline)UI and editorEditor asset hot reloadEditor Project Selector
Language reference2
OverviewAkari language referenceKawa language reference
Engine overview
Tutorials5 min read

Tutorial: first character controller

On this page
On this pageZig, Kawa, or both?What you get1. Entity with physics2. Author the controller (scene or editor)Scene JSONEditor3. Drive desired velocity from Zig4. Optional: spawn at runtimeVerifyCommon mistakesNext Back to top

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. Prerequisites: First physics, First input action.

Zig, Kawa, or both?

ApproachAuthor CCT (scene/editor)Drive desired velocity / grounded
Zig-onlyYesYes — w.setVelocity / w.physicsState in Zig update
Zig + KawaYesZig owns locomotion; Kawa can handle on_collision, messages, camera, etc. on the same actor
Kawa-only locomotionYes (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

PieceRole
Capsule colliderShape for the controller (collider_radius + collider_half_height)
Kinematic bodyOwned by the CCT; not a dynamic solver body
Desired linear_velocityWorld-space intent only (m/s) via setVelocity — never bake platform velocity in
RideTenkai adds ground surface velocity while grounded (elevators). Jump inherits one frame automatically
PhysicsStateViewgrounded / 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");

// PlayerBehaviour → defineComponent + list next to capabilities
const PlayerLogic = hi.defineComponent(.{
    .name = "player_logic",
    .storage = .embedded,
    .data = struct {
        // … fields + update that call setVelocity / physicsState …
    },
});

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

Export defineActor under project src/ like any other actor (First entity). 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.

FieldMeaningDefault
collider_radiusCapsule radius0.5 if omitted
collider_half_heightHalf-length of the cylindrical segment (excluding caps)0.5
max_slope_degSteepest walkable surface45
step_offsetMax step height the controller will climb0.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).

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

// Condensed from sample player_entity.zig (camera-yaw move + move_up jump).
const PlayerBehaviour = struct {
    move_speed: f32 = 6.0,
    jump_speed: f32 = 7.0,
    vertical_velocity: f32 = 0.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.vertical_velocity = 0;
            // Sample binds jump to move_up; add a dedicated "jump" action when ready.
            if (w.actionPressed("move_up")) {
                self.vertical_velocity = self.jump_speed;
            }
        } else {
            self.vertical_velocity -= self.gravity * ctx.dt;
        }

        wish[1] = self.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:

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

MistakeFix
Dynamic body + CCT flagUse the Character checkbox / is_character_controller; body becomes kinematic
Box collider “player”CCT is capsule-only
Expecting gravity from the solverApply gravity (and jump) yourself into desired velocity .y
Reading velocity as post-slideTreat setVelocity as the wish vector; use physicsState.grounded for support

Next

  • First physics — floors, props, triggers around the player
  • First input action — bind move / jump
  • First messages — react to pads / pickups via collisions
PreviousTutorial: first physics body and triggerNext Tutorial: first session services

Documentation follows the current engine checkout.

Snapshot cc148c75Source docs/tutorials/first-character-controller.md
On this pageZig, Kawa, or both?What you get1. Entity with physics2. Author the controller (scene or editor)Scene JSONEditor3. Drive desired velocity from Zig4. Optional: spawn at runtimeVerifyCommon mistakesNext Back to top