forked from george/snake-compose
Змейка непрерывно поворачивает в сторону смещения пальца от точки касания, пока ведёшь по экрану (а не один поворот на отпускание свайпа). - обработка в onDrag (а не onDragEnd): turn() вызывается на каждое движение - направление по доминирующей оси смещения от origin; мёртвая зона 28dp - общий turn() с запретом разворота на 180°; быстрый флик тоже работает - подсказка на старте обновлена Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
100 lines
7.6 KiB
Markdown
100 lines
7.6 KiB
Markdown
# 🐍 Змейка на 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 (рекомендуется)
|
||
1. Открыть папку проекта в Android Studio (**File → Open**).
|
||
2. Дождаться Gradle Sync.
|
||
3. Подключить телефон по USB (с включённой *отладкой по USB*) или запустить эмулятор.
|
||
4. Нажать ▶ **Run**.
|
||
|
||
### Вариант 2: командная строка
|
||
```bash
|
||
# нужен 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`](app/src/main/java/com/example/snake/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:
|
||
```bash
|
||
git diff snake-stage-6 snake-stage-7
|
||
```
|
||
|
||
Идеи для следующих этапов: два игрока на одном экране, выбор размера поля, экран настроек.
|