Add World.Preallocate

This function creates and immediately removes the specified number of entities.
This commit is contained in:
Trevor Slocum 2021-12-09 19:39:21 -08:00
parent 21d6f53dcc
commit 3c7785e5e5
8 changed files with 141 additions and 79 deletions

View File

@ -12,8 +12,8 @@ for [Ebiten](https://ebiten.org)
Documentation is available via [godoc](https://docs.rocketnine.space/code.rocketnine.space/tslocum/gohan).
An [example game](https://code.rocketnine.space/tslocum/gohan/src/branch/main/examples/twinstick)
is included. See godoc for build instructions.
An [example game](https://rocketnine.itch.io/gohan-twinstick) is included at
`/examples/twinstick`. See godoc for build instructions.
## Support
@ -22,5 +22,5 @@ Please share issues and suggestions [here](https://code.rocketnine.space/tslocum
## List of games powered by Gohan
- **Monovania** is a Metroidvania-style platform game.
- [Play on itch.io](https://rocketnine.itch.io/monovania)
- [Play game](https://rocketnine.itch.io/monovania?secret=monovania)
- [View source code](https://code.rocketnine.space/tslocum/monovania)

View File

@ -1,11 +1,5 @@
package gohan
import (
"sync"
)
var componentMutex sync.Mutex
// ComponentID is a component identifier. Each Component is assigned a unique ID
// via World.NewComponentID, and implements a ComponentID method returning its ID.
type ComponentID int
@ -17,11 +11,11 @@ type Component interface {
// NewComponentID returns the next available ComponentID.
func (w *World) NewComponentID() ComponentID {
entityMutex.Lock()
defer entityMutex.Unlock()
w.entityMutex.Lock()
defer w.entityMutex.Unlock()
componentMutex.Lock()
defer componentMutex.Unlock()
w.componentMutex.Lock()
defer w.componentMutex.Unlock()
w.maxComponentID++
@ -34,15 +28,15 @@ func (w *World) NewComponentID() ComponentID {
// AddComponent adds a Component to an Entity.
func (w *World) AddComponent(entity Entity, component Component) {
componentMutex.Lock()
defer componentMutex.Unlock()
w.componentMutex.Lock()
defer w.componentMutex.Unlock()
componentID := component.ComponentID()
w.components[entity][componentID] = component
entityMutex.Lock()
defer entityMutex.Unlock()
w.entityMutex.Lock()
defer w.entityMutex.Unlock()
w.modifiedEntities = append(w.modifiedEntities, entity)
}

View File

@ -8,31 +8,32 @@ import "log"
type Context struct {
Entity Entity
s int // System index.
c []ComponentID
w *World
allowed []ComponentID
components []interface{}
systemIndex int
world *World
}
// Component gets a Component of the currently handled Entity.
func (ctx *Context) Component(componentID ComponentID) interface{} {
if debug != 0 {
var found bool
for _, id := range ctx.c {
for _, id := range ctx.allowed {
if id == componentID {
found = true
break
}
}
if !found {
log.Panicf("illegal component access: component %d is not queried by %s", componentID, ctx.w.systemName(ctx.s))
log.Panicf("illegal component access: component %d is not needed or used by %s", componentID, ctx.world.systemName(ctx.systemIndex))
}
}
return ctx.w.Component(ctx.Entity, componentID)
return ctx.components[componentID]
}
// RemoveEntity removes the currently handled Entity's components, causing it
// to no longer be handled by any system. Because Gohan reuses removed Entity
// IDs, applications must also remove any other references to the removed Entity.
func (ctx *Context) RemoveEntity() bool {
return ctx.w.RemoveEntity(ctx.Entity)
return ctx.world.RemoveEntity(ctx.Entity)
}

14
doc.go
View File

@ -1,7 +1,7 @@
/*
Package gohan provides an Entity Component System framework for Ebiten.
An example game is available at /examples/twinstick which may be built by
An example game is available at /examples/twinstick, which may be built by
executing the following command (in /examples/twinstick):
go build -tags example .
@ -25,7 +25,7 @@ Component Design Guidelines
Components are located in a separate package, typically named component. They
should be public (start with an uppercase letter) and may have any number of
publicly accessible data fields. They should not have any logic (i.e. game code)
within them, as all logic should be implemented within a System.
within them, as all logic should be implemented within a system.
Rather than accessing components via Context.Component, using helper functions
(such as the following) helps to reduce code verbosity.
@ -50,11 +50,11 @@ call, or else the application will encounter race conditions.
Environment Variables
Running an application with the environment variable GOHAN_DEBUG set to 1
enables verification of Systems' access to Components. This verification is
enables verification of systems' access to components. This verification is
disabled by default for performance reasons. While verification is enabled,
if a System attempts to access a Component which is not included in the
System's Needs or Uses, the application will panic and print information about
the illegal Component access. Setting GOHAN_DEBUG to 1 will also enable
printing System registration events and statistics.
if a system attempts to access a component which is not included in the
system's Needs or Uses, the application will panic and print information about
the illegal component access. Setting GOHAN_DEBUG to 1 will also enable
printing system registration events and statistics.
*/
package gohan

View File

@ -1,20 +1,17 @@
package gohan
import (
"sync"
"time"
)
// Entity is an entity identifier.
type Entity int
var entityMutex sync.Mutex
// NewEntity returns a new (or previously removed and cleared) Entity. Because
// Gohan reuses removed Entity IDs, a previously removed ID may be returned.
func (w *World) NewEntity() Entity {
entityMutex.Lock()
defer entityMutex.Unlock()
w.entityMutex.Lock()
defer w.entityMutex.Unlock()
if len(w.availableEntities) > 0 {
id := w.availableEntities[0]
@ -26,6 +23,10 @@ func (w *World) NewEntity() Entity {
w.maxEntityID++
w.allEntities = append(w.allEntities, w.maxEntityID)
w.components = append(w.components, make([]interface{}, w.maxComponentID+1))
for i := range w.systems {
w.systemComponents[i] = append(w.systemComponents[i], make([]interface{}, w.maxComponentID+1))
}
return w.maxEntityID
}
@ -33,8 +34,8 @@ func (w *World) NewEntity() Entity {
// longer be handled by any system. Because Gohan reuses removed EntityIDs,
// applications must also remove any internal references to the removed Entity.
func (w *World) RemoveEntity(entity Entity) bool {
entityMutex.Lock()
defer entityMutex.Unlock()
w.entityMutex.Lock()
defer w.entityMutex.Unlock()
for i, e := range w.allEntities {
if e == entity {

View File

@ -53,6 +53,8 @@ func NewGame() (*game, error) {
asset.ImgWhiteSquare.Fill(color.White)
world.World.Preallocate(10000)
return g, nil
}

View File

@ -13,15 +13,15 @@ import (
//
// See the special error values ErrSystemWithoutUpdate and ErrSystemWithoutDraw.
type System interface {
// Needs returns a list of Components (specified by ID) which are required
// Needs returns a list of components (specified by ID) which are required
// for an Entity to be handled by the System. When the game is running,
// matching entities will be passed to the Update and Draw methods.
Needs() []ComponentID
// Uses returns a list of Components (specified by ID) which are used by
// the System, in addition to any required Components. Because required
// Components are automatically included in this list, Uses should only
// return Components which are not also returned by Needs.
// Uses returns a list of components (specified by ID) which are used by
// the System, in addition to any required components. Because required
// components are automatically included in this list, Uses should only
// return components which are not also returned by Needs.
Uses() []ComponentID
// Update is called once for each matching Entity each time the game state is updated.

136
world.go
View File

@ -31,7 +31,7 @@ func init() {
}
}
// World represents a collection of Entities, Components and Systems.
// World represents a collection of Entities, components and Systems.
type World struct {
maxEntityID Entity
@ -50,10 +50,12 @@ type World struct {
// removed from the game.
availableEntities []Entity
systems []System
systemEntities [][]Entity // Slice of entities matching each system.
systemNeeds [][]ComponentID // Slice of Components needed by each system.
systemUses [][]ComponentID // Slice of Components used by each system.
systems []System
systemEntities [][]Entity // Slice of entities matching each system.
systemNeeds [][]ComponentID // Slice of ComponentIDs needed by each system.
systemUses [][]ComponentID // Slice of ComponentIDs used by each system.
systemComponentIDs [][]ComponentID // Slice of ComponentIDs needed or used by each system.
systemComponents [][][]interface{} // Slice of components for matching entities.
systemReceivesUpdate []bool
systemReceivesDraw []bool
@ -70,6 +72,9 @@ type World struct {
ctx *Context
entityMutex sync.Mutex
componentMutex sync.Mutex
sync.Mutex
}
@ -82,7 +87,7 @@ func NewWorld() *World {
}
w.ctx = &Context{
w: w,
world: w,
}
// Pad slices to match IDs starting with 1.
@ -91,38 +96,38 @@ func NewWorld() *World {
return w
}
func (w *World) attachEntitiesToSystem(systemIndex int) {
components := w.systemNeeds[systemIndex]
ATTACH:
for entity := Entity(1); entity <= w.maxEntityID; entity++ {
// Skip Entities missing required Components.
for _, c := range components {
if w.Component(entity, c) == nil {
continue ATTACH
}
}
w.systemEntities[systemIndex] = append(w.systemEntities[systemIndex], entity)
if debug > 1 {
log.Printf("Attached entity %d to %s.", entity, w.systemName(systemIndex))
}
}
}
// AddSystem registers a system to start receiving Update and Draw calls.
func (w *World) AddSystem(system System) {
w.Lock()
defer w.Unlock()
systemIndex := len(w.systems)
w.systems = append(w.systems, system)
w.systemNeeds = append(w.systemNeeds, system.Needs())
w.systemNeeds = append(w.systemNeeds, uniqueComponentIDs(system.Needs()))
w.systemUses = append(w.systemUses, nil)
for _, componentID := range uniqueComponentIDs(system.Uses()) {
var found bool
for _, neededComponentID := range w.systemNeeds[systemIndex] {
if neededComponentID == componentID {
found = true
break
}
}
if found {
continue
}
w.systemUses[systemIndex] = append(w.systemUses[systemIndex], componentID)
}
w.systemComponentIDs = append(w.systemComponentIDs, append(w.systemNeeds[systemIndex], w.systemUses[systemIndex]...))
w.systemReceivesUpdate = append(w.systemReceivesUpdate, true)
w.systemReceivesDraw = append(w.systemReceivesDraw, true)
w.systemEntities = append(w.systemEntities, nil)
w.systemComponents = append(w.systemComponents, nil)
w.attachEntitiesToSystem(len(w.systems) - 1)
w.entityMutex.Lock()
defer w.entityMutex.Unlock()
w.modifiedEntities = append(w.modifiedEntities, w.allEntities...)
}
/*
@ -147,11 +152,12 @@ func (w *World) systemName(i int) string {
}
func (w *World) updateSystem(i int) (int, error) {
w.ctx.s = i
w.ctx.c = w.systemNeeds[i]
w.ctx.systemIndex = i
w.ctx.allowed = w.systemComponentIDs[i]
updated := 0
for _, entity := range w.systemEntities[i] {
w.ctx.Entity = entity
w.ctx.components = w.systemComponents[i][entity]
err := w.systems[i].Update(w.ctx)
if err != nil {
if err == ErrSystemWithoutUpdate {
@ -174,6 +180,7 @@ func (w *World) _handleRemovedEntities() {
for j, e := range w.systemEntities[i] {
if e == entity {
w.systemEntities[i] = append(w.systemEntities[i][:j], w.systemEntities[i][j+1:]...)
w.systemComponents[i][entity] = w.systemComponents[i][entity][:0] // TODO Could this lead to memory issues?
continue REMOVED
}
}
@ -186,7 +193,13 @@ func (w *World) _handleRemovedEntities() {
w.removedEntities = w.removedEntities[:0]
}
// _handleModifiedEntities handles changes to entity components by attaching
// and detaching modified entities from affected systems.
func (w *World) _handleModifiedEntities() {
if len(w.modifiedEntities) == 0 {
return
}
for _, entity := range w.modifiedEntities {
if w.handledModifiedEntities[entity] {
continue
@ -194,6 +207,15 @@ func (w *World) _handleModifiedEntities() {
w.handledModifiedEntities[entity] = true
for i := range w.systems {
l := len(w.systemComponents[i])
if l != int(w.maxEntityID+1) {
w.systemComponents[i] = append(w.systemComponents[i], make([][]interface{}, int(w.maxEntityID+1)-l)...)
}
if len(w.systemComponents[i][entity]) != int(w.maxComponentID+1) {
w.systemComponents[i][entity] = make([]interface{}, w.maxComponentID+1)
}
systemEntityIndex := -1
for j, systemEntity := range w.systemEntities[i] {
if systemEntity == entity {
@ -203,13 +225,21 @@ func (w *World) _handleModifiedEntities() {
}
var skip bool
for _, c := range w.systemNeeds[i] {
if w.Component(entity, c) == nil {
for _, componentID := range w.systemNeeds[i] {
c := w.Component(entity, componentID)
if c == nil {
skip = true
break
}
w.systemComponents[i][entity][componentID] = c
}
if !skip {
for _, componentID := range w.systemUses[i] {
c := w.Component(entity, componentID)
w.systemComponents[i][entity][componentID] = c
}
if systemEntityIndex != -1 {
// Already attached.
continue
@ -223,6 +253,8 @@ func (w *World) _handleModifiedEntities() {
} else if systemEntityIndex != -1 {
// Detach from system.
w.systemEntities[i] = append(w.systemEntities[i][:systemEntityIndex], w.systemEntities[i][systemEntityIndex+1:]...)
w.systemComponents[i][entity] = w.systemComponents[i][entity][:0] // TODO Could this lead to memory issues?
}
}
}
@ -235,8 +267,8 @@ func (w *World) _handleModifiedEntities() {
}
func (w *World) propagateEntityChanges() {
entityMutex.Lock()
defer entityMutex.Unlock()
w.entityMutex.Lock()
defer w.entityMutex.Unlock()
w._handleRemovedEntities()
w._handleModifiedEntities()
@ -290,11 +322,12 @@ func (w *World) CurrentUpdates() int {
}
func (w *World) drawSystem(i int, screen *ebiten.Image) (int, error) {
w.ctx.s = i
w.ctx.c = w.systemNeeds[i]
w.ctx.systemIndex = i
w.ctx.allowed = w.systemComponentIDs[i]
var drawn int
for _, entity := range w.systemEntities[i] {
w.ctx.Entity = entity
w.ctx.components = w.systemComponents[i][entity]
err := w.systems[i].Draw(w.ctx, screen)
if err != nil {
if err == ErrSystemWithoutDraw {
@ -356,3 +389,34 @@ func (w *World) CurrentDraws() int {
}
return w.systemDrawnEntitiesV
}
func uniqueComponentIDs(v []ComponentID) []ComponentID {
var list []ComponentID
keys := make(map[ComponentID]bool)
for _, entry := range v {
if _, value := keys[entry]; !value {
keys[entry] = true
list = append(list, entry)
}
}
return list
}
// Preallocate creates and immediately removes the specified number of entities.
// Because Gohan reuses removed entities, this has the effect of pre-allocating
// the memory used later to create entities normally. Pre-allocating enough
// entities to run your application after its systems has been added, but
// before any entities are created, will provide the greatest performance boost.
func (w *World) Preallocate(entities int) {
if len(w.availableEntities) >= entities {
return
}
e := make([]Entity, entities)
for i := 0; i < entities; i++ {
e[i] = w.NewEntity()
}
for i := 0; i < entities; i++ {
w.RemoveEntity(e[i])
}
}