gram is a modular game engine for Go. A game is a set of named Stages, each with its own entity-component world and its own Scenes; an engine drives the active Stage on the goke ECS, a tick and a picture a frame, and draws it on the GPU through WebGPU (gogpu, pure Go). Everything beyond the tick loop is a plugin: the built-in ones give a Stage a world of moving boxes, collisions, sight, a board with terrain, pathfinding and mouse selection, and a game adds its own the same way.
Features • Installation • Example • Demos • Model • Architecture • Benchmarks • Documentation
- One extension point. Everything beyond the tick loop is a
plugin.Plugin, built-in or yours. There is no registry and no lookup by name: a plugin that needs another takes it as a constructor argument. - A Stage owns its ECS. Each Stage gets a fresh world the moment it is entered, so a menu Stage sits idle with no gameplay entities until the player starts.
- Behaviour is rules. What a game does about what a plugin finds is a rule, a sentence of who, when and what, run inside that plugin's own pass — written in ceremony, the library gram builds on. A rule is a role's, a role is played, and the engine hands it to the plugin catching its moment; one no plugin catches is an error.
- Kinds say what an entity is. A kind is the list of components its entities carry, each constant or read from the entity's own row; it also tells save files what to expect.
- Saves survive change. Persisted resources are matched by name, never by position.
- Contract in the open, orchestration inside. What a game implements (
game,plugin,render) is public and documented; only the engine that drives it isinternal.
go get github.com/kjkrol/gramPrerequisites: Go 1.27+ and a GPU with a Vulkan, Metal or DirectX 12 driver; gogpu needs no cgo. Without a GPU the tests that draw skip themselves.
| Capability | Package | What you get |
|---|---|---|
| Stages and Scenes | game, game/stage |
Named Stages with their own ECS, defined a section at a time; Scenes as ui screens showing the world through cameras; a live Composition of what is shown and which Scene takes input |
| Plugins | plugin |
The one extension contract; Installer.Hosts, where a plugin hands the engine the hosts that run the rules of its moments inside its own pass |
| Behaviour | ceremony |
The library gram builds on, through its binding over goke (ceremony/goke): act.Branch[P]{Name, When, Then} a rule of a moment, plan.Loop[P]{Name, When, Then, Until, Timeout, Cooldown} a loop of a plan, every clause of either saying whom it is about (cond.Self.If(x), act.Other.Do(cmd)); roles a kind, a cell or a plugin plays; effects that hold for a while and turn the plugins' knobs; messages between plans; commands written as sentences, for entities found by name or group |
| World | plugins/world |
Every entity's Base; the spatial index under stop, wrap or open edges; kinds and spawning before the game and during it; heights or planes |
| Motion | plugins/motion, plugins/motion/steering |
Every entity moved each step, Moving and Leaving for rules; a Steering profile turned into heading and speed; Driven for a unit steered by hand |
| Collisions | plugins/collision |
Collider to take part, Physics to bounce and be pushed apart, the solid ground of a board, Meeting and Struck for rules |
| Gravity | plugins/gravity |
Bodies falling, standing on what bears them and leaping, along Z or across the plane, as the pull goes; Leap, Away and Landing for rules |
| Shooting | plugins/bullet |
Shots as entities fired from a unit's muzzle or thrown in an arc, swept by collision; Landing, Resting and Blast for rules |
| Sight | plugins/vision |
A Sight cone scanned into Sighted, cut by walls and dimmed by forests, over them by height; Sighting rules per observer |
| Conversations | plugins/dialog |
Nodes of lines and answers from YAML, every unit with lines of its own; what an answer changes is the game's own |
| Board and navigation | plugins/board, plugins/navigation |
Square or hex grid with terrain, ways and what stands on it, in a world with heights; a map laid out of a file; MoveOrder paths that re-route, onto a bridge's deck or under it |
| Bridges | plugins/bridges |
Bridges built by one set of rules whoever builds them: a level's file, the map maker, a player with a bridge in hand |
| Light | plugins/light |
The sun and the lamps as sources on entities, what each thing casts and takes; the shadows of the relief and of what stands still baked into the ground as the sun moves, the live ones laid over whatever ground is drawn |
| Topography | plugins/topography |
A map in relief on the GPU: heights shaped, lit and shaded, every step's slope for the board's mobility, water glinting and running; the cameras' cage |
| Atmosphere | plugins/atmosphere |
The calendar, the sky of the hour (the Earth's with the real stars, or a game's own of four parts), the climate and the weather, what falls and what it does to the board |
| Selection, players, cameras | plugins/selection, plugins/players, plugins/cameras |
Click and marquee into Selected; players over the command handlers, their bindings and a list of keys (K); cameras made where a scene shows them, on a stick turned and leaned from straight down to isometric and flatter, orthographic or in perspective, kept out of what a map makes solid, fastened over a unit, behind it or inside it |
| Minimap | plugins/minimap |
A small picture round a player's camera, turning with it or north up: the ground in miniature, a dot for each kind its figure marks, the camera's view outlined; a click on it takes the camera there |
| Telemetry | plugins/telemetry |
The panel in the corner saying how the game runs — the frame rate, the ticks, where a frame's time goes, the entities, the game time, the plugins' and the game's own lines — in every Stage with a world, shown and hidden with ~ |
| Persistence | game.Persistence |
Save, load and list the active Stage's ECS and every tracked value by name |
| Rendering | render, camera |
Everything drawn on the GPU (WebGPU, WGSL): sources drawn over one another by height in a shared depth buffer, the ground first and the marks last, Direct sources with shaders of their own, an atlas baked at Close |
The smallest game that does something: one Stage with a torus of bouncing boxes, a collision
plugin counting their contacts, and one Scene showing them through a camera from above, the
telemetry panel over it (~ shows everything, then hides it). This is
examples/start/minimal; run it with make demo-start-minimal.
package main
import (
"image/color"
"math/rand/v2"
"time"
"github.com/kjkrol/aabbworld"
"github.com/kjkrol/aabbworld/geom"
"github.com/kjkrol/ceremony/comp"
"github.com/kjkrol/goke/v3"
"github.com/kjkrol/gram"
"github.com/kjkrol/gram/camera"
"github.com/kjkrol/gram/entity/kind"
"github.com/kjkrol/gram/game"
"github.com/kjkrol/gram/game/stage"
"github.com/kjkrol/gram/plugins/cameras"
"github.com/kjkrol/gram/plugins/collision"
"github.com/kjkrol/gram/plugins/motion"
"github.com/kjkrol/gram/plugins/players"
"github.com/kjkrol/gram/plugins/telemetry"
"github.com/kjkrol/gram/plugins/world"
"github.com/kjkrol/gram/render"
"github.com/kjkrol/gram/ui"
)
const (
screenWidth, screenHeight = 640, 480
boxSize = 12
boxCount = 300
)
type boxRow struct {
pos world.Position
vel world.Velocity
}
// box is the one kind: a bouncing square
type box struct{}
var _ world.Figure = box{}
func (box) Define(w *world.Plugin) {
kind.Define[boxRow](w.Kinds(), BoxKind, kind.Spec{
comp.Load(func(b boxRow) world.Position { return b.pos }),
comp.Load(func(b boxRow) world.Velocity { return b.vel }),
comp.Const(collision.Collider{}),
comp.Const(collision.Physics{Restitution: 1}),
})
}
func (box) Look(a *world.Atlas, w *world.Plugin) {
a.Add(kind.Named[boxRow](w.Kinds(), BoxKind), boxSize, render.Solid(boxColor))
}
const (
BoxKind = "box"
ArenaStage = "arena"
ViewScene = "view"
)
type arena struct {
world *world.Plugin
motion *motion.Plugin
collision *collision.Plugin
cameras *cameras.Plugin
players *players.Plugin
telemetry *telemetry.Plugin
player *players.Player
stats collision.ContactStats
}
func main() {
props := game.Props{
Title: "gram minimal",
ScreenWidth: screenWidth,
ScreenHeight: screenHeight,
TargetTPS: 60,
}
a := &arena{}
mainStage := stage.New(ArenaStage).
World(world.Config{
Space: world.SpaceCfg{
Width: screenWidth,
Height: screenHeight,
Edges: aabbworld.Torus,
},
Entities: world.EntitiesCfg{MaxCount: boxCount, MinSize: boxSize, MaxSize: boxSize},
}).
Plugins(a.makePlugins).
Figures(a.defineFigures).
Players(a.definePlayer).
Spawn(a.spawnUnits).
Scenes(a.defineScenes).
Shows(ViewScene, telemetry.SceneName).
Update(a.update)
gram.Run(game.New(props, mainStage))
}
func (a *arena) makePlugins(w *world.Plugin) {
a.world = w
a.motion = motion.NewPlugin(a.world)
a.collision = collision.NewPlugin(a.world, a.motion).WithStats(&a.stats)
a.cameras = cameras.NewPlugin(a.world, cameras.Config{})
a.telemetry = telemetry.NewPlugin(a.world)
a.players = players.NewPlugin(a.world, a.collision, a.cameras)
}
func (a *arena) definePlayer() error {
a.player = a.players.Local("player")
return a.player.Bind(a.players.Defaults()...)
}
func (a *arena) defineFigures() { a.world.Figures(box{}) }
func (a *arena) spawnUnits() {
boxKind := kind.Named[boxRow](a.world.Kinds(), BoxKind)
rng := rand.New(rand.NewPCG(1, 2))
placement := world.NewGridPlacement(screenWidth, screenHeight, boxSize)
entries := make([]kind.Entry, boxCount)
for i := range entries {
var vel world.Velocity
vel.SetDelta(geom.NewVec(rng.Float32()*200-100, rng.Float32()*200-100))
entries[i] = boxKind.Entry(boxRow{pos: placement.Place(i, boxCount), vel: vel})
}
a.world.Write().Seed(entries...)
}
func (a *arena) defineScenes(ctx game.Initializer) []game.Scene {
m := &mainScene{arena: a}
a.telemetry.With(a.stats.Reporter(&ctx.TPS().Ticks))
return []game.Scene{ui.NewScene(ViewScene, m.screen()).Input(a.players.Handle)}
}
func (a *arena) update(ctx goke.RunCtx, d time.Duration) {
a.world.RunPlan(ctx, d)
a.motion.RunPlan(ctx, d)
a.collision.RunPlan(ctx, d)
a.cameras.RunPlan(ctx, d)
a.players.RunPlan(ctx, d)
ctx.Sync()
}
type mainScene struct {
arena *arena
}
var (
boxColor = color.RGBA{R: 90, G: 200, B: 110, A: 255}
backgroundColor = color.RGBA{R: 30, G: 30, B: 30, A: 255}
)
func (m *mainScene) screen() *ui.Element {
a := m.arena
view := render.NewFeed(a.cameras.New(cameras.FromAbove, camera.Config{}), a.world.Picture())
return ui.Layers(
ui.Blank().Fill(backgroundColor),
ui.Image(view).Input(a.players.Through(a.player)),
)
}examples/motion/collision is the same idea at scale: thousands of
colliding boxes at a fixed 120 TPS, saved on F5 and loaded on F9.
gokebiten-2306-50.mp4Stats: 2306 colliding AABBs | 120 TPS | 50 collisions/tick |
gokebiten-524-15.mp4Stats: 524 colliding AABBs | 120 TPS | 15 collisions/tick |
|---|
The demos are grouped by what they show, each group from the simplest to the richest. Every demo opens a window; what it shows is tested in the library's package that does it.
| Demo | Shows | Run |
|---|---|---|
start/minimal |
One Stage, one Scene, a world with collisions: the README example | make demo-start-minimal |
start/scenes |
A menu Stage switching into a gameplay Stage; a modal panel over the ticking world, open or closed as the game was saved | make demo-start-scenes |
start/split-screen |
Two players at one keyboard, each driving its own block through a camera of its own in its half of the screen; a minimap over them | make demo-start-split-screen |
| Demo | Shows | Run |
|---|---|---|
look/animation |
A gallery of animations drawn frame after frame from sheets in game time (Shift+P freezes them), each figure labelled: a walker on a patrol stepping the way it goes in eight directions and braking, an idler bored by stages (blinking, stamping, looking about), a puller's hands on a lever, a lever swinging down and up | make demo-look-animation |
look/material |
Materials of the game's own WGSL in place of sprites: a ward pulsing on the clock and four embers circling it, drawn out of their own state, breathing standing and streaming a tail driven (WSAD); G calms the ward and F douses the embers into plain sprites for a while | make demo-look-material |
look/sky |
A sky of the game's own over a valley in relief, a day in a minute and a half: its own WGSL vault, turquoise by day and violet at night with a band shimmering along the stars' equator, the Earth's moon three times as big and orange, showing by day too, made-up stars; Tab turns to the perspective, Q and E turn it, F lays the stick down to the sky | make demo-look-sky |
look/light |
A flat board under a sun of the game's own, circling in a day and a half a minute: a tower's shadow baked into the ground and swinging with the sun, walkers casting soft patches, a hero (WSAD) casting the silhouette of its sprite, a lantern lighting the night and casting their shadows by it (switched on the settings screen, F10), lava that takes no shade | make demo-look-light |
| Demo | Shows | Run |
|---|---|---|
rules/effect |
An ice witch turns the ground round her into snow and the lake into ice; it thaws behind her, a walker slips on it, a boat is frozen in, each kind in its own frozen look; F freezes whoever the cursor points at, J hastens the selected for two seconds | make demo-rules-effect |
rules/mechanisms |
Three commands, each a sentence saying what it does, whom it is for and who sets it off: a hero walked on WSAD; a lever he walks up to, pulled and pushed back at hand (Space), its colour its position; a plate stood on, a gate flipped (G); zoom by the wheel, = and - | make demo-rules-mechanisms |
rules/dialog |
Conversations read from YAML: the traveller greeted by the miller and the smith in a window, the answers changing the host's Regard, shown under it as Friend, Neutral or Enemy |
make demo-rules-dialog |
rules/vision |
Entities keeping out of each other's way by sight, and a hunter living off the ones that fail | make demo-rules-vision |
rules/guard |
Guards on a plan (plan.New): patrol the yard, step aside for whoever blocks them long enough, rest where they arrive, run to the gate while the alarm (A) is on |
make demo-rules-guard |
| Demo | Shows | Run |
|---|---|---|
board/atlas |
A small flat board drawn from the game's own atlas, the water a material shimmering per pixel, a road laid as a way | make demo-board-atlas |
board/custom-map |
A board drawn by a map of the game's own (board.Plugin.WithMap) and walked by a mobility of its own (cell.Mobility): hills rising out of the grass, shaded by how high they lie; the walkers go round the steep and slow down climbing |
make demo-board-custom-map |
board/flat |
Madeira on the simple map out of cmd/gramap: the island in its colours, rivers and roads as bands, bridges and fords standing on it, a day and the weather going by, a minimap north up |
make demo-board-flat |
board/topography |
Madeira in relief, its true heights, a cell 333 m: lit by the sun, water running, roads laid as Transport Tycoon lays them, bridges on piers (Shift+B builds one), the ground shaped under the cursor; the camera on a stick turned (Q, E) and leaned (R, F) from straight down to isometric and flatter, orthographic or in perspective (Tab); a minimap turning with it, a click on it taking the camera there; the ground textured from photographs (make demo-board-topography) or painted, the light in bands (make demo-board-topography-ghibli); the hour, the date and the weather in a bar or apart by the corners (make demo-board-topography-widgets) |
make demo-board-topography |
| Demo | Shows | Run |
|---|---|---|
navigation/square |
Units sent by a right click round a wall and over a river by a bridge, a wagon too wide for it going round by the ford; sight cones that stop at walls and fade in forests, and a hawk that flies over both | make demo-navigation-square |
navigation/hex |
The same on a hex board | make demo-navigation-hex |
navigation/spawn |
Walkers spawned under the cursor (N) hold their cells at once, the cells held outlined | make demo-navigation-spawn |
| Demo | Shows | Run |
|---|---|---|
motion/collision |
Thousands of bouncing boxes of many kinds, hit overlays, telemetry, save and load | make demo-motion-collision |
motion/expanse |
A hundred thousand boxes on a torus of about 98 km: the world lives in bands round the camera, the settings screen (F10) shows the sectors by their bands or by what the camera sees and switches the bands off to weigh what they spare; a minimap of the boxes round the camera | make demo-motion-expanse |
motion/bullet |
A soldier on WSAD, the camera following him: Space fires a round the way he faces, Shift held and let go throws a grenade the further the longer held, in an arc that lies with a spark and bursts; the shots are the plugin's, the wounds and the fuse rules, effects and a plan | make demo-motion-bullet |
motion/platformer |
A hero in a side view under gravity: A and D walk, W jumps, G turns gravity over, a hard landing dazes | make demo-motion-platformer |
A game.Game supplies Props (window, tick rate) and its Stages by name; game.New(props, stages...) is one. A Stage is one self-contained context, a menu or the gameplay, with its own
goke ECS built fresh the moment Runtime.SwitchStage enters it: Init installs its plugins,
Spawn seeds a fresh game, Loaded stands in for Spawn when the Stage is entered from a save,
Update is one tick. A game defines a Stage a section at a time with
game/stage, always in the same order; a chain out of order does not
compile, a section the game has no use for is left out, and what is defined in the wrong
section is refused:
stage.New(MeadowStage).
World(worldCfg). // the world…
Plugins(s.makePlugins). // …and the plugins the arena makes over it
Players(s.definePlayer). // the players, the plugins' default keys
Effects(s.defineEffects). // the states
Rules(s.defineRules). // the roles and their rules
Commands(s.defineCommands). // what can be asked for
With(dialog.Conversations(s.defineConversations)). // a plugin's own section, in its place
Kinds(s.defineCells, s.defineKinds). // the kinds of cells and of units
Controls(s.bindKeys). // the game's own keys, each a command
Spawn(s.spawn). // a fresh game, out of its map
Scenes(s.defineScenes). // the scenes and their cameras
Update(s.update) // the tick; hands back the game.StageSaving and loading are the engine's: commands like any other (game.Save, game.Load,
game.Enter, game.Quit) bound to keys (F5, F9, Shift+Esc in every game, as the controls
screen (K) lists them and F10 opens the settings) or to ui buttons, a
Stage entered from its save from any other, a menu's Continue.
A Scene is one thing a Stage can show. Its screen is a ui tree: the world's picture, all its
plugins draw, through a camera made beside it, render.NewFeed(s.cameras.New(cameras.FromAbove, camera.Config{}), s.world.Picture()), wired to the player who acts through it,
ui.Image(feed).Input(s.players.Through(player)). The Stage's Scenes registry is static; the
Composition over it is live: which Scenes are visible, in what order, and which is active, the
topmost focusable one and the only one whose HandleEvents runs. Runtime (pause, quit, switch
Stage, persistence, full screen) reaches a Stage and every Scene alike.
A plugin's Install only queues ECS wiring, flushed in one ecs.Setup after the Stage's
Init. What a game does about what a plugin finds is a rule of ceremony, the
library of behaviour gram builds on, run in the pass of the plugin that catches its moment:
roles := s.world.Roles()
roles.Define(PreyRole)
roles.Define(PredatorRole, act.Branch[collision.Meeting]{
Name: "caught",
When: cond.Other.If(roles.Named(PreyRole)),
Then: act.Other.Do(world.Despawn{}),
})
kind.Define[body](kinds, HunterKind, kind.Spec{…, ceremony.Plays(roles.Named(PredatorRole))})reads as a sentence, who, when, what: a predator, meeting one playing prey, has it give itself a
Despawn. Every clause says whom it is about: cond.Self the entity, cond.Other the other of
the pair or the moment's subject, cond.At(board.Around(n)) the places round, cond.On(s.world)
the world's own entity; and in what state, .If(x): under an effect, playing a role, as a word of
the moment says (unit.Fallen(), vision.SeesNobody()), cond.Chance(p), cond.Always, or
cond.Not of one; clauses join with And and Or. The steps say the same, act.Self.Put(e),
act.Other.Do(cmd), act.At(board.Around(1)).Put(e), in order (act.Steps) or the first that does
(act.OneOf). Nobody hands the rule to collision: once Init returns, the engine gives the rules
of every role somebody plays to the plugin that catches their moment, and a rule none catches is
an error. What an entity does over time is a plan of loops, plan.New(name, root), carried
by its kind: a plan.Loop is a branch with an end — Until the fact a plugin tells it
(cond.Self.If(navigation.Arrived{})), Timeout, Cooldown — choosing what to do
(plan.OneOf) or doing one thing after another (plan.Steps). Rules and plans cast effects
that hold for a while and give commands for their entity (plan.Self.Do(navigation.MoveTo{…})),
the same a player gives. An effect turns the knobs a plugin gives, components it only reads, like
steering.Steering or a cell's cell.Ground. A rule holds no Go code but its words; how
entities are drawn is the one place rules are Go (world.Rule), declared on the world's atlas
in a scene, where a kind's look under an effect is declared too (Slot.Under(effect, look)).
A plugin ships no ready-made reactions: it gives moments and their words, and the game says
what follows. The language is ceremony's doc/ceremony.md;
how a game uses it over gram's plugins is doc/behaviour.md.
What a player wants is a command too, a command.Command (ceremony's): the plugin that
defines its type
(navigation.MoveTo, selection.Select) is a plugin.CommandHandler that drains its queue in
its own pass; the players plugin carries what a player's bindings, an AI or a network issue,
the world what the entities give themselves.
A role is a behaviour an entity plays, mortal, hasty, a plate, not a group:
s.world.Roles().Define(name, rules...) fires the rules for those playing it alone. Defining
registers and hands nothing back: effects, roles, commands and kinds are defined under a name
in the Stage's world and taken back with Named(name) wherever they are built on; a game keeps
its names as constants, each with a suffix saying what it names (Ef, Cmd, Role, Plan,
Kind, Cell). A kind plays roles through ceremony.Plays(roles...), a cell through its kind
or alone, a plugin through its own Plays: a plugin is an entity of the world too
(world.Self), so a state of the sky is an effect on the atmosphere,
ceremony.Put(bloodMoon).On(s.atmosphere), read back by cond.On(s.atmosphere).If(bloodMoon).
A command about an effect is a sentence: put it on (ceremony.Put), take it off (Take) or
switch it (Toggle), for the entities bearing a
name, those in a group, the world itself, the player's selected units or the one pointed at
(On), set off by the entity named (By):
const (
OpenEf = "open"
HasteEf = "haste"
MortalRole = "mortal"
HastyRole = "hasty"
PlateRole = "plate"
OpenWestCmd = "open west"
OpenEastCmd = "open east"
HastenCmd = "hasten"
ScoutKind = "scout"
TrapdoorKind = "trapdoor"
PlateKind = "plate"
)
fx, roles, cmds := s.world.Effects(), s.world.Roles(), s.world.Commands()
// Effects
fx.Define(OpenEf, effect.Spec{effect.Lasts(2 * time.Second),
effect.Alter(func(f *cell.Floor) { f.Allows = 0 })})
fx.Define(HasteEf, effect.Spec{effect.Lasts(3 * time.Second),
effect.Alter(func(st *steering.Steering) { st.MaxSpeed *= 2 })})
// Rules
roles.Define(MortalRole, act.Branch[unit.Standing]{
Name: "fall in",
When: cond.Self.If(unit.Fallen()),
Then: act.Self.Do(world.Despawn{}),
})
roles.Define(HastyRole)
roles.Define(PlateRole, act.Branch[collision.Meeting]{
Name: "press",
When: cond.Self.If(cond.Always),
Then: act.Self.Do(ceremony.Trigger{}),
})
// Commands
cmds.Define(OpenWestCmd, ceremony.Put(fx.Named(OpenEf)).On(ceremony.Group("west trapdoors")))
cmds.Define(OpenEastCmd, ceremony.Put(fx.Named(OpenEf)).On(ceremony.Group("east trapdoors")).
By(ceremony.Named("plate")))
cmds.Define(HastenCmd, ceremony.Put(fx.Named(HasteEf)).
On(s.selection.Selected(roles.Named(HastyRole))))
// Kinds
units.Define(ScoutKind, land, profile,
ceremony.Plays(roles.Named(MortalRole), roles.Named(HastyRole)))
standing.Define(TrapdoorKind, comp.Const(cell.Floor{Allows: cell.Land}))
standing.Define(PlateKind, comp.Const(collision.Collider{}),
ceremony.Plays(roles.Named(PlateRole)))
// Controls
s.player.Bind(
control.Give(control.KeyPress{Key: control.Key1}, "Pull the west lever",
cmds.Named(OpenWestCmd)),
control.Give(control.KeyPress{Key: control.KeyJ}, "Hasten the selected scouts",
cmds.Named(HastenCmd)))
// Spawn: the map; a row's yaml tags are the file's keys
type unitRow struct {
Start cell.ID `yaml:"start"`
}
level, _ := s.board.Lay(mapFile, "map.yaml")
board.Spawn[standingRow](level, nil)
board.Spawn[unitRow](level, board.Sides{
PlayerSide: {players.Give{To: s.player.ID}, selection.Allow{}}})board:
entities:
- {kind: trapdoor, group: west trapdoors, at: [7, 2]}
- {kind: plate, name: plate, at: [20, 14]}
units:
- {kind: scout, side: player, start: [3, 14]}1 opens every trapdoor in the group "west trapdoors" for two seconds under whoever stands there;
the plate, met, sets off the command that names it; J hastens the selected scouts. An AI gives
them as a key does (players.Issue).
examples/rules/mechanisms is the whole program.
kind.Define[Row](world.Kinds(), "name", kind.Spec{...}) says what an entity is: each component
comp.Const(v) (the same for all) or comp.Load(func(row Row) T) (read from that entity's row).
A unit over a board is defined through board.NewUnits[Row](brd, shape, at), which derives its
position and cell from the one point at reads off a row and runs the world's roster, what the
plugins in the game bring by default and what they require. What stands still on the board, a
lever, a trapdoor, a gate, is a kind of board.NewEntities. Spawn puts entries on the world's
roster, or lays the game's map out of a file (board.Plugin.Lay: the board a character a cell,
the relief in metres, roads and rivers along paths, the entities and the units, each a kind and
the fields of its row tagged yaml); a fresh Stage spawns them, a loaded one does not. Mid-game
the command world.Spawn{Entry} adds an entity the same way. Kinds tell save files every
component type their entities carry, so a game's own state survives a save without being
registered anywhere else.
cmd/gramap makes such a map of any stretch of the Earth at any scale out of a recipe
(cmd/gramap/madeira.yaml is Madeira's):
go run ./cmd/gramap -recipe cmd/gramap/madeira.yaml -out examples/board/topography/map.yaml
go run ./cmd/gramap -recipe cmd/gramap/madeira.yaml -out examples/board/flat/map.yaml -flatThe recipe says where, how many cells and how many metres a cell spans, what the ground is by
its slope and height, how much drained land makes a brook, a stream and a river, and the roads
as chains of named places. The heights come from the
Terrain Tiles on AWS, fetched once into the
user's cache. The rivers are carved into the ground, the roads routed and graded as Transport
Tycoon lays them, with bridges over water and down valleys and fords where a river is shallow;
with -flat the map has no heights.
The packages form a strict acyclic graph; each imports only the layers below it. Every package
has a doc.go describing what it brings. What is left to do is in
doc/roadmap.md.
| Package | Responsibility |
|---|---|
camera |
The contract of a view onto a world: screen conversion, culling, move and zoom, projections, fastening over an entity |
control |
The input vocabulary: Binding, the triggers, Context, Keys, a player's Tool; the commands a binding builds are ceremony/command's |
render |
Drawing: Renderer, the Composer over Sources and its Frame, Direct sources, Feed, Atlas, Font, the drawing rules a renderer runs every frame |
plugin |
The extension contract: Plugin, Installer (with Hosts, the hosts of a plugin's moments), CommandHandler, Serializable, PostLoader, Populator, Made; plugin/section the parts of a Stage |
entity |
What every entity carries: Base, Position, Velocity, Z, Layers, Eye, Lie; entity/kind what an entity is (Spec, Define, Of, the roster) |
clock |
The tactical clock: game time as the sum of the simulation's steps, the pause (Shift+P), the tempo (] and [), the Moment of a step with At and Every |
ceremony |
The library gram builds on, outside the module: how entities behave — cond the conditions, act the steps and the branch of a rule, plan the loops of a plan, effect the states and knobs, fact the facts and messages, roles, commands about effects and labels in its root, host the hosts a plugin runs rules with, command the queues and the carrier, tag and comp; the language in its doc/ceremony.md, how a game uses it over gram's plugins in doc/behaviour.md |
plugins/world |
The foundation: the space under the edge rules, kinds and tags, Seed/Populate, Spawn, Despawn, the commands about effects, the carrier of commands, the entities' looks on the atlas; it runs the core's systems; view beside it |
plugins/motion |
The entities moved: Moving with Still and Braking, Leaving and Outside past an open edge, Drift, UnitTemplate; steering the heading and speed reached gradually |
game |
What a game implements and receives: Game, Stage, Scene, Scenes, Composition, Initializer, Runtime, Persistence; game/stage a Stage defined a section at a time |
ui |
A scene's screen as a tree of elements: Layers, Columns/Rows, anchors, Panel, Label, Image of a feed, Window, Button giving commands, elements pinned to entities Under an effect or On a name, a Theme |
plugins/collision |
Collision over the world's space: Collider, Physics, Sweep, the solid ground of a Field, Meeting and Struck |
plugins/vision |
Sight cones into Sighted, Sighting rules, the views drawn |
plugins/board |
A square or hex grid with terrain over the world: the Board, Layout and Map, NewUnits, NewEntities, a level out of a file (Lay); cell the vocabulary of a cell, unit of a unit on it, grid the topology, look how it is drawn, ground what its ground is to the others, water and network rivers and roads |
plugins/atmosphere |
The sky over a world on the world's clock: calendar, sky, celestial, climate and its weather, air, precipitation, weathering, backdrop |
plugins/topography |
A map in relief drawn on the GPU: the heights, the light and the shadows, the water, the commands shaping the ground, the cameras' cage; relief and painter its vocabulary |
plugins/navigation |
MoveOrder paths across a board, re-routing as terrain changes, on a bridge's deck or under it; MoveTo a cell or an entity by name; a patrol as a plan over the unit's Round (Onward); the crowd as rules over Touch; routes drawn |
plugins/driving |
Units driven by hand: Ahead, Back, Turn, Toward, Jump, Tank and Compass keys |
plugins/gravity |
Bodies falling, standing and leaping: Pull, Body, Leap, Away, Landing |
plugins/bridges |
Bridges: Types of them, a level's, a player's with a Tool in hand; where one may stand in bridges/site |
plugins/bullet |
Shots fired and flown: Shoot, Shots, Flight, Landing, Resting, Burst into Blasts |
plugins/selection |
Select into Selected, Hovered, Selected(roles...) and Pointed() as the targets of commands |
plugins/dialog |
Conversations as data: Nodes from YAML, a unit's Script, the game's Changes and Conditions, Window |
plugins/cameras |
The cameras a game looks through: Config, New, the Cage, Pan, Zoom, Follow, CenterOn, the views' View, Turn, Tilt, Ride, Keys |
plugins/players |
Players over the command handlers: bindings into commands, Through and IssueAs for a ui scene, the list of keys; players/owner whose a unit is |
plugins/telemetry |
The telemetry panel: standard lines, the plugins' reporters and the game's own lines, a scene of its own in every Stage with a world, ~ |
internal/engine |
The Engine: the window's loop, one active Stage, saves and loads |
gram |
Run; the package you import. The root doc.go carries the concepts and the full package graph |
camera ───► render ──► plugin ──► plugins/world ──► game ──► internal/engine ──► gram
control ────┘ ▲ │ ▲
ceremony ──────┘ ▼ │
plugins/{cameras, collision, selection, vision} ──► plugins/board ──► plugins/driving ──► plugins/navigation
└──► plugins/{bullet, gravity, bridges, topography, atmosphere} ──► plugins/players
Outside the module: ceremony is the behaviour — rules, plans, effects, roles, commands, facts — which gram's plugins perceive for and carry out, with goke as its store; goke is the ECS every Stage runs on, aabbworld the space, collisions and line of sight under the world, gogpu the window, the loop and the GPU (WebGPU), hpastar the pathfinding.
One tick of the built-in plugins on an Intel i5-8265U (see BENCHMARKS.md), 0 allocs/op throughout once warm, the median of 5 runs at v0.6.0:
| Tick | Scene | Cost |
|---|---|---|
| World: move every entity, the space reading them in place | 5,000 boxes, 20×20 each, on a 4000×4000 torus | 36 µs |
| World + collisions | 524 boxes, 20×20 each, covering 20% of a 1024×1024 torus | 124 µs |
| World + collisions | 8,388 boxes, 5×5 each, covering 20% of the same torus | 2.7 ms |
| Vision: every observer scans its cone | 500 observers, 60° cones of radius 200 | 232 µs |
| Navigation: walkers under orders, in the open / through one gap | 1,000 walkers on a 64×64 board | 3.0 / 10.5 ms |
Deep dive: every scene, what each benchmark measures, and how to reproduce, in BENCHMARKS.md.
make benchgram began as goke's Ebitengine example and was extracted so the ECS stays free of GUI
dependencies; it has since left Ebitengine for WebGPU. The world, collisions and line of sight
are aabbworld's; gram is where they meet an ECS and a screen.
- API reference on pkg.go.dev.
- Concepts and package graph in the root
doc.go; each package's owndoc.goexplains what it brings (see Architecture). - The language of behaviour in ceremony's README and
doc/ceremony.md;
doc/behaviour.mdis how a game uses it over gram's plugins. - Benchmarks in BENCHMARKS.md; changes in CHANGELOG.md.
- The night sky's stars: the Yale Bright Star Catalogue, 5th revised edition (Hoffleit &
Warren 1991), from the CDS, catalogue V/50 (
plugins/atmosphere/celestial/stars_gen.go). - The moon's face: NASA's Scientific Visualization Studio, CGI Moon Kit (Lunar Reconnaissance
Orbiter LROC data) (
plugins/atmosphere/celestial/moon.png). - The animation demo's lever: "Lever and Button" on OpenGameArt (CC0), its two positions, with
two frames between drawn after them (
examples/look/animation/lever.png). Its figure (hero.png) is drawn in-house. - The heights of Madeira's maps and of every map
cmd/gramapmakes: the Terrain Tiles on AWS, Mapzen's Tilezen terrarium — SRTM, EU-DEM (Copernicus), GMTED2010, ETOPO1 and others; see their attribution.
