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_colisions→collisions(src/post_update.rs,src/game.rs)GridBarier→GridBarrier(src/domain/events.rsи все использования)splited→split(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.