Documentation ¶
Overview ¶
Package ecs provides the core plumbing necessary to build Entity Component Systems on top of Go structs.
TODO: say more, for now see the employee example.
Example (Employees) ¶
package main import ( "fmt" "sort" "github.com/borkshop/bork/internal/ecs" "github.com/borkshop/bork/internal/moremath" ) type workers struct { ecs.Core name []string age []int skills []skill // TODO employee relations like reporting chain } type jobs struct { ecs.Core name []string skills []skill work []int } type skill struct { brawn int brain int } type assignment struct { ecs.Relation wrk *workers jb *jobs // NOTE can attach data to each relation since // ecs.Relation is just an ecs.Core } // declare your type constants // // NOTE these need not correspond 1:1 with data storage in your structure: // - some types may just be a flag, corresponding to no data aspect // - some types may encompass more than one aspect of data // - in short, they're mostly up to the ecs-embedding struct itself to hang // semantics off of. const ( workerName ecs.ComponentType = 1 << iota workerStats workerAssigned ) const ( jobInfo ecs.ComponentType = 1 << iota jobWork jobAssigned ) const ( amtWorking ecs.ComponentType = 1 << iota // NOTE components may define kinds of relation, and/or attached data ) // declare common combinations of our types; these are all about domain // semantics, and not at all about ecs.Core logic. const ( workerInfo = workerName | workerStats ) // init hooks up the ecs.Core plumbing; note the choice to go with init-style // methods, which allow both a `NewFoo() *Foo` style constructor, and also // usage as a value (embedded or standalone). func (wrk *workers) init() { // start out with the zero sentinel and room for 1k entities wrk.name = make([]string, 1, 1+1024) wrk.age = make([]int, 1, 1+1024) wrk.skills = make([]skill, 1, 1+1024) // the [0] sentinel will be used by our creators to (re-)initialze data wrk.name[0] = "Unnamed" wrk.age[0] = 18 wrk.skills[0] = skill{1, 1} // must register one or more allocators that cover all statically-allocated // aspects of our data; by "static" we mean: not necessarily tied to Entity // lifecycle or type. NOTE: // - allocators must be disjoint by registered type // - they may initialize memory (similar to creators and destroyers) wrk.Core.RegisterAllocator(workerInfo, wrk.alloc) // creators and destroyers on the other hand, need not be disjoint (you may // registor 0-or-more of them for any overlapping set of types); they serve // as entity lifecycle callbacks. // // NOTE: you have nothing but a field of design choice here: // - You could clear/reset state in the destroyer... // - ...or in the creator ahead of re-use. // - You can also register destroyers and creators against ecs.NoType, and // whey will fire at end-of-life and start-of-life respectively for any // Entity. wrk.Core.RegisterCreator(workerName, wrk.createName) wrk.Core.RegisterCreator(workerStats, wrk.createStats) wrk.Core.RegisterDestroyer(workerName, wrk.destroyName) wrk.Core.RegisterDestroyer(workerStats, wrk.destroyStats) } func (jb *jobs) init() { jb.name = make([]string, 1, 1+1024) jb.skills = make([]skill, 1, 1+1024) jb.work = make([]int, 1, 1+1024) jb.Core.RegisterAllocator(jobInfo|jobWork, jb.alloc) jb.Core.RegisterCreator(jobInfo, jb.createInfo) jb.Core.RegisterCreator(jobWork, jb.createWork) jb.Core.RegisterDestroyer(jobInfo, jb.destroyInfo) jb.Core.RegisterDestroyer(jobWork, jb.destroyWork) } func (amt *assignment) init(wrk *workers, jb *jobs) { amt.wrk = wrk amt.jb = jb amt.Relation.Init(&wrk.Core, 0, &jb.Core, 0) } func (wrk *workers) alloc(id ecs.EntityID, t ecs.ComponentType) { // N.B. we could choose to copy from [0], but the creators do that, so no // need; if they didn't, the destroyers shoul reset to [0]'s state for consistency. wrk.name = append(wrk.name, "") wrk.age = append(wrk.age, 0) wrk.skills = append(wrk.skills, skill{}) } func (wrk *workers) createName(id ecs.EntityID, t ecs.ComponentType) { wrk.name[id] = wrk.name[0] } func (wrk *workers) destroyName(id ecs.EntityID, t ecs.ComponentType) { wrk.name[id] = "" } func (wrk *workers) createStats(id ecs.EntityID, t ecs.ComponentType) { wrk.age[id] = wrk.age[0] wrk.skills[id] = wrk.skills[0] } func (wrk *workers) destroyStats(id ecs.EntityID, t ecs.ComponentType) { wrk.age[id] = 0 wrk.skills[id] = skill{} } func (jb *jobs) alloc(id ecs.EntityID, t ecs.ComponentType) { jb.name = append(jb.name, "") jb.skills = append(jb.skills, skill{}) jb.work = append(jb.work, 0) } func (jb *jobs) createInfo(id ecs.EntityID, t ecs.ComponentType) { jb.name[id] = jb.name[0] jb.skills[id] = jb.skills[0] } func (jb *jobs) destroyInfo(id ecs.EntityID, t ecs.ComponentType) { jb.name[id] = "" jb.skills[id] = skill{} } func (jb *jobs) createWork(id ecs.EntityID, t ecs.ComponentType) { jb.work[id] = jb.work[0] } func (jb *jobs) destroyWork(id ecs.EntityID, t ecs.ComponentType) { jb.work[id] = 0 } func (wrk *workers) load(args ...interface{}) { for i := 0; i < len(args); { id := wrk.AddEntity(workerInfo).ID() wrk.name[id] = args[i].(string) i++ wrk.age[id] = args[i].(int) i++ wrk.skills[id].brawn = args[i].(int) i++ wrk.skills[id].brain = args[i].(int) i++ } } func (jb *jobs) load(args ...interface{}) { for i := 0; i < len(args); { id := jb.AddEntity(workerInfo).ID() jb.name[id] = args[i].(string) i++ jb.skills[id].brawn = args[i].(int) i++ jb.skills[id].brain = args[i].(int) i++ jb.work[id] = args[i].(int) i++ } } func (sk skill) key() uint64 { return moremath.Shuffle( moremath.ClampInt32(sk.brawn), moremath.ClampInt32(sk.brain), ) } func (wrk *workers) unassigned() ecs.Iterator { cl := ecs.And( workerStats.All(), workerAssigned.NotAny(), ) return wrk.Iter(cl) } func (jb *jobs) unassigned() ecs.Iterator { return jb.Iter((jobInfo | jobWork).All(), jobAssigned.NotAny()) } func (amt *assignment) assign() { // order workers by their skill var wids []ecs.EntityID for it := amt.wrk.unassigned(); it.Next(); { wids = append(wids, it.ID()) } sort.Slice(wids, func(i, j int) bool { return amt.wrk.skills[wids[i]].key() < amt.wrk.skills[wids[j]].key() }) // match each job with best worker amt.Upsert(nil, func(uc *ecs.UpsertCursor) { for it := amt.jb.unassigned(); len(wids) > 0 && it.Next(); { // pick best worker and remove it from the list jk := amt.jb.skills[it.ID()].key() wix := sort.Search(len(wids), func(i int) bool { return amt.wrk.skills[wids[i]].key() >= jk }) if wix >= len(wids) { wix = len(wids) - 1 } wid := wids[wix] copy(wids[wix:], wids[wix+1:]) wids = wids[:len(wids)-1] // assign worker to job worker, job := amt.wrk.Ref(wid), it.Entity() uc.Create(amtWorking, worker, job) worker.Add(workerAssigned) job.Add(jobAssigned) } }) } func main() { var ( // NOTE if this were for real, you'd probably wrap // this in a world struct; world itself could be an // ecs.Core to bind worker and job info into some sort // of space. wrk workers jb jobs amt assignment ) wrk.init() jb.init() amt.init(&wrk, &jb) // put some data in wrk.load( "Doug", 31, 7, 4, "Bob", 23, 3, 6, "Alice", 33, 4, 7, "Cathy", 27, 8, 4, "Ent", 25, 6, 6, ) jb.load( "t1", 10, 1, 5, "t2", 1, 10, 5, "t3", 3, 6, 3, "t4", 6, 3, 3, "t5", 4, 4, 2, ) // NOTE finally we get to The Point: an ECS is designed primarily in // service to its processing phase; what you do with all the data matters // most, not how you use a single or a few pieces of data. amt.assign() fmt.Printf("assignments:\n") for cur := amt.Select(amtWorking.All()); cur.Scan(); { worker, job := cur.A(), cur.B() ws := wrk.skills[worker.ID()] js := jb.skills[job.ID()] fmt.Printf( "- %s working on %q, rating: <%.1f, %.1f>\n", wrk.name[worker.ID()], jb.name[job.ID()], float64(ws.brawn)/float64(js.brawn), float64(ws.brain)/float64(js.brain), ) } it := wrk.unassigned() fmt.Printf("\nunassigned workers %v\n", it.Count()) for it.Next() { fmt.Printf("- %v %q\n", it.Entity(), wrk.name[it.ID()]) } it = jb.unassigned() fmt.Printf("\nunassigned jobs %v\n", it.Count()) for it.Next() { fmt.Printf("- %v %q\n", it.Entity(), jb.name[it.ID()]) } }
Output: assignments: - Cathy working on "t1", rating: <0.8, 4.0> - Ent working on "t2", rating: <6.0, 0.6> - Bob working on "t3", rating: <1.0, 1.0> - Doug working on "t4", rating: <1.2, 1.3> - Alice working on "t5", rating: <1.0, 1.8> unassigned workers 0 unassigned jobs 0
Index ¶
- Variables
- type ComponentType
- func (t ComponentType) All() TypeClause
- func (t ComponentType) Any() TypeClause
- func (t ComponentType) ApplyTo(ent Entity)
- func (t ComponentType) HasAll(mask ComponentType) bool
- func (t ComponentType) HasAny(mask ComponentType) bool
- func (t ComponentType) NotAll() TypeClause
- func (t ComponentType) NotAny() TypeClause
- func (t ComponentType) String() string
- type Core
- func (co *Core) AddEntity(nt ComponentType) Entity
- func (co *Core) Cap() int
- func (co *Core) Clear()
- func (co *Core) Deref(e Entity) EntityID
- func (co *Core) Empty() bool
- func (co *Core) Iter(tcls ...TypeClause) Iterator
- func (co *Core) Len() int
- func (co *Core) Ref(id EntityID) Entity
- func (co *Core) RegisterAllocator(t ComponentType, allocator func(EntityID, ComponentType))
- func (co *Core) RegisterCreator(t ComponentType, creator func(EntityID, ComponentType))
- func (co *Core) RegisterDestroyer(t ComponentType, destroyer func(EntityID, ComponentType))
- func (co *Core) SetType(id EntityID, new ComponentType)
- func (co *Core) Type(id EntityID) ComponentType
- type Cursor
- type CursorOpt
- type Entity
- type EntityID
- type Filter
- type Graph
- func (G *Graph) Init(core *Core, flags RelationFlags)
- func (G *Graph) Leaves(tcl TypeClause, where func(ent, a, b Entity, r ComponentType) bool) []Entity
- func (G *Graph) Roots(tcl TypeClause, where func(ent, a, b Entity, r ComponentType) bool) []Entity
- func (G *Graph) Traverse(tcl TypeClause, mode TraversalMode) GraphTraverser
- type GraphTraverser
- type Iterator
- type Proc
- type ProcFunc
- type Relation
- func (rel *Relation) A(ent Entity) Entity
- func (rel *Relation) B(ent Entity) Entity
- func (rel *Relation) Init(aCore *Core, aFlags RelationFlags, bCore *Core, bFlags RelationFlags)
- func (rel *Relation) Select(opts ...CursorOpt) Cursor
- func (rel *Relation) Upsert(cur Cursor, each func(*UpsertCursor)) (n, m int)
- type RelationFlags
- type System
- type TraversalMode
- type TypeClause
- type UpsertCursor
Examples ¶
Constants ¶
This section is empty.
Variables ¶
var NilEntity = Entity{}
NilEntity is the zero of Entity, representing "no entity, in no Core".
Functions ¶
This section is empty.
Types ¶
type ComponentType ¶
type ComponentType uint64
ComponentType represents the type of an Entity in a Core.
const NoType ComponentType = 0
NoType represents an unused entity; one that has been allocated, but not yet handed out by AddEntity.
func (ComponentType) All ¶
func (t ComponentType) All() TypeClause
All return a clause that matches only if all of the type bits are set. If the type is NoType, the clause never matches (always returns false).
func (ComponentType) Any ¶
func (t ComponentType) Any() TypeClause
Any return a clause that matches only if at least one of the type bits is set. If the type is NoType, the clause always matches (always returns true).
func (ComponentType) ApplyTo ¶
func (t ComponentType) ApplyTo(ent Entity)
ApplyTo sets the given entity's type to t; simply a dual of Entity.SetType.
func (ComponentType) HasAll ¶
func (t ComponentType) HasAll(mask ComponentType) bool
HasAll returns true only if all of the masked type bits are set. If the mask is NoType, always returns false.
func (ComponentType) HasAny ¶
func (t ComponentType) HasAny(mask ComponentType) bool
HasAny returns true only if at least one of the masked type bits is set. If the mask is NoType, always returns true.
func (ComponentType) NotAll ¶
func (t ComponentType) NotAll() TypeClause
NotAll return a clause that matches only if at least one of the type bits is not set.
func (ComponentType) NotAny ¶
func (t ComponentType) NotAny() TypeClause
NotAny return a clause that matches only if none of the type bits are not set.
func (ComponentType) String ¶
func (t ComponentType) String() string
type Core ¶
type Core struct {
// contains filtered or unexported fields
}
Core is the core of an Entity Component System: it manages the entity IDs and types.
func (*Core) AddEntity ¶
func (co *Core) AddEntity(nt ComponentType) Entity
AddEntity adds an entity to a core, returning an Entity reference; it MAY re-use a previously-used but since-destroyed entity (one whose type is still NoType). MAY invokes all allocators to make space for more entities (will do so if Cap() == Len()).
func (*Core) Cap ¶
Cap returns how many entities have been statically allocated within the Core. If Len() < Cap() then calls to AddEntity will re-use a prior id.
func (*Core) Deref ¶
Deref unpacks an Entity reference, returning its ID; it panics if the Core doesn't own the Entity.
func (*Core) Iter ¶
func (co *Core) Iter(tcls ...TypeClause) Iterator
Iter returns a new iterator over the Core's entities which satisfy all of given TypeClause(s).
func (*Core) Ref ¶
Ref returns an Entity reference to the given ID; it is valid to return a reference to the zero entity, to represent "no entity, in this Core" (e.g. will Deref() to 0 EntityID).
func (*Core) RegisterAllocator ¶
func (co *Core) RegisterAllocator(t ComponentType, allocator func(EntityID, ComponentType))
RegisterAllocator registers an allocator function; it panics if any allocator is registered that overlaps the given type.
Allocators are called when the Core grows its entity capacity. An allocator must create space in each of its data collections so that the given id has corresponding element(s).
func (*Core) RegisterCreator ¶
func (co *Core) RegisterCreator(t ComponentType, creator func(EntityID, ComponentType))
RegisterCreator registers a creator function. The Type may overlap any number of other creator Types, so each should be written cooperatively.
Creators are called when an Entity has all of its Type bits added to it; they may initialize static data, allocate dynamic data, or do other Type specific things.
Any creators registered against NoType trigger simply at entity creation time; they will be called when an entity transitions from NoType to any arbitrary type. NOTE: this may or may not be proximate to allocation time!
func (*Core) RegisterDestroyer ¶
func (co *Core) RegisterDestroyer(t ComponentType, destroyer func(EntityID, ComponentType))
RegisterDestroyer registers a destroyer function. The Type may overlap any number of other destroyer Types, so each should be written cooperatively.
Destroyers are called when an Entity has any of its Type bits removed from it; they may clear static data, de-allocate dynamic data, or do other Type specific things. NOTE: destroyers must not de-allocate static data.
Any destroyers registered against NoType trigger at entity deletion time; they will be called when an entity transitions to NoType.
func (*Core) SetType ¶
func (co *Core) SetType(id EntityID, new ComponentType)
SetType changes an entity's type, calling any relevant lifecycle functions.
type CursorOpt ¶
type CursorOpt interface {
// contains filtered or unexported methods
}
CursorOpt specifies which relational entites are scanned by the cursor. TypeClause implements CursorOpt, and is the most basic one.
type Entity ¶
type Entity struct {
// contains filtered or unexported fields
}
Entity is a reference to an entity in a Core
func (Entity) Add ¶
func (ent Entity) Add(t ComponentType)
Add sets bits in the entity's type, calling any creators that are newly satisfied by the new type.
func (Entity) Delete ¶
func (ent Entity) Delete(t ComponentType)
Delete clears bits in the entity's type, calling any destroyers that are no longer satisfied by the new type (which may be NoType).
func (Entity) Destroy ¶
func (ent Entity) Destroy()
Destroy sets the entity's type to NoType, invoking any destroyers that match the prior type.`
func (Entity) ID ¶
ID returns the ID of the referenced entity; it SHOULD only be called in a context where the caller is sure of ownership; when in doubt, use Core.Deref(ent) instead.
func (Entity) SetType ¶
func (ent Entity) SetType(t ComponentType)
SetType sets the entity's type; may invoke creators and destroyers as appropriate.
func (Entity) Type ¶
func (ent Entity) Type() ComponentType
Type returns the type of the referenced entity, or NoType if the reference is empty.
type EntityID ¶
type EntityID int
EntityID is the ID of an Entity in a Core; the 0 value is an invalid ID, meaning "null entity".
type Filter ¶
Filter is a CursorOpt that applies a filtering predicate function to a cursor. NOTE this the returned Cursor's Count() method may ignore the filter, drastically over-counting.
type Graph ¶
type Graph struct {
Relation
}
Graph is an auto-relation: one where both the A-side and B-side are the same Core system.
func NewGraph ¶
func NewGraph(core *Core, flags RelationFlags) *Graph
NewGraph creates a new graph relation for the given Core system.
func (*Graph) Init ¶
func (G *Graph) Init(core *Core, flags RelationFlags)
Init initializes the graph relation; useful for embedding.
func (*Graph) Leaves ¶
func (G *Graph) Leaves( tcl TypeClause, where func(ent, a, b Entity, r ComponentType) bool, ) []Entity
Leaves returns a slice of Entities that have no out-relation (i.e. there's no relation `a R b for all b in the result`).
func (*Graph) Roots ¶
func (G *Graph) Roots( tcl TypeClause, where func(ent, a, b Entity, r ComponentType) bool, ) []Entity
Roots returns a slice of Entities that have no in-relation (i.e. there's no relation `a R b for all a in the result`).
func (*Graph) Traverse ¶
func (G *Graph) Traverse(tcl TypeClause, mode TraversalMode) GraphTraverser
Traverse returns a new graph travers for the given type clause and mode.
type GraphTraverser ¶
type GraphTraverser interface { Init(seed ...EntityID) Traverse() bool G() *Graph Edge() Entity Node() Entity }
GraphTraverser traverses a graph in some order.
type Iterator ¶
type Iterator interface { Next() bool Reset() Count() int Type() ComponentType ID() EntityID Entity() Entity }
Iterator is an entity iterator.
type ProcFunc ¶
type ProcFunc func()
ProcFunc is a convenience for implementing Proc around an arbitrary void function.
type Relation ¶
type Relation struct { Core // contains filtered or unexported fields }
Relation contains entities that represent relations between entities in two (maybe different) Cores. Users may attach arbitrary data to these relations the same way you would with Core.
func NewRelation ¶
func NewRelation( aCore *Core, aFlags RelationFlags, bCore *Core, bFlags RelationFlags, ) *Relation
NewRelation creates a new relation for the given Core systems.
func (*Relation) Init ¶
func (rel *Relation) Init( aCore *Core, aFlags RelationFlags, bCore *Core, bFlags RelationFlags, )
Init initializes the entity relation; useful for embedding.
func (*Relation) Select ¶
Select creates a cursor with the given options applied. If none are given, TrueClause is used; so the default is basically "select all".
func (*Relation) Upsert ¶
func (rel *Relation) Upsert(cur Cursor, each func(*UpsertCursor)) (n, m int)
Upsert updates any relations that the given cursor iterates, and may insert new ones.
If the each function is nil, all matched relations are destroyed.
If the cursor is nil, then each is called exactly once with an empty cursor that it should use to create new relations.
type RelationFlags ¶
type RelationFlags uint32
RelationFlags specifies options for the A or B dimension in a Relation.
const ( // RelationCascadeDestroy causes destruction of an entity relation to // destroy related entities within the flagged dimension. RelationCascadeDestroy RelationFlags = 1 << iota )
type System ¶
System is a Core with an attached set of Proc-s; it is itself a Proc.
func (*System) AddProcFunc ¶
func (sys *System) AddProcFunc(fns ...func())
AddProcFunc adds processing fucntion(s) to the system.
type TraversalMode ¶
type TraversalMode uint8
TraversalMode represents a graph traversal mode.
const ( // TraverseDFS is Depth First Search traversal, starting from all matching // roots. TraverseDFS TraversalMode = 1 << iota // TraverseCoDFS is Reversed Depth First Search traversal, starting from // all matching leaves. TraverseCoDFS = traverseCo | TraverseDFS )
type TypeClause ¶
type TypeClause interface { CursorOpt // contains filtered or unexported methods }
TypeClause is a logical filter for ComponentTypes.
var ( // TrueClause matches any type. TrueClause TypeClause = constClause(true) // FalseClause matches no type. FalseClause TypeClause = constClause(false) )
func And ¶
func And(tcls ...TypeClause) TypeClause
And returns a type clause that matches only if all of its component clauses match.
func Not ¶
func Not(tcl TypeClause) TypeClause
Not returns a type clause that matches only the given clause does not match.
func Or ¶
func Or(tcls ...TypeClause) TypeClause
Or returns a type clause that matches only if any of its component clauses match.
type UpsertCursor ¶
type UpsertCursor struct { Cursor // contains filtered or unexported fields }
UpsertCursor allows inserting, updating, and deleting relations.
func (*UpsertCursor) Create ¶
func (uc *UpsertCursor) Create(r ComponentType, a, b Entity) Entity
Create a new relation, ignoring the current; when bulk loading data (no underlying Cursor), this is the prefered method.
func (*UpsertCursor) Emit ¶
func (uc *UpsertCursor) Emit(er ComponentType, ea, eb Entity) Entity
Emit a record, replacing the current, or inserting a new one if the current record has already been updated.
func (*UpsertCursor) Scan ¶
func (uc *UpsertCursor) Scan() bool
Scan advances the underlying cursor; but first, it destroys the last scanned relation if no updated record was emitted.