Компоненты и сущности

В этой главе мы создадим наши компоненты и увидим, как создавать сущности и регистрировать всё правильно, чтобы specs был доволен.

Определение компонентов

Начнём с определения компонентов. Ранее мы обсуждали компоненты позиции, отображения и перемещения. На данном этапе мы опустим перемещение. Также нам необходимы некоторые компоненты для идентификации каждой сущности — например, нам нужен компонент стены, чтобы мы могли идентифицировать сущность как стену по его наличию.

Достаточно очевидно, что компонент позиции будет хранить координаты x, y и z, которые скажут нам о положении чего-либо на карте. В компоненте отображения мы будем хранить строковый путь, указывающий на изображение, которое мы сможем отрисовать. Все остальные компоненты — маркерные, не содержащие (пока что) данных.


#![allow(unused)]
fn main() {
    x: u8,
    y: u8,
    z: u8,
}

#[allow(dead_code)]
pub struct Renderable {
    path: String,
}

pub struct Wall {}

pub struct Player {}

pub struct Box {}

pub struct BoxSpot {}

// ANCHOR_END: components

// ANCHOR: game
// This struct will hold all our game state
// For now there is nothing to be held, but we'll add
// things shortly.
#[allow(dead_code)]
struct Game {
    world: World,
}
// ANCHOR_END: game

}

Вместе с уже знакомым кодом на Rust тут мы видим новый синтаксис. Здесь, в #[storage(VecStorage)], мы используем могущественную особенность Rust — процедурные макросы. Макросы этого типа по сути являются функциями, которые во время компиляции принимают один синтаксис и производят из него другой.

ЕЩЁ: Узнать больше о процедурных макросах вы можете здесь.

Регистрация компонентов

Чтобы specs не ругался, мы должны заранее сообщить ему, какие компоненты мы будем использовать. Давайте создадим функцию, которая будет регистрировать компоненты в specs.


#![allow(unused)]
fn main() {
        },
        Wall {},
    ))
}
pub fn create_floor(world: &mut World, position: Position) -> Entity {
    world.spawn((
        Position { z: 5, ..position },
        Renderable {
            path: "/images/floor.png".to_string(),
}

Создание сущностей

Сущность — это просто числовой идентификатор, привязанный к набору компонентов. Таким образом, создание сущностей — это просто указание, какие компоненты они содержат.

Теперь создание сущностей выглядит так:

    ))
}

pub fn create_box(world: &mut World, position: Position) -> Entity {
    world.spawn((
        Position { z: 10, ..position },
        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(),
        },
        BoxSpot {},
    ))
}

pub fn create_player(world: &mut World, position: Position) -> Entity {
    world.spawn((
        Position { z: 10, ..position },
        Renderable {
            path: "/images/player.png".to_string(),
        },
        Player {},
    ))
}
// ANCHOR_END: entities

// ANCHOR: main
pub fn main() -> GameResult {
    let world = World::new();

    // 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)
}
// ANCHOR_END: main

/* ANCHOR_END: all */

Ресурсы

Вы могли заметить, что при создании сущностей мы ссылаемся на ресурсы, которые будем использовать. Вы можете создать свои собственные ресурсы или загрузить те, которые использую я. Они находятся ниже (просто сохраните их как изображения из контекстного меню).

Floor tile Wall tile Player tile Box tile Box tile

Давайте добавим изображения в наш проект. Мы создадим директорию resources, которая будет содержать все наши ресурсы. На данный момент у нас будут только изображения, но в будущем появятся другие типы ресурсов, такие как конфигурационные файлы или аудио (об этом мы узнаем в главе 3.3 — Звуки и события). Мы также добавим директорию images и поместим туда наши изображения. Если хотите — можете использовать другую структуру каталогов, но будьте внимательны в дальнейшем везде, где мы используем пути к изображениям.

├── resources
│   └── images
│       ├── box.png
│       ├── box_spot.png
│       ├── floor.png
│       ├── player.png
│       └── wall.png
├── src
│   └── main.rs
└── Cargo.toml

Создание мира

Наконец, соберём всё вместе. Мы должны создать объект specs::World, чтобы добавить его в структуру Game. Он будет первым, что мы инициализируем в нашем main. Ниже находится полный код, который при запуске выведет то же самое пустое окно — но мы сделали огромный прогресс в настройке игровых компонентов и сущностей! Далее мы перейдём к отрисовке, чтобы наконец увидеть что-то на экране!

/* ANCHOR: all */
// Rust sokoban
// main.rs

use ggez::{conf, event, Context, GameResult};
use hecs::{Entity, World};

use std::path;

// ANCHOR: components
#[allow(dead_code)]
pub struct Position {
    x: u8,
    y: u8,
    z: u8,
}

#[allow(dead_code)]
pub struct Renderable {
    path: String,
}

pub struct Wall {}

pub struct Player {}

pub struct Box {}

pub struct BoxSpot {}

// ANCHOR_END: components

// ANCHOR: game
// This struct will hold all our game state
// For now there is nothing to be held, but we'll add
// things shortly.
#[allow(dead_code)]
struct Game {
    world: World,
}
// ANCHOR_END: game

// ANCHOR: handler
impl event::EventHandler<ggez::GameError> for Game {
    fn update(&mut self, _context: &mut Context) -> GameResult {
        Ok(())
    }

    fn draw(&mut self, _context: &mut Context) -> GameResult {
        Ok(())
    }
}
// ANCHOR_END: handler

// ANCHOR: entities
pub fn create_wall(world: &mut World, position: Position) -> Entity {
    world.spawn((
        Position { z: 10, ..position },
        Renderable {
            path: "/images/wall.png".to_string(),
        },
        Wall {},
    ))
}
pub fn create_floor(world: &mut World, position: Position) -> Entity {
    world.spawn((
        Position { z: 5, ..position },
        Renderable {
            path: "/images/floor.png".to_string(),
        },
    ))
}

pub fn create_box(world: &mut World, position: Position) -> Entity {
    world.spawn((
        Position { z: 10, ..position },
        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(),
        },
        BoxSpot {},
    ))
}

pub fn create_player(world: &mut World, position: Position) -> Entity {
    world.spawn((
        Position { z: 10, ..position },
        Renderable {
            path: "/images/player.png".to_string(),
        },
        Player {},
    ))
}
// ANCHOR_END: entities

// ANCHOR: main
pub fn main() -> GameResult {
    let world = World::new();

    // 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)
}
// ANCHOR_END: main

/* ANCHOR_END: all */

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

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