Модули
Файл 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
ЕЩЁ: Узнать больше о модулях и о том, как управлять растущим проектом, можно здесь.
Начнём с того, что переместим все компоненты в отдельный файл. Это не должно вызвать необходимости в каких-либо дополнительных изменениях — за исключением того, что некоторые поля придётся сделать публичными. Всё дело в том, что пока у нас был только один файл, всё в этом файле имело свободный доступ друг к другу. Это было проще для понимания, но теперь, когда мы начали разделять компоненты, нам нужно обращать больше внимания на области видимости. Пока мы просто сделаем поля публичными, чтобы всё снова заработало, — но есть способ получше, и его мы позже обсудим. Кроме того, мы переместили регистрацию компонентов в самый низ файла, чтобы сделать добавление новых удобнее.
Затем ресурсы:
Теперь мы сделаем отдельный файл для констант. Пока размеры карты намертво запечатаны в коде — они нужны нам для перемещения, чтобы знать, когда мы достигнем краёв. Но наш проект будет расти, и однажды мы сделаем размеры карты динамическими — они будут считаться при её загрузке.
Код создания сущностей мы тоже вынесем в отдельный файл:
Теперь очередь кода загрузки карты:
Наконец, мы создадим отдельные файлы для систем: RenderingSystem
будет в rendering_system.rs
, а InputSystem
— в input_system.rs
. Просто копируйте и вставляйте — и не забудьте удалить весь лишний импорт.
Кое-что интересное о системах: они представляют из себя директории с несколькими файлами внутри. Если мы остановимся на этом и попробуем запустить RenderingSystem
или InputSystem
из main
, мы получим ошибки при компиляции. Нам нужно добавить файл mod.rs
в папку systems
и объяснить Rust, что именно мы хотим экспортировать из этой папки. Всё, что делает этот кусочек кода, — сообщает, что мы хотим дать доступ к типам RenderingSystem
и InputSystem
из внешнего мира (мира вне этой папки):
Отлично. Теперь, после всего, что мы сделали, наш упрощённый файл 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 */
Не стесняйтесь — попробуйте запустить игру. Всё должно работать точно так же, как раньше. Единственное отличие в том, что код стал гораздо чище, и теперь нам будет намного проще добавлять новые интересные механики.
КОД: Увидеть весь код из данной главы можно здесь.