From 18be0ebe38eb4bc04e7917b93e757549ed45b1d3 Mon Sep 17 00:00:00 2001 From: Thomas von Dein Date: Tue, 27 Feb 2024 14:45:23 +0100 Subject: [PATCH] added hud system, turned all observers into one central observer --- game/game.go | 6 +-- game/level_scene.go | 11 ----- game/levels.go | 6 ++- grid/grid.go | 10 +++-- observers/game_observer.go | 82 +++++++++++++++++++++++++++++++++- observers/obstacle_observer.go | 54 ---------------------- observers/particle_observer.go | 53 ---------------------- observers/player_observer.go | 56 ----------------------- systems/collectible_system.go | 13 +++--- systems/destroyable_system.go | 4 +- systems/hud_system.go | 44 ++++++++++++++++++ systems/obstacle_system.go | 23 +++++----- systems/transient_system.go | 4 +- 13 files changed, 160 insertions(+), 206 deletions(-) delete mode 100644 observers/obstacle_observer.go delete mode 100644 observers/particle_observer.go delete mode 100644 observers/player_observer.go create mode 100644 systems/hud_system.go diff --git a/game/game.go b/game/game.go index d3c697b..473f9b2 100644 --- a/game/game.go +++ b/game/game.go @@ -35,9 +35,9 @@ func NewGame(width, height, cellsize int, cfg *config.Config, startscene SceneNa Config: cfg, } - observers.NewPlayerObserver(&world) - observers.NewParticleObserver(&world) - observers.NewObstacleObserver(&world) + // observers.NewPlayerObserver(&world) + // observers.NewParticleObserver(&world) + // observers.NewObstacleObserver(&world) game.Observer = observers.NewGameObserver(&world, cfg.Startlevel, width, height, cellsize) game.Scenes[Welcome] = NewWelcomeScene(game) diff --git a/game/level_scene.go b/game/level_scene.go index 0033e8a..bb93528 100644 --- a/game/level_scene.go +++ b/game/level_scene.go @@ -1,12 +1,10 @@ package game import ( - "fmt" "log/slog" "openquell/assets" "github.com/hajimehoshi/ebiten/v2" - "github.com/hajimehoshi/ebiten/v2/ebitenutil" ) type LevelScene struct { @@ -81,13 +79,4 @@ func (scene *LevelScene) Draw(screen *ebiten.Image) { screen.Clear() scene.Levels[scene.CurrentLevel].Draw(screen) - - // FIXME: put into hud_system - op := &ebiten.DrawImageOptions{} - screen.DrawImage(assets.Assets["hud"], op) - ebitenutil.DebugPrintAt(screen, fmt.Sprintf( - "FPS: %02.f TPS: %02.f", - ebiten.ActualFPS(), - ebiten.ActualTPS(), - ), 10, 10) } diff --git a/game/levels.go b/game/levels.go index 18aaf20..b02b538 100644 --- a/game/levels.go +++ b/game/levels.go @@ -52,6 +52,8 @@ func NewLevel(game *Game, cellsize int, plan *assets.RawLevel) *Level { systemlist = append(systemlist, systems.NewDestroyableSystem(game.World, gridcontainer)) + systemlist = append(systemlist, systems.NewHudSystem(game.World, plan)) + mapslice, backupmap := LevelToSlice(game, plan, cellsize) return &Level{ @@ -105,8 +107,8 @@ func (level *Level) SetupGrid(game *Game) { level.World.Batch().RemoveEntities(selector) // get rid of any players on PlayerObserver. FIXME: remove them in grid.NewGrid()? - playerobserver := observers.GetPlayerObserver(level.World) - playerobserver.RemoveEntities() + observer := observers.GetGameObserver(level.World) + observer.RemoveEntities() // get rid of possibly manipulated map level.RestoreMap() diff --git a/grid/grid.go b/grid/grid.go index 09c52b8..068c402 100644 --- a/grid/grid.go +++ b/grid/grid.go @@ -71,8 +71,10 @@ func NewGrid(world *ecs.World, var player *components.Player var destroyable *components.Destroyable - playerobserver := observers.GetPlayerObserver(world) - obstacleobserver := observers.GetObstacleObserver(world) + playerID := ecs.ComponentID[components.Player](world) + obstacleID := ecs.ComponentID[components.Obstacle](world) + + observer := observers.GetGameObserver(world) for point, tile := range mapslice { switch tile.Renderable { @@ -84,7 +86,7 @@ func NewGrid(world *ecs.World, case tile.Player: entity := playermapper.New() pos, vel, render, player = playermapper.Get(entity) - playerobserver.AddEntity(entity) + observer.AddEntity(entity, playerID) vel.Speed = config.PLAYERSPEED player.IsPrimary = tile.IsPrimary player.Sprites = tile.Tiles @@ -97,7 +99,7 @@ func NewGrid(world *ecs.World, vel.Direction = tile.Direction vel.PointingAt = tile.Direction vel.Speed = config.PLAYERSPEED - obstacleobserver.AddEntity(entity) + observer.AddEntity(entity, obstacleID) case tile.Transient: entity := transmapper.New() pos, render, transient = transmapper.Get(entity) diff --git a/observers/game_observer.go b/observers/game_observer.go index a68564d..4a28853 100644 --- a/observers/game_observer.go +++ b/observers/game_observer.go @@ -4,17 +4,33 @@ import ( "openquell/components" "github.com/mlange-42/arche/ecs" + "github.com/mlange-42/arche/ecs/event" "github.com/mlange-42/arche/generic" + "github.com/mlange-42/arche/listener" ) -// Used for global game state +// Used for global game state. Also stores mobile entities of the +// current level. The Entities map will be reset on each level +// initialization in grid/grid.go type GameObserver struct { + World *ecs.World CurrentLevel, Width int Height, Cellsize, Score int StopTimer *components.Timer Lost bool // set to true if player is struck or something, by default: win! Retry bool NextlevelText string + Entities map[ecs.ID]map[ecs.Entity]int +} + +func (observer *GameObserver) GetListenerCallback(comp ecs.ID) listener.Callback { + return listener.NewCallback( + func(world *ecs.World, event ecs.EntityEvent) { + observer.RemoveEntity(event.Entity, comp) + }, + event.EntityRemoved, + comp, + ) } func NewGameObserver(world *ecs.World, startlevel, width, height, cellsize int) *GameObserver { @@ -24,11 +40,33 @@ func NewGameObserver(world *ecs.World, startlevel, width, height, cellsize int) Width: width, Height: height, Cellsize: cellsize, + World: world, } resmanger := generic.NewResource[GameObserver](world) resmanger.Add(observer) + playerID := ecs.ComponentID[components.Player](world) + obstacleID := ecs.ComponentID[components.Obstacle](world) + particleID := ecs.ComponentID[components.Particle](world) + + playerListener := observer.GetListenerCallback(playerID) + obstacleListener := observer.GetListenerCallback(obstacleID) + particleListener := observer.GetListenerCallback(particleID) + + listen := listener.NewDispatch( + &playerListener, + &obstacleListener, + &particleListener, + ) + + world.SetListener(&listen) + + observer.Entities = make(map[ecs.ID]map[ecs.Entity]int) + observer.Entities[playerID] = make(map[ecs.Entity]int) + observer.Entities[obstacleID] = make(map[ecs.Entity]int) + observer.Entities[particleID] = make(map[ecs.Entity]int) + return observer } @@ -41,3 +79,45 @@ func GetGameObserver(world *ecs.World) *GameObserver { func (observer *GameObserver) Gameover() { observer.Lost = true } + +func (observer *GameObserver) AddEntity(entity ecs.Entity, comp ecs.ID) { + observer.Entities[comp][entity] = 1 +} + +func (observer *GameObserver) RemoveEntity(entity ecs.Entity, comp ecs.ID) { + delete(observer.Entities[comp], entity) +} + +func (observer *GameObserver) GetEntities(comp ecs.ID) []ecs.Entity { + keys := make([]ecs.Entity, 0, len(observer.Entities[comp])) + for k, _ := range observer.Entities[comp] { + keys = append(keys, k) + } + return keys +} + +func (observer *GameObserver) GetPlayers() []ecs.Entity { + playerID := ecs.ComponentID[components.Player](observer.World) + return observer.GetEntities(playerID) +} + +func (observer *GameObserver) GetObstacles() []ecs.Entity { + obstacleID := ecs.ComponentID[components.Obstacle](observer.World) + return observer.GetEntities(obstacleID) +} + +func (observer *GameObserver) GetParticles() []ecs.Entity { + particleID := ecs.ComponentID[components.Particle](observer.World) + return observer.GetEntities(particleID) +} + +func (observer *GameObserver) RemoveEntities() { + playerID := ecs.ComponentID[components.Player](observer.World) + obstacleID := ecs.ComponentID[components.Obstacle](observer.World) + particleID := ecs.ComponentID[components.Particle](observer.World) + + observer.Entities = make(map[ecs.ID]map[ecs.Entity]int) + observer.Entities[playerID] = make(map[ecs.Entity]int) + observer.Entities[obstacleID] = make(map[ecs.Entity]int) + observer.Entities[particleID] = make(map[ecs.Entity]int) +} diff --git a/observers/obstacle_observer.go b/observers/obstacle_observer.go deleted file mode 100644 index 52a71a5..0000000 --- a/observers/obstacle_observer.go +++ /dev/null @@ -1,54 +0,0 @@ -package observers - -import ( - "github.com/mlange-42/arche/ecs" - "github.com/mlange-42/arche/ecs/event" - "github.com/mlange-42/arche/generic" - "github.com/mlange-42/arche/listener" -) - -// will be added as an ecs.Resource to the world so we can use it in -// CollisionSystem to dynamically make it appear or disappear -type ObstacleObserver struct { - // we only have one obstacle so far, if we use multiple ones, turn - // this in to a map, see player_observer.go for an example - Entities map[ecs.Entity]int -} - -// Create a new resource of type PlayerObserver which will hold all -// obstacle entities. We also add a Listener which looks for -// EntityRemoved events and if a player gets removed, we also remove -// it from the Entity map in our observer. -func NewObstacleObserver(world *ecs.World) { - observer := &ObstacleObserver{} - observer.Entities = make(map[ecs.Entity]int) - - resmanger := generic.NewResource[ObstacleObserver](world) - resmanger.Add(observer) - - listen := listener.NewCallback( - func(world *ecs.World, event ecs.EntityEvent) { - observerID := ecs.ResourceID[ObstacleObserver](world) - observer := world.Resources().Get(observerID).(*ObstacleObserver) - observer.RemoveEntity(event.Entity) - }, - - event.EntityRemoved, - ) - - world.SetListener(&listen) -} - -func GetObstacleObserver(world *ecs.World) *ObstacleObserver { - observerID := ecs.ResourceID[ObstacleObserver](world) - observer := world.Resources().Get(observerID).(*ObstacleObserver) - return observer -} - -func (observer *ObstacleObserver) AddEntity(entity ecs.Entity) { - observer.Entities[entity] = 1 -} - -func (observer *ObstacleObserver) RemoveEntity(entity ecs.Entity) { - observer.Entities = make(map[ecs.Entity]int) -} diff --git a/observers/particle_observer.go b/observers/particle_observer.go deleted file mode 100644 index c10ab52..0000000 --- a/observers/particle_observer.go +++ /dev/null @@ -1,53 +0,0 @@ -package observers - -import ( - "github.com/mlange-42/arche/ecs" - "github.com/mlange-42/arche/ecs/event" - "github.com/mlange-42/arche/generic" - "github.com/mlange-42/arche/listener" -) - -// will be added as an ecs.Resource to the world so we can use it in -// CollisionSystem to dynamically make it appear or disappear -type ParticleObserver struct { - // we only have one particle so far, if we use multiple ones, turn - // this in to a map, see player_observer.go for an example - Entity ecs.Entity -} - -// Create a new resource of type PlayerObserver which will hold all -// player entities. We also add a Listener which looks for -// EntityRemoved events and if a player gets removed, we also remove -// it from the Entity map in our observer. -func NewParticleObserver(world *ecs.World) { - observer := &ParticleObserver{} - - resmanger := generic.NewResource[ParticleObserver](world) - resmanger.Add(observer) - - listen := listener.NewCallback( - func(world *ecs.World, event ecs.EntityEvent) { - observerID := ecs.ResourceID[ParticleObserver](world) - observer := world.Resources().Get(observerID).(*ParticleObserver) - observer.RemoveEntity(event.Entity) - }, - - event.EntityRemoved, - ) - - world.SetListener(&listen) -} - -func GetParticleObserver(world *ecs.World) *ParticleObserver { - observerID := ecs.ResourceID[ParticleObserver](world) - observer := world.Resources().Get(observerID).(*ParticleObserver) - return observer -} - -func (observer *ParticleObserver) AddEntity(entity ecs.Entity) { - observer.Entity = entity -} - -func (observer *ParticleObserver) RemoveEntity(entity ecs.Entity) { - observer.Entity = ecs.Entity{} -} diff --git a/observers/player_observer.go b/observers/player_observer.go deleted file mode 100644 index a5a4577..0000000 --- a/observers/player_observer.go +++ /dev/null @@ -1,56 +0,0 @@ -package observers - -import ( - "github.com/mlange-42/arche/ecs" - "github.com/mlange-42/arche/ecs/event" - "github.com/mlange-42/arche/generic" - "github.com/mlange-42/arche/listener" -) - -// will be added as an ecs.Resource to the world so we can use it in -// CollisionSystem to check for player collisions. -type PlayerObserver struct { - Entities map[ecs.Entity]int -} - -// Create a new resource of type PlayerObserver which will hold all -// player entities. We also add a Listener which looks for -// EntityRemoved events and if a player gets removed, we also remove -// it from the Entity map in our observer. -func NewPlayerObserver(world *ecs.World) { - observer := &PlayerObserver{} - observer.Entities = make(map[ecs.Entity]int) - - resmanger := generic.NewResource[PlayerObserver](world) - resmanger.Add(observer) - - listen := listener.NewCallback( - func(world *ecs.World, event ecs.EntityEvent) { - observerID := ecs.ResourceID[PlayerObserver](world) - observer := world.Resources().Get(observerID).(*PlayerObserver) - observer.RemoveEntity(event.Entity) - }, - - event.EntityRemoved, - ) - - world.SetListener(&listen) -} - -func GetPlayerObserver(world *ecs.World) *PlayerObserver { - observerID := ecs.ResourceID[PlayerObserver](world) - observer := world.Resources().Get(observerID).(*PlayerObserver) - return observer -} - -func (observer *PlayerObserver) AddEntity(entity ecs.Entity) { - observer.Entities[entity] = 1 -} - -func (observer *PlayerObserver) RemoveEntity(entity ecs.Entity) { - delete(observer.Entities, entity) -} - -func (observer *PlayerObserver) RemoveEntities() { - observer.Entities = make(map[ecs.Entity]int) -} diff --git a/systems/collectible_system.go b/systems/collectible_system.go index 42e00e2..7ad50cd 100644 --- a/systems/collectible_system.go +++ b/systems/collectible_system.go @@ -29,8 +29,7 @@ func NewCollectibleSystem(world *ecs.World) System { } func (system *CollectibleSystem) Update() error { - playerobserver := observers.GetPlayerObserver(system.World) - gameobserver := observers.GetGameObserver(system.World) + observer := observers.GetGameObserver(system.World) posID := ecs.ComponentID[components.Position](system.World) veloID := ecs.ComponentID[components.Velocity](system.World) @@ -41,7 +40,7 @@ func (system *CollectibleSystem) Update() error { query := system.Selector.Query(system.World) numcollectibles := query.Count() - if numcollectibles == 0 || gameobserver.Lost { + if numcollectibles == 0 || observer.Lost { query.Close() return nil } @@ -49,7 +48,7 @@ func (system *CollectibleSystem) Update() error { for query.Next() { colposition, collectible, _ := query.Get() - for player := range playerobserver.Entities { + for _, player := range observer.GetPlayers() { if !system.World.Alive(player) { continue } @@ -102,7 +101,7 @@ func (system *CollectibleSystem) Draw(screen *ebiten.Image) { } func (system *CollectibleSystem) AddParticle(position *components.Position) { - particleobserver := observers.GetParticleObserver(system.World) + observer := observers.GetGameObserver(system.World) ptmapper := generic.NewMap3[ components.Position, @@ -110,9 +109,11 @@ func (system *CollectibleSystem) AddParticle(position *components.Position) { components.Timer, ](system.World) + particleID := ecs.ComponentID[components.Particle](system.World) + entity := ptmapper.New() pos, particle, timer := ptmapper.Get(entity) - particleobserver.AddEntity(entity) + observer.AddEntity(entity, particleID) particle.Index = assets.Tiles['*'].Particle particle.Tiles = assets.Tiles['*'].Tiles diff --git a/systems/destroyable_system.go b/systems/destroyable_system.go index ca497a4..3964388 100644 --- a/systems/destroyable_system.go +++ b/systems/destroyable_system.go @@ -48,7 +48,7 @@ func NewDestroyableSystem(world *ecs.World, gridcontainer *grid.GridContainer) S } func (system *DestroyableSystem) Update() error { - playerobserver := observers.GetPlayerObserver(system.World) + observer := observers.GetGameObserver(system.World) posID := ecs.ComponentID[components.Position](system.World) veloID := ecs.ComponentID[components.Velocity](system.World) @@ -59,7 +59,7 @@ func (system *DestroyableSystem) Update() error { for query.Next() { doorposition, renderable, door := query.Get() - for player := range playerobserver.Entities { + for _, player := range observer.GetPlayers() { if !system.World.Alive(player) { continue } diff --git a/systems/hud_system.go b/systems/hud_system.go new file mode 100644 index 0000000..5306de4 --- /dev/null +++ b/systems/hud_system.go @@ -0,0 +1,44 @@ +package systems + +import ( + "fmt" + "openquell/assets" + "openquell/observers" + + "github.com/hajimehoshi/ebiten/v2" + "github.com/hajimehoshi/ebiten/v2/ebitenutil" + "github.com/mlange-42/arche/ecs" +) + +type HudSystem struct { + World *ecs.World + Cellsize int + Observer *observers.GameObserver + Plan *assets.RawLevel +} + +func NewHudSystem(world *ecs.World, plan *assets.RawLevel) System { + system := &HudSystem{ + Observer: observers.GetGameObserver(world), + World: world, + Plan: plan, + } + + return system +} + +func (system *HudSystem) Update() error { + return nil +} + +func (system *HudSystem) Draw(screen *ebiten.Image) { + op := &ebiten.DrawImageOptions{} + screen.DrawImage(assets.Assets["hud"], op) + ebitenutil.DebugPrintAt(screen, fmt.Sprintf( + "FPS: %02.f TPS: %02.f Level %s: %s", + ebiten.ActualFPS(), + ebiten.ActualTPS(), + system.Plan.Name, + system.Plan.Description, + ), 10, 10) +} diff --git a/systems/obstacle_system.go b/systems/obstacle_system.go index c3d5bcf..7c607a5 100644 --- a/systems/obstacle_system.go +++ b/systems/obstacle_system.go @@ -32,11 +32,9 @@ func NewObstacleSystem(world *ecs.World, gridcontainer *grid.GridContainer) Syst } func (system *ObstacleSystem) Update() error { - playerobserver := observers.GetPlayerObserver(system.World) - gameobserver := observers.GetGameObserver(system.World) - obstacleobserver := observers.GetObstacleObserver(system.World) + observer := observers.GetGameObserver(system.World) - if gameobserver.Lost { + if observer.Lost { return nil } @@ -51,7 +49,7 @@ func (system *ObstacleSystem) Update() error { obsposition, obsvelocity, _, _ := query.Get() // check if one player has bumped into current obstacle - for player := range playerobserver.Entities { + for _, player := range observer.GetPlayers() { if !system.World.Alive(player) { continue } @@ -78,7 +76,7 @@ func (system *ObstacleSystem) Update() error { } // check if current obstacle bumped into another obstacle - for foreign_obstacle := range obstacleobserver.Entities { + for _, foreign_obstacle := range observer.GetObstacles() { if foreign_obstacle == query.Entity() { // don't check obstacle against itself continue @@ -117,18 +115,17 @@ func (system *ObstacleSystem) Update() error { for _, entity := range EntitiesToRemove { slog.Debug("remove player") system.World.RemoveEntity(entity) - playerobserver.RemoveEntity(entity) } - if len(playerobserver.Entities) == 0 { + if len(observer.GetPlayers()) == 0 { // lost - timer := gameobserver.StopTimer + timer := observer.StopTimer if !timer.Running { timer.Start(LEVEL_END_WAIT) } - gameobserver.Gameover() + observer.Gameover() } return nil @@ -150,7 +147,7 @@ func (system *ObstacleSystem) Draw(screen *ebiten.Image) { } func (system *ObstacleSystem) AddParticle(position *components.Position) { - particleobserver := observers.GetParticleObserver(system.World) + observer := observers.GetGameObserver(system.World) ptmapper := generic.NewMap3[ components.Position, @@ -158,9 +155,11 @@ func (system *ObstacleSystem) AddParticle(position *components.Position) { components.Timer, ](system.World) + particleID := ecs.ComponentID[components.Particle](system.World) + entity := ptmapper.New() pos, particle, timer := ptmapper.Get(entity) - particleobserver.AddEntity(entity) + observer.AddEntity(entity, particleID) particle.Index = assets.Tiles['*'].Particle particle.Tiles = assets.Tiles['*'].Tiles diff --git a/systems/transient_system.go b/systems/transient_system.go index 85cb386..006409c 100644 --- a/systems/transient_system.go +++ b/systems/transient_system.go @@ -48,7 +48,7 @@ func NewTransientSystem(world *ecs.World, gridcontainer *grid.GridContainer) Sys } func (system *TransientSystem) Update() error { - playerobserver := observers.GetPlayerObserver(system.World) + observer := observers.GetGameObserver(system.World) posID := ecs.ComponentID[components.Position](system.World) veloID := ecs.ComponentID[components.Velocity](system.World) @@ -59,7 +59,7 @@ func (system *TransientSystem) Update() error { for query.Next() { transientposition, _, transient := query.Get() - for player := range playerobserver.Entities { + for _, player := range observer.GetPlayers() { if !system.World.Alive(player) { continue }