Змейка непрерывно поворачивает в сторону смещения пальца от точки касания, пока ведёшь по экрану (а не один поворот на отпускание свайпа). - обработка в onDrag (а не onDragEnd): turn() вызывается на каждое движение - направление по доминирующей оси смещения от origin; мёртвая зона 28dp - общий turn() с запретом разворота на 180°; быстрый флик тоже работает - подсказка на старте обновлена Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
🐍 Змейка на Jetpack Compose
Учебный пример простого Android-приложения для студентов: классическая игра «Змейка» на одном экране. Цель примера — показать, как устроено реальное Android-приложение на современном стеке (Kotlin + Jetpack Compose) без лишней сложности.
Что внутри (итоговая версия на main)
- Игровое поле-сетка 20×30, управление пальцем как джойстиком (ведёшь — змейка поворачивает)
- Змейка растёт и ускоряется с ростом счёта; ведётся счёт, уровень и рекорд (сохраняется между запусками)
- Бонус-еда с таймером даёт больше очков; препятствия добавляются с каждым уровнем
- Границы поля телепортируют на противоположную сторону (тор); смерть — от самой себя и от препятствий
- Звук и анимация-вспышка при поедании, пауза по тапу
- Вся отрисовка — через Compose
Canvas, игровой цикл — на корутине
Каждая фича добавлялась отдельным этапом-веткой — см. раздел Этапы ниже: там видно, что именно меняется на каждом шаге.
Стек
| Что | Версия |
|---|---|
| Язык | Kotlin 2.0.20 |
| UI | Jetpack Compose (BOM 2024.10.01, Material 3) |
| Android Gradle Plugin | 8.13.2 |
| Gradle | 8.13 |
| compileSdk / minSdk | 36 / 24 |
Как запустить
Вариант 1: Android Studio (рекомендуется)
- Открыть папку проекта в Android Studio (File → Open).
- Дождаться Gradle Sync.
- Подключить телефон по USB (с включённой отладкой по USB) или запустить эмулятор.
- Нажать ▶ Run.
Вариант 2: командная строка
# нужен JDK 17+ и установленный Android SDK
./gradlew installDebug # соберёт и поставит приложение на подключённое устройство
Файл
local.propertiesс путём к SDK создаётся автоматически Android Studio и не хранится в репозитории — у каждого свой путь.
Структура проекта
app/src/main/
├── java/com/example/snake/
│ ├── MainActivity.kt # точка входа: ComponentActivity + setContent { SnakeGame() }
│ └── SnakeGame.kt # вся игра: состояние, игровой цикл, отрисовка, управление
├── res/values/themes.xml # тема приложения
└── AndroidManifest.xml
👉 Главный файл для изучения — SnakeGame.kt.
Там по шагам видно идеи Compose:
- состояние через
remember { mutableStateOf(...) }— UI сам перерисовывается при его изменении; - игровой цикл через
LaunchedEffect { while(true) { delay(); step() } }; - ввод через
Modifier.pointerInput { detectTapGestures / detectDragGestures }; - рисование через
Canvas { drawRoundRect / drawLine }.
Управление
| Действие | Жест |
|---|---|
| Начать игру | касание/движение или тап |
| Повернуть | веди пальцем как джойстиком (быстрый флик тоже работает) |
| Пауза / продолжить | тап по полю во время игры |
| Рестарт после проигрыша | тап по экрану |
Этапы (ветки)
Проект построен как серия этапов: каждый — отдельная ветка, добавляющая
одну фичу поверх предыдущего. Между соседними ветками — маленький diff,
поэтому легко увидеть, что именно меняется и какой Android/Kotlin-приём за это
отвечает. main = последний этап (вся игра целиком).
| Этап (ветка) | Что меняется | Ключевой приём | Где в коде |
|---|---|---|---|
snake-stage-1 |
Базовая игра: поле 20×30, рост змейки, счёт, рекорд за сессию, свайпы, Game Over об стену/себя | Compose Canvas, игровой цикл на корутине, состояние в remember |
весь SnakeGame.kt |
snake-stage-2 |
Змейка ускоряется с ростом счёта (150→60 мс), индикатор «Скорость» | задержка шага пересчитывается из состояния | stepDelay(), speedLevel() |
snake-stage-3 |
Рекорд сохраняется между запусками | Jetpack DataStore (Flow на чтение, suspend на запись) |
ScoreStore.kt, загрузка в LaunchedEffect |
snake-stage-4 |
Звук при поедании еды | SoundPool + res/raw/eat.wav, очистка в DisposableEffect |
soundPool.play(...) в step() |
snake-stage-5 |
Анимация-вспышка (кольцо) в момент укуса | Animatable (0→1) + drawCircle |
popProgress, drawBoard() |
snake-stage-6 |
Пауза по тапу во время игры | флаг paused в условии игрового цикла |
detectTapGestures, оверлей «ПАУЗА» |
snake-stage-7 |
Бонус-еда с таймером (1 из 4), +3 очка, золотая, исчезает по времени | таймер в «шагах» (учитывает паузу) + drawArc |
bonusFood/bonusStepsLeft, step()/drawBoard() |
snake-stage-8 |
Препятствия и уровни: каждые 5 очков +порция стен; врезался — Game Over | генерация на Set<Cell>, коллизии, прогрессия |
obstacles/level, addObstacles() |
snake-stage-9 |
Сквозь границы (тор): уходишь за край — появляешься с другой стороны | модульная арифметика координат % COLS/ROWS |
step() |
snake-stage-10 |
Джойстик: ведёшь пальцем — змейка непрерывно поворачивает в сторону смещения от точки касания | detectDragGestures с обработкой в onDrag (а не на отпускании) + мёртвая зона |
turn(), блок pointerInput |
Посмотреть, что именно поменялось на этапе N, удобно diff'ом между ветками, например для этапа 7:
git diff snake-stage-6 snake-stage-7
Идеи для следующих этапов: два игрока на одном экране, выбор размера поля, экран настроек.