模块

主文件已经变得相当大了,可以想象,随着我们的项目增长,这种方式将无法维持下去。幸运的是,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),
            }
        }
    }
}
}

最后,我们将系统代码移到它们自己的文件中(RenderingSystemrendering.rsInputSysteminput.rs)。这应该只是从主文件中复制粘贴并移除一些导入,因此可以直接进行。

我们需要更新 mod.rs,告诉 Rust 我们想将所有系统导出到外部(在这里是主模块)。


#![allow(unused)]
fn main() {
// systems/mod.rs
pub mod input;
pub mod rendering;
}

太棒了,现在我们完成了这些操作,以下是简化后的主文件的样子。注意导入后的 moduse 声明,它们再次告诉 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: 你可以在 这里 查看本示例的完整代码。