308 lines
7.2 KiB
Go
308 lines
7.2 KiB
Go
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
|
|
}
|