27 changed files with 664 additions and 437 deletions
@ -0,0 +1,33 @@
@@ -0,0 +1,33 @@
|
||||
package gohan |
||||
|
||||
import "log" |
||||
|
||||
type Context struct { |
||||
Entity Entity |
||||
|
||||
s int // System index.
|
||||
c []ComponentID |
||||
w *World |
||||
} |
||||
|
||||
// Component gets a Component of the currently handled Entity.
|
||||
func (ctx *Context) Component(componentID ComponentID) interface{} { |
||||
var found bool |
||||
for _, id := range ctx.c { |
||||
if id == componentID { |
||||
found = true |
||||
break |
||||
} |
||||
} |
||||
if !found { |
||||
log.Panicf("illegal component access: component %d is not queried by system %d", componentID, ctx.s) |
||||
} |
||||
return ctx.w.Component(ctx.Entity, componentID) |
||||
} |
||||
|
||||
// RemoveEntity removes the currently handled Entity's components, causing it
|
||||
// to no longer be handled by any system. Because Gohan reuses removed EntityIDs,
|
||||
// applications must also remove any internal references to the removed Entity.
|
||||
func (ctx *Context) RemoveEntity() { |
||||
ctx.w.RemoveEntity(ctx.Entity) |
||||
} |
@ -0,0 +1,5 @@
@@ -0,0 +1,5 @@
|
||||
package world |
||||
|
||||
import "code.rocketnine.space/tslocum/gohan" |
||||
|
||||
var World = gohan.NewWorld() |
@ -1,267 +0,0 @@
@@ -1,267 +0,0 @@
|
||||
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 |
||||
} |
@ -0,0 +1,307 @@
@@ -0,0 +1,307 @@
|
||||
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) |
||||
} |
||||
} |
||||
} |
||||
|
||||