Refactor System
Systems must now specify required and optional components separately.
This commit is contained in:
parent
039863b55b
commit
83c9c1fb0c
16
README.md
16
README.md
|
@ -1,16 +1,26 @@
|
|||
# gohan
|
||||
|
||||
[![GoDoc](https://code.rocketnine.space/tslocum/godoc-static/raw/branch/master/badge.svg)](https://docs.rocketnine.space/code.rocketnine.space/tslocum/gohan)
|
||||
[![Donate](https://img.shields.io/liberapay/receives/rocketnine.space.svg?logo=liberapay)](https://liberapay.com/rocketnine.space)
|
||||
|
||||
[Entity component system](https://en.wikipedia.org/wiki/Entity_component_system) framework for [Ebiten](https://ebiten.org)
|
||||
[Entity component system](https://en.wikipedia.org/wiki/Entity_component_system) framework
|
||||
for [Ebiten](https://ebiten.org)
|
||||
|
||||
**Note:** This framework is still in development. Breaking changes may be
|
||||
made until v1.0 is released.
|
||||
**Note:** This framework is still in development. Breaking changes may be made until v1.0 is released.
|
||||
|
||||
## Documentation
|
||||
|
||||
Documentation is available via [godoc](https://docs.rocketnine.space/code.rocketnine.space/tslocum/gohan).
|
||||
|
||||
An [example game](https://code.rocketnine.space/tslocum/gohan/src/branch/main/examples/twinstick)
|
||||
is included. See godoc for build instructions.
|
||||
|
||||
## Support
|
||||
|
||||
Please share issues and suggestions [here](https://code.rocketnine.space/tslocum/gohan/issues).
|
||||
|
||||
## List of games powered by Gohan
|
||||
|
||||
- **Monovania** is a Metroidvania-style platform game.
|
||||
- [Play on itch.io](https://rocketnine.itch.io/monovania)
|
||||
- [View source code](https://code.rocketnine.space/tslocum/monovania)
|
||||
|
|
18
context.go
18
context.go
|
@ -12,15 +12,17 @@ type Context struct {
|
|||
|
||||
// Component gets a Component of the currently handled Entity.
|
||||
func (ctx *Context) Component(componentID ComponentID) interface{} {
|
||||
var found bool
|
||||
for _, id := range ctx.c {
|
||||
if id == componentID {
|
||||
found = true
|
||||
break
|
||||
if debug {
|
||||
var found bool
|
||||
for _, id := range ctx.c {
|
||||
if id == componentID {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
log.Panicf("illegal component access: component %d is not queried by system %s", componentID, ctx.w.systemName(ctx.s))
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
log.Panicf("illegal component access: component %d is not queried by system %d", componentID, ctx.s)
|
||||
}
|
||||
return ctx.w.Component(ctx.Entity, componentID)
|
||||
}
|
||||
|
|
|
@ -20,13 +20,21 @@ func NewDrawBulletsSystem() *DrawBulletsSystem {
|
|||
}
|
||||
}
|
||||
|
||||
func (s *DrawBulletsSystem) Components() []gohan.ComponentID {
|
||||
func (s *DrawBulletsSystem) Name() string {
|
||||
return "DrawBullets"
|
||||
}
|
||||
|
||||
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(ctx *gohan.Context) error {
|
||||
return gohan.ErrSystemWithoutUpdate
|
||||
}
|
||||
|
|
|
@ -22,13 +22,21 @@ func NewDrawPlayerSystem(player gohan.Entity) *drawPlayerSystem {
|
|||
}
|
||||
}
|
||||
|
||||
func (s *drawPlayerSystem) Components() []gohan.ComponentID {
|
||||
func (s *drawPlayerSystem) Name() string {
|
||||
return "DrawPlayer"
|
||||
}
|
||||
|
||||
func (s *drawPlayerSystem) Needs() []gohan.ComponentID {
|
||||
return []gohan.ComponentID{
|
||||
component.PositionComponentID,
|
||||
component.WeaponComponentID,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *drawPlayerSystem) Uses() []gohan.ComponentID {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *drawPlayerSystem) Update(_ *gohan.Context) error {
|
||||
return gohan.ErrSystemWithoutUpdate
|
||||
}
|
||||
|
|
|
@ -27,13 +27,21 @@ func NewFireInputSystem(player gohan.Entity) *fireInputSystem {
|
|||
}
|
||||
}
|
||||
|
||||
func (_ *fireInputSystem) Components() []gohan.ComponentID {
|
||||
func (s *fireInputSystem) Name() string {
|
||||
return "FireInput"
|
||||
}
|
||||
|
||||
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 {
|
||||
return
|
||||
|
|
|
@ -19,13 +19,21 @@ func NewMovementInputSystem(player gohan.Entity) *movementInputSystem {
|
|||
}
|
||||
}
|
||||
|
||||
func (s *movementInputSystem) Components() []gohan.ComponentID {
|
||||
func (s *movementInputSystem) Name() string {
|
||||
return "MovementInput"
|
||||
}
|
||||
|
||||
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)
|
||||
if ebiten.IsKeyPressed(ebiten.KeyA) {
|
||||
|
|
|
@ -27,12 +27,20 @@ func NewProfileSystem(player gohan.Entity) *profileSystem {
|
|||
}
|
||||
}
|
||||
|
||||
func (s *profileSystem) Components() []gohan.ComponentID {
|
||||
func (s *profileSystem) Name() string {
|
||||
return "Profile"
|
||||
}
|
||||
|
||||
func (s *profileSystem) Needs() []gohan.ComponentID {
|
||||
return []gohan.ComponentID{
|
||||
component.WeaponComponentID,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *profileSystem) Uses() []gohan.ComponentID {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *profileSystem) Update(ctx *gohan.Context) error {
|
||||
if ebiten.IsKeyPressed(ebiten.KeyControl) && inpututil.IsKeyJustPressed(ebiten.KeyP) {
|
||||
if s.cpuProfile == nil {
|
||||
|
|
|
@ -14,13 +14,21 @@ type MovementSystem struct {
|
|||
Player gohan.Entity
|
||||
}
|
||||
|
||||
func (_ *MovementSystem) Components() []gohan.ComponentID {
|
||||
func (s *MovementSystem) Name() string {
|
||||
return "Movement"
|
||||
}
|
||||
|
||||
func (_ *MovementSystem) Needs() []gohan.ComponentID {
|
||||
return []gohan.ComponentID{
|
||||
component.PositionComponentID,
|
||||
component.VelocityComponentID,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *MovementSystem) Uses() []gohan.ComponentID {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *MovementSystem) Update(ctx *gohan.Context) error {
|
||||
position := component.Position(ctx)
|
||||
velocity := component.Velocity(ctx)
|
||||
|
|
|
@ -29,12 +29,20 @@ func NewPrintInfoSystem(player gohan.Entity) *printInfoSystem {
|
|||
return p
|
||||
}
|
||||
|
||||
func (s *printInfoSystem) Components() []gohan.ComponentID {
|
||||
func (s *printInfoSystem) Name() string {
|
||||
return "PrintInfo"
|
||||
}
|
||||
|
||||
func (s *printInfoSystem) Needs() []gohan.ComponentID {
|
||||
return []gohan.ComponentID{
|
||||
component.WeaponComponentID,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *printInfoSystem) Uses() []gohan.ComponentID {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *printInfoSystem) Update(ctx *gohan.Context) error {
|
||||
return gohan.ErrSystemWithoutUpdate
|
||||
}
|
||||
|
|
18
system.go
18
system.go
|
@ -13,12 +13,20 @@ import (
|
|||
//
|
||||
// See ErrSystemWithoutUpdate and ErrSystemWithoutDraw.
|
||||
type System interface {
|
||||
// Name returns the name of the system.
|
||||
//Name() string
|
||||
// Name returns the name of the System. This is only used when printing
|
||||
// debug and error messages.
|
||||
Name() string
|
||||
|
||||
// Components returns a list of Components (specified by ID) required for
|
||||
// an Entity to be handled by the System.
|
||||
Components() []ComponentID
|
||||
// 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
|
||||
|
|
34
world.go
34
world.go
|
@ -4,6 +4,7 @@ import (
|
|||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
@ -40,7 +41,8 @@ type World struct {
|
|||
|
||||
systems []System
|
||||
systemEntities [][]Entity // Slice of entities matching each system.
|
||||
systemQueries [][]ComponentID // Slice of entities matching each system.
|
||||
systemNeeds [][]ComponentID // Slice of Components needed by each system.
|
||||
systemUses [][]ComponentID // Slice of Components used by each system.
|
||||
|
||||
systemReceivesUpdate []bool
|
||||
systemReceivesDraw []bool
|
||||
|
@ -76,7 +78,7 @@ func NewWorld() *World {
|
|||
}
|
||||
|
||||
func (w *World) attachEntitiesToSystem(systemIndex int) {
|
||||
components := w.systemQueries[systemIndex]
|
||||
components := w.systemNeeds[systemIndex]
|
||||
|
||||
ATTACH:
|
||||
for entity := Entity(1); entity <= w.maxEntityID; entity++ {
|
||||
|
@ -90,7 +92,7 @@ ATTACH:
|
|||
w.systemEntities[systemIndex] = append(w.systemEntities[systemIndex], entity)
|
||||
|
||||
if debug {
|
||||
log.Printf("Attached entity %d to system %d.", entity, systemIndex)
|
||||
log.Printf("Attached entity %d to system %s.", entity, w.systemName(systemIndex))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -101,7 +103,7 @@ func (w *World) AddSystem(system System) {
|
|||
defer w.Unlock()
|
||||
|
||||
w.systems = append(w.systems, system)
|
||||
w.systemQueries = append(w.systemQueries, system.Components())
|
||||
w.systemNeeds = append(w.systemNeeds, system.Needs())
|
||||
w.systemReceivesUpdate = append(w.systemReceivesUpdate, true)
|
||||
w.systemReceivesDraw = append(w.systemReceivesDraw, true)
|
||||
w.systemEntities = append(w.systemEntities, nil)
|
||||
|
@ -121,9 +123,17 @@ func AddSystemAfter(system System, after ...System) {
|
|||
attachEntitiesToSystem(system)
|
||||
}
|
||||
*/
|
||||
func (w *World) systemName(i int) string {
|
||||
name := w.systems[i].Name()
|
||||
if name == "" {
|
||||
name = strconv.Itoa(i)
|
||||
}
|
||||
return name
|
||||
}
|
||||
|
||||
func (w *World) updateSystem(i int) (int, error) {
|
||||
w.ctx.s = i
|
||||
w.ctx.c = w.systemQueries[i]
|
||||
w.ctx.c = w.systemNeeds[i]
|
||||
updated := 0
|
||||
for _, entity := range w.systemEntities[i] {
|
||||
w.ctx.Entity = entity
|
||||
|
@ -134,7 +144,7 @@ func (w *World) updateSystem(i int) (int, error) {
|
|||
w.systemReceivesUpdate[i] = false
|
||||
return 0, nil
|
||||
}
|
||||
return 0, fmt.Errorf("failed to update system %d for entity %d: %+v", i, entity, err)
|
||||
return 0, fmt.Errorf("failed to update system %s for entity %d: %+v", w.systemName(i), entity, err)
|
||||
}
|
||||
updated++
|
||||
}
|
||||
|
@ -167,7 +177,7 @@ func (w *World) propagateEntityChanges() {
|
|||
w.removedEntities = nil
|
||||
|
||||
for _, entity := range w.modifiedEntities {
|
||||
for i, _ := range w.systems {
|
||||
for i := range w.systems {
|
||||
systemEntityIndex := -1
|
||||
for j, systemEntity := range w.systemEntities[i] {
|
||||
if systemEntity == entity {
|
||||
|
@ -177,7 +187,7 @@ func (w *World) propagateEntityChanges() {
|
|||
}
|
||||
|
||||
var skip bool
|
||||
for _, c := range w.systemQueries[i] {
|
||||
for _, c := range w.systemNeeds[i] {
|
||||
if w.Component(entity, c) == nil {
|
||||
skip = true
|
||||
break
|
||||
|
@ -192,7 +202,7 @@ func (w *World) propagateEntityChanges() {
|
|||
w.systemEntities[i] = append(w.systemEntities[i], entity)
|
||||
|
||||
if debug {
|
||||
log.Printf("Attached entity %d to system %d.", entity, i)
|
||||
log.Printf("Attached entity %d to system %s.", entity, w.systemName(i))
|
||||
}
|
||||
} else if systemEntityIndex != -1 {
|
||||
// Detach from system.
|
||||
|
@ -229,7 +239,7 @@ func (w *World) Update() error {
|
|||
systems++
|
||||
|
||||
if debug {
|
||||
log.Printf("System %d: updated %d entities.", i, updated)
|
||||
log.Printf("System %s: updated %d entities.", w.systemName(i), updated)
|
||||
}
|
||||
}
|
||||
w.systemUpdatedEntities = entitiesUpdated
|
||||
|
@ -253,7 +263,7 @@ func (w *World) UpdatedEntities() int {
|
|||
|
||||
func (w *World) drawSystem(i int, screen *ebiten.Image) (int, error) {
|
||||
w.ctx.s = i
|
||||
w.ctx.c = w.systemQueries[i]
|
||||
w.ctx.c = w.systemNeeds[i]
|
||||
var drawn int
|
||||
for _, entity := range w.systemEntities[i] {
|
||||
w.ctx.Entity = entity
|
||||
|
@ -264,7 +274,7 @@ func (w *World) drawSystem(i int, screen *ebiten.Image) (int, error) {
|
|||
w.systemReceivesDraw[i] = false
|
||||
return 0, nil
|
||||
}
|
||||
return 0, fmt.Errorf("failed to draw system %d for entity %d: %+v", i, entity, err)
|
||||
return 0, fmt.Errorf("failed to draw system %s for entity %d: %+v", w.systemName(i), entity, err)
|
||||
}
|
||||
drawn++
|
||||
}
|
||||
|
|
|
@ -12,13 +12,21 @@ type movementSystem struct {
|
|||
velocityComponentID ComponentID
|
||||
}
|
||||
|
||||
func (s *movementSystem) Components() []ComponentID {
|
||||
func (s *movementSystem) Name() string {
|
||||
return "Movement"
|
||||
}
|
||||
|
||||
func (s *movementSystem) Needs() []ComponentID {
|
||||
return []ComponentID{
|
||||
s.positionComponentID,
|
||||
s.velocityComponentID,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *movementSystem) Uses() []ComponentID {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *movementSystem) Update(ctx *Context) error {
|
||||
position := ctx.Component(s.positionComponentID).(*positionComponent)
|
||||
velocity := ctx.Component(s.velocityComponentID).(*velocityComponent)
|
||||
|
|
Loading…
Reference in New Issue