feat: add line clear animation
This commit is contained in:
parent
fdab0a1992
commit
db9ec53d35
4 changed files with 112 additions and 20 deletions
|
|
@ -64,7 +64,7 @@ go build tetris.go
|
||||||
|
|
||||||
## Roadmap
|
## Roadmap
|
||||||
|
|
||||||
- [ ] Line clear animations
|
- [x] Line clear animations
|
||||||
- [ ] Hard drop and ghost piece
|
- [ ] Hard drop and ghost piece
|
||||||
- [ ] Level/speed progression and pause
|
- [ ] Level/speed progression and pause
|
||||||
- [ ] High score persistence
|
- [ ] High score persistence
|
||||||
|
|
|
||||||
23
game/grid.go
23
game/grid.go
|
|
@ -1,6 +1,8 @@
|
||||||
package game
|
package game
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"slices"
|
||||||
|
|
||||||
"tetris/engine/graphics"
|
"tetris/engine/graphics"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -44,6 +46,16 @@ func (g *Grid) ClearFullRows() byte {
|
||||||
return completed
|
return completed
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (g *Grid) FullRows() []byte {
|
||||||
|
rows := []byte{}
|
||||||
|
for y := byte(0); y < byte(g.Height()); y++ {
|
||||||
|
if g.IsRowFull(y) {
|
||||||
|
rows = append(rows, y)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return rows
|
||||||
|
}
|
||||||
|
|
||||||
func (g *Grid) IsRowFull(y byte) bool {
|
func (g *Grid) IsRowFull(y byte) bool {
|
||||||
for x := range byte(g.Width()) {
|
for x := range byte(g.Width()) {
|
||||||
if g.At(x, y) == BLOCK_EMPTY {
|
if g.At(x, y) == BLOCK_EMPTY {
|
||||||
|
|
@ -53,6 +65,17 @@ func (g *Grid) IsRowFull(y byte) bool {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (g *Grid) MoveRowsDown(rows ...byte) {
|
||||||
|
completed := byte(0)
|
||||||
|
for y := int(g.Height() - 1); y >= 0; y-- {
|
||||||
|
if slices.Contains(rows, byte(y)) {
|
||||||
|
completed++
|
||||||
|
} else if completed > 0 {
|
||||||
|
g.MoveRowDown(byte(y), completed)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func (g *Grid) MoveRowDown(y, num_rows byte) {
|
func (g *Grid) MoveRowDown(y, num_rows byte) {
|
||||||
w := uint16(g.Width())
|
w := uint16(g.Width())
|
||||||
src := uint16(y) * w
|
src := uint16(y) * w
|
||||||
|
|
|
||||||
57
game/line_clear_animation.go
Normal file
57
game/line_clear_animation.go
Normal file
|
|
@ -0,0 +1,57 @@
|
||||||
|
package game
|
||||||
|
|
||||||
|
// LineClearAnimation clears completed lines cell-by-cell to create
|
||||||
|
// a visual wipe effect from the center outward.
|
||||||
|
//
|
||||||
|
// Once the lines are cleared, the remaining lines are moved down.
|
||||||
|
type LineClearAnimation struct {
|
||||||
|
// lines to clear.
|
||||||
|
lines []byte
|
||||||
|
// next index to clear to the left
|
||||||
|
leftIndex int8
|
||||||
|
// next index to clear to the right
|
||||||
|
rightIndex int8
|
||||||
|
}
|
||||||
|
|
||||||
|
func (lca *LineClearAnimation) Reset() {
|
||||||
|
lca.lines = []byte{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (lca LineClearAnimation) NumLines() int {
|
||||||
|
return len(lca.lines)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (lca *LineClearAnimation) SetLines(rows []byte) {
|
||||||
|
lca.lines = rows
|
||||||
|
|
||||||
|
lca.leftIndex = (GRID_WIDTH - 1) / 2
|
||||||
|
if (GRID_WIDTH-1)%2 != 0 {
|
||||||
|
lca.rightIndex = lca.leftIndex + 1
|
||||||
|
} else {
|
||||||
|
lca.rightIndex = lca.leftIndex
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (lca *LineClearAnimation) Update(grid *Grid) {
|
||||||
|
if lca.Completed() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if lca.leftIndex < 0 || lca.rightIndex > GRID_WIDTH {
|
||||||
|
grid.MoveRowsDown(lca.lines...)
|
||||||
|
lca.Reset()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, lineIndex := range lca.lines {
|
||||||
|
grid.Set(byte(lca.leftIndex), lineIndex, BLOCK_EMPTY)
|
||||||
|
grid.Set(byte(lca.rightIndex), lineIndex, BLOCK_EMPTY)
|
||||||
|
}
|
||||||
|
|
||||||
|
lca.leftIndex--
|
||||||
|
lca.rightIndex++
|
||||||
|
}
|
||||||
|
|
||||||
|
func (lca LineClearAnimation) Completed() bool {
|
||||||
|
return lca.NumLines() == 0
|
||||||
|
}
|
||||||
|
|
@ -16,21 +16,24 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
type GamePlay struct {
|
type GamePlay struct {
|
||||||
shape game.Shape
|
shape game.Shape
|
||||||
shape_pos core.Vec2i8
|
shape_pos core.Vec2i8
|
||||||
score game.Score
|
score game.Score
|
||||||
dropTimer core.IntervalTimer
|
dropTimer core.IntervalTimer
|
||||||
moveTimer core.IntervalTimer
|
moveTimer core.IntervalTimer
|
||||||
grid game.Grid
|
lineClearTimer core.IntervalTimer
|
||||||
nextShape game.Shape
|
grid game.Grid
|
||||||
shapeQueue *game.ShapeQueue
|
nextShape game.Shape
|
||||||
r draw.Renderer
|
shapeQueue *game.ShapeQueue
|
||||||
|
r draw.Renderer
|
||||||
|
lineClearAnimation game.LineClearAnimation
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewGamePlay() *GamePlay {
|
func NewGamePlay() *GamePlay {
|
||||||
return &GamePlay{
|
return &GamePlay{
|
||||||
dropTimer: core.NewIntervalTimer(0.3),
|
dropTimer: core.NewIntervalTimer(0.3),
|
||||||
moveTimer: core.NewIntervalTimer(0.1),
|
moveTimer: core.NewIntervalTimer(0.1),
|
||||||
|
lineClearTimer: core.NewIntervalTimer(0.05),
|
||||||
r: draw.Renderer{
|
r: draw.Renderer{
|
||||||
Theme: &draw.Theme{
|
Theme: &draw.Theme{
|
||||||
FrameBG: color.RGBA{R: 30, G: 30, B: 46, A: 255},
|
FrameBG: color.RGBA{R: 30, G: 30, B: 46, A: 255},
|
||||||
|
|
@ -50,6 +53,7 @@ func (gp *GamePlay) Enter() {
|
||||||
gp.shapeQueue = game.NewShapeQueue()
|
gp.shapeQueue = game.NewShapeQueue()
|
||||||
gp.nextShape = gp.shapeQueue.Next()
|
gp.nextShape = gp.shapeQueue.Next()
|
||||||
gp.SpawnShape()
|
gp.SpawnShape()
|
||||||
|
gp.lineClearAnimation.Reset()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (GamePlay) Exit() {
|
func (GamePlay) Exit() {
|
||||||
|
|
@ -81,6 +85,13 @@ func (gp *GamePlay) Update(fsm state.Transitioner, delta float32) {
|
||||||
gp.dropTimer.SetInterval(0.3)
|
gp.dropTimer.SetInterval(0.3)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if !gp.lineClearAnimation.Completed() {
|
||||||
|
if gp.lineClearTimer.UpdateReset(delta) {
|
||||||
|
gp.lineClearAnimation.Update(&gp.grid)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
if rl.IsKeyPressed(rl.KeyUp) {
|
if rl.IsKeyPressed(rl.KeyUp) {
|
||||||
rotated := gp.shape.RotateCW()
|
rotated := gp.shape.RotateCW()
|
||||||
if !game.CheckShapeCollision(gp.shape_pos, &rotated, &gp.grid) {
|
if !game.CheckShapeCollision(gp.shape_pos, &rotated, &gp.grid) {
|
||||||
|
|
@ -107,15 +118,16 @@ func (gp *GamePlay) Update(fsm state.Transitioner, delta float32) {
|
||||||
// Update position if it does not collide
|
// Update position if it does not collide
|
||||||
if game.CheckShapeCollision(new_pos, &gp.shape, &gp.grid) {
|
if game.CheckShapeCollision(new_pos, &gp.shape, &gp.grid) {
|
||||||
gp.LockShape()
|
gp.LockShape()
|
||||||
num_rows := gp.grid.ClearFullRows()
|
|
||||||
if num_rows > 0 {
|
|
||||||
audio.Play(assets.SFX_ROW_CLEARED)
|
|
||||||
gp.score.Lines(num_rows)
|
|
||||||
}
|
|
||||||
gp.SpawnShape()
|
gp.SpawnShape()
|
||||||
|
lines := gp.grid.FullRows()
|
||||||
if game.CheckShapeCollision(gp.shape_pos, &gp.shape, &gp.grid) {
|
if len(lines) > 0 {
|
||||||
fsm.Switch("gameover")
|
gp.lineClearAnimation.SetLines(lines)
|
||||||
|
gp.score.Lines(byte(len(lines)))
|
||||||
|
audio.Play(assets.SFX_ROW_CLEARED)
|
||||||
|
} else {
|
||||||
|
if game.CheckShapeCollision(gp.shape_pos, &gp.shape, &gp.grid) {
|
||||||
|
fsm.Switch("gameover")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
gp.shape_pos = new_pos
|
gp.shape_pos = new_pos
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue