How gamekit is built
Every gamekit family follows the same few rules. Once you know them, the rest of the API is predictable.
No central Game class
gamekit has no Game to subclass and no global registry. You create a World, a SceneStack or an AssetManager yourself and keep a reference to it. A small game can use SceneStack alone, or a World alone, or neither.
Ownership is explicit
Each object has one owner, and the owner decides when it ends.
- A
Worldowns the entities it creates or adopts.world.disposeEntity(e)ends one;world.dispose()ends them all. - A
SceneStackalone starts, covers, uncovers and ends scenes. A scene belongs to one stack at a time. - An
AssetScopeowns the handles it loaded. Disposing the scope releases them. - A
WillowPresentationAdapterowns exactly the nodes its factory created, never your other nodes.
When something is released, it says so: a handle used after its scope is disposed throws StaleHandle instead of returning old data.
Time is passed in
gamekit never reads the wall clock and never starts a timer of its own. Time arrives as an argument:
- Systems receive
dtand the tick number fromPhaseSchedule.run. Gauge.advance(dt)fills a gauge.StateMachine.update(delta)updates a state.- Timed effects and
StateMachine.transitionAfterneed a scheduler you supply: in a game a coreTicker(App.tickeror a node's ticker), in a test aClockScheduler.
So the same input gives the same result every run, and a test can run a minute of game time in a few milliseconds.
Events are local, rules are pipelines
An EventScope delivers completed facts ("the door opened") to listeners. Each entity has its own scope (entity.events); nothing bubbles up a hierarchy on its own. When rules need to change something before it happens ("armour reduces this hit"), that is a MutablePipeline, a separate tool with stages that can modify, produce a result or cancel. See Events and pipelines.
Simulation and presentation stay apart
Simulation code works on your own records and components. Willow nodes are touched only at the presentation boundary, the present() call of a WillowPresentationAdapter, usually from a present phase. That keeps the simulation testable without a window.
No genre built in
gamekit has no nouns like "unit", "spell" or "quest". Conditions, actions, effects, definitions and interactions carry your own data. The tactics example keeps its grid, teams and rules in the example, not in gamekit.
Families and what they may use
gamekit checks its own layering in its tests. The rules, as written in src/functions.tks:
| Family | May import these gamekit families | External systems |
|---|---|---|
core | none | willow, willowui |
ecs | core | willow, willowui |
save | none | fs (for FsByteStorage), willow, willowui |
app | none | willow, willowui |
audio | none | willow, willowui |
willow | core, ecs, app | willow, willowui |
ui | core, app | willow, willowui |
testing | core, ecs, save, app | fs, willow, willowui |
The package root (import ... from "gamekit") exports core, ecs, app, save and audio, plus a few testing debug classes. It never loads the willow or ui adapters; import those from gamekit/src/willow and gamekit/src/ui when you want them.
When to skip gamekit
gamekit is for structure. For some jobs, willow or willowui directly is clearer:
- Custom rendering. Multi-pass effects or your own display containers: work with willow nodes and render targets directly.
- Desktop-style tools. Splitters, docking, trees and file dialogs: use willowui widgets directly rather than through scene windows.
- One-screen prototypes. A single screen with a tick loop needs no
SceneCoordinatoror save slots.
Costs to know about
| Situation | What happens | What to do |
|---|---|---|
Keeping an AssetHandle after its scope is disposed | handle.get() throws StaleHandle. | Tie handles to the scene that owns the scope. Do not keep them in globals. |
| Ticks take longer than the frame allows | The runtime runs at most App.maxCatchUpTicks ticks per frame and counts the rest in App.droppedTicks. | Watch App.droppedTicks; make the slow systems cheaper. |
| Input during a scene change | Input and the scene's clocks are held until the new scene is ready. | Nothing; SceneCoordinator does this for registered input and clocks. |
| An asset plan with a cycle | AssetLoadPlan.validate() throws CyclicDependency before any loading starts. | Make dependencies a tree; load shared files in an earlier wave. |
| Changing the world inside a phase | Direct structural changes throw while a phase runs. | Use the defer... methods; they apply at the next flush. |