Documentation
¶
Index ¶
- type Entity
- type EventReader
- type Events
- type Query
- type Query2
- type Query3
- type Query4
- type Query5
- type Storage
- type World
- func (w *World) Alive(e Entity) bool
- func (w *World) Clean()
- func (w *World) Destroy(e Entity)
- func (w *World) Events[T any]() *Events[T]
- func (w *World) FlushEvents()
- func (w *World) Get[T any](e Entity) *T
- func (w *World) Has[T any](e Entity) bool
- func (w *World) NewEntity() Entity
- func (w *World) Query[A any]() Query[A]
- func (w *World) Query2[A, B any]() Query2[A, B]
- func (w *World) Query3[A, B, C any]() Query3[A, B, C]
- func (w *World) Query4[A, B, C, D any]() Query4[A, B, C, D]
- func (w *World) Query5[A, B, C, D, E any]() Query5[A, B, C, D, E]
- func (w *World) Remove[T any](e Entity)
- func (w *World) Send[T any](event T)
- func (w *World) Set[T any](e Entity, value T)
- func (w *World) Storage[T any]() *Storage[T]
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
This section is empty.
Types ¶
type Entity ¶
type Entity uint64
Entity is a unique handle to an object in a World.
The low 32 bits are an index into the World's internal tables and the high 32 bits are a version counter. When an entity is destroyed its index is recycled with a bumped version, so stale handles held by game code are safely detected as dead instead of pointing at a new entity.
const Nil Entity = 0
Nil is the zero Entity. Live entities always have a version of at least 1, so Nil (and any zero-valued Entity field) never refers to a live entity: World.Alive(Nil) is always false. Use it as a "no entity" sentinel.
type EventReader ¶ added in v0.0.2
type EventReader[T any] struct { // contains filtered or unexported fields }
EventReader is one consumer's cursor into an Events[T] queue. Each reader advances independently; reading never removes events from the queue.
Store the reader by value in the consuming system and call Each through a pointer (methods mutate the cursor).
func (*EventReader[T]) Clear ¶ added in v0.0.2
func (r *EventReader[T]) Clear()
Clear marks every currently buffered event as seen without visiting it.
func (*EventReader[T]) Each ¶ added in v0.0.2
func (r *EventReader[T]) Each(fn func(T))
Each calls fn once for every event this reader has not seen yet, oldest first, and marks them seen. Events are passed by value: they are shared by every reader of the queue, so treat them as immutable messages.
Events sent by fn itself (same type, from inside the callback) are not visited by this call — they are picked up by the next Each, which keeps a system that reacts to events by sending more from looping forever.
type Events ¶ added in v0.0.2
type Events[T any] struct { // contains filtered or unexported fields }
Events is a double-buffered queue of events of type T — the pull-based channel between systems that the rest of the library's design implies: a producer system Sends events, consumer systems poll them through their own EventReader, and nobody holds a reference to anybody else.
clicks := w.Events[ButtonClicked]() // in the producer's constructor
clicks.Send(ButtonClicked{Button: e}) // during its Update
reader := w.Events[ButtonClicked]().Reader() // in a consumer's constructor
reader.Each(func(ev ButtonClicked) { ... }) // during its Update
w.FlushEvents() // once per frame, after all systems ran
Lifetime: an event survives until the second FlushEvents after it was sent. That two-frame window is what makes the queue order-independent — a consumer that runs *before* the producer in the frame still sees the event on the next frame, one frame late but never lost. The flip side: a reader must poll at least once per frame, or events expire unread.
Each reader tracks its own cursor, so any number of systems can consume the same queue independently; reading never removes anything.
Performance: Send is an append into a buffer whose capacity is reused after every flush, and reading is a linear walk of a packed slice — in steady state nothing allocates, and worlds that never touch events pay nothing at all.
Like the rest of the World, an Events queue is not goroutine-safe: do not Send or read from inside an EachParallel pass.
func (*Events[T]) Reader ¶ added in v0.0.2
func (ev *Events[T]) Reader() EventReader[T]
Reader returns a new independent cursor over the queue. Create one per consuming system (at construction, alongside its queries) and store it — the cursor is what remembers which events this system has already seen. A fresh reader starts before the oldest buffered event, so it also sees the up-to-two frames of history still in the queue.
type Query ¶
type Query[A any] struct { // contains filtered or unexported fields }
Query iterates every entity that has component A.
Create a query once (typically when a system is constructed) and reuse it every frame: construction resolves the storage, so Each is just a loop over a packed slice.
func (Query[A]) Each ¶
Each calls fn for every entity with component A. Iteration runs backwards over the dense slice, so destroying the current entity (or removing its A component) inside fn is safe.
func (Query[A]) EachParallel ¶
EachParallel calls fn for every entity with component A, splitting the pass across worker goroutines. fn receives the entity's dense slot index — stable for the duration of the pass and unique per entity, so concurrent workers can write results into disjoint slots of one shared buffer with no locks (e.g. vertices at index*4).
workers selects the parallelism: <= 0 means one worker per CPU (runtime.GOMAXPROCS(0), re-read each pass since the runtime may adjust it), and 1 runs inline on the calling goroutine with no goroutines or WaitGroup at all. The count is also capped so each worker gets at least a few thousand entities — small passes always run inline, so EachParallel(0, fn) is safe as a default.
Unlike Each, iteration is forward and NO structural change to the world (Destroy, Remove, Set of a new component, NewEntity) is allowed anywhere during the pass. No two workers ever see the same entity.
type Query2 ¶
type Query2[A, B any] struct { // contains filtered or unexported fields }
Query2 iterates every entity that has both components A and B.
func (Query2[A, B]) Each ¶
Each calls fn for every entity with both A and B. It walks the smaller of the two storages and probes the other, so cost scales with the rarer component. As with Query.Each, destroying the current entity inside fn is safe.
func (Query2[A, B]) EachParallel ¶
EachParallel is the multi-goroutine variant of Each — see Query.EachParallel for the workers semantics and rules. Unlike Each it always drives from A's storage (so cost scales with A, not the rarest component) and skips entities lacking B.
type Query3 ¶
type Query3[A, B, C any] struct { // contains filtered or unexported fields }
Query3 iterates every entity that has components A, B, and C.
func (Query3[A, B, C]) Each ¶
Each calls fn for every entity with all of A, B, and C. It walks the smallest of the three storages and probes the other two, so cost scales with the rarest component. As with Query.Each, destroying the current entity inside fn is safe.
func (Query3[A, B, C]) EachParallel ¶
EachParallel is the multi-goroutine variant of Each — see Query.EachParallel for the workers semantics and rules. Unlike Each it always drives from A's storage and skips entities lacking B or C.
type Query4 ¶
type Query4[A, B, C, D any] struct { // contains filtered or unexported fields }
Query4 iterates every entity that has components A, B, C, and D.
func (Query4[A, B, C, D]) Each ¶
Each calls fn for every entity with all of A, B, C, and D. It walks the smallest of the four storages and probes the others, so cost scales with the rarest component. As with Query.Each, destroying the current entity inside fn is safe.
func (Query4[A, B, C, D]) EachParallel ¶
EachParallel is the multi-goroutine variant of Each — see Query.EachParallel for the workers semantics and rules. Unlike Each it always drives from A's storage and skips entities lacking any other component.
type Query5 ¶
type Query5[A, B, C, D, E any] struct { // contains filtered or unexported fields }
Query5 iterates every entity that has components A, B, C, D, and E.
func (Query5[A, B, C, D, E]) Each ¶
Each calls fn for every entity with all of A, B, C, D, and E. It walks the smallest of the five storages and probes the others, so cost scales with the rarest component. As with Query.Each, destroying the current entity inside fn is safe.
func (Query5[A, B, C, D, E]) EachParallel ¶
EachParallel is the multi-goroutine variant of Each — see Query.EachParallel for the workers semantics and rules. Unlike Each it always drives from A's storage and skips entities lacking any other component.
type Storage ¶
type Storage[T any] struct { // contains filtered or unexported fields }
Storage is a sparse set holding every component of type T in the World.
All component values live in one densely packed slice, so iterating them is a linear scan over contiguous memory. The sparse slice maps an entity index to its slot in the dense slice, giving O(1) lookup, insert, and remove.
World.Set/Get/Has/Remove look the storage up in a reflection-keyed map on every call. The equivalent methods here skip that: cache the storage once (via World.Storage) and spawn/mutate through it in hot paths.
func (*Storage[T]) Get ¶
Get returns a pointer to e's component, or nil if e is dead or has no such component. Equivalent to World.Get without the per-call storage lookup.
type World ¶
type World struct {
// contains filtered or unexported fields
}
World owns all entities and their components.
Component access uses Go 1.27 generic methods: the component type is a type argument, so there is no interface boxing and no type assertions in user code, e.g.
e := w.NewEntity()
w.Set(e, Position{X: 10})
pos := w.Get[Position](e)
func (*World) Clean ¶ added in v0.0.4
func (w *World) Clean()
Clean removes every entity, component, and buffered event from the world, leaving it logically as empty as NewWorld but keeping all of its allocated memory — the point being to rebuild a scene into arrays that are already the right size, with no allocation and no GC churn on the transition.
It is not a loop of Destroy calls: each storage is emptied in one shot rather than probed once per entity, so the cost is a pass over the version table plus O(1) work per component type, independent of how many components each entity had.
Everything systems cache stays valid: the *Storage[T] returned by Storage and any Query built from the world keep working and observe the cleaned state, so systems constructed once at startup survive any number of scene transitions.
Every Entity handle taken before the call is dead afterwards (Alive reports false), including handles held inside components of the next scene. Buffered events are dropped too — otherwise a queue could deliver last scene's events, carrying now-dead entities, to the scene that replaces it.
func (*World) Destroy ¶
Destroy removes an entity and all of its components. The entity's index is recycled, but any Entity handles to it become dead (Alive returns false).
func (*World) Events ¶ added in v0.0.2
Events returns the event queue for type T, creating it on first use — the event-side twin of Storage. Producers and consumers that name the same type get the same queue; that shared type is their only coupling.
Like Storage, the lookup costs a reflection map hit: call it at system construction and keep the result, not inside the frame loop.
func (*World) FlushEvents ¶ added in v0.0.2
func (w *World) FlushEvents()
FlushEvents advances every event queue one frame. Call it exactly once per frame, after all systems have run. Events sent during frame N are readable for the rest of frame N and all of frame N+1, then dropped.
func (*World) Get ¶
Get returns a pointer to e's component of type T, or nil if e is dead or has no such component. The pointer is valid until the component is removed.
func (*World) Send ¶ added in v0.0.2
Send appends an event to the world's queue for type T. Convenience twin of World.Set: it pays the reflection map lookup per call, so hot producers should cache w.Events[T]() and Send on that instead.
func (*World) Storage ¶
Storage returns the Storage for component type T, creating it on first use. Queries cache these, so the map lookup happens once per query, not per call.
Hot paths that Set/Get outside a query (e.g. spawning bursts of entities) should cache the returned *Storage[T] and use its methods directly: they do the same work as World.Set/Get minus the per-call reflection map lookup.