Skip to content

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 World owns the entities it creates or adopts. world.disposeEntity(e) ends one; world.dispose() ends them all.
  • A SceneStack alone starts, covers, uncovers and ends scenes. A scene belongs to one stack at a time.
  • An AssetScope owns the handles it loaded. Disposing the scope releases them.
  • A WillowPresentationAdapter owns 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 dt and the tick number from PhaseSchedule.run.
  • Gauge.advance(dt) fills a gauge. StateMachine.update(delta) updates a state.
  • Timed effects and StateMachine.transitionAfter need a scheduler you supply: in a game a core Ticker (App.ticker or a node's ticker), in a test a ClockScheduler.

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:

FamilyMay import these gamekit familiesExternal systems
corenonewillow, willowui
ecscorewillow, willowui
savenonefs (for FsByteStorage), willow, willowui
appnonewillow, willowui
audiononewillow, willowui
willowcore, ecs, appwillow, willowui
uicore, appwillow, willowui
testingcore, ecs, save, appfs, 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 SceneCoordinator or save slots.

Costs to know about ​

SituationWhat happensWhat to do
Keeping an AssetHandle after its scope is disposedhandle.get() throws StaleHandle.Tie handles to the scene that owns the scope. Do not keep them in globals.
Ticks take longer than the frame allowsThe 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 changeInput 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 cycleAssetLoadPlan.validate() throws CyclicDependency before any loading starts.Make dependencies a tree; load shared files in an earlier wave.
Changing the world inside a phaseDirect structural changes throw while a phase runs.Use the defer... methods; they apply at the next flush.