Fix line clearing performance by processing on background thread

This commit is contained in:
cmclark00 2025-03-26 19:39:14 -04:00
parent 9fbffc00d0
commit 8dc1d433ea
2 changed files with 57 additions and 129 deletions

View file

@ -90,16 +90,8 @@ class GameView @JvmOverloads constructor(
maskFilter = BlurMaskFilter(8f, BlurMaskFilter.Blur.OUTER) maskFilter = BlurMaskFilter(8f, BlurMaskFilter.Blur.OUTER)
} }
private val lineClearPaint = Paint().apply { // Pre-allocate paint objects to avoid GC
color = Color.WHITE private val tmpPaint = Paint()
alpha = 255
isAntiAlias = true
}
// Animation
private var lineClearAnimator: ValueAnimator? = null
private var lineClearProgress = 0f
private val lineClearDuration = 100L // milliseconds
// Calculate block size based on view dimensions and board size // Calculate block size based on view dimensions and board size
private var blockSize = 0f private var blockSize = 0f
@ -147,7 +139,7 @@ class GameView @JvmOverloads constructor(
gameBoard.onPieceMove = { onPieceMove?.invoke() } gameBoard.onPieceMove = { onPieceMove?.invoke() }
gameBoard.onPieceLock = { onPieceLock?.invoke() } gameBoard.onPieceLock = { onPieceLock?.invoke() }
// Enable hardware acceleration // Force hardware acceleration - This is critical for performance
setLayerType(LAYER_TYPE_HARDWARE, null) setLayerType(LAYER_TYPE_HARDWARE, null)
// Set better frame rate using modern APIs // Set better frame rate using modern APIs
@ -185,7 +177,6 @@ class GameView @JvmOverloads constructor(
fun pause() { fun pause() {
isPaused = true isPaused = true
handler.removeCallbacks(gameLoopRunnable) handler.removeCallbacks(gameLoopRunnable)
lineClearAnimator?.cancel()
invalidate() invalidate()
} }
@ -198,7 +189,6 @@ class GameView @JvmOverloads constructor(
gameBoard.reset() gameBoard.reset()
gameBoard.startGame() // Add this line to ensure a new piece is spawned gameBoard.startGame() // Add this line to ensure a new piece is spawned
handler.removeCallbacks(gameLoopRunnable) handler.removeCallbacks(gameLoopRunnable)
lineClearAnimator?.cancel()
invalidate() invalidate()
} }
@ -216,55 +206,31 @@ class GameView @JvmOverloads constructor(
// Move the current tetromino down automatically // Move the current tetromino down automatically
gameBoard.moveDown() gameBoard.moveDown()
// Check if lines need to be cleared and start animation if needed // Check if lines need to be cleared
if (gameBoard.linesToClear.isNotEmpty()) { if (gameBoard.linesToClear.isNotEmpty()) {
// Trigger line clear callback for vibration // Trigger line clear callback for vibration
onLineClear?.invoke(gameBoard.linesToClear.size) onLineClear?.invoke(gameBoard.linesToClear.size)
// Start line clearing animation immediately // Trigger line clearing on a background thread to prevent UI freezes
startLineClearAnimation() Thread {
} // Process the line clearing off the UI thread
gameBoard.clearLinesFromGrid()
// Update UI with current game state
onGameStateChanged?.invoke(gameBoard.score, gameBoard.level, gameBoard.lines) // Then update UI on the main thread
} handler.post {
/**
* Start the line clearing animation
*/
private fun startLineClearAnimation() {
// Cancel any existing animation
lineClearAnimator?.cancel()
// Reset progress
lineClearProgress = 0f
// Create and start new animation immediately
lineClearAnimator = ValueAnimator.ofFloat(0f, 1f).apply {
duration = lineClearDuration
interpolator = LinearInterpolator()
addUpdateListener { animator ->
lineClearProgress = animator.animatedValue as Float
invalidate()
}
addListener(object : android.animation.AnimatorListenerAdapter() {
override fun onAnimationEnd(animation: android.animation.Animator) {
// When animation completes, actually clear the lines
gameBoard.clearLinesFromGrid()
invalidate() invalidate()
} }
}) }.start()
} else {
start() // Update UI with current game state
onGameStateChanged?.invoke(gameBoard.score, gameBoard.level, gameBoard.lines)
} }
} }
override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) { override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
super.onSizeChanged(w, h, oldw, oldh) super.onSizeChanged(w, h, oldw, oldh)
// Enable hardware acceleration // Force hardware acceleration - Critical for performance
setLayerType(LAYER_TYPE_HARDWARE, null) setLayerType(LAYER_TYPE_HARDWARE, null)
// Update gesture exclusion rect for edge-to-edge rendering // Update gesture exclusion rect for edge-to-edge rendering
@ -295,6 +261,18 @@ class GameView @JvmOverloads constructor(
} }
override fun onDraw(canvas: Canvas) { override fun onDraw(canvas: Canvas) {
// Skip drawing if paused or game over - faster return
if (isPaused || gameBoard.isGameOver) {
super.onDraw(canvas)
return
}
// Set hardware layer type during draw for better performance
val wasHardwareAccelerated = isHardwareAccelerated
if (!wasHardwareAccelerated) {
setLayerType(LAYER_TYPE_HARDWARE, null)
}
super.onDraw(canvas) super.onDraw(canvas)
// Draw background (already black from theme) // Draw background (already black from theme)
@ -305,14 +283,8 @@ class GameView @JvmOverloads constructor(
// Draw grid (very subtle) // Draw grid (very subtle)
drawGrid(canvas) drawGrid(canvas)
// Check if line clear animation is in progress // Draw locked pieces
if (gameBoard.isLineClearAnimationInProgress) { drawLockedBlocks(canvas)
// Draw the line clearing animation
drawLineClearAnimation(canvas)
} else {
// Draw locked pieces
drawLockedBlocks(canvas)
}
if (!gameBoard.isGameOver && isRunning) { if (!gameBoard.isGameOver && isRunning) {
// Draw ghost piece (landing preview) // Draw ghost piece (landing preview)
@ -323,49 +295,6 @@ class GameView @JvmOverloads constructor(
} }
} }
/**
* Draw the line clearing animation
*/
private fun drawLineClearAnimation(canvas: Canvas) {
// Draw non-clearing blocks
for (y in 0 until gameBoard.height) {
if (gameBoard.linesToClear.contains(y)) continue
for (x in 0 until gameBoard.width) {
if (gameBoard.isOccupied(x, y)) {
drawBlock(canvas, x, y, false)
}
}
}
// Draw all clearing lines with a single animation effect
for (lineY in gameBoard.linesToClear) {
for (x in 0 until gameBoard.width) {
// Animation effects for all lines simultaneously
val brightness = 255 - (lineClearProgress * 150).toInt() // Reduced from 200 for smoother fade
val scale = 1.0f - lineClearProgress * 0.3f // Reduced from 0.5f for subtler scaling
// Set the paint for the clear animation
lineClearPaint.color = Color.WHITE
lineClearPaint.alpha = brightness.coerceIn(0, 255)
// Calculate block position with scaling
val left = boardLeft + x * blockSize + (blockSize * (1 - scale) / 2)
val top = boardTop + lineY * blockSize + (blockSize * (1 - scale) / 2)
val right = left + blockSize * scale
val bottom = top + blockSize * scale
// Draw the shrinking, fading block
val rect = RectF(left, top, right, bottom)
canvas.drawRect(rect, lineClearPaint)
// Add a more subtle glow effect
lineClearPaint.setShadowLayer(8f * (1f - lineClearProgress), 0f, 0f, Color.WHITE)
canvas.drawRect(rect, lineClearPaint)
}
}
}
/** /**
* Draw glowing border around the playable area * Draw glowing border around the playable area
*/ */
@ -675,4 +604,4 @@ class GameView @JvmOverloads constructor(
update() update()
invalidate() invalidate()
} }
} }

View file

@ -281,20 +281,15 @@ class GameBoard(
* Find lines that should be cleared and store them * Find lines that should be cleared and store them
*/ */
private fun findLinesToClear() { private fun findLinesToClear() {
// Clear existing lines before finding new ones // Clear existing lines
linesToClear.clear() linesToClear.clear()
// Quick scan for completed lines
for (y in 0 until height) { for (y in 0 until height) {
// Check if line is full val row = grid[y]
var lineFull = true
for (x in 0 until width) {
if (!grid[y][x]) {
lineFull = false
break
}
}
if (lineFull) { // Check if line is full - use all() for better performance
if (row.all { it }) {
linesToClear.add(y) linesToClear.add(y)
} }
} }
@ -322,30 +317,34 @@ class GameBoard(
if (linesToClear.isNotEmpty()) { if (linesToClear.isNotEmpty()) {
val clearedLines = linesToClear.size val clearedLines = linesToClear.size
// Get the highest line that needs to be cleared // Much faster approach: shift rows in-place without creating temporary arrays
val highestLine = linesToClear.minOrNull() ?: return // Pre-compute all row movements to minimize array operations
val rowMoves = IntArray(height) { -1 } // Where each row should move to
var shiftAmount = 0
// Create a temporary grid to store the new state // Calculate how much to shift each row
val newGrid = Array(height) { BooleanArray(width) { false } }
// Copy non-cleared lines to their new positions
var newY = height - 1
for (y in height - 1 downTo 0) { for (y in height - 1 downTo 0) {
if (y !in linesToClear) { if (y in linesToClear) {
for (x in 0 until width) { shiftAmount++
newGrid[newY][x] = grid[y][x] } else if (shiftAmount > 0) {
} rowMoves[y] = y + shiftAmount
newY--
} }
} }
// Update the grid with the new state // Apply row shifts in a single pass, bottom to top
for (y in 0 until height) { for (y in height - 1 downTo 0) {
for (x in 0 until width) { val targetY = rowMoves[y]
grid[y][x] = newGrid[y][x] if (targetY != -1 && targetY < height) {
// Shift this row down
System.arraycopy(grid[y], 0, grid[targetY], 0, width)
} }
} }
// Clear top rows (faster than creating a new array)
for (y in 0 until clearedLines) {
java.util.Arrays.fill(grid[y], false)
}
// Calculate base score (NES scoring system) // Calculate base score (NES scoring system)
val baseScore = when (clearedLines) { val baseScore = when (clearedLines) {
1 -> 40 1 -> 40
@ -427,7 +426,7 @@ class GameBoard(
// Update game speed based on level (NES formula) // Update game speed based on level (NES formula)
dropInterval = (1000 * Math.pow(0.8, (level - 1).toDouble())).toLong() dropInterval = (1000 * Math.pow(0.8, (level - 1).toDouble())).toLong()
// Reset animation state and clear lines // Reset animation state immediately
isLineClearAnimationInProgress = false isLineClearAnimationInProgress = false
linesToClear.clear() linesToClear.clear()