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.

443 lines
11 KiB

package gohan
import (
"fmt"
"log"
"os"
"reflect"
"strconv"
"strings"
"sync"
"time"
"github.com/hajimehoshi/ebiten/v2"
)
var debug int
func init() {
debugEnv := os.Getenv("GOHAN_DEBUG")
debugEnv = strings.TrimSpace(debugEnv)
debugEnv = strings.ToLower(debugEnv)
i, err := strconv.Atoi(debugEnv)
if err == nil {
debug = i
return
}
if debugEnv == "t" || debugEnv == "y" || debugEnv == "on" || debugEnv == "yes" || debugEnv == "true" {
debug = 1
}
}
// World represents a collection of Entities, components and Systems.
type World struct {
maxEntityID Entity
maxComponentID ComponentID
components [][]interface{} // components[Entity][ComponentID]Component
allEntities []Entity
modifiedEntities []Entity
removedEntities []Entity
handledModifiedEntities map[Entity]bool
// availableEntities is the set of EntityIDs available because they were
// removed from the game.
availableEntities []Entity
systems []System
systemEntities [][]Entity // Slice of entities matching each system.
systemNeeds [][]ComponentID // Slice of ComponentIDs needed by each system.
systemUses [][]ComponentID // Slice of ComponentIDs used by each system.
systemComponentIDs [][]ComponentID // Slice of ComponentIDs needed or used by each system.
systemComponents [][][]interface{} // Slice of components for matching entities.
systemReceivesUpdate []bool
systemReceivesDraw []bool
systemUpdatedEntities int
systemUpdatedEntitiesV int
systemUpdatedEntitiesT time.Time
systemDrawnEntities int
systemDrawnEntitiesV int
systemDrawnEntitiesT time.Time
systemComponentNames []string
haveSystemComponentName map[ComponentID]bool
cacheTime time.Duration
ctx *Context
entityMutex sync.Mutex
componentMutex sync.Mutex
sync.Mutex
}
// NewWorld returns a new World.
func NewWorld() *World {
w := &World{
cacheTime: time.Second,
handledModifiedEntities: make(map[Entity]bool),
haveSystemComponentName: make(map[ComponentID]bool),
}
w.ctx = &Context{
world: w,
}
// Pad slices to match IDs starting with 1.
w.components = append(w.components, nil)
w.systemComponentNames = append(w.systemComponentNames, "")
return w
}
// AddSystem registers a system to start receiving Update and Draw calls.
func (w *World) AddSystem(system System) {
w.Lock()
defer w.Unlock()
systemIndex := len(w.systems)
w.systems = append(w.systems, system)
w.systemNeeds = append(w.systemNeeds, uniqueComponentIDs(system.Needs()))
w.systemUses = append(w.systemUses, nil)
for _, componentID := range uniqueComponentIDs(system.Uses()) {
var found bool
for _, neededComponentID := range w.systemNeeds[systemIndex] {
if neededComponentID == componentID {
found = true
break
}
}
if found {
continue
}
w.systemUses[systemIndex] = append(w.systemUses[systemIndex], componentID)
}
w.systemComponentIDs = append(w.systemComponentIDs, append(w.systemNeeds[systemIndex], w.systemUses[systemIndex]...))
w.systemReceivesUpdate = append(w.systemReceivesUpdate, true)
w.systemReceivesDraw = append(w.systemReceivesDraw, true)
w.systemEntities = append(w.systemEntities, nil)
w.systemComponents = append(w.systemComponents, nil)
w.entityMutex.Lock()
defer w.entityMutex.Unlock()
w.modifiedEntities = append(w.modifiedEntities, w.allEntities...)
}
/*
// 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.systemIndex = i
w.ctx.allowed = w.systemComponentIDs[i]
updated := 0
for _, entity := range w.systemEntities[i] {
w.ctx.Entity = entity
w.ctx.components = w.systemComponents[i][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 %s for entity %d: %+v", w.systemName(i), entity, err)
}
updated++
}
return updated, nil
}
func (w *World) _handleRemovedEntities() {
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] = _removeAt(w.systemEntities[i], j)
w.systemComponents[i][entity] = w.systemComponents[i][entity][:0] // TODO Could this lead to memory issues?
continue REMOVED
}
}
}
}
// Mark EntityID as available.
w.availableEntities = append(w.availableEntities, w.removedEntities...)
w.removedEntities = w.removedEntities[:0]
}
// _handleModifiedEntities handles changes to entity components by attaching
// and detaching modified entities from affected systems.
func (w *World) _handleModifiedEntities() {
if len(w.modifiedEntities) == 0 {
return
}
for _, entity := range w.modifiedEntities {
if w.handledModifiedEntities[entity] {
continue
}
w.handledModifiedEntities[entity] = true
for i := range w.systems {
l := len(w.systemComponents[i])
if l != int(w.maxEntityID+1) {
w.systemComponents[i] = append(w.systemComponents[i], make([][]interface{}, int(w.maxEntityID+1)-l)...)
}
if len(w.systemComponents[i][entity]) != int(w.maxComponentID+1) {
w.systemComponents[i][entity] = make([]interface{}, w.maxComponentID+1)
}
systemEntityIndex := -1
for j, systemEntity := range w.systemEntities[i] {
if systemEntity == entity {
systemEntityIndex = j
break
}
}
var skip bool
for _, componentID := range w.systemNeeds[i] {
c := w.Component(entity, componentID)
if c == nil {
skip = true
break
}
w.systemComponents[i][entity][componentID] = c
}
if !skip {
for _, componentID := range w.systemUses[i] {
c := w.Component(entity, componentID)
w.systemComponents[i][entity][componentID] = c
}
if systemEntityIndex != -1 {
// Already attached.
continue
}
w.systemEntities[i] = append(w.systemEntities[i], entity)
if debug > 1 {
log.Printf("Attached entity %d to %s.", entity, w.systemName(i))
}
} else if systemEntityIndex != -1 {
// Detach from system.
w.systemEntities[i] = _removeAt(w.systemEntities[i], systemEntityIndex)
w.systemComponents[i][entity] = w.systemComponents[i][entity][:0] // TODO Could this lead to memory issues?
}
}
}
for k := range w.handledModifiedEntities {
delete(w.handledModifiedEntities, k)
}
w.modifiedEntities = w.modifiedEntities[:0]
}
func (w *World) propagateEntityChanges() {
w.entityMutex.Lock()
defer w.entityMutex.Unlock()
w._handleRemovedEntities()
w._handleModifiedEntities()
}
// Update updates the game state.
func (w *World) Update() error {
w.Lock()
defer w.Unlock()
w.propagateEntityChanges()
var t time.Time
if debug != 0 {
t = time.Now()
}
var entitiesUpdated int
for i, registered := range w.systemReceivesUpdate {
if !registered {
continue
}
updated, err := w.updateSystem(i)
if err != nil {
return err
}
entitiesUpdated += updated
if debug != 0 {
log.Printf("- %s: %d updated.", w.systemName(i), updated)
}
}
w.systemUpdatedEntities = entitiesUpdated
if debug != 0 {
log.Printf("Handled %d entity updates in %.2fms.", entitiesUpdated, float64(time.Since(t).Microseconds())/1000)
}
return nil
}
// CurrentUpdates returns the number of System Update calls required to update
// the game state. Because entities may be handled by more than one System,
// this number may be higher than the number of active entities.
func (w *World) CurrentUpdates() 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.systemIndex = i
w.ctx.allowed = w.systemComponentIDs[i]
var drawn int
for _, entity := range w.systemEntities[i] {
w.ctx.Entity = entity
w.ctx.components = w.systemComponents[i][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 %s for entity %d: %+v", w.systemName(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 t time.Time
if debug != 0 {
t = time.Now()
}
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
if debug != 0 {
log.Printf("- %s: %d drawn.", w.systemName(i), drawn)
}
}
w.systemDrawnEntities = entitiesDrawn
if debug != 0 {
log.Printf("Handled %d entity draws in %.2fms.", entitiesDrawn, float64(time.Since(t).Microseconds())/1000)
}
return nil
}
// CurrentDraws returns the number of System Draw calls required to draw the
// game on to the screen. Because entities may be handled by more than one
// System, this number may be higher than the number of active entities.
func (w *World) CurrentDraws() int {
if time.Since(w.systemDrawnEntitiesT) >= w.cacheTime {
w.systemDrawnEntitiesV = w.systemDrawnEntities
w.systemDrawnEntitiesT = time.Now()
}
return w.systemDrawnEntitiesV
}
// Preallocate creates and then immediately removes the specified number of entities.
// Because Gohan reuses removed entities, this has the effect of pre-allocating
// the memory used later to create entities normally. Pre-allocating enough
// entities to run your application after its systems has been added, but
// before any entities are created, will provide the greatest performance boost.
func (w *World) Preallocate(entities int) {
if len(w.availableEntities) >= entities {
return
}
e := make([]Entity, entities)
for i := 0; i < entities; i++ {
e[i] = w.NewEntity()
}
for i := 0; i < entities; i++ {
w.RemoveEntity(e[i])
}
}
func uniqueComponentIDs(v []ComponentID) []ComponentID {
var list []ComponentID
keys := make(map[ComponentID]bool)
for _, entry := range v {
if _, value := keys[entry]; !value {
keys[entry] = true
list = append(list, entry)
}
}
return list
}
func (w *World) componentName(id ComponentID) string {
if int(id) < len(w.systemComponentNames) {
return w.systemComponentNames[id]
}
return strconv.Itoa(int(id))
}
func (w *World) systemName(i int) string {
if i < len(w.systems) {
return getName(w.systems[i])
}
return strconv.Itoa(i)
}
func getName(v interface{}) string {
t := reflect.TypeOf(v)
if t.Kind() == reflect.Ptr {
return strings.Title(t.Elem().Name())
} else if t.Kind() == reflect.Struct {
return strings.Title(t.Name())
}
return ""
}