From 039863b55b2eb7f647176123b53ae1a17675681d Mon Sep 17 00:00:00 2001 From: Trevor Slocum Date: Sun, 5 Dec 2021 17:17:15 -0800 Subject: [PATCH] Add World This enables applications to use multiple instances. --- component.go | 28 +- component_test.go | 46 ++- context.go | 33 +++ entity.go | 40 ++- entity_test.go | 34 +-- examples/twinstick/component/bullet.go | 7 +- examples/twinstick/component/position.go | 7 +- examples/twinstick/component/velocity.go | 7 +- examples/twinstick/component/weapon.go | 8 +- examples/twinstick/entity/bullet.go | 9 +- examples/twinstick/entity/player.go | 10 +- examples/twinstick/game/game.go | 36 ++- examples/twinstick/main.go | 1 - examples/twinstick/system/draw_bullets.go | 16 +- examples/twinstick/system/draw_player.go | 13 +- examples/twinstick/system/input_fire.go | 18 +- examples/twinstick/system/input_move.go | 13 +- examples/twinstick/system/input_profile.go | 15 +- examples/twinstick/system/movement.go | 39 +-- examples/twinstick/system/printinfo.go | 16 +- examples/twinstick/world/world.go | 5 + go.mod | 2 +- go.sum | 4 +- gohan.go | 267 ------------------ system.go | 12 +- world.go | 307 +++++++++++++++++++++ world_test.go | 108 ++++++++ 27 files changed, 664 insertions(+), 437 deletions(-) create mode 100644 context.go create mode 100644 examples/twinstick/world/world.go delete mode 100644 gohan.go create mode 100644 world.go create mode 100644 world_test.go diff --git a/component.go b/component.go index 395f61c..f896075 100644 --- a/component.go +++ b/component.go @@ -15,42 +15,40 @@ type Component interface { ComponentID() ComponentID } -var maxComponentID ComponentID - // NewComponentID returns the next available ComponentID. -func NewComponentID() ComponentID { - mutex.Lock() - defer mutex.Unlock() - +func (w *World) NewComponentID() ComponentID { entityMutex.Lock() defer entityMutex.Unlock() componentMutex.Lock() defer componentMutex.Unlock() - maxComponentID++ + w.maxComponentID++ - for i := Entity(1); i < maxEntityID; i++ { - gameComponents[i] = append(gameComponents[i], nil) + for i := Entity(1); i <= w.maxEntityID; i++ { + w.components[i] = append(w.components[i], nil) } - return maxComponentID + return w.maxComponentID } // AddComponent adds a Component to an Entity. -func (entity Entity) AddComponent(component Component) { +func (w *World) AddComponent(entity Entity, component Component) { + componentMutex.Lock() + defer componentMutex.Unlock() + componentID := component.ComponentID() - gameComponents[entity][componentID] = component + w.components[entity][componentID] = component entityMutex.Lock() defer entityMutex.Unlock() - modifiedEntities = append(modifiedEntities, entity) + w.modifiedEntities = append(w.modifiedEntities, entity) } // Component gets a Component of an Entity. -func (entity Entity) Component(componentID ComponentID) interface{} { - components := gameComponents[entity] +func (w *World) Component(entity Entity, componentID ComponentID) interface{} { + components := w.components[entity] if components == nil { return nil } diff --git a/component_test.go b/component_test.go index 4027e60..b7187ac 100644 --- a/component_test.go +++ b/component_test.go @@ -2,22 +2,36 @@ package gohan import "testing" -var testComponentID = NewComponentID() +type positionComponent struct { + componentID ComponentID -type testComponent struct { X, Y float64 } -func (t testComponent) ComponentID() ComponentID { - return testComponentID +func (c *positionComponent) ComponentID() ComponentID { + return c.componentID +} + +type velocityComponent struct { + componentID ComponentID + + X, Y float64 +} + +func (c *velocityComponent) ComponentID() ComponentID { + return c.componentID } func BenchmarkComponent(b *testing.B) { - e := NewEntity() + w := NewWorld() - e.AddComponent(&testComponent{ - X: 108, - Y: 0, + e := w.NewEntity() + + positionComponentID := w.NewComponentID() + w.AddComponent(e, &positionComponent{ + X: 108, + Y: 0, + componentID: positionComponentID, }) b.StopTimer() @@ -26,16 +40,20 @@ func BenchmarkComponent(b *testing.B) { b.StartTimer() for i := 0; i < b.N; i++ { - _ = e.Component(testComponentID) + _ = w.Component(e, positionComponentID) } } func BenchmarkAddComponent(b *testing.B) { - e := NewEntity() + w := NewWorld() + + e := w.NewEntity() - c := &testComponent{ - X: 108, - Y: 0, + positionComponentID := w.NewComponentID() + c := &positionComponent{ + X: 108, + Y: 0, + componentID: positionComponentID, } b.StopTimer() @@ -44,6 +62,6 @@ func BenchmarkAddComponent(b *testing.B) { b.StartTimer() for i := 0; i < b.N; i++ { - e.AddComponent(c) + w.AddComponent(e, c) } } diff --git a/context.go b/context.go new file mode 100644 index 0000000..407db3b --- /dev/null +++ b/context.go @@ -0,0 +1,33 @@ +package gohan + +import "log" + +type Context struct { + Entity Entity + + s int // System index. + c []ComponentID + w *World +} + +// Component gets a Component of the currently handled Entity. +func (ctx *Context) Component(componentID ComponentID) interface{} { + var found bool + for _, id := range ctx.c { + if id == componentID { + found = true + break + } + } + if !found { + log.Panicf("illegal component access: component %d is not queried by system %d", componentID, ctx.s) + } + return ctx.w.Component(ctx.Entity, componentID) +} + +// RemoveEntity removes the currently handled 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 (ctx *Context) RemoveEntity() { + ctx.w.RemoveEntity(ctx.Entity) +} diff --git a/entity.go b/entity.go index 7ce915e..25503c1 100644 --- a/entity.go +++ b/entity.go @@ -8,40 +8,38 @@ import ( // Entity is an entity identifier. type Entity int -var maxEntityID Entity - 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 NewEntity() Entity { +func (w *World) NewEntity() Entity { entityMutex.Lock() defer entityMutex.Unlock() - if len(availableEntityIDs) > 0 { - id := availableEntityIDs[0] - availableEntityIDs = availableEntityIDs[1:] - allEntities = append(allEntities, id) + if len(w.availableEntityIDs) > 0 { + id := w.availableEntityIDs[0] + w.availableEntityIDs = w.availableEntityIDs[1:] + w.allEntities = append(w.allEntities, id) return id } - maxEntityID++ - allEntities = append(allEntities, maxEntityID) - gameComponents = append(gameComponents, make([]interface{}, maxComponentID+1)) - return maxEntityID + w.maxEntityID++ + w.allEntities = append(w.allEntities, w.maxEntityID) + w.components = append(w.components, make([]interface{}, w.maxComponentID+1)) + return w.maxEntityID } -// 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() { +// RemoveEntity 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 (w *World) RemoveEntity(entity Entity) { entityMutex.Lock() defer entityMutex.Unlock() - for i, e := range allEntities { + for i, e := range w.allEntities { if e == entity { - allEntities = append(allEntities[:i], allEntities[i+1:]...) - removedEntities = append(removedEntities, e) + w.allEntities = append(w.allEntities[:i], w.allEntities[i+1:]...) + w.removedEntities = append(w.removedEntities, e) return } } @@ -51,9 +49,9 @@ 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) +func (w *World) ActiveEntities() int { + if time.Since(numEntitiesT) >= w.cacheTime { + numEntities = len(w.allEntities) numEntitiesT = time.Now() } return numEntities diff --git a/entity_test.go b/entity_test.go index 802c875..5aacdd9 100644 --- a/entity_test.go +++ b/entity_test.go @@ -2,59 +2,49 @@ package gohan import ( "testing" - "time" ) func TestActiveEntities(t *testing.T) { - t.Parallel() + w := NewWorld() + w.cacheTime = 0 - active := ActiveEntities() + active := w.ActiveEntities() if active != 0 { t.Fatalf("expected 0 active entities, got %d", active) } - wait() - active = ActiveEntities() + active = w.ActiveEntities() if active != 0 { t.Fatalf("expected 0 active entities, got %d", active) } // Create entity. - e1 := NewEntity() + e1 := w.NewEntity() - wait() - active = ActiveEntities() + active = w.ActiveEntities() if active != 1 { t.Fatalf("expected 1 active entities, got %d", active) } // Create entity. - e2 := NewEntity() + e2 := w.NewEntity() - wait() - active = ActiveEntities() + active = w.ActiveEntities() if active != 2 { t.Fatalf("expected 2 active entities, got %d", active) } - e1.Remove() + w.RemoveEntity(e1) - wait() - active = ActiveEntities() + active = w.ActiveEntities() if active != 1 { t.Fatalf("expected 1 active entities, got %d", active) } - e2.Remove() + w.RemoveEntity(e2) - wait() - active = ActiveEntities() + active = w.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 668d6ba..6010981 100644 --- a/examples/twinstick/component/bullet.go +++ b/examples/twinstick/component/bullet.go @@ -5,19 +5,20 @@ package component import ( "code.rocketnine.space/tslocum/gohan" + "code.rocketnine.space/tslocum/gohan/examples/twinstick/world" ) type BulletComponent struct { } -var BulletComponentID = gohan.NewComponentID() +var BulletComponentID = world.World.NewComponentID() func (p *BulletComponent) ComponentID() gohan.ComponentID { return BulletComponentID } -func Bullet(e gohan.Entity) *BulletComponent { - c, ok := e.Component(BulletComponentID).(*BulletComponent) +func Bullet(ctx *gohan.Context) *BulletComponent { + c, ok := ctx.Component(BulletComponentID).(*BulletComponent) if !ok { return nil } diff --git a/examples/twinstick/component/position.go b/examples/twinstick/component/position.go index 25577f4..ac2569c 100644 --- a/examples/twinstick/component/position.go +++ b/examples/twinstick/component/position.go @@ -5,20 +5,21 @@ package component import ( "code.rocketnine.space/tslocum/gohan" + "code.rocketnine.space/tslocum/gohan/examples/twinstick/world" ) type PositionComponent struct { X, Y float64 } -var PositionComponentID = gohan.NewComponentID() +var PositionComponentID = world.World.NewComponentID() func (p *PositionComponent) ComponentID() gohan.ComponentID { return PositionComponentID } -func Position(e gohan.Entity) *PositionComponent { - c, ok := e.Component(PositionComponentID).(*PositionComponent) +func Position(ctx *gohan.Context) *PositionComponent { + c, ok := ctx.Component(PositionComponentID).(*PositionComponent) if !ok { return nil } diff --git a/examples/twinstick/component/velocity.go b/examples/twinstick/component/velocity.go index fea1886..8ef83d1 100644 --- a/examples/twinstick/component/velocity.go +++ b/examples/twinstick/component/velocity.go @@ -5,20 +5,21 @@ package component import ( "code.rocketnine.space/tslocum/gohan" + "code.rocketnine.space/tslocum/gohan/examples/twinstick/world" ) type VelocityComponent struct { X, Y float64 } -var VelocityComponentID = gohan.NewComponentID() +var VelocityComponentID = world.World.NewComponentID() func (c *VelocityComponent) ComponentID() gohan.ComponentID { return VelocityComponentID } -func Velocity(e gohan.Entity) *VelocityComponent { - c, ok := e.Component(VelocityComponentID).(*VelocityComponent) +func Velocity(ctx *gohan.Context) *VelocityComponent { + c, ok := ctx.Component(VelocityComponentID).(*VelocityComponent) if !ok { return nil } diff --git a/examples/twinstick/component/weapon.go b/examples/twinstick/component/weapon.go index 69a7e64..74c5b22 100644 --- a/examples/twinstick/component/weapon.go +++ b/examples/twinstick/component/weapon.go @@ -6,6 +6,8 @@ package component import ( "time" + "code.rocketnine.space/tslocum/gohan/examples/twinstick/world" + "code.rocketnine.space/tslocum/gohan" ) @@ -20,14 +22,14 @@ type WeaponComponent struct { BulletSpeed float64 } -var WeaponComponentID = gohan.NewComponentID() +var WeaponComponentID = world.World.NewComponentID() func (p *WeaponComponent) ComponentID() gohan.ComponentID { return WeaponComponentID } -func Weapon(e gohan.Entity) *WeaponComponent { - c, ok := e.Component(WeaponComponentID).(*WeaponComponent) +func Weapon(ctx *gohan.Context) *WeaponComponent { + c, ok := ctx.Component(WeaponComponentID).(*WeaponComponent) if !ok { return nil } diff --git a/examples/twinstick/entity/bullet.go b/examples/twinstick/entity/bullet.go index 0aafc72..3babe83 100644 --- a/examples/twinstick/entity/bullet.go +++ b/examples/twinstick/entity/bullet.go @@ -6,22 +6,23 @@ package entity import ( "code.rocketnine.space/tslocum/gohan" "code.rocketnine.space/tslocum/gohan/examples/twinstick/component" + "code.rocketnine.space/tslocum/gohan/examples/twinstick/world" ) func NewBullet(x, y, xSpeed, ySpeed float64) gohan.Entity { - bullet := gohan.NewEntity() + bullet := world.World.NewEntity() - bullet.AddComponent(&component.PositionComponent{ + world.World.AddComponent(bullet, &component.PositionComponent{ X: x, Y: y, }) - bullet.AddComponent(&component.VelocityComponent{ + world.World.AddComponent(bullet, &component.VelocityComponent{ X: xSpeed, Y: ySpeed, }) - bullet.AddComponent(&component.BulletComponent{}) + world.World.AddComponent(bullet, &component.BulletComponent{}) return bullet } diff --git a/examples/twinstick/entity/player.go b/examples/twinstick/entity/player.go index a3fa4ab..92cd3fe 100644 --- a/examples/twinstick/entity/player.go +++ b/examples/twinstick/entity/player.go @@ -7,22 +7,24 @@ import ( "math" "time" + "code.rocketnine.space/tslocum/gohan/examples/twinstick/world" + "code.rocketnine.space/tslocum/gohan" "code.rocketnine.space/tslocum/gohan/examples/twinstick/component" ) func NewPlayer() gohan.Entity { - player := gohan.NewEntity() + player := world.World.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 // we receive the screen dimensions for the first time. - player.AddComponent(&component.PositionComponent{ + world.World.AddComponent(player, &component.PositionComponent{ X: -1, Y: -1, }) - player.AddComponent(&component.VelocityComponent{}) + world.World.AddComponent(player, &component.VelocityComponent{}) weapon := &component.WeaponComponent{ Ammo: math.MaxInt64, @@ -30,7 +32,7 @@ func NewPlayer() gohan.Entity { FireRate: 100 * time.Millisecond, BulletSpeed: 15, } - player.AddComponent(weapon) + world.World.AddComponent(player, weapon) return player } diff --git a/examples/twinstick/game/game.go b/examples/twinstick/game/game.go index 5a16542..0f7cf89 100644 --- a/examples/twinstick/game/game.go +++ b/examples/twinstick/game/game.go @@ -8,6 +8,8 @@ import ( "os" "sync" + "code.rocketnine.space/tslocum/gohan/examples/twinstick/world" + "code.rocketnine.space/tslocum/gohan" "code.rocketnine.space/tslocum/gohan/examples/twinstick/asset" "code.rocketnine.space/tslocum/gohan/examples/twinstick/component" @@ -59,13 +61,13 @@ func (g *game) Layout(outsideWidth, outsideHeight int) (int, int) { s := ebiten.DeviceScaleFactor() w, h := int(s*float64(outsideWidth)), int(s*float64(outsideHeight)) if w != g.w || h != g.h { + if g.w == 0 || g.h == 0 { + position := world.World.Component(g.player, component.PositionComponentID).(*component.PositionComponent) + position.X, position.Y = float64(w)/2-16, float64(h)/2-16 + } + g.w, g.h = w, h g.movementSystem.ScreenW, g.movementSystem.ScreenH = float64(w), float64(h) - - 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 - } } return g.w, g.h } @@ -76,34 +78,38 @@ func (g *game) Update() error { return nil } - return gohan.Update() + return world.World.Update() } func (g *game) Draw(screen *ebiten.Image) { - err := gohan.Draw(screen) + err := world.World.Draw(screen) if err != nil { panic(err) } } func (g *game) addSystems() { - gohan.AddSystem(system.NewMovementInputSystem(g.player)) + w := world.World + + w.AddSystem(system.NewMovementInputSystem(g.player)) - g.movementSystem = &system.MovementSystem{} - gohan.AddSystem(g.movementSystem) + g.movementSystem = &system.MovementSystem{ + Player: g.player, + } + w.AddSystem(g.movementSystem) - gohan.AddSystem(system.NewFireInputSystem(g.player)) + w.AddSystem(system.NewFireInputSystem(g.player)) renderBullet := system.NewDrawBulletsSystem() - gohan.AddSystem(renderBullet) + w.AddSystem(renderBullet) renderPlayer := system.NewDrawPlayerSystem(g.player) - gohan.AddSystem(renderPlayer) + w.AddSystem(renderPlayer) printInfo := system.NewPrintInfoSystem(g.player) - gohan.AddSystem(printInfo) + w.AddSystem(printInfo) - gohan.AddSystem(system.NewProfileSystem(g.player)) + w.AddSystem(system.NewProfileSystem(g.player)) } func (g *game) loadAssets() error { diff --git a/examples/twinstick/main.go b/examples/twinstick/main.go index e630c57..bc50fa2 100644 --- a/examples/twinstick/main.go +++ b/examples/twinstick/main.go @@ -16,7 +16,6 @@ import ( func main() { ebiten.SetWindowTitle("Twin-Stick Shooter Example - Gohan") ebiten.SetWindowResizable(true) - ebiten.SetFullscreen(true) ebiten.SetMaxTPS(144) ebiten.SetRunnableOnUnfocused(true) ebiten.SetWindowClosingHandled(true) diff --git a/examples/twinstick/system/draw_bullets.go b/examples/twinstick/system/draw_bullets.go index 03086f7..ce1a419 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.Entity) bool { - position := entity.Component(component.PositionComponentID) - bullet := entity.Component(component.BulletComponentID) - - return position != nil && bullet != nil +func (s *DrawBulletsSystem) Components() []gohan.ComponentID { + return []gohan.ComponentID{ + component.PositionComponentID, + component.BulletComponentID, + } } -func (s *DrawBulletsSystem) Update(_ gohan.Entity) error { +func (s *DrawBulletsSystem) Update(ctx *gohan.Context) error { return gohan.ErrSystemWithoutUpdate } -func (s *DrawBulletsSystem) Draw(entity gohan.Entity, screen *ebiten.Image) error { - position := component.Position(entity) +func (s *DrawBulletsSystem) Draw(ctx *gohan.Context, screen *ebiten.Image) error { + position := component.Position(ctx) 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 2e1a42f..fae2d00 100644 --- a/examples/twinstick/system/draw_player.go +++ b/examples/twinstick/system/draw_player.go @@ -22,16 +22,19 @@ func NewDrawPlayerSystem(player gohan.Entity) *drawPlayerSystem { } } -func (s *drawPlayerSystem) Matches(entity gohan.Entity) bool { - return entity == s.player +func (s *drawPlayerSystem) Components() []gohan.ComponentID { + return []gohan.ComponentID{ + component.PositionComponentID, + component.WeaponComponentID, + } } -func (s *drawPlayerSystem) Update(_ gohan.Entity) error { +func (s *drawPlayerSystem) Update(_ *gohan.Context) error { return gohan.ErrSystemWithoutUpdate } -func (s *drawPlayerSystem) Draw(entity gohan.Entity, screen *ebiten.Image) error { - position := component.Position(entity) +func (s *drawPlayerSystem) Draw(ctx *gohan.Context, screen *ebiten.Image) error { + position := component.Position(ctx) 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 d034d8a..f6cbf13 100644 --- a/examples/twinstick/system/input_fire.go +++ b/examples/twinstick/system/input_fire.go @@ -27,10 +27,11 @@ func NewFireInputSystem(player gohan.Entity) *fireInputSystem { } } -func (_ *fireInputSystem) Matches(e gohan.Entity) bool { - weapon := e.Component(component.WeaponComponentID) - - return weapon != nil +func (_ *fireInputSystem) Components() []gohan.ComponentID { + return []gohan.ComponentID{ + component.PositionComponentID, + component.WeaponComponentID, + } } func (s *fireInputSystem) fire(weapon *component.WeaponComponent, position *component.PositionComponent, fireAngle float64) { @@ -48,15 +49,14 @@ func (s *fireInputSystem) fire(weapon *component.WeaponComponent, position *comp _ = bullet } -func (s *fireInputSystem) Update(_ gohan.Entity) error { - weapon := component.Weapon(s.player) +func (s *fireInputSystem) Update(ctx *gohan.Context) error { + position := component.Position(ctx) + weapon := component.Weapon(ctx) if weapon.Ammo <= 0 { return nil } - position := component.Position(s.player) - if ebiten.IsMouseButtonPressed(ebiten.MouseButtonLeft) { cursorX, cursorY := ebiten.CursorPosition() fireAngle := angle(position.X, position.Y, float64(cursorX), float64(cursorY)) @@ -99,6 +99,6 @@ func (s *fireInputSystem) Update(_ gohan.Entity) error { return nil } -func (_ *fireInputSystem) Draw(_ gohan.Entity, _ *ebiten.Image) error { +func (_ *fireInputSystem) Draw(ctx *gohan.Context, _ *ebiten.Image) error { return gohan.ErrSystemWithoutDraw } diff --git a/examples/twinstick/system/input_move.go b/examples/twinstick/system/input_move.go index c69e037..0a5eb4e 100644 --- a/examples/twinstick/system/input_move.go +++ b/examples/twinstick/system/input_move.go @@ -19,12 +19,15 @@ func NewMovementInputSystem(player gohan.Entity) *movementInputSystem { } } -func (s *movementInputSystem) Matches(e gohan.Entity) bool { - return e == s.player +func (s *movementInputSystem) Components() []gohan.ComponentID { + return []gohan.ComponentID{ + component.VelocityComponentID, + component.WeaponComponentID, + } } -func (s *movementInputSystem) Update(e gohan.Entity) error { - velocity := component.Velocity(s.player) +func (s *movementInputSystem) Update(ctx *gohan.Context) error { + velocity := component.Velocity(ctx) if ebiten.IsKeyPressed(ebiten.KeyA) { velocity.X -= 0.5 if velocity.X < -5 { @@ -52,6 +55,6 @@ func (s *movementInputSystem) Update(e gohan.Entity) error { return nil } -func (s *movementInputSystem) Draw(_ gohan.Entity, _ *ebiten.Image) error { +func (s *movementInputSystem) Draw(ctx *gohan.Context, _ *ebiten.Image) error { return gohan.ErrSystemWithoutDraw } diff --git a/examples/twinstick/system/input_profile.go b/examples/twinstick/system/input_profile.go index 149fb26..a4a963f 100644 --- a/examples/twinstick/system/input_profile.go +++ b/examples/twinstick/system/input_profile.go @@ -7,9 +7,11 @@ import ( "log" "os" "path" + "runtime" "runtime/pprof" "code.rocketnine.space/tslocum/gohan" + "code.rocketnine.space/tslocum/gohan/examples/twinstick/component" "github.com/hajimehoshi/ebiten/v2" "github.com/hajimehoshi/ebiten/v2/inpututil" ) @@ -25,15 +27,20 @@ func NewProfileSystem(player gohan.Entity) *profileSystem { } } -func (s *profileSystem) Matches(e gohan.Entity) bool { - return e == s.player +func (s *profileSystem) Components() []gohan.ComponentID { + return []gohan.ComponentID{ + component.WeaponComponentID, + } } -func (s *profileSystem) Update(e gohan.Entity) error { +func (s *profileSystem) Update(ctx *gohan.Context) error { if ebiten.IsKeyPressed(ebiten.KeyControl) && inpututil.IsKeyJustPressed(ebiten.KeyP) { if s.cpuProfile == nil { log.Println("CPU profiling started...") + runtime.SetCPUProfileRate(0) + runtime.SetCPUProfileRate(1000) + homeDir, err := os.UserHomeDir() if err != nil { return err @@ -60,6 +67,6 @@ func (s *profileSystem) Update(e gohan.Entity) error { return nil } -func (s *profileSystem) Draw(_ gohan.Entity, _ *ebiten.Image) error { +func (s *profileSystem) Draw(ctx *gohan.Context, _ *ebiten.Image) error { return gohan.ErrSystemWithoutDraw } diff --git a/examples/twinstick/system/movement.go b/examples/twinstick/system/movement.go index b2c3d56..fea5547 100644 --- a/examples/twinstick/system/movement.go +++ b/examples/twinstick/system/movement.go @@ -11,33 +11,34 @@ import ( type MovementSystem struct { ScreenW, ScreenH float64 + Player gohan.Entity } -func (_ *MovementSystem) Matches(entity gohan.Entity) bool { - position := component.Position(entity) - velocity := component.Velocity(entity) - - return position != nil && velocity != nil +func (_ *MovementSystem) Components() []gohan.ComponentID { + return []gohan.ComponentID{ + component.PositionComponentID, + component.VelocityComponentID, + } } -func (s *MovementSystem) Update(entity gohan.Entity) error { - position := component.Position(entity) - velocity := component.Velocity(entity) +func (s *MovementSystem) Update(ctx *gohan.Context) error { + position := component.Position(ctx) + velocity := component.Velocity(ctx) - bullet := entity.Component(component.BulletComponentID) + bullet := ctx.Entity != s.Player // Check for collision. if position.X+velocity.X < 16 { - if bullet != nil { - entity.Remove() + if bullet { + ctx.RemoveEntity() return nil } position.X = 16 velocity.X = 0 } else if position.X+velocity.X > s.ScreenW-16 { - if bullet != nil { - entity.Remove() + if bullet { + ctx.RemoveEntity() return nil } @@ -45,16 +46,16 @@ func (s *MovementSystem) Update(entity gohan.Entity) error { velocity.X = 0 } if position.Y+velocity.Y < 16 { - if bullet != nil { - entity.Remove() + if bullet { + ctx.RemoveEntity() return nil } position.Y = 16 velocity.Y = 0 } else if position.Y+velocity.Y > s.ScreenH-16 { - if bullet != nil { - entity.Remove() + if bullet { + ctx.RemoveEntity() return nil } @@ -64,7 +65,7 @@ func (s *MovementSystem) Update(entity gohan.Entity) error { position.X, position.Y = position.X+velocity.X, position.Y+velocity.Y - if bullet == nil { + if !bullet { velocity.X *= 0.95 velocity.Y *= 0.95 } @@ -72,6 +73,6 @@ func (s *MovementSystem) Update(entity gohan.Entity) error { return nil } -func (_ *MovementSystem) Draw(_ gohan.Entity, _ *ebiten.Image) error { +func (_ *MovementSystem) Draw(ctx *gohan.Context, _ *ebiten.Image) error { return gohan.ErrSystemWithoutDraw } diff --git a/examples/twinstick/system/printinfo.go b/examples/twinstick/system/printinfo.go index 393ab9b..6ced16a 100644 --- a/examples/twinstick/system/printinfo.go +++ b/examples/twinstick/system/printinfo.go @@ -7,6 +7,8 @@ import ( "fmt" "code.rocketnine.space/tslocum/gohan" + "code.rocketnine.space/tslocum/gohan/examples/twinstick/component" + "code.rocketnine.space/tslocum/gohan/examples/twinstick/world" "github.com/hajimehoshi/ebiten/v2" "github.com/hajimehoshi/ebiten/v2/ebitenutil" ) @@ -27,17 +29,21 @@ func NewPrintInfoSystem(player gohan.Entity) *printInfoSystem { return p } -func (s *printInfoSystem) Matches(e gohan.Entity) bool { - return e == s.player +func (s *printInfoSystem) Components() []gohan.ComponentID { + return []gohan.ComponentID{ + component.WeaponComponentID, + } } -func (s *printInfoSystem) Update(_ gohan.Entity) error { +func (s *printInfoSystem) Update(ctx *gohan.Context) error { return gohan.ErrSystemWithoutUpdate } -func (s *printInfoSystem) Draw(_ gohan.Entity, screen *ebiten.Image) error { +func (s *printInfoSystem) Draw(ctx *gohan.Context, screen *ebiten.Image) error { + w := world.World + 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())) + ebitenutil.DebugPrint(s.img, fmt.Sprintf("KEY WASD+MOUSE\nENT %d\nUPD %d\nDRA %d\nTPS %0.0f\nFPS %0.0f", w.ActiveEntities(), w.UpdatedEntities(), w.DrawnEntities(), ebiten.CurrentTPS(), ebiten.CurrentFPS())) screen.DrawImage(s.img, s.op) return nil } diff --git a/examples/twinstick/world/world.go b/examples/twinstick/world/world.go new file mode 100644 index 0000000..2233873 --- /dev/null +++ b/examples/twinstick/world/world.go @@ -0,0 +1,5 @@ +package world + +import "code.rocketnine.space/tslocum/gohan" + +var World = gohan.NewWorld() diff --git a/go.mod b/go.mod index 2ec3a46..9f5d24a 100644 --- a/go.mod +++ b/go.mod @@ -2,7 +2,7 @@ module code.rocketnine.space/tslocum/gohan go 1.17 -require github.com/hajimehoshi/ebiten/v2 v2.2.2 +require github.com/hajimehoshi/ebiten/v2 v2.2.3 require ( github.com/go-gl/glfw/v3.3/glfw v0.0.0-20210727001814-0db043d8d5be // indirect diff --git a/go.sum b/go.sum index 250c017..0162cd0 100644 --- a/go.sum +++ b/go.sum @@ -2,8 +2,8 @@ github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym github.com/go-gl/glfw/v3.3/glfw v0.0.0-20210727001814-0db043d8d5be h1:vEIVIuBApEBQTEJt19GfhoU+zFSV+sNTa9E9FdnRYfk= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20210727001814-0db043d8d5be/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/hajimehoshi/bitmapfont/v2 v2.1.3/go.mod h1:2BnYrkTQGThpr/CY6LorYtt/zEPNzvE/ND69CRTaHMs= -github.com/hajimehoshi/ebiten/v2 v2.2.2 h1:92E+ogdNyH1P/LlvMQ7vonbFDh6bl+O7Ak+H1HX0RX8= -github.com/hajimehoshi/ebiten/v2 v2.2.2/go.mod h1:olKl/qqhMBBAm2oI7Zy292nCtE+nitlmYKNF3UpbFn0= +github.com/hajimehoshi/ebiten/v2 v2.2.3 h1:jZUP3XWP6mXaw9SCrjWT5Pl6EPuz6FY737dZQgN1KJ4= +github.com/hajimehoshi/ebiten/v2 v2.2.3/go.mod h1:olKl/qqhMBBAm2oI7Zy292nCtE+nitlmYKNF3UpbFn0= github.com/hajimehoshi/file2byteslice v0.0.0-20210813153925-5340248a8f41/go.mod h1:CqqAHp7Dk/AqQiwuhV1yT2334qbA/tFWQW0MD2dGqUE= github.com/hajimehoshi/go-mp3 v0.3.2/go.mod h1:qMJj/CSDxx6CGHiZeCgbiq2DSUkbK0UbtXShQcnfyMM= github.com/hajimehoshi/oto v0.6.1/go.mod h1:0QXGEkbuJRohbJaxr7ZQSxnju7hEhseiPx2hrh6raOI= diff --git a/gohan.go b/gohan.go deleted file mode 100644 index 9663679..0000000 --- a/gohan.go +++ /dev/null @@ -1,267 +0,0 @@ -package gohan - -import ( - "fmt" - "log" - "os" - "strings" - "sync" - "time" - - "github.com/hajimehoshi/ebiten/v2" -) - -var ( - gameComponents [][]interface{} - - allEntities []Entity - - modifiedEntities []Entity - removedEntities []Entity - - // availableEntityIDs is the set of EntityIDs available because they were - // removed from the game. - availableEntityIDs []Entity - - gameSystems []System - gameSystemEntities [][]Entity // 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() { - // 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) - - debug = debugEnv == "1" || debugEnv == "t" || debugEnv == "y" || debugEnv == "on" || debugEnv == "yes" || debugEnv == "true" -} - -// print prints debug information (when enabled). -func print(s string) { - if !debug { - return - } - - log.Println(s) -} - -func attachEntitiesToSystem(system System) { - // This function is always called on a newly added system. - systemID := len(gameSystemEntities) - 1 - - for entity := Entity(0); entity < maxEntityID; entity++ { - if system.Matches(entity) { - gameSystemEntities[systemID] = append(gameSystemEntities[systemID], entity) - - print(fmt.Sprintf("Attached entity %d to system %d.", entity, systemID)) - } - } -} - -// 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) - gameSystemEntities = append(gameSystemEntities, nil) - - 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 { - // Remove from attached systems. - REMOVED: - for i := range gameSystemEntities { - for j, e := range gameSystemEntities[i] { - if e == entity { - gameSystemEntities[i] = append(gameSystemEntities[i][:j], gameSystemEntities[i][j+1:]...) - continue REMOVED - } - } - } - - // Remove components. - gameComponents[entity] = make([]interface{}, maxComponentID+1) - } - - // Mark EntityIDs as available. - availableEntityIDs = append(availableEntityIDs, removedEntities...) - - 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] { - err := gameSystems[i].Update(entity) - if err != nil { - if err == ErrSystemWithoutUpdate { - // Unregister system from Update events. - gameSystemReceivesUpdate[i] = false - return 0, nil - } - return 0, fmt.Errorf("failed to update system %d for entity %d: %+v", i, entity, err) - } - updated++ - } - return updated, nil -} - -// 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 { - mutex.Lock() - defer mutex.Unlock() - - propagateEntityChanges() - - var entitiesDrawn int - for i, registered := range gameSystemReceivesDraw { - if !registered { - continue - } - - 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 -} diff --git a/system.go b/system.go index 945f099..2394909 100644 --- a/system.go +++ b/system.go @@ -13,14 +13,18 @@ import ( // // See ErrSystemWithoutUpdate and ErrSystemWithoutDraw. type System interface { - // Matches returns whether the provided entity is handled by this system. - Matches(entity Entity) bool + // Name returns the name of the system. + //Name() string + + // Components returns a list of Components (specified by ID) required for + // an Entity to be handled by the System. + Components() []ComponentID // Update is called once for each matching entity each time the game state is updated. - Update(entity Entity) error + Update(ctx *Context) error // Draw is called once for each matching entity each time the game is drawn to the screen. - Draw(entity Entity, screen *ebiten.Image) error + Draw(ctx *Context, screen *ebiten.Image) error } // Special error values. diff --git a/world.go b/world.go new file mode 100644 index 0000000..e677f5b --- /dev/null +++ b/world.go @@ -0,0 +1,307 @@ +package gohan + +import ( + "fmt" + "log" + "os" + "strings" + "sync" + "time" + + "github.com/hajimehoshi/ebiten/v2" +) + +var debug bool + +func init() { + debugEnv := os.Getenv("GOHAN_DEBUG") + debugEnv = strings.TrimSpace(debugEnv) + debugEnv = strings.ToLower(debugEnv) + + debug = debugEnv == "1" || debugEnv == "t" || debugEnv == "y" || debugEnv == "on" || debugEnv == "yes" || debugEnv == "true" +} + +// World represents a collection of Entities, Components and Systems. +type World struct { + maxEntityID Entity + + maxComponentID ComponentID + + components [][]interface{} + + allEntities []Entity + + modifiedEntities []Entity + removedEntities []Entity + + // availableEntityIDs is the set of EntityIDs available because they were + // removed from the game. + availableEntityIDs []Entity + + systems []System + systemEntities [][]Entity // Slice of entities matching each system. + systemQueries [][]ComponentID // Slice of entities matching each system. + + systemReceivesUpdate []bool + systemReceivesDraw []bool + + systemUpdatedEntities int + systemUpdatedEntitiesV int + systemUpdatedEntitiesT time.Time + + systemDrawnEntities int + systemDrawnEntitiesV int + systemDrawnEntitiesT time.Time + + cacheTime time.Duration + + ctx *Context + + sync.Mutex +} + +func NewWorld() *World { + w := &World{ + cacheTime: time.Second, + } + + w.ctx = &Context{ + w: w, + } + + // Pad slices to match IDs starting with 1. + w.components = append(w.components, nil) + + return w +} + +func (w *World) attachEntitiesToSystem(systemIndex int) { + components := w.systemQueries[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 { + log.Printf("Attached entity %d to system %d.", entity, systemIndex) + } + } +} + +// AddSystem registers a system to start receiving Update and Draw calls. +func (w *World) AddSystem(system System) { + w.Lock() + defer w.Unlock() + + w.systems = append(w.systems, system) + w.systemQueries = append(w.systemQueries, system.Components()) + w.systemReceivesUpdate = append(w.systemReceivesUpdate, true) + w.systemReceivesDraw = append(w.systemReceivesDraw, true) + w.systemEntities = append(w.systemEntities, nil) + + w.attachEntitiesToSystem(len(w.systems) - 1) +} + +/* +// 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 (w *World) updateSystem(i int) (int, error) { + w.ctx.s = i + w.ctx.c = w.systemQueries[i] + updated := 0 + for _, entity := range w.systemEntities[i] { + w.ctx.Entity = entity + err := w.systems[i].Update(w.ctx) + if err != nil { + if err == ErrSystemWithoutUpdate { + // Unregister system from Update events. + w.systemReceivesUpdate[i] = false + return 0, nil + } + return 0, fmt.Errorf("failed to update system %d for entity %d: %+v", i, entity, err) + } + updated++ + } + return updated, nil +} + +func (w *World) propagateEntityChanges() { + entityMutex.Lock() + defer entityMutex.Unlock() + + for _, entity := range w.removedEntities { + // Remove from attached systems. + REMOVED: + for i := range w.systemEntities { + for j, e := range w.systemEntities[i] { + if e == entity { + w.systemEntities[i] = append(w.systemEntities[i][:j], w.systemEntities[i][j+1:]...) + continue REMOVED + } + } + } + + // Remove components. + w.components[entity] = make([]interface{}, w.maxComponentID+1) + } + + // Mark EntityIDs as available. + w.availableEntityIDs = append(w.availableEntityIDs, w.removedEntities...) + + w.removedEntities = nil + + for _, entity := range w.modifiedEntities { + for i, _ := range w.systems { + systemEntityIndex := -1 + for j, systemEntity := range w.systemEntities[i] { + if systemEntity == entity { + systemEntityIndex = j + break + } + } + + var skip bool + for _, c := range w.systemQueries[i] { + if w.Component(entity, c) == nil { + skip = true + break + } + } + if !skip { + if systemEntityIndex != -1 { + // Already attached. + continue + } + + w.systemEntities[i] = append(w.systemEntities[i], entity) + + if debug { + log.Printf("Attached entity %d to system %d.", entity, i) + } + } else if systemEntityIndex != -1 { + // Detach from system. + w.systemEntities[i] = append(w.systemEntities[i][:systemEntityIndex], w.systemEntities[i][systemEntityIndex+1:]...) + } + } + } + w.modifiedEntities = nil +} + +// Update updates the game state. +func (w *World) Update() error { + w.Lock() + defer w.Unlock() + + w.propagateEntityChanges() + + var t time.Time + if debug { + t = time.Now() + } + var systems int + var entitiesUpdated int + for i, registered := range w.systemReceivesUpdate { + if !registered { + continue + } + updated, err := w.updateSystem(i) + if err != nil { + return err + } + + entitiesUpdated += updated + systems++ + + if debug { + log.Printf("System %d: updated %d entities.", i, updated) + } + } + w.systemUpdatedEntities = entitiesUpdated + + if debug { + log.Printf("Finished updating %d systems in %.2fms.", systems, float64(time.Since(t).Microseconds())/1000) + } + 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 (w *World) UpdatedEntities() int { + if time.Since(w.systemUpdatedEntitiesT) >= w.cacheTime { + w.systemUpdatedEntitiesV = w.systemUpdatedEntities + w.systemUpdatedEntitiesT = time.Now() + } + return w.systemUpdatedEntitiesV +} + +func (w *World) drawSystem(i int, screen *ebiten.Image) (int, error) { + w.ctx.s = i + w.ctx.c = w.systemQueries[i] + var drawn int + for _, entity := range w.systemEntities[i] { + w.ctx.Entity = entity + err := w.systems[i].Draw(w.ctx, screen) + if err != nil { + if err == ErrSystemWithoutDraw { + // Unregister system from Draw events. + w.systemReceivesDraw[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 (w *World) Draw(screen *ebiten.Image) error { + w.Lock() + defer w.Unlock() + + w.propagateEntityChanges() + + var entitiesDrawn int + for i, registered := range w.systemReceivesDraw { + if !registered { + continue + } + + drawn, err := w.drawSystem(i, screen) + if err != nil { + return err + } + + entitiesDrawn += drawn + } + w.systemDrawnEntities = 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 (w *World) DrawnEntities() int { + if time.Since(w.systemDrawnEntitiesT) >= w.cacheTime { + w.systemDrawnEntitiesV = w.systemDrawnEntities + w.systemDrawnEntitiesT = time.Now() + } + return w.systemDrawnEntitiesV +} diff --git a/world_test.go b/world_test.go new file mode 100644 index 0000000..e33122b --- /dev/null +++ b/world_test.go @@ -0,0 +1,108 @@ +package gohan + +import ( + "math" + "testing" + + "github.com/hajimehoshi/ebiten/v2" +) + +type movementSystem struct { + positionComponentID ComponentID + velocityComponentID ComponentID +} + +func (s *movementSystem) Components() []ComponentID { + return []ComponentID{ + s.positionComponentID, + s.velocityComponentID, + } +} + +func (s *movementSystem) Update(ctx *Context) error { + position := ctx.Component(s.positionComponentID).(*positionComponent) + velocity := ctx.Component(s.velocityComponentID).(*velocityComponent) + + position.X, position.Y = position.X+velocity.X, position.Y+velocity.Y + return nil +} + +func (s *movementSystem) Draw(ctx *Context, screen *ebiten.Image) error { + return nil +} + +func TestWorld(t *testing.T) { + const iterations = 1024 + + w, e, positionComponentID, velocityComponentID := newTestWorld() + + position := w.Component(e, positionComponentID).(*positionComponent) + velocity := w.Component(e, velocityComponentID).(*velocityComponent) + + expectedX, expectedY := position.X+(velocity.X*iterations), position.Y+(velocity.Y*iterations) + + for i := 0; i < iterations; i++ { + err := w.Update() + if err != nil { + t.Fatal(err) + } + } + + // Fetch component again to ensure consistency. + position = w.Component(e, positionComponentID).(*positionComponent) + if round(position.X) != round(expectedX) || round(position.Y) != round(expectedY) { + t.Errorf("failed to update system: expected position (%f,%f), got (%f,%f)", expectedX, expectedY, position.X, position.Y) + } +} + +func BenchmarkUpdateWorld(b *testing.B) { + w, _, _, _ := newTestWorld() + + b.StopTimer() + b.ResetTimer() + b.ReportAllocs() + b.StartTimer() + + for i := 0; i < b.N; i++ { + err := w.Update() + if err != nil { + b.Fatal(err) + } + } +} + +func newTestWorld() (w *World, e Entity, positionComponentID ComponentID, velocityComponentID ComponentID) { + w = NewWorld() + + e = w.NewEntity() + + positionComponentID = w.NewComponentID() + position := &positionComponent{ + componentID: positionComponentID, + X: 108, + Y: 0, + } + w.AddComponent(e, position) + + velocityComponentID = w.NewComponentID() + velocity := &velocityComponent{ + componentID: velocityComponentID, + X: -0.1, + Y: 0.2, + } + w.AddComponent(e, velocity) + + movement := &movementSystem{ + positionComponentID: positionComponentID, + velocityComponentID: velocityComponentID, + } + w.AddSystem(movement) + + return w, e, positionComponentID, velocityComponentID +} + +// Round values to eliminate floating point precision errors. This is only +// necessary during testing because we validate the final values. +func round(f float64) float64 { + return math.Round(f*10) / 10 +}