added transient entities: when crossed, turns into wall tile

This commit is contained in:
Thomas von Dein 2024-02-22 14:33:01 +01:00
parent f696660ccd
commit 308f335cd1
20 changed files with 375 additions and 53 deletions

View File

@ -4,8 +4,8 @@ Background: background-lila
#######
# v #
# #
# t #
#> S <#
# #
# ^ #

View File

@ -3,13 +3,13 @@ Background: background-lila
#######
# ^ #
#############
# #
#< S >#
# #
#^ v#
#######
#> S # #
# ># >#
#< # #
# v# <#
#############

View File

@ -30,8 +30,10 @@ type Tile struct {
Renderable bool
Velocity bool
Collectible bool
Transient bool
Particle int // -1=unused, 0-3 = show image of slice
Particles []*ebiten.Image
Tiles []*ebiten.Image
TileNames []string // same thing, only the names
Obstacle bool
Direction int // obstacles
}
@ -77,6 +79,7 @@ func NewTileObstacle(class string, direction int) *Tile {
Renderable: true,
Obstacle: true,
Direction: direction,
Velocity: true,
}
}
@ -93,7 +96,28 @@ func NewTileParticle(class []string) *Tile {
Solid: false,
Renderable: false,
Particle: 0,
Particles: sprites,
Tiles: sprites,
}
}
func NewTileTranswall(class []string) *Tile {
sprites := []*ebiten.Image{}
names := []string{}
for _, sprite := range class {
sprites = append(sprites, Assets[sprite])
names = append(names, sprite)
}
return &Tile{
Id: '*',
Class: "transwall",
Solid: false,
Renderable: true,
Transient: true,
Tiles: sprites,
Sprite: sprites[0], // initially use the first
TileNames: names,
}
}
@ -123,6 +147,7 @@ func InitTiles() TileRegistry {
return TileRegistry{
' ': {Id: ' ', Class: "floor", Renderable: false},
'#': NewTileBlock("block-grey32"),
'B': NewTileBlock("block-orange-32"),
'S': NewTilePlayer(),
'o': NewTileCollectible("collectible-orange"),
'+': NewTileObstacle("obstacle-star", config.All),
@ -138,6 +163,7 @@ func InitTiles() TileRegistry {
"particle-ring-5",
"particle-ring-6",
}),
't': NewTileTranswall([]string{"transwall", "block-orange-32"}),
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

@ -13,7 +13,7 @@ type Renderable struct {
type Particle struct {
Show bool
Index int
Particles []*ebiten.Image
Tiles []*ebiten.Image
}
type Speed struct {
@ -26,4 +26,7 @@ type Solid struct{}
type Floor struct{}
type Player struct{}
type Collectible struct{}
type Obstacle struct{}
type Obstacle struct {
Direction int
}

View File

@ -3,7 +3,6 @@ package components
import (
"fmt"
"image"
"log/slog"
. "openquell/config"
)
@ -90,13 +89,14 @@ func (tile *Position) Intersects(moving *Position, velocity *Velocity) (bool, *P
is := tile.Rect.Bounds().Intersect(object.Rect.Bounds())
if is != image.ZR {
/*
slog.Debug("Intersect",
"velocity", velocity.Data,
"player", moving.Rect,
"moved player", object.Rect,
"collision at", is,
)
*/
// collision, snap into neighbouring tile depending on the direction
switch velocity.Direction {
case West:

24
components/transient.go Normal file
View File

@ -0,0 +1,24 @@
package components
import (
"log"
)
type Transient struct {
Activated bool
Sprites []string
Current int // sprite index
}
func (trans *Transient) GetNext() string {
if len(trans.Sprites) > trans.Current {
trans.Current++
return trans.Sprites[trans.Current]
}
log.Fatalf("not enough sprites in transient tile, have %d sprites, index requested: %d",
len(trans.Sprites), trans.Current+1,
)
return ""
}

View File

@ -8,6 +8,18 @@ import (
type Velocity struct {
Data Position
Direction int
PointingAt int
}
func (velocity *Velocity) Set(new *Velocity) {
velocity.Direction = new.Direction
velocity.Data.Set(&new.Data)
}
func (velocity *Velocity) ResetDirectionAndStop() {
velocity.Data.X = 0
velocity.Data.Y = 0
velocity.Direction = velocity.PointingAt
}
func (velocity *Velocity) Change(direction int) {

View File

@ -33,6 +33,7 @@ func NewGame(width, height, cellsize, startlevel int, startscene SceneName) *Gam
observers.NewPlayerObserver(&world)
observers.NewParticleObserver(&world)
observers.NewObstacleObserver(&world)
game.Observer = observers.NewGameObserver(&world, startlevel, width, height, cellsize)
game.Scenes[Welcome] = NewWelcomeScene(game)

View File

@ -70,7 +70,8 @@ func (scene *LevelScene) Update() error {
func (scene *LevelScene) Draw(screen *ebiten.Image) {
if scene.CurrentLevel != scene.Game.Observer.CurrentLevel {
slog.Debug("level", "current", scene.CurrentLevel, "next", scene.Game.Observer.CurrentLevel)
slog.Debug("level", "current", scene.CurrentLevel,
"next", scene.Game.Observer.CurrentLevel)
scene.CurrentLevel = scene.Game.Observer.CurrentLevel
scene.Levels[scene.CurrentLevel].SetupGrid(scene.Game)
}

View File

@ -8,6 +8,7 @@ import (
"openquell/grid"
"openquell/observers"
"openquell/systems"
"openquell/util"
"strings"
"github.com/hajimehoshi/ebiten/v2"
@ -79,9 +80,11 @@ func (level *Level) SetupGrid(game *Game) {
playerobserver.RemoveEntities()
// setup world
level.GridSystem.SetGrid(grid.NewGrid(game.World, level.Cellsize, level.Width, level.Height, level.Mapslice))
level.GridSystem.SetGrid(
grid.NewGrid(game.World, level.Cellsize, level.Width, level.Height, level.Mapslice))
}
// parses a RawLevel and generates a mapslice from it, which is being used as grid
func LevelToSlice(game *Game, level *assets.RawLevel, tilesize int) map[image.Point]*assets.Tile {
size := game.ScreenWidth * game.ScreenHeight
mapslice := make(map[image.Point]*assets.Tile, size)
@ -93,6 +96,10 @@ func LevelToSlice(game *Game, level *assets.RawLevel, tilesize int) map[image.Po
}
for x, char := range line {
if !util.Exists(assets.Tiles, byte(char)) {
log.Fatalf("unregistered tile type %c encountered", char)
}
mapslice[image.Point{x, y}] = assets.Tiles[byte(char)]
}
}

View File

@ -3,7 +3,6 @@ package grid
import (
"image"
"log"
"log/slog"
"openquell/assets"
"openquell/components"
"openquell/config"
@ -14,6 +13,7 @@ import (
)
type Grid struct {
World *ecs.World
Width int
Height int
Size int
@ -48,18 +48,26 @@ func NewGrid(world *ecs.World,
components.Renderable,
components.Collectible](world)
obsmapper := generic.NewMap4[
obsmapper := generic.NewMap5[
components.Position,
components.Velocity,
components.Renderable,
components.Speed,
components.Obstacle](world)
transmapper := generic.NewMap3[
components.Position,
components.Renderable,
components.Transient](world)
var pos *components.Position
var vel *components.Velocity
var render *components.Renderable
var speed *components.Speed
var transient *components.Transient
playerobserver := observers.GetPlayerObserver(world)
obstacleobserver := observers.GetObstacleObserver(world)
for point, tile := range mapslice {
switch tile.Renderable {
@ -73,15 +81,20 @@ func NewGrid(world *ecs.World,
pos, _, render, speed, _ = playermapper.Get(entity)
playerobserver.AddEntity(entity)
speed.Value = config.PLAYERSPEED
slog.Debug("player start pos", "X", point.X*tilesize,
"Y", point.Y*tilesize, "Z", 191)
case tile.Collectible:
entity := colmapper.New()
pos, render, _ = colmapper.Get(entity)
case tile.Obstacle:
entity := obsmapper.New()
pos, vel, render, _ = obsmapper.Get(entity)
pos, vel, render, speed, _ = obsmapper.Get(entity)
vel.Direction = tile.Direction
vel.PointingAt = tile.Direction
speed.Value = config.PLAYERSPEED
obstacleobserver.AddEntity(entity)
case tile.Transient:
entity := transmapper.New()
pos, render, transient = transmapper.Get(entity)
transient.Sprites = tile.TileNames
default:
log.Fatalln("unsupported tile type encountered")
}
@ -106,6 +119,7 @@ func NewGrid(world *ecs.World,
Width: width,
Height: height,
Map: mapslice,
World: world,
}
}
@ -122,3 +136,19 @@ func (grid *Grid) GetTile(
tile := grid.Map[newpoint]
return tile
}
func (grid *Grid) SetTile(tile *assets.Tile, point image.Point) {
solidmapper := generic.NewMap4[
components.Position,
components.Renderable,
components.Tilish,
components.Solid](grid.World)
grid.Map[point] = tile
entity := solidmapper.New()
pos, render, _, _ := solidmapper.Get(entity)
render.Image = tile.Sprite
pos.Update(point.X*grid.Tilesize, point.Y*grid.Tilesize, grid.Tilesize)
}

View File

@ -0,0 +1,54 @@
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)
}

BIN
src/transwall.xcf Normal file

Binary file not shown.

View File

@ -109,7 +109,7 @@ func (system *CollectibleSystem) AddParticle(position *components.Position) {
particleobserver.AddEntity(entity)
particle.Index = assets.Tiles['*'].Particle
particle.Particles = assets.Tiles['*'].Particles
particle.Tiles = assets.Tiles['*'].Tiles
pos.Update(
position.X-(16), // FIXME: use global tilesize!

View File

@ -18,6 +18,7 @@ type GridSystem struct {
Selector *generic.Filter3[Renderable, Position, Solid]
UseCache bool
Cache *ebiten.Image
Count int // register tile count, invalidates cache
Background *ebiten.Image
Width, Height, TilesX, TilesY, Tilesize int
Grid *grid.Grid
@ -52,12 +53,13 @@ func (system *GridSystem) Update() {}
func (system *GridSystem) Draw(screen *ebiten.Image) {
op := &ebiten.DrawImageOptions{}
query := system.Selector.Query(system.World)
if !system.UseCache {
if !system.UseCache || query.Count() != system.Count {
// map not cached or cacheable, write it to the cache
draw.Draw(system.Cache, system.Background.Bounds(), system.Background, image.ZP, draw.Src)
query := system.Selector.Query(system.World)
system.Count = query.Count()
for query.Next() {
sprite, pos, _ := query.Get()
@ -76,6 +78,7 @@ func (system *GridSystem) Draw(screen *ebiten.Image) {
// use the cached map
op.GeoM.Reset()
screen.DrawImage(system.Cache, op)
query.Close()
}
}
@ -88,7 +91,7 @@ func (system *GridSystem) GetSolidNeighborPosition(
return false, nil
}
// set to true, ifwe are already on the last tile in the current
// set to true, if we are already on the last tile in the current
// direction, i.e. on the edge of the grid
edge := true
neighborpos := position.Point()

View File

@ -16,14 +16,15 @@ import (
type ObstacleSystem struct {
World *ecs.World
Selector *generic.Filter4[Position, Velocity, Obstacle, Renderable]
PreviousFreePos *components.Position
Selector *generic.Filter5[Position, Velocity, Obstacle, Renderable, Speed]
Grid *GridSystem
}
func NewObstacleSystem(world *ecs.World) *ObstacleSystem {
func NewObstacleSystem(world *ecs.World, grid *GridSystem) *ObstacleSystem {
system := &ObstacleSystem{
Selector: generic.NewFilter4[Position, Velocity, Obstacle, Renderable](),
Selector: generic.NewFilter5[Position, Velocity, Obstacle, Renderable, Speed](),
World: world,
Grid: grid,
}
return system
@ -32,6 +33,7 @@ func NewObstacleSystem(world *ecs.World) *ObstacleSystem {
func (system *ObstacleSystem) Update() {
playerobserver := observers.GetPlayerObserver(system.World)
gameobserver := observers.GetGameObserver(system.World)
obstacleobserver := observers.GetObstacleObserver(system.World)
if gameobserver.Lost {
return
@ -46,27 +48,67 @@ func (system *ObstacleSystem) Update() {
gameover := false
for query.Next() {
obsposition, obsvelocity, obstacle, _ := query.Get()
obsposition, obsvelocity, _, _, speed := 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)
// slog.Debug("bumped into obstacle", "obstacle", obstacle)
if CheckObstacleSide(playervelocity, obsvelocity.Direction) {
// player died
EntitiesToRemove = append(EntitiesToRemove, player)
gameover = true
} else {
playervelocity.Change(Stop)
// 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)
slog.Debug("bump not die", "newpos", 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.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, speed)
}
}
for _, entity := range EntitiesToRemove {
@ -91,7 +133,7 @@ func (system *ObstacleSystem) Draw(screen *ebiten.Image) {
query := system.Selector.Query(system.World)
for query.Next() {
pos, _, _, sprite := query.Get()
pos, _, _, sprite, _ := query.Get()
op.GeoM.Reset()
op.GeoM.Translate(float64(pos.X), float64(pos.Y))
@ -114,7 +156,7 @@ func (system *ObstacleSystem) AddParticle(position *components.Position) {
particleobserver.AddEntity(entity)
particle.Index = assets.Tiles['*'].Particle
particle.Particles = assets.Tiles['*'].Particles
particle.Tiles = assets.Tiles['*'].Tiles
pos.Update(
position.X-(16), // FIXME: use global tilesize!

View File

@ -40,7 +40,7 @@ func (system *ParticleSystem) Update() {
if timer.IsReady() {
switch {
// particle shows from earlier tick, animate
case particle.Index > -1 && particle.Index < len(particle.Particles)-1:
case particle.Index > -1 && particle.Index < len(particle.Tiles)-1:
particle.Index++
timer.Start(config.PARTICLE_LOOPWAIT)
default:
@ -69,7 +69,7 @@ func (system *ParticleSystem) Draw(screen *ebiten.Image) {
if particle.Show {
op.GeoM.Reset()
op.GeoM.Translate(float64(pos.X), float64(pos.Y))
screen.DrawImage(particle.Particles[particle.Index], op)
screen.DrawImage(particle.Tiles[particle.Index], op)
}
}
}

View File

@ -1,7 +1,6 @@
package systems
import (
"log/slog"
. "openquell/components"
. "openquell/config"
@ -16,6 +15,7 @@ type PlayerSystem struct {
Particle *ParticleSystem
Collectible *CollectibleSystem
Obstacle *ObstacleSystem
Transient *TransientSystem
Grid *GridSystem
}
@ -24,7 +24,8 @@ func NewPlayerSystem(world *ecs.World, grid *GridSystem) *PlayerSystem {
Selector: generic.NewFilter5[Position, Velocity, Player, Renderable, Speed](),
Particle: NewParticleSystem(world, grid.Tilesize),
Collectible: NewCollectibleSystem(world),
Obstacle: NewObstacleSystem(world),
Obstacle: NewObstacleSystem(world, grid),
Transient: NewTransientSystem(world, grid),
Grid: grid,
World: world,
}
@ -55,15 +56,15 @@ func (system PlayerSystem) Update() error {
if velocity.Moving() {
ok, newpos := system.Grid.BumpEdge(playerposition, velocity)
if ok {
slog.Debug("falling off the edge", "newpos", newpos)
//slog.Debug("falling off the edge", "newpos", newpos)
playerposition.Set(newpos)
} else {
ok, tilepos := system.Grid.GetSolidNeighborPosition(playerposition, velocity, true)
if ok {
intersects, newpos := tilepos.Intersects(playerposition, velocity)
if intersects {
slog.Debug("collision detected", "tile",
tilepos, "player", playerposition, "new", newpos)
// slog.Debug("collision detected", "tile",
// tilepos, "player", playerposition, "new", newpos)
playerposition.Set(newpos)
velocity.Change(Stop)
@ -76,6 +77,7 @@ func (system PlayerSystem) Update() error {
system.Particle.Update() // may set player position
system.Obstacle.Update()
system.Collectible.Update()
system.Transient.Update()
query = system.Selector.Query(system.World)
for query.Next() {
@ -104,4 +106,5 @@ func (system *PlayerSystem) Draw(screen *ebiten.Image) {
system.Collectible.Draw(screen)
system.Particle.Draw(screen)
system.Obstacle.Draw(screen)
system.Transient.Draw(screen)
}

116
systems/transient_system.go Normal file
View File

@ -0,0 +1,116 @@
package systems
import (
"log/slog"
"openquell/assets"
"openquell/components"
. "openquell/components"
"openquell/observers"
"github.com/hajimehoshi/ebiten/v2"
"github.com/mlange-42/arche/ecs"
"github.com/mlange-42/arche/generic"
)
type TransientSystem struct {
World *ecs.World
Selector *generic.Filter3[Position, Renderable, Transient]
Grid *GridSystem
SolidMapper generic.Map4[ // needed for replacement
components.Position,
components.Renderable,
components.Tilish,
components.Solid]
}
type TransientToWall struct {
Entity ecs.Entity
NewSprite string
Position components.Position
}
func NewTransientSystem(world *ecs.World, grid *GridSystem) *TransientSystem {
solidmapper := generic.NewMap4[
components.Position,
components.Renderable,
components.Tilish,
components.Solid](world)
system := &TransientSystem{
Selector: generic.NewFilter3[Position, Renderable, Transient](),
World: world,
Grid: grid,
SolidMapper: solidmapper,
}
return system
}
func (system *TransientSystem) Update() {
playerobserver := observers.GetPlayerObserver(system.World)
posID := ecs.ComponentID[components.Position](system.World)
veloID := ecs.ComponentID[components.Velocity](system.World)
query := system.Selector.Query(system.World)
EntitiestoMakeSolid := []TransientToWall{}
for query.Next() {
transientposition, _, transient := query.Get()
for player := range playerobserver.Entities {
playerposition := (*Position)(system.World.Get(player, posID))
playervelocity := (*Velocity)(system.World.Get(player, veloID))
ok, _ := transientposition.Intersects(playerposition, playervelocity)
if ok {
// display the transient sprite as long as the player crosses it
transient.Activated = true
} else {
// the player crossed the transient wall completely
if transient.Activated {
EntitiestoMakeSolid = append(EntitiestoMakeSolid, TransientToWall{
Entity: query.Entity(),
Position: *transientposition,
NewSprite: transient.GetNext(),
})
slog.Debug("done transient", "transient", transientposition)
}
}
}
}
for _, convertible := range EntitiestoMakeSolid {
// remove transient entity
system.World.RemoveEntity(convertible.Entity)
// replace with solid entity
entity := system.SolidMapper.New()
pos, render, _, _ := system.SolidMapper.Get(entity)
// set it up apropriately
pos.Set(&convertible.Position)
render.Image = assets.Assets[convertible.NewSprite]
// also setup the grid tile with a new solid, so that
// collision detection works
system.Grid.Grid.SetTile(
assets.NewTileBlock(convertible.NewSprite),
convertible.Position.Point(),
)
}
}
func (system *TransientSystem) Draw(screen *ebiten.Image) {
// write transients (these are no tiles!)
op := &ebiten.DrawImageOptions{}
query := system.Selector.Query(system.World)
for query.Next() {
pos, render, _ := query.Get()
op.GeoM.Reset()
op.GeoM.Translate(float64(pos.X), float64(pos.Y))
screen.DrawImage(render.Image, op)
}
}