Refactor System

Systems must now specify required and optional components separately.
This commit is contained in:
Trevor Slocum 2021-12-06 21:02:33 -08:00
parent 039863b55b
commit 83c9c1fb0c
12 changed files with 130 additions and 36 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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