Игровой процесс

Игрок может двигаться сам и перемещает коробки по игровому полю. У множества (хотя и не у всех!) игр есть какая-то цель — и цель игр, похожих на 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}}
}

Теперь в игре есть базовая обратная связь для игрока:

  • Подсчёт количества шагов
  • Сообщения о том, что игрок победил

Вот как она выглядит:

Sokoban play

Впереди ещё много других улучшений!

КОД: Увидеть весь код из данной главы можно здесь.