From 8d4dabd62e76fad943aca1487406884f0639c0cc Mon Sep 17 00:00:00 2001 From: Trevor Slocum Date: Mon, 22 Nov 2021 11:18:28 -0800 Subject: [PATCH] Add benchmarks and optimize Store all data in slices rather than maps. --- component.go | 40 +++++++-------- component_test.go | 49 ++++++++++++++++++ doc.go | 5 ++ entity.go | 43 ++++++++-------- entity_test.go | 60 ++++++++++++++++++++++ examples/twinstick/component/bullet.go | 4 +- examples/twinstick/component/position.go | 4 +- examples/twinstick/component/velocity.go | 4 +- examples/twinstick/component/weapon.go | 4 +- examples/twinstick/entity/bullet.go | 4 +- examples/twinstick/entity/player.go | 4 +- examples/twinstick/game/game.go | 4 +- examples/twinstick/system/draw_bullets.go | 8 +-- examples/twinstick/system/draw_player.go | 12 ++--- examples/twinstick/system/input_fire.go | 14 ++--- examples/twinstick/system/input_move.go | 12 ++--- examples/twinstick/system/input_profile.go | 10 ++-- examples/twinstick/system/movement.go | 22 ++++---- examples/twinstick/system/printinfo.go | 10 ++-- gohan.go | 31 +++++++---- system.go | 6 +-- 21 files changed, 236 insertions(+), 114 deletions(-) create mode 100644 component_test.go create mode 100644 entity_test.go diff --git a/component.go b/component.go index 372cc08..395f61c 100644 --- a/component.go +++ b/component.go @@ -4,10 +4,10 @@ import ( "sync" ) -var componentMutex sync.RWMutex +var componentMutex sync.Mutex // ComponentID is a component identifier. Each Component is assigned a unique ID -// via NextComponentID, and implements a ComponentID method which returns the ID. +// via NewComponentID, and implements a ComponentID method returning its ID. type ComponentID int // Component represents data for an entity, and how it interacts with the world. @@ -15,31 +15,32 @@ type Component interface { ComponentID() ComponentID } -var nextComponentID ComponentID +var maxComponentID ComponentID -// NextComponentID returns the next available ComponentID. -func NextComponentID() ComponentID { - componentMutex.Lock() - defer componentMutex.Unlock() +// NewComponentID returns the next available ComponentID. +func NewComponentID() ComponentID { + mutex.Lock() + defer mutex.Unlock() - nextComponentID++ - return nextComponentID -} + entityMutex.Lock() + defer entityMutex.Unlock() -// AddComponent adds a Component to an Entity. -func (entity EntityID) AddComponent(component Component) { componentMutex.Lock() defer componentMutex.Unlock() - if wasRemoved(entity) { - return + maxComponentID++ + + for i := Entity(1); i < maxEntityID; i++ { + gameComponents[i] = append(gameComponents[i], nil) } + return maxComponentID +} + +// AddComponent adds a Component to an Entity. +func (entity Entity) AddComponent(component Component) { componentID := component.ComponentID() - if gameComponents[entity] == nil { - gameComponents[entity] = make(map[ComponentID]interface{}) - } gameComponents[entity][componentID] = component entityMutex.Lock() @@ -48,10 +49,7 @@ func (entity EntityID) AddComponent(component Component) { } // Component gets a Component of an Entity. -func (entity EntityID) Component(componentID ComponentID) interface{} { - componentMutex.RLock() - defer componentMutex.RUnlock() - +func (entity Entity) Component(componentID ComponentID) interface{} { components := gameComponents[entity] if components == nil { return nil diff --git a/component_test.go b/component_test.go new file mode 100644 index 0000000..4027e60 --- /dev/null +++ b/component_test.go @@ -0,0 +1,49 @@ +package gohan + +import "testing" + +var testComponentID = NewComponentID() + +type testComponent struct { + X, Y float64 +} + +func (t testComponent) ComponentID() ComponentID { + return testComponentID +} + +func BenchmarkComponent(b *testing.B) { + e := NewEntity() + + e.AddComponent(&testComponent{ + X: 108, + Y: 0, + }) + + b.StopTimer() + b.ResetTimer() + b.ReportAllocs() + b.StartTimer() + + for i := 0; i < b.N; i++ { + _ = e.Component(testComponentID) + } +} + +func BenchmarkAddComponent(b *testing.B) { + e := NewEntity() + + c := &testComponent{ + X: 108, + Y: 0, + } + + b.StopTimer() + b.ResetTimer() + b.ReportAllocs() + b.StartTimer() + + for i := 0; i < b.N; i++ { + e.AddComponent(c) + } +} diff --git a/doc.go b/doc.go index 2e9ca2d..a29c390 100644 --- a/doc.go +++ b/doc.go @@ -1,6 +1,11 @@ /* Package gohan provides an Entity Component System framework for Ebiten. +An example game is available at /examples/twinstick which may be built by +executing the following command (in /examples/twinstick): + + go build -tags example . + Entity A general-purpose object, which consists of a unique ID, starting with 1. diff --git a/entity.go b/entity.go index 4585e1c..7ce915e 100644 --- a/entity.go +++ b/entity.go @@ -5,25 +5,36 @@ import ( "time" ) -// EntityID is an entity identifier. -type EntityID int +// Entity is an entity identifier. +type Entity int -var nextEntityID EntityID +var maxEntityID Entity var entityMutex sync.Mutex -// NextEntityID returns the next available EntityID. -func NextEntityID() EntityID { +// NewEntity returns a new (or previously removed and cleared) Entity. Because +// Gohan reuses removed Entity IDs, a previously removed ID may be returned. +func NewEntity() Entity { entityMutex.Lock() defer entityMutex.Unlock() - nextEntityID++ - allEntities = append(allEntities, nextEntityID) - return nextEntityID + if len(availableEntityIDs) > 0 { + id := availableEntityIDs[0] + availableEntityIDs = availableEntityIDs[1:] + allEntities = append(allEntities, id) + return id + } + + maxEntityID++ + allEntities = append(allEntities, maxEntityID) + gameComponents = append(gameComponents, make([]interface{}, maxComponentID+1)) + return maxEntityID } -// RemoveEntity removes the provided Entity, and all of its components. -func RemoveEntity(entity EntityID) { +// Remove removes the provided Entity's components, causing it to no longer be +// handled by any system. Because Gohan reuses removed EntityIDs, applications +// must also remove any internal references to the removed Entity. +func (entity Entity) Remove() { entityMutex.Lock() defer entityMutex.Unlock() @@ -36,18 +47,6 @@ func RemoveEntity(entity EntityID) { } } -func wasRemoved(entity EntityID) bool { - entityMutex.Lock() - defer entityMutex.Unlock() - - for _, e := range allEntities { - if e == entity { - return false - } - } - return true -} - var numEntities int var numEntitiesT time.Time diff --git a/entity_test.go b/entity_test.go new file mode 100644 index 0000000..802c875 --- /dev/null +++ b/entity_test.go @@ -0,0 +1,60 @@ +package gohan + +import ( + "testing" + "time" +) + +func TestActiveEntities(t *testing.T) { + t.Parallel() + + active := ActiveEntities() + if active != 0 { + t.Fatalf("expected 0 active entities, got %d", active) + } + + wait() + active = ActiveEntities() + if active != 0 { + t.Fatalf("expected 0 active entities, got %d", active) + } + + // Create entity. + e1 := NewEntity() + + wait() + active = ActiveEntities() + if active != 1 { + t.Fatalf("expected 1 active entities, got %d", active) + } + + // Create entity. + e2 := NewEntity() + + wait() + active = ActiveEntities() + if active != 2 { + t.Fatalf("expected 2 active entities, got %d", active) + } + + e1.Remove() + + wait() + active = ActiveEntities() + if active != 1 { + t.Fatalf("expected 1 active entities, got %d", active) + } + + e2.Remove() + + wait() + active = ActiveEntities() + if active != 0 { + t.Fatalf("expected 0 active entities, got %d", active) + } +} + +// wait causes the program to wait long enough to expire all duration-based caches. +func wait() { + time.Sleep(2 * time.Second) +} diff --git a/examples/twinstick/component/bullet.go b/examples/twinstick/component/bullet.go index f44beda..668d6ba 100644 --- a/examples/twinstick/component/bullet.go +++ b/examples/twinstick/component/bullet.go @@ -10,13 +10,13 @@ import ( type BulletComponent struct { } -var BulletComponentID = gohan.NextComponentID() +var BulletComponentID = gohan.NewComponentID() func (p *BulletComponent) ComponentID() gohan.ComponentID { return BulletComponentID } -func Bullet(e gohan.EntityID) *BulletComponent { +func Bullet(e gohan.Entity) *BulletComponent { c, ok := e.Component(BulletComponentID).(*BulletComponent) if !ok { return nil diff --git a/examples/twinstick/component/position.go b/examples/twinstick/component/position.go index ac4df65..25577f4 100644 --- a/examples/twinstick/component/position.go +++ b/examples/twinstick/component/position.go @@ -11,13 +11,13 @@ type PositionComponent struct { X, Y float64 } -var PositionComponentID = gohan.NextComponentID() +var PositionComponentID = gohan.NewComponentID() func (p *PositionComponent) ComponentID() gohan.ComponentID { return PositionComponentID } -func Position(e gohan.EntityID) *PositionComponent { +func Position(e gohan.Entity) *PositionComponent { c, ok := e.Component(PositionComponentID).(*PositionComponent) if !ok { return nil diff --git a/examples/twinstick/component/velocity.go b/examples/twinstick/component/velocity.go index c183f7d..fea1886 100644 --- a/examples/twinstick/component/velocity.go +++ b/examples/twinstick/component/velocity.go @@ -11,13 +11,13 @@ type VelocityComponent struct { X, Y float64 } -var VelocityComponentID = gohan.NextComponentID() +var VelocityComponentID = gohan.NewComponentID() func (c *VelocityComponent) ComponentID() gohan.ComponentID { return VelocityComponentID } -func Velocity(e gohan.EntityID) *VelocityComponent { +func Velocity(e gohan.Entity) *VelocityComponent { c, ok := e.Component(VelocityComponentID).(*VelocityComponent) if !ok { return nil diff --git a/examples/twinstick/component/weapon.go b/examples/twinstick/component/weapon.go index c719128..69a7e64 100644 --- a/examples/twinstick/component/weapon.go +++ b/examples/twinstick/component/weapon.go @@ -20,13 +20,13 @@ type WeaponComponent struct { BulletSpeed float64 } -var WeaponComponentID = gohan.NextComponentID() +var WeaponComponentID = gohan.NewComponentID() func (p *WeaponComponent) ComponentID() gohan.ComponentID { return WeaponComponentID } -func Weapon(e gohan.EntityID) *WeaponComponent { +func Weapon(e gohan.Entity) *WeaponComponent { c, ok := e.Component(WeaponComponentID).(*WeaponComponent) if !ok { return nil diff --git a/examples/twinstick/entity/bullet.go b/examples/twinstick/entity/bullet.go index 2a9170a..0aafc72 100644 --- a/examples/twinstick/entity/bullet.go +++ b/examples/twinstick/entity/bullet.go @@ -8,8 +8,8 @@ import ( "code.rocketnine.space/tslocum/gohan/examples/twinstick/component" ) -func NewBullet(x, y, xSpeed, ySpeed float64) gohan.EntityID { - bullet := gohan.NextEntityID() +func NewBullet(x, y, xSpeed, ySpeed float64) gohan.Entity { + bullet := gohan.NewEntity() bullet.AddComponent(&component.PositionComponent{ X: x, diff --git a/examples/twinstick/entity/player.go b/examples/twinstick/entity/player.go index 5824ed3..a3fa4ab 100644 --- a/examples/twinstick/entity/player.go +++ b/examples/twinstick/entity/player.go @@ -11,8 +11,8 @@ import ( "code.rocketnine.space/tslocum/gohan/examples/twinstick/component" ) -func NewPlayer() gohan.EntityID { - player := gohan.NextEntityID() +func NewPlayer() gohan.Entity { + player := gohan.NewEntity() // Set position to -1,-1 to indicate the player has not been assigned a // position yet. We will place the player in the center of the screen when diff --git a/examples/twinstick/game/game.go b/examples/twinstick/game/game.go index f2f2b73..5a16542 100644 --- a/examples/twinstick/game/game.go +++ b/examples/twinstick/game/game.go @@ -20,7 +20,7 @@ import ( type game struct { w, h int - player gohan.EntityID + player gohan.Entity op *ebiten.DrawImageOptions @@ -62,7 +62,7 @@ func (g *game) Layout(outsideWidth, outsideHeight int) (int, int) { g.w, g.h = w, h g.movementSystem.ScreenW, g.movementSystem.ScreenH = float64(w), float64(h) - position := g.player.Component(component.PositionComponentID).(*component.PositionComponent) + position := component.Position(g.player) if position.X == -1 && position.Y == -1 { position.X, position.Y = float64(g.w)/2-16, float64(g.h)/2-16 } diff --git a/examples/twinstick/system/draw_bullets.go b/examples/twinstick/system/draw_bullets.go index 0c98b4a..03086f7 100644 --- a/examples/twinstick/system/draw_bullets.go +++ b/examples/twinstick/system/draw_bullets.go @@ -20,19 +20,19 @@ func NewDrawBulletsSystem() *DrawBulletsSystem { } } -func (s *DrawBulletsSystem) Matches(entity gohan.EntityID) bool { +func (s *DrawBulletsSystem) Matches(entity gohan.Entity) bool { position := entity.Component(component.PositionComponentID) bullet := entity.Component(component.BulletComponentID) return position != nil && bullet != nil } -func (s *DrawBulletsSystem) Update(_ gohan.EntityID) error { +func (s *DrawBulletsSystem) Update(_ gohan.Entity) error { return gohan.ErrSystemWithoutUpdate } -func (s *DrawBulletsSystem) Draw(entity gohan.EntityID, screen *ebiten.Image) error { - position := entity.Component(component.PositionComponentID).(*component.PositionComponent) +func (s *DrawBulletsSystem) Draw(entity gohan.Entity, screen *ebiten.Image) error { + position := component.Position(entity) s.op.GeoM.Reset() s.op.GeoM.Translate(-16, -16) diff --git a/examples/twinstick/system/draw_player.go b/examples/twinstick/system/draw_player.go index 94b11ae..2e1a42f 100644 --- a/examples/twinstick/system/draw_player.go +++ b/examples/twinstick/system/draw_player.go @@ -11,27 +11,27 @@ import ( ) type drawPlayerSystem struct { - player gohan.EntityID + player gohan.Entity op *ebiten.DrawImageOptions } -func NewDrawPlayerSystem(player gohan.EntityID) *drawPlayerSystem { +func NewDrawPlayerSystem(player gohan.Entity) *drawPlayerSystem { return &drawPlayerSystem{ player: player, op: &ebiten.DrawImageOptions{}, } } -func (s *drawPlayerSystem) Matches(entity gohan.EntityID) bool { +func (s *drawPlayerSystem) Matches(entity gohan.Entity) bool { return entity == s.player } -func (s *drawPlayerSystem) Update(_ gohan.EntityID) error { +func (s *drawPlayerSystem) Update(_ gohan.Entity) error { return gohan.ErrSystemWithoutUpdate } -func (s *drawPlayerSystem) Draw(entity gohan.EntityID, screen *ebiten.Image) error { - position := entity.Component(component.PositionComponentID).(*component.PositionComponent) +func (s *drawPlayerSystem) Draw(entity gohan.Entity, screen *ebiten.Image) error { + position := component.Position(entity) s.op.GeoM.Reset() s.op.GeoM.Translate(position.X-16, position.Y-16) diff --git a/examples/twinstick/system/input_fire.go b/examples/twinstick/system/input_fire.go index 9d4e4ff..d034d8a 100644 --- a/examples/twinstick/system/input_fire.go +++ b/examples/twinstick/system/input_fire.go @@ -18,16 +18,16 @@ func angle(x1, y1, x2, y2 float64) float64 { } type fireInputSystem struct { - player gohan.EntityID + player gohan.Entity } -func NewFireInputSystem(player gohan.EntityID) *fireInputSystem { +func NewFireInputSystem(player gohan.Entity) *fireInputSystem { return &fireInputSystem{ player: player, } } -func (_ *fireInputSystem) Matches(e gohan.EntityID) bool { +func (_ *fireInputSystem) Matches(e gohan.Entity) bool { weapon := e.Component(component.WeaponComponentID) return weapon != nil @@ -48,14 +48,14 @@ func (s *fireInputSystem) fire(weapon *component.WeaponComponent, position *comp _ = bullet } -func (s *fireInputSystem) Update(_ gohan.EntityID) error { - weapon := s.player.Component(component.WeaponComponentID).(*component.WeaponComponent) +func (s *fireInputSystem) Update(_ gohan.Entity) error { + weapon := component.Weapon(s.player) if weapon.Ammo <= 0 { return nil } - position := s.player.Component(component.PositionComponentID).(*component.PositionComponent) + position := component.Position(s.player) if ebiten.IsMouseButtonPressed(ebiten.MouseButtonLeft) { cursorX, cursorY := ebiten.CursorPosition() @@ -99,6 +99,6 @@ func (s *fireInputSystem) Update(_ gohan.EntityID) error { return nil } -func (_ *fireInputSystem) Draw(_ gohan.EntityID, _ *ebiten.Image) error { +func (_ *fireInputSystem) Draw(_ gohan.Entity, _ *ebiten.Image) error { return gohan.ErrSystemWithoutDraw } diff --git a/examples/twinstick/system/input_move.go b/examples/twinstick/system/input_move.go index 0c4a458..c69e037 100644 --- a/examples/twinstick/system/input_move.go +++ b/examples/twinstick/system/input_move.go @@ -10,21 +10,21 @@ import ( ) type movementInputSystem struct { - player gohan.EntityID + player gohan.Entity } -func NewMovementInputSystem(player gohan.EntityID) *movementInputSystem { +func NewMovementInputSystem(player gohan.Entity) *movementInputSystem { return &movementInputSystem{ player: player, } } -func (s *movementInputSystem) Matches(e gohan.EntityID) bool { +func (s *movementInputSystem) Matches(e gohan.Entity) bool { return e == s.player } -func (s *movementInputSystem) Update(e gohan.EntityID) error { - velocity := s.player.Component(component.VelocityComponentID).(*component.VelocityComponent) +func (s *movementInputSystem) Update(e gohan.Entity) error { + velocity := component.Velocity(s.player) if ebiten.IsKeyPressed(ebiten.KeyA) { velocity.X -= 0.5 if velocity.X < -5 { @@ -52,6 +52,6 @@ func (s *movementInputSystem) Update(e gohan.EntityID) error { return nil } -func (s *movementInputSystem) Draw(_ gohan.EntityID, _ *ebiten.Image) error { +func (s *movementInputSystem) Draw(_ gohan.Entity, _ *ebiten.Image) error { return gohan.ErrSystemWithoutDraw } diff --git a/examples/twinstick/system/input_profile.go b/examples/twinstick/system/input_profile.go index ab3b251..149fb26 100644 --- a/examples/twinstick/system/input_profile.go +++ b/examples/twinstick/system/input_profile.go @@ -15,21 +15,21 @@ import ( ) type profileSystem struct { - player gohan.EntityID + player gohan.Entity cpuProfile *os.File } -func NewProfileSystem(player gohan.EntityID) *profileSystem { +func NewProfileSystem(player gohan.Entity) *profileSystem { return &profileSystem{ player: player, } } -func (s *profileSystem) Matches(e gohan.EntityID) bool { +func (s *profileSystem) Matches(e gohan.Entity) bool { return e == s.player } -func (s *profileSystem) Update(e gohan.EntityID) error { +func (s *profileSystem) Update(e gohan.Entity) error { if ebiten.IsKeyPressed(ebiten.KeyControl) && inpututil.IsKeyJustPressed(ebiten.KeyP) { if s.cpuProfile == nil { log.Println("CPU profiling started...") @@ -60,6 +60,6 @@ func (s *profileSystem) Update(e gohan.EntityID) error { return nil } -func (s *profileSystem) Draw(_ gohan.EntityID, _ *ebiten.Image) error { +func (s *profileSystem) Draw(_ gohan.Entity, _ *ebiten.Image) error { return gohan.ErrSystemWithoutDraw } diff --git a/examples/twinstick/system/movement.go b/examples/twinstick/system/movement.go index 38dcbc1..b2c3d56 100644 --- a/examples/twinstick/system/movement.go +++ b/examples/twinstick/system/movement.go @@ -13,23 +13,23 @@ type MovementSystem struct { ScreenW, ScreenH float64 } -func (_ *MovementSystem) Matches(entity gohan.EntityID) bool { - position := entity.Component(component.PositionComponentID) - velocity := entity.Component(component.VelocityComponentID) +func (_ *MovementSystem) Matches(entity gohan.Entity) bool { + position := component.Position(entity) + velocity := component.Velocity(entity) return position != nil && velocity != nil } -func (s *MovementSystem) Update(entity gohan.EntityID) error { - position := entity.Component(component.PositionComponentID).(*component.PositionComponent) - velocity := entity.Component(component.VelocityComponentID).(*component.VelocityComponent) +func (s *MovementSystem) Update(entity gohan.Entity) error { + position := component.Position(entity) + velocity := component.Velocity(entity) bullet := entity.Component(component.BulletComponentID) // Check for collision. if position.X+velocity.X < 16 { if bullet != nil { - gohan.RemoveEntity(entity) + entity.Remove() return nil } @@ -37,7 +37,7 @@ func (s *MovementSystem) Update(entity gohan.EntityID) error { velocity.X = 0 } else if position.X+velocity.X > s.ScreenW-16 { if bullet != nil { - gohan.RemoveEntity(entity) + entity.Remove() return nil } @@ -46,7 +46,7 @@ func (s *MovementSystem) Update(entity gohan.EntityID) error { } if position.Y+velocity.Y < 16 { if bullet != nil { - gohan.RemoveEntity(entity) + entity.Remove() return nil } @@ -54,7 +54,7 @@ func (s *MovementSystem) Update(entity gohan.EntityID) error { velocity.Y = 0 } else if position.Y+velocity.Y > s.ScreenH-16 { if bullet != nil { - gohan.RemoveEntity(entity) + entity.Remove() return nil } @@ -72,6 +72,6 @@ func (s *MovementSystem) Update(entity gohan.EntityID) error { return nil } -func (_ *MovementSystem) Draw(_ gohan.EntityID, _ *ebiten.Image) error { +func (_ *MovementSystem) Draw(_ gohan.Entity, _ *ebiten.Image) error { return gohan.ErrSystemWithoutDraw } diff --git a/examples/twinstick/system/printinfo.go b/examples/twinstick/system/printinfo.go index 816bf59..393ab9b 100644 --- a/examples/twinstick/system/printinfo.go +++ b/examples/twinstick/system/printinfo.go @@ -14,10 +14,10 @@ import ( type printInfoSystem struct { img *ebiten.Image op *ebiten.DrawImageOptions - player gohan.EntityID + player gohan.Entity } -func NewPrintInfoSystem(player gohan.EntityID) *printInfoSystem { +func NewPrintInfoSystem(player gohan.Entity) *printInfoSystem { p := &printInfoSystem{ img: ebiten.NewImage(200, 100), op: &ebiten.DrawImageOptions{}, @@ -27,15 +27,15 @@ func NewPrintInfoSystem(player gohan.EntityID) *printInfoSystem { return p } -func (s *printInfoSystem) Matches(e gohan.EntityID) bool { +func (s *printInfoSystem) Matches(e gohan.Entity) bool { return e == s.player } -func (s *printInfoSystem) Update(_ gohan.EntityID) error { +func (s *printInfoSystem) Update(_ gohan.Entity) error { return gohan.ErrSystemWithoutUpdate } -func (s *printInfoSystem) Draw(_ gohan.EntityID, screen *ebiten.Image) error { +func (s *printInfoSystem) Draw(_ gohan.Entity, screen *ebiten.Image) error { s.img.Clear() ebitenutil.DebugPrint(s.img, fmt.Sprintf("KEY WASD+MOUSE\nENT %d\nUPD %d\nDRA %d\nTPS %0.0f\nFPS %0.0f", gohan.ActiveEntities(), gohan.UpdatedEntities(), gohan.DrawnEntities(), ebiten.CurrentTPS(), ebiten.CurrentFPS())) screen.DrawImage(s.img, s.op) diff --git a/gohan.go b/gohan.go index ba58a57..9663679 100644 --- a/gohan.go +++ b/gohan.go @@ -12,15 +12,19 @@ import ( ) var ( - gameComponents = make(map[EntityID]map[ComponentID]interface{}) + gameComponents [][]interface{} - allEntities []EntityID + allEntities []Entity - modifiedEntities []EntityID - removedEntities []EntityID + modifiedEntities []Entity + removedEntities []Entity + + // availableEntityIDs is the set of EntityIDs available because they were + // removed from the game. + availableEntityIDs []Entity gameSystems []System - gameSystemEntities [][]EntityID // Slice of entities matching each system. + gameSystemEntities [][]Entity // Slice of entities matching each system. gameSystemReceivesUpdate []bool gameSystemReceivesDraw []bool @@ -39,6 +43,9 @@ var ( ) func init() { + // Pad slices to match IDs starting with 1. + gameComponents = append(gameComponents, nil) + debugEnv := os.Getenv("GOHAN_DEBUG") debugEnv = strings.TrimSpace(debugEnv) debugEnv = strings.ToLower(debugEnv) @@ -59,7 +66,7 @@ func attachEntitiesToSystem(system System) { // This function is always called on a newly added system. systemID := len(gameSystemEntities) - 1 - for entity := EntityID(0); entity < nextEntityID; entity++ { + for entity := Entity(0); entity < maxEntityID; entity++ { if system.Matches(entity) { gameSystemEntities[systemID] = append(gameSystemEntities[systemID], entity) @@ -99,12 +106,9 @@ func propagateEntityChanges() { defer entityMutex.Unlock() for _, entity := range removedEntities { - delete(gameComponents, entity) - + // Remove from attached systems. REMOVED: for i := range gameSystemEntities { - delete(gameComponents, entity) - for j, e := range gameSystemEntities[i] { if e == entity { gameSystemEntities[i] = append(gameSystemEntities[i][:j], gameSystemEntities[i][j+1:]...) @@ -112,7 +116,14 @@ func propagateEntityChanges() { } } } + + // Remove components. + gameComponents[entity] = make([]interface{}, maxComponentID+1) } + + // Mark EntityIDs as available. + availableEntityIDs = append(availableEntityIDs, removedEntities...) + removedEntities = nil for _, entity := range modifiedEntities { diff --git a/system.go b/system.go index d073c0f..945f099 100644 --- a/system.go +++ b/system.go @@ -14,13 +14,13 @@ import ( // See ErrSystemWithoutUpdate and ErrSystemWithoutDraw. type System interface { // Matches returns whether the provided entity is handled by this system. - Matches(entity EntityID) bool + Matches(entity Entity) bool // Update is called once for each matching entity each time the game state is updated. - Update(entity EntityID) error + Update(entity Entity) error // Draw is called once for each matching entity each time the game is drawn to the screen. - Draw(entity EntityID, screen *ebiten.Image) error + Draw(entity Entity, screen *ebiten.Image) error } // Special error values.