Browse Source

Add ActiveEntities, UpdatedEntities and DrawnEntities

These functions return information particularly useful when debugging
and profiling an application.
main
Trevor Slocum 2 months ago
parent
commit
a820927a43
  1. 49
      component.go
  2. 30
      doc.go
  3. 52
      entity.go
  4. 8
      examples/twinstick/component/bullet.go
  5. 8
      examples/twinstick/component/position.go
  6. 8
      examples/twinstick/component/velocity.go
  7. 8
      examples/twinstick/component/weapon.go
  8. 2
      examples/twinstick/entity/bullet.go
  9. 2
      examples/twinstick/entity/player.go
  10. 14
      examples/twinstick/game/game.go
  11. 2
      examples/twinstick/main.go
  12. 4
      examples/twinstick/system/draw_bullets.go
  13. 4
      examples/twinstick/system/draw_player.go
  14. 18
      examples/twinstick/system/input_fire.go
  15. 2
      examples/twinstick/system/input_move.go
  16. 65
      examples/twinstick/system/input_profile.go
  17. 48
      examples/twinstick/system/movement.go
  18. 4
      examples/twinstick/system/printinfo.go
  19. 157
      gohan.go

49
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
return nextComponentID
}
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
}
// AddComponent adds a Component to an Entity.
func (entity EntityID) AddComponent(component Component) {
componentMutex.Lock()
defer componentMutex.Unlock()
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:]...)
}
if wasRemoved(entity) {
return
}
}
// AddComponent adds a Component to an Entity.
func (entity EntityID) AddComponent(component Component) {
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

30
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

52
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
}

8
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
}

8
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
}

8
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
}

8
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
}

2
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 {

2
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 {

14
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 {

2
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"
)

4
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"
)

4
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"
)

18
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)

2
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"
)

65
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
}

48
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
}
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
}
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.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
}

4
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
}

157
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,23 @@ 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)
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)
@ -61,6 +92,55 @@ func RegisterSystem(system System) {
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
@ -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
}

Loading…
Cancel
Save