Add World
This enables applications to use multiple instances.
This commit is contained in:
parent
8d4dabd62e
commit
039863b55b
28
component.go
28
component.go
|
@ -15,42 +15,40 @@ type Component interface {
|
|||
ComponentID() ComponentID
|
||||
}
|
||||
|
||||
var maxComponentID ComponentID
|
||||
|
||||
// NewComponentID returns the next available ComponentID.
|
||||
func NewComponentID() ComponentID {
|
||||
mutex.Lock()
|
||||
defer mutex.Unlock()
|
||||
|
||||
func (w *World) NewComponentID() ComponentID {
|
||||
entityMutex.Lock()
|
||||
defer entityMutex.Unlock()
|
||||
|
||||
componentMutex.Lock()
|
||||
defer componentMutex.Unlock()
|
||||
|
||||
maxComponentID++
|
||||
w.maxComponentID++
|
||||
|
||||
for i := Entity(1); i < maxEntityID; i++ {
|
||||
gameComponents[i] = append(gameComponents[i], nil)
|
||||
for i := Entity(1); i <= w.maxEntityID; i++ {
|
||||
w.components[i] = append(w.components[i], nil)
|
||||
}
|
||||
|
||||
return maxComponentID
|
||||
return w.maxComponentID
|
||||
}
|
||||
|
||||
// AddComponent adds a Component to an Entity.
|
||||
func (entity Entity) AddComponent(component Component) {
|
||||
func (w *World) AddComponent(entity Entity, component Component) {
|
||||
componentMutex.Lock()
|
||||
defer componentMutex.Unlock()
|
||||
|
||||
componentID := component.ComponentID()
|
||||
|
||||
gameComponents[entity][componentID] = component
|
||||
w.components[entity][componentID] = component
|
||||
|
||||
entityMutex.Lock()
|
||||
defer entityMutex.Unlock()
|
||||
modifiedEntities = append(modifiedEntities, entity)
|
||||
w.modifiedEntities = append(w.modifiedEntities, entity)
|
||||
}
|
||||
|
||||
// Component gets a Component of an Entity.
|
||||
func (entity Entity) Component(componentID ComponentID) interface{} {
|
||||
components := gameComponents[entity]
|
||||
func (w *World) Component(entity Entity, componentID ComponentID) interface{} {
|
||||
components := w.components[entity]
|
||||
if components == nil {
|
||||
return nil
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
40
entity.go
40
entity.go
|
@ -8,40 +8,38 @@ import (
|
|||
// Entity is an entity identifier.
|
||||
type Entity int
|
||||
|
||||
var maxEntityID Entity
|
||||
|
||||
var entityMutex sync.Mutex
|
||||
|
||||
// NewEntity returns a new (or previously removed and cleared) Entity. Because
|
||||
// Gohan reuses removed Entity IDs, a previously removed ID may be returned.
|
||||
func NewEntity() Entity {
|
||||
func (w *World) NewEntity() Entity {
|
||||
entityMutex.Lock()
|
||||
defer entityMutex.Unlock()
|
||||
|
||||
if len(availableEntityIDs) > 0 {
|
||||
id := availableEntityIDs[0]
|
||||
availableEntityIDs = availableEntityIDs[1:]
|
||||
allEntities = append(allEntities, id)
|
||||
if len(w.availableEntityIDs) > 0 {
|
||||
id := w.availableEntityIDs[0]
|
||||
w.availableEntityIDs = w.availableEntityIDs[1:]
|
||||
w.allEntities = append(w.allEntities, id)
|
||||
return id
|
||||
}
|
||||
|
||||
maxEntityID++
|
||||
allEntities = append(allEntities, maxEntityID)
|
||||
gameComponents = append(gameComponents, make([]interface{}, maxComponentID+1))
|
||||
return maxEntityID
|
||||
w.maxEntityID++
|
||||
w.allEntities = append(w.allEntities, w.maxEntityID)
|
||||
w.components = append(w.components, make([]interface{}, w.maxComponentID+1))
|
||||
return w.maxEntityID
|
||||
}
|
||||
|
||||
// Remove removes the provided Entity's components, causing it to no longer be
|
||||
// handled by any system. Because Gohan reuses removed EntityIDs, applications
|
||||
// must also remove any internal references to the removed Entity.
|
||||
func (entity Entity) Remove() {
|
||||
// RemoveEntity removes the provided Entity's components, causing it to no
|
||||
// longer be handled by any system. Because Gohan reuses removed EntityIDs,
|
||||
// applications must also remove any internal references to the removed Entity.
|
||||
func (w *World) RemoveEntity(entity Entity) {
|
||||
entityMutex.Lock()
|
||||
defer entityMutex.Unlock()
|
||||
|
||||
for i, e := range allEntities {
|
||||
for i, e := range w.allEntities {
|
||||
if e == entity {
|
||||
allEntities = append(allEntities[:i], allEntities[i+1:]...)
|
||||
removedEntities = append(removedEntities, e)
|
||||
w.allEntities = append(w.allEntities[:i], w.allEntities[i+1:]...)
|
||||
w.removedEntities = append(w.removedEntities, e)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
@ -51,9 +49,9 @@ var numEntities int
|
|||
var numEntitiesT time.Time
|
||||
|
||||
// ActiveEntities returns the number of currently active entities.
|
||||
func ActiveEntities() int {
|
||||
if time.Since(numEntitiesT) >= time.Second {
|
||||
numEntities = len(allEntities)
|
||||
func (w *World) ActiveEntities() int {
|
||||
if time.Since(numEntitiesT) >= w.cacheTime {
|
||||
numEntities = len(w.allEntities)
|
||||
numEntitiesT = time.Now()
|
||||
}
|
||||
return numEntities
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
package world
|
||||
|
||||
import "code.rocketnine.space/tslocum/gohan"
|
||||
|
||||
var World = gohan.NewWorld()
|
2
go.mod
2
go.mod
|
@ -2,7 +2,7 @@ module code.rocketnine.space/tslocum/gohan
|
|||
|
||||
go 1.17
|
||||
|
||||
require github.com/hajimehoshi/ebiten/v2 v2.2.2
|
||||
require github.com/hajimehoshi/ebiten/v2 v2.2.3
|
||||
|
||||
require (
|
||||
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20210727001814-0db043d8d5be // indirect
|
||||
|
|
4
go.sum
4
go.sum
|
@ -2,8 +2,8 @@ github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym
|
|||
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20210727001814-0db043d8d5be h1:vEIVIuBApEBQTEJt19GfhoU+zFSV+sNTa9E9FdnRYfk=
|
||||
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20210727001814-0db043d8d5be/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
|
||||
github.com/hajimehoshi/bitmapfont/v2 v2.1.3/go.mod h1:2BnYrkTQGThpr/CY6LorYtt/zEPNzvE/ND69CRTaHMs=
|
||||
github.com/hajimehoshi/ebiten/v2 v2.2.2 h1:92E+ogdNyH1P/LlvMQ7vonbFDh6bl+O7Ak+H1HX0RX8=
|
||||
github.com/hajimehoshi/ebiten/v2 v2.2.2/go.mod h1:olKl/qqhMBBAm2oI7Zy292nCtE+nitlmYKNF3UpbFn0=
|
||||
github.com/hajimehoshi/ebiten/v2 v2.2.3 h1:jZUP3XWP6mXaw9SCrjWT5Pl6EPuz6FY737dZQgN1KJ4=
|
||||
github.com/hajimehoshi/ebiten/v2 v2.2.3/go.mod h1:olKl/qqhMBBAm2oI7Zy292nCtE+nitlmYKNF3UpbFn0=
|
||||
github.com/hajimehoshi/file2byteslice v0.0.0-20210813153925-5340248a8f41/go.mod h1:CqqAHp7Dk/AqQiwuhV1yT2334qbA/tFWQW0MD2dGqUE=
|
||||
github.com/hajimehoshi/go-mp3 v0.3.2/go.mod h1:qMJj/CSDxx6CGHiZeCgbiq2DSUkbK0UbtXShQcnfyMM=
|
||||
github.com/hajimehoshi/oto v0.6.1/go.mod h1:0QXGEkbuJRohbJaxr7ZQSxnju7hEhseiPx2hrh6raOI=
|
||||
|
|
267
gohan.go
267
gohan.go
|
@ -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
|
||||
}
|
12
system.go
12
system.go
|
@ -13,14 +13,18 @@ import (
|
|||
//
|
||||
// See ErrSystemWithoutUpdate and ErrSystemWithoutDraw.
|
||||
type System interface {
|
||||
// Matches returns whether the provided entity is handled by this system.
|
||||
Matches(entity Entity) bool
|
||||
// Name returns the name of the system.
|
||||
//Name() string
|
||||
|
||||
// Components returns a list of Components (specified by ID) required for
|
||||
// an Entity to be handled by the System.
|
||||
Components() []ComponentID
|
||||
|
||||
// Update is called once for each matching entity each time the game state is updated.
|
||||
Update(entity Entity) error
|
||||
Update(ctx *Context) error
|
||||
|
||||
// Draw is called once for each matching entity each time the game is drawn to the screen.
|
||||
Draw(entity Entity, screen *ebiten.Image) error
|
||||
Draw(ctx *Context, screen *ebiten.Image) error
|
||||
}
|
||||
|
||||
// Special error values.
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
Loading…
Reference in New Issue