From 72f0aa769183d9479ce54921079014d54167df26 Mon Sep 17 00:00:00 2001 From: Thomas von Dein Date: Sun, 11 Feb 2024 13:00:56 +0100 Subject: [PATCH] refactoring to systems complete. also added observers for collision checks --- TODO.md | 15 ++----- assets/levels/gurke.lvl | 2 +- assets/loader-levels.go | 2 +- game/game.go | 4 ++ grid/grid.go | 16 +++----- observers/particle_observer.go | 53 +++++++++++++++++++++++++ observers/player_observer.go | 52 ++++++++++++++++++++++++ systems/collectible_system.go | 72 ++++++++++++++++++++++------------ systems/particle_system.go | 45 +++++++++------------ systems/player_system.go | 8 +--- 10 files changed, 187 insertions(+), 82 deletions(-) create mode 100644 observers/particle_observer.go create mode 100644 observers/player_observer.go diff --git a/TODO.md b/TODO.md index 471ec89..8db425f 100644 --- a/TODO.md +++ b/TODO.md @@ -1,16 +1,9 @@ ## Levels: -- get rid of floor image -- use background image over whole screen size -- put level on top of it -- paint level in levels/**.lvl on whole screen into the middle - use first line as background image name - ignore comments in lvl files -- add whitespace-mode flag in lvl files -- check when sphere bounces from one end to the other endlessly w/o any solids in between. this is a game lock and equals game over +- check when sphere bounces from one end to the other endlessly w/o + any solids in between. this is a game lock and equals game over -SYSTEM Stuff: - -- initialize systems in NewLevel() -- replace level.Update() with systems update -- mv Draw() from level to systems +- Grid Observer: + https://github.com/mlange-42/arche/issues/374 diff --git a/assets/levels/gurke.lvl b/assets/levels/gurke.lvl index 7b38eee..74e257e 100644 --- a/assets/levels/gurke.lvl +++ b/assets/levels/gurke.lvl @@ -5,7 +5,7 @@ Background: background-orange # ############ # # ############ # - # S # + # o S # # ######## ### # ############ # diff --git a/assets/loader-levels.go b/assets/loader-levels.go index bc6f601..363b29a 100644 --- a/assets/loader-levels.go +++ b/assets/loader-levels.go @@ -76,7 +76,7 @@ func NewTileParticle(class []string) *Tile { Class: "particle", Solid: false, Renderable: false, - Particle: -1, + Particle: 0, Particles: sprites, } } diff --git a/game/game.go b/game/game.go index ea8e094..9da877d 100644 --- a/game/game.go +++ b/game/game.go @@ -3,6 +3,7 @@ package game import ( "fmt" "image" + "openquell/observers" "github.com/hajimehoshi/ebiten/v2" "github.com/mlange-42/arche/ecs" @@ -28,6 +29,9 @@ func NewGame(width, height, startlevel int, startscene int) *Game { Scenes: map[int]Scene{}, } + observers.NewPlayerObserver(&world) + observers.NewParticleObserver(&world) + game.Scenes[Play] = NewLevelScene(game, startlevel) fmt.Println(game.World.Stats().String()) diff --git a/grid/grid.go b/grid/grid.go index a9ba74f..9004f58 100644 --- a/grid/grid.go +++ b/grid/grid.go @@ -6,6 +6,7 @@ import ( "log" "openquell/assets" "openquell/components" + "openquell/observers" "github.com/mlange-42/arche/ecs" "github.com/mlange-42/arche/generic" @@ -45,14 +46,11 @@ func NewGrid(world *ecs.World, components.Renderable, components.Collectible](world) - ptmapper := generic.NewMap2[ - components.Position, - components.Particle, - ](world) - var pos *components.Position var render *components.Renderable + playerobserver := observers.GetPlayerObserver(world) + for point, tile := range mapslice { switch tile.Renderable { case true: @@ -63,6 +61,8 @@ func NewGrid(world *ecs.World, case tile.Player: entity := playermapper.New() pos, _, render, _ = playermapper.Get(entity) + playerobserver.AddEntity(entity) + fmt.Printf("player start pos: %d,%d\n", point.X*tilesize, point.Y*tilesize) case tile.Collectible: entity := colmapper.New() @@ -85,12 +85,6 @@ func NewGrid(world *ecs.World, pos.Update(point.X*tilesize, point.Y*tilesize, tilesize) } - // not part of the grid, but add them as well - entity := ptmapper.New() - _, particle := ptmapper.Get(entity) - particle.Index = assets.Tiles['*'].Particle - particle.Particles = assets.Tiles['*'].Particles - return &Grid{ Size: len(mapslice), Tilesize: tilesize, diff --git a/observers/particle_observer.go b/observers/particle_observer.go new file mode 100644 index 0000000..c10ab52 --- /dev/null +++ b/observers/particle_observer.go @@ -0,0 +1,53 @@ +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 new file mode 100644 index 0000000..fed55f4 --- /dev/null +++ b/observers/player_observer.go @@ -0,0 +1,52 @@ +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) +} diff --git a/systems/collectible_system.go b/systems/collectible_system.go index 0164897..8a303a9 100644 --- a/systems/collectible_system.go +++ b/systems/collectible_system.go @@ -2,8 +2,10 @@ package systems import ( "fmt" - "image" + "openquell/assets" + "openquell/components" . "openquell/components" + "openquell/observers" "github.com/hajimehoshi/ebiten/v2" "github.com/mlange-42/arche/ecs" @@ -11,9 +13,8 @@ import ( ) type CollectibleSystem struct { - World *ecs.World - Selector *generic.Filter3[Position, Collectible, Renderable] - EntitiesToRemove []ecs.Entity + World *ecs.World + Selector *generic.Filter3[Position, Collectible, Renderable] } func NewCollectibleSystem(world *ecs.World) *CollectibleSystem { @@ -25,38 +26,39 @@ func NewCollectibleSystem(world *ecs.World) *CollectibleSystem { return system } -func (system *CollectibleSystem) CheckPlayerCollision( - playerposition *Position, - playervelocity *Velocity) (bool, image.Point) { +func (system *CollectibleSystem) Update() { + playerobserver := observers.GetPlayerObserver(system.World) - particle_pos := image.Point{} - var bumped bool + posID := ecs.ComponentID[components.Position](system.World) + veloID := ecs.ComponentID[components.Velocity](system.World) + particlepositions := []*components.Position{} + EntitiesToRemove := []ecs.Entity{} query := system.Selector.Query(system.World) for query.Next() { colposition, collectible, _ := query.Get() - ok, _ := playerposition.Intersects(colposition, playervelocity) - if ok { - fmt.Printf("bumped into collectible %v\n", collectible) - system.EntitiesToRemove = append(system.EntitiesToRemove, query.Entity()) - particle_pos.X = colposition.X - particle_pos.Y = colposition.Y - bumped = true + for player := range playerobserver.Entities { + playerposition := (*Position)(system.World.Get(player, posID)) + playervelocity := (*Velocity)(system.World.Get(player, veloID)) + + ok, _ := playerposition.Intersects(colposition, playervelocity) + if ok { + fmt.Printf("bumped into collectible %v\n", collectible) + particlepositions = append(particlepositions, colposition) + EntitiesToRemove = append(EntitiesToRemove, query.Entity()) + } } } - return bumped, particle_pos -} - -func (system *CollectibleSystem) Update() { - // remove collectible if collected - for _, entity := range system.EntitiesToRemove { - system.World.RemoveEntity(entity) + for _, pos := range particlepositions { + system.AddParticle(pos) } - system.EntitiesToRemove = []ecs.Entity{} + for _, entity := range EntitiesToRemove { + system.World.RemoveEntity(entity) + } } func (system *CollectibleSystem) Draw(screen *ebiten.Image) { @@ -73,3 +75,25 @@ func (system *CollectibleSystem) Draw(screen *ebiten.Image) { screen.DrawImage(sprite.Image, op) } } + +func (system *CollectibleSystem) AddParticle(position *components.Position) { + particleobserver := observers.GetParticleObserver(system.World) + + ptmapper := generic.NewMap2[ + components.Position, + components.Particle, + ](system.World) + + entity := ptmapper.New() + pos, particle := ptmapper.Get(entity) + particleobserver.AddEntity(entity) + + particle.Index = assets.Tiles['*'].Particle + particle.Particles = assets.Tiles['*'].Particles + + pos.Update( + position.X-(16), // FIXME: use global tilesize! + position.Y-(16), + 64, + ) +} diff --git a/systems/particle_system.go b/systems/particle_system.go index f9ab9c4..74ada10 100644 --- a/systems/particle_system.go +++ b/systems/particle_system.go @@ -1,7 +1,6 @@ package systems import ( - "image" . "openquell/components" "github.com/hajimehoshi/ebiten/v2" @@ -25,34 +24,29 @@ func NewParticleSystem(world *ecs.World, cellsize int) *ParticleSystem { return system } -func (system *ParticleSystem) Update(detonate bool, position *image.Point) { +func (system *ParticleSystem) Update() { // display debris after collecting + EntitiesToRemove := []ecs.Entity{} + query := system.Selector.Query(system.World) for query.Next() { // we loop, but it's only one anyway - ptposition, particle := query.Get() + _, particle := query.Get() - if detonate { - // particle appears - ptposition.Update( - position.X-(system.Cellsize/2), - position.Y-(system.Cellsize/2), - 64, - ) - - particle.Index = 0 // start displaying the particle - } else { - switch { - // particle shows from earlier tick, animate - case particle.Index > -1 && particle.Index < len(particle.Particles)-1: - particle.Index++ - default: - // last sprite reached, remove it - particle.Index = -1 - } + switch { + // particle shows from earlier tick, animate + case particle.Index > -1 && particle.Index < len(particle.Particles)-1: + particle.Index++ + default: + // last sprite reached, remove it + EntitiesToRemove = append(EntitiesToRemove, query.Entity()) } } + + for _, entity := range EntitiesToRemove { + system.World.RemoveEntity(entity) + } } func (system *ParticleSystem) Draw(screen *ebiten.Image) { @@ -63,11 +57,8 @@ func (system *ParticleSystem) Draw(screen *ebiten.Image) { for query.Next() { pos, particle := query.Get() - if particle.Index > -1 { - op.GeoM.Reset() - op.GeoM.Translate(float64(pos.X), float64(pos.Y)) - screen.DrawImage(particle.Particles[particle.Index], op) - } + op.GeoM.Reset() + op.GeoM.Translate(float64(pos.X), float64(pos.Y)) + screen.DrawImage(particle.Particles[particle.Index], op) } - } diff --git a/systems/player_system.go b/systems/player_system.go index 26f4f6b..18eac83 100644 --- a/systems/player_system.go +++ b/systems/player_system.go @@ -2,7 +2,6 @@ package systems import ( "fmt" - "image" . "openquell/components" . "openquell/config" @@ -34,9 +33,6 @@ func NewPlayerSystem(world *ecs.World, grid *GridSystem) *PlayerSystem { func (system PlayerSystem) Update() error { query := system.Selector.Query(system.World) - var bumped bool - var particle_pos image.Point - for query.Next() { playerposition, velocity, _, _ := query.Get() @@ -74,14 +70,12 @@ func (system PlayerSystem) Update() error { } } } - - bumped, particle_pos = system.Collectible.CheckPlayerCollision(playerposition, velocity) } playerposition.Move(velocity) } - system.Particle.Update(bumped, &particle_pos) + system.Particle.Update() system.Collectible.Update() return nil