From a820927a43beaf74eb8b35a05a98ef694096f372 Mon Sep 17 00:00:00 2001 From: Trevor Slocum Date: Sun, 21 Nov 2021 18:48:54 -0800 Subject: [PATCH] Add ActiveEntities, UpdatedEntities and DrawnEntities These functions return information particularly useful when debugging and profiling an application. --- component.go | 51 +++---- doc.go | 30 ++++ entity.go | 52 ++++++- examples/twinstick/component/bullet.go | 8 ++ examples/twinstick/component/position.go | 8 ++ examples/twinstick/component/velocity.go | 8 ++ examples/twinstick/component/weapon.go | 8 ++ examples/twinstick/entity/bullet.go | 2 +- examples/twinstick/entity/player.go | 2 +- examples/twinstick/game/game.go | 14 +- examples/twinstick/main.go | 2 +- examples/twinstick/system/draw_bullets.go | 4 +- examples/twinstick/system/draw_player.go | 4 +- examples/twinstick/system/input_fire.go | 18 ++- examples/twinstick/system/input_move.go | 2 +- examples/twinstick/system/input_profile.go | 65 +++++++++ examples/twinstick/system/movement.go | 48 +++++-- examples/twinstick/system/printinfo.go | 4 +- gohan.go | 157 +++++++++++++++++++-- 19 files changed, 408 insertions(+), 79 deletions(-) create mode 100644 examples/twinstick/system/input_profile.go diff --git a/component.go b/component.go index 8bd7070..372cc08 100644 --- a/component.go +++ b/component.go @@ -1,6 +1,10 @@ package gohan -import "fmt" +import ( + "sync" +) + +var componentMutex sync.RWMutex // ComponentID is a component identifier. Each Component is assigned a unique ID // via NextComponentID, and implements a ComponentID method which returns the ID. @@ -15,38 +19,22 @@ var nextComponentID ComponentID // NextComponentID returns the next available ComponentID. func NextComponentID() ComponentID { - id := nextComponentID + componentMutex.Lock() + defer componentMutex.Unlock() + nextComponentID++ - return id -} - -func (entity EntityID) propagateChanges() { - for i, system := range gameSystems { - systemEntityIndex := -1 - for j, systemEntity := range gameSystemEntities[i] { - if systemEntity == entity { - systemEntityIndex = j - break - } - } - - if system.Matches(entity) { - if systemEntityIndex != -1 { - // Already attached. - continue - } - - gameSystemEntities[i] = append(gameSystemEntities[i], entity) - print(fmt.Sprintf("Attached entity %d to system %d.", entity, i)) - } else if systemEntityIndex != -1 { - // Detach from system. - gameSystemEntities[i] = append(gameSystemEntities[i][:systemEntityIndex], gameSystemEntities[i][systemEntityIndex+1:]...) - } - } + return nextComponentID } // AddComponent adds a Component to an Entity. func (entity EntityID) AddComponent(component Component) { + componentMutex.Lock() + defer componentMutex.Unlock() + + if wasRemoved(entity) { + return + } + componentID := component.ComponentID() if gameComponents[entity] == nil { @@ -54,11 +42,16 @@ func (entity EntityID) AddComponent(component Component) { } gameComponents[entity][componentID] = component - entity.propagateChanges() + entityMutex.Lock() + defer entityMutex.Unlock() + modifiedEntities = append(modifiedEntities, entity) } // Component gets a Component of an Entity. func (entity EntityID) Component(componentID ComponentID) interface{} { + componentMutex.RLock() + defer componentMutex.RUnlock() + components := gameComponents[entity] if components == nil { return nil diff --git a/doc.go b/doc.go index 0dd60f3..2e9ca2d 100644 --- a/doc.go +++ b/doc.go @@ -1,4 +1,34 @@ /* Package gohan provides an Entity Component System framework for Ebiten. + +Entity + +A general-purpose object, which consists of a unique ID, starting with 1. + +Component + +The raw data for one aspect of an object, and how it interacts with the world. +Each component is assigned a unique ID, starting with 1. + +System + +Each system runs continuously, performing actions on every Entity that fits +each systems' set of required matching components. + +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. + +System Design Guidelines + +Systems are located in a separate package, typically named system. They should +be private (start with a lowercase letter) and offer an instantiation function +named as follows: NewSystemNameHere(). Data should be stored within components +attached to one or more entities, rather than within the systems themselves. +References to components must not be maintained outside each Update and Draw +call, or else the application will encounter race conditions. */ package gohan diff --git a/entity.go b/entity.go index b0ff94d..4585e1c 100644 --- a/entity.go +++ b/entity.go @@ -1,13 +1,61 @@ package gohan +import ( + "sync" + "time" +) + // EntityID is an entity identifier. type EntityID int var nextEntityID EntityID +var entityMutex sync.Mutex + // NextEntityID returns the next available EntityID. func NextEntityID() EntityID { - entityID := nextEntityID + entityMutex.Lock() + defer entityMutex.Unlock() + nextEntityID++ - return entityID + allEntities = append(allEntities, nextEntityID) + return nextEntityID +} + +// RemoveEntity removes the provided Entity, and all of its components. +func RemoveEntity(entity EntityID) { + entityMutex.Lock() + defer entityMutex.Unlock() + + for i, e := range allEntities { + if e == entity { + allEntities = append(allEntities[:i], allEntities[i+1:]...) + removedEntities = append(removedEntities, e) + return + } + } +} + +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 + +// ActiveEntities returns the number of currently active entities. +func ActiveEntities() int { + if time.Since(numEntitiesT) >= time.Second { + numEntities = len(allEntities) + numEntitiesT = time.Now() + } + return numEntities } diff --git a/examples/twinstick/component/bullet.go b/examples/twinstick/component/bullet.go index eefd9b2..f44beda 100644 --- a/examples/twinstick/component/bullet.go +++ b/examples/twinstick/component/bullet.go @@ -15,3 +15,11 @@ var BulletComponentID = gohan.NextComponentID() func (p *BulletComponent) ComponentID() gohan.ComponentID { return BulletComponentID } + +func Bullet(e gohan.EntityID) *BulletComponent { + c, ok := e.Component(BulletComponentID).(*BulletComponent) + if !ok { + return nil + } + return c +} diff --git a/examples/twinstick/component/position.go b/examples/twinstick/component/position.go index 646a755..ac4df65 100644 --- a/examples/twinstick/component/position.go +++ b/examples/twinstick/component/position.go @@ -16,3 +16,11 @@ var PositionComponentID = gohan.NextComponentID() func (p *PositionComponent) ComponentID() gohan.ComponentID { return PositionComponentID } + +func Position(e gohan.EntityID) *PositionComponent { + c, ok := e.Component(PositionComponentID).(*PositionComponent) + if !ok { + return nil + } + return c +} diff --git a/examples/twinstick/component/velocity.go b/examples/twinstick/component/velocity.go index 1c98b48..c183f7d 100644 --- a/examples/twinstick/component/velocity.go +++ b/examples/twinstick/component/velocity.go @@ -16,3 +16,11 @@ var VelocityComponentID = gohan.NextComponentID() func (c *VelocityComponent) ComponentID() gohan.ComponentID { return VelocityComponentID } + +func Velocity(e gohan.EntityID) *VelocityComponent { + c, ok := e.Component(VelocityComponentID).(*VelocityComponent) + if !ok { + return nil + } + return c +} diff --git a/examples/twinstick/component/weapon.go b/examples/twinstick/component/weapon.go index b2f4a51..c719128 100644 --- a/examples/twinstick/component/weapon.go +++ b/examples/twinstick/component/weapon.go @@ -25,3 +25,11 @@ var WeaponComponentID = gohan.NextComponentID() func (p *WeaponComponent) ComponentID() gohan.ComponentID { return WeaponComponentID } + +func Weapon(e gohan.EntityID) *WeaponComponent { + c, ok := e.Component(WeaponComponentID).(*WeaponComponent) + if !ok { + return nil + } + return c +} diff --git a/examples/twinstick/entity/bullet.go b/examples/twinstick/entity/bullet.go index 54f8b50..2a9170a 100644 --- a/examples/twinstick/entity/bullet.go +++ b/examples/twinstick/entity/bullet.go @@ -5,7 +5,7 @@ package entity import ( "code.rocketnine.space/tslocum/gohan" - "code.rocketnine.space/tslocum/gohan/_examples/twinstick/component" + "code.rocketnine.space/tslocum/gohan/examples/twinstick/component" ) func NewBullet(x, y, xSpeed, ySpeed float64) gohan.EntityID { diff --git a/examples/twinstick/entity/player.go b/examples/twinstick/entity/player.go index c947391..5824ed3 100644 --- a/examples/twinstick/entity/player.go +++ b/examples/twinstick/entity/player.go @@ -8,7 +8,7 @@ import ( "time" "code.rocketnine.space/tslocum/gohan" - "code.rocketnine.space/tslocum/gohan/_examples/twinstick/component" + "code.rocketnine.space/tslocum/gohan/examples/twinstick/component" ) func NewPlayer() gohan.EntityID { diff --git a/examples/twinstick/game/game.go b/examples/twinstick/game/game.go index 049588d..f2f2b73 100644 --- a/examples/twinstick/game/game.go +++ b/examples/twinstick/game/game.go @@ -9,10 +9,10 @@ import ( "sync" "code.rocketnine.space/tslocum/gohan" - "code.rocketnine.space/tslocum/gohan/_examples/twinstick/asset" - "code.rocketnine.space/tslocum/gohan/_examples/twinstick/component" - "code.rocketnine.space/tslocum/gohan/_examples/twinstick/entity" - "code.rocketnine.space/tslocum/gohan/_examples/twinstick/system" + "code.rocketnine.space/tslocum/gohan/examples/twinstick/asset" + "code.rocketnine.space/tslocum/gohan/examples/twinstick/component" + "code.rocketnine.space/tslocum/gohan/examples/twinstick/entity" + "code.rocketnine.space/tslocum/gohan/examples/twinstick/system" "github.com/hajimehoshi/ebiten/v2" ) @@ -40,6 +40,8 @@ func NewGame() (*game, error) { op: &ebiten.DrawImageOptions{}, } + g.player = entity.NewPlayer() + g.addSystems() err := g.loadAssets() @@ -47,8 +49,6 @@ func NewGame() (*game, error) { return nil, err } - g.player = entity.NewPlayer() - asset.ImgWhiteSquare.Fill(color.White) return g, nil @@ -102,6 +102,8 @@ func (g *game) addSystems() { printInfo := system.NewPrintInfoSystem(g.player) gohan.AddSystem(printInfo) + + gohan.AddSystem(system.NewProfileSystem(g.player)) } func (g *game) loadAssets() error { diff --git a/examples/twinstick/main.go b/examples/twinstick/main.go index 6accdfc..e630c57 100644 --- a/examples/twinstick/main.go +++ b/examples/twinstick/main.go @@ -9,7 +9,7 @@ import ( "os/signal" "syscall" - "code.rocketnine.space/tslocum/gohan/_examples/twinstick/game" + "code.rocketnine.space/tslocum/gohan/examples/twinstick/game" "github.com/hajimehoshi/ebiten/v2" ) diff --git a/examples/twinstick/system/draw_bullets.go b/examples/twinstick/system/draw_bullets.go index b12c12d..0c98b4a 100644 --- a/examples/twinstick/system/draw_bullets.go +++ b/examples/twinstick/system/draw_bullets.go @@ -5,8 +5,8 @@ package system import ( "code.rocketnine.space/tslocum/gohan" - "code.rocketnine.space/tslocum/gohan/_examples/twinstick/asset" - "code.rocketnine.space/tslocum/gohan/_examples/twinstick/component" + "code.rocketnine.space/tslocum/gohan/examples/twinstick/asset" + "code.rocketnine.space/tslocum/gohan/examples/twinstick/component" "github.com/hajimehoshi/ebiten/v2" ) diff --git a/examples/twinstick/system/draw_player.go b/examples/twinstick/system/draw_player.go index 1bbbbfc..94b11ae 100644 --- a/examples/twinstick/system/draw_player.go +++ b/examples/twinstick/system/draw_player.go @@ -5,8 +5,8 @@ package system import ( "code.rocketnine.space/tslocum/gohan" - "code.rocketnine.space/tslocum/gohan/_examples/twinstick/asset" - "code.rocketnine.space/tslocum/gohan/_examples/twinstick/component" + "code.rocketnine.space/tslocum/gohan/examples/twinstick/asset" + "code.rocketnine.space/tslocum/gohan/examples/twinstick/component" "github.com/hajimehoshi/ebiten/v2" ) diff --git a/examples/twinstick/system/input_fire.go b/examples/twinstick/system/input_fire.go index 12a7142..9d4e4ff 100644 --- a/examples/twinstick/system/input_fire.go +++ b/examples/twinstick/system/input_fire.go @@ -8,8 +8,8 @@ import ( "time" "code.rocketnine.space/tslocum/gohan" - "code.rocketnine.space/tslocum/gohan/_examples/twinstick/component" - "code.rocketnine.space/tslocum/gohan/_examples/twinstick/entity" + "code.rocketnine.space/tslocum/gohan/examples/twinstick/component" + "code.rocketnine.space/tslocum/gohan/examples/twinstick/entity" "github.com/hajimehoshi/ebiten/v2" ) @@ -60,9 +60,23 @@ func (s *fireInputSystem) Update(_ gohan.EntityID) error { if ebiten.IsMouseButtonPressed(ebiten.MouseButtonLeft) { cursorX, cursorY := ebiten.CursorPosition() fireAngle := angle(position.X, position.Y, float64(cursorX), float64(cursorY)) + s.fire(weapon, position, fireAngle) } + if ebiten.IsMouseButtonPressed(ebiten.MouseButtonRight) { + cursorX, cursorY := ebiten.CursorPosition() + fireAngle := angle(position.X, position.Y, float64(cursorX), float64(cursorY)) + + const div = 5 + weapon.BulletSpeed /= div + for i := 0.0; i < 24; i++ { + s.fire(weapon, position, fireAngle+i*(math.Pi/12)) + weapon.LastFire = time.Time{} + } + weapon.BulletSpeed *= div + } + switch { case ebiten.IsKeyPressed(ebiten.KeyLeft) && ebiten.IsKeyPressed(ebiten.KeyUp): s.fire(weapon, position, math.Pi/4) diff --git a/examples/twinstick/system/input_move.go b/examples/twinstick/system/input_move.go index 6be9041..0c4a458 100644 --- a/examples/twinstick/system/input_move.go +++ b/examples/twinstick/system/input_move.go @@ -5,7 +5,7 @@ package system import ( "code.rocketnine.space/tslocum/gohan" - "code.rocketnine.space/tslocum/gohan/_examples/twinstick/component" + "code.rocketnine.space/tslocum/gohan/examples/twinstick/component" "github.com/hajimehoshi/ebiten/v2" ) diff --git a/examples/twinstick/system/input_profile.go b/examples/twinstick/system/input_profile.go new file mode 100644 index 0000000..ab3b251 --- /dev/null +++ b/examples/twinstick/system/input_profile.go @@ -0,0 +1,65 @@ +//go:build example +// +build example + +package system + +import ( + "log" + "os" + "path" + "runtime/pprof" + + "code.rocketnine.space/tslocum/gohan" + "github.com/hajimehoshi/ebiten/v2" + "github.com/hajimehoshi/ebiten/v2/inpututil" +) + +type profileSystem struct { + player gohan.EntityID + cpuProfile *os.File +} + +func NewProfileSystem(player gohan.EntityID) *profileSystem { + return &profileSystem{ + player: player, + } +} + +func (s *profileSystem) Matches(e gohan.EntityID) bool { + return e == s.player +} + +func (s *profileSystem) Update(e gohan.EntityID) error { + if ebiten.IsKeyPressed(ebiten.KeyControl) && inpututil.IsKeyJustPressed(ebiten.KeyP) { + if s.cpuProfile == nil { + log.Println("CPU profiling started...") + + homeDir, err := os.UserHomeDir() + if err != nil { + return err + } + + s.cpuProfile, err = os.Create(path.Join(homeDir, "gohan.prof")) + if err != nil { + return err + } + + err = pprof.StartCPUProfile(s.cpuProfile) + if err != nil { + return err + } + } else { + pprof.StopCPUProfile() + + s.cpuProfile.Close() + s.cpuProfile = nil + + log.Println("CPU profiling stopped") + } + } + return nil +} + +func (s *profileSystem) Draw(_ gohan.EntityID, _ *ebiten.Image) error { + return gohan.ErrSystemWithoutDraw +} diff --git a/examples/twinstick/system/movement.go b/examples/twinstick/system/movement.go index c78e424..38dcbc1 100644 --- a/examples/twinstick/system/movement.go +++ b/examples/twinstick/system/movement.go @@ -5,7 +5,7 @@ package system import ( "code.rocketnine.space/tslocum/gohan" - "code.rocketnine.space/tslocum/gohan/_examples/twinstick/component" + "code.rocketnine.space/tslocum/gohan/examples/twinstick/component" "github.com/hajimehoshi/ebiten/v2" ) @@ -27,21 +27,39 @@ func (s *MovementSystem) Update(entity gohan.EntityID) error { bullet := entity.Component(component.BulletComponentID) // Check for collision. - if bullet == nil { - if position.X+velocity.X < 16 { - position.X = 16 - velocity.X = 0 - } else if position.X+velocity.X > s.ScreenW-16 { - position.X = s.ScreenW - 16 - velocity.X = 0 + if position.X+velocity.X < 16 { + if bullet != nil { + gohan.RemoveEntity(entity) + return nil } - if position.Y+velocity.Y < 16 { - position.Y = 16 - velocity.Y = 0 - } else if position.Y+velocity.Y > s.ScreenH-16 { - position.Y = s.ScreenH - 16 - velocity.Y = 0 + + position.X = 16 + velocity.X = 0 + } else if position.X+velocity.X > s.ScreenW-16 { + if bullet != nil { + gohan.RemoveEntity(entity) + return nil } + + position.X = s.ScreenW - 16 + velocity.X = 0 + } + if position.Y+velocity.Y < 16 { + if bullet != nil { + gohan.RemoveEntity(entity) + return nil + } + + position.Y = 16 + velocity.Y = 0 + } else if position.Y+velocity.Y > s.ScreenH-16 { + if bullet != nil { + gohan.RemoveEntity(entity) + return nil + } + + position.Y = s.ScreenH - 16 + velocity.Y = 0 } position.X, position.Y = position.X+velocity.X, position.Y+velocity.Y @@ -54,6 +72,6 @@ func (s *MovementSystem) Update(entity gohan.EntityID) error { return nil } -func (_ *MovementSystem) Draw(entity gohan.EntityID, screen *ebiten.Image) error { +func (_ *MovementSystem) Draw(_ gohan.EntityID, _ *ebiten.Image) error { return gohan.ErrSystemWithoutDraw } diff --git a/examples/twinstick/system/printinfo.go b/examples/twinstick/system/printinfo.go index 5ff779e..816bf59 100644 --- a/examples/twinstick/system/printinfo.go +++ b/examples/twinstick/system/printinfo.go @@ -35,9 +35,9 @@ func (s *printInfoSystem) Update(_ gohan.EntityID) error { return gohan.ErrSystemWithoutUpdate } -func (s *printInfoSystem) Draw(entity gohan.EntityID, screen *ebiten.Image) error { +func (s *printInfoSystem) Draw(_ gohan.EntityID, screen *ebiten.Image) error { s.img.Clear() - ebitenutil.DebugPrint(s.img, fmt.Sprintf("KEY WASD+MOUSE\nTPS %0.0f\nFPS %0.0f", ebiten.CurrentTPS(), ebiten.CurrentFPS())) + 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) return nil } diff --git a/gohan.go b/gohan.go index f00261f..ba58a57 100644 --- a/gohan.go +++ b/gohan.go @@ -5,6 +5,7 @@ import ( "log" "os" "strings" + "sync" "time" "github.com/hajimehoshi/ebiten/v2" @@ -13,13 +14,28 @@ import ( var ( gameComponents = make(map[EntityID]map[ComponentID]interface{}) + allEntities []EntityID + + modifiedEntities []EntityID + removedEntities []EntityID + gameSystems []System gameSystemEntities [][]EntityID // Slice of entities matching each system. gameSystemReceivesUpdate []bool gameSystemReceivesDraw []bool + gameSystemUpdatedEntities int + gameSystemUpdatedEntitiesV int + gameSystemUpdatedEntitiesT time.Time + + gameSystemDrawnEntities int + gameSystemDrawnEntitiesV int + gameSystemDrawnEntitiesT time.Time + debug bool + + mutex sync.Mutex ) func init() { @@ -52,8 +68,11 @@ func attachEntitiesToSystem(system System) { } } -// RegisterSystem registers a system to start receiving Update and Draw calls. -func RegisterSystem(system System) { +// AddSystem registers a system to start receiving Update and Draw calls. +func AddSystem(system System) { + mutex.Lock() + defer mutex.Unlock() + gameSystems = append(gameSystems, system) gameSystemReceivesUpdate = append(gameSystemReceivesUpdate, true) gameSystemReceivesDraw = append(gameSystemReceivesDraw, true) @@ -62,6 +81,67 @@ func RegisterSystem(system System) { attachEntitiesToSystem(system) } +/* +// AddSystemAfter registers a system to start receiving Update and Draw calls +// after the specified system (or systems) are called first. +func AddSystemAfter(system System, after ...System) { + gameSystems = append(gameSystems, system) + gameSystemReceivesUpdate = append(gameSystemReceivesUpdate, true) + gameSystemReceivesDraw = append(gameSystemReceivesDraw, true) + gameSystemEntities = append(gameSystemEntities, nil) + + attachEntitiesToSystem(system) +} +*/ + +func propagateEntityChanges() { + entityMutex.Lock() + defer entityMutex.Unlock() + + for _, entity := range removedEntities { + delete(gameComponents, entity) + + 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:]...) + continue REMOVED + } + } + } + } + removedEntities = nil + + for _, entity := range modifiedEntities { + for i, system := range gameSystems { + systemEntityIndex := -1 + for j, systemEntity := range gameSystemEntities[i] { + if systemEntity == entity { + systemEntityIndex = j + break + } + } + + if system.Matches(entity) { + if systemEntityIndex != -1 { + // Already attached. + continue + } + + gameSystemEntities[i] = append(gameSystemEntities[i], entity) + print(fmt.Sprintf("Attached entity %d to system %d.", entity, i)) + } else if systemEntityIndex != -1 { + // Detach from system. + gameSystemEntities[i] = append(gameSystemEntities[i][:systemEntityIndex], gameSystemEntities[i][systemEntityIndex+1:]...) + } + } + } + modifiedEntities = nil +} + func updateSystem(i int) (int, error) { updated := 0 for _, entity := range gameSystemEntities[i] { @@ -72,7 +152,7 @@ func updateSystem(i int) (int, error) { gameSystemReceivesUpdate[i] = false return 0, nil } - return 0, fmt.Errorf("failed to update system %d for entity %d: %s", i, entity, err) + return 0, fmt.Errorf("failed to update system %d for entity %d: %+v", i, entity, err) } updated++ } @@ -81,49 +161,96 @@ func updateSystem(i int) (int, error) { // Update updates the game state. func Update() error { + mutex.Lock() + defer mutex.Unlock() + + propagateEntityChanges() + var t time.Time if debug { t = time.Now() } var systems int + var entitiesUpdated int for i, registered := range gameSystemReceivesUpdate { if !registered { continue } - updated, err := updateSystem(i) if err != nil { return err } print(fmt.Sprintf("System %d: updated %d entities.", i, updated)) + entitiesUpdated += updated systems++ } if debug { print(fmt.Sprintf("Finished updating %d systems in %.2fms.", systems, float64(time.Since(t).Microseconds())/1000)) } + gameSystemUpdatedEntities = entitiesUpdated return nil } +// UpdatedEntities returns the total number of Entities handled by System Update +// calls. Because each Entity may be handled by more than one System, this +// number may be higher than the number of active entities. +func UpdatedEntities() int { + if time.Since(gameSystemUpdatedEntitiesT) >= time.Second { + gameSystemUpdatedEntitiesV = gameSystemUpdatedEntities + gameSystemUpdatedEntitiesT = time.Now() + } + return gameSystemUpdatedEntitiesV +} + +func drawSystem(i int, screen *ebiten.Image) (int, error) { + var drawn int + for _, entity := range gameSystemEntities[i] { + err := gameSystems[i].Draw(entity, screen) + if err != nil { + if err == ErrSystemWithoutDraw { + // Unregister system from Draw events. + gameSystemReceivesDraw[i] = false + return 0, nil + } + return 0, fmt.Errorf("failed to draw system %d for entity %d: %+v", i, entity, err) + } + drawn++ + } + return drawn, nil +} + // Draw draws the game on to the screen. func Draw(screen *ebiten.Image) error { -DRAWSYSTEMS: + mutex.Lock() + defer mutex.Unlock() + + propagateEntityChanges() + + var entitiesDrawn int for i, registered := range gameSystemReceivesDraw { if !registered { continue } - for _, entity := range gameSystemEntities[i] { - err := gameSystems[i].Draw(entity, screen) - if err != nil { - if err == ErrSystemWithoutDraw { - // Unregister system from Draw events. - gameSystemReceivesDraw[i] = false - continue DRAWSYSTEMS - } - return fmt.Errorf("failed to draw system %d for entity %d: %s", i, entity, err) - } + drawn, err := drawSystem(i, screen) + if err != nil { + return err } + + entitiesDrawn += drawn } + gameSystemDrawnEntities = entitiesDrawn return nil } + +// DrawnEntities returns the total number of Entities handled by System Draw +// calls. Because each Entity may be handled by more than one System, this +// number may be higher than the number of active entities. +func DrawnEntities() int { + if time.Since(gameSystemDrawnEntitiesT) >= time.Second { + gameSystemDrawnEntitiesV = gameSystemDrawnEntities + gameSystemDrawnEntitiesT = time.Now() + } + return gameSystemDrawnEntitiesV +}