Refactor System interface
Systems now specify their components via public fields.
This commit is contained in:
parent
0231e09ad7
commit
193532a951
25 changed files with 398 additions and 550 deletions
|
@ -12,7 +12,7 @@ for [Ebiten](https://ebiten.org)
|
|||
|
||||
Documentation is available via [godoc](https://docs.rocketnine.space/code.rocketnine.space/tslocum/gohan).
|
||||
|
||||
An [example game](https://rocketnine.itch.io/gohan-twinstick) is included at
|
||||
An [example game](https://rocketnine.itch.io/gohan-twinstick?secret=gohan) is included at
|
||||
`/examples/twinstick`. See godoc for build instructions.
|
||||
|
||||
## Support
|
||||
|
|
77
component.go
77
component.go
|
@ -1,57 +1,76 @@
|
|||
package gohan
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
"reflect"
|
||||
)
|
||||
|
||||
// ComponentID is a component identifier. Each Component is assigned a unique ID
|
||||
// via World.NewComponentID, and implements a ComponentID method returning its ID.
|
||||
type ComponentID int
|
||||
|
||||
// Component represents data for an entity, and how it interacts with the world.
|
||||
type Component interface {
|
||||
ComponentID() ComponentID
|
||||
}
|
||||
|
||||
// NewComponentID returns the next available ComponentID.
|
||||
func (w *World) NewComponentID() ComponentID {
|
||||
w.entityMutex.Lock()
|
||||
defer w.entityMutex.Unlock()
|
||||
|
||||
w.componentMutex.Lock()
|
||||
defer w.componentMutex.Unlock()
|
||||
// componentID is a component identifier. Each Component is assigned a unique ID
|
||||
// via world.NewComponentID, and implements a componentID method returning its ID.
|
||||
type componentID int
|
||||
|
||||
// newComponentID returns the next available componentID.
|
||||
func newComponentID() componentID {
|
||||
w.maxComponentID++
|
||||
|
||||
for i := Entity(1); i <= w.maxEntityID; i++ {
|
||||
w.components[i] = append(w.components[i], nil)
|
||||
}
|
||||
|
||||
w.systemComponentNames = append(w.systemComponentNames, strconv.Itoa(int(w.maxComponentID)))
|
||||
|
||||
return w.maxComponentID
|
||||
}
|
||||
|
||||
func componentIDByValue(v interface{}) componentID {
|
||||
sV := reflect.ValueOf(v)
|
||||
sT := reflect.TypeOf(v)
|
||||
if sV.Kind() == reflect.Ptr {
|
||||
sV = sV.Elem()
|
||||
sT = sT.Elem()
|
||||
}
|
||||
|
||||
componentName := sV.Type().String()
|
||||
return componentIDByName(componentName)
|
||||
}
|
||||
|
||||
func componentIDByName(name string) componentID {
|
||||
if len(name) == 0 {
|
||||
return 0
|
||||
}
|
||||
|
||||
if name[0:1] == "*" {
|
||||
name = name[1:]
|
||||
}
|
||||
|
||||
if !w.haveSystemComponentName[name] {
|
||||
w.systemComponentNames = append(w.systemComponentNames, name)
|
||||
w.haveSystemComponentName[name] = true
|
||||
id := newComponentID() // ComponentNames index now aligns with componentID
|
||||
return id
|
||||
}
|
||||
|
||||
for i, savedName := range w.systemComponentNames {
|
||||
if savedName == name {
|
||||
return componentID(i)
|
||||
}
|
||||
}
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
// AddComponent adds a Component to an Entity.
|
||||
func (w *World) AddComponent(entity Entity, component Component) {
|
||||
func (entity Entity) AddComponent(component interface{}) {
|
||||
w.componentMutex.Lock()
|
||||
defer w.componentMutex.Unlock()
|
||||
|
||||
componentID := component.ComponentID()
|
||||
if debug != 0 && !w.haveSystemComponentName[componentID] {
|
||||
w.systemComponentNames[componentID] = getName(component)
|
||||
w.haveSystemComponentName[componentID] = true
|
||||
}
|
||||
|
||||
w.components[entity][componentID] = component
|
||||
id := componentIDByValue(component)
|
||||
w.components[entity][id] = component
|
||||
|
||||
w.entityMutex.Lock()
|
||||
defer w.entityMutex.Unlock()
|
||||
w.modifiedEntities = append(w.modifiedEntities, entity)
|
||||
}
|
||||
|
||||
// Component gets a Component of an Entity.
|
||||
func (w *World) Component(entity Entity, componentID ComponentID) interface{} {
|
||||
// getComponent gets a Component of an Entity.
|
||||
func (entity Entity) getComponent(componentID componentID) interface{} {
|
||||
components := w.components[entity]
|
||||
if components == nil {
|
||||
return nil
|
||||
|
|
|
@ -3,36 +3,35 @@ package gohan
|
|||
import "testing"
|
||||
|
||||
type positionComponent struct {
|
||||
componentID ComponentID
|
||||
componentID componentID
|
||||
|
||||
X, Y float64
|
||||
}
|
||||
|
||||
func (c *positionComponent) ComponentID() ComponentID {
|
||||
func (c *positionComponent) ComponentID() componentID {
|
||||
return c.componentID
|
||||
}
|
||||
|
||||
type velocityComponent struct {
|
||||
componentID ComponentID
|
||||
componentID componentID
|
||||
|
||||
X, Y float64
|
||||
}
|
||||
|
||||
func (c *velocityComponent) ComponentID() ComponentID {
|
||||
func (c *velocityComponent) ComponentID() componentID {
|
||||
return c.componentID
|
||||
}
|
||||
|
||||
func BenchmarkComponent(b *testing.B) {
|
||||
w := NewWorld()
|
||||
Reset()
|
||||
|
||||
e := w.NewEntity()
|
||||
e := NewEntity()
|
||||
|
||||
positionComponentID := w.NewComponentID()
|
||||
w.AddComponent(e, &positionComponent{
|
||||
X: 108,
|
||||
Y: 0,
|
||||
componentID: positionComponentID,
|
||||
e.AddComponent(&positionComponent{
|
||||
X: 108,
|
||||
Y: 0,
|
||||
})
|
||||
positionComponentID := componentID(1)
|
||||
|
||||
b.StopTimer()
|
||||
b.ResetTimer()
|
||||
|
@ -40,20 +39,18 @@ func BenchmarkComponent(b *testing.B) {
|
|||
b.StartTimer()
|
||||
|
||||
for i := 0; i < b.N; i++ {
|
||||
_ = w.Component(e, positionComponentID)
|
||||
_ = e.getComponent(positionComponentID)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkAddComponent(b *testing.B) {
|
||||
w := NewWorld()
|
||||
Reset()
|
||||
|
||||
e := w.NewEntity()
|
||||
e := NewEntity()
|
||||
|
||||
positionComponentID := w.NewComponentID()
|
||||
c := &positionComponent{
|
||||
X: 108,
|
||||
Y: 0,
|
||||
componentID: positionComponentID,
|
||||
X: 108,
|
||||
Y: 0,
|
||||
}
|
||||
|
||||
b.StopTimer()
|
||||
|
@ -62,6 +59,6 @@ func BenchmarkAddComponent(b *testing.B) {
|
|||
b.StartTimer()
|
||||
|
||||
for i := 0; i < b.N; i++ {
|
||||
w.AddComponent(e, c)
|
||||
e.AddComponent(c)
|
||||
}
|
||||
}
|
||||
|
|
23
context.go
23
context.go
|
@ -1,25 +1,8 @@
|
|||
package gohan
|
||||
|
||||
// Context represents the current iteration of a System's matching entities. It
|
||||
// provides methods for retrieving components for the currently matched Entity,
|
||||
// and removing the currently matched Entity.
|
||||
type Context struct {
|
||||
Entity Entity
|
||||
|
||||
allowed []ComponentID
|
||||
// context represents the current iteration of a System's matching entities.
|
||||
type context struct {
|
||||
allowed []componentID
|
||||
components []interface{}
|
||||
systemIndex int
|
||||
world *World
|
||||
}
|
||||
|
||||
// Component gets a Component of the currently handled Entity.
|
||||
func (ctx *Context) Component(componentID ComponentID) interface{} {
|
||||
return ctx.components[componentID]
|
||||
}
|
||||
|
||||
// RemoveEntity removes the currently handled Entity's components, causing it
|
||||
// to no longer be handled by any system. Because Gohan reuses removed Entity
|
||||
// IDs, applications must also remove any other references to the removed Entity.
|
||||
func (ctx *Context) RemoveEntity() bool {
|
||||
return ctx.world.RemoveEntity(ctx.Entity)
|
||||
}
|
||||
|
|
25
doc.go
25
doc.go
|
@ -15,15 +15,21 @@ Component
|
|||
The raw data for one aspect of an object, and how it interacts with the world.
|
||||
Each component is assigned a unique ID, starting with 1.
|
||||
|
||||
type ExampleComponent struct {
|
||||
X, Y float64
|
||||
}
|
||||
|
||||
System
|
||||
|
||||
Each system runs continuously, performing actions on every Entity that fits
|
||||
each systems' set of required matching components.
|
||||
|
||||
World
|
||||
|
||||
Each entity, component and system belongs to an isolated world. Applications
|
||||
may utilize one or multiple worlds.
|
||||
type ExampleSystem struct {
|
||||
Position *component.PositionComponent // Required component.
|
||||
Velocity *component.VelocityComponent // Required component.
|
||||
Sprite *component.SpriteComponent `gohan:"?"` // Optional component.
|
||||
Enabled bool `gohan:"-"` // Not a component.
|
||||
}
|
||||
|
||||
Component Design Guidelines
|
||||
|
||||
|
@ -32,17 +38,6 @@ should be public (start with an uppercase letter) and may have any number of
|
|||
publicly accessible data fields. They should not have any logic (i.e. game code)
|
||||
within them, as all logic should be implemented within a system.
|
||||
|
||||
Rather than accessing components via Context.Component directly, using helper
|
||||
functions (such as the following) helps to reduce code verbosity.
|
||||
|
||||
func Position(ctx *gohan.Context) *PositionComponent {
|
||||
c, ok := ctx.Component(PositionComponentID).(*PositionComponent)
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
return c
|
||||
}
|
||||
|
||||
System Design Guidelines
|
||||
|
||||
Systems are located in a separate package, typically named system. They should
|
||||
|
|
13
entity.go
13
entity.go
|
@ -9,7 +9,7 @@ type Entity int
|
|||
|
||||
// NewEntity returns a new (or previously removed and cleared) Entity. Because
|
||||
// Gohan reuses removed Entity IDs, a previously removed ID may be returned.
|
||||
func (w *World) NewEntity() Entity {
|
||||
func NewEntity() Entity {
|
||||
w.entityMutex.Lock()
|
||||
defer w.entityMutex.Unlock()
|
||||
|
||||
|
@ -30,10 +30,10 @@ func (w *World) NewEntity() Entity {
|
|||
return w.maxEntityID
|
||||
}
|
||||
|
||||
// RemoveEntity removes the provided Entity's components, causing it to no
|
||||
// Remove removes the provided Entity's components, causing it to no
|
||||
// longer be handled by any system. Because Gohan reuses removed EntityIDs,
|
||||
// applications must also remove any internal references to the removed Entity.
|
||||
func (w *World) RemoveEntity(entity Entity) bool {
|
||||
func (entity Entity) Remove() bool {
|
||||
w.entityMutex.Lock()
|
||||
defer w.entityMutex.Unlock()
|
||||
|
||||
|
@ -53,8 +53,9 @@ func (w *World) RemoveEntity(entity Entity) bool {
|
|||
return false
|
||||
}
|
||||
|
||||
// Entities returns all active entities.
|
||||
func (w *World) Entities() []Entity {
|
||||
// AllEntities returns a slice of all active entities. To retrieve only the
|
||||
// number of currently active entities, use CurrentEntities.
|
||||
func AllEntities() []Entity {
|
||||
w.entityMutex.Lock()
|
||||
defer w.entityMutex.Unlock()
|
||||
allEntities := make([]Entity, len(w.allEntities))
|
||||
|
@ -66,7 +67,7 @@ var numEntities int
|
|||
var numEntitiesT time.Time
|
||||
|
||||
// CurrentEntities returns the number of currently active entities.
|
||||
func (w *World) CurrentEntities() int {
|
||||
func CurrentEntities() int {
|
||||
if time.Since(numEntitiesT) >= w.cacheTime {
|
||||
numEntities = len(w.allEntities)
|
||||
numEntitiesT = time.Now()
|
||||
|
|
|
@ -5,45 +5,45 @@ import (
|
|||
)
|
||||
|
||||
func TestActiveEntities(t *testing.T) {
|
||||
w := NewWorld()
|
||||
Reset()
|
||||
w.cacheTime = 0
|
||||
|
||||
active := w.CurrentEntities()
|
||||
active := CurrentEntities()
|
||||
if active != 0 {
|
||||
t.Fatalf("expected 0 active entities, got %d", active)
|
||||
}
|
||||
|
||||
active = w.CurrentEntities()
|
||||
active = CurrentEntities()
|
||||
if active != 0 {
|
||||
t.Fatalf("expected 0 active entities, got %d", active)
|
||||
}
|
||||
|
||||
// Create entity.
|
||||
e1 := w.NewEntity()
|
||||
e1 := NewEntity()
|
||||
|
||||
active = w.CurrentEntities()
|
||||
active = CurrentEntities()
|
||||
if active != 1 {
|
||||
t.Fatalf("expected 1 active entities, got %d", active)
|
||||
}
|
||||
|
||||
// Create entity.
|
||||
e2 := w.NewEntity()
|
||||
e2 := NewEntity()
|
||||
|
||||
active = w.CurrentEntities()
|
||||
active = CurrentEntities()
|
||||
if active != 2 {
|
||||
t.Fatalf("expected 2 active entities, got %d", active)
|
||||
}
|
||||
|
||||
w.RemoveEntity(e1)
|
||||
e1.Remove()
|
||||
|
||||
active = w.CurrentEntities()
|
||||
active = CurrentEntities()
|
||||
if active != 1 {
|
||||
t.Fatalf("expected 1 active entities, got %d", active)
|
||||
}
|
||||
|
||||
w.RemoveEntity(e2)
|
||||
e2.Remove()
|
||||
|
||||
active = w.CurrentEntities()
|
||||
active = CurrentEntities()
|
||||
if active != 0 {
|
||||
t.Fatalf("expected 0 active entities, got %d", active)
|
||||
}
|
||||
|
|
|
@ -3,24 +3,5 @@
|
|||
|
||||
package component
|
||||
|
||||
import (
|
||||
"code.rocketnine.space/tslocum/gohan"
|
||||
"code.rocketnine.space/tslocum/gohan/examples/twinstick/world"
|
||||
)
|
||||
|
||||
type BulletComponent struct {
|
||||
}
|
||||
|
||||
var BulletComponentID = world.World.NewComponentID()
|
||||
|
||||
func (p *BulletComponent) ComponentID() gohan.ComponentID {
|
||||
return BulletComponentID
|
||||
}
|
||||
|
||||
func Bullet(ctx *gohan.Context) *BulletComponent {
|
||||
c, ok := ctx.Component(BulletComponentID).(*BulletComponent)
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
return c
|
||||
}
|
||||
|
|
|
@ -3,25 +3,6 @@
|
|||
|
||||
package component
|
||||
|
||||
import (
|
||||
"code.rocketnine.space/tslocum/gohan"
|
||||
"code.rocketnine.space/tslocum/gohan/examples/twinstick/world"
|
||||
)
|
||||
|
||||
type PositionComponent struct {
|
||||
X, Y float64
|
||||
}
|
||||
|
||||
var PositionComponentID = world.World.NewComponentID()
|
||||
|
||||
func (p *PositionComponent) ComponentID() gohan.ComponentID {
|
||||
return PositionComponentID
|
||||
}
|
||||
|
||||
func Position(ctx *gohan.Context) *PositionComponent {
|
||||
c, ok := ctx.Component(PositionComponentID).(*PositionComponent)
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
return c
|
||||
}
|
||||
|
|
|
@ -3,25 +3,6 @@
|
|||
|
||||
package component
|
||||
|
||||
import (
|
||||
"code.rocketnine.space/tslocum/gohan"
|
||||
"code.rocketnine.space/tslocum/gohan/examples/twinstick/world"
|
||||
)
|
||||
|
||||
type VelocityComponent struct {
|
||||
X, Y float64
|
||||
}
|
||||
|
||||
var VelocityComponentID = world.World.NewComponentID()
|
||||
|
||||
func (c *VelocityComponent) ComponentID() gohan.ComponentID {
|
||||
return VelocityComponentID
|
||||
}
|
||||
|
||||
func Velocity(ctx *gohan.Context) *VelocityComponent {
|
||||
c, ok := ctx.Component(VelocityComponentID).(*VelocityComponent)
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
return c
|
||||
}
|
||||
|
|
|
@ -5,10 +5,6 @@ package component
|
|||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"code.rocketnine.space/tslocum/gohan/examples/twinstick/world"
|
||||
|
||||
"code.rocketnine.space/tslocum/gohan"
|
||||
)
|
||||
|
||||
type WeaponComponent struct {
|
||||
|
@ -21,17 +17,3 @@ type WeaponComponent struct {
|
|||
|
||||
BulletSpeed float64
|
||||
}
|
||||
|
||||
var WeaponComponentID = world.World.NewComponentID()
|
||||
|
||||
func (p *WeaponComponent) ComponentID() gohan.ComponentID {
|
||||
return WeaponComponentID
|
||||
}
|
||||
|
||||
func Weapon(ctx *gohan.Context) *WeaponComponent {
|
||||
c, ok := ctx.Component(WeaponComponentID).(*WeaponComponent)
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
return c
|
||||
}
|
||||
|
|
|
@ -6,23 +6,22 @@ package entity
|
|||
import (
|
||||
"code.rocketnine.space/tslocum/gohan"
|
||||
"code.rocketnine.space/tslocum/gohan/examples/twinstick/component"
|
||||
"code.rocketnine.space/tslocum/gohan/examples/twinstick/world"
|
||||
)
|
||||
|
||||
func NewBullet(x, y, xSpeed, ySpeed float64) gohan.Entity {
|
||||
bullet := world.World.NewEntity()
|
||||
bullet := gohan.NewEntity()
|
||||
|
||||
world.World.AddComponent(bullet, &component.PositionComponent{
|
||||
bullet.AddComponent(&component.PositionComponent{
|
||||
X: x,
|
||||
Y: y,
|
||||
})
|
||||
|
||||
world.World.AddComponent(bullet, &component.VelocityComponent{
|
||||
bullet.AddComponent(&component.VelocityComponent{
|
||||
X: xSpeed,
|
||||
Y: ySpeed,
|
||||
})
|
||||
|
||||
world.World.AddComponent(bullet, &component.BulletComponent{})
|
||||
bullet.AddComponent(&component.BulletComponent{})
|
||||
|
||||
return bullet
|
||||
}
|
||||
|
|
|
@ -7,24 +7,22 @@ import (
|
|||
"math"
|
||||
"time"
|
||||
|
||||
"code.rocketnine.space/tslocum/gohan/examples/twinstick/world"
|
||||
|
||||
"code.rocketnine.space/tslocum/gohan"
|
||||
"code.rocketnine.space/tslocum/gohan/examples/twinstick/component"
|
||||
)
|
||||
|
||||
func NewPlayer() gohan.Entity {
|
||||
player := world.World.NewEntity()
|
||||
player := gohan.NewEntity()
|
||||
|
||||
// Set position to -1,-1 to indicate the player has not been assigned a
|
||||
// position yet. We will place the player in the center of the screen when
|
||||
// we receive the screen dimensions for the first time.
|
||||
world.World.AddComponent(player, &component.PositionComponent{
|
||||
player.AddComponent(&component.PositionComponent{
|
||||
X: -1,
|
||||
Y: -1,
|
||||
})
|
||||
|
||||
world.World.AddComponent(player, &component.VelocityComponent{})
|
||||
player.AddComponent(&component.VelocityComponent{})
|
||||
|
||||
weapon := &component.WeaponComponent{
|
||||
Ammo: math.MaxInt64,
|
||||
|
@ -32,7 +30,7 @@ func NewPlayer() gohan.Entity {
|
|||
FireRate: 100 * time.Millisecond,
|
||||
BulletSpeed: 15,
|
||||
}
|
||||
world.World.AddComponent(player, weapon)
|
||||
player.AddComponent(weapon)
|
||||
|
||||
return player
|
||||
}
|
||||
|
|
|
@ -12,7 +12,6 @@ import (
|
|||
|
||||
"code.rocketnine.space/tslocum/gohan"
|
||||
"code.rocketnine.space/tslocum/gohan/examples/twinstick/asset"
|
||||
"code.rocketnine.space/tslocum/gohan/examples/twinstick/component"
|
||||
"code.rocketnine.space/tslocum/gohan/examples/twinstick/entity"
|
||||
"code.rocketnine.space/tslocum/gohan/examples/twinstick/system"
|
||||
"github.com/hajimehoshi/ebiten/v2"
|
||||
|
@ -31,8 +30,6 @@ type game struct {
|
|||
debugMode bool
|
||||
cpuProfile *os.File
|
||||
|
||||
movementSystem *system.MovementSystem
|
||||
|
||||
sync.Mutex
|
||||
}
|
||||
|
||||
|
@ -42,7 +39,7 @@ func NewGame() (*game, error) {
|
|||
op: &ebiten.DrawImageOptions{},
|
||||
}
|
||||
|
||||
g.player = entity.NewPlayer()
|
||||
world.Player = entity.NewPlayer()
|
||||
|
||||
g.addSystems()
|
||||
|
||||
|
@ -53,7 +50,7 @@ func NewGame() (*game, error) {
|
|||
|
||||
asset.ImgWhiteSquare.Fill(color.White)
|
||||
|
||||
world.World.Preallocate(10000)
|
||||
gohan.Preallocate(10000)
|
||||
|
||||
return g, nil
|
||||
}
|
||||
|
@ -63,13 +60,8 @@ func (g *game) Layout(outsideWidth, outsideHeight int) (int, int) {
|
|||
s := ebiten.DeviceScaleFactor()
|
||||
w, h := int(s*float64(outsideWidth)), int(s*float64(outsideHeight))
|
||||
if w != g.w || h != g.h {
|
||||
if g.w == 0 || g.h == 0 {
|
||||
position := world.World.Component(g.player, component.PositionComponentID).(*component.PositionComponent)
|
||||
position.X, position.Y = float64(w)/2-16, float64(h)/2-16
|
||||
}
|
||||
|
||||
g.w, g.h = w, h
|
||||
g.movementSystem.ScreenW, g.movementSystem.ScreenH = float64(w), float64(h)
|
||||
world.ScreenW, world.ScreenH = float64(w), float64(h)
|
||||
}
|
||||
return g.w, g.h
|
||||
}
|
||||
|
@ -80,38 +72,27 @@ func (g *game) Update() error {
|
|||
return nil
|
||||
}
|
||||
|
||||
return world.World.Update()
|
||||
return gohan.Update()
|
||||
}
|
||||
|
||||
func (g *game) Draw(screen *ebiten.Image) {
|
||||
err := world.World.Draw(screen)
|
||||
err := gohan.Draw(screen)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
func (g *game) addSystems() {
|
||||
w := world.World
|
||||
// Handle input.
|
||||
gohan.AddSystem(system.NewProfileSystem())
|
||||
gohan.AddSystem(system.NewMovementInputSystem())
|
||||
gohan.AddSystem(system.NewMovementSystem())
|
||||
gohan.AddSystem(system.NewFireInputSystem())
|
||||
|
||||
w.AddSystem(system.NewMovementInputSystem(g.player))
|
||||
|
||||
g.movementSystem = &system.MovementSystem{
|
||||
Player: g.player,
|
||||
}
|
||||
w.AddSystem(g.movementSystem)
|
||||
|
||||
w.AddSystem(system.NewFireInputSystem(g.player))
|
||||
|
||||
renderBullet := system.NewDrawBulletsSystem()
|
||||
w.AddSystem(renderBullet)
|
||||
|
||||
renderPlayer := system.NewDrawPlayerSystem(g.player)
|
||||
w.AddSystem(renderPlayer)
|
||||
|
||||
printInfo := system.NewPrintInfoSystem(g.player)
|
||||
w.AddSystem(printInfo)
|
||||
|
||||
w.AddSystem(system.NewProfileSystem(g.player))
|
||||
// Render game.
|
||||
gohan.AddSystem(system.NewDrawBulletsSystem())
|
||||
gohan.AddSystem(system.NewDrawPlayerSystem())
|
||||
gohan.AddSystem(system.NewPrintInfoSystem())
|
||||
}
|
||||
|
||||
func (g *game) loadAssets() error {
|
||||
|
|
|
@ -11,6 +11,9 @@ import (
|
|||
)
|
||||
|
||||
type DrawBulletsSystem struct {
|
||||
Position *component.PositionComponent
|
||||
Bullet *component.BulletComponent
|
||||
|
||||
op *ebiten.DrawImageOptions
|
||||
}
|
||||
|
||||
|
@ -20,28 +23,15 @@ func NewDrawBulletsSystem() *DrawBulletsSystem {
|
|||
}
|
||||
}
|
||||
|
||||
func (s *DrawBulletsSystem) Needs() []gohan.ComponentID {
|
||||
return []gohan.ComponentID{
|
||||
component.PositionComponentID,
|
||||
component.BulletComponentID,
|
||||
}
|
||||
func (s *DrawBulletsSystem) Update(_ gohan.Entity) error {
|
||||
return gohan.ErrUnregister
|
||||
}
|
||||
|
||||
func (s *DrawBulletsSystem) Uses() []gohan.ComponentID {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *DrawBulletsSystem) Update(_ *gohan.Context) error {
|
||||
return gohan.ErrSystemWithoutUpdate
|
||||
}
|
||||
|
||||
func (s *DrawBulletsSystem) Draw(ctx *gohan.Context, screen *ebiten.Image) error {
|
||||
position := component.Position(ctx)
|
||||
|
||||
func (s *DrawBulletsSystem) Draw(_ gohan.Entity, screen *ebiten.Image) error {
|
||||
s.op.GeoM.Reset()
|
||||
s.op.GeoM.Translate(-16, -16)
|
||||
s.op.GeoM.Scale(0.5, 0.5)
|
||||
s.op.GeoM.Translate(position.X, position.Y)
|
||||
s.op.GeoM.Translate(s.Position.X, s.Position.Y)
|
||||
screen.DrawImage(asset.ImgWhiteSquare, s.op)
|
||||
return nil
|
||||
}
|
||||
|
|
|
@ -11,37 +11,25 @@ import (
|
|||
)
|
||||
|
||||
type drawPlayerSystem struct {
|
||||
player gohan.Entity
|
||||
op *ebiten.DrawImageOptions
|
||||
Position *component.PositionComponent
|
||||
Weapon *component.WeaponComponent
|
||||
|
||||
op *ebiten.DrawImageOptions
|
||||
}
|
||||
|
||||
func NewDrawPlayerSystem(player gohan.Entity) *drawPlayerSystem {
|
||||
func NewDrawPlayerSystem() *drawPlayerSystem {
|
||||
return &drawPlayerSystem{
|
||||
player: player,
|
||||
op: &ebiten.DrawImageOptions{},
|
||||
op: &ebiten.DrawImageOptions{},
|
||||
}
|
||||
}
|
||||
|
||||
func (s *drawPlayerSystem) Needs() []gohan.ComponentID {
|
||||
return []gohan.ComponentID{
|
||||
component.PositionComponentID,
|
||||
component.WeaponComponentID,
|
||||
}
|
||||
func (s *drawPlayerSystem) Update(_ gohan.Entity) error {
|
||||
return gohan.ErrUnregister
|
||||
}
|
||||
|
||||
func (s *drawPlayerSystem) Uses() []gohan.ComponentID {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *drawPlayerSystem) Update(_ *gohan.Context) error {
|
||||
return gohan.ErrSystemWithoutUpdate
|
||||
}
|
||||
|
||||
func (s *drawPlayerSystem) Draw(ctx *gohan.Context, screen *ebiten.Image) error {
|
||||
position := component.Position(ctx)
|
||||
|
||||
func (s *drawPlayerSystem) Draw(entity gohan.Entity, screen *ebiten.Image) error {
|
||||
s.op.GeoM.Reset()
|
||||
s.op.GeoM.Translate(position.X-16, position.Y-16)
|
||||
s.op.GeoM.Translate(s.Position.X-16, s.Position.Y-16)
|
||||
screen.DrawImage(asset.ImgWhiteSquare, s.op)
|
||||
return nil
|
||||
}
|
||||
|
|
|
@ -18,91 +18,76 @@ func angle(x1, y1, x2, y2 float64) float64 {
|
|||
}
|
||||
|
||||
type fireInputSystem struct {
|
||||
player gohan.Entity
|
||||
Position *component.PositionComponent
|
||||
Weapon *component.WeaponComponent
|
||||
}
|
||||
|
||||
func NewFireInputSystem(player gohan.Entity) *fireInputSystem {
|
||||
return &fireInputSystem{
|
||||
player: player,
|
||||
}
|
||||
func NewFireInputSystem() *fireInputSystem {
|
||||
return &fireInputSystem{}
|
||||
}
|
||||
|
||||
func (_ *fireInputSystem) Needs() []gohan.ComponentID {
|
||||
return []gohan.ComponentID{
|
||||
component.PositionComponentID,
|
||||
component.WeaponComponentID,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *fireInputSystem) Uses() []gohan.ComponentID {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *fireInputSystem) fire(weapon *component.WeaponComponent, position *component.PositionComponent, fireAngle float64) {
|
||||
if time.Since(weapon.LastFire) < weapon.FireRate {
|
||||
func (s *fireInputSystem) fire(fireAngle float64) {
|
||||
if time.Since(s.Weapon.LastFire) < s.Weapon.FireRate {
|
||||
return
|
||||
}
|
||||
|
||||
weapon.Ammo--
|
||||
weapon.LastFire = time.Now()
|
||||
s.Weapon.Ammo--
|
||||
s.Weapon.LastFire = time.Now()
|
||||
|
||||
speedX := math.Cos(fireAngle) * -weapon.BulletSpeed
|
||||
speedY := math.Sin(fireAngle) * -weapon.BulletSpeed
|
||||
speedX := math.Cos(fireAngle) * -s.Weapon.BulletSpeed
|
||||
speedY := math.Sin(fireAngle) * -s.Weapon.BulletSpeed
|
||||
|
||||
bullet := entity.NewBullet(position.X, position.Y, speedX, speedY)
|
||||
bullet := entity.NewBullet(s.Position.X, s.Position.Y, speedX, speedY)
|
||||
_ = bullet
|
||||
}
|
||||
|
||||
func (s *fireInputSystem) Update(ctx *gohan.Context) error {
|
||||
position := component.Position(ctx)
|
||||
weapon := component.Weapon(ctx)
|
||||
|
||||
if weapon.Ammo <= 0 {
|
||||
func (s *fireInputSystem) Update(entity gohan.Entity) error {
|
||||
if s.Weapon.Ammo <= 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
if ebiten.IsMouseButtonPressed(ebiten.MouseButtonLeft) {
|
||||
cursorX, cursorY := ebiten.CursorPosition()
|
||||
fireAngle := angle(position.X, position.Y, float64(cursorX), float64(cursorY))
|
||||
fireAngle := angle(s.Position.X, s.Position.Y, float64(cursorX), float64(cursorY))
|
||||
|
||||
s.fire(weapon, position, fireAngle)
|
||||
s.fire(fireAngle)
|
||||
}
|
||||
|
||||
if ebiten.IsMouseButtonPressed(ebiten.MouseButtonRight) {
|
||||
cursorX, cursorY := ebiten.CursorPosition()
|
||||
fireAngle := angle(position.X, position.Y, float64(cursorX), float64(cursorY))
|
||||
fireAngle := angle(s.Position.X, s.Position.Y, float64(cursorX), float64(cursorY))
|
||||
|
||||
const div = 5
|
||||
weapon.BulletSpeed /= div
|
||||
s.Weapon.BulletSpeed /= div
|
||||
for i := 0.0; i < 24; i++ {
|
||||
s.fire(weapon, position, fireAngle+i*(math.Pi/12))
|
||||
weapon.LastFire = time.Time{}
|
||||
s.fire(fireAngle + i*(math.Pi/12))
|
||||
s.Weapon.LastFire = time.Time{}
|
||||
}
|
||||
weapon.BulletSpeed *= div
|
||||
s.Weapon.BulletSpeed *= div
|
||||
}
|
||||
|
||||
switch {
|
||||
case ebiten.IsKeyPressed(ebiten.KeyLeft) && ebiten.IsKeyPressed(ebiten.KeyUp):
|
||||
s.fire(weapon, position, math.Pi/4)
|
||||
s.fire(math.Pi / 4)
|
||||
case ebiten.IsKeyPressed(ebiten.KeyLeft) && ebiten.IsKeyPressed(ebiten.KeyDown):
|
||||
s.fire(weapon, position, -math.Pi/4)
|
||||
s.fire(-math.Pi / 4)
|
||||
case ebiten.IsKeyPressed(ebiten.KeyRight) && ebiten.IsKeyPressed(ebiten.KeyUp):
|
||||
s.fire(weapon, position, math.Pi*.75)
|
||||
s.fire(math.Pi * .75)
|
||||
case ebiten.IsKeyPressed(ebiten.KeyRight) && ebiten.IsKeyPressed(ebiten.KeyDown):
|
||||
s.fire(weapon, position, -math.Pi*.75)
|
||||
s.fire(-math.Pi * .75)
|
||||
case ebiten.IsKeyPressed(ebiten.KeyLeft):
|
||||
s.fire(weapon, position, 0)
|
||||
s.fire(0)
|
||||
case ebiten.IsKeyPressed(ebiten.KeyRight):
|
||||
s.fire(weapon, position, math.Pi)
|
||||
s.fire(math.Pi)
|
||||
case ebiten.IsKeyPressed(ebiten.KeyUp):
|
||||
s.fire(weapon, position, math.Pi/2)
|
||||
s.fire(math.Pi / 2)
|
||||
case ebiten.IsKeyPressed(ebiten.KeyDown):
|
||||
s.fire(weapon, position, -math.Pi/2)
|
||||
s.fire(-math.Pi / 2)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (_ *fireInputSystem) Draw(_ *gohan.Context, _ *ebiten.Image) error {
|
||||
return gohan.ErrSystemWithoutDraw
|
||||
func (_ *fireInputSystem) Draw(_ gohan.Entity, _ *ebiten.Image) error {
|
||||
return gohan.ErrUnregister
|
||||
}
|
||||
|
|
|
@ -10,55 +10,42 @@ import (
|
|||
)
|
||||
|
||||
type movementInputSystem struct {
|
||||
player gohan.Entity
|
||||
Velocity *component.VelocityComponent
|
||||
Weapon *component.WeaponComponent
|
||||
}
|
||||
|
||||
func NewMovementInputSystem(player gohan.Entity) *movementInputSystem {
|
||||
return &movementInputSystem{
|
||||
player: player,
|
||||
}
|
||||
func NewMovementInputSystem() *movementInputSystem {
|
||||
return &movementInputSystem{}
|
||||
}
|
||||
|
||||
func (s *movementInputSystem) Needs() []gohan.ComponentID {
|
||||
return []gohan.ComponentID{
|
||||
component.VelocityComponentID,
|
||||
component.WeaponComponentID,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *movementInputSystem) Uses() []gohan.ComponentID {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *movementInputSystem) Update(ctx *gohan.Context) error {
|
||||
velocity := component.Velocity(ctx)
|
||||
func (s *movementInputSystem) Update(entity gohan.Entity) error {
|
||||
if ebiten.IsKeyPressed(ebiten.KeyA) {
|
||||
velocity.X -= 0.5
|
||||
if velocity.X < -5 {
|
||||
velocity.X = -5
|
||||
s.Velocity.X -= 0.5
|
||||
if s.Velocity.X < -5 {
|
||||
s.Velocity.X = -5
|
||||
}
|
||||
}
|
||||
if ebiten.IsKeyPressed(ebiten.KeyD) {
|
||||
velocity.X += 0.5
|
||||
if velocity.X > 5 {
|
||||
velocity.X = 5
|
||||
s.Velocity.X += 0.5
|
||||
if s.Velocity.X > 5 {
|
||||
s.Velocity.X = 5
|
||||
}
|
||||
}
|
||||
if ebiten.IsKeyPressed(ebiten.KeyW) {
|
||||
velocity.Y -= 0.5
|
||||
if velocity.Y < -5 {
|
||||
velocity.Y = -5
|
||||
s.Velocity.Y -= 0.5
|
||||
if s.Velocity.Y < -5 {
|
||||
s.Velocity.Y = -5
|
||||
}
|
||||
}
|
||||
if ebiten.IsKeyPressed(ebiten.KeyS) {
|
||||
velocity.Y += 0.5
|
||||
if velocity.Y > 5 {
|
||||
velocity.Y = 5
|
||||
s.Velocity.Y += 0.5
|
||||
if s.Velocity.Y > 5 {
|
||||
s.Velocity.Y = 5
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *movementInputSystem) Draw(_ *gohan.Context, _ *ebiten.Image) error {
|
||||
return gohan.ErrSystemWithoutDraw
|
||||
func (s *movementInputSystem) Draw(_ gohan.Entity, _ *ebiten.Image) error {
|
||||
return gohan.ErrUnregister
|
||||
}
|
||||
|
|
|
@ -10,34 +10,24 @@ import (
|
|||
"runtime"
|
||||
"runtime/pprof"
|
||||
|
||||
"code.rocketnine.space/tslocum/gohan"
|
||||
"code.rocketnine.space/tslocum/gohan/examples/twinstick/component"
|
||||
|
||||
"code.rocketnine.space/tslocum/gohan"
|
||||
"github.com/hajimehoshi/ebiten/v2"
|
||||
"github.com/hajimehoshi/ebiten/v2/inpututil"
|
||||
)
|
||||
|
||||
type profileSystem struct {
|
||||
player gohan.Entity
|
||||
Weapon *component.WeaponComponent
|
||||
|
||||
cpuProfile *os.File
|
||||
}
|
||||
|
||||
func NewProfileSystem(player gohan.Entity) *profileSystem {
|
||||
return &profileSystem{
|
||||
player: player,
|
||||
}
|
||||
func NewProfileSystem() *profileSystem {
|
||||
return &profileSystem{}
|
||||
}
|
||||
|
||||
func (s *profileSystem) Needs() []gohan.ComponentID {
|
||||
return []gohan.ComponentID{
|
||||
component.WeaponComponentID,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *profileSystem) Uses() []gohan.ComponentID {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *profileSystem) Update(_ *gohan.Context) error {
|
||||
func (s *profileSystem) Update(_ gohan.Entity) error {
|
||||
if ebiten.IsKeyPressed(ebiten.KeyControl) && inpututil.IsKeyJustPressed(ebiten.KeyP) {
|
||||
if s.cpuProfile == nil {
|
||||
log.Println("CPU profiling started...")
|
||||
|
@ -70,6 +60,6 @@ func (s *profileSystem) Update(_ *gohan.Context) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
func (s *profileSystem) Draw(_ *gohan.Context, _ *ebiten.Image) error {
|
||||
return gohan.ErrSystemWithoutDraw
|
||||
func (s *profileSystem) Draw(_ gohan.Entity, _ *ebiten.Image) error {
|
||||
return gohan.ErrUnregister
|
||||
}
|
||||
|
|
|
@ -6,77 +6,73 @@ package system
|
|||
import (
|
||||
"code.rocketnine.space/tslocum/gohan"
|
||||
"code.rocketnine.space/tslocum/gohan/examples/twinstick/component"
|
||||
"code.rocketnine.space/tslocum/gohan/examples/twinstick/world"
|
||||
"github.com/hajimehoshi/ebiten/v2"
|
||||
)
|
||||
|
||||
type MovementSystem struct {
|
||||
ScreenW, ScreenH float64
|
||||
Player gohan.Entity
|
||||
Position *component.PositionComponent
|
||||
Velocity *component.VelocityComponent
|
||||
}
|
||||
|
||||
func (_ *MovementSystem) Needs() []gohan.ComponentID {
|
||||
return []gohan.ComponentID{
|
||||
component.PositionComponentID,
|
||||
component.VelocityComponentID,
|
||||
func NewMovementSystem() *MovementSystem {
|
||||
return &MovementSystem{}
|
||||
}
|
||||
|
||||
func (s *MovementSystem) Update(entity gohan.Entity) error {
|
||||
bullet := entity != world.Player
|
||||
|
||||
// Position the player at the center of the screen when the game starts.
|
||||
if !bullet && s.Position.X == -1 && s.Position.Y == -1 {
|
||||
s.Position.X, s.Position.Y = float64(world.ScreenW)/2-16, float64(world.ScreenH)/2-16
|
||||
}
|
||||
}
|
||||
|
||||
func (s *MovementSystem) Uses() []gohan.ComponentID {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *MovementSystem) Update(ctx *gohan.Context) error {
|
||||
position := component.Position(ctx)
|
||||
velocity := component.Velocity(ctx)
|
||||
|
||||
bullet := ctx.Entity != s.Player
|
||||
|
||||
// Check for collision.
|
||||
if position.X+velocity.X < 16 {
|
||||
if s.Position.X+s.Velocity.X < 16 {
|
||||
if bullet {
|
||||
ctx.RemoveEntity()
|
||||
entity.Remove()
|
||||
return nil
|
||||
}
|
||||
|
||||
position.X = 16
|
||||
velocity.X = 0
|
||||
} else if position.X+velocity.X > s.ScreenW-16 {
|
||||
s.Position.X = 16
|
||||
s.Velocity.X = 0
|
||||
} else if s.Position.X+s.Velocity.X > world.ScreenW-16 {
|
||||
if bullet {
|
||||
ctx.RemoveEntity()
|
||||
entity.Remove()
|
||||
return nil
|
||||
}
|
||||
|
||||
position.X = s.ScreenW - 16
|
||||
velocity.X = 0
|
||||
s.Position.X = world.ScreenW - 16
|
||||
s.Velocity.X = 0
|
||||
}
|
||||
if position.Y+velocity.Y < 16 {
|
||||
if s.Position.Y+s.Velocity.Y < 16 {
|
||||
if bullet {
|
||||
ctx.RemoveEntity()
|
||||
entity.Remove()
|
||||
return nil
|
||||
}
|
||||
|
||||
position.Y = 16
|
||||
velocity.Y = 0
|
||||
} else if position.Y+velocity.Y > s.ScreenH-16 {
|
||||
s.Position.Y = 16
|
||||
s.Velocity.Y = 0
|
||||
} else if s.Position.Y+s.Velocity.Y > world.ScreenH-16 {
|
||||
if bullet {
|
||||
ctx.RemoveEntity()
|
||||
entity.Remove()
|
||||
return nil
|
||||
}
|
||||
|
||||
position.Y = s.ScreenH - 16
|
||||
velocity.Y = 0
|
||||
s.Position.Y = world.ScreenH - 16
|
||||
s.Velocity.Y = 0
|
||||
}
|
||||
|
||||
position.X, position.Y = position.X+velocity.X, position.Y+velocity.Y
|
||||
s.Position.X, s.Position.Y = s.Position.X+s.Velocity.X, s.Position.Y+s.Velocity.Y
|
||||
|
||||
if !bullet {
|
||||
velocity.X *= 0.95
|
||||
velocity.Y *= 0.95
|
||||
s.Velocity.X *= 0.95
|
||||
s.Velocity.Y *= 0.95
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (_ *MovementSystem) Draw(_ *gohan.Context, _ *ebiten.Image) error {
|
||||
return gohan.ErrSystemWithoutDraw
|
||||
func (_ *MovementSystem) Draw(_ gohan.Entity, _ *ebiten.Image) error {
|
||||
return gohan.ErrUnregister
|
||||
}
|
||||
|
|
|
@ -6,48 +6,36 @@ package system
|
|||
import (
|
||||
"fmt"
|
||||
|
||||
"code.rocketnine.space/tslocum/gohan"
|
||||
"code.rocketnine.space/tslocum/gohan/examples/twinstick/component"
|
||||
"code.rocketnine.space/tslocum/gohan/examples/twinstick/world"
|
||||
|
||||
"code.rocketnine.space/tslocum/gohan"
|
||||
"github.com/hajimehoshi/ebiten/v2"
|
||||
"github.com/hajimehoshi/ebiten/v2/ebitenutil"
|
||||
)
|
||||
|
||||
type printInfoSystem struct {
|
||||
img *ebiten.Image
|
||||
op *ebiten.DrawImageOptions
|
||||
player gohan.Entity
|
||||
Weapon *component.WeaponComponent
|
||||
|
||||
img *ebiten.Image
|
||||
op *ebiten.DrawImageOptions
|
||||
}
|
||||
|
||||
func NewPrintInfoSystem(player gohan.Entity) *printInfoSystem {
|
||||
func NewPrintInfoSystem() *printInfoSystem {
|
||||
p := &printInfoSystem{
|
||||
img: ebiten.NewImage(200, 100),
|
||||
op: &ebiten.DrawImageOptions{},
|
||||
player: player,
|
||||
img: ebiten.NewImage(200, 100),
|
||||
op: &ebiten.DrawImageOptions{},
|
||||
}
|
||||
p.op.GeoM.Scale(2, 2)
|
||||
return p
|
||||
}
|
||||
|
||||
func (s *printInfoSystem) Needs() []gohan.ComponentID {
|
||||
return []gohan.ComponentID{
|
||||
component.WeaponComponentID,
|
||||
}
|
||||
func (s *printInfoSystem) Update(_ gohan.Entity) error {
|
||||
return gohan.ErrUnregister
|
||||
}
|
||||
|
||||
func (s *printInfoSystem) Uses() []gohan.ComponentID {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *printInfoSystem) Update(_ *gohan.Context) error {
|
||||
return gohan.ErrSystemWithoutUpdate
|
||||
}
|
||||
|
||||
func (s *printInfoSystem) Draw(_ *gohan.Context, screen *ebiten.Image) error {
|
||||
w := world.World
|
||||
|
||||
func (s *printInfoSystem) Draw(_ gohan.Entity, screen *ebiten.Image) error {
|
||||
s.img.Clear()
|
||||
ebitenutil.DebugPrint(s.img, fmt.Sprintf("KEY WASD+MOUSE\nENT %d\nUPD %d\nDRA %d\nTPS %0.0f\nFPS %0.0f", w.CurrentEntities(), w.CurrentUpdates(), w.CurrentDraws(), ebiten.CurrentTPS(), ebiten.CurrentFPS()))
|
||||
ebitenutil.DebugPrint(s.img, fmt.Sprintf("KEY WASD+MOUSE\nENT %d\nUPD %d\nDRA %d\nTPS %0.0f\nFPS %0.0f", gohan.CurrentEntities(), gohan.CurrentUpdates(), gohan.CurrentDraws(), ebiten.CurrentTPS(), ebiten.CurrentFPS()))
|
||||
screen.DrawImage(s.img, s.op)
|
||||
return nil
|
||||
}
|
||||
|
|
|
@ -5,4 +5,8 @@ package world
|
|||
|
||||
import "code.rocketnine.space/tslocum/gohan"
|
||||
|
||||
var World = gohan.NewWorld()
|
||||
var (
|
||||
Player gohan.Entity
|
||||
|
||||
ScreenW, ScreenH float64
|
||||
)
|
||||
|
|
37
system.go
37
system.go
|
@ -6,36 +6,21 @@ import (
|
|||
"github.com/hajimehoshi/ebiten/v2"
|
||||
)
|
||||
|
||||
// System represents a system that runs continuously. While the system must
|
||||
// implement the Update and Draw methods, a special error value may be returned
|
||||
// indicating that the system does not utilize one of the methods. System
|
||||
// methods which return one of these special error values will not be called again.
|
||||
// System represents a system that runs continuously.
|
||||
//
|
||||
// See the special error values ErrSystemWithoutUpdate and ErrSystemWithoutDraw.
|
||||
// While systems must implement the Update and Draw methods, the special error
|
||||
// value ErrUnregister may be returned at any time by systems to indicate the
|
||||
// method returning the error should not be called again.
|
||||
//
|
||||
// Systems do not need to implement locking to prevent race conditions between
|
||||
// Update and Draw methods. Ebiten calls only one of these methods at a time.
|
||||
type System interface {
|
||||
// Needs returns a list of components (specified by ID) which are required
|
||||
// for an Entity to be handled by the System. When the game is running,
|
||||
// matching entities will be passed to the Update and Draw methods.
|
||||
Needs() []ComponentID
|
||||
|
||||
// Uses returns a list of components (specified by ID) which are used by
|
||||
// the System, in addition to any required components. Because required
|
||||
// components are automatically included in this list, Uses should only
|
||||
// return components which are not also returned by Needs.
|
||||
Uses() []ComponentID
|
||||
|
||||
// Update is called once for each matching Entity each time the game state is updated.
|
||||
Update(ctx *Context) error
|
||||
Update(entity Entity) error
|
||||
|
||||
// Draw is called once for each matching Entity each time the game is drawn to the screen.
|
||||
Draw(ctx *Context, screen *ebiten.Image) error
|
||||
Draw(entity Entity, screen *ebiten.Image) error
|
||||
}
|
||||
|
||||
// Special error values.
|
||||
var (
|
||||
// ErrSystemWithoutUpdate is the error returned when a System does not implement Update.
|
||||
ErrSystemWithoutUpdate = errors.New("system does not implement update")
|
||||
|
||||
// ErrSystemWithoutDraw is the error returned when a System does not implement Draw.
|
||||
ErrSystemWithoutDraw = errors.New("system does not implement draw")
|
||||
)
|
||||
// ErrUnregister is the error returned to unregister from Draw or Update events.
|
||||
var ErrUnregister = errors.New("unregister system")
|
||||
|
|
176
world.go
176
world.go
|
@ -31,13 +31,15 @@ func init() {
|
|||
}
|
||||
}
|
||||
|
||||
// World represents a collection of Entities, components and Systems.
|
||||
type World struct {
|
||||
var w = newWorld()
|
||||
|
||||
// world represents a collection of AllEntities, components and Systems.
|
||||
type world struct {
|
||||
maxEntityID Entity
|
||||
|
||||
maxComponentID ComponentID
|
||||
maxComponentID componentID
|
||||
|
||||
components [][]interface{} // components[Entity][ComponentID]Component
|
||||
components [][]interface{} // components[Entity][componentID]Component
|
||||
|
||||
allEntities []Entity
|
||||
|
||||
|
@ -50,12 +52,13 @@ type World struct {
|
|||
// removed from the game.
|
||||
availableEntities []Entity
|
||||
|
||||
systems []System
|
||||
systemEntities [][]Entity // Slice of entities matching each system.
|
||||
systemNeeds [][]ComponentID // Slice of ComponentIDs needed by each system.
|
||||
systemUses [][]ComponentID // Slice of ComponentIDs used by each system.
|
||||
systemComponentIDs [][]ComponentID // Slice of ComponentIDs needed or used by each system.
|
||||
systemComponents [][][]interface{} // Slice of components for matching entities.
|
||||
systems []System
|
||||
systemEntities [][]Entity // Slice of entities matching each system.
|
||||
systemNeeds [][]componentID // Slice of ComponentIDs needed by each system.
|
||||
systemUses [][]componentID // Slice of ComponentIDs used by each system.
|
||||
systemComponentIDs [][]componentID // Slice of ComponentIDs needed or used by each system.
|
||||
systemComponents [][][]interface{} // Slice of components for matching entities.
|
||||
systemComponentFields [][]reflect.Value // Slice of component struct fields used by each system.
|
||||
|
||||
systemReceivesUpdate []bool
|
||||
systemReceivesDraw []bool
|
||||
|
@ -69,11 +72,11 @@ type World struct {
|
|||
systemDrawnEntitiesT time.Time
|
||||
|
||||
systemComponentNames []string
|
||||
haveSystemComponentName map[ComponentID]bool
|
||||
haveSystemComponentName map[string]bool
|
||||
|
||||
cacheTime time.Duration
|
||||
|
||||
ctx *Context
|
||||
ctx *context
|
||||
|
||||
entityMutex sync.Mutex
|
||||
componentMutex sync.Mutex
|
||||
|
@ -81,18 +84,16 @@ type World struct {
|
|||
sync.Mutex
|
||||
}
|
||||
|
||||
// NewWorld returns a new World.
|
||||
func NewWorld() *World {
|
||||
w := &World{
|
||||
// NewWorld returns a new world.
|
||||
func newWorld() *world {
|
||||
w := &world{
|
||||
cacheTime: time.Second,
|
||||
|
||||
handledModifiedEntities: make(map[Entity]bool),
|
||||
haveSystemComponentName: make(map[ComponentID]bool),
|
||||
haveSystemComponentName: make(map[string]bool),
|
||||
}
|
||||
|
||||
w.ctx = &Context{
|
||||
world: w,
|
||||
}
|
||||
w.ctx = &context{}
|
||||
|
||||
// Pad slices to match IDs starting with 1.
|
||||
w.components = append(w.components, nil)
|
||||
|
@ -102,37 +103,63 @@ func NewWorld() *World {
|
|||
}
|
||||
|
||||
// AddSystem registers a system to start receiving Update and Draw calls.
|
||||
func (w *World) AddSystem(system System) {
|
||||
func AddSystem(system System) {
|
||||
w.Lock()
|
||||
defer w.Unlock()
|
||||
|
||||
systemIndex := len(w.systems)
|
||||
|
||||
w.systems = append(w.systems, system)
|
||||
w.systemNeeds = append(w.systemNeeds, uniqueComponentIDs(system.Needs()))
|
||||
w.systemUses = append(w.systemUses, nil)
|
||||
for _, componentID := range uniqueComponentIDs(system.Uses()) {
|
||||
var found bool
|
||||
for _, neededComponentID := range w.systemNeeds[systemIndex] {
|
||||
if neededComponentID == componentID {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if found {
|
||||
continue
|
||||
}
|
||||
w.systemUses[systemIndex] = append(w.systemUses[systemIndex], componentID)
|
||||
}
|
||||
w.systemComponentIDs = append(w.systemComponentIDs, append(w.systemNeeds[systemIndex], w.systemUses[systemIndex]...))
|
||||
w.systemReceivesUpdate = append(w.systemReceivesUpdate, true)
|
||||
w.systemReceivesDraw = append(w.systemReceivesDraw, true)
|
||||
w.systemEntities = append(w.systemEntities, nil)
|
||||
w.systemComponents = append(w.systemComponents, nil)
|
||||
w.systemComponentFields = append(w.systemComponentFields, nil)
|
||||
|
||||
w.entityMutex.Lock()
|
||||
defer w.entityMutex.Unlock()
|
||||
w.modifiedEntities = append(w.modifiedEntities, w.allEntities...)
|
||||
|
||||
sV := reflect.ValueOf(system)
|
||||
sT := reflect.TypeOf(system)
|
||||
if sV.Kind() == reflect.Ptr {
|
||||
sV = sV.Elem()
|
||||
sT = sT.Elem()
|
||||
}
|
||||
if sV.Kind() != reflect.Struct {
|
||||
panic("system must be a struct type")
|
||||
}
|
||||
|
||||
var usedComponentIDs []componentID
|
||||
var neededComponentIDs []componentID
|
||||
w.systemComponentIDs = append(w.systemComponentIDs, nil)
|
||||
for i := 0; i < sT.NumField(); i++ {
|
||||
field := sV.Field(i)
|
||||
|
||||
if !field.CanSet() {
|
||||
continue
|
||||
}
|
||||
|
||||
tag := sT.Field(i).Tag.Get("gohan")
|
||||
if tag == "-" {
|
||||
continue
|
||||
}
|
||||
|
||||
//log.Println("SET FIELD", systemIndex, field.String(), tag, field.CanSet())
|
||||
w.systemComponentFields[systemIndex] = append(w.systemComponentFields[systemIndex], field)
|
||||
|
||||
id := componentIDByName(field.Type().String())
|
||||
if tag == "?" {
|
||||
usedComponentIDs = append(usedComponentIDs, id)
|
||||
} else {
|
||||
neededComponentIDs = append(neededComponentIDs, id)
|
||||
}
|
||||
|
||||
w.systemComponentIDs[systemIndex] = append(w.systemComponentIDs[systemIndex], id)
|
||||
}
|
||||
|
||||
w.systemNeeds = append(w.systemNeeds, neededComponentIDs)
|
||||
w.systemUses = append(w.systemUses, usedComponentIDs)
|
||||
}
|
||||
|
||||
/*
|
||||
|
@ -148,16 +175,32 @@ func AddSystemAfter(system System, after ...System) {
|
|||
}
|
||||
*/
|
||||
|
||||
func (w *World) updateSystem(i int) (int, error) {
|
||||
func (w *world) setSystemComponentFields(i int) {
|
||||
//log.Println(len(w.systemComponentFields[i]))
|
||||
//log.Println(w.systemComponentFields[i])
|
||||
for j, field := range w.systemComponentFields[i] {
|
||||
//log.Println(j, field, field.String())
|
||||
id := w.systemComponentIDs[i][j]
|
||||
//log.Println("SYSTEM", i, "FIELD", j, "ID", id)
|
||||
if w.ctx.components[id] == nil {
|
||||
field.Set(reflect.Zero(field.Type()))
|
||||
} else {
|
||||
field.Set(reflect.ValueOf(w.ctx.components[id]))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (w *world) updateSystem(i int) (int, error) {
|
||||
w.ctx.systemIndex = i
|
||||
w.ctx.allowed = w.systemComponentIDs[i]
|
||||
updated := 0
|
||||
for _, entity := range w.systemEntities[i] {
|
||||
w.ctx.Entity = entity
|
||||
w.ctx.components = w.systemComponents[i][entity]
|
||||
err := w.systems[i].Update(w.ctx)
|
||||
w.setSystemComponentFields(i)
|
||||
|
||||
err := w.systems[i].Update(entity)
|
||||
if err != nil {
|
||||
if err == ErrSystemWithoutUpdate {
|
||||
if err == ErrUnregister {
|
||||
// Unregister system from Update events.
|
||||
w.systemReceivesUpdate[i] = false
|
||||
return 0, nil
|
||||
|
@ -169,7 +212,7 @@ func (w *World) updateSystem(i int) (int, error) {
|
|||
return updated, nil
|
||||
}
|
||||
|
||||
func (w *World) _handleRemovedEntities() {
|
||||
func (w *world) _handleRemovedEntities() {
|
||||
for _, entity := range w.removedEntities {
|
||||
// Remove from attached systems.
|
||||
REMOVED:
|
||||
|
@ -192,7 +235,7 @@ func (w *World) _handleRemovedEntities() {
|
|||
|
||||
// _handleModifiedEntities handles changes to entity components by attaching
|
||||
// and detaching modified entities from affected systems.
|
||||
func (w *World) _handleModifiedEntities() {
|
||||
func (w *world) _handleModifiedEntities() {
|
||||
if len(w.modifiedEntities) == 0 {
|
||||
return
|
||||
}
|
||||
|
@ -223,7 +266,7 @@ func (w *World) _handleModifiedEntities() {
|
|||
|
||||
var skip bool
|
||||
for _, componentID := range w.systemNeeds[i] {
|
||||
c := w.Component(entity, componentID)
|
||||
c := entity.getComponent(componentID)
|
||||
if c == nil {
|
||||
skip = true
|
||||
break
|
||||
|
@ -233,7 +276,7 @@ func (w *World) _handleModifiedEntities() {
|
|||
}
|
||||
if !skip {
|
||||
for _, componentID := range w.systemUses[i] {
|
||||
c := w.Component(entity, componentID)
|
||||
c := entity.getComponent(componentID)
|
||||
w.systemComponents[i][entity][componentID] = c
|
||||
}
|
||||
|
||||
|
@ -263,7 +306,7 @@ func (w *World) _handleModifiedEntities() {
|
|||
w.modifiedEntities = w.modifiedEntities[:0]
|
||||
}
|
||||
|
||||
func (w *World) propagateEntityChanges() {
|
||||
func (w *world) propagateEntityChanges() {
|
||||
w.entityMutex.Lock()
|
||||
defer w.entityMutex.Unlock()
|
||||
|
||||
|
@ -272,7 +315,7 @@ func (w *World) propagateEntityChanges() {
|
|||
}
|
||||
|
||||
// Update updates the game state.
|
||||
func (w *World) Update() error {
|
||||
func Update() error {
|
||||
w.Lock()
|
||||
defer w.Unlock()
|
||||
|
||||
|
@ -310,7 +353,7 @@ func (w *World) Update() error {
|
|||
// CurrentUpdates returns the number of System Update calls required to update
|
||||
// the game state. Because entities may be handled by more than one System,
|
||||
// this number may be higher than the number of active entities.
|
||||
func (w *World) CurrentUpdates() int {
|
||||
func CurrentUpdates() int {
|
||||
if time.Since(w.systemUpdatedEntitiesT) >= w.cacheTime {
|
||||
w.systemUpdatedEntitiesV = w.systemUpdatedEntities
|
||||
w.systemUpdatedEntitiesT = time.Now()
|
||||
|
@ -318,16 +361,17 @@ func (w *World) CurrentUpdates() int {
|
|||
return w.systemUpdatedEntitiesV
|
||||
}
|
||||
|
||||
func (w *World) drawSystem(i int, screen *ebiten.Image) (int, error) {
|
||||
func (w *world) drawSystem(i int, screen *ebiten.Image) (int, error) {
|
||||
w.ctx.systemIndex = i
|
||||
w.ctx.allowed = w.systemComponentIDs[i]
|
||||
var drawn int
|
||||
for _, entity := range w.systemEntities[i] {
|
||||
w.ctx.Entity = entity
|
||||
w.ctx.components = w.systemComponents[i][entity]
|
||||
err := w.systems[i].Draw(w.ctx, screen)
|
||||
w.setSystemComponentFields(i)
|
||||
|
||||
err := w.systems[i].Draw(entity, screen)
|
||||
if err != nil {
|
||||
if err == ErrSystemWithoutDraw {
|
||||
if err == ErrUnregister {
|
||||
// Unregister system from Draw events.
|
||||
w.systemReceivesDraw[i] = false
|
||||
return 0, nil
|
||||
|
@ -340,7 +384,7 @@ func (w *World) drawSystem(i int, screen *ebiten.Image) (int, error) {
|
|||
}
|
||||
|
||||
// Draw draws the game on to the screen.
|
||||
func (w *World) Draw(screen *ebiten.Image) error {
|
||||
func Draw(screen *ebiten.Image) error {
|
||||
w.Lock()
|
||||
defer w.Unlock()
|
||||
|
||||
|
@ -379,7 +423,7 @@ func (w *World) Draw(screen *ebiten.Image) error {
|
|||
// CurrentDraws returns the number of System Draw calls required to draw the
|
||||
// game on to the screen. Because entities may be handled by more than one
|
||||
// System, this number may be higher than the number of active entities.
|
||||
func (w *World) CurrentDraws() int {
|
||||
func CurrentDraws() int {
|
||||
if time.Since(w.systemDrawnEntitiesT) >= w.cacheTime {
|
||||
w.systemDrawnEntitiesV = w.systemDrawnEntities
|
||||
w.systemDrawnEntitiesT = time.Now()
|
||||
|
@ -392,23 +436,23 @@ func (w *World) CurrentDraws() int {
|
|||
// the memory used later to create entities normally. Pre-allocating enough
|
||||
// entities to run your application after its systems has been added, but
|
||||
// before any entities are created, will provide the greatest performance boost.
|
||||
func (w *World) Preallocate(entities int) {
|
||||
func Preallocate(entities int) {
|
||||
if len(w.availableEntities) >= entities {
|
||||
return
|
||||
}
|
||||
|
||||
e := make([]Entity, entities)
|
||||
for i := 0; i < entities; i++ {
|
||||
e[i] = w.NewEntity()
|
||||
e[i] = NewEntity()
|
||||
}
|
||||
for i := 0; i < entities; i++ {
|
||||
w.RemoveEntity(e[i])
|
||||
e[i].Remove()
|
||||
}
|
||||
}
|
||||
|
||||
func uniqueComponentIDs(v []ComponentID) []ComponentID {
|
||||
var list []ComponentID
|
||||
keys := make(map[ComponentID]bool)
|
||||
func uniqueComponentIDs(v []componentID) []componentID {
|
||||
var list []componentID
|
||||
keys := make(map[componentID]bool)
|
||||
for _, entry := range v {
|
||||
if _, value := keys[entry]; !value {
|
||||
keys[entry] = true
|
||||
|
@ -418,14 +462,14 @@ func uniqueComponentIDs(v []ComponentID) []ComponentID {
|
|||
return list
|
||||
}
|
||||
|
||||
func (w *World) componentName(id ComponentID) string {
|
||||
func (w *world) componentName(id componentID) string {
|
||||
if int(id) < len(w.systemComponentNames) {
|
||||
return w.systemComponentNames[id]
|
||||
}
|
||||
return strconv.Itoa(int(id))
|
||||
}
|
||||
|
||||
func (w *World) systemName(i int) string {
|
||||
func (w *world) systemName(i int) string {
|
||||
if i < len(w.systems) {
|
||||
return getName(w.systems[i])
|
||||
}
|
||||
|
@ -441,3 +485,13 @@ func getName(v interface{}) string {
|
|||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// Reset removes all entities, components and systems.
|
||||
func Reset() {
|
||||
old := w
|
||||
old.Lock()
|
||||
|
||||
w = newWorld()
|
||||
|
||||
old.Unlock()
|
||||
}
|
||||
|
|
|
@ -8,68 +8,54 @@ import (
|
|||
)
|
||||
|
||||
type movementSystem struct {
|
||||
positionComponentID ComponentID
|
||||
velocityComponentID ComponentID
|
||||
Position *positionComponent
|
||||
Velocity *velocityComponent
|
||||
}
|
||||
|
||||
func (s *movementSystem) Needs() []ComponentID {
|
||||
return []ComponentID{
|
||||
s.positionComponentID,
|
||||
s.velocityComponentID,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *movementSystem) Uses() []ComponentID {
|
||||
func (s *movementSystem) Update(entity Entity) error {
|
||||
s.Position.X, s.Position.Y = s.Position.X+s.Velocity.X, s.Position.Y+s.Velocity.Y
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *movementSystem) Update(ctx *Context) error {
|
||||
position := ctx.Component(s.positionComponentID).(*positionComponent)
|
||||
velocity := ctx.Component(s.velocityComponentID).(*velocityComponent)
|
||||
|
||||
position.X, position.Y = position.X+velocity.X, position.Y+velocity.Y
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *movementSystem) Draw(ctx *Context, screen *ebiten.Image) error {
|
||||
func (s *movementSystem) Draw(entity Entity, screen *ebiten.Image) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func TestWorld(t *testing.T) {
|
||||
const iterations = 1024
|
||||
|
||||
w, e, positionComponentID, velocityComponentID := newTestWorld()
|
||||
_, e, positionComponentID, velocityComponentID := newTestWorld()
|
||||
|
||||
entities := make([]Entity, iterations)
|
||||
|
||||
position := w.Component(e, positionComponentID).(*positionComponent)
|
||||
velocity := w.Component(e, velocityComponentID).(*velocityComponent)
|
||||
position := e.getComponent(positionComponentID).(*positionComponent)
|
||||
velocity := e.getComponent(velocityComponentID).(*velocityComponent)
|
||||
|
||||
expectedX, expectedY := position.X+(velocity.X*iterations), position.Y+(velocity.Y*iterations)
|
||||
|
||||
for i := 0; i < iterations; i++ {
|
||||
entities[i] = w.NewEntity()
|
||||
entities[i] = NewEntity()
|
||||
if i > 0 {
|
||||
if !w.RemoveEntity(entities[i-1]) {
|
||||
if !entities[i-1].Remove() {
|
||||
t.Errorf("failed to remove entity %d", entities[i-1])
|
||||
}
|
||||
}
|
||||
|
||||
err := w.Update()
|
||||
err := Update()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
// Fetch component again to ensure consistency.
|
||||
position = w.Component(e, positionComponentID).(*positionComponent)
|
||||
position = e.getComponent(positionComponentID).(*positionComponent)
|
||||
if round(position.X) != round(expectedX) || round(position.Y) != round(expectedY) {
|
||||
t.Errorf("failed to update system: expected position (%f,%f), got (%f,%f)", expectedX, expectedY, position.X, position.Y)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkUpdateWorldInactive(b *testing.B) {
|
||||
w, _, _, _ := newTestWorld()
|
||||
newTestWorld()
|
||||
|
||||
b.StopTimer()
|
||||
b.ResetTimer()
|
||||
|
@ -77,7 +63,7 @@ func BenchmarkUpdateWorldInactive(b *testing.B) {
|
|||
b.StartTimer()
|
||||
|
||||
for i := 0; i < b.N; i++ {
|
||||
err := w.Update()
|
||||
err := Update()
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
|
@ -85,7 +71,7 @@ func BenchmarkUpdateWorldInactive(b *testing.B) {
|
|||
}
|
||||
|
||||
func BenchmarkUpdateWorldActive(b *testing.B) {
|
||||
w, _, _, _ := newTestWorld()
|
||||
newTestWorld()
|
||||
|
||||
entities := make([]Entity, b.N)
|
||||
|
||||
|
@ -95,46 +81,43 @@ func BenchmarkUpdateWorldActive(b *testing.B) {
|
|||
b.StartTimer()
|
||||
|
||||
for i := 0; i < b.N; i++ {
|
||||
entities[i] = w.NewEntity()
|
||||
entities[i] = NewEntity()
|
||||
if i > 0 {
|
||||
if !w.RemoveEntity(entities[i-1]) {
|
||||
if !entities[i-1].Remove() {
|
||||
b.Errorf("failed to remove entity %d", entities[i-1])
|
||||
}
|
||||
}
|
||||
|
||||
err := w.Update()
|
||||
err := Update()
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func newTestWorld() (w *World, e Entity, positionComponentID ComponentID, velocityComponentID ComponentID) {
|
||||
w = NewWorld()
|
||||
func newTestWorld() (w *world, e Entity, positionComponentID componentID, velocityComponentID componentID) {
|
||||
Reset()
|
||||
|
||||
e = w.NewEntity()
|
||||
e = NewEntity()
|
||||
|
||||
positionComponentID = w.NewComponentID()
|
||||
position := &positionComponent{
|
||||
componentID: positionComponentID,
|
||||
X: 108,
|
||||
Y: 0,
|
||||
}
|
||||
w.AddComponent(e, position)
|
||||
e.AddComponent(position)
|
||||
positionComponentID = componentID(1)
|
||||
|
||||
velocityComponentID = w.NewComponentID()
|
||||
velocity := &velocityComponent{
|
||||
componentID: velocityComponentID,
|
||||
X: -0.1,
|
||||
Y: 0.2,
|
||||
}
|
||||
w.AddComponent(e, velocity)
|
||||
e.AddComponent(velocity)
|
||||
velocityComponentID = componentID(2)
|
||||
|
||||
movement := &movementSystem{
|
||||
positionComponentID: positionComponentID,
|
||||
velocityComponentID: velocityComponentID,
|
||||
}
|
||||
w.AddSystem(movement)
|
||||
movement := &movementSystem{}
|
||||
AddSystem(movement)
|
||||
|
||||
return w, e, positionComponentID, velocityComponentID
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue