Передвижение игрока
Игра не будет игрой, если игрок просто стоит на месте, не так ли? Поэтому здесь мы научимся перехватывать входящие события.
Входящие события
Первый шаг к тому, чтобы наш игрок двигался, — это прослушивание входящих событий. Если мы посмотрим на этот пример в ggez, то увидим, что у нас есть возможность подписаться на любые события от мыши и клавиатуры. Но сейчас нам нужен только key_down_event
.
Начнём прослушивать события нажатия клавиш. Для начала подключим ещё несколько модулей:
#![allow(unused)] fn main() { // Rust sokoban // main.rs use glam::Vec2; use ggez::{conf, Context, GameResult, event::{self, KeyCode, KeyMods}, graphics::{self, DrawParam, Image}}; use specs::{ join::Join, Builder, Component, ReadStorage, RunNow, System, VecStorage, World, WorldExt, Write, WriteStorage, }; }
Затем добавим этот код в блок event::EventHandler
нашей игры:
#![allow(unused)] fn main() { impl event::EventHandler<ggez::GameError> for Game { // ... fn key_down_event( &mut self, _context: &mut Context, keycode: KeyCode, _keymod: KeyMods, _repeat: bool, ) { println!("Key pressed: {:?}", keycode); } // ... } }
Если сейчас мы это запустим, в консоли появятся следующие строки:
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() { // Resources #[derive(Default)] pub struct InputQueue { pub keys_pressed: Vec<KeyCode>, } }
Затем мы добавляем новые нажатия клавиш в очередь, когда вызывается событие key_down_event
:
#![allow(unused)] fn main() { impl event::EventHandler<ggez::GameError> for Game { // ... fn key_down_event( &mut self, _context: &mut Context, keycode: KeyCode, _keymod: KeyMods, _repeat: bool, ) { println!("Key pressed: {:?}", keycode); let mut input_queue = self.world.write_resource::<InputQueue>(); input_queue.keys_pressed.push(keycode); } // ... } }
И напоследок нам нужно зарегистрировать наши ресурсы в specs
— так же, как мы сделали с компонентами:
// Регистрация ресурсов pub fn register_resources(world: &mut World) { world.insert(InputQueue::default()) } // Регистрация ресурсов в `main` pub fn main() -> GameResult { let mut world = World::new(); register_components(&mut world); register_resources(&mut world); initialize_level(&mut world); // Create a game context and event loop let context_builder = ggez::ContextBuilder::new("rust_sokoban", "sokoban") .window_setup(conf::WindowSetup::default().title("Rust Sokoban!")) .window_mode(conf::WindowMode::default().dimensions(800.0, 600.0)) .add_resource_path(path::PathBuf::from("./resources")); let (context, event_loop) = context_builder.build()?; // Create the game state let game = Game { world }; // Run the main event loop event::run(context, event_loop, game)
Система ввода
С помощью этого кода мы получили ресурс, который представляет собой непрерывную очередь сигналов о нажатиях клавиш. Далее мы начнём обрабатывать её в системе:
#![allow(unused)] fn main() { pub struct InputSystem {} impl<'a> System<'a> for InputSystem { // Data type SystemData = ( Write<'a, InputQueue>, WriteStorage<'a, Position>, ReadStorage<'a, Player>, ); fn run(&mut self, data: Self::SystemData) { let (mut input_queue, mut positions, players) = data; for (position, _player) in (&mut positions, &players).join() { // Get the first key pressed if let Some(key) = input_queue.keys_pressed.pop() { // Apply the key to the position match key { KeyCode::Up => position.y -= 1, KeyCode::Down => position.y += 1, KeyCode::Left => position.x -= 1, KeyCode::Right => position.x += 1, _ => (), } } } } } }
После чего нам остаётся только запустить систему в цикле обновлений:
#![allow(unused)] fn main() { fn update(&mut self, _context: &mut Context) -> GameResult { // Run input system { let mut is = InputSystem {}; is.run_now(&self.world); } Ok(()) } }
Эта система ввода довольно простая: сначала она получает информацию обо всех игроках и их позициях (он у нас будет всего один, но коду не нужно об этом знать — в теории у нас может быть несколько игроков, которых мы захотим контролировать одним и тем же устройством ввода). Затем для каждой комбинации игрока и его позиции система берёт первую нажатую клавишу и удаляет её из очереди ввода. После чего она сопоставляет её с требуемым перемещением: если мы нажмём клавишу вверх
, то нам нужно сдвинуться на одну клетку вверх — и так далее. И в конце она обновляет позицию игрока.
Круто, правда? Ниже — то, как это должно выглядеть. Ничего страшного, что мы пока можем ходить через стены и коробки насквозь, — мы починим это в следующем разделе, когда будем добавлять перемещаемые компоненты.
КОД: Увидеть весь код из данной главы можно здесь.