Package github.com/matjam/bunyip/ecs
ecs
Package ecs is the engine's entity component system. Entities are cheap handles, components are plain Go structs stored in dense per-type columns, and queries iterate every entity carrying a set of components with no lookups in the loop.
Storage is by archetype. Every distinct set of component types gets a table with one column per type, and an entity lives in exactly one table. Adding or removing a component moves the entity to another table, so structural changes cost a copy, while iterating a hundred thousand entities reads a few slices in order.
w := ecs.NewWorld()
e := w.SpawnWith(Position{1, 2}, Velocity{0.5, 0})
q := w.Query2[Position, Velocity]()
q.Each(func(e ecs.Entity, p *Position, v *Velocity) {
p.X += v.X
})
Systems are functions registered on the world and run in order by Update. Resources are singletons such as the score or the rules. Events are per-Update queues that carry values between systems; Update clears them before running the first system.
Query lifetime and mutation
Query callbacks receive pointers into component storage. Use them before a structural change touches their table; do not retain them across frames. Rows are visited last to first and matched table lengths are captured at the start of a walk. The currently visited entity may be restructured or despawned, but changes to other entities must be deferred with World.Defer or an explicit Commands buffer. Despawning a parent also removes its children, so defer that operation. Do not nest a walk of the same query within its callback. The world's Each methods share a cached query per world and ordered component set, so nesting the same helper and component set has the same restriction. Worlds and their queries require externally serialized access.
Persistence and scenes
Worlds save and load as JSON. Register names each component and resource type so files stay valid across builds. Save writes every live entity, its parent links and the registered resources, and Load recreates them with fresh handles, rewriting the Entity fields inside components. A Prefab is a template of components (and child prefabs) that spawns independent copies. Clone and CloneTree copy an entity that already exists.
A Scene is a JSON document of entities to spawn as a unit: a level, a room, a squad. Instantiate spawns a copy and returns a SceneInstance that finds its entities by name and despawns them again, so several copies live side by side; ExportScene captures live entities back into a document. A scene entity may reference a prefab from a PrefabLibrary and override its components.
Index
- Variables
func NameOf(w *World, e Entity) (string, bool)func Register[T any](name string)func SetParent(w *World, child, parent Entity)func UpdateWorldMatrices(w *World)func WorldMatrix(w *World, e Entity) lin.Mat4- type Children
- type Commands
- type ComponentID
- type Entity
func ChildrenOf(w *World, e Entity) []Entityfunc Clone(w *World, e Entity) Entityfunc CloneTree(w *World, e Entity) Entityfunc ParentOf(w *World, e Entity) (Entity, bool)func SceneRef(n int) Entityfunc (e Entity) ID() uint64func (e Entity) MarshalJSON() ([]byte, error)func (e Entity) MarshalText() ([]byte, error)func (e Entity) String() stringfunc (e *Entity) UnmarshalJSON(b []byte) errorfunc (e *Entity) UnmarshalText(b []byte) errorfunc (e Entity) Valid() bool
- type Filter
- type InstantiateOptions
- type LoadOptions
- type MissingPrefabError
- type Name
- type Parent
- type Prefab
func NewPrefab(comps ...any) *Prefabfunc ParsePrefab(data []byte) (*Prefab, error)func PrefabOf(w *World, e Entity) *Prefabfunc (p *Prefab) Child(children ...*Prefab) *Prefabfunc (p *Prefab) Children() []*Prefabfunc (p *Prefab) Components() []anyfunc (p Prefab) MarshalJSON() ([]byte, error)func (p *Prefab) Spawn(w *World) Entityfunc (p *Prefab) UnmarshalJSON(data []byte) error
- type PrefabLibrary
- type Query1
- type Query2
- type Query3
- type Query4
- type Remapper
- type SaveOptions
- type Scene
func NewScene(name string) *Scenefunc ParseScene(data []byte) (*Scene, error)func (s *Scene) AddEntity(name string, comps ...any) (int, error)func (s *Scene) AddPrefab(name, prefab string, overrides ...any) (int, error)func (s *Scene) Encode() ([]byte, error)func (s *Scene) SetParent(child, parent int)func (s *Scene) SetProperty(key string, v any)
- type SceneEntity
- type SceneInstance
- type System
- type SystemStat
- type UnregisteredError
- type World
func NewWorld() *Worldfunc (w *World) Add[T any](e Entity, v T)func (w *World) AddSystem(name string, fn System)func (w *World) Alive(e Entity) boolfunc (w *World) ComponentValues(e Entity) []anyfunc (w *World) Components(e Entity) []reflect.Typefunc (w *World) Count[T any]() intfunc (w *World) Defer(fn func(*Commands))func (w *World) Despawn(e Entity)func (w *World) Each[T any](fn func(e Entity, t *T))func (w *World) Each2[A, B any](fn func(e Entity, a *A, b *B))func (w *World) Each3[A, B, C any](fn func(e Entity, a *A, b *B, c *C))func (w *World) Each4[A, B, C, D any](fn func(e Entity, a *A, b *B, c *C, d *D))func (w *World) Emit[T any](ev T)func (w *World) Entities() []Entityfunc (w *World) Events[T any]() []Tfunc (w *World) ExportScene(roots ...Entity) (*Scene, error)func (w *World) Get[T any](e Entity) (*T, bool)func (w *World) Has[T any](e Entity) boolfunc (w *World) Instantiate(scene *Scene, opts ...InstantiateOptions) (*SceneInstance, error)func (w *World) Len() intfunc (w *World) Load(in io.Reader, opts ...LoadOptions) errorfunc (w *World) MustResource[T any]() *Tfunc (w *World) Query1[A any](filters ...Filter) *Query1[A]func (w *World) Query2[A, B any](filters ...Filter) *Query2[A, B]func (w *World) Query3[A, B, C any](filters ...Filter) *Query3[A, B, C]func (w *World) Query4[A, B, C, D any](filters ...Filter) *Query4[A, B, C, D]func (w *World) Remove[T any](e Entity)func (w *World) Resource[T any]() *Tfunc (w *World) Resources() []stringfunc (w *World) Save(out io.Writer, opts ...SaveOptions) errorfunc (w *World) SetComponent(e Entity, v any)func (w *World) SetResource[T any](v T)func (w *World) SetSystemEnabled(name string, on bool)func (w *World) Spawn() Entityfunc (w *World) SpawnWith(comps ...any) Entityfunc (w *World) Stats() []SystemStatfunc (w *World) SystemEnabled(name string) boolfunc (w *World) Update(dt float64)func (w *World) Updates() uint64
Examples
Example
package main
import (
"fmt"
"github.com/matjam/bunyip/ecs"
)
type Position struct{ X, Y float32 }
type Velocity struct{ X, Y float32 }
type Health struct{ HP int }
func main() {
w := ecs.NewWorld()
w.SpawnWith(Position{0, 0}, Velocity{1, 0}, Health{10})
w.SpawnWith(Position{5, 5}, Velocity{0, 1})
w.SpawnWith(Position{9, 9}) // no velocity: never moves
// A query walks every entity with both components; keep it and reuse it.
movers := w.Query2[Position, Velocity]()
movers.Each(func(e ecs.Entity, p *Position, v *Velocity) {
p.X += v.X
p.Y += v.Y
})
// Iteration order is by table and then by row, not by spawn order.
w.Each(func(e ecs.Entity, p *Position) { fmt.Println(p.X, p.Y) })
fmt.Println(movers.Count(), "movers of", w.Len())
}
1 0 5 6 9 9 2 movers of 3
Variables
Functions
NameOf source
func NameOf(w *World, e Entity) (string, bool)
NameOf returns the entity's name, and false when it has none or the name is empty.
Register source
func Register[T any](name string)
Register names a component or resource type for Save, Load and prefab files. The name is what the file holds, so choose one that stays stable when the type moves or is renamed. Registering the same type under the same name again does nothing; binding a name or a type that is already bound to something else panics. gfx.Transform, gfx.Transform2 and ecs.Name are registered under those names by default. A registered type is also stored in typed columns when it first reaches a world through SpawnWith, Load or a prefab, so spawning it that way costs the same as through Add.
SetParent source
func SetParent(w *World, child, parent Entity)
SetParent attaches child under parent, or detaches it with None. A
child follows its parent's transform (see WorldMatrix) and is
despawned with it. A missing parent detaches the child. Self-parenting
is ignored; an attempted longer cycle detaches the child from its old parent.
Example
package main
import (
"fmt"
"github.com/matjam/bunyip/ecs"
"github.com/matjam/bunyip/gfx"
"github.com/matjam/bunyip/lin"
)
func main() {
// A turret on a tank: the turret's transform is relative to the tank.
w := ecs.NewWorld()
tank := w.SpawnWith(gfx.At(10, 0, 0))
turret := w.SpawnWith(gfx.At(0, 1, 0))
ecs.SetParent(w, turret, tank)
fmt.Println(ecs.WorldMatrix(w, turret).MulPoint(lin.Vec3{}))
w.Despawn(tank) // children go with their parent
fmt.Println(w.Alive(turret))
}
{10 1 0}
false
UpdateWorldMatrices source
func UpdateWorldMatrices(w *World)
UpdateWorldMatrices composes every entity's world matrix in one walk from the roots down and caches the results, so WorldMatrix costs a slice index instead of a climb up the parent chain. Call it from a system after the ones that move transforms and before the ones that read world positions. The cache lasts until the next World.Update, so Draw reads what the last Update left; spawning, changing a parent link or despawning drops it, and a transform written after the pass is not seen until the pass runs again.
WorldMatrix source
func WorldMatrix(w *World, e Entity) lin.Mat4
WorldMatrix composes the gfx.Transform components from the root down to e; entities without one contribute identity. When UpdateWorldMatrices has run in this update the cached matrix is returned instead of walking the chain.
Types
type Children source
type Children struct{ List []Entity }
Children lists an entity's direct children; SetParent maintains it.
type Commands source
type Commands struct {
// contains filtered or unexported fields
}
Commands record structural changes to apply later with Apply, for code running inside a query that must not change other entities' tables mid-iteration. The zero value is ready to use. Component values passed to Spawn and Add are retained until Apply, not deep-copied; do not mutate their referenced storage before then. The argument slice itself is copied, so it may be reused at once. World.Defer manages a command buffer for a single closure.
Add source
func (c *Commands) Add(e Entity, comps ...any)
Add records attaching component values to an entity.
Apply source
func (c *Commands) Apply(w *World)
Apply runs the recorded changes in order and clears the buffer.
type ComponentID source
type ComponentID uint16
ComponentID numbers a component type within a world.
type Entity source
type Entity struct {
// contains filtered or unexported fields
}
Entity identifies a thing in a World. The zero Entity is None. A handle from a despawned entity is never confused with a new one that reused its slot.
ChildrenOf source
func ChildrenOf(w *World, e Entity) []Entity
ChildrenOf returns the entity's direct children; do not modify the slice.
Clone source
func Clone(w *World, e Entity) Entity
Clone makes a new entity carrying copies of e's components, attached to e's parent. Children are not copied; CloneTree does that. Entity fields that referred to e refer to the copy.
The copy is deep through exported fields: slices, maps, pointers and interface values get their own storage. Unexported fields are copied as values, so a slice or pointer in one is shared with the original. Custom JSON methods are not used for cloning. Remapper handles entity references, not general deep copying; initialize private mutable state separately when it must be independent in the clone.
CloneTree source
func CloneTree(w *World, e Entity) Entity
CloneTree clones e and all its descendants, keeping the hierarchy between the copies and attaching the root copy to e's parent. Entity fields that referred to something in the tree refer to its copy; references outside the tree are kept as they are.
ParentOf source
func ParentOf(w *World, e Entity) (Entity, bool)
ParentOf returns the entity's parent, if it has one.
SceneRef source
func SceneRef(n int) Entity
SceneRef returns the value to put in a component's Entity field to refer to the entity numbered n in a scene, counting from one. Instantiate rewrites it to the handle that entity was given. A number of zero, or one no entity has, is None.
ID source
func (e Entity) ID() uint64
ID combines the slot and generation, for debugging and maps. It is unique among live entities within one world, not across worlds.
MarshalJSON source
func (e Entity) MarshalJSON() ([]byte, error)
MarshalJSON encodes the entity as its ID, so components and resources holding entities save and load with encoding/json. Load rewrites the IDs to the handles it creates.
MarshalText source
func (e Entity) MarshalText() ([]byte, error)
MarshalText encodes the entity as its ID, for use as a map key.
UnmarshalJSON source
func (e *Entity) UnmarshalJSON(b []byte) error
UnmarshalJSON decodes an entity written by MarshalJSON.
UnmarshalText source
func (e *Entity) UnmarshalText(b []byte) error
UnmarshalText decodes an entity written by MarshalText.
type Filter source
type Filter func(w *World, incl, excl *mask)
Filter narrows a query beyond the components it reads.
type InstantiateOptions source
type InstantiateOptions struct {
// Prefabs resolves the scene's prefab references. The zero value
// falls back to the world's PrefabLibrary resource.
Prefabs PrefabLibrary
// Parent hangs every root of the instance under this entity, which
// is how a level goes under a container or a room under a floor.
// The zero value, None, leaves the roots unparented.
Parent Entity
// Offset moves each root of the instance by this much, added to the
// position in its gfx.Transform or gfx.Transform2. A root with
// neither component is not moved. The zero value moves nothing.
Offset lin.Vec3
// SkipUnknown leaves out components stored under a name this build
// has not registered instead of failing with UnregisteredError.
SkipUnknown bool
}
InstantiateOptions adjust World.Instantiate.
type LoadOptions source
type LoadOptions struct {
// SkipUnknown leaves out components and resources saved under a
// name this build has not registered instead of failing with
// UnregisteredError.
SkipUnknown bool
}
LoadOptions adjust World.Load.
type MissingPrefabError source
type MissingPrefabError struct{ Name string }
MissingPrefabError reports a scene reference to a prefab the library does not hold, or holds as nil. Name is the name the scene asked for.
type Name source
type Name struct{ Text string }
Name labels an entity so a scene can find it again. Instantiate adds one to every entity its scene names, and ExportScene writes it back as the scene entity's name instead of as a component. The zero value is the empty name, which no lookup matches.
type Parent source
type Parent struct{ Entity Entity }
Parent links an entity under another; SetParent maintains it.
type Prefab source
type Prefab struct {
// contains filtered or unexported fields
}
Prefab is a template of components, with child prefabs for a
hierarchy, that Spawn stamps into a world any number of times. Build
one in code with NewPrefab and Child, read one from JSON with
ParsePrefab, or take one from a live entity with PrefabOf.
Example
package main
import (
"fmt"
"github.com/matjam/bunyip/ecs"
"github.com/matjam/bunyip/gfx"
)
type Position struct{ X, Y float32 }
type Health struct{ HP int }
func main() {
// A template spawns as many independent copies as the game needs.
tank := ecs.NewPrefab(Position{0, 0}, Health{10}).
Child(ecs.NewPrefab(gfx.At(0, 1, 0))) // the turret
w := ecs.NewWorld()
a := tank.Spawn(w)
b := tank.Spawn(w)
if h, ok := w.Get[Health](a); ok {
h.HP = 3
}
hb, _ := w.Get[Health](b)
fmt.Println(hb.HP, len(ecs.ChildrenOf(w, a)), w.Len())
}
10 1 4
NewPrefab source
func NewPrefab(comps ...any) *Prefab
NewPrefab makes a prefab from component values. Pointers are refused; pass the struct.
ParsePrefab source
func ParsePrefab(data []byte) (*Prefab, error)
ParsePrefab reads a prefab from JSON. The form is an object with a "components" object keyed by registered name, each value encoded as in a save file, and an optional "children" array of the same form. An unregistered name is an UnregisteredError.
PrefabOf source
func PrefabOf(w *World, e Entity) *Prefab
PrefabOf snapshots an entity and its descendants as a prefab, leaving out Parent and Children. Spawning it gives a copy of the tree.
Child source
func (p *Prefab) Child(children ...*Prefab) *Prefab
Child adds child prefabs, spawned under the instance with SetParent, and returns the prefab for chaining.
Children source
func (p *Prefab) Children() []*Prefab
Children returns the prefab's child prefabs; do not modify the slice.
Components source
func (p *Prefab) Components() []any
Components returns copies of the prefab's component values.
MarshalJSON source
func (p Prefab) MarshalJSON() ([]byte, error)
MarshalJSON writes the prefab in the form ParsePrefab reads. A component type without a registered name is an UnregisteredError.
Spawn source
func (p *Prefab) Spawn(w *World) Entity
Spawn creates an entity from the prefab, and its children under it. Every instance gets its own copy of the components (see Clone for how deep the copy goes), so changing one does not change the others or the prefab.
UnmarshalJSON source
func (p *Prefab) UnmarshalJSON(data []byte) error
UnmarshalJSON reads the form MarshalJSON writes.
type PrefabLibrary source
type PrefabLibrary map[string]*Prefab
PrefabLibrary holds prefabs under the names scene documents reference. Build one at start-up and either pass it in InstantiateOptions or store it on the world with SetResource, which is where Instantiate looks when the options carry none. The zero value is an empty library, and every reference into it is a MissingPrefabError.
type Query1 source
type Query1[A any] struct {
// contains filtered or unexported fields
}
Query1 iterates entities with an A.
type Query2 source
type Query2[A, B any] struct {
// contains filtered or unexported fields
}
Query2 iterates entities with an A and a B.
type Query3 source
type Query3[A, B, C any] struct {
// contains filtered or unexported fields
}
Query3 iterates entities with an A, a B and a C.
type Query4 source
type Query4[A, B, C, D any] struct {
// contains filtered or unexported fields
}
Query4 iterates entities with four components.
type Remapper source
type Remapper interface {
Remap(fn func(Entity) Entity)
}
Remapper is implemented by components and resources that hold entities somewhere Load and CloneTree cannot see: unexported fields, or values behind an interface they should not rewrite. Remap replaces every entity the value holds with fn of it. A type without Remap has its exported fields walked with reflection instead, which covers Entity fields, slices, arrays, maps and pointers of them.
type SaveOptions source
type SaveOptions struct {
// SkipUnregistered leaves out components and resources whose type
// has no registered name instead of failing with UnregisteredError.
SkipUnregistered bool
// Indent writes the file with line breaks and indentation.
Indent bool
}
SaveOptions adjust World.Save.
type Scene source
type Scene struct {
// Name labels the scene. It is written to the document and read
// back; nothing else uses it. The zero value is no name.
Name string
// Properties are the game's own values, such as the music track or
// the wave number. They are encoded with the scene and come back
// decoded by encoding/json, so a number arrives as a float64. The
// zero value is no properties.
Properties map[string]any
// Entities are the entities the scene spawns, in the order
// Instantiate creates them. The zero value is an empty scene.
Entities []SceneEntity
// contains filtered or unexported fields
}
Scene is a document of entities a world spawns as a unit: a name, free-form properties for the game's own use, and a list of entities with their components, parent links and prefab references. Read one with ParseScene, build one with NewScene, take one from a live world with World.ExportScene, and spawn it with World.Instantiate.
Entities are numbered from one in Entities order. SceneEntity.Parent holds that number, and so does an Entity field inside a component, so zero means no parent and the None entity in both places.
ParseScene source
func ParseScene(data []byte) (*Scene, error)
ParseScene reads a scene document: a JSON object with a "version" of 1, an optional "name" and "properties", and an "entities" array. Each entity has an optional "name", an optional "parent" holding another entity's number counted from one, an optional "prefab" naming a prefab in the library Instantiate uses, and a "components" object keyed by registered name with each value encoded as a save file encodes it.
A version other than 1, a parent outside the scene or in a cycle, and two entities with the same name are all errors. Component names are checked by Instantiate, not here, so a scene parses in a build that has not registered every type in it.
AddEntity source
func (s *Scene) AddEntity(name string, comps ...any) (int, error)
AddEntity appends an entity carrying the component values and returns its number, its place in the scene counted from one. Pass an empty name for an entity nothing needs to find again. Each component is encoded the way World.Save encodes it, so its type needs a name from Register; a type without one is an UnregisteredError and nothing is added. Use SceneRef for an Entity field that points at another entity of the scene.
AddPrefab source
func (s *Scene) AddPrefab(name, prefab string, overrides ...any) (int, error)
AddPrefab appends an entity built from the library prefab called prefab and returns its number. Each override replaces the prefab's whole component of that type rather than merging field by field, and the prefab's children spawn as they are. Instantiate fails with a MissingPrefabError when its library has no prefab of that name.
Encode source
func (s *Scene) Encode() ([]byte, error)
Encode writes the scene as the document ParseScene reads, indented so it stays readable in a text editor and useful in version control. A scene whose parent links are out of range or circular, or that names two entities the same, is an error.
SetParent source
func (s *Scene) SetParent(child, parent int)
SetParent hangs the entity numbered child under the entity numbered parent, both counted from one. A parent of zero makes the child a root. A number outside the scene, or a child equal to its parent, is ignored.
SetProperty source
func (s *Scene) SetProperty(key string, v any)
SetProperty stores a free-form value under key for the game's own use. It is encoded with the scene and comes back decoded by encoding/json, so a number arrives as a float64.
type SceneEntity source
type SceneEntity struct {
// Name labels the entity for SceneInstance.Entity and becomes a
// Name component on the spawned entity. Names are unique within a
// scene. The zero value leaves the entity unnamed.
Name string `json:"name,omitempty"`
// Parent is the number of the entity this one hangs under, counted
// from one in Scene.Entities order. The zero value makes it a root
// of the scene.
Parent int `json:"parent,omitempty"`
// Prefab names a prefab in the library Instantiate uses. The zero
// value builds the entity from Components alone; otherwise the
// prefab is spawned and each entry of Components replaces the
// prefab's whole component of that type.
Prefab string `json:"prefab,omitempty"`
// Components holds each component encoded the way World.Save
// encodes it, keyed by the name Register gave its type. The zero
// value is no components.
Components map[string]json.RawMessage `json:"components,omitempty"`
}
SceneEntity is one entity of a Scene.
type SceneInstance source
type SceneInstance struct {
// Roots are the entities the scene left unparented, in scene order.
// With InstantiateOptions.Parent they hang under that entity.
Roots []Entity
// Spawned lists every entity the copy created, in scene order, with
// a prefab's children after the entity that referenced it.
Spawned []Entity
// contains filtered or unexported fields
}
SceneInstance is one spawned copy of a scene. Keep it to find the copy's entities by name and to remove the copy again.
type System source
type System func(w *World, dt float64)
System is a step of the simulation run by World.Update.
type SystemStat source
type SystemStat struct {
Name string
MS float64
}
SystemStat is the last Update's timing for one system.
type UnregisteredError source
type UnregisteredError struct {
Names []string
}
UnregisteredError reports types Save met that have no registered name, or names Load or ParsePrefab met that no type is registered under. Names holds the Go type names or the file's names respectively.
type World source
type World struct {
// contains filtered or unexported fields
}
World holds entities, their components, systems, resources and events. Construct it with NewWorld; the zero value is not initialized. A world supports at most 256 distinct component types and is not concurrency-safe.
Add source
func (w *World) Add[T any](e Entity, v T)
Add attaches a component, or replaces it when already present.
AddSystem source
func (w *World) AddSystem(name string, fn System)
AddSystem registers a system; systems run in registration order.
Example
package main
import (
"fmt"
"github.com/matjam/bunyip/ecs"
)
type Health struct{ HP int }
func main() {
type Score struct{ Points int }
type Killed struct{ Entity ecs.Entity }
w := ecs.NewWorld()
w.SetResource(Score{})
w.SpawnWith(Health{1})
w.SpawnWith(Health{5})
// Systems run in order; producers before consumers.
damage := w.Query1[Health]()
w.AddSystem("damage", func(w *ecs.World, dt float64) {
damage.Each(func(e ecs.Entity, h *Health) {
h.HP--
if h.HP <= 0 {
w.Despawn(e) // safe: the visited entity may be despawned
w.Emit(Killed{e})
}
})
})
w.AddSystem("score", func(w *ecs.World, dt float64) {
w.Resource[Score]().Points += 10 * len(w.Events[Killed]())
})
w.Update(1.0 / 60)
fmt.Println(w.Resource[Score]().Points, w.Len())
}
10 1
ComponentValues source
func (w *World) ComponentValues(e Entity) []any
ComponentValues returns a shallow copy of each component as an any value, in the same order as Components. Replacing fields in the copy requires Add to write them back; slices, maps and pointers still share their underlying storage with the entity. A dead entity returns nil.
Components source
func (w *World) Components(e Entity) []reflect.Type
Components lists the component types an entity carries.
Defer source
func (w *World) Defer(fn func(*Commands))
Defer records structural changes in fn and applies them, in order, after fn returns normally. Wrap the whole query walk in this scope so changes to other entities wait until the walk finishes. If fn panics, pending commands are discarded and the panic propagates.
Each scope has its own queue: a nested scope applies when its own fn
returns. This is not a transaction; direct world writes and changes
from completed nested scopes are not rolled back. Do not retain the
command buffer or call Apply on it inside fn. Use a separate Commands
value when commands need to survive beyond one closure.
Example
package main
import (
"fmt"
"github.com/matjam/bunyip/ecs"
)
type Position struct{ X, Y float32 }
type Health struct{ HP int }
func main() {
w := ecs.NewWorld()
w.SpawnWith(Health{3})
w.SpawnWith(Health{0})
// Changing other entities while a query walks them is not allowed;
// the scope applies the recorded changes after the walk finishes.
w.Defer(func(cmd *ecs.Commands) {
w.Each(func(e ecs.Entity, h *Health) {
if h.HP == 0 {
cmd.Spawn(Position{1, 1}) // a corpse
cmd.Despawn(e)
}
})
})
fmt.Println(w.Count[Health](), w.Count[Position]())
}
1 1
Despawn source
func (w *World) Despawn(e Entity)
Despawn removes an entity, its components and its children.
Each source
func (w *World) Each[T any](fn func(e Entity, t *T))
Each is a one-off iteration over entities with a T, for code that does not keep a query around. The query it needs is built once per world and component set and kept, so calling this every frame costs no more than a query of your own.
Each2 source
func (w *World) Each2[A, B any](fn func(e Entity, a *A, b *B))
Each2 is a one-off iteration over entities with an A and a B.
Each3 source
func (w *World) Each3[A, B, C any](fn func(e Entity, a *A, b *B, c *C))
Each3 is a one-off iteration over entities with an A, a B and a C.
Each4 source
func (w *World) Each4[A, B, C, D any](fn func(e Entity, a *A, b *B, c *C, d *D))
Each4 is a one-off iteration over entities with four components.
Entities source
func (w *World) Entities() []Entity
Entities lists every live entity, in no particular order.
Events source
func (w *World) Events[T any]() []T
Events returns the events of type T emitted since the start of this Update. Draw sees what the last Update emitted. The slice belongs to the event queue; copy it to retain events across Emit or Update calls.
ExportScene source
func (w *World) ExportScene(roots ...Entity) (*Scene, error)
ExportScene captures entities and everything under them as a scene document Instantiate spawns again, which is how a game writes out what it built or what the player changed. Pass the roots to capture, or none to capture every entity in the world that has no parent.
Each entity's components are encoded the way World.Save encodes them, its Name component becomes the scene entity's name, and an Entity field pointing at another captured entity becomes that entity's number; a reference to anything else becomes None. A component type with no name from Register fails the call with an UnregisteredError naming every such type.
The document holds components, never prefab references, because a live entity does not remember which prefab made it.
Get source
func (w *World) Get[T any](e Entity) (*T, bool)
Get returns a pointer to the entity's component. The pointer is valid until the next structural change (Add, Remove, Despawn, Spawn) that touches its table, so read or write it right away.
Instantiate source
func (w *World) Instantiate(scene *Scene, opts ...InstantiateOptions) (*SceneInstance, error)
Instantiate spawns a copy of the scene into the world and returns what it made. Every call makes fresh entities, so several copies of one scene live side by side. Components are decoded and attached, the scene's parent links are rebuilt, every named entity gets a Name component, and an Entity field holding an entity number is rewritten to the handle that entity was given; a number the scene does not use becomes None. A prefab reference spawns the library's prefab with its children and writes the entity's own components over the prefab's.
A component name this build has not registered fails the call with an
UnregisteredError listing the names, unless
InstantiateOptions.SkipUnknown is set, and a prefab reference the
library cannot resolve is a MissingPrefabError. Nothing is left in the
world when the call fails.
Example
package main
import (
"fmt"
"github.com/matjam/bunyip/ecs"
"github.com/matjam/bunyip/gfx"
"github.com/matjam/bunyip/lin"
)
type Position struct{ X, Y float32 }
type Health struct{ HP int }
func main() {
// Register names once, at start-up; a scene file holds these.
ecs.Register[Position]("Position")
ecs.Register[Health]("Health")
// A camp of two: the guard's Leader field points at the chief, which
// the document writes as the chief's number in the entity list.
doc := []byte(`{
"version": 1,
"name": "west camp",
"properties": {"music": "wind.ogg"},
"entities": [
{"name": "camp", "components": {"gfx.Transform": {"Position": {"X": 40}}}},
{"name": "chief", "parent": 1, "prefab": "orc", "components": {"Health": {"HP": 40}}},
{"parent": 1, "prefab": "orc", "components": {"gfx.Transform": {"Position": {"X": 2}}}}
]
}`)
scene, err := ecs.ParseScene(doc)
if err != nil {
fmt.Println(err)
return
}
w := ecs.NewWorld()
w.SetResource(ecs.PrefabLibrary{"orc": ecs.NewPrefab(Health{8}, gfx.Transform{})})
// Two copies of the camp, the second a hundred units east.
west, err := w.Instantiate(scene)
if err != nil {
fmt.Println(err)
return
}
if _, err := w.Instantiate(scene, ecs.InstantiateOptions{Offset: lin.V3(100, 0, 0)}); err != nil {
fmt.Println(err)
return
}
chief, _ := west.Entity("chief")
h, _ := w.Get[Health](chief)
fmt.Println(scene.Properties["music"], h.HP, w.Len())
// Removing one copy takes its entities and leaves the other whole.
west.Despawn(w)
fmt.Println(w.Len())
}
wind.ogg 40 6
3
Load source
func (w *World) Load(in io.Reader, opts ...LoadOptions) error
Load adds the entities and resources of a file written by Save to the world. Every entity gets a new handle; parent links, Children lists and the Entity fields inside components and resources are rewritten to the new handles (see Remapper), and a reference to an entity the file does not contain becomes None. Loaded resources replace the world's. Entities already in the world are untouched.
Names with no registered type fail the load before anything is added, with an UnregisteredError listing them, unless LoadOptions.SkipUnknown is set. A value that fails to decode stops the load part way.
MustResource source
func (w *World) MustResource[T any]() *T
MustResource returns the world's T and panics when it is missing.
Query1 source
func (w *World) Query1[A any](filters ...Filter) *Query1[A]
Query1 makes a query; keep it and reuse it across frames.
Query2 source
func (w *World) Query2[A, B any](filters ...Filter) *Query2[A, B]
Query2 makes a query; keep it and reuse it across frames.
Query3 source
func (w *World) Query3[A, B, C any](filters ...Filter) *Query3[A, B, C]
Query3 makes a query; keep it and reuse it across frames.
Query4 source
func (w *World) Query4[A, B, C, D any](filters ...Filter) *Query4[A, B, C, D]
Query4 makes a query; keep it and reuse it across frames.
Remove source
func (w *World) Remove[T any](e Entity)
Remove detaches a component; nothing happens if the entity has none.
Resource source
func (w *World) Resource[T any]() *T
Resource returns a pointer to the world's T, or nil when unset.
Resources source
func (w *World) Resources() []string
Resources lists the type names of the resources set on the world, sorted, for a debug view that shows what a world holds.
Save source
func (w *World) Save(out io.Writer, opts ...SaveOptions) error
Save writes every live entity as JSON: its ID, its parent and
children, and each component encoded with encoding/json under its
registered name. Registered resources follow. Parent and Children
components are written as the links, not as components. A component
or resource type without a registered name fails the save with an
UnregisteredError naming every such type, unless
SaveOptions.SkipUnregistered is set.
Example
package main
import (
"bytes"
"fmt"
"github.com/matjam/bunyip/ecs"
)
type Position struct{ X, Y float32 }
type Health struct{ HP int }
func main() {
// Register names once, at start-up, so files outlive renames.
ecs.Register[Position]("Position")
ecs.Register[Health]("Health")
w := ecs.NewWorld()
w.SpawnWith(Position{1, 2}, Health{5})
var save bytes.Buffer
if err := w.Save(&save); err != nil {
fmt.Println(err)
}
loaded := ecs.NewWorld()
if err := loaded.Load(&save); err != nil {
fmt.Println(err)
}
loaded.Each(func(e ecs.Entity, p *Position) { fmt.Println(p.X, p.Y) })
}
1 2
SetComponent source
func (w *World) SetComponent(e Entity, v any)
SetComponent attaches a component given as a value, replacing the one the entity carries of that type. It is what an editor or a debug panel writes an edited component back through, where the type is known only at run time; game code that knows the type calls Add. The value must not be a pointer, and nothing happens if the entity is gone.
SetResource source
func (w *World) SetResource[T any](v T)
SetResource stores a singleton value of type T on the world: the rules, the score, the input state, anything there is one of.
SetSystemEnabled source
func (w *World) SetSystemEnabled(name string, on bool)
SetSystemEnabled turns a system on or off by the name it was registered under, for a debugger that pauses part of the simulation. A system that is off is skipped by Update and keeps its place in the order; a name no system has is ignored. Systems start on.
SpawnWith source
func (w *World) SpawnWith(comps ...any) Entity
SpawnWith creates an entity carrying the given component values.
Stats source
func (w *World) Stats() []SystemStat
Stats reports each system's most recent timing in milliseconds. The returned slice belongs to the world; do not modify it, and copy it to retain a snapshot across updates. Disabled systems keep their last timing.
SystemEnabled source
func (w *World) SystemEnabled(name string) bool
SystemEnabled reports whether a system runs. A name no system has reads as off.
Update source
func (w *World) Update(dt float64)
Update runs every system in order with dt. Events emitted during the previous Update (or between updates, in Draw) are cleared first, so order systems with producers before consumers. A system turned off with SetSystemEnabled is skipped and keeps its last timing.
Source files
bench_test.go defer_test.go ecs.go ecs_test.go example_test.go load_bench_test.go perf_bench_test.go perf_test.go prefab.go prefab_test.go query.go reflect.go review_test.go save.go save_test.go scene.go scene_names_test.go scene_test.go world.go