Move on a grid
Goal
Give a creature-RPG explorer tile stepping: a quick tap turns, holding walks, Shift runs, and walls or NPCs cause a bump. Start from templates/blank.
Files you will edit
src/game.ts
Steps
Replace src/game.ts with the example below. Keep your template's settings. The grid component is the root body. Its default capsule extends 0.9 metres above and below the actor position, so place its centre at y = 0.9. spawnTileColliders addresses cells by their lower corner; origin (0.5, 0.5) aligns the walkers with those cell centres.
ts
import {
box,
defineActor,
defineGame,
gridInputFromKeys,
gridMovement,
mesh,
requestGridMove,
spawnActor,
spawnTileColliders,
} from 'gameable';
import settings from '../settings.json';
const Explorer = defineActor('Explorer', {
components: {
root: gridMovement({
origin: { x: 0.5, z: 0.5 },
onBump(self, tile, hitEntity) {
// Feed an animation or sound system through saved state.
self.state.bumpX = tile.x;
self.state.bumpZ = tile.z;
self.state.bumpEntity = hitEntity ?? -1;
},
}),
look: mesh('cube-mint', { scale: [0.5, 0.7, 0.5] }),
},
tick(self, ctx) {
const direction = gridInputFromKeys(ctx);
if (direction) requestGridMove(self.entity, direction, ctx.input.isDown('Shift'));
},
});
const Villager = defineActor('Villager', {
components: {
root: box([0.35, 0.9, 0.35], { collision: 'Pawn' }),
look: mesh('cube-muted', { scale: [0.5, 0.7, 0.5] }),
},
});
export default defineGame({
features: settings.features,
world: settings.world,
init(ctx) {
gridInputFromKeys(ctx); // Allocate input history before gameplay.
spawnTileColliders(ctx, [
{ x: 2, z: 0, height: 2 },
{ x: 2, z: 1, height: 2 },
{ x: 2, z: 2, height: 2 },
]);
spawnActor(ctx, Explorer, { position: { x: 0.5, y: 0.9, z: 0.5 } });
spawnActor(ctx, Villager, { position: { x: 0.5, y: 0.9, z: 2.5 } });
ctx.camera.set({ x: 6, y: 10, z: 8 }, { x: 0, y: 0, z: 0, w: 1 });
ctx.camera.lookAt({ x: 1.5, y: 0, z: 1.5 });
},
});Include the placeholder pack in the template's asset manifest for cube-mint and cube-muted, as in Use the placeholder assets. The wall bodies are invisible; replace their look with your world artwork. NPCs using gridMovement() also reserve destinations, preventing two walkers from entering the same tile. Resting walkers keep their occupied tile blocked.
Requests from actor ticks and user systems are consumed on the next component step. Repeat the request each tick while held. A fresh hold waits 0.08 seconds before stepping; continuing steps take 0.22 seconds each, or that duration divided by 1.8 when running. North is -Z. For eight-way movement, set directions: 8 and call gridInputFromKeys(ctx, 8); diagonal steps take the same time as cardinal steps and check their destination tile. They can pass between blocked corners.
Verify
Run npm run dev. Tap D briefly: turn east without changing tiles. Hold D: walk one tile, then bump against the wall at cell (2,0). Hold S to approach the villager at (0,2); bump without sliding. Hold Shift with a direction to run.