1
0

Этап 10: джойстик-управление пальцем

Змейка непрерывно поворачивает в сторону смещения пальца от точки касания,
пока ведёшь по экрану (а не один поворот на отпускание свайпа).

- обработка в onDrag (а не onDragEnd): turn() вызывается на каждое движение
- направление по доминирующей оси смещения от origin; мёртвая зона 28dp
- общий turn() с запретом разворота на 180°; быстрый флик тоже работает
- подсказка на старте обновлена

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
george 2026-06-17 18:49:27 +03:00
parent 64b47a5536
commit f81a1a88a2
2 changed files with 27 additions and 21 deletions

View File

@ -7,7 +7,7 @@ Android-приложение на современном стеке (Kotlin + Je
## Что внутри (итоговая версия на `main`) ## Что внутри (итоговая версия на `main`)
- Игровое поле-сетка 20×30, управление **свайпами** - Игровое поле-сетка 20×30, управление пальцем как **джойстиком** (ведёшь — змейка поворачивает)
- Змейка растёт и **ускоряется** с ростом счёта; ведётся счёт, **уровень** и **рекорд** (сохраняется между запусками) - Змейка растёт и **ускоряется** с ростом счёта; ведётся счёт, **уровень** и **рекорд** (сохраняется между запусками)
- **Бонус-еда** с таймером даёт больше очков; **препятствия** добавляются с каждым уровнем - **Бонус-еда** с таймером даёт больше очков; **препятствия** добавляются с каждым уровнем
- Границы поля **телепортируют** на противоположную сторону (тор); смерть — от самой себя и от препятствий - Границы поля **телепортируют** на противоположную сторону (тор); смерть — от самой себя и от препятствий
@ -65,8 +65,8 @@ app/src/main/
| Действие | Жест | | Действие | Жест |
|----------|------| |----------|------|
| Начать игру | свайп или тап | | Начать игру | касание/движение или тап |
| Повернуть | свайп ↑ ↓ ← → | | Повернуть | **веди пальцем** как джойстиком (быстрый флик тоже работает) |
| Пауза / продолжить | тап по полю во время игры | | Пауза / продолжить | тап по полю во время игры |
| Рестарт после проигрыша | тап по экрану | | Рестарт после проигрыша | тап по экрану |
@ -88,6 +88,7 @@ app/src/main/
| **`snake-stage-7`** | **Бонус-еда** с таймером (1 из 4), +3 очка, золотая, исчезает по времени | таймер в «шагах» (учитывает паузу) + `drawArc` | `bonusFood`/`bonusStepsLeft`, `step()`/`drawBoard()` | | **`snake-stage-7`** | **Бонус-еда** с таймером (1 из 4), +3 очка, золотая, исчезает по времени | таймер в «шагах» (учитывает паузу) + `drawArc` | `bonusFood`/`bonusStepsLeft`, `step()`/`drawBoard()` |
| **`snake-stage-8`** | **Препятствия и уровни**: каждые 5 очков +порция стен; врезался — Game Over | генерация на `Set<Cell>`, коллизии, прогрессия | `obstacles`/`level`, `addObstacles()` | | **`snake-stage-8`** | **Препятствия и уровни**: каждые 5 очков +порция стен; врезался — Game Over | генерация на `Set<Cell>`, коллизии, прогрессия | `obstacles`/`level`, `addObstacles()` |
| **`snake-stage-9`** | **Сквозь границы (тор)**: уходишь за край — появляешься с другой стороны | модульная арифметика координат `% COLS/ROWS` | `step()` | | **`snake-stage-9`** | **Сквозь границы (тор)**: уходишь за край — появляешься с другой стороны | модульная арифметика координат `% COLS/ROWS` | `step()` |
| **`snake-stage-10`** | **Джойстик**: ведёшь пальцем — змейка непрерывно поворачивает в сторону смещения от точки касания | `detectDragGestures` с обработкой в `onDrag` (а не на отпускании) + мёртвая зона | `turn()`, блок `pointerInput` |
Посмотреть, что именно поменялось на этапе N, удобно diff'ом между ветками, Посмотреть, что именно поменялось на этапе N, удобно diff'ом между ветками,
например для этапа 7: например для этапа 7:

View File

@ -146,6 +146,15 @@ fun SnakeGame() {
obstacles = emptySet() obstacles = emptySet()
} }
// Сменить направление (для джойстика). Запрет разворота на 180°; первый ввод запускает игру.
fun turn(dx: Int, dy: Int) {
if (gameOver) return
if (!started) started = true
if (dx != 0 && dir.x == -dx) return
if (dy != 0 && dir.y == -dy) return
nextDir = Cell(dx, dy)
}
// Добавить порцию препятствий в свободные клетки (не у головы змейки). // Добавить порцию препятствий в свободные клетки (не у головы змейки).
fun addObstacles(count: Int) { fun addObstacles(count: Int) {
val head = snake.first() val head = snake.first()
@ -255,28 +264,24 @@ fun SnakeGame() {
} }
} }
} }
// Свайпы: смена направления (и старт первым свайпом) // Джойстик: ведёшь пальцем — змейка непрерывно поворачивает в сторону
// смещения пальца от точки касания (доминирующая ось). Мёртвая зона
// у центра гасит дрожание.
.pointerInput(Unit) { .pointerInput(Unit) {
var total = Offset.Zero val deadZone = 28.dp.toPx()
var origin = Offset.Zero
detectDragGestures( detectDragGestures(
onDragStart = { onDragStart = { pos ->
total = Offset.Zero origin = pos
if (!started && !gameOver) started = true if (!started && !gameOver) started = true
},
onDragEnd = {
if (started && !gameOver) {
if (abs(total.x) > abs(total.y)) {
if (total.x > 0 && dir.x != -1) nextDir = Cell(1, 0)
else if (total.x < 0 && dir.x != 1) nextDir = Cell(-1, 0)
} else {
if (total.y > 0 && dir.y != -1) nextDir = Cell(0, 1)
else if (total.y < 0 && dir.y != 1) nextDir = Cell(0, -1)
}
}
} }
) { change, drag -> ) { change, _ ->
total += drag
change.consume() change.consume()
val off = change.position - origin
if (off.getDistance() >= deadZone) {
if (abs(off.x) > abs(off.y)) turn(if (off.x > 0) 1 else -1, 0)
else turn(0, if (off.y > 0) 1 else -1)
}
} }
} }
) { ) {
@ -299,7 +304,7 @@ fun SnakeGame() {
// Оверлеи: старт, пауза и проигрыш // Оверлеи: старт, пауза и проигрыш
if (!started) { if (!started) {
Overlay(title = "ЗМЕЙКА", subtitle = "Свайпни, чтобы начать · тап — пауза", modifier = Modifier.align(Alignment.Center)) Overlay(title = "ЗМЕЙКА", subtitle = "Веди пальцем — поворот · тап — пауза", modifier = Modifier.align(Alignment.Center))
} else if (gameOver) { } else if (gameOver) {
Overlay(title = "GAME OVER", subtitle = "Счёт: $score · тапни для рестарта", modifier = Modifier.align(Alignment.Center)) Overlay(title = "GAME OVER", subtitle = "Счёт: $score · тапни для рестарта", modifier = Modifier.align(Alignment.Center))
} else if (paused) { } else if (paused) {