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

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

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

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

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

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

Moving player

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