Модули
Файл 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 */
Не стесняйтесь — попробуйте запустить игру. Всё должно работать точно так же, как раньше. Единственное отличие в том, что код стал гораздо чище, и теперь нам будет намного проще добавлять новые интересные механики.
КОД: Увидеть весь код из данной главы можно здесь.