Add rewind ability
Players may rewind to their previous locations, even after they have died.
This commit is contained in:
parent
a1abf7eab5
commit
248036dbd9
Binary file not shown.
Before Width: | Height: | Size: 10 KiB After Width: | Height: | Size: 10 KiB |
1672
asset/map/m1.tmx
1672
asset/map/m1.tmx
File diff suppressed because it is too large
Load Diff
11
game/game.go
11
game/game.go
|
@ -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
8
go.mod
|
@ -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
16
go.sum
|
@ -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=
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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]
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue