模块化

main.rs文件已经太大了,随着功能的增加,这样下去还会越来越大,越来越难于维护.怎么办呢?还好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

MORE: 想了解更多关于模块的知识可以点 这里.

接下来我们就开始把每一个组件放到一个文件中.放到单独的文件中后除了要把属性声明成public的,也没什么不一样的.之所以现在需要把属性声明成public的是因为先前都在一个文件中,刚开始可以都放在一个文件中,但随着文件越来越大我们就需要把代码拆分到不同的文件中了,为了保证不同的文件(模块)间还能互相访问的到就需要把属性声明成public的,这样代码就不会报错了.我们后面也会介绍另外一种拆分方式.另外把注册组件的代码放到文件的下面.拆分好后如果需要修改或者增加组件只需要修改对应的文件就可以了.


#![allow(unused)]
fn main() {
// components.rs
use specs::{Component, NullStorage, VecStorage, World, WorldExt};

// Components
#[derive(Debug, Component, Clone, Copy)]
#[storage(VecStorage)]
pub struct Position {
    pub x: u8,
    pub y: u8,
    pub z: u8,
}

#[derive(Component)]
#[storage(VecStorage)]
pub struct Renderable {
    pub path: String,
}

#[derive(Component)]
#[storage(VecStorage)]
pub struct Wall {}

#[derive(Component)]
#[storage(VecStorage)]
pub struct Player {}

#[derive(Component)]
#[storage(VecStorage)]
pub struct Box {}

#[derive(Component)]
#[storage(VecStorage)]
pub struct BoxSpot {}

#[derive(Component, Default)]
#[storage(NullStorage)]
pub struct Movable;

#[derive(Component, Default)]
#[storage(NullStorage)]
pub struct Immovable;

pub fn register_components(world: &mut World) {
    world.register::<Position>();
    world.register::<Renderable>();
    world.register::<Player>();
    world.register::<Wall>();
    world.register::<Box>();
    world.register::<BoxSpot>();
    world.register::<Movable>();
    world.register::<Immovable>();
}
}

下面是资源文件:


#![allow(unused)]
fn main() {
// resources.rs
use ggez::event::KeyCode;
use specs::World;

// Resources
#[derive(Default)]
pub struct InputQueue {
    pub keys_pressed: Vec<KeyCode>,
}

pub fn register_resources(world: &mut World) {
    world.insert(InputQueue::default())
}
}

接下来我们把常量也拆分到一个单独文件中.先前地图的维度信息是在代码中硬编码的,最好是根据加载地图的维度动态设置.


#![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;
}

接下来把创建实体的代码放到entities.rs文件中:


#![allow(unused)]
fn main() {
// entities.rs
use crate::components::*;
use specs::{Builder, World, WorldExt};

// Create a wall entity
pub fn create_wall(world: &mut World, position: Position) {
    world
        .create_entity()
        .with(Position { z: 10, ..position })
        .with(Renderable {
            path: "/images/wall.png".to_string(),
        })
        .with(Wall {})
        .with(Immovable)
        .build();
}

pub fn create_floor(world: &mut World, position: Position) {
    world
        .create_entity()
        .with(Position { z: 5, ..position })
        .with(Renderable {
            path: "/images/floor.png".to_string(),
        })
        .build();
}

pub fn create_box(world: &mut World, position: Position) {
    world
        .create_entity()
        .with(Position { z: 10, ..position })
        .with(Renderable {
            path: "/images/box.png".to_string(),
        })
        .with(Box {})
        .with(Movable)
        .build();
}

pub fn create_box_spot(world: &mut World, position: Position) {
    world
        .create_entity()
        .with(Position { z: 9, ..position })
        .with(Renderable {
            path: "/images/box_spot.png".to_string(),
        })
        .with(BoxSpot {})
        .build();
}

pub fn create_player(world: &mut World, position: Position) {
    world
        .create_entity()
        .with(Position { z: 10, ..position })
        .with(Renderable {
            path: "/images/player.png".to_string(),
        })
        .with(Player {})
        .with(Movable)
        .build();
}
}

下面是地图加载的代码:


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

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

最后我们再把渲染代码放到randering_system.rs文件中,把输入处理代码放到input_system.rs文件中,其实也就是复制粘贴改下导入语句.

现在还有个有意思的事,在一个文件夹下包含了多个文件.如果不做些其它操作在main.rs文件中要使用RenderingSystem或者InputSystem程序会报错的.咋办呢?只需在文件夹下添加一个mod.rs文件告诉Rust当前这个文件夹下包含那些内容.这样在外部就可以访问RenderingSystem和InputSystem了.


#![allow(unused)]
fn main() {
// systems/mod.rs
mod input_system;
mod rendering_system;

pub use self::input_system::InputSystem;
pub use self::rendering_system::RenderingSystem;
}

齐活了!现在再看main.rs是不是清爽多了!注意我们用了一些mod告诉Rust需要用到的模块.

// main.rs
// Rust sokoban
// main.rs

use ggez::{conf, event::{self, KeyCode, KeyMods}, Context, GameResult};
use specs::{RunNow, World, WorldExt};
use std::path;

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

use crate::components::*;
use crate::map::*;
use crate::resources::*;
use crate::systems::*;

struct Game {
    world: World,
}

impl event::EventHandler<ggez::GameError> for Game {
    fn update(&mut self, _context: &mut Context) -> GameResult {
        // Run input system
        {
            let mut is = InputSystem {};
            is.run_now(&self.world);
        }

        Ok(())
    }

    fn draw(&mut self, context: &mut Context) -> GameResult {
        // Render game entities
        {
            let mut rs = RenderingSystem { context };
            rs.run_now(&self.world);
        }

        Ok(())
    }

    fn key_down_event(
        &mut self,
        _context: &mut Context,
        keycode: KeyCode,
        _keymod: KeyMods,
        _repeat: bool,
    ) {
        println!("Key pressed: {:?}", keycode);

        let mut input_queue = self.world.write_resource::<InputQueue>();
        input_queue.keys_pressed.push(keycode);
    }
}

// Initialize the level
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 main() -> GameResult {
    let mut world = World::new();
    register_components(&mut world);
    register_resources(&mut world);
    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)
}

至此拆分模块的任务就完成了,运行下代码应该跟先前的功能是一样的,但代码没那么臃肿了也方便后续添加新功能了.

CODELINK:这里获取当前完整代码.