Add World.Preallocate
This function creates and immediately removes the specified number of entities.
This commit is contained in:
parent
21d6f53dcc
commit
3c7785e5e5
|
@ -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)
|
||||
|
|
22
component.go
22
component.go
|
@ -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)
|
||||
}
|
||||
|
||||
|
|
15
context.go
15
context.go
|
@ -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
14
doc.go
|
@ -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
|
||||
|
|
15
entity.go
15
entity.go
|
@ -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 {
|
||||
|
|
|
@ -53,6 +53,8 @@ func NewGame() (*game, error) {
|
|||
|
||||
asset.ImgWhiteSquare.Fill(color.White)
|
||||
|
||||
world.World.Preallocate(10000)
|
||||
|
||||
return g, nil
|
||||
}
|
||||
|
||||
|
|
10
system.go
10
system.go
|
@ -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
136
world.go
|
@ -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])
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue