Модули

Файл 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
#[derive(Clone, Copy, Eq, Hash, PartialEq)]
pub struct Position {
    pub x: u8,
    pub y: u8,
    pub z: u8,
}

pub struct Renderable {
    pub path: String,
}

pub struct Wall {}

pub struct Player {}

pub struct Box {}

pub struct BoxSpot {}

pub struct Movable;

pub struct Immovable;
}

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


#![allow(unused)]
fn main() {
// resources.rs
{{#include ../../../code/rust-sokoban-c02-04/src/resources.rs:}}
}

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


#![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 hecs::{Entity, World};

pub fn create_wall(world: &mut World, position: Position) -> Entity {
    world.spawn((
        Position { z: 10, ..position },
        Renderable {
            path: "/images/wall.png".to_string(),
        },
        Wall {},
        Immovable {},
    ))
}
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 {},
        Movable {},
    ))
}

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 {},
        Movable {},
    ))
}
}

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


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

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 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
pub mod input;
pub mod rendering;
}

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

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

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

use std::path;

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

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

// ANCHOR: handler
impl event::EventHandler<ggez::GameError> for Game {
    fn update(&mut self, context: &mut Context) -> GameResult {
        // Run input system
        {
            systems::input::run_input(&self.world, context);
        }

        Ok(())
    }

    fn draw(&mut self, context: &mut Context) -> GameResult {
        // Render game entities
        {
            systems::rendering::run_rendering(&self.world, context);
        }

        Ok(())
    }
}
// ANCHOR_END: handler

// ANCHOR: main
pub fn main() -> GameResult {
    let mut world = World::new();
    map::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)
}
// ANCHOR_END: main

/* ANCHOR_END: all */

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

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