Передвижение игрока
Игра не будет игрой, если игрок просто стоит на месте, не так ли? Поэтому здесь мы научимся перехватывать входящие события.
Входящие события
Первый шаг к тому, чтобы наш игрок двигался, — это прослушивание входящих событий. Если мы посмотрим на этот пример в ggez, то увидим, что у нас есть возможность подписаться на любые события от мыши и клавиатуры. Но сейчас нам нужен только key_down_event
.
Начнём прослушивать события нажатия клавиш. Для начала подключим ещё несколько модулей:
#![allow(unused)] fn main() { /* ANCHOR: all */ // Rust sokoban // main.rs use ggez::{ conf, event, graphics::{self, DrawParam, Image}, input::keyboard::KeyCode, Context, GameResult, }; use glam::Vec2; }
Затем добавим этот код в блок event::EventHandler
нашей игры:
#![allow(unused)] fn main() { pub fn create_wall(world: &mut World, position: Position) -> Entity { // ... Renderable { path: "/images/box.png".to_string(), }, Box {}, )) } pub fn create_box_spot(world: &mut World, position: Position) -> Entity { path: "/images/box_spot.png".to_string(), // ... }, }
Если сейчас мы это запустим, в консоли появятся следующие строки:
Key pressed: Left
Key pressed: Left
Key pressed: Right
Key pressed: Up
Key pressed: Down
Key pressed: Left
Если вы ещё не знакомы с нотацией {:?}
, которая использовалась для вывода строк, — то это просто удобный способ, которым Rust позволяет нам выводить информацию об объектах в консоль. Его можно использовать для отладки. В нашем случае мы смогли вывести объект KeyCode
(который является перечислением), потому что тип KeyCode
реализует типаж Debug
с помощью макроса Debug
(помните, мы уже обсуждали макросы в главе 1.3? Вернитесь и перечитайте, если вам нужно освежить память). Если бы KeyCode
не реализовывал Debug
, мы бы не смогли использовать этот синтаксис и получили бы ошибку при компиляции. Но благодаря этому мы избавлены от необходимости писать код с нуля, чтобы преобразовать коды клавиш в строку — можно просто положиться на встроенный функционал.
Ресурсы
Следующим шагом мы добавим ресурсы — так specs
сможет обмениваться состояниями внутри систем, которые не являются частью вашего мира. Мы будем использовать ресурсы для моделирования очереди нажатий клавиш, потому что этот тип взаимодействия немного не вписывается в ECS — нашу существующую модель сущностей и компонентов.
#![allow(unused)] fn main() { // ANCHOR: init // Initialize the level// Initialize the level pub fn initialize_level(world: &mut World) { const MAP: &str = " }
Затем мы добавляем новые нажатия клавиш в очередь, когда вызывается событие key_down_event
:
#![allow(unused)] fn main() { pub fn create_wall(world: &mut World, position: Position) -> Entity { // ... Renderable { path: "/images/box.png".to_string(), }, Box {}, )) } pub fn create_box_spot(world: &mut World, position: Position) -> Entity { world.spawn(( Position { z: 9, ..position }, Renderable { path: "/images/box_spot.png".to_string(), // ... }, }
И напоследок нам нужно зарегистрировать наши ресурсы в specs
— так же, как мы сделали с компонентами:
#![allow(unused)] fn main() { // Регистрация ресурсов )) } // ANCHOR_END: entities // Регистрация ресурсов в `main` }
Система ввода
С помощью этого кода мы получили ресурс, который представляет собой непрерывную очередь сигналов о нажатиях клавиш. Далее мы начнём обрабатывать её в системе:
#![allow(unused)] fn main() { } "B" => { create_floor(world, position); create_box(world, position); } "S" => { create_floor(world, position); create_box_spot(world, position); } "N" => (), c => panic!("unrecognized map item {}", c), } } } } // ANCHOR_END: init // ANCHOR: handler impl event::EventHandler<ggez::GameError> for Game { fn update(&mut self, context: &mut Context) -> GameResult { // Run input system { run_input(&self.world, context); } Ok(()) } }
После чего нам остаётся только запустить систему в цикле обновлений:
#![allow(unused)] fn main() { world.spawn(( Position { z: 10, ..position }, Renderable { path: "/images/wall.png".to_string(), }, Wall {}, )) } pub fn create_floor(world: &mut World, position: Position) -> Entity { }
Эта система ввода довольно простая: сначала она получает информацию обо всех игроках и их позициях (он у нас будет всего один, но коду не нужно об этом знать — в теории у нас может быть несколько игроков, которых мы захотим контролировать одним и тем же устройством ввода). Затем для каждой комбинации игрока и его позиции система берёт первую нажатую клавишу и удаляет её из очереди ввода. После чего она сопоставляет её с требуемым перемещением: если мы нажмём клавишу вверх
, то нам нужно сдвинуться на одну клетку вверх — и так далее. И в конце она обновляет позицию игрока.
Круто, правда? Ниже — то, как это должно выглядеть. Ничего страшного, что мы пока можем ходить через стены и коробки насквозь, — мы починим это в следующем разделе, когда будем добавлять перемещаемые компоненты.
КОД: Увидеть весь код из данной главы можно здесь.