ecs

package
v0.0.0-...-e2c65c2 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jan 16, 2018 License: Apache-2.0 Imports: 2 Imported by: 0

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

Examples

Constants

This section is empty.

Variables

View Source
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

func (co *Core) Cap() int

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) Clear

func (co *Core) Clear()

Clear destroys all active entities.

func (*Core) Deref

func (co *Core) Deref(e Entity) EntityID

Deref unpacks an Entity reference, returning its ID; it panics if the Core doesn't own the Entity.

func (*Core) Empty

func (co *Core) Empty() bool

Empty returns true only if there are no active entities.

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) Len

func (co *Core) Len() int

Len counts how many active entities exist.

func (*Core) Ref

func (co *Core) Ref(id EntityID) Entity

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.

func (*Core) Type

func (co *Core) Type(id EntityID) ComponentType

Type returns the entity's type.

type Cursor

type Cursor interface {
	Scan() bool
	Count() int
	R() Entity
	A() Entity
	B() Entity
}

Cursor iterates through a Relation.

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.

func InA

func InA(ids ...EntityID) CursorOpt

InA returns a cursor option that limits the cursor to relations involving one or more given A-side entities.

func InB

func InB(ids ...EntityID) CursorOpt

InB returns a cursor option that limits the cursor to relations involving one or more given A-side entities.

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

func (ent Entity) ID() EntityID

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) String

func (ent Entity) String() string

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

type Filter func(Cursor) bool

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 Proc

type Proc interface {
	Process()
}

Proc is a piece of domain logic attached to a Core.

type ProcFunc

type ProcFunc func()

ProcFunc is a convenience for implementing Proc around an arbitrary void function.

func (ProcFunc) Process

func (f ProcFunc) Process()

Process calls the wrapped 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) A

func (rel *Relation) A(ent Entity) Entity

A returns a reference to the A-side entity for the given relation entity.

func (*Relation) B

func (rel *Relation) B(ent Entity) Entity

B returns a reference to the B-side entity for the given relation entity.

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

func (rel *Relation) Select(opts ...CursorOpt) Cursor

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

type System struct {
	Core
	Procs []Proc
}

System is a Core with an attached set of Proc-s; it is itself a Proc.

func (*System) AddProc

func (sys *System) AddProc(procs ...Proc)

AddProc adds processor(s) to the system.

func (*System) AddProcFunc

func (sys *System) AddProcFunc(fns ...func())

AddProcFunc adds processing fucntion(s) to the system.

func (*System) Process

func (sys *System) Process()

Process calls each Proc.

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.

Directories

Path Synopsis
Package time provides a Facility for managing time in an ecs.System.
Package time provides a Facility for managing time in an ecs.System.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL