Modules
The main file is getting quite big and as you can imagine, that will not be very sustainable as our project grows. Luckily, Rust has the concept of modules which will alow us to nicely split out functionality based on concerns into separate files.
For now, let's aim for this folder structure. Eventually as we get more components and systems, we'll probably want more than one file, but this should be a pretty good place to start.
├── 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: Read more about modules and managing growing projects here.
Let's start by moving all the components into a file. There should be no changes apart from making some fields public. The reason why we need to make the fields public is because when everything was in the same file everything had access to everything else, which was convenient to start with, but now that we have split things out we need to pay more attention to visibilities. For now we'll make the fields public to get things working again, but there is a better way which we will discuss in a later section. We've also moved the components registration at the bottom of this file which is quite handy when we add components we only need to change this file.
#![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>(); } }
Now for the resources.
#![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()) } }
Next up, let's move the constants into their own file. For now we are hardcoding the map dimensions, we need them for the movement to know when we've reached the edge of the map, but as an improvement would could later store the dimensions of the map and make them dynamic based on the map loading.
#![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; }
Next up, entity creation code is now into an entities file.
#![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(); } }
Now for the map loading.
#![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), } } } } }
Finally, we'll move the systems code into their own files (RenderingSystem to rendering_system.rs and InputSystem to input_system.rs). It should just be a copy paste from main with some import removals, so go ahead and do that.
Now the interesting thing about systems is that it's a folder with multiple files inside. If we do nothing else and try to use RenderingSystem
or InputSystem
in main we will get some compilation failures. We will have to add a mod.rs
file in the systems
folder and tell Rust what we want to export out of this folder. All this bit is doing is it's telling Rust we want the outside world (the world out of this folder) to be able to access RenderingSystem and InputSystem types.
#![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; }
Awesome, now that we've done that here is how our simplified main file looks like. Notice the mod and use declarations after the imports, those are again telling Rust that we want to use those modules.
// 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) }
Feel free to run at this point, everything should work just the same, the only difference is now our code is much nicer and ready for more amazing Sokoban features.
CODELINK: You can see the full code in this example here.