Refactor System interface

Systems now specify their components via public fields.
This commit is contained in:
Trevor Slocum 2022-01-31 20:07:55 -08:00
parent 0231e09ad7
commit 193532a951
25 changed files with 398 additions and 550 deletions

View File

@ -12,7 +12,7 @@ for [Ebiten](https://ebiten.org)
Documentation is available via [godoc](https://docs.rocketnine.space/code.rocketnine.space/tslocum/gohan).
An [example game](https://rocketnine.itch.io/gohan-twinstick) is included at
An [example game](https://rocketnine.itch.io/gohan-twinstick?secret=gohan) is included at
`/examples/twinstick`. See godoc for build instructions.
## Support

View File

@ -1,57 +1,76 @@
package gohan
import (
"strconv"
"reflect"
)
// ComponentID is a component identifier. Each Component is assigned a unique ID
// via World.NewComponentID, and implements a ComponentID method returning its ID.
type ComponentID int
// Component represents data for an entity, and how it interacts with the world.
type Component interface {
ComponentID() ComponentID
}
// NewComponentID returns the next available ComponentID.
func (w *World) NewComponentID() ComponentID {
w.entityMutex.Lock()
defer w.entityMutex.Unlock()
w.componentMutex.Lock()
defer w.componentMutex.Unlock()
// componentID is a component identifier. Each Component is assigned a unique ID
// via world.NewComponentID, and implements a componentID method returning its ID.
type componentID int
// newComponentID returns the next available componentID.
func newComponentID() componentID {
w.maxComponentID++
for i := Entity(1); i <= w.maxEntityID; i++ {
w.components[i] = append(w.components[i], nil)
}
w.systemComponentNames = append(w.systemComponentNames, strconv.Itoa(int(w.maxComponentID)))
return w.maxComponentID
}
func componentIDByValue(v interface{}) componentID {
sV := reflect.ValueOf(v)
sT := reflect.TypeOf(v)
if sV.Kind() == reflect.Ptr {
sV = sV.Elem()
sT = sT.Elem()
}
componentName := sV.Type().String()
return componentIDByName(componentName)
}
func componentIDByName(name string) componentID {
if len(name) == 0 {
return 0
}
if name[0:1] == "*" {
name = name[1:]
}
if !w.haveSystemComponentName[name] {
w.systemComponentNames = append(w.systemComponentNames, name)
w.haveSystemComponentName[name] = true
id := newComponentID() // ComponentNames index now aligns with componentID
return id
}
for i, savedName := range w.systemComponentNames {
if savedName == name {
return componentID(i)
}
}
return 0
}
// AddComponent adds a Component to an Entity.
func (w *World) AddComponent(entity Entity, component Component) {
func (entity Entity) AddComponent(component interface{}) {
w.componentMutex.Lock()
defer w.componentMutex.Unlock()
componentID := component.ComponentID()
if debug != 0 && !w.haveSystemComponentName[componentID] {
w.systemComponentNames[componentID] = getName(component)
w.haveSystemComponentName[componentID] = true
}
w.components[entity][componentID] = component
id := componentIDByValue(component)
w.components[entity][id] = component
w.entityMutex.Lock()
defer w.entityMutex.Unlock()
w.modifiedEntities = append(w.modifiedEntities, entity)
}
// Component gets a Component of an Entity.
func (w *World) Component(entity Entity, componentID ComponentID) interface{} {
// getComponent gets a Component of an Entity.
func (entity Entity) getComponent(componentID componentID) interface{} {
components := w.components[entity]
if components == nil {
return nil

View File

@ -3,36 +3,35 @@ package gohan
import "testing"
type positionComponent struct {
componentID ComponentID
componentID componentID
X, Y float64
}
func (c *positionComponent) ComponentID() ComponentID {
func (c *positionComponent) ComponentID() componentID {
return c.componentID
}
type velocityComponent struct {
componentID ComponentID
componentID componentID
X, Y float64
}
func (c *velocityComponent) ComponentID() ComponentID {
func (c *velocityComponent) ComponentID() componentID {
return c.componentID
}
func BenchmarkComponent(b *testing.B) {
w := NewWorld()
Reset()
e := w.NewEntity()
e := NewEntity()
positionComponentID := w.NewComponentID()
w.AddComponent(e, &positionComponent{
X: 108,
Y: 0,
componentID: positionComponentID,
e.AddComponent(&positionComponent{
X: 108,
Y: 0,
})
positionComponentID := componentID(1)
b.StopTimer()
b.ResetTimer()
@ -40,20 +39,18 @@ func BenchmarkComponent(b *testing.B) {
b.StartTimer()
for i := 0; i < b.N; i++ {
_ = w.Component(e, positionComponentID)
_ = e.getComponent(positionComponentID)
}
}
func BenchmarkAddComponent(b *testing.B) {
w := NewWorld()
Reset()
e := w.NewEntity()
e := NewEntity()
positionComponentID := w.NewComponentID()
c := &positionComponent{
X: 108,
Y: 0,
componentID: positionComponentID,
X: 108,
Y: 0,
}
b.StopTimer()
@ -62,6 +59,6 @@ func BenchmarkAddComponent(b *testing.B) {
b.StartTimer()
for i := 0; i < b.N; i++ {
w.AddComponent(e, c)
e.AddComponent(c)
}
}

View File

@ -1,25 +1,8 @@
package gohan
// Context represents the current iteration of a System's matching entities. It
// provides methods for retrieving components for the currently matched Entity,
// and removing the currently matched Entity.
type Context struct {
Entity Entity
allowed []ComponentID
// context represents the current iteration of a System's matching entities.
type context struct {
allowed []componentID
components []interface{}
systemIndex int
world *World
}
// Component gets a Component of the currently handled Entity.
func (ctx *Context) Component(componentID ComponentID) interface{} {
return ctx.components[componentID]
}
// RemoveEntity removes the currently handled Entity's components, causing it
// to no longer be handled by any system. Because Gohan reuses removed Entity
// IDs, applications must also remove any other references to the removed Entity.
func (ctx *Context) RemoveEntity() bool {
return ctx.world.RemoveEntity(ctx.Entity)
}

25
doc.go
View File

@ -15,15 +15,21 @@ 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.
type ExampleComponent struct {
X, Y float64
}
System
Each system runs continuously, performing actions on every Entity that fits
each systems' set of required matching components.
World
Each entity, component and system belongs to an isolated world. Applications
may utilize one or multiple worlds.
type ExampleSystem struct {
Position *component.PositionComponent // Required component.
Velocity *component.VelocityComponent // Required component.
Sprite *component.SpriteComponent `gohan:"?"` // Optional component.
Enabled bool `gohan:"-"` // Not a component.
}
Component Design Guidelines
@ -32,17 +38,6 @@ 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.
Rather than accessing components via Context.Component directly, using helper
functions (such as the following) helps to reduce code verbosity.
func Position(ctx *gohan.Context) *PositionComponent {
c, ok := ctx.Component(PositionComponentID).(*PositionComponent)
if !ok {
return nil
}
return c
}
System Design Guidelines
Systems are located in a separate package, typically named system. They should

View File

@ -9,7 +9,7 @@ type Entity int
// NewEntity returns a new (or previously removed and cleared) Entity. Because
// Gohan reuses removed Entity IDs, a previously removed ID may be returned.
func (w *World) NewEntity() Entity {
func NewEntity() Entity {
w.entityMutex.Lock()
defer w.entityMutex.Unlock()
@ -30,10 +30,10 @@ func (w *World) NewEntity() Entity {
return w.maxEntityID
}
// RemoveEntity removes the provided Entity's components, causing it to no
// 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 (w *World) RemoveEntity(entity Entity) bool {
func (entity Entity) Remove() bool {
w.entityMutex.Lock()
defer w.entityMutex.Unlock()
@ -53,8 +53,9 @@ func (w *World) RemoveEntity(entity Entity) bool {
return false
}
// Entities returns all active entities.
func (w *World) Entities() []Entity {
// AllEntities returns a slice of all active entities. To retrieve only the
// number of currently active entities, use CurrentEntities.
func AllEntities() []Entity {
w.entityMutex.Lock()
defer w.entityMutex.Unlock()
allEntities := make([]Entity, len(w.allEntities))
@ -66,7 +67,7 @@ var numEntities int
var numEntitiesT time.Time
// CurrentEntities returns the number of currently active entities.
func (w *World) CurrentEntities() int {
func CurrentEntities() int {
if time.Since(numEntitiesT) >= w.cacheTime {
numEntities = len(w.allEntities)
numEntitiesT = time.Now()

View File

@ -5,45 +5,45 @@ import (
)
func TestActiveEntities(t *testing.T) {
w := NewWorld()
Reset()
w.cacheTime = 0
active := w.CurrentEntities()
active := CurrentEntities()
if active != 0 {
t.Fatalf("expected 0 active entities, got %d", active)
}
active = w.CurrentEntities()
active = CurrentEntities()
if active != 0 {
t.Fatalf("expected 0 active entities, got %d", active)
}
// Create entity.
e1 := w.NewEntity()
e1 := NewEntity()
active = w.CurrentEntities()
active = CurrentEntities()
if active != 1 {
t.Fatalf("expected 1 active entities, got %d", active)
}
// Create entity.
e2 := w.NewEntity()
e2 := NewEntity()
active = w.CurrentEntities()
active = CurrentEntities()
if active != 2 {
t.Fatalf("expected 2 active entities, got %d", active)
}
w.RemoveEntity(e1)
e1.Remove()
active = w.CurrentEntities()
active = CurrentEntities()
if active != 1 {
t.Fatalf("expected 1 active entities, got %d", active)
}
w.RemoveEntity(e2)
e2.Remove()
active = w.CurrentEntities()
active = CurrentEntities()
if active != 0 {
t.Fatalf("expected 0 active entities, got %d", active)
}

View File

@ -3,24 +3,5 @@
package component
import (
"code.rocketnine.space/tslocum/gohan"
"code.rocketnine.space/tslocum/gohan/examples/twinstick/world"
)
type BulletComponent struct {
}
var BulletComponentID = world.World.NewComponentID()
func (p *BulletComponent) ComponentID() gohan.ComponentID {
return BulletComponentID
}
func Bullet(ctx *gohan.Context) *BulletComponent {
c, ok := ctx.Component(BulletComponentID).(*BulletComponent)
if !ok {
return nil
}
return c
}

View File

@ -3,25 +3,6 @@
package component
import (
"code.rocketnine.space/tslocum/gohan"
"code.rocketnine.space/tslocum/gohan/examples/twinstick/world"
)
type PositionComponent struct {
X, Y float64
}
var PositionComponentID = world.World.NewComponentID()
func (p *PositionComponent) ComponentID() gohan.ComponentID {
return PositionComponentID
}
func Position(ctx *gohan.Context) *PositionComponent {
c, ok := ctx.Component(PositionComponentID).(*PositionComponent)
if !ok {
return nil
}
return c
}

View File

@ -3,25 +3,6 @@
package component
import (
"code.rocketnine.space/tslocum/gohan"
"code.rocketnine.space/tslocum/gohan/examples/twinstick/world"
)
type VelocityComponent struct {
X, Y float64
}
var VelocityComponentID = world.World.NewComponentID()
func (c *VelocityComponent) ComponentID() gohan.ComponentID {
return VelocityComponentID
}
func Velocity(ctx *gohan.Context) *VelocityComponent {
c, ok := ctx.Component(VelocityComponentID).(*VelocityComponent)
if !ok {
return nil
}
return c
}

View File

@ -5,10 +5,6 @@ package component
import (
"time"
"code.rocketnine.space/tslocum/gohan/examples/twinstick/world"
"code.rocketnine.space/tslocum/gohan"
)
type WeaponComponent struct {
@ -21,17 +17,3 @@ type WeaponComponent struct {
BulletSpeed float64
}
var WeaponComponentID = world.World.NewComponentID()
func (p *WeaponComponent) ComponentID() gohan.ComponentID {
return WeaponComponentID
}
func Weapon(ctx *gohan.Context) *WeaponComponent {
c, ok := ctx.Component(WeaponComponentID).(*WeaponComponent)
if !ok {
return nil
}
return c
}

View File

@ -6,23 +6,22 @@ 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 := world.World.NewEntity()
bullet := gohan.NewEntity()
world.World.AddComponent(bullet, &component.PositionComponent{
bullet.AddComponent(&component.PositionComponent{
X: x,
Y: y,
})
world.World.AddComponent(bullet, &component.VelocityComponent{
bullet.AddComponent(&component.VelocityComponent{
X: xSpeed,
Y: ySpeed,
})
world.World.AddComponent(bullet, &component.BulletComponent{})
bullet.AddComponent(&component.BulletComponent{})
return bullet
}

View File

@ -7,24 +7,22 @@ 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 := world.World.NewEntity()
player := gohan.NewEntity()
// Set position to -1,-1 to indicate the player has not been assigned a
// position yet. We will place the player in the center of the screen when
// we receive the screen dimensions for the first time.
world.World.AddComponent(player, &component.PositionComponent{
player.AddComponent(&component.PositionComponent{
X: -1,
Y: -1,
})
world.World.AddComponent(player, &component.VelocityComponent{})
player.AddComponent(&component.VelocityComponent{})
weapon := &component.WeaponComponent{
Ammo: math.MaxInt64,
@ -32,7 +30,7 @@ func NewPlayer() gohan.Entity {
FireRate: 100 * time.Millisecond,
BulletSpeed: 15,
}
world.World.AddComponent(player, weapon)
player.AddComponent(weapon)
return player
}

View File

@ -12,7 +12,6 @@ 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/entity"
"code.rocketnine.space/tslocum/gohan/examples/twinstick/system"
"github.com/hajimehoshi/ebiten/v2"
@ -31,8 +30,6 @@ type game struct {
debugMode bool
cpuProfile *os.File
movementSystem *system.MovementSystem
sync.Mutex
}
@ -42,7 +39,7 @@ func NewGame() (*game, error) {
op: &ebiten.DrawImageOptions{},
}
g.player = entity.NewPlayer()
world.Player = entity.NewPlayer()
g.addSystems()
@ -53,7 +50,7 @@ func NewGame() (*game, error) {
asset.ImgWhiteSquare.Fill(color.White)
world.World.Preallocate(10000)
gohan.Preallocate(10000)
return g, nil
}
@ -63,13 +60,8 @@ 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)
world.ScreenW, world.ScreenH = float64(w), float64(h)
}
return g.w, g.h
}
@ -80,38 +72,27 @@ func (g *game) Update() error {
return nil
}
return world.World.Update()
return gohan.Update()
}
func (g *game) Draw(screen *ebiten.Image) {
err := world.World.Draw(screen)
err := gohan.Draw(screen)
if err != nil {
panic(err)
}
}
func (g *game) addSystems() {
w := world.World
// Handle input.
gohan.AddSystem(system.NewProfileSystem())
gohan.AddSystem(system.NewMovementInputSystem())
gohan.AddSystem(system.NewMovementSystem())
gohan.AddSystem(system.NewFireInputSystem())
w.AddSystem(system.NewMovementInputSystem(g.player))
g.movementSystem = &system.MovementSystem{
Player: g.player,
}
w.AddSystem(g.movementSystem)
w.AddSystem(system.NewFireInputSystem(g.player))
renderBullet := system.NewDrawBulletsSystem()
w.AddSystem(renderBullet)
renderPlayer := system.NewDrawPlayerSystem(g.player)
w.AddSystem(renderPlayer)
printInfo := system.NewPrintInfoSystem(g.player)
w.AddSystem(printInfo)
w.AddSystem(system.NewProfileSystem(g.player))
// Render game.
gohan.AddSystem(system.NewDrawBulletsSystem())
gohan.AddSystem(system.NewDrawPlayerSystem())
gohan.AddSystem(system.NewPrintInfoSystem())
}
func (g *game) loadAssets() error {

View File

@ -11,6 +11,9 @@ import (
)
type DrawBulletsSystem struct {
Position *component.PositionComponent
Bullet *component.BulletComponent
op *ebiten.DrawImageOptions
}
@ -20,28 +23,15 @@ func NewDrawBulletsSystem() *DrawBulletsSystem {
}
}
func (s *DrawBulletsSystem) Needs() []gohan.ComponentID {
return []gohan.ComponentID{
component.PositionComponentID,
component.BulletComponentID,
}
func (s *DrawBulletsSystem) Update(_ gohan.Entity) error {
return gohan.ErrUnregister
}
func (s *DrawBulletsSystem) Uses() []gohan.ComponentID {
return nil
}
func (s *DrawBulletsSystem) Update(_ *gohan.Context) error {
return gohan.ErrSystemWithoutUpdate
}
func (s *DrawBulletsSystem) Draw(ctx *gohan.Context, screen *ebiten.Image) error {
position := component.Position(ctx)
func (s *DrawBulletsSystem) Draw(_ gohan.Entity, screen *ebiten.Image) error {
s.op.GeoM.Reset()
s.op.GeoM.Translate(-16, -16)
s.op.GeoM.Scale(0.5, 0.5)
s.op.GeoM.Translate(position.X, position.Y)
s.op.GeoM.Translate(s.Position.X, s.Position.Y)
screen.DrawImage(asset.ImgWhiteSquare, s.op)
return nil
}

View File

@ -11,37 +11,25 @@ import (
)
type drawPlayerSystem struct {
player gohan.Entity
op *ebiten.DrawImageOptions
Position *component.PositionComponent
Weapon *component.WeaponComponent
op *ebiten.DrawImageOptions
}
func NewDrawPlayerSystem(player gohan.Entity) *drawPlayerSystem {
func NewDrawPlayerSystem() *drawPlayerSystem {
return &drawPlayerSystem{
player: player,
op: &ebiten.DrawImageOptions{},
op: &ebiten.DrawImageOptions{},
}
}
func (s *drawPlayerSystem) Needs() []gohan.ComponentID {
return []gohan.ComponentID{
component.PositionComponentID,
component.WeaponComponentID,
}
func (s *drawPlayerSystem) Update(_ gohan.Entity) error {
return gohan.ErrUnregister
}
func (s *drawPlayerSystem) Uses() []gohan.ComponentID {
return nil
}
func (s *drawPlayerSystem) Update(_ *gohan.Context) error {
return gohan.ErrSystemWithoutUpdate
}
func (s *drawPlayerSystem) Draw(ctx *gohan.Context, screen *ebiten.Image) error {
position := component.Position(ctx)
func (s *drawPlayerSystem) Draw(entity gohan.Entity, screen *ebiten.Image) error {
s.op.GeoM.Reset()
s.op.GeoM.Translate(position.X-16, position.Y-16)
s.op.GeoM.Translate(s.Position.X-16, s.Position.Y-16)
screen.DrawImage(asset.ImgWhiteSquare, s.op)
return nil
}

View File

@ -18,91 +18,76 @@ func angle(x1, y1, x2, y2 float64) float64 {
}
type fireInputSystem struct {
player gohan.Entity
Position *component.PositionComponent
Weapon *component.WeaponComponent
}
func NewFireInputSystem(player gohan.Entity) *fireInputSystem {
return &fireInputSystem{
player: player,
}
func NewFireInputSystem() *fireInputSystem {
return &fireInputSystem{}
}
func (_ *fireInputSystem) Needs() []gohan.ComponentID {
return []gohan.ComponentID{
component.PositionComponentID,
component.WeaponComponentID,
}
}
func (s *fireInputSystem) Uses() []gohan.ComponentID {
return nil
}
func (s *fireInputSystem) fire(weapon *component.WeaponComponent, position *component.PositionComponent, fireAngle float64) {
if time.Since(weapon.LastFire) < weapon.FireRate {
func (s *fireInputSystem) fire(fireAngle float64) {
if time.Since(s.Weapon.LastFire) < s.Weapon.FireRate {
return
}
weapon.Ammo--
weapon.LastFire = time.Now()
s.Weapon.Ammo--
s.Weapon.LastFire = time.Now()
speedX := math.Cos(fireAngle) * -weapon.BulletSpeed
speedY := math.Sin(fireAngle) * -weapon.BulletSpeed
speedX := math.Cos(fireAngle) * -s.Weapon.BulletSpeed
speedY := math.Sin(fireAngle) * -s.Weapon.BulletSpeed
bullet := entity.NewBullet(position.X, position.Y, speedX, speedY)
bullet := entity.NewBullet(s.Position.X, s.Position.Y, speedX, speedY)
_ = bullet
}
func (s *fireInputSystem) Update(ctx *gohan.Context) error {
position := component.Position(ctx)
weapon := component.Weapon(ctx)
if weapon.Ammo <= 0 {
func (s *fireInputSystem) Update(entity gohan.Entity) error {
if s.Weapon.Ammo <= 0 {
return nil
}
if ebiten.IsMouseButtonPressed(ebiten.MouseButtonLeft) {
cursorX, cursorY := ebiten.CursorPosition()
fireAngle := angle(position.X, position.Y, float64(cursorX), float64(cursorY))
fireAngle := angle(s.Position.X, s.Position.Y, float64(cursorX), float64(cursorY))
s.fire(weapon, position, fireAngle)
s.fire(fireAngle)
}
if ebiten.IsMouseButtonPressed(ebiten.MouseButtonRight) {
cursorX, cursorY := ebiten.CursorPosition()
fireAngle := angle(position.X, position.Y, float64(cursorX), float64(cursorY))
fireAngle := angle(s.Position.X, s.Position.Y, float64(cursorX), float64(cursorY))
const div = 5
weapon.BulletSpeed /= div
s.Weapon.BulletSpeed /= div
for i := 0.0; i < 24; i++ {
s.fire(weapon, position, fireAngle+i*(math.Pi/12))
weapon.LastFire = time.Time{}
s.fire(fireAngle + i*(math.Pi/12))
s.Weapon.LastFire = time.Time{}
}
weapon.BulletSpeed *= div
s.Weapon.BulletSpeed *= div
}
switch {
case ebiten.IsKeyPressed(ebiten.KeyLeft) && ebiten.IsKeyPressed(ebiten.KeyUp):
s.fire(weapon, position, math.Pi/4)
s.fire(math.Pi / 4)
case ebiten.IsKeyPressed(ebiten.KeyLeft) && ebiten.IsKeyPressed(ebiten.KeyDown):
s.fire(weapon, position, -math.Pi/4)
s.fire(-math.Pi / 4)
case ebiten.IsKeyPressed(ebiten.KeyRight) && ebiten.IsKeyPressed(ebiten.KeyUp):
s.fire(weapon, position, math.Pi*.75)
s.fire(math.Pi * .75)
case ebiten.IsKeyPressed(ebiten.KeyRight) && ebiten.IsKeyPressed(ebiten.KeyDown):
s.fire(weapon, position, -math.Pi*.75)
s.fire(-math.Pi * .75)
case ebiten.IsKeyPressed(ebiten.KeyLeft):
s.fire(weapon, position, 0)
s.fire(0)
case ebiten.IsKeyPressed(ebiten.KeyRight):
s.fire(weapon, position, math.Pi)
s.fire(math.Pi)
case ebiten.IsKeyPressed(ebiten.KeyUp):
s.fire(weapon, position, math.Pi/2)
s.fire(math.Pi / 2)
case ebiten.IsKeyPressed(ebiten.KeyDown):
s.fire(weapon, position, -math.Pi/2)
s.fire(-math.Pi / 2)
}
return nil
}
func (_ *fireInputSystem) Draw(_ *gohan.Context, _ *ebiten.Image) error {
return gohan.ErrSystemWithoutDraw
func (_ *fireInputSystem) Draw(_ gohan.Entity, _ *ebiten.Image) error {
return gohan.ErrUnregister
}

View File

@ -10,55 +10,42 @@ import (
)
type movementInputSystem struct {
player gohan.Entity
Velocity *component.VelocityComponent
Weapon *component.WeaponComponent
}
func NewMovementInputSystem(player gohan.Entity) *movementInputSystem {
return &movementInputSystem{
player: player,
}
func NewMovementInputSystem() *movementInputSystem {
return &movementInputSystem{}
}
func (s *movementInputSystem) Needs() []gohan.ComponentID {
return []gohan.ComponentID{
component.VelocityComponentID,
component.WeaponComponentID,
}
}
func (s *movementInputSystem) Uses() []gohan.ComponentID {
return nil
}
func (s *movementInputSystem) Update(ctx *gohan.Context) error {
velocity := component.Velocity(ctx)
func (s *movementInputSystem) Update(entity gohan.Entity) error {
if ebiten.IsKeyPressed(ebiten.KeyA) {
velocity.X -= 0.5
if velocity.X < -5 {
velocity.X = -5
s.Velocity.X -= 0.5
if s.Velocity.X < -5 {
s.Velocity.X = -5
}
}
if ebiten.IsKeyPressed(ebiten.KeyD) {
velocity.X += 0.5
if velocity.X > 5 {
velocity.X = 5
s.Velocity.X += 0.5
if s.Velocity.X > 5 {
s.Velocity.X = 5
}
}
if ebiten.IsKeyPressed(ebiten.KeyW) {
velocity.Y -= 0.5
if velocity.Y < -5 {
velocity.Y = -5
s.Velocity.Y -= 0.5
if s.Velocity.Y < -5 {
s.Velocity.Y = -5
}
}
if ebiten.IsKeyPressed(ebiten.KeyS) {
velocity.Y += 0.5
if velocity.Y > 5 {
velocity.Y = 5
s.Velocity.Y += 0.5
if s.Velocity.Y > 5 {
s.Velocity.Y = 5
}
}
return nil
}
func (s *movementInputSystem) Draw(_ *gohan.Context, _ *ebiten.Image) error {
return gohan.ErrSystemWithoutDraw
func (s *movementInputSystem) Draw(_ gohan.Entity, _ *ebiten.Image) error {
return gohan.ErrUnregister
}

View File

@ -10,34 +10,24 @@ import (
"runtime"
"runtime/pprof"
"code.rocketnine.space/tslocum/gohan"
"code.rocketnine.space/tslocum/gohan/examples/twinstick/component"
"code.rocketnine.space/tslocum/gohan"
"github.com/hajimehoshi/ebiten/v2"
"github.com/hajimehoshi/ebiten/v2/inpututil"
)
type profileSystem struct {
player gohan.Entity
Weapon *component.WeaponComponent
cpuProfile *os.File
}
func NewProfileSystem(player gohan.Entity) *profileSystem {
return &profileSystem{
player: player,
}
func NewProfileSystem() *profileSystem {
return &profileSystem{}
}
func (s *profileSystem) Needs() []gohan.ComponentID {
return []gohan.ComponentID{
component.WeaponComponentID,
}
}
func (s *profileSystem) Uses() []gohan.ComponentID {
return nil
}
func (s *profileSystem) Update(_ *gohan.Context) error {
func (s *profileSystem) Update(_ gohan.Entity) error {
if ebiten.IsKeyPressed(ebiten.KeyControl) && inpututil.IsKeyJustPressed(ebiten.KeyP) {
if s.cpuProfile == nil {
log.Println("CPU profiling started...")
@ -70,6 +60,6 @@ func (s *profileSystem) Update(_ *gohan.Context) error {
return nil
}
func (s *profileSystem) Draw(_ *gohan.Context, _ *ebiten.Image) error {
return gohan.ErrSystemWithoutDraw
func (s *profileSystem) Draw(_ gohan.Entity, _ *ebiten.Image) error {
return gohan.ErrUnregister
}

View File

@ -6,77 +6,73 @@ package system
import (
"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"
)
type MovementSystem struct {
ScreenW, ScreenH float64
Player gohan.Entity
Position *component.PositionComponent
Velocity *component.VelocityComponent
}
func (_ *MovementSystem) Needs() []gohan.ComponentID {
return []gohan.ComponentID{
component.PositionComponentID,
component.VelocityComponentID,
func NewMovementSystem() *MovementSystem {
return &MovementSystem{}
}
func (s *MovementSystem) Update(entity gohan.Entity) error {
bullet := entity != world.Player
// Position the player at the center of the screen when the game starts.
if !bullet && s.Position.X == -1 && s.Position.Y == -1 {
s.Position.X, s.Position.Y = float64(world.ScreenW)/2-16, float64(world.ScreenH)/2-16
}
}
func (s *MovementSystem) Uses() []gohan.ComponentID {
return nil
}
func (s *MovementSystem) Update(ctx *gohan.Context) error {
position := component.Position(ctx)
velocity := component.Velocity(ctx)
bullet := ctx.Entity != s.Player
// Check for collision.
if position.X+velocity.X < 16 {
if s.Position.X+s.Velocity.X < 16 {
if bullet {
ctx.RemoveEntity()
entity.Remove()
return nil
}
position.X = 16
velocity.X = 0
} else if position.X+velocity.X > s.ScreenW-16 {
s.Position.X = 16
s.Velocity.X = 0
} else if s.Position.X+s.Velocity.X > world.ScreenW-16 {
if bullet {
ctx.RemoveEntity()
entity.Remove()
return nil
}
position.X = s.ScreenW - 16
velocity.X = 0
s.Position.X = world.ScreenW - 16
s.Velocity.X = 0
}
if position.Y+velocity.Y < 16 {
if s.Position.Y+s.Velocity.Y < 16 {
if bullet {
ctx.RemoveEntity()
entity.Remove()
return nil
}
position.Y = 16
velocity.Y = 0
} else if position.Y+velocity.Y > s.ScreenH-16 {
s.Position.Y = 16
s.Velocity.Y = 0
} else if s.Position.Y+s.Velocity.Y > world.ScreenH-16 {
if bullet {
ctx.RemoveEntity()
entity.Remove()
return nil
}
position.Y = s.ScreenH - 16
velocity.Y = 0
s.Position.Y = world.ScreenH - 16
s.Velocity.Y = 0
}
position.X, position.Y = position.X+velocity.X, position.Y+velocity.Y
s.Position.X, s.Position.Y = s.Position.X+s.Velocity.X, s.Position.Y+s.Velocity.Y
if !bullet {
velocity.X *= 0.95
velocity.Y *= 0.95
s.Velocity.X *= 0.95
s.Velocity.Y *= 0.95
}
return nil
}
func (_ *MovementSystem) Draw(_ *gohan.Context, _ *ebiten.Image) error {
return gohan.ErrSystemWithoutDraw
func (_ *MovementSystem) Draw(_ gohan.Entity, _ *ebiten.Image) error {
return gohan.ErrUnregister
}

View File

@ -6,48 +6,36 @@ package system
import (
"fmt"
"code.rocketnine.space/tslocum/gohan"
"code.rocketnine.space/tslocum/gohan/examples/twinstick/component"
"code.rocketnine.space/tslocum/gohan/examples/twinstick/world"
"code.rocketnine.space/tslocum/gohan"
"github.com/hajimehoshi/ebiten/v2"
"github.com/hajimehoshi/ebiten/v2/ebitenutil"
)
type printInfoSystem struct {
img *ebiten.Image
op *ebiten.DrawImageOptions
player gohan.Entity
Weapon *component.WeaponComponent
img *ebiten.Image
op *ebiten.DrawImageOptions
}
func NewPrintInfoSystem(player gohan.Entity) *printInfoSystem {
func NewPrintInfoSystem() *printInfoSystem {
p := &printInfoSystem{
img: ebiten.NewImage(200, 100),
op: &ebiten.DrawImageOptions{},
player: player,
img: ebiten.NewImage(200, 100),
op: &ebiten.DrawImageOptions{},
}
p.op.GeoM.Scale(2, 2)
return p
}
func (s *printInfoSystem) Needs() []gohan.ComponentID {
return []gohan.ComponentID{
component.WeaponComponentID,
}
func (s *printInfoSystem) Update(_ gohan.Entity) error {
return gohan.ErrUnregister
}
func (s *printInfoSystem) Uses() []gohan.ComponentID {
return nil
}
func (s *printInfoSystem) Update(_ *gohan.Context) error {
return gohan.ErrSystemWithoutUpdate
}
func (s *printInfoSystem) Draw(_ *gohan.Context, screen *ebiten.Image) error {
w := world.World
func (s *printInfoSystem) Draw(_ gohan.Entity, screen *ebiten.Image) error {
s.img.Clear()
ebitenutil.DebugPrint(s.img, fmt.Sprintf("KEY WASD+MOUSE\nENT %d\nUPD %d\nDRA %d\nTPS %0.0f\nFPS %0.0f", w.CurrentEntities(), w.CurrentUpdates(), w.CurrentDraws(), 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.CurrentEntities(), gohan.CurrentUpdates(), gohan.CurrentDraws(), ebiten.CurrentTPS(), ebiten.CurrentFPS()))
screen.DrawImage(s.img, s.op)
return nil
}

View File

@ -5,4 +5,8 @@ package world
import "code.rocketnine.space/tslocum/gohan"
var World = gohan.NewWorld()
var (
Player gohan.Entity
ScreenW, ScreenH float64
)

View File

@ -6,36 +6,21 @@ import (
"github.com/hajimehoshi/ebiten/v2"
)
// System represents a system that runs continuously. While the system must
// implement the Update and Draw methods, a special error value may be returned
// indicating that the system does not utilize one of the methods. System
// methods which return one of these special error values will not be called again.
// System represents a system that runs continuously.
//
// See the special error values ErrSystemWithoutUpdate and ErrSystemWithoutDraw.
// While systems must implement the Update and Draw methods, the special error
// value ErrUnregister may be returned at any time by systems to indicate the
// method returning the error should not be called again.
//
// Systems do not need to implement locking to prevent race conditions between
// Update and Draw methods. Ebiten calls only one of these methods at a time.
type System interface {
// Needs returns a list of components (specified by ID) which are required
// for an Entity to be handled by the System. When the game is running,
// matching entities will be passed to the Update and Draw methods.
Needs() []ComponentID
// Uses returns a list of components (specified by ID) which are used by
// the System, in addition to any required components. Because required
// components are automatically included in this list, Uses should only
// return components which are not also returned by Needs.
Uses() []ComponentID
// Update is called once for each matching Entity each time the game state is updated.
Update(ctx *Context) error
Update(entity Entity) error
// Draw is called once for each matching Entity each time the game is drawn to the screen.
Draw(ctx *Context, screen *ebiten.Image) error
Draw(entity Entity, screen *ebiten.Image) error
}
// Special error values.
var (
// ErrSystemWithoutUpdate is the error returned when a System does not implement Update.
ErrSystemWithoutUpdate = errors.New("system does not implement update")
// ErrSystemWithoutDraw is the error returned when a System does not implement Draw.
ErrSystemWithoutDraw = errors.New("system does not implement draw")
)
// ErrUnregister is the error returned to unregister from Draw or Update events.
var ErrUnregister = errors.New("unregister system")

176
world.go
View File

@ -31,13 +31,15 @@ func init() {
}
}
// World represents a collection of Entities, components and Systems.
type World struct {
var w = newWorld()
// world represents a collection of AllEntities, components and Systems.
type world struct {
maxEntityID Entity
maxComponentID ComponentID
maxComponentID componentID
components [][]interface{} // components[Entity][ComponentID]Component
components [][]interface{} // components[Entity][componentID]Component
allEntities []Entity
@ -50,12 +52,13 @@ type World struct {
// removed from the game.
availableEntities []Entity
systems []System
systemEntities [][]Entity // Slice of entities matching each system.
systemNeeds [][]ComponentID // Slice of ComponentIDs needed by each system.
systemUses [][]ComponentID // Slice of ComponentIDs used by each system.
systemComponentIDs [][]ComponentID // Slice of ComponentIDs needed or used by each system.
systemComponents [][][]interface{} // Slice of components for matching entities.
systems []System
systemEntities [][]Entity // Slice of entities matching each system.
systemNeeds [][]componentID // Slice of ComponentIDs needed by each system.
systemUses [][]componentID // Slice of ComponentIDs used by each system.
systemComponentIDs [][]componentID // Slice of ComponentIDs needed or used by each system.
systemComponents [][][]interface{} // Slice of components for matching entities.
systemComponentFields [][]reflect.Value // Slice of component struct fields used by each system.
systemReceivesUpdate []bool
systemReceivesDraw []bool
@ -69,11 +72,11 @@ type World struct {
systemDrawnEntitiesT time.Time
systemComponentNames []string
haveSystemComponentName map[ComponentID]bool
haveSystemComponentName map[string]bool
cacheTime time.Duration
ctx *Context
ctx *context
entityMutex sync.Mutex
componentMutex sync.Mutex
@ -81,18 +84,16 @@ type World struct {
sync.Mutex
}
// NewWorld returns a new World.
func NewWorld() *World {
w := &World{
// NewWorld returns a new world.
func newWorld() *world {
w := &world{
cacheTime: time.Second,
handledModifiedEntities: make(map[Entity]bool),
haveSystemComponentName: make(map[ComponentID]bool),
haveSystemComponentName: make(map[string]bool),
}
w.ctx = &Context{
world: w,
}
w.ctx = &context{}
// Pad slices to match IDs starting with 1.
w.components = append(w.components, nil)
@ -102,37 +103,63 @@ func NewWorld() *World {
}
// AddSystem registers a system to start receiving Update and Draw calls.
func (w *World) AddSystem(system System) {
func AddSystem(system System) {
w.Lock()
defer w.Unlock()
systemIndex := len(w.systems)
w.systems = append(w.systems, system)
w.systemNeeds = append(w.systemNeeds, uniqueComponentIDs(system.Needs()))
w.systemUses = append(w.systemUses, nil)
for _, componentID := range uniqueComponentIDs(system.Uses()) {
var found bool
for _, neededComponentID := range w.systemNeeds[systemIndex] {
if neededComponentID == componentID {
found = true
break
}
}
if found {
continue
}
w.systemUses[systemIndex] = append(w.systemUses[systemIndex], componentID)
}
w.systemComponentIDs = append(w.systemComponentIDs, append(w.systemNeeds[systemIndex], w.systemUses[systemIndex]...))
w.systemReceivesUpdate = append(w.systemReceivesUpdate, true)
w.systemReceivesDraw = append(w.systemReceivesDraw, true)
w.systemEntities = append(w.systemEntities, nil)
w.systemComponents = append(w.systemComponents, nil)
w.systemComponentFields = append(w.systemComponentFields, nil)
w.entityMutex.Lock()
defer w.entityMutex.Unlock()
w.modifiedEntities = append(w.modifiedEntities, w.allEntities...)
sV := reflect.ValueOf(system)
sT := reflect.TypeOf(system)
if sV.Kind() == reflect.Ptr {
sV = sV.Elem()
sT = sT.Elem()
}
if sV.Kind() != reflect.Struct {
panic("system must be a struct type")
}
var usedComponentIDs []componentID
var neededComponentIDs []componentID
w.systemComponentIDs = append(w.systemComponentIDs, nil)
for i := 0; i < sT.NumField(); i++ {
field := sV.Field(i)
if !field.CanSet() {
continue
}
tag := sT.Field(i).Tag.Get("gohan")
if tag == "-" {
continue
}
//log.Println("SET FIELD", systemIndex, field.String(), tag, field.CanSet())
w.systemComponentFields[systemIndex] = append(w.systemComponentFields[systemIndex], field)
id := componentIDByName(field.Type().String())
if tag == "?" {
usedComponentIDs = append(usedComponentIDs, id)
} else {
neededComponentIDs = append(neededComponentIDs, id)
}
w.systemComponentIDs[systemIndex] = append(w.systemComponentIDs[systemIndex], id)
}
w.systemNeeds = append(w.systemNeeds, neededComponentIDs)
w.systemUses = append(w.systemUses, usedComponentIDs)
}
/*
@ -148,16 +175,32 @@ func AddSystemAfter(system System, after ...System) {
}
*/
func (w *World) updateSystem(i int) (int, error) {
func (w *world) setSystemComponentFields(i int) {
//log.Println(len(w.systemComponentFields[i]))
//log.Println(w.systemComponentFields[i])
for j, field := range w.systemComponentFields[i] {
//log.Println(j, field, field.String())
id := w.systemComponentIDs[i][j]
//log.Println("SYSTEM", i, "FIELD", j, "ID", id)
if w.ctx.components[id] == nil {
field.Set(reflect.Zero(field.Type()))
} else {
field.Set(reflect.ValueOf(w.ctx.components[id]))
}
}
}
func (w *world) updateSystem(i int) (int, error) {
w.ctx.systemIndex = i
w.ctx.allowed = w.systemComponentIDs[i]
updated := 0
for _, entity := range w.systemEntities[i] {
w.ctx.Entity = entity
w.ctx.components = w.systemComponents[i][entity]
err := w.systems[i].Update(w.ctx)
w.setSystemComponentFields(i)
err := w.systems[i].Update(entity)
if err != nil {
if err == ErrSystemWithoutUpdate {
if err == ErrUnregister {
// Unregister system from Update events.
w.systemReceivesUpdate[i] = false
return 0, nil
@ -169,7 +212,7 @@ func (w *World) updateSystem(i int) (int, error) {
return updated, nil
}
func (w *World) _handleRemovedEntities() {
func (w *world) _handleRemovedEntities() {
for _, entity := range w.removedEntities {
// Remove from attached systems.
REMOVED:
@ -192,7 +235,7 @@ func (w *World) _handleRemovedEntities() {
// _handleModifiedEntities handles changes to entity components by attaching
// and detaching modified entities from affected systems.
func (w *World) _handleModifiedEntities() {
func (w *world) _handleModifiedEntities() {
if len(w.modifiedEntities) == 0 {
return
}
@ -223,7 +266,7 @@ func (w *World) _handleModifiedEntities() {
var skip bool
for _, componentID := range w.systemNeeds[i] {
c := w.Component(entity, componentID)
c := entity.getComponent(componentID)
if c == nil {
skip = true
break
@ -233,7 +276,7 @@ func (w *World) _handleModifiedEntities() {
}
if !skip {
for _, componentID := range w.systemUses[i] {
c := w.Component(entity, componentID)
c := entity.getComponent(componentID)
w.systemComponents[i][entity][componentID] = c
}
@ -263,7 +306,7 @@ func (w *World) _handleModifiedEntities() {
w.modifiedEntities = w.modifiedEntities[:0]
}
func (w *World) propagateEntityChanges() {
func (w *world) propagateEntityChanges() {
w.entityMutex.Lock()
defer w.entityMutex.Unlock()
@ -272,7 +315,7 @@ func (w *World) propagateEntityChanges() {
}
// Update updates the game state.
func (w *World) Update() error {
func Update() error {
w.Lock()
defer w.Unlock()
@ -310,7 +353,7 @@ func (w *World) Update() error {
// CurrentUpdates returns the number of System Update calls required to update
// the game state. Because entities may be handled by more than one System,
// this number may be higher than the number of active entities.
func (w *World) CurrentUpdates() int {
func CurrentUpdates() int {
if time.Since(w.systemUpdatedEntitiesT) >= w.cacheTime {
w.systemUpdatedEntitiesV = w.systemUpdatedEntities
w.systemUpdatedEntitiesT = time.Now()
@ -318,16 +361,17 @@ func (w *World) CurrentUpdates() int {
return w.systemUpdatedEntitiesV
}
func (w *World) drawSystem(i int, screen *ebiten.Image) (int, error) {
func (w *world) drawSystem(i int, screen *ebiten.Image) (int, error) {
w.ctx.systemIndex = i
w.ctx.allowed = w.systemComponentIDs[i]
var drawn int
for _, entity := range w.systemEntities[i] {
w.ctx.Entity = entity
w.ctx.components = w.systemComponents[i][entity]
err := w.systems[i].Draw(w.ctx, screen)
w.setSystemComponentFields(i)
err := w.systems[i].Draw(entity, screen)
if err != nil {
if err == ErrSystemWithoutDraw {
if err == ErrUnregister {
// Unregister system from Draw events.
w.systemReceivesDraw[i] = false
return 0, nil
@ -340,7 +384,7 @@ func (w *World) drawSystem(i int, screen *ebiten.Image) (int, error) {
}
// Draw draws the game on to the screen.
func (w *World) Draw(screen *ebiten.Image) error {
func Draw(screen *ebiten.Image) error {
w.Lock()
defer w.Unlock()
@ -379,7 +423,7 @@ func (w *World) Draw(screen *ebiten.Image) error {
// CurrentDraws returns the number of System Draw calls required to draw the
// game on to the screen. Because entities may be handled by more than one
// System, this number may be higher than the number of active entities.
func (w *World) CurrentDraws() int {
func CurrentDraws() int {
if time.Since(w.systemDrawnEntitiesT) >= w.cacheTime {
w.systemDrawnEntitiesV = w.systemDrawnEntities
w.systemDrawnEntitiesT = time.Now()
@ -392,23 +436,23 @@ func (w *World) CurrentDraws() int {
// the memory used later to create entities normally. Pre-allocating enough
// entities to run your application after its systems has been added, but
// before any entities are created, will provide the greatest performance boost.
func (w *World) Preallocate(entities int) {
func Preallocate(entities int) {
if len(w.availableEntities) >= entities {
return
}
e := make([]Entity, entities)
for i := 0; i < entities; i++ {
e[i] = w.NewEntity()
e[i] = NewEntity()
}
for i := 0; i < entities; i++ {
w.RemoveEntity(e[i])
e[i].Remove()
}
}
func uniqueComponentIDs(v []ComponentID) []ComponentID {
var list []ComponentID
keys := make(map[ComponentID]bool)
func uniqueComponentIDs(v []componentID) []componentID {
var list []componentID
keys := make(map[componentID]bool)
for _, entry := range v {
if _, value := keys[entry]; !value {
keys[entry] = true
@ -418,14 +462,14 @@ func uniqueComponentIDs(v []ComponentID) []ComponentID {
return list
}
func (w *World) componentName(id ComponentID) string {
func (w *world) componentName(id componentID) string {
if int(id) < len(w.systemComponentNames) {
return w.systemComponentNames[id]
}
return strconv.Itoa(int(id))
}
func (w *World) systemName(i int) string {
func (w *world) systemName(i int) string {
if i < len(w.systems) {
return getName(w.systems[i])
}
@ -441,3 +485,13 @@ func getName(v interface{}) string {
}
return ""
}
// Reset removes all entities, components and systems.
func Reset() {
old := w
old.Lock()
w = newWorld()
old.Unlock()
}

View File

@ -8,68 +8,54 @@ import (
)
type movementSystem struct {
positionComponentID ComponentID
velocityComponentID ComponentID
Position *positionComponent
Velocity *velocityComponent
}
func (s *movementSystem) Needs() []ComponentID {
return []ComponentID{
s.positionComponentID,
s.velocityComponentID,
}
}
func (s *movementSystem) Uses() []ComponentID {
func (s *movementSystem) Update(entity Entity) error {
s.Position.X, s.Position.Y = s.Position.X+s.Velocity.X, s.Position.Y+s.Velocity.Y
return nil
}
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 {
func (s *movementSystem) Draw(entity Entity, screen *ebiten.Image) error {
return nil
}
func TestWorld(t *testing.T) {
const iterations = 1024
w, e, positionComponentID, velocityComponentID := newTestWorld()
_, e, positionComponentID, velocityComponentID := newTestWorld()
entities := make([]Entity, iterations)
position := w.Component(e, positionComponentID).(*positionComponent)
velocity := w.Component(e, velocityComponentID).(*velocityComponent)
position := e.getComponent(positionComponentID).(*positionComponent)
velocity := e.getComponent(velocityComponentID).(*velocityComponent)
expectedX, expectedY := position.X+(velocity.X*iterations), position.Y+(velocity.Y*iterations)
for i := 0; i < iterations; i++ {
entities[i] = w.NewEntity()
entities[i] = NewEntity()
if i > 0 {
if !w.RemoveEntity(entities[i-1]) {
if !entities[i-1].Remove() {
t.Errorf("failed to remove entity %d", entities[i-1])
}
}
err := w.Update()
err := Update()
if err != nil {
t.Fatal(err)
}
}
// Fetch component again to ensure consistency.
position = w.Component(e, positionComponentID).(*positionComponent)
position = e.getComponent(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 BenchmarkUpdateWorldInactive(b *testing.B) {
w, _, _, _ := newTestWorld()
newTestWorld()
b.StopTimer()
b.ResetTimer()
@ -77,7 +63,7 @@ func BenchmarkUpdateWorldInactive(b *testing.B) {
b.StartTimer()
for i := 0; i < b.N; i++ {
err := w.Update()
err := Update()
if err != nil {
b.Fatal(err)
}
@ -85,7 +71,7 @@ func BenchmarkUpdateWorldInactive(b *testing.B) {
}
func BenchmarkUpdateWorldActive(b *testing.B) {
w, _, _, _ := newTestWorld()
newTestWorld()
entities := make([]Entity, b.N)
@ -95,46 +81,43 @@ func BenchmarkUpdateWorldActive(b *testing.B) {
b.StartTimer()
for i := 0; i < b.N; i++ {
entities[i] = w.NewEntity()
entities[i] = NewEntity()
if i > 0 {
if !w.RemoveEntity(entities[i-1]) {
if !entities[i-1].Remove() {
b.Errorf("failed to remove entity %d", entities[i-1])
}
}
err := w.Update()
err := Update()
if err != nil {
b.Fatal(err)
}
}
}
func newTestWorld() (w *World, e Entity, positionComponentID ComponentID, velocityComponentID ComponentID) {
w = NewWorld()
func newTestWorld() (w *world, e Entity, positionComponentID componentID, velocityComponentID componentID) {
Reset()
e = w.NewEntity()
e = NewEntity()
positionComponentID = w.NewComponentID()
position := &positionComponent{
componentID: positionComponentID,
X: 108,
Y: 0,
}
w.AddComponent(e, position)
e.AddComponent(position)
positionComponentID = componentID(1)
velocityComponentID = w.NewComponentID()
velocity := &velocityComponent{
componentID: velocityComponentID,
X: -0.1,
Y: 0.2,
}
w.AddComponent(e, velocity)
e.AddComponent(velocity)
velocityComponentID = componentID(2)
movement := &movementSystem{
positionComponentID: positionComponentID,
velocityComponentID: velocityComponentID,
}
w.AddSystem(movement)
movement := &movementSystem{}
AddSystem(movement)
return w, e, positionComponentID, velocityComponentID
}