1
0
Fork 0

feat: add line clear animation

This commit is contained in:
Henrik Hautakoski 2025-10-18 11:28:26 +02:00
parent a2cc5a4319
commit ec249318e9
4 changed files with 112 additions and 20 deletions

View file

@ -64,7 +64,7 @@ go build tetris.go
## Roadmap
- [ ] Line clear animations
- [x] Line clear animations
- [ ] Hard drop and ghost piece
- [ ] Level/speed progression and pause
- [ ] High score persistence

View file

@ -1,6 +1,8 @@
package game
import (
"slices"
"tetris/engine/graphics"
)
@ -44,6 +46,16 @@ func (g *Grid) ClearFullRows() byte {
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 {
for x := range byte(g.Width()) {
if g.At(x, y) == BLOCK_EMPTY {
@ -53,6 +65,17 @@ func (g *Grid) IsRowFull(y byte) bool {
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) {
w := uint16(g.Width())
src := uint16(y) * w

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

View file

@ -16,21 +16,24 @@ import (
)
type GamePlay struct {
shape game.Shape
shape_pos core.Vec2i8
score game.Score
dropTimer core.IntervalTimer
moveTimer core.IntervalTimer
grid game.Grid
nextShape game.Shape
shapeQueue *game.ShapeQueue
r draw.Renderer
shape game.Shape
shape_pos core.Vec2i8
score game.Score
dropTimer core.IntervalTimer
moveTimer core.IntervalTimer
lineClearTimer core.IntervalTimer
grid game.Grid
nextShape game.Shape
shapeQueue *game.ShapeQueue
r draw.Renderer
lineClearAnimation game.LineClearAnimation
}
func NewGamePlay() *GamePlay {
return &GamePlay{
dropTimer: core.NewIntervalTimer(0.3),
moveTimer: core.NewIntervalTimer(0.1),
dropTimer: core.NewIntervalTimer(0.3),
moveTimer: core.NewIntervalTimer(0.1),
lineClearTimer: core.NewIntervalTimer(0.05),
r: draw.Renderer{
Theme: &draw.Theme{
FrameBG: color.RGBA{R: 30, G: 30, B: 46, A: 255},
@ -50,6 +53,7 @@ func (gp *GamePlay) Enter() {
gp.shapeQueue = game.NewShapeQueue()
gp.nextShape = gp.shapeQueue.Next()
gp.SpawnShape()
gp.lineClearAnimation.Reset()
}
func (GamePlay) Exit() {
@ -81,6 +85,13 @@ func (gp *GamePlay) Update(fsm state.Transitioner, delta float32) {
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) {
rotated := gp.shape.RotateCW()
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
if game.CheckShapeCollision(new_pos, &gp.shape, &gp.grid) {
gp.LockShape()
num_rows := gp.grid.ClearFullRows()
if num_rows > 0 {
audio.Play(assets.SFX_ROW_CLEARED)
gp.score.Lines(num_rows)
}
gp.SpawnShape()
if game.CheckShapeCollision(gp.shape_pos, &gp.shape, &gp.grid) {
fsm.Switch("gameover")
lines := gp.grid.FullRows()
if len(lines) > 0 {
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 {
gp.shape_pos = new_pos