Add World

This enables applications to use multiple instances.
This commit is contained in:
Trevor Slocum 2021-12-05 17:17:15 -08:00
parent 8d4dabd62e
commit 039863b55b
27 changed files with 664 additions and 437 deletions

View File

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

View File

@ -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()
c := &testComponent{
X: 108,
Y: 0,
e := w.NewEntity()
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)
}
}

33
context.go Normal file
View File

@ -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)
}

View File

@ -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

View File

@ -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)
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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
g.movementSystem = &system.MovementSystem{}
gohan.AddSystem(g.movementSystem)
w.AddSystem(system.NewMovementInputSystem(g.player))
gohan.AddSystem(system.NewFireInputSystem(g.player))
g.movementSystem = &system.MovementSystem{
Player: g.player,
}
w.AddSystem(g.movementSystem)
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 {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,5 @@
package world
import "code.rocketnine.space/tslocum/gohan"
var World = gohan.NewWorld()

2
go.mod
View File

@ -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

4
go.sum
View File

@ -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=

267
gohan.go
View File

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

View File

@ -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.

307
world.go Normal file
View File

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

108
world_test.go Normal file
View File

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