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.rs
│ │ ├── rendering.rs
│ │ └── mod.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 #[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; }
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 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 {}, )) } }
Now for the map loading.
#![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), } } } } }
Finally, we'll move the systems code into their own files (RenderingSystem to rendering.rs
and InputSystem to input.rs
). It should just be a copy paste from main with some import removals, so go ahead and do that.
We have to update the mod.rs
to tell Rust we want to export all the systems to the outside world (in this case the main module).
#![allow(unused)] fn main() { // systems/mod.rs pub mod input; pub mod rendering; }
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 /* ANCHOR: all */ // Rust sokoban // main.rs use ggez::{ conf, event, graphics::{self, DrawParam, Image}, input::keyboard, input::keyboard::{KeyCode, KeyInput}, Context, GameResult, }; use glam::Vec2; use hecs::{Entity, World}; use std::collections::HashMap; 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 */
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.