Entity Component System framework for Ebiten
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

307 lines
7.2 KiB

package gohan
import (
"fmt"
"log"
"os"
"strings"
"sync"
"time"
"github.com/hajimehoshi/ebiten/v2"
)
var debug bool
func init() {
debugEnv := os.Getenv("GOHAN_DEBUG")
debugEnv = strings.TrimSpace(debugEnv)
debugEnv = strings.ToLower(debugEnv)
debug = debugEnv == "1" || debugEnv == "t" || debugEnv == "y" || debugEnv == "on" || debugEnv == "yes" || debugEnv == "true"
}
// World represents a collection of Entities, Components and Systems.
type World struct {
maxEntityID Entity
maxComponentID ComponentID
components [][]interface{}
allEntities []Entity
modifiedEntities []Entity
removedEntities []Entity
// availableEntityIDs is the set of EntityIDs available because they were
// removed from the game.
availableEntityIDs []Entity
systems []System
systemEntities [][]Entity // Slice of entities matching each system.
systemQueries [][]ComponentID // Slice of entities matching each system.
systemReceivesUpdate []bool
systemReceivesDraw []bool
systemUpdatedEntities int
systemUpdatedEntitiesV int
systemUpdatedEntitiesT time.Time
systemDrawnEntities int
systemDrawnEntitiesV int
systemDrawnEntitiesT time.Time
cacheTime time.Duration
ctx *Context
sync.Mutex
}
func NewWorld() *World {
w := &World{
cacheTime: time.Second,
}
w.ctx = &Context{
w: w,
}
// Pad slices to match IDs starting with 1.
w.components = append(w.components, nil)
return w
}
func (w *World) attachEntitiesToSystem(systemIndex int) {
components := w.systemQueries[systemIndex]
ATTACH:
for entity := Entity(1); entity <= w.maxEntityID; entity++ {
// Skip Entities missing required Components.
for _, c := range components {
if w.Component(entity, c) == nil {
continue ATTACH
}
}
w.systemEntities[systemIndex] = append(w.systemEntities[systemIndex], entity)
if debug {
log.Printf("Attached entity %d to system %d.", entity, systemIndex)
}
}
}
// AddSystem registers a system to start receiving Update and Draw calls.
func (w *World) AddSystem(system System) {
w.Lock()
defer w.Unlock()
w.systems = append(w.systems, system)
w.systemQueries = append(w.systemQueries, system.Components())
w.systemReceivesUpdate = append(w.systemReceivesUpdate, true)
w.systemReceivesDraw = append(w.systemReceivesDraw, true)
w.systemEntities = append(w.systemEntities, nil)
w.attachEntitiesToSystem(len(w.systems) - 1)
}
/*
// AddSystemAfter registers a system to start receiving Update and Draw calls
// after the specified system (or systems) are called first.
func AddSystemAfter(system System, after ...System) {
gameSystems = append(gameSystems, system)
gameSystemReceivesUpdate = append(gameSystemReceivesUpdate, true)
gameSystemReceivesDraw = append(gameSystemReceivesDraw, true)
gameSystemEntities = append(gameSystemEntities, nil)
attachEntitiesToSystem(system)
}
*/
func (w *World) updateSystem(i int) (int, error) {
w.ctx.s = i
w.ctx.c = w.systemQueries[i]
updated := 0
for _, entity := range w.systemEntities[i] {
w.ctx.Entity = entity
err := w.systems[i].Update(w.ctx)
if err != nil {
if err == ErrSystemWithoutUpdate {
// Unregister system from Update events.
w.systemReceivesUpdate[i] = false
return 0, nil
}
return 0, fmt.Errorf("failed to update system %d for entity %d: %+v", i, entity, err)
}
updated++
}
return updated, nil
}
func (w *World) propagateEntityChanges() {
entityMutex.Lock()
defer entityMutex.Unlock()
for _, entity := range w.removedEntities {
// Remove from attached systems.
REMOVED:
for i := range w.systemEntities {
for j, e := range w.systemEntities[i] {
if e == entity {
w.systemEntities[i] = append(w.systemEntities[i][:j], w.systemEntities[i][j+1:]...)
continue REMOVED
}
}
}
// Remove components.
w.components[entity] = make([]interface{}, w.maxComponentID+1)
}
// Mark EntityIDs as available.
w.availableEntityIDs = append(w.availableEntityIDs, w.removedEntities...)
w.removedEntities = nil
for _, entity := range w.modifiedEntities {
for i, _ := range w.systems {
systemEntityIndex := -1
for j, systemEntity := range w.systemEntities[i] {
if systemEntity == entity {
systemEntityIndex = j
break
}
}
var skip bool
for _, c := range w.systemQueries[i] {
if w.Component(entity, c) == nil {
skip = true
break
}
}
if !skip {
if systemEntityIndex != -1 {
// Already attached.
continue
}
w.systemEntities[i] = append(w.systemEntities[i], entity)
if debug {
log.Printf("Attached entity %d to system %d.", entity, i)
}
} else if systemEntityIndex != -1 {
// Detach from system.
w.systemEntities[i] = append(w.systemEntities[i][:systemEntityIndex], w.systemEntities[i][systemEntityIndex+1:]...)
}
}
}
w.modifiedEntities = nil
}
// Update updates the game state.
func (w *World) Update() error {
w.Lock()
defer w.Unlock()
w.propagateEntityChanges()
var t time.Time
if debug {
t = time.Now()
}
var systems int
var entitiesUpdated int
for i, registered := range w.systemReceivesUpdate {
if !registered {
continue
}
updated, err := w.updateSystem(i)
if err != nil {
return err
}
entitiesUpdated += updated
systems++
if debug {
log.Printf("System %d: updated %d entities.", i, updated)
}
}
w.systemUpdatedEntities = entitiesUpdated
if debug {
log.Printf("Finished updating %d systems in %.2fms.", systems, float64(time.Since(t).Microseconds())/1000)
}
return nil
}
// UpdatedEntities returns the total number of Entities handled by System Update
// calls. Because each Entity may be handled by more than one System, this
// number may be higher than the number of active entities.
func (w *World) UpdatedEntities() int {
if time.Since(w.systemUpdatedEntitiesT) >= w.cacheTime {
w.systemUpdatedEntitiesV = w.systemUpdatedEntities
w.systemUpdatedEntitiesT = time.Now()
}
return w.systemUpdatedEntitiesV
}
func (w *World) drawSystem(i int, screen *ebiten.Image) (int, error) {
w.ctx.s = i
w.ctx.c = w.systemQueries[i]
var drawn int
for _, entity := range w.systemEntities[i] {
w.ctx.Entity = entity
err := w.systems[i].Draw(w.ctx, screen)
if err != nil {
if err == ErrSystemWithoutDraw {
// Unregister system from Draw events.
w.systemReceivesDraw[i] = false
return 0, nil
}
return 0, fmt.Errorf("failed to draw system %d for entity %d: %+v", i, entity, err)
}
drawn++
}
return drawn, nil
}
// Draw draws the game on to the screen.
func (w *World) Draw(screen *ebiten.Image) error {
w.Lock()
defer w.Unlock()
w.propagateEntityChanges()
var entitiesDrawn int
for i, registered := range w.systemReceivesDraw {
if !registered {
continue
}
drawn, err := w.drawSystem(i, screen)
if err != nil {
return err
}
entitiesDrawn += drawn
}
w.systemDrawnEntities = entitiesDrawn
return nil
}
// DrawnEntities returns the total number of Entities handled by System Draw
// calls. Because each Entity may be handled by more than one System, this
// number may be higher than the number of active entities.
func (w *World) DrawnEntities() int {
if time.Since(w.systemDrawnEntitiesT) >= w.cacheTime {
w.systemDrawnEntitiesV = w.systemDrawnEntities
w.systemDrawnEntitiesT = time.Now()
}
return w.systemDrawnEntitiesV
}