Add rewind ability

Players may rewind to their previous locations, even after they have died.
This commit is contained in:
Trevor Slocum 2021-12-13 20:50:25 -08:00
parent a1abf7eab5
commit 248036dbd9
12 changed files with 1649 additions and 545 deletions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

After

Width:  |  Height:  |  Size: 10 KiB

File diff suppressed because it is too large Load Diff

View File

@ -120,6 +120,7 @@ func (g *game) Layout(outsideWidth, outsideHeight int) (int, int) {
g.w, g.h = w, h
g.movementSystem.ScreenW, g.movementSystem.ScreenH = float64(w), float64(h)
g.renderSystem.ScreenW, g.renderSystem.ScreenH = w, h
g.renderSystem.SizeUpdated()
}
return g.w, g.h
}
@ -151,7 +152,7 @@ func (g *game) Draw(screen *ebiten.Image) {
func (g *game) addSystems() {
ecs := engine.Engine
g.movementSystem = system.NewMovementSystem() // TODO move into component
g.movementSystem = system.NewMovementSystem()
ecs.AddSystem(system.NewPlayerMoveSystem(g.player, g.movementSystem))
@ -164,9 +165,17 @@ func (g *game) addSystems() {
g.renderSystem = system.NewRenderSystem()
ecs.AddSystem(g.renderSystem)
ecs.AddSystem(system.NewRenderMessageSystem(g.player))
ecs.AddSystem(system.NewRenderDebugTextSystem(g.player))
ecs.AddSystem(system.NewProfileSystem(g.player))
// TODO
/*
world.World.MessageVisible = true
world.World.MessageText = "BOMB"
world.World.MessageText = "V & set it with X button."*/
}
func (g *game) loadAssets() error {

8
go.mod
View File

@ -6,17 +6,17 @@ require (
code.rocketnine.space/tslocum/gohan v0.0.0-20211212050415-e08cfe7970d8
github.com/fogleman/ease v0.0.0-20170301025033-8da417bf1776
github.com/hajimehoshi/ebiten/v2 v2.2.3
github.com/lafriks/go-tiled v0.5.0
github.com/lafriks/go-tiled v0.6.0
golang.org/x/image v0.0.0-20211028202545-6944b10bf410
golang.org/x/text v0.3.7
)
require (
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20211204153444-caad923f49f4 // indirect
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20211213063430-748e38ca8aec // indirect
github.com/hajimehoshi/oto/v2 v2.1.0-alpha.4 // indirect
github.com/jezek/xgb v0.0.0-20210312150743-0e0f116e1240 // indirect
golang.org/x/exp v0.0.0-20211210185655-e05463a05a18 // indirect
golang.org/x/exp v0.0.0-20211213173848-79cd87713b62 // indirect
golang.org/x/mobile v0.0.0-20211207041440-4e6c2922fdee // indirect
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c // indirect
golang.org/x/sys v0.0.0-20211210111614-af8b64212486 // indirect
golang.org/x/sys v0.0.0-20211213223007-03aa0b5f6827 // indirect
)

16
go.sum
View File

@ -7,8 +7,8 @@ github.com/disintegration/imaging v1.6.2/go.mod h1:44/5580QXChDfwIclfc/PCwrr44am
github.com/fogleman/ease v0.0.0-20170301025033-8da417bf1776 h1:VRIbnDWRmAh5yBdz+J6yFMF5vso1It6vn+WmM/5l7MA=
github.com/fogleman/ease v0.0.0-20170301025033-8da417bf1776/go.mod h1:9wvnDu3YOfxzWM9Cst40msBF1C2UdQgDv962oTxSuMs=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20210727001814-0db043d8d5be/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20211204153444-caad923f49f4 h1:KgfIc81yNEUKNAsF+Mt3C1Cl+iQqKF1r7nWEKzL0c2Y=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20211204153444-caad923f49f4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20211213063430-748e38ca8aec h1:3FLiRYO6PlQFDpUU7OEFlWgjGD1jnBIVSJ5SYRWk+9c=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20211213063430-748e38ca8aec/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
github.com/hajimehoshi/bitmapfont/v2 v2.1.3/go.mod h1:2BnYrkTQGThpr/CY6LorYtt/zEPNzvE/ND69CRTaHMs=
github.com/hajimehoshi/ebiten/v2 v2.2.3 h1:jZUP3XWP6mXaw9SCrjWT5Pl6EPuz6FY737dZQgN1KJ4=
github.com/hajimehoshi/ebiten/v2 v2.2.3/go.mod h1:olKl/qqhMBBAm2oI7Zy292nCtE+nitlmYKNF3UpbFn0=
@ -23,8 +23,8 @@ github.com/jezek/xgb v0.0.0-20210312150743-0e0f116e1240 h1:dy+DS31tGEGCsZzB45HmJ
github.com/jezek/xgb v0.0.0-20210312150743-0e0f116e1240/go.mod h1:3P4UH/k22rXyHIJD2w4h2XMqPX4Of/eySEZq9L6wqc4=
github.com/jfreymuth/oggvorbis v1.0.3/go.mod h1:1U4pqWmghcoVsCJJ4fRBKv9peUJMBHixthRlBeD6uII=
github.com/jfreymuth/vorbis v1.0.2/go.mod h1:DoftRo4AznKnShRl1GxiTFCseHr4zR9BN3TWXyuzrqQ=
github.com/lafriks/go-tiled v0.5.0 h1:f2BmYuBXGZHkArJJQ6NDGLfYJFJlDme7RVDjVzqxhTU=
github.com/lafriks/go-tiled v0.5.0/go.mod h1:Wm/iyI8TPSPWCHERNrAIRqh71ARI/liKyil/UNHg9dE=
github.com/lafriks/go-tiled v0.6.0 h1:kDJSvRPep6/5dtfdu0hqiAhXL40HmciMEXRb1TN/AU0=
github.com/lafriks/go-tiled v0.6.0/go.mod h1:xy+4iO8AKWpFNBWeqBqnq+Cb3Oirm5oin/irP/jPx6A=
github.com/pkg/browser v0.0.0-20180916011732-0a3d74bf9ce4/go.mod h1:4OwLy04Bl9Ef3GJJCoec+30X3LQs/0/m4HFRt/2LUSA=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
@ -38,8 +38,8 @@ golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8U
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190731235908-ec7cb31e5a56/go.mod h1:JhuoJpWY28nO4Vef9tZUw9qufEGTyX1+7lmHxV5q5G4=
golang.org/x/exp v0.0.0-20211210185655-e05463a05a18 h1:5GT90y+j5D8fWZ22z0SH3iQ8ZeTWX8quGjetD8Symgg=
golang.org/x/exp v0.0.0-20211210185655-e05463a05a18/go.mod h1:b9TAUYHmRtqA6klRHApnXMnj+OyLce4yF5cZCUbk2ps=
golang.org/x/exp v0.0.0-20211213173848-79cd87713b62 h1:NLQyV2V75F1Y0l2EyOgXGh0Z44+AlPxVIN6BHOniazA=
golang.org/x/exp v0.0.0-20211213173848-79cd87713b62/go.mod h1:b9TAUYHmRtqA6klRHApnXMnj+OyLce4yF5cZCUbk2ps=
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
golang.org/x/image v0.0.0-20190703141733-d6a02ce849c9/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
@ -72,8 +72,8 @@ golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210917161153-d61c044b1678/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20211210111614-af8b64212486 h1:5hpz5aRr+W1erYCL5JRhSUBJRph7l9XkNveoExlrKYk=
golang.org/x/sys v0.0.0-20211210111614-af8b64212486/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20211213223007-03aa0b5f6827 h1:A0Qkn7Z/n8zC1xd9LTw17AiKlBRK64tw3ejWQiEqca0=
golang.org/x/sys v0.0.0-20211213223007-03aa0b5f6827/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=

View File

@ -42,6 +42,14 @@ func (s *RenderBackgroundSystem) Draw(ctx *gohan.Context, screen *ebiten.Image)
return nil
}
alpha := 1.0
if world.World.FadingIn {
alpha = float64(world.World.FadeInTicks) / fadeInTime
if alpha > 1 {
alpha = 1
}
}
position := component.Position(ctx)
pctX, pctY := position.X/(512*16), position.Y/(512*16)
@ -51,18 +59,30 @@ func (s *RenderBackgroundSystem) Draw(ctx *gohan.Context, screen *ebiten.Image)
op := &ebiten.DrawImageOptions{}
op.GeoM.Scale(scale, scale)
if world.World.FadingIn {
op.ColorM.Scale(1, 1, 1, alpha)
}
screen.DrawImage(asset.ImgBackground1, op)
op = &ebiten.DrawImageOptions{}
op.GeoM.Scale(scale, scale)
op.GeoM.Translate(-offsetX*0.5, -offsetY*0.5)
if world.World.FadingIn {
op.ColorM.Scale(1, 1, 1, alpha)
}
screen.DrawImage(asset.ImgBackground2, op)
op = &ebiten.DrawImageOptions{}
op.GeoM.Scale(scale, scale)
op.GeoM.Translate(-offsetX*0.75, -offsetY*0.75)
if world.World.FadingIn {
op.ColorM.Scale(1, 1, 1, alpha)
}
screen.DrawImage(asset.ImgBackground3, op)
op = &ebiten.DrawImageOptions{}
op.GeoM.Scale(scale, scale)
op.GeoM.Translate(-offsetX, -offsetY)
if world.World.FadingIn {
op.ColorM.Scale(1, 1, 1, alpha)
}
screen.DrawImage(asset.ImgBackground4, op)
return nil
}

View File

@ -2,6 +2,7 @@ package system
import (
"log"
"os"
"time"
"code.rocketnine.space/tslocum/gohan"
@ -18,6 +19,9 @@ type playerMoveSystem struct {
player gohan.Entity
movement *MovementSystem
lastWalkDirL bool
rewindTicks int
nextRewindTick int
}
func NewPlayerMoveSystem(player gohan.Entity, m *MovementSystem) *playerMoveSystem {
@ -29,6 +33,7 @@ func NewPlayerMoveSystem(player gohan.Entity, m *MovementSystem) *playerMoveSyst
func (_ *playerMoveSystem) Needs() []gohan.ComponentID {
return []gohan.ComponentID{
component.PositionComponentID,
component.VelocityComponentID,
component.WeaponComponentID,
component.SpriteComponentID,
@ -40,10 +45,36 @@ func (_ *playerMoveSystem) Uses() []gohan.ComponentID {
}
func (s *playerMoveSystem) Update(ctx *gohan.Context) error {
if ebiten.IsKeyPressed(ebiten.KeyEscape) && !world.World.DisableEsc {
os.Exit(0)
return nil
}
if !world.World.GameStarted {
world.World.GameStartedTicks++
if world.World.GameStartedTicks == logoTime {
world.World.GameStarted = true
world.World.FadingIn = true
}
return nil
}
if world.World.FadingIn {
world.World.FadeInTicks++
if world.World.FadeInTicks == fadeInTime {
world.World.FadingIn = false
}
}
if ebiten.IsKeyPressed(ebiten.KeyControl) && inpututil.IsKeyJustPressed(ebiten.KeyV) {
world.World.Debug++
if world.World.Debug > 2 {
v := 1
if ebiten.IsKeyPressed(ebiten.KeyShift) {
v = 2
}
if world.World.Debug == v {
world.World.Debug = 0
} else {
world.World.Debug = v
}
s.movement.UpdateDebugCollisionRects()
return nil
@ -59,6 +90,115 @@ func (s *playerMoveSystem) Update(ctx *gohan.Context) error {
return nil
}
setWalkFrames := func() {
if world.World.NoClip {
return
}
sprite := component.Sprite(ctx)
if s.lastWalkDirL {
sprite.Frames = []*ebiten.Image{
asset.PlayerSS.WalkL1,
asset.PlayerSS.IdleL,
asset.PlayerSS.WalkL2,
asset.PlayerSS.IdleL,
}
} else {
sprite.Frames = []*ebiten.Image{
asset.PlayerSS.WalkR1,
asset.PlayerSS.IdleR,
asset.PlayerSS.WalkR2,
asset.PlayerSS.IdleR,
}
}
sprite.NumFrames = 4
sprite.FrameTime = 150 * time.Millisecond
}
setJumpAndIdleFrames := func() {
sprite := component.Sprite(ctx)
sprite.NumFrames = 0
if s.lastWalkDirL {
if (s.movement.OnGround == -1 && s.movement.OnLadder == -1) || s.movement.Jumping || world.World.NoClip {
sprite.Image = asset.PlayerSS.WalkL2
} else {
sprite.Image = asset.PlayerSS.IdleL
}
} else {
if (s.movement.OnGround == -1 && s.movement.OnLadder == -1) || s.movement.Jumping || world.World.NoClip {
sprite.Image = asset.PlayerSS.WalkR2
} else {
sprite.Image = asset.PlayerSS.IdleR
}
}
}
// Rewind time.
const minRewindTicks = 144 / 3
const maxRewindTicks = 144 * 1.5
if ebiten.IsKeyPressed(ebiten.KeyR) {
if len(s.movement.playerPositions) > 1 {
position := component.Position(ctx)
if !world.World.Rewinding {
world.World.Rewinding = true
world.World.GameOver = false
velocity := component.Velocity(ctx)
velocity.X, velocity.Y = 0, 0
s.movement.RecordPosition(position)
setWalkFrames()
}
lastPos := s.movement.playerPositions[len(s.movement.playerPositions)-1]
nextPos := s.movement.playerPositions[len(s.movement.playerPositions)-2]
rx, ry := nextPos[0]-lastPos[0], nextPos[1]-lastPos[1]
if s.rewindTicks == 0 {
dx, dy := deltaXY(lastPos[0], lastPos[1], nextPos[0], nextPos[1])
s.nextRewindTick = 144 * int((dx+dy)/150)
if s.nextRewindTick < minRewindTicks {
s.nextRewindTick = minRewindTicks
} else if s.nextRewindTick > maxRewindTicks {
s.nextRewindTick = maxRewindTicks
}
s.rewindTicks++
// Update player direction.
rewindDirL := rx >= 0
if s.lastWalkDirL != rewindDirL {
s.lastWalkDirL = rewindDirL
setWalkFrames()
}
return nil
}
pct := 1.0
if s.nextRewindTick > 0 {
pct = float64(s.rewindTicks) / float64(s.nextRewindTick)
if pct > 1 {
pct = 1
}
}
position.X, position.Y = lastPos[0]+(rx*pct), lastPos[1]+(ry*pct)
if s.rewindTicks == s.nextRewindTick {
s.movement.RemoveLastPosition()
s.rewindTicks = 0
} else {
s.rewindTicks++
}
} else {
setJumpAndIdleFrames()
}
return nil
} else if s.nextRewindTick != 0 {
s.rewindTicks = 0
s.nextRewindTick = 0
world.World.Rewinding = false
}
moveSpeed := 0.1
maxSpeed := 0.5
maxLevitateSpeed := 1.0
@ -86,11 +226,13 @@ func (s *playerMoveSystem) Update(ctx *gohan.Context) error {
}
if s.movement.OnGround != -1 && ebiten.IsKeyPressed(ebiten.KeyS) && !world.World.NoClip {
// Update player direction.
if ebiten.IsKeyPressed(ebiten.KeyA) {
s.lastWalkDirL = true
} else if ebiten.IsKeyPressed(ebiten.KeyD) {
s.lastWalkDirL = false
}
// Duck and look down.
sprite := component.Sprite(ctx)
sprite.NumFrames = 0
@ -211,35 +353,12 @@ func (s *playerMoveSystem) Update(ctx *gohan.Context) error {
}
if s.movement.OnLadder != -1 || world.World.NoClip {
setLadderFrames := func() {
if world.World.NoClip {
return
}
sprite := component.Sprite(ctx)
if s.lastWalkDirL {
sprite.Frames = []*ebiten.Image{
asset.PlayerSS.WalkL1,
asset.PlayerSS.IdleL,
asset.PlayerSS.WalkL2,
asset.PlayerSS.IdleL,
}
} else {
sprite.Frames = []*ebiten.Image{
asset.PlayerSS.WalkR1,
asset.PlayerSS.IdleR,
asset.PlayerSS.WalkR2,
asset.PlayerSS.IdleR,
}
}
sprite.NumFrames = 4
sprite.FrameTime = 150 * time.Millisecond
}
if ebiten.IsKeyPressed(ebiten.KeyW) {
if velocity.Y > -maxYSpeed || world.World.NoClip {
velocity.Y -= moveSpeed
}
setLadderFrames()
setWalkFrames()
walkKeyPressed = true
}
if ebiten.IsKeyPressed(ebiten.KeyS) && s.movement.OnGround == -1 {
@ -247,7 +366,7 @@ func (s *playerMoveSystem) Update(ctx *gohan.Context) error {
velocity.Y += moveSpeed
}
setLadderFrames()
setWalkFrames()
walkKeyPressed = true
}
}
@ -263,21 +382,7 @@ func (s *playerMoveSystem) Update(ctx *gohan.Context) error {
}
if !walkKeyPressed || s.movement.Jumping || (s.movement.OnGround == -1 && s.movement.OnLadder == -1) || world.World.NoClip {
sprite := component.Sprite(ctx)
sprite.NumFrames = 0
if s.lastWalkDirL {
if (s.movement.OnGround == -1 && s.movement.OnLadder == -1) || s.movement.Jumping || world.World.NoClip {
sprite.Image = asset.PlayerSS.WalkL2
} else {
sprite.Image = asset.PlayerSS.IdleL
}
} else {
if (s.movement.OnGround == -1 && s.movement.OnLadder == -1) || s.movement.Jumping || world.World.NoClip {
sprite.Image = asset.PlayerSS.WalkR2
} else {
sprite.Image = asset.PlayerSS.IdleR
}
}
setJumpAndIdleFrames()
}
return nil
@ -286,3 +391,14 @@ func (s *playerMoveSystem) Update(ctx *gohan.Context) error {
func (s *playerMoveSystem) Draw(_ *gohan.Context, _ *ebiten.Image) error {
return gohan.ErrSystemWithoutDraw
}
func deltaXY(x1, y1, x2, y2 float64) (dx float64, dy float64) {
dx, dy = x1-x2, y1-y2
if dx < 0 {
dx *= -1
}
if dy < 0 {
dy *= -1
}
return dx, dy
}

View File

@ -1,7 +1,6 @@
package system
import (
"log"
"os"
"path"
"runtime"
@ -37,8 +36,6 @@ func (s *profileSystem) Uses() []gohan.ComponentID {
func (s *profileSystem) Update(_ *gohan.Context) error {
if ebiten.IsKeyPressed(ebiten.KeyControl) && inpututil.IsKeyJustPressed(ebiten.KeyP) {
if s.cpuProfile == nil {
log.Println("CPU profiling started...")
runtime.SetCPUProfileRate(1000)
homeDir, err := os.UserHomeDir()
@ -61,7 +58,6 @@ func (s *profileSystem) Update(_ *gohan.Context) error {
s.cpuProfile.Close()
s.cpuProfile = nil
log.Println("CPU profiling stopped")
}
}
return nil

View File

@ -3,6 +3,7 @@ package system
import (
"image"
"image/color"
"math"
"time"
"code.rocketnine.space/tslocum/gohan"
@ -12,6 +13,8 @@ import (
"github.com/hajimehoshi/ebiten/v2"
)
const rewindThreshold = 1
type MovementSystem struct {
ScreenW, ScreenH float64
@ -33,6 +36,11 @@ type MovementSystem struct {
debugCollisionRects []gohan.Entity
debugLadderRects []gohan.Entity
debugFireRects []gohan.Entity
playerPositions [][2]float64
playerPosition [2]float64
playerPositionTicks int
recordedPosition bool
}
func NewMovementSystem() *MovementSystem {
@ -213,15 +221,14 @@ func (_ *MovementSystem) Uses() []gohan.ComponentID {
return nil
}
func (s *MovementSystem) checkFire(r image.Rectangle) {
func (s *MovementSystem) checkFire(ctx *gohan.Context, r image.Rectangle) {
for _, fireRect := range s.fireRects {
if r.Overlaps(fireRect) {
//world.World.GameOver = true
// TODO
position := engine.Engine.Component(world.World.Player, component.PositionComponentID).(*component.PositionComponent)
velocity := engine.Engine.Component(world.World.Player, component.VelocityComponentID).(*component.VelocityComponent)
position.X, position.Y = world.World.SpawnX, world.World.SpawnY
velocity.X, velocity.Y = 0, 0
world.World.GameOver = true
position := component.Position(ctx)
s.RecordPosition(position)
return
}
}
@ -235,6 +242,8 @@ func (s *MovementSystem) checkTriggers(r image.Rectangle) {
world.World.CanDoubleJump = true
case "DASH":
world.World.CanDash = true
case "KEY":
world.World.Keys++
default:
panic("unknown trigger " + world.World.TriggerNames[i])
}
@ -245,17 +254,23 @@ func (s *MovementSystem) checkTriggers(r image.Rectangle) {
world.World.TriggerRects = append(world.World.TriggerRects[:i], world.World.TriggerRects[i+1:]...)
world.World.TriggerEntities = append(world.World.TriggerEntities[:i], world.World.TriggerEntities[i+1:]...)
world.World.TriggerNames = append(world.World.TriggerNames[:i], world.World.TriggerNames[i+1:]...)
// TODO Show ability info and usage popup.
return
}
}
}
func (s *MovementSystem) checkCollisions(r image.Rectangle) {
s.checkFire(r)
func (s *MovementSystem) checkCollisions(ctx *gohan.Context, r image.Rectangle) {
s.checkFire(ctx, r)
s.checkTriggers(r)
}
func (s *MovementSystem) Update(ctx *gohan.Context) error {
if world.World.GameOver {
return nil
}
lastOnGround := s.OnGround
lastOnLadder := s.OnLadder
@ -264,6 +279,10 @@ func (s *MovementSystem) Update(ctx *gohan.Context) error {
bullet := ctx.Entity != world.World.Player
if ctx.Entity == world.World.Player && world.World.Rewinding {
return nil
}
// TODO apply left and right X collision adjustments (too large, can hang entirely off edge of cliff)
onLadder := -1
@ -324,19 +343,19 @@ func (s *MovementSystem) Update(ctx *gohan.Context) error {
}
if playerRectX.Overlaps(rect) {
collideX = i
s.checkCollisions(playerRectX)
s.checkCollisions(ctx, playerRectX)
}
if playerRectY.Overlaps(rect) {
collideY = i
s.checkCollisions(playerRectY)
s.checkCollisions(ctx, playerRectY)
}
if playerRectXY.Overlaps(rect) {
collideXY = i
s.checkCollisions(playerRectXY)
s.checkCollisions(ctx, playerRectXY)
}
if playerRectG.Overlaps(rect) {
collideG = i
s.checkCollisions(playerRectG)
s.checkCollisions(ctx, playerRectG)
}
}
if collideXY == -1 {
@ -362,6 +381,22 @@ func (s *MovementSystem) Update(ctx *gohan.Context) error {
}
}
// Remember positions to support rewinding time.
const recordTicks = 144 / 2
if ctx.Entity == world.World.Player {
if world.World.Rewinding {
s.playerPositionTicks = 0
} else {
s.playerPositionTicks++
if s.playerPositionTicks >= recordTicks || !s.recordedPosition {
s.RecordPosition(position)
s.playerPositionTicks = 0
s.recordedPosition = true
}
}
}
// TODO: Does this use enough memory to require pruning positions?
// Update debug rects.
if s.OnGround != lastOnGround || s.OnLadder != lastOnLadder {
@ -374,3 +409,15 @@ func (s *MovementSystem) Update(ctx *gohan.Context) error {
func (_ *MovementSystem) Draw(_ *gohan.Context, screen *ebiten.Image) error {
return gohan.ErrSystemWithoutDraw
}
func (s *MovementSystem) RecordPosition(position *component.PositionComponent) {
if math.Abs(position.X-s.playerPosition[0]) >= rewindThreshold || math.Abs(position.Y-s.playerPosition[1]) >= rewindThreshold {
s.playerPosition[0], s.playerPosition[1] = position.X, position.Y
s.playerPositions = append(s.playerPositions, s.playerPosition)
}
}
func (s *MovementSystem) RemoveLastPosition() {
s.playerPositions = s.playerPositions[:len(s.playerPositions)-1]
s.playerPosition = s.playerPositions[len(s.playerPositions)-1]
}

View File

@ -1,6 +1,8 @@
package system
import (
"image"
"image/color"
_ "image/png"
"time"
@ -9,10 +11,20 @@ import (
"code.rocketnine.space/tslocum/monovania/engine"
"code.rocketnine.space/tslocum/monovania/world"
"github.com/hajimehoshi/ebiten/v2"
"github.com/hajimehoshi/ebiten/v2/ebitenutil"
"golang.org/x/image/colornames"
)
const TileWidth = 16
const (
TileWidth = 16
logoText = "POWERED BY EBITEN"
logoTextScale = 4.75
logoTextWidth = 6.0 * float64(len(logoText)) * logoTextScale
logoTime = 144 * 3.5
fadeInTime = 144 * 1.25
)
var CamX, CamY float64
@ -25,12 +37,13 @@ type RenderSystem struct {
renderer gohan.Entity
debugImg *ebiten.Image
logoImg *ebiten.Image
}
func NewRenderSystem() *RenderSystem {
s := &RenderSystem{
renderer: engine.Engine.NewEntity(),
logoImg: ebiten.NewImage(1, 1),
op: &ebiten.DrawImageOptions{},
camScale: 4,
}
@ -38,6 +51,10 @@ func NewRenderSystem() *RenderSystem {
return s
}
func (s *RenderSystem) SizeUpdated() {
s.drawLogo()
}
func (s *RenderSystem) Needs() []gohan.ComponentID {
return []gohan.ComponentID{
component.PositionComponentID,
@ -65,6 +82,13 @@ func (s *RenderSystem) renderSprite(x float64, y float64, offsetx float64, offse
return 0
}
if world.World.FadingIn {
alpha = float64(world.World.FadeInTicks) / (fadeInTime / 2)
if alpha > 1 {
alpha = 1
}
}
// Skip drawing off-screen tiles.
drawX, drawY := s.levelCoordinatesToScreen(x, y)
const padding = TileWidth * 4
@ -114,8 +138,57 @@ func (s *RenderSystem) renderSprite(x float64, y float64, offsetx float64, offse
return 1
}
func (s *RenderSystem) drawLogo() {
s.logoImg = ebiten.NewImage(s.ScreenW, s.ScreenH)
s.logoImg.Fill(color.Black)
// Draw Ebiten logo.
logoSize := 172
totalSize := int(float64(logoSize) * 2.778)
logoColor := color.RGBA{219, 86, 32, 255}
logoOffset := int(float64(logoSize) * (4.0 / 9.0))
tailWidth := int(float64(logoSize) * (5.0 / 9.0))
x := (s.ScreenW / 2) - (totalSize / 2)
y := (s.ScreenH / 2)
for i := 0; i < 3; i++ {
offset := i * logoOffset
s.logoImg.SubImage(image.Rect(x+offset, y-offset, x+logoSize+offset, y+logoSize-offset)).(*ebiten.Image).Fill(logoColor)
}
offset := 4 * logoOffset
s.logoImg.SubImage(image.Rect(x+offset, y-offset, x+tailWidth+offset, y+logoSize-offset)).(*ebiten.Image).Fill(logoColor)
s.logoImg.SubImage(image.Rect(x+offset+logoOffset, y-offset+logoOffset, x+offset+logoSize, y-offset+logoSize)).(*ebiten.Image).Fill(logoColor)
img := ebiten.NewImage(200, 200)
ebitenutil.DebugPrint(img, logoText)
op := &ebiten.DrawImageOptions{}
op.GeoM.Scale(logoTextScale, logoTextScale)
op.GeoM.Translate(float64(s.ScreenW)/2-float64(logoTextWidth)/2, float64(s.ScreenH)/2+float64(logoSize))
s.logoImg.DrawImage(img, op)
}
func (s *RenderSystem) Draw(ctx *gohan.Context, screen *ebiten.Image) error {
if world.World.GameOver {
if !world.World.GameStarted {
if ctx.Entity == world.World.Player {
screen.Fill(color.RGBA{0, 0, 0, 255})
var alpha float64
if world.World.GameStartedTicks <= 144*.5 {
alpha = float64(world.World.GameStartedTicks) / (144 * .5)
} else if world.World.GameStartedTicks < 144*2.5 {
alpha = 1.0
} else {
alpha = 1.0 - (float64(world.World.GameStartedTicks-(144*2.5)) / (144 * 0.5))
}
if alpha > 1 {
alpha = 1
}
op := &ebiten.DrawImageOptions{}
op.ColorM.ChangeHSV(0, 1, alpha)
screen.DrawImage(s.logoImg, op)
}
return nil
} else if world.World.GameOver {
if ctx.Entity == world.World.Player {
screen.Fill(colornames.Darkred)
}

80
system/rendermessage.go Normal file
View File

@ -0,0 +1,80 @@
package system
import (
"image"
"image/color"
_ "image/png"
"strings"
"code.rocketnine.space/tslocum/gohan"
"code.rocketnine.space/tslocum/monovania/component"
"code.rocketnine.space/tslocum/monovania/world"
"github.com/hajimehoshi/ebiten/v2"
"github.com/hajimehoshi/ebiten/v2/ebitenutil"
)
type RenderMessageSystem struct {
player gohan.Entity
op *ebiten.DrawImageOptions
debugImg *ebiten.Image
}
func NewRenderMessageSystem(player gohan.Entity) *RenderMessageSystem {
s := &RenderMessageSystem{
player: player,
op: &ebiten.DrawImageOptions{},
debugImg: ebiten.NewImage(200, 200),
}
return s
}
func (s *RenderMessageSystem) Needs() []gohan.ComponentID {
return []gohan.ComponentID{
component.PositionComponentID,
component.VelocityComponentID,
component.WeaponComponentID,
}
}
func (s *RenderMessageSystem) Uses() []gohan.ComponentID {
return nil
}
func (s *RenderMessageSystem) Update(_ *gohan.Context) error {
return gohan.ErrSystemWithoutUpdate
}
func (s *RenderMessageSystem) Draw(ctx *gohan.Context, screen *ebiten.Image) error {
if !world.World.MessageVisible {
return nil
}
/*position := component.Position(ctx)
velocity := component.Velocity(ctx)*/
split := strings.Split(world.World.MessageText, "\n")
width := 0
for _, line := range split {
lineSize := len(line) * 12
if lineSize > width {
width = lineSize
}
}
height := len(split) * 32
const padding = 8
x, y := (world.World.ScreenW-width)/2, (world.World.ScreenH-height)/2
screen.SubImage(image.Rect(x-padding, y-padding, x+width+padding, y+height+padding)).(*ebiten.Image).Fill(color.Black)
s.debugImg.Clear()
s.op.GeoM.Reset()
s.op.GeoM.Scale(2, 2)
s.op.GeoM.Translate(float64(world.World.ScreenW-width)/2, float64(world.World.ScreenH-height)/2)
ebitenutil.DebugPrint(s.debugImg, world.World.MessageText)
screen.DrawImage(s.debugImg, s.op)
return nil
}

View File

@ -1,11 +1,9 @@
package world
import (
"bytes"
"image"
"log"
"math"
"net/http"
"path/filepath"
"time"
@ -36,12 +34,18 @@ type GameWorld struct {
SpawnX, SpawnY float64
ObjectGroups []*tiled.ObjectGroup
StartedAt time.Time
GameStarted bool
GameOver bool
Player gohan.Entity
ScreenW, ScreenH int
NoClip bool
Debug int
GameStartedTicks int
FadingIn bool
FadeInTicks int
OffsetX, OffsetY float64
DuckStart float64
@ -52,15 +56,24 @@ type GameWorld struct {
CanDash bool
CanLevitate bool
// Items
Keys int
Jumps int
Dashes int
Levitating bool
Rewinding bool
MessageVisible bool
MessageTitle string
MessageText string
TriggerRects []image.Rectangle
TriggerEntities []gohan.Entity
TriggerNames []string
DisableEsc bool // TODO
DisableEsc bool
}
func TileToGameCoords(x, y int) (float64, float64) {
@ -70,16 +83,11 @@ func TileToGameCoords(x, y int) (float64, float64) {
func LoadMap(filePath string) {
loader := tiled.Loader{
FileSystem: http.FS(asset.FS),
}
b, err := asset.FS.ReadFile(filePath)
if err != nil {
panic(err)
FileSystem: asset.FS,
}
// Parse .tmx file.
m, err := loader.LoadFromReader("/", bytes.NewReader(b))
m, err := loader.LoadFromFile(filePath)
if err != nil {
log.Fatalf("error parsing world: %+v", err)
}
@ -177,9 +185,6 @@ func LoadMap(filePath string) {
continue // No tile at this position.
}
// TODO use Tileset.Animation
// use current time in millis (cached) % total animation time
tileImg := tileCache[t.Tileset.FirstGID+t.ID]
if tileImg == nil {
continue