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