Opt into physics with hi.ComponentPhysics on the archetype, then author a physics block under scene "components". Scene-authored physics defaults to Static when body_type is omitted (or the whole block is missing on a physics-capable archetype) — set "body_type": "Dynamic" explicitly for props that should fall. Runtime spawn still defaults to Dynamic. Collisions reach Zig via onCollision and Kawa via on_collision.
Deep reference: Physics. Samples: trigger_entity.zig, cube_entity.zig, fall_sensor in scenes/physics_playground.json.
1. Dynamic body (simple)
const hi = @import("hikari_game");
pub const BoxEntity = hi.defineActor(.{
.archetype = "box",
.components = .{ hi.ComponentRender, hi.ComponentPhysics },
});{
"id": "box_01",
"archetype": "box",
"transform": { "position": [0, 2, 0], "rotation_euler": [0, 0, 0], "scale": [1, 1, 1] },
"components": {
"render": {
"mesh": "asset://./models/cube",
"material": "asset://./materials/cube"
},
"physics": {
"body_type": "Dynamic",
"collider_shape": "Box",
"collider_box_size": [1, 1, 1],
"mass": 1.0
}
}
}Render scale and collider size are independent when hand-authored. To size the collider from the mesh local AABB (box / sphere / capsule, local space — transform scale is applied at body create):
_ = hi.world().fitColliderToRender(id);
// or read bounds yourself:
if (hi.render().localBounds(id)) |aabb| {
const min_v, const max_v = aabb;
_ = .{ min_v, max_v };
}Common body_type values in samples: Dynamic, Static, Trigger.
Optional "is_active": false parks the body without removing the component (entity "active": false also disables effective physics). "enabled" is accepted as an alias, matching how light / visual_zone spell the same switch; is_active stays canonical and is what gets written back on save. Runtime: hi.world().setPhysicsActive(id, false). See Active / enable trio.
2. Trigger volume
const std = @import("hikari_std");
const hi = @import("hikari_game");
const TriggerLogic = hi.defineComponent(.{
.name = "trigger_logic",
.storage = .embedded,
.data = struct {
pub const metadata = .{ .runtime_only = true };
pub fn onCollision(_: *@This(), _: hi.ActorContext, info: *const hi.CollisionInfo) void {
if (!info.other.isValid()) return;
const other_name = hi.world().name(info.other);
switch (info.phase) {
.enter => std.log.info("enter {s}", .{other_name}),
.exit => std.log.info("exit {s}", .{other_name}),
.stay => {},
}
}
},
});
pub const TriggerEntity = hi.defineActor(.{
.archetype = "trigger",
.components = .{ hi.ComponentPhysics, TriggerLogic },
});{
"id": "fall_sensor",
"archetype": "trigger",
"transform": { "position": [0, 6, 0], "rotation_euler": [0, 0, 0], "scale": [1, 1, 1] },
"components": {
"physics": {
"body_type": "Trigger",
"collider_shape": "Box",
"collider_box_size": [24, 0.5, 24]
}
}
}Triggers do not need a mesh. Play, drop a dynamic cube through the volume, watch ENTER/EXIT logs.
3. Fire gameplay from a collision
Sample message_pad_entity.zig sends messages on first .enter — combine with First messages.
Verify
- Play
scenes/physics_playground— cubes fall;FallSensorlogs when they pass through. - Edit mode — no simulation (bodies frozen).
- Stop — document transforms restore; play-time physics poses do not write back.
Next
- Character controller → First character controller