Модули

Файл main заметно подрос — и вы уже понимаете, что если проект будет продолжать развиваться, его поддержка будет не самым приятным делом. К счастью, в Rust есть концепция модулей, которая позволит нам разделять функционал по принципу разделения файлов.

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

├── resources
│   └── images
│       ├── box.png
│       ├── box_spot.png
│       ├── floor.png
│       ├── player.png
│       └── wall.png
├── src
│   ├── systems
│   │   ├── input_system.rs
│   │   ├── mod.rs
│   │   └── rendering_system.rs
│   ├── components.rs
│   ├── constants.rs
│   ├── entities.rs
│   ├── main.rs
│   ├── map.rs
│   └── resources.rs
└── Cargo.toml

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

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


#![allow(unused)]
fn main() {
// components.rs
use specs::{Component, NullStorage, VecStorage, World, WorldExt};

// Components
#[derive(Debug, Component, Clone, Copy)]
#[storage(VecStorage)]
pub struct Position {
    pub x: u8,
    pub y: u8,
    pub z: u8,
}

#[derive(Component)]
#[storage(VecStorage)]
pub struct Renderable {
    pub path: String,
}

#[derive(Component)]
#[storage(VecStorage)]
pub struct Wall {}

#[derive(Component)]
#[storage(VecStorage)]
pub struct Player {}

#[derive(Component)]
#[storage(VecStorage)]
pub struct Box {}

#[derive(Component)]
#[storage(VecStorage)]
pub struct BoxSpot {}

#[derive(Component, Default)]
#[storage(NullStorage)]
pub struct Movable;

#[derive(Component, Default)]
#[storage(NullStorage)]
pub struct Immovable;

pub fn register_components(world: &mut World) {
    world.register::<Position>();
    world.register::<Renderable>();
    world.register::<Player>();
    world.register::<Wall>();
    world.register::<Box>();
    world.register::<BoxSpot>();
    world.register::<Movable>();
    world.register::<Immovable>();
}
}

Затем ресурсы:


#![allow(unused)]
fn main() {
// resources.rs
use ggez::event::KeyCode;
use specs::World;

// Resources
#[derive(Default)]
pub struct InputQueue {
    pub keys_pressed: Vec<KeyCode>,
}

pub fn register_resources(world: &mut World) {
    world.insert(InputQueue::default())
}
}

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


#![allow(unused)]
fn main() {
// constants.rs
pub const TILE_WIDTH: f32 = 32.0;
pub const MAP_WIDTH: u8 = 8;
pub const MAP_HEIGHT: u8 = 9;
}

Код создания сущностей мы тоже вынесем в отдельный файл:


#![allow(unused)]
fn main() {
// entities.rs
use crate::components::*;
use specs::{Builder, World, WorldExt};

// Create a wall entity
pub fn create_wall(world: &mut World, position: Position) {
    world
        .create_entity()
        .with(Position { z: 10, ..position })
        .with(Renderable {
            path: "/images/wall.png".to_string(),
        })
        .with(Wall {})
        .with(Immovable)
        .build();
}

pub fn create_floor(world: &mut World, position: Position) {
    world
        .create_entity()
        .with(Position { z: 5, ..position })
        .with(Renderable {
            path: "/images/floor.png".to_string(),
        })
        .build();
}

pub fn create_box(world: &mut World, position: Position) {
    world
        .create_entity()
        .with(Position { z: 10, ..position })
        .with(Renderable {
            path: "/images/box.png".to_string(),
        })
        .with(Box {})
        .with(Movable)
        .build();
}

pub fn create_box_spot(world: &mut World, position: Position) {
    world
        .create_entity()
        .with(Position { z: 9, ..position })
        .with(Renderable {
            path: "/images/box_spot.png".to_string(),
        })
        .with(BoxSpot {})
        .build();
}

pub fn create_player(world: &mut World, position: Position) {
    world
        .create_entity()
        .with(Position { z: 10, ..position })
        .with(Renderable {
            path: "/images/player.png".to_string(),
        })
        .with(Player {})
        .with(Movable)
        .build();
}
}

Теперь очередь кода загрузки карты:


#![allow(unused)]
fn main() {
// map.rs
use crate::components::Position;
use crate::entities::*;
use specs::World;

pub fn load_map(world: &mut World, map_string: String) {
    // read all lines
    let rows: Vec<&str> = map_string.trim().split('\n').map(|x| x.trim()).collect();

    for (y, row) in rows.iter().enumerate() {
        let columns: Vec<&str> = row.split(' ').collect();

        for (x, column) in columns.iter().enumerate() {
            // Create the position at which to create something on the map
            let position = Position {
                x: x as u8,
                y: y as u8,
                z: 0, // we will get the z from the factory functions
            };

            // Figure out what object we should create
            match *column {
                "." => create_floor(world, position),
                "W" => {
                    create_floor(world, position);
                    create_wall(world, position);
                }
                "P" => {
                    create_floor(world, position);
                    create_player(world, position);
                }
                "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),
            }
        }
    }
}
}

Наконец, мы создадим отдельные файлы для систем: RenderingSystem будет в rendering_system.rs, а InputSystem — в input_system.rs. Просто копируйте и вставляйте — и не забудьте удалить весь лишний импорт.

Кое-что интересное о системах: они представляют из себя директории с несколькими файлами внутри. Если мы остановимся на этом и попробуем запустить RenderingSystem или InputSystem из main, мы получим ошибки при компиляции. Нам нужно добавить файл mod.rs в папку systems и объяснить Rust, что именно мы хотим экспортировать из этой папки. Всё, что делает этот кусочек кода, — сообщает, что мы хотим дать доступ к типам RenderingSystem и InputSystem из внешнего мира (мира вне этой папки):


#![allow(unused)]
fn main() {
// systems/mod.rs
mod input_system;
mod rendering_system;

pub use self::input_system::InputSystem;
pub use self::rendering_system::RenderingSystem;
}

Отлично. Теперь, после всего, что мы сделали, наш упрощённый файл main выглядит вот так. Обратите внимание на декларации use и mod после импортирования из сторонних библиотек — опять же, это нужно для того, чтобы Rust понимал, что мы хотим использовать эти модули.

// main.rs
// Rust sokoban
// main.rs

use ggez::{conf, event::{self, KeyCode, KeyMods}, Context, GameResult};
use specs::{RunNow, World, WorldExt};
use std::path;

mod components;
mod constants;
mod entities;
mod map;
mod resources;
mod systems;

use crate::components::*;
use crate::map::*;
use crate::resources::*;
use crate::systems::*;

struct Game {
    world: World,
}

impl event::EventHandler<ggez::GameError> for Game {
    fn update(&mut self, _context: &mut Context) -> GameResult {
        // Run input system
        {
            let mut is = InputSystem {};
            is.run_now(&self.world);
        }

        Ok(())
    }

    fn draw(&mut self, context: &mut Context) -> GameResult {
        // Render game entities
        {
            let mut rs = RenderingSystem { context };
            rs.run_now(&self.world);
        }

        Ok(())
    }

    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);
    }
}

// Initialize the level
pub fn initialize_level(world: &mut World) {
    const MAP: &str = "
    N N W W W W W W
    W W W . . . . W
    W . . . B . . W
    W . . . . . . W 
    W . P . . . . W
    W . . . . . . W
    W . . S . . . W
    W . . . . . . W
    W W W W W W W W
    ";

    load_map(world, MAP.to_string());
}

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)
}

Не стесняйтесь — попробуйте запустить игру. Всё должно работать точно так же, как раньше. Единственное отличие в том, что код стал гораздо чище, и теперь нам будет намного проще добавлять новые интересные механики.

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