Files
rust-otus/snake-game/REVIEW.md
T
2026-07-06 12:50:03 +03:00

7.5 KiB

Код-ревью проекта «Змейка»

Общая оценка

Крепкая, аккуратно организованная работа. Автор хорошо владеет идиомами Bevy ECS: разделение на модули по расписаниям (startup / update / fixed_update / post_update), использование событий и обзёрверов, разнесение доменной модели в domain/. Логика движения по сетке с разбиением шага на границе (split_by_grid) — нетривиальная и покрыта хорошим юнит-тестом. Документирующие комментарии присутствуют почти везде.

Есть один реальный функциональный баг (таймер спавна еды) и несколько игровых/стилистических замечаний.


Критичные замечания

1. Таймер спавна еды тикается общим временем вместо дельты кадра

src/fixed_update.rs:92

if meal_timer.tick(time.elapsed()).just_finished() && query.count() < MAX_MEALS_ON_FIELD as usize {

Timer::tick принимает дельту времени за кадр, а time.elapsed() возвращает суммарное время с момента старта (постоянно растущее). В результате:

  • Первые ~2 секунды таймер копит elapsed, затем на каждом fixed-шаге тикается на всё накопленное время (2.0s, 2.03s, 2.06s …). Для Repeating-таймера это значит just_finished() становится true практически каждый кадр после первых 2 секунд.
  • Как следствие поле мгновенно заполняется до MAX_MEALS_ON_FIELD (5 штук), а после поедания еды тут же добивается обратно — заявленной механики «одна еда раз в 2 секунды» нет.

Исправление:

if meal_timer.tick(time.delta()).just_finished() && query.count() < MAX_MEALS_ON_FIELD as usize {

Средние замечания

2. Еда может появиться прямо под змеёй

src/fixed_update.rs:93-103

used_positions собирается только из других объектов еды — позиции головы и сегментов хвоста не учитываются. Поэтому еда легко спавнится под телом змеи, что портит игровой процесс (съедается «бесплатно» либо появляется в недосягаемом месте). Стоит добавить в исключаемые позиции клетки, занятые Head/Tail.

3. Столкновение с границей триггерится каждый кадр

src/post_update.rs:6-16

После выхода за границу скорость обнуляется, но координаты головы остаются за пределами поля, поэтому Collision::Bounds генерируется на каждом PostUpdate, и handle_collisions бесконечно переустанавливает скорость/цвет. У столкновения с хвостом такой проблемы нет (там есть отсечка velocity.0 == 0.0). Логичнее ввести явное состояние «игра окончена» (например, компонент-маркер GameOver или State), которое остановит проверки и позволит сделать рестарт. Сейчас после проигрыша игру можно только перезапустить процессом.


Мелкие замечания и стиль

4. Диапазон случайной позиции еды меньше поля и смещён

src/fixed_update.rs:96-99

const ABS: i8 = (FIELD_SIZE as f32 / 2.0) as i8;   // = 10
let rand_x = rng.random::<i8>() % ABS;             // диапазон -9..=9

При FIELD_SIZE = 20 поле по логике занимает клетки -10..=10, но еда никогда не появляется в крайних клетках ±10. Также остаток от деления даёт чуть неравномерное распределение и 0 встречается вдвое чаще краёв. Для равномерного распределения удобнее rng.random_range(-ABS..=ABS).

5. Неограниченное ускорение/замедление времени

src/update.rs:30-40

Equal/Minus умножают relative_speed без верхней/нижней границы — можно довести скорость до неиграбельных значений (или почти до нуля). Стоит ограничить, например, диапазоном [0.125, 8.0].

6. Опечатки в именах (для читаемости кода)

  • check_bounds_colisions, check_tail_colisionscollisions (src/post_update.rs, src/game.rs)
  • GridBarierGridBarrier (src/domain/events.rs и все использования)
  • splitedsplit (src/fixed_update.rs:23)

Не влияет на работу, но публичные имена типов/систем лучше писать без ошибок.

8. Учебный «мусор» в репозитории поставки

  • tests/rust_learning.rs — это личные эксперименты (dbg! по saturating_sub, ceil, floor и т.п.), а не тесты проекта. В сдаваемой работе их лучше убрать или вынести за пределы tests/.
  • examples/ содержит туториалы по Bevy (bevy_learning.rs, paddle_demo.rs …), не относящиеся к игре. Для учебного проекта допустимо, но стоит хотя бы упомянуть их статус в README.

Что сделано хорошо

  • Чистая модульная структура: доменная модель отделена от систем, системы сгруппированы по расписаниям Bevy.
  • Идиоматичный ECS: события (Collision, GrowUp, GridBarier), обзёрверы, Single-запросы, ресурсы (SnakePath, MealRespawnTimer).
  • Аккуратная механика поворотов: змея не может развернуться в себя (только rotate_left/ rotate_right), а первый сегмент за головой исключён из проверки столкновений — грамотное решение классических проблем «Змейки».
  • split_by_grid — сложная логика движения через узлы сетки покрыта развёрнутым юнит-тестом со множеством сценариев.
  • Настроен rustfmt.toml, продуманный релизный профиль (opt-level="z", lto, strip), ведётся логирование в файл и stdout.