模块
主文件已经变得相当大了,可以想象,随着我们的项目增长,这种方式将无法维持下去。幸运的是,Rust 提供了模块的概念,可以让我们根据关注点将功能整齐地拆分到单独的文件中。
目前,我们的目标是实现以下文件夹结构。随着我们添加更多的组件和系统,我们可能需要不止一个文件,但这是一个不错的起点。
├── resources
│ └── images
│ ├── box.png
│ ├── box_spot.png
│ ├── floor.png
│ ├── player.png
│ └── wall.png
├── src
│ ├── systems
│ │ ├── input.rs
│ │ ├── rendering.rs
│ │ └── mod.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() { // 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.rs
,InputSystem
到 input.rs
)。这应该只是从主文件中复制粘贴并移除一些导入,因此可以直接进行。
我们需要更新 mod.rs
,告诉 Rust 我们想将所有系统导出到外部(在这里是主模块)。
#![allow(unused)] fn main() { // systems/mod.rs pub mod input; pub mod rendering; }
太棒了,现在我们完成了这些操作,以下是简化后的主文件的样子。注意导入后的 mod
和 use
声明,它们再次告诉 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 */
此时可以运行,所有功能应该与之前完全相同,不同的是,现在我们的代码更加整洁,为更多令人惊叹的 Sokoban 功能做好了准备。
CODELINK: 你可以在 这里 查看本示例的完整代码。