Игровой процесс
Игрок может двигаться сам и перемещает коробки по игровому полю. У множества (хотя и не у всех!) игр есть какая-то цель — и цель игр, похожих на Sokoban, заключается в размещении коробок на правильные места. Ничто не мешает игроку делать это прямо сейчас, но игра никак не проверяет его успех. Игрок может достичь цели и даже этого не понять! Давайте обновим игру, чтобы за этим следить.
Подумаем, что нам нужно, чтобы добавить в игру проверку условий успеха и сообщить пользователю о том, что уровень пройден:
- Ресурс для отслеживания состояния игры.
- Игра в процессе или завершена?
- Как много шагов сделал игрок?
- Система для проверки того, достиг ли игрок своей цели.
- Система для обновления числа сделанных шагов.
- Пользовательский интерфейс, который отображает состояние игры.
Ресурсы игрового процесса
Мы решили использовать resource
для того, чтобы отслеживать состояние игры — потому что оно не ассоциировано с какой-либо сущностью. Начнём с того, что определим ресурс игрового процесса — Gameplay
.
#![allow(unused)] fn main() { // resources.rs {{#include ../../../code/rust-sokoban-c02-05/src/resources.rs:38:43}} }
У Gameplay
есть два поля: состояние — state
и число шагов — moves_count
. Они используются для того, чтобы отслеживать состояние игры (игрок ещё играет или уже выиграл?) и число сделанных шагов. state
описана перечислением (enum
) следующим образом:
#![allow(unused)] fn main() { // resources.rs {{#include ../../../code/rust-sokoban-c02-05/src/resources.rs:17:20}} }
Внимательный читатель заметит, что мы использовали макрос, чтобы унаследовать типаж Default
для ресурса Gameplay
, но не для перечисления GameplayState
. Причина проста: если мы хотим использовать Gameplay
как ресурс, оно должно реализовывать Default
.
Итак, что дальше? Так как макросы Rust не могут наследовать Default
для перечислений автоматически, мы должны реализовать Default
для Gameplay
своими руками.
#![allow(unused)] fn main() { // resources.rs {{#include ../../../code/rust-sokoban-c02-05/src/resources.rs:32:36}} }
Определив ресурсы, зарегистрируем их в нашем мире:
#![allow(unused)] fn main() { // resources.rs {{#include ../../../code/rust-sokoban-c02-05/src/resources.rs:12:15}} }
Теперь после того, как начнётся игра, ресурс Gameplay
будет выглядеть так:
#![allow(unused)] fn main() { Gameplay { state: GameplayState::Playing, moves_count: 0 } }
Система подсчёта шагов
Мы можем инкрементировать поле moves_count
в ресурсе Gameplay
, чтобы отслеживать число сделанных шагов. У нас уже есть система пользовательского ввода — InputSystem
, поэтому просто адаптируем её для этой задачи.
Поскольку нам нужно изменять ресурс Gameplay
, мы должны зарегистрировать его в InputSystem
. Добавим Write<'a, Gameplay>
в определение типов SystemData
.
#![allow(unused)] fn main() { // input_system.rs {{#include ../../../code/rust-sokoban-c02-05/src/systems/input_system.rs:0:25}} ... }
Так как мы уже сделали всё нужное для того, чтобы изменять позицию игрока в ответ на нажатие клавиш, мы можем использовать этот же код, чтобы понять, когда нужно инкрементировать счётчик шагов:
#![allow(unused)] fn main() { // input_system.rs ... {{#include ../../../code/rust-sokoban-c02-05/src/systems/input_system.rs:83:105}} }
Система игрового процесса
Теперь давайте внедрим этот ресурс в новую систему — GamePlayStateSystem
. Она будет непрерывно проверять, все ли коробки на нужных местах. Как только они окажутся там, где задумано, игра будет выиграна!
Помимо ресурса Gameplay
, этой системе нужен доступ к чтению содержимого Position
, Box
и BoxSpot
.
Система использует Join
, чтобы создать вектор из Box
и Position
. Этот вектор размечен как HashMap
и содержит позицию каждой коробки на игровом поле.
Дальше система снова использует метод Join
, чтобы создать последовательность из сущностей, у которых есть оба компонента: и BoxSpot
, и Position
. Система проходит по этой последовательности, и если у каждого места для коробки найдётся коробка с той же позицией — игра пройдена, игрок победил. Иначе она всё ещё идёт.
#![allow(unused)] fn main() { // gameplay_state_system.rs {{#include ../../../code/rust-sokoban-c02-05/src/systems/gameplay_state_system.rs::}} }
Наконец, запустим нашу систему игрового процесса в главном цикле обновлений:
#![allow(unused)] fn main() { // main.rs // ANCHOR: handler impl event::EventHandler<ggez::GameError> for Game { fn update(&mut self, context: &mut Context) -> GameResult { // Run input system { systems::input::run_input(&self.world, context); } // Run gameplay state { systems::gameplay::run_gameplay_state(&self.world); } Ok(()) } // ... }
Игровой интерфейс
Последний шаг заключается в создании обратной связи для пользователя, чтобы тот понимал, в каком состоянии находится сейчас игра. Для этого потребуется ресурс, который будет отслеживать её состояние, и система, которая будет это состояние обновлять. Под эту задачу мы можем адаптировать уже существующие GameplayState
и RenderingSystem
.
Для начала мы реализуем типаж Display
для GameplayState
, чтобы мы могли вывести состояние игры в виде текста. Мы будем использовать выражение соответствия, чтобы разрешить GameplayState
отрисовать текст "Играем" — "Playing" или "Победили" — "Won".
#![allow(unused)] fn main() { // resources.rs {{#include ../../../code/rust-sokoban-c02-05/src/resources.rs:21:30}} }
Затем мы добавим метод draw_text
в систему RenderingSystem
, чтобы она могла вывести GameplayState
на экран...
#![allow(unused)] fn main() { // rendering_systems.rs {{#include ../../../code/rust-sokoban-c02-05/src/systems/rendering_system.rs:16:32}} }
...и после этого добавим ресурс Gameplay
в систему RenderingSystem
. Для того, чтобы мы могли вызвать draw_text
, RenderingSystem
должна иметь возможность читать ресурс Gameplay
.
#![allow(unused)] fn main() { // rendering_system.rs {{#include ../../../code/rust-sokoban-c02-05/src/systems/rendering_system.rs:35:71}} }
Теперь в игре есть базовая обратная связь для игрока:
- Подсчёт количества шагов
- Сообщения о том, что игрок победил
Вот как она выглядит:
Впереди ещё много других улучшений!
КОД: Увидеть весь код из данной главы можно здесь.