From 267e15cc274aa0030cf6b09705ce7777eb8b139f Mon Sep 17 00:00:00 2001 From: Thomas von Dein Date: Mon, 26 Feb 2024 18:31:36 +0100 Subject: [PATCH] started with generic collision_system (not working yet) --- components/collider.go | 14 +++++ game/game.go | 1 + game/levels.go | 3 ++ grid/grid.go | 16 ++++-- handlers/collectible_handler.go | 25 +++++++++ handlers/player_handler.go | 72 ++++++++++++++++++++++++++ observers/entity_observer.go | 50 ++++++++++++++++++ systems/collision_system.go | 92 +++++++++++++++++++++++++++++++++ systems/obstacle_system.go | 78 ++-------------------------- systems/player_system.go | 24 +-------- 10 files changed, 275 insertions(+), 100 deletions(-) create mode 100644 components/collider.go create mode 100644 handlers/collectible_handler.go create mode 100644 handlers/player_handler.go create mode 100644 observers/entity_observer.go create mode 100644 systems/collision_system.go diff --git a/components/collider.go b/components/collider.go new file mode 100644 index 0000000..16459a2 --- /dev/null +++ b/components/collider.go @@ -0,0 +1,14 @@ +package components + +import "github.com/mlange-42/arche/ecs" + +// if the function returns true, the entity will be removed. The +// supplied entity is the target entitiy with which the checked one +// collided. + +type Collider struct { + CollisionHandler func(*ecs.World, *Position, *Position, *Velocity, ecs.Entity) bool + EdgeHandler func(*ecs.World, *Position, *Position, *Velocity, ecs.Entity) bool + PassoverHandler func(*ecs.World, *Position, *Velocity, *ecs.Entity) bool + PassoverFinishedHandler func(*ecs.World, *Position, *Velocity, *ecs.Entity) bool +} diff --git a/game/game.go b/game/game.go index d3c697b..488e083 100644 --- a/game/game.go +++ b/game/game.go @@ -38,6 +38,7 @@ func NewGame(width, height, cellsize int, cfg *config.Config, startscene SceneNa observers.NewPlayerObserver(&world) observers.NewParticleObserver(&world) observers.NewObstacleObserver(&world) + observers.NewEntityObserver(&world) game.Observer = observers.NewGameObserver(&world, cfg.Startlevel, width, height, cellsize) game.Scenes[Welcome] = NewWelcomeScene(game) diff --git a/game/levels.go b/game/levels.go index 4cb52e8..1b4a69c 100644 --- a/game/levels.go +++ b/game/levels.go @@ -41,6 +41,9 @@ func NewLevel(game *Game, cellsize int, plan *assets.RawLevel) *Level { systemlist = append(systemlist, systems.NewCollectibleSystem(game.World)) + systemlist = append(systemlist, + systems.NewCollisionSystem(game.World, gridcontainer)) + systemlist = append(systemlist, systems.NewPlayerSystem(game.World, gridcontainer)) diff --git a/grid/grid.go b/grid/grid.go index 09c52b8..1f46b33 100644 --- a/grid/grid.go +++ b/grid/grid.go @@ -6,6 +6,7 @@ import ( "openquell/assets" "openquell/components" "openquell/config" + "openquell/handlers" "openquell/observers" "github.com/mlange-42/arche/ecs" @@ -29,11 +30,12 @@ func NewGrid(world *ecs.World, // we use this to turn our tiles into iterable entities, used for // collision detection, transformation and other things - playermapper := generic.NewMap4[ + playermapper := generic.NewMap5[ components.Position, components.Velocity, components.Renderable, - components.Player](world) + components.Player, + components.Collider](world) solidmapper := generic.NewMap4[ components.Position, @@ -70,9 +72,11 @@ func NewGrid(world *ecs.World, var transient *components.Transient var player *components.Player var destroyable *components.Destroyable + var collider *components.Collider playerobserver := observers.GetPlayerObserver(world) obstacleobserver := observers.GetObstacleObserver(world) + entityobserver := observers.GetEntityObserver(world) for point, tile := range mapslice { switch tile.Renderable { @@ -83,14 +87,17 @@ func NewGrid(world *ecs.World, pos, render, _, _ = solidmapper.Get(entity) case tile.Player: entity := playermapper.New() - pos, vel, render, player = playermapper.Get(entity) + pos, vel, render, player, collider = playermapper.Get(entity) playerobserver.AddEntity(entity) vel.Speed = config.PLAYERSPEED player.IsPrimary = tile.IsPrimary player.Sprites = tile.Tiles + collider.CollisionHandler = handlers.HandlePlayerCollision + collider.EdgeHandler = handlers.HandlePlayerEdgeBump case tile.Collectible: entity := colmapper.New() pos, render, _ = colmapper.Get(entity) + entityobserver.AddEntity(entity) case tile.Obstacle: entity := obsmapper.New() pos, vel, render, _ = obsmapper.Get(entity) @@ -98,14 +105,17 @@ func NewGrid(world *ecs.World, vel.PointingAt = tile.Direction vel.Speed = config.PLAYERSPEED obstacleobserver.AddEntity(entity) + entityobserver.AddEntity(entity) case tile.Transient: entity := transmapper.New() pos, render, transient = transmapper.Get(entity) transient.Sprites = tile.TileNames + entityobserver.AddEntity(entity) case tile.Destroyable: entity := doormapper.New() pos, render, destroyable = doormapper.Get(entity) destroyable.Sprites = tile.Tiles + entityobserver.AddEntity(entity) default: log.Fatalln("unsupported tile type encountered") } diff --git a/handlers/collectible_handler.go b/handlers/collectible_handler.go new file mode 100644 index 0000000..7ee8fa6 --- /dev/null +++ b/handlers/collectible_handler.go @@ -0,0 +1,25 @@ +package handlers + +import ( + "openquell/components" + . "openquell/config" +) + +// type Collider struct { +// CollisionHandler func(*Position, *Position, *Velocity) bool +// EdgeHandler func(*Position, *Position, *Velocity) bool +// PassoverHandler func(*Position, *Velocity) bool +// PassoverFinishedHandler func(*Position, *Velocity) bool +// } + +func HandleCollectibleCollision(pos, newpos *components.Position, velocity *components.Velocity) bool { + pos.Set(newpos) + velocity.Change(Stop) + + return true +} + +func HandleCollectibleEdgeBump(pos, newpos *components.Position, velocity *components.Velocity) bool { + pos.Set(newpos) + return true +} diff --git a/handlers/player_handler.go b/handlers/player_handler.go new file mode 100644 index 0000000..ab9b932 --- /dev/null +++ b/handlers/player_handler.go @@ -0,0 +1,72 @@ +package handlers + +import ( + "openquell/components" + . "openquell/config" + "openquell/util" + + "log/slog" + + "github.com/mlange-42/arche/ecs" + "github.com/mlange-42/arche/generic" +) + +// type Collider struct { +// CollisionHandler func(*Position, *Position, *Velocity) bool +// EdgeHandler func(*Position, *Position, *Velocity) bool +// PassoverHandler func(*Position, *Velocity) bool +// PassoverFinishedHandler func(*Position, *Velocity) bool +// } + +func HandlePlayerCollision(world *ecs.World, pos, newpos *components.Position, + velocity *components.Velocity, target ecs.Entity) bool { + + velmapper := generic.NewMap[components.Velocity](world) + wallmapper := generic.NewMap[components.Solid](world) + obsmapper := generic.NewMap[components.Obstacle](world) + + if wallmapper.Has(target) { + pos.Set(newpos) + velocity.Change(Stop) + return true + } + + if obsmapper.Has(target) { + obsvelocity := velmapper.Get(target) + + if CheckObstacleSide(velocity, obsvelocity.Direction) { + // player died + return false + } else { + // bumped into nonlethal obstacle side, stop the + // player, set the obstacle in motion, if possible + obsvelocity.Set(velocity) + velocity.Change(Stop) + pos.Set(newpos) + } + + } + + return true +} + +func HandlePlayerEdgeBump(world *ecs.World, playerpos, + newpos *components.Position, velocity *components.Velocity, target ecs.Entity) bool { + playerpos.Set(newpos) + return true +} + +func CheckObstacleSide(playervelocity *components.Velocity, obsdirection int) bool { + movingdirection := playervelocity.InvertDirection() + + if movingdirection == Stop || movingdirection == obsdirection { + slog.Debug("Damaging obstacle collision", + "playerdirection", util.DirectionStr(playervelocity.Direction), + "obsdirection", util.DirectionStr(obsdirection), + "movingdirection", util.DirectionStr(movingdirection), + ) + return true + } + + return false +} diff --git a/observers/entity_observer.go b/observers/entity_observer.go new file mode 100644 index 0000000..a585694 --- /dev/null +++ b/observers/entity_observer.go @@ -0,0 +1,50 @@ +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 EntityObserver 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 +} + +func NewEntityObserver(world *ecs.World) { + observer := &EntityObserver{} + observer.Entities = make(map[ecs.Entity]int) + + resmanger := generic.NewResource[EntityObserver](world) + resmanger.Add(observer) + + listen := listener.NewCallback( + func(world *ecs.World, event ecs.EntityEvent) { + observerID := ecs.ResourceID[EntityObserver](world) + observer := world.Resources().Get(observerID).(*EntityObserver) + observer.RemoveEntity(event.Entity) + }, + + event.EntityRemoved, + ) + + world.SetListener(&listen) +} + +func GetEntityObserver(world *ecs.World) *EntityObserver { + observerID := ecs.ResourceID[EntityObserver](world) + observer := world.Resources().Get(observerID).(*EntityObserver) + return observer +} + +func (observer *EntityObserver) AddEntity(entity ecs.Entity) { + observer.Entities[entity] = 1 +} + +func (observer *EntityObserver) RemoveEntity(entity ecs.Entity) { + observer.Entities = make(map[ecs.Entity]int) +} diff --git a/systems/collision_system.go b/systems/collision_system.go new file mode 100644 index 0000000..398827e --- /dev/null +++ b/systems/collision_system.go @@ -0,0 +1,92 @@ +package systems + +import ( + "openquell/components" + . "openquell/components" + "openquell/grid" + "openquell/observers" + + "github.com/hajimehoshi/ebiten/v2" + "github.com/mlange-42/arche/ecs" + "github.com/mlange-42/arche/generic" +) + +type CollisionSystem struct { + World *ecs.World + Selector *generic.Filter4[Position, Velocity, Renderable, Collider] + GridContainer *grid.GridContainer +} + +func NewCollisionSystem(world *ecs.World, gridcontainer *grid.GridContainer) System { + system := &CollisionSystem{ + Selector: generic.NewFilter4[Position, Velocity, Renderable, Collider](), + GridContainer: gridcontainer, + World: world, + } + + return system +} + +func (system CollisionSystem) Update() error { + entityobserver := observers.GetEntityObserver(system.World) + + posID := ecs.ComponentID[components.Position](system.World) + EntitiesToRemove := []ecs.Entity{} + query := system.Selector.Query(system.World) + + for query.Next() { + position, velocity, _, handler := query.Get() + if velocity.Moving() { + // check if we bump agains the screen edge + ok, newpos := system.GridContainer.Grid.BumpEdge(position, velocity) + if ok { + if handler.EdgeHandler != nil { + if !handler.EdgeHandler(system.World, position, newpos, velocity, query.Entity()) { + EntitiesToRemove = append(EntitiesToRemove, query.Entity()) + } + } + } else { + // check if we collide with some solid entity + ok, tilepos := system.GridContainer.Grid.GetSolidNeighborPosition(position, velocity, true) + if ok { + intersects, newpos := tilepos.Intersects(position, velocity) + if intersects { + if handler.CollisionHandler != nil { + if !handler.CollisionHandler(system.World, position, newpos, velocity, query.Entity()) { + EntitiesToRemove = append(EntitiesToRemove, query.Entity()) + } + } + } + } + } + + // check if current entity bumps into another entity + for foreign_entity := range entityobserver.Entities { + if foreign_entity == query.Entity() { + // don't check entity against itself + continue + } + + foreign_position := (*Position)(system.World.Get(foreign_entity, posID)) + + ok, newpos := foreign_position.Intersects(position, velocity) + if ok { + if handler.CollisionHandler != nil { + if !handler.CollisionHandler(system.World, position, newpos, velocity, foreign_entity) { + EntitiesToRemove = append(EntitiesToRemove, query.Entity()) + } + } + } + } + + } + } + + for _, entity := range EntitiesToRemove { + system.World.RemoveEntity(entity) + } + + return nil +} + +func (system *CollisionSystem) Draw(screen *ebiten.Image) {} diff --git a/systems/obstacle_system.go b/systems/obstacle_system.go index e1a5b9e..19a9cf2 100644 --- a/systems/obstacle_system.go +++ b/systems/obstacle_system.go @@ -34,86 +34,16 @@ 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) if gameobserver.Lost { return nil } - posID := ecs.ComponentID[components.Position](system.World) - veloID := ecs.ComponentID[components.Velocity](system.World) + var gameover bool - EntitiesToRemove := []ecs.Entity{} - - query := system.Selector.Query(system.World) - gameover := false - - for query.Next() { - obsposition, obsvelocity, _, _ := query.Get() - - // check if one player has bumped into current obstacle - for player := range playerobserver.Entities { - playerposition := (*Position)(system.World.Get(player, posID)) - playervelocity := (*Velocity)(system.World.Get(player, veloID)) - - ok, newpos := obsposition.Intersects(playerposition, playervelocity) - if ok { - // slog.Debug("bumped into obstacle", "obstacle", obstacle) - - if CheckObstacleSide(playervelocity, obsvelocity.Direction) { - // player died - EntitiesToRemove = append(EntitiesToRemove, player) - gameover = true - } else { - // bumped into nonlethal obstacle side, stop the - // player, set the obstacle in motion, if possible - slog.Debug("bump not die", "originalpos", playerposition) - obsvelocity.Set(playervelocity) - playervelocity.Change(Stop) - playerposition.Set(newpos) - } - } - } - - // check if current obstacle bumped into another obstacle - for foreign_obstacle := range obstacleobserver.Entities { - if foreign_obstacle == query.Entity() { - // don't check obstacle against itself - continue - } - - foreign_obstacle_position := (*Position)(system.World.Get(foreign_obstacle, posID)) - - ok, newpos := foreign_obstacle_position.Intersects(obsposition, obsvelocity) - if ok { - //slog.Debug("bumped into foreign obstacle", "obstacle", foreign_obstacle) - obsposition.Set(newpos) - obsvelocity.ResetDirectionAndStop() - } - } - - // FIXME: this is the same loop as in player_system, unite the - // two, just iterate over all entities with pos,vel,render, dammit - if obsvelocity.Moving() { - ok, tilepos := system.GridContainer.Grid.GetSolidNeighborPosition(obsposition, obsvelocity, true) - if ok { - intersects, newpos := tilepos.Intersects(obsposition, obsvelocity) - if intersects { - // slog.Debug("collision with foreign obstacle detected", "tile", - // tilepos, "obs", obsposition, "new", newpos) - - obsposition.Set(newpos) - obsvelocity.ResetDirectionAndStop() - } - } - - obsposition.Move(obsvelocity) - } - - } - - for _, entity := range EntitiesToRemove { - system.World.RemoveEntity(entity) + if len(playerobserver.Entities) == 0 { + // FIXME: check this in game or elsewhere + gameover = true } if gameover { diff --git a/systems/player_system.go b/systems/player_system.go index 078d7b1..1121a2c 100644 --- a/systems/player_system.go +++ b/systems/player_system.go @@ -59,7 +59,7 @@ func (system PlayerSystem) Update() error { // check player movements etc query = system.Selector.Query(system.World) for query.Next() { - playerposition, velocity, player, _ := query.Get() + _, velocity, player, _ := query.Get() if !player.IsPrimary { continue @@ -96,28 +96,6 @@ func (system PlayerSystem) Update() error { // other keys: : switch player, etc } } - - if velocity.Moving() { - ok, newpos := system.GridContainer.Grid.BumpEdge(playerposition, velocity) - if ok { - //slog.Debug("falling off the edge", "newpos", newpos) - playerposition.Set(newpos) - } else { - ok, tilepos := system.GridContainer.Grid.GetSolidNeighborPosition(playerposition, velocity, true) - if ok { - slog.Debug("HaveSolidNeighbor", "ok", ok, "tilepos", - tilepos.Point(), "playerpos", playerposition) - intersects, newpos := tilepos.Intersects(playerposition, velocity) - if intersects { - // slog.Debug("collision detected", "tile", - // tilepos, "player", playerposition, "new", newpos) - - playerposition.Set(newpos) - velocity.Change(Stop) - } - } - } - } } query = system.Selector.Query(system.World)