Browse Source

Refactor System interface

Systems now specify their components via public fields.
main
Trevor Slocum 4 months ago
parent
commit
193532a951
  1. 2
      README.md
  2. 75
      component.go
  3. 35
      component_test.go
  4. 23
      context.go
  5. 25
      doc.go
  6. 13
      entity.go
  7. 22
      entity_test.go
  8. 19
      examples/twinstick/component/bullet.go
  9. 19
      examples/twinstick/component/position.go
  10. 19
      examples/twinstick/component/velocity.go
  11. 18
      examples/twinstick/component/weapon.go
  12. 9
      examples/twinstick/entity/bullet.go
  13. 10
      examples/twinstick/entity/player.go
  14. 49
      examples/twinstick/game/game.go
  15. 24
      examples/twinstick/system/draw_bullets.go
  16. 32
      examples/twinstick/system/draw_player.go
  17. 75
      examples/twinstick/system/input_fire.go
  18. 51
      examples/twinstick/system/input_move.go
  19. 28
      examples/twinstick/system/input_profile.go
  20. 68
      examples/twinstick/system/movement.go
  21. 38
      examples/twinstick/system/printinfo.go
  22. 6
      examples/twinstick/world/world.go
  23. 37
      system.go
  24. 176
      world.go
  25. 71
      world_test.go

2
README.md

@ -12,7 +12,7 @@ for [Ebiten](https://ebiten.org) @@ -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

75
component.go

@ -1,57 +1,76 @@ @@ -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
// 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 newComponentID() componentID {
w.maxComponentID++
for i := Entity(1); i <= w.maxEntityID; i++ {
w.components[i] = append(w.components[i], nil)
}
return w.maxComponentID
}
// NewComponentID returns the next available ComponentID.
func (w *World) NewComponentID() ComponentID {
w.entityMutex.Lock()
defer w.entityMutex.Unlock()
func componentIDByValue(v interface{}) componentID {
sV := reflect.ValueOf(v)
sT := reflect.TypeOf(v)
if sV.Kind() == reflect.Ptr {
sV = sV.Elem()
sT = sT.Elem()
}
w.componentMutex.Lock()
defer w.componentMutex.Unlock()
componentName := sV.Type().String()
return componentIDByName(componentName)
}
w.maxComponentID++
func componentIDByName(name string) componentID {
if len(name) == 0 {
return 0
}
for i := Entity(1); i <= w.maxEntityID; i++ {
w.components[i] = append(w.components[i], nil)
if name[0:1] == "*" {
name = name[1:]
}
w.systemComponentNames = append(w.systemComponentNames, strconv.Itoa(int(w.maxComponentID)))
if !w.haveSystemComponentName[name] {
w.systemComponentNames = append(w.systemComponentNames, name)
w.haveSystemComponentName[name] = true
id := newComponentID() // ComponentNames index now aligns with componentID
return id
}
return w.maxComponentID
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

35
component_test.go

@ -3,36 +3,35 @@ package gohan @@ -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) { @@ -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) { @@ -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

@ -1,25 +1,8 @@ @@ -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

@ -15,15 +15,21 @@ Component @@ -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 @@ -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

@ -9,7 +9,7 @@ type Entity int @@ -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 { @@ -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 { @@ -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 @@ -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()

22
entity_test.go

@ -5,45 +5,45 @@ import ( @@ -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)
}

19
examples/twinstick/component/bullet.go

@ -3,24 +3,5 @@ @@ -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
}

19
examples/twinstick/component/position.go

@ -3,25 +3,6 @@ @@ -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
}

19
examples/twinstick/component/velocity.go

@ -3,25 +3,6 @@ @@ -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
}

18
examples/twinstick/component/weapon.go

@ -5,10 +5,6 @@ package component @@ -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 { @@ -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
}

9
examples/twinstick/entity/bullet.go

@ -6,23 +6,22 @@ package entity @@ -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
}

10
examples/twinstick/entity/player.go

@ -7,24 +7,22 @@ import ( @@ -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 { @@ -32,7 +30,7 @@ func NewPlayer() gohan.Entity {
FireRate: 100 * time.Millisecond,
BulletSpeed: 15,
}
world.World.AddComponent(player, weapon)
player.AddComponent(weapon)
return player
}

49
examples/twinstick/game/game.go

@ -12,7 +12,6 @@ import ( @@ -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 { @@ -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) { @@ -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) { @@ -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) { @@ -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 { @@ -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
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))
// Handle input.
gohan.AddSystem(system.NewProfileSystem())
gohan.AddSystem(system.NewMovementInputSystem())
gohan.AddSystem(system.NewMovementSystem())
gohan.AddSystem(system.NewFireInputSystem())
// Render game.
gohan.AddSystem(system.NewDrawBulletsSystem())
gohan.AddSystem(system.NewDrawPlayerSystem())
gohan.AddSystem(system.NewPrintInfoSystem())
}
func (g *game) loadAssets() error {

24
examples/twinstick/system/draw_bullets.go

@ -11,6 +11,9 @@ import ( @@ -11,6 +11,9 @@ import (
)
type DrawBulletsSystem struct {
Position *component.PositionComponent
Bullet *component.BulletComponent
op *ebiten.DrawImageOptions
}
@ -20,28 +23,15 @@ func NewDrawBulletsSystem() *DrawBulletsSystem { @@ -20,28 +23,15 @@ func NewDrawBulletsSystem() *DrawBulletsSystem {
}
}
func (s *DrawBulletsSystem) Needs() []gohan.ComponentID {
return []gohan.ComponentID{
component.PositionComponentID,
component.BulletComponentID,
}
}
func (s *DrawBulletsSystem) Uses() []gohan.ComponentID {
return nil
}
func (s *DrawBulletsSystem) Update(_ *gohan.Context) error {
return gohan.ErrSystemWithoutUpdate
func (s *DrawBulletsSystem) Update(_ gohan.Entity) error {
return gohan.ErrUnregister
}
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
}

32
examples/twinstick/system/draw_player.go

@ -11,37 +11,25 @@ import ( @@ -11,37 +11,25 @@ import (
)
type drawPlayerSystem struct {
player gohan.Entity
op *ebiten.DrawImageOptions
}
Position *component.PositionComponent
Weapon *component.WeaponComponent
func NewDrawPlayerSystem(player gohan.Entity) *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 NewDrawPlayerSystem() *drawPlayerSystem {
return &drawPlayerSystem{
op: &ebiten.DrawImageOptions{},
}
}
func (s *drawPlayerSystem) Uses() []gohan.ComponentID {
return nil
}
func (s *drawPlayerSystem) Update(_ *gohan.Context) error {
return gohan.ErrSystemWithoutUpdate
func (s *drawPlayerSystem) Update(_ gohan.Entity) error {
return gohan.ErrUnregister
}
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
}

75
examples/twinstick/system/input_fire.go

@ -18,91 +18,76 @@ func angle(x1, y1, x2, y2 float64) float64 { @@ -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 (_ *fireInputSystem) Needs() []gohan.ComponentID {
return []gohan.ComponentID{
component.PositionComponentID,
component.WeaponComponentID,
}
func NewFireInputSystem() *fireInputSystem {
return &fireInputSystem{}
}
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
}

51
examples/twinstick/system/input_move.go

@ -10,55 +10,42 @@ import ( @@ -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 (s *movementInputSystem) Needs() []gohan.ComponentID {
return []gohan.ComponentID{
component.VelocityComponentID,
component.WeaponComponentID,
}
}
func (s *movementInputSystem) Uses() []gohan.ComponentID {
return nil
func NewMovementInputSystem() *movementInputSystem {
return &movementInputSystem{}
}
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
}

28
examples/twinstick/system/input_profile.go

@ -10,34 +10,24 @@ import ( @@ -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
cpuProfile *os.File
}
Weapon *component.WeaponComponent
func NewProfileSystem(player gohan.Entity) *profileSystem {
return &profileSystem{
player: player,
}
}
func (s *profileSystem) Needs() []gohan.ComponentID {
return []gohan.ComponentID{
component.WeaponComponentID,
}
cpuProfile *os.File
}
func (s *profileSystem) Uses() []gohan.ComponentID {
return nil
func NewProfileSystem() *profileSystem {
return &profileSystem{}
}
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 { @@ -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
}

68
examples/twinstick/system/movement.go

@ -6,77 +6,73 @@ package system @@ -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) Uses() []gohan.ComponentID {
return nil
}
func (s *MovementSystem) Update(entity gohan.Entity) error {
bullet := entity != world.Player
func (s *MovementSystem) Update(ctx *gohan.Context) error {
position := component.Position(ctx)
velocity := component.Velocity(ctx)
bullet := ctx.Entity != s.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
}
// 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
}

38
examples/twinstick/system/printinfo.go

@ -6,48 +6,36 @@ package system @@ -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
}

6
examples/twinstick/world/world.go

@ -5,4 +5,8 @@ package world @@ -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

@ -6,36 +6,21 @@ import ( @@ -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

<
@ -31,13 +31,15 @@ func init() { @@ -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 { @@ -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 { @@ -69,11 +72,11 @@ type World struct {
systemDrawnEntitiesT time.Time
systemComponentNames []string
haveSystemComponentName map[ComponentID]bool
haveSystemComponentName map[string]bool
cacheTime time.Duration