Physics, collisions and raycasts

Add rigid bodies, sensors, layers, impulses and raycasts.

Bodies and colliders

dynamic bodies move under gravity and contact, kinematic ones are moved by you and push dynamic ones, and fixed ones never move. The collider matches the mesh (models get a box) and is sized from the entity when the body is created.

Later scale changes don't resize it, so set explicit sizes for gameplay volumes. set(entity, { physics: null }) removes the body.

Floor, ball and paddle

React and React Native · shared engine API

Floor, ball and paddle
import type { World } from "@nayan-ui/engine";
export function setupPhysics(world: World) {
world.spawn({ mesh: "cube", position: [0, -0.5, 0], scale: [30, 1, 30], physics: "fixed" });
const ball = world.spawn({ mesh: "sphere", position: [0, 5, 0], physics: { type: "dynamic", bounce: 0.6 } });
const paddle = world.spawn({ mesh: "cube", position: [0, 0.5, 4], scale: [3, 0.5, 0.5], physics: "kinematic" });
return { ball, paddle };
}

PhysicsOptions reference

physics takes a BodyType string for defaults, or PhysicsOptions.

PhysicsOptions reference reference
OptionBehavior
typedynamic, kinematic or fixed.
shapeball, box, cylinder, capsule or cone; default matches the mesh.
radius / heightBall, cylinder, capsule or cone radius and total height.
sizeFull box size Vec3; default the entity's size.
layer / maskLayer bits (default 1) and layers it meets (default all).
sensorReports touches without pushing.
friction / bounce / densityDefaults 0.5, 0 (0..1) and 1 (mass per volume).
drag / angularDragLinear and spin damping; defaults 0 and 0.05.
gravityScaleMultiplier on world gravity; default 1.
uprightNever tumbles (characters).
ccdStops fast, small bodies passing through walls.
planar2D: stays in XY and spins around z; camera looks down -z.

Character capsule

React and React Native · shared engine API

Character capsule
import type { PhysicsOptions, World } from "@nayan-ui/engine";
const body: PhysicsOptions = {
type: "dynamic", shape: "capsule", radius: 0.4, height: 1.8,
upright: true, ccd: true, friction: 0.2, drag: 0.5,
};
export function addHero(world: World) {
return world.spawn({ mesh: "capsule", position: [0, 2, 0], scale: [0.8, 1.8, 0.8], physics: body });
}

Steer and push

groundVelocity sets horizontal [x, z] speed and keeps the vertical one, so a steered body still falls. impulse(entity, vec) is a one-off push for dynamic bodies: jumps, knockback.

Steer and jump

React and React Native · shared engine API

Call steer from onUpdate with joystick input.

Steer and jump
import type { Entity, World } from "@nayan-ui/engine";
export function steer(world: World, player: Entity, x: number, z: number) {
world.set(player, { groundVelocity: [x * 6, z * 6] });
}
export function jump(world: World, player: Entity) {
world.impulse(player, [0, 5, 0]);
}

Sensors, layers and masks

A sensor reports touches but never pushes: pickups and trigger zones. Two colliders interact when either one's mask includes the other's layer.

Pickup on its own layer

React and React Native · shared engine API

Pickup on its own layer
import type { World } from "@nayan-ui/engine";
export const PLAYER = 2;
export const PICKUP = 4;
export function addPickup(world: World, x: number, z: number) {
return world.spawn({
mesh: "sphere", position: [x, 1, z], color: "yellow",
physics: { type: "fixed", shape: "ball", radius: 0.7, sensor: true, layer: PICKUP, mask: PLAYER },
});
}

Collision events

forEachCollision in onUpdate visits the touches that started or ended during the last step. CollisionInfo (started, sensor, speed) is reused, and either entity may already be despawned by an earlier callback.

Collect pickups

React and React Native · shared engine API

Call from onUpdate; GameView steps the world right after.

Collect pickups
import type { Entity, World } from "@nayan-ui/engine";
export function collect(world: World, player: Entity, pickups: Set<Entity>) {
world.forEachCollision((a, b, info) => {
if (!info.started || !info.sensor) return;
const other = a === player ? b : b === player ? a : null;
if (other === null || !pickups.delete(other)) return;
world.despawn(other);
world.scoreboard.add(100);
});
}

Raycasts

raycast(origin, direction, maxDistance, mask?, out?) returns the first solid, non-sensor collider as of the last step, as { entity, distance, normal, point }, or null. Reuse an out hit per frame.

Ground probe

React and React Native · shared engine API

Ground probe
import type { World } from "@nayan-ui/engine";
export function groundHeight(world: World, x: number, y: number, z: number) {
const hit = world.raycast([x, y + 1, z], [0, -1, 0], 20, 1);
return hit ? hit.point[1] : null;
}

Impact feedback

impact makes the core play a sound and haptic by itself when the entity hits something solid, scaled by speed. Sounds pan relative to world.listener; leave impact off when the player muted sounds.

Impact feedback reference
ImpactFeedbackBehavior
soundA loaded Sound; omit for haptics only.
minSpeed / maxSpeedSilent below; full strength at; defaults 1 and 10.
volumeVolume at full strength, 0..2; default 1.
hapticVibration at full strength, 0..1; default 0.

Bouncing crate

React and React Native · shared engine API

Bouncing crate
import type { Sound, World } from "@nayan-ui/engine";
export function addCrate(world: World, thud: Sound, sounds: boolean) {
return world.spawn({
mesh: "cube", position: [0, 6, 0], physics: "dynamic",
impact: sounds ? { sound: thud, minSpeed: 2, maxSpeed: 12, haptic: 0.4 } : null,
});
}