Передвижение игрока

Игра не будет игрой, если игрок просто стоит на месте, не так ли? Поэтому здесь мы научимся перехватывать входящие события.

Входящие события

Первый шаг к тому, чтобы наш игрок двигался, — это прослушивание входящих событий. Если мы посмотрим на этот пример в 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 { }

Эта система ввода довольно простая: сначала она получает информацию обо всех игроках и их позициях (он у нас будет всего один, но коду не нужно об этом знать — в теории у нас может быть несколько игроков, которых мы захотим контролировать одним и тем же устройством ввода). Затем для каждой комбинации игрока и его позиции система берёт первую нажатую клавишу и удаляет её из очереди ввода. После чего она сопоставляет её с требуемым перемещением: если мы нажмём клавишу вверх, то нам нужно сдвинуться на одну клетку вверх — и так далее. И в конце она обновляет позицию игрока.

Круто, правда? Ниже — то, как это должно выглядеть. Ничего страшного, что мы пока можем ходить через стены и коробки насквозь, — мы починим это в следующем разделе, когда будем добавлять перемещаемые компоненты.

Moving player

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