Getting started
This page builds the smallest useful gamekit program: one entity, two components and one system, stepped by the runtime's fixed tick. Then it moves the same idea into a window with a scene, input and a drawn square.
You need thicket and a copy of the gamekit folder (gamekit-tks) with the willowui folder (willowui-tks) beside it. gamekit finds willowui through its own packages, so the two folders sit side by side.
1. Make a project
text
mygame/
thicket.json5
app.tks
gamekit-tks/
willowui-tks/thicket.json5:
json5
{
entry: "app.tks",
systems: ["willow", "bitmap"],
packages: { gamekit: "../gamekit-tks" },
}There is no window section yet, so this is a console program. willow works without a window, which is what lets gamekit code run in a terminal and in headless tests.
2. Components are plain classes
ts
import { ComponentKey, Entity, PhaseSchedule, World } from "gamekit";
class Position {
x: float;
y: float;
constructor(x: float, y: float) {
this.x = x;
this.y = y;
}
}
class Velocity {
dx: float;
dy: float;
constructor(dx: float, dy: float) {
this.dx = dx;
this.dy = dy;
}
}
const POSITION: ComponentKey<Position> = new ComponentKey<Position>("position");
const VELOCITY: ComponentKey<Velocity> = new ComponentKey<Velocity>("velocity");A component is any class. It needs no base class. A ComponentKey<T> names a kind of component and carries its type, so reading a component back gives you a Position, not an any. The string label is for debugging only; two keys with the same label are still different keys.
3. A world holds the entities
ts
const world: World = new World();
world.registerStore(POSITION);
world.registerStore(VELOCITY);
const ball: Entity = world.createEntity();
world.put(ball, POSITION, new Position(0.0, 0.0));
world.put(ball, VELOCITY, new Velocity(60.0, 30.0));registerStore gives the world one store per key. createEntity makes an entity the world owns, and put files a component in that key's store, where queries can find it.
4. A system is a function
ts
const schedule: PhaseSchedule = new PhaseSchedule();
schedule.addPhase("simulate", [], true);
schedule.addSystem("simulate", (w: World, dt: float, tick: int): void => {
w.query2(POSITION, VELOCITY).each((entity: Entity, p: Position, v: Velocity): void => {
p.x += v.dx * dt;
p.y += v.dy * dt;
});
});A PhaseSchedule runs systems in named phases, in the order you add them. addPhase(name, after, flushAfter) takes the phases it must come after, and flushAfter: true applies any deferred entity changes when the phase ends. query2 visits every entity that has both components.
5. Let the runtime tick it
ts
App.tps = 10.0;
App.onTick((): void => {
schedule.run(world, App.tickDelta, App.tick);
const p: Position = world.getRequired(ball, POSITION);
console.log("tick " + App.tick + ": ball at " + p.x + ", " + p.y);
if (App.tick >= 4) {
App.quit();
}
});App.onTick runs once per fixed logic tick, App.tps times a second, and a console program with a tick handler keeps running until App.quit(). Put the five snippets in app.tks in order and run it:
sh
thicket runtext
tick 0: ball at 6, 3
tick 1: ball at 12, 6
tick 2: ball at 18, 9
tick 3: ball at 24, 12
tick 4: ball at 30, 156. Put it in a window
A window game adds a window section:
json5
{
entry: "game.tks",
systems: ["willow", "bitmap"],
packages: { gamekit: "../gamekit-tks" },
window: { title: "Drift", width: 640, height: 480 },
}This version moves a square left and right with the arrow keys or A and D. It adds three gamekit parts:
- A scene (
AppScene) that owns the world and a willow node. ASceneStackstarts it. - A
WorldTicker, which steps the world on the scene node's tick. Pause or dispose the node and the world stops with it. - An
InputPolicy, which turns the coreInputMapinto anActionFrameof named actions, and aWillowPresentationAdapter, which draws eachBodyas a willowQuadin thepresentphase.
ts
import { Node, Quad } from "willow";
import { AppScene, ComponentKey, Entity, InputPolicy, PhaseSchedule, SceneStack, World, WorldTicker } from "gamekit";
import { WillowPresentationAdapter, WillowPresentationState } from "gamekit/src/willow";
enum Action { LEFT, RIGHT, MOVE_X }
class Body {
x: float;
y: float;
speed: float = 0.0;
constructor(x: float, y: float) {
this.x = x;
this.y = y;
}
}
const BODY: ComponentKey<Body> = new ComponentKey<Body>("body");
class PlayScene extends AppScene {
private world: World = new World();
private schedule: PhaseSchedule = new PhaseSchedule();
private input: InputPolicy = new InputPolicy();
private adapter: WillowPresentationAdapter<Body>;
private ticker: WorldTicker | null = null;
private root: Node = new Node("play");
constructor() {
super("play");
this.world.registerStore(BODY);
InputMap.bind(Action.LEFT as int, [Key.LEFT, Key.A]);
InputMap.bind(Action.RIGHT as int, [Key.RIGHT, Key.D]);
InputMap.axis(Action.MOVE_X as int, Action.LEFT as int, Action.RIGHT as int);
this.input.watch("move_x", Action.MOVE_X as int);
// Presentation: one Quad per Body, synced in the present phase.
this.adapter = new WillowPresentationAdapter<Body>(
(body: Body): Node => new Quad(32.0, 32.0, Color{ value: 0x40C080FF }),
(body: Body): WillowPresentationState => {
const state: WillowPresentationState = new WillowPresentationState(this.root);
state.x = body.x;
state.y = body.y;
return state;
});
this.schedule.addPhase("input", [], true);
this.schedule.addPhase("simulate", ["input"], true);
this.schedule.addPhase("present", ["simulate"]);
this.schedule.addSystem("input", (w: World, dt: float, tick: int): void => {
const axis: float = this.input.frame().axis("move_x");
w.query(BODY).each((e: Entity, b: Body): void => { b.speed = axis * 200.0; });
});
this.schedule.addSystem("simulate", (w: World, dt: float, tick: int): void => {
w.query(BODY).each((e: Entity, b: Body): void => { b.x += b.speed * dt; });
});
this.schedule.addSystem("present", (w: World, dt: float, tick: int): void => {
this.adapter.present();
});
engine.root2d.add(this.root);
this.attachNode(this.root);
}
override async enter(): Promise<void> {
const player: Entity = this.world.createEntity();
const body: Body = new Body(304.0, 224.0);
this.world.put(player, BODY, body);
this.adapter.bind(body);
// The runtime's fixed tick steps the world while this scene's node runs.
this.ticker = new WorldTicker(this.root, this.world, this.schedule, App.tickDelta);
}
override async exit(): Promise<void> {
if (this.ticker != null) { this.ticker.dispose(); }
this.adapter.dispose();
this.world.dispose();
}
}
const scenes: SceneStack = new SceneStack();
await scenes.start(new PlayScene());The simulation never touches a willow node. Systems change Body records, and only the present phase copies them onto the screen. That split is what lets the same simulation run headless in a test.
Next
- How gamekit is built: the ideas every family shares.
- World, queries and systems: stores, deferred commands and phase rules.
- Scenes and Input actions: the two parts every game reaches for next.