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

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

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

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

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