268 lines
6.6 KiB
Go
268 lines
6.6 KiB
Go
package gohan
|
|
|
|
import (
|
|
"fmt"
|
|
"log"
|
|
"os"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/hajimehoshi/ebiten/v2"
|
|
)
|
|
|
|
var (
|
|
gameComponents [][]interface{}
|
|
|
|
allEntities []Entity
|
|
|
|
modifiedEntities []Entity
|
|
removedEntities []Entity
|
|
|
|
// availableEntityIDs is the set of EntityIDs available because they were
|
|
// removed from the game.
|
|
availableEntityIDs []Entity
|
|
|
|
gameSystems []System
|
|
gameSystemEntities [][]Entity // Slice of entities matching each system.
|
|
|
|
gameSystemReceivesUpdate []bool
|
|
gameSystemReceivesDraw []bool
|
|
|
|
gameSystemUpdatedEntities int
|
|
gameSystemUpdatedEntitiesV int
|
|
gameSystemUpdatedEntitiesT time.Time
|
|
|
|
gameSystemDrawnEntities int
|
|
gameSystemDrawnEntitiesV int
|
|
gameSystemDrawnEntitiesT time.Time
|
|
|
|
debug bool
|
|
|
|
mutex sync.Mutex
|
|
)
|
|
|
|
func init() {
|
|
// Pad slices to match IDs starting with 1.
|
|
gameComponents = append(gameComponents, nil)
|
|
|
|
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"
|
|
}
|
|
|
|
// print prints debug information (when enabled).
|
|
func print(s string) {
|
|
if !debug {
|
|
return
|
|
}
|
|
|
|
log.Println(s)
|
|
}
|
|
|
|
func attachEntitiesToSystem(system System) {
|
|
// This function is always called on a newly added system.
|
|
systemID := len(gameSystemEntities) - 1
|
|
|
|
for entity := Entity(0); entity < maxEntityID; entity++ {
|
|
if system.Matches(entity) {
|
|
gameSystemEntities[systemID] = append(gameSystemEntities[systemID], entity)
|
|
|
|
print(fmt.Sprintf("Attached entity %d to system %d.", entity, systemID))
|
|
}
|
|
}
|
|
}
|
|
|
|
// AddSystem registers a system to start receiving Update and Draw calls.
|
|
func AddSystem(system System) {
|
|
mutex.Lock()
|
|
defer mutex.Unlock()
|
|
|
|
gameSystems = append(gameSystems, system)
|
|
gameSystemReceivesUpdate = append(gameSystemReceivesUpdate, true)
|
|
gameSystemReceivesDraw = append(gameSystemReceivesDraw, true)
|
|
gameSystemEntities = append(gameSystemEntities, nil)
|
|
|
|
attachEntitiesToSystem(system)
|
|
}
|
|
|
|
/*
|
|
// 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 propagateEntityChanges() {
|
|
entityMutex.Lock()
|
|
defer entityMutex.Unlock()
|
|
|
|
for _, entity := range removedEntities {
|
|
// Remove from attached systems.
|
|
REMOVED:
|
|
for i := range gameSystemEntities {
|
|
for j, e := range gameSystemEntities[i] {
|
|
if e == entity {
|
|
gameSystemEntities[i] = append(gameSystemEntities[i][:j], gameSystemEntities[i][j+1:]...)
|
|
continue REMOVED
|
|
}
|
|
}
|
|
}
|
|
|
|
// Remove components.
|
|
gameComponents[entity] = make([]interface{}, maxComponentID+1)
|
|
}
|
|
|
|
// Mark EntityIDs as available.
|
|
availableEntityIDs = append(availableEntityIDs, removedEntities...)
|
|
|
|
removedEntities = nil
|
|
|
|
for _, entity := range modifiedEntities {
|
|
for i, system := range gameSystems {
|
|
systemEntityIndex := -1
|
|
for j, systemEntity := range gameSystemEntities[i] {
|
|
if systemEntity == entity {
|
|
systemEntityIndex = j
|
|
break
|
|
}
|
|
}
|
|
|
|
if system.Matches(entity) {
|
|
if systemEntityIndex != -1 {
|
|
// Already attached.
|
|
continue
|
|
}
|
|
|
|
gameSystemEntities[i] = append(gameSystemEntities[i], entity)
|
|
print(fmt.Sprintf("Attached entity %d to system %d.", entity, i))
|
|
} else if systemEntityIndex != -1 {
|
|
// Detach from system.
|
|
gameSystemEntities[i] = append(gameSystemEntities[i][:systemEntityIndex], gameSystemEntities[i][systemEntityIndex+1:]...)
|
|
}
|
|
}
|
|
}
|
|
modifiedEntities = nil
|
|
}
|
|
|
|
func updateSystem(i int) (int, error) {
|
|
updated := 0
|
|
for _, entity := range gameSystemEntities[i] {
|
|
err := gameSystems[i].Update(entity)
|
|
if err != nil {
|
|
if err == ErrSystemWithoutUpdate {
|
|
// Unregister system from Update events.
|
|
gameSystemReceivesUpdate[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
|
|
}
|
|
|
|
// Update updates the game state.
|
|
func Update() error {
|
|
mutex.Lock()
|
|
defer mutex.Unlock()
|
|
|
|
propagateEntityChanges()
|
|
|
|
var t time.Time
|
|
if debug {
|
|
t = time.Now()
|
|
}
|
|
var systems int
|
|
var entitiesUpdated int
|
|
for i, registered := range gameSystemReceivesUpdate {
|
|
if !registered {
|
|
continue
|
|
}
|
|
updated, err := updateSystem(i)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
print(fmt.Sprintf("System %d: updated %d entities.", i, updated))
|
|
entitiesUpdated += updated
|
|
systems++
|
|
}
|
|
if debug {
|
|
print(fmt.Sprintf("Finished updating %d systems in %.2fms.", systems, float64(time.Since(t).Microseconds())/1000))
|
|
}
|
|
gameSystemUpdatedEntities = entitiesUpdated
|
|
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 UpdatedEntities() int {
|
|
if time.Since(gameSystemUpdatedEntitiesT) >= time.Second {
|
|
gameSystemUpdatedEntitiesV = gameSystemUpdatedEntities
|
|
gameSystemUpdatedEntitiesT = time.Now()
|
|
}
|
|
return gameSystemUpdatedEntitiesV
|
|
}
|
|
|
|
func drawSystem(i int, screen *ebiten.Image) (int, error) {
|
|
var drawn int
|
|
for _, entity := range gameSystemEntities[i] {
|
|
err := gameSystems[i].Draw(entity, screen)
|
|
if err != nil {
|
|
if err == ErrSystemWithoutDraw {
|
|
// Unregister system from Draw events.
|
|
gameSystemReceivesDraw[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 Draw(screen *ebiten.Image) error {
|
|
mutex.Lock()
|
|
defer mutex.Unlock()
|
|
|
|
propagateEntityChanges()
|
|
|
|
var entitiesDrawn int
|
|
for i, registered := range gameSystemReceivesDraw {
|
|
if !registered {
|
|
continue
|
|
}
|
|
|
|
drawn, err := drawSystem(i, screen)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
entitiesDrawn += drawn
|
|
}
|
|
gameSystemDrawnEntities = 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 DrawnEntities() int {
|
|
if time.Since(gameSystemDrawnEntitiesT) >= time.Second {
|
|
gameSystemDrawnEntitiesV = gameSystemDrawnEntities
|
|
gameSystemDrawnEntitiesT = time.Now()
|
|
}
|
|
return gameSystemDrawnEntitiesV
|
|
}
|