From 8dc1d433eaff1da7ede396c00fad6a59ee927e9b Mon Sep 17 00:00:00 2001 From: cmclark00 Date: Wed, 26 Mar 2025 19:39:14 -0400 Subject: [PATCH] Fix line clearing performance by processing on background thread --- .../main/java/com/mintris/game/GameView.kt | 133 ++++-------------- .../main/java/com/mintris/model/GameBoard.kt | 53 ++++--- 2 files changed, 57 insertions(+), 129 deletions(-) diff --git a/app/src/main/java/com/mintris/game/GameView.kt b/app/src/main/java/com/mintris/game/GameView.kt index b9f06e7..e82baaf 100644 --- a/app/src/main/java/com/mintris/game/GameView.kt +++ b/app/src/main/java/com/mintris/game/GameView.kt @@ -90,16 +90,8 @@ class GameView @JvmOverloads constructor( maskFilter = BlurMaskFilter(8f, BlurMaskFilter.Blur.OUTER) } - private val lineClearPaint = Paint().apply { - color = Color.WHITE - alpha = 255 - isAntiAlias = true - } - - // Animation - private var lineClearAnimator: ValueAnimator? = null - private var lineClearProgress = 0f - private val lineClearDuration = 100L // milliseconds + // Pre-allocate paint objects to avoid GC + private val tmpPaint = Paint() // Calculate block size based on view dimensions and board size private var blockSize = 0f @@ -147,7 +139,7 @@ class GameView @JvmOverloads constructor( gameBoard.onPieceMove = { onPieceMove?.invoke() } gameBoard.onPieceLock = { onPieceLock?.invoke() } - // Enable hardware acceleration + // Force hardware acceleration - This is critical for performance setLayerType(LAYER_TYPE_HARDWARE, null) // Set better frame rate using modern APIs @@ -185,7 +177,6 @@ class GameView @JvmOverloads constructor( fun pause() { isPaused = true handler.removeCallbacks(gameLoopRunnable) - lineClearAnimator?.cancel() invalidate() } @@ -198,7 +189,6 @@ class GameView @JvmOverloads constructor( gameBoard.reset() gameBoard.startGame() // Add this line to ensure a new piece is spawned handler.removeCallbacks(gameLoopRunnable) - lineClearAnimator?.cancel() invalidate() } @@ -216,55 +206,31 @@ class GameView @JvmOverloads constructor( // Move the current tetromino down automatically 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()) { // Trigger line clear callback for vibration onLineClear?.invoke(gameBoard.linesToClear.size) - // Start line clearing animation immediately - startLineClearAnimation() - } - - // Update UI with current game state - onGameStateChanged?.invoke(gameBoard.score, gameBoard.level, gameBoard.lines) - } - - /** - * 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() + // Trigger line clearing on a background thread to prevent UI freezes + Thread { + // Process the line clearing off the UI thread + gameBoard.clearLinesFromGrid() + + // Then update UI on the main thread + handler.post { invalidate() } - }) - - start() + }.start() + } else { + // 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) { super.onSizeChanged(w, h, oldw, oldh) - // Enable hardware acceleration + // Force hardware acceleration - Critical for performance setLayerType(LAYER_TYPE_HARDWARE, null) // Update gesture exclusion rect for edge-to-edge rendering @@ -295,6 +261,18 @@ class GameView @JvmOverloads constructor( } 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) // Draw background (already black from theme) @@ -305,14 +283,8 @@ class GameView @JvmOverloads constructor( // Draw grid (very subtle) drawGrid(canvas) - // Check if line clear animation is in progress - if (gameBoard.isLineClearAnimationInProgress) { - // Draw the line clearing animation - drawLineClearAnimation(canvas) - } else { - // Draw locked pieces - drawLockedBlocks(canvas) - } + // Draw locked pieces + drawLockedBlocks(canvas) if (!gameBoard.isGameOver && isRunning) { // 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 */ @@ -675,4 +604,4 @@ class GameView @JvmOverloads constructor( update() invalidate() } -} \ No newline at end of file +} diff --git a/app/src/main/java/com/mintris/model/GameBoard.kt b/app/src/main/java/com/mintris/model/GameBoard.kt index 15c3033..5302ba2 100644 --- a/app/src/main/java/com/mintris/model/GameBoard.kt +++ b/app/src/main/java/com/mintris/model/GameBoard.kt @@ -281,20 +281,15 @@ class GameBoard( * Find lines that should be cleared and store them */ private fun findLinesToClear() { - // Clear existing lines before finding new ones + // Clear existing lines linesToClear.clear() + // Quick scan for completed lines for (y in 0 until height) { - // Check if line is full - var lineFull = true - for (x in 0 until width) { - if (!grid[y][x]) { - lineFull = false - break - } - } + val row = grid[y] - if (lineFull) { + // Check if line is full - use all() for better performance + if (row.all { it }) { linesToClear.add(y) } } @@ -322,30 +317,34 @@ class GameBoard( if (linesToClear.isNotEmpty()) { val clearedLines = linesToClear.size - // Get the highest line that needs to be cleared - val highestLine = linesToClear.minOrNull() ?: return + // Much faster approach: shift rows in-place without creating temporary arrays + // 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 - val newGrid = Array(height) { BooleanArray(width) { false } } - - // Copy non-cleared lines to their new positions - var newY = height - 1 + // Calculate how much to shift each row for (y in height - 1 downTo 0) { - if (y !in linesToClear) { - for (x in 0 until width) { - newGrid[newY][x] = grid[y][x] - } - newY-- + if (y in linesToClear) { + shiftAmount++ + } else if (shiftAmount > 0) { + rowMoves[y] = y + shiftAmount } } - // Update the grid with the new state - for (y in 0 until height) { - for (x in 0 until width) { - grid[y][x] = newGrid[y][x] + // Apply row shifts in a single pass, bottom to top + for (y in height - 1 downTo 0) { + val targetY = rowMoves[y] + 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) val baseScore = when (clearedLines) { 1 -> 40 @@ -427,7 +426,7 @@ class GameBoard( // Update game speed based on level (NES formula) dropInterval = (1000 * Math.pow(0.8, (level - 1).toDouble())).toLong() - // Reset animation state and clear lines + // Reset animation state immediately isLineClearAnimationInProgress = false linesToClear.clear()