started with generic collision_system (not working yet)

This commit is contained in:
Thomas von Dein 2024-02-26 18:31:36 +01:00
parent 0ca5d8f4a0
commit 267e15cc27
10 changed files with 275 additions and 100 deletions

14
components/collider.go Normal file
View File

@ -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
}

View File

@ -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)

View File

@ -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))

View File

@ -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")
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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)
}

View File

@ -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) {}

View File

@ -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 {

View File

@ -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: <tab>: 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)