1
0

Этап 5: анимация поедания еды

В момент съедания на месте еды расходится затухающее кольцо.

- Animatable popProgress (0->1 за один укус), запуск из step()
- отрисовка кольца в drawBoard() через drawCircle + Stroke,
  радиус растёт, прозрачность падает с прогрессом
- цвет кольца — цвет еды

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
george 2026-06-17 18:00:07 +03:00
parent 05d65eab14
commit da783d9f0d
2 changed files with 33 additions and 3 deletions

View File

@ -84,4 +84,8 @@ app/src/main/
(низкая задержка). Смотри настройку `SoundPool` и `soundPool.play(...)`
в `step()`; освобождение ресурса — в `DisposableEffect`.
Идеи для следующих этапов: анимации поедания, пауза, выбор размера поля.
- **`snake-stage-5`** — анимация поедания: в момент укуса на месте еды
расходится затухающее кольцо. Смотри `Animatable` (`popProgress` 0→1),
запуск анимации в `step()` и отрисовку кольца в `drawBoard()`.
Идеи для следующих этапов: пауза, выбор размера поля, экран настроек.

View File

@ -2,6 +2,8 @@ package com.example.snake
import android.media.AudioAttributes
import android.media.SoundPool
import androidx.compose.animation.core.Animatable
import androidx.compose.animation.core.tween
import androidx.compose.foundation.Canvas
import androidx.compose.foundation.background
import androidx.compose.foundation.gestures.detectDragGestures
@ -27,6 +29,7 @@ import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.Size
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.drawscope.DrawScope
import androidx.compose.ui.graphics.drawscope.Stroke
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
@ -107,6 +110,11 @@ fun SnakeGame() {
onDispose { soundPool.release() }
}
// Этап 5: анимация поедания — расходящееся затухающее кольцо на месте еды.
// popProgress идёт 0→1 за один укус; 1f означает «анимация завершена».
val popProgress = remember { Animatable(1f) }
var popCell by remember { mutableStateOf(food) }
fun reset() {
snake = startSnake
dir = Cell(0, -1)
@ -135,6 +143,12 @@ fun SnakeGame() {
score++
// звук поедания (left=1f, right=1f, prio=1, без зацикливания, обычная скорость)
soundPool.play(eatSoundId, 1f, 1f, 1, 0, 1f)
// запускаем анимацию вспышки на месте съеденной еды
popCell = nh
scope.launch {
popProgress.snapTo(0f)
popProgress.animateTo(1f, animationSpec = tween(durationMillis = 260))
}
if (score > best) {
best = score
// новый рекорд — сохраняем на диск (асинхронно, UI не блокируется)
@ -196,7 +210,7 @@ fun SnakeGame() {
) {
// Игровое поле
Canvas(modifier = Modifier.fillMaxSize()) {
drawBoard(snake, food)
drawBoard(snake, food, popCell, popProgress.value)
}
// Счёт сверху (+ индикатор скорости — видно ускорение)
@ -231,7 +245,7 @@ private fun Overlay(title: String, subtitle: String, modifier: Modifier = Modifi
}
/** Вся отрисовка поля внутри DrawScope. */
private fun DrawScope.drawBoard(snake: List<Cell>, food: Cell) {
private fun DrawScope.drawBoard(snake: List<Cell>, food: Cell, popCell: Cell, popProgress: Float) {
val cell = minOf(size.width / COLS, size.height / ROWS)
val offX = (size.width - cell * COLS) / 2f
val offY = (size.height - cell * ROWS) / 2f
@ -256,6 +270,18 @@ private fun DrawScope.drawBoard(snake: List<Cell>, food: Cell) {
snake.forEachIndexed { idx, c ->
drawCell(c, if (idx == 0) HEAD else BODY, cell, offX, offY)
}
// анимация поедания: расходящееся затухающее кольцо на месте съеденной еды
if (popProgress < 1f) {
val center = Offset(offX + popCell.x * cell + cell / 2f, offY + popCell.y * cell + cell / 2f)
val radius = cell * (0.4f + popProgress * 1.4f)
drawCircle(
color = FOOD.copy(alpha = 1f - popProgress),
radius = radius,
center = center,
style = Stroke(width = cell * 0.16f)
)
}
}
private fun DrawScope.drawCell(c: Cell, color: Color, cell: Float, offX: Float, offY: Float) {