Entity Component System framework for Ebitengine
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.

267 lines
6.6 KiB

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
}