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 }