#StandWithUkraine

Russian Aggression Must Stop


I wrote a (small) roguelike in Rust

2019/09/28

Tags: gaming programming game dev

Some time ago I wrote about doing some programming livestreams and mentioned that I was working on a roguelike in the Rust programming language. A few weeks back I decided that the project had reached its goals and I decided that I’d write a blog post documenting that particular programming journey and move on to other projects.

Now, just to temper some people’s expectations, my roguelike is rather basic. It is not visually flashy, nor does it have notable, unique gameplay mechanics. If I were to make any comparisons, I’d say it’s an even simpler (and more boring) Rogue (yes, the original one).

If you would like to dive straight into the code, you can find it in my GitLab repo.

You can also have a look at how it plays here:

Beginnings

The original inspiration for the project was a small dungeon crawler I wrote in Java for a university course on software engineering. It was even less flashy, the code was even more horrible and it was written in Java, so I won’t bother linking to the source code. However, I did have fun writing that project (and got a good grade too), so when I wanted to learn more about Rust, I figured reimplementing that project and maybe improving upon it would be a good exercise.

Since the Java project utilized a graphical user interface, I also decided that I’d just pull in the sprites I had made for that project and settled on SDL2 as my graphics and input library. Much of the Rust gamedev buzz seems to be focused on projects like https://amethyst.rs/ or https://ggez.rs/ but I think you can’t go too wrong with https://github.com/Rust-SDL2/rust-sdl2 particularly if you have some prior experience with SDL2.

OOPs, this doesn’t work

This project being my first bigger Rust project, I dove in maybe a bit naively at times. My Java project was quite object-oriented and I began by trying to essentially recreate the architecture of that project. I made heavy use of Traits to implement my inheritance relations and attempted to create a hierarchy of classes.

However, it didn’t take long before I started running into issues of ownership, since Rust is quite particular about who owns what data and when said data can be modified. In Java this is not an issue, you can easily have objects referring to each other and you won’t get much complaints. In Rust this is a big no-no.

I ended up trying to hack my way around the problem by introducing a lot of interior mutability through RefCell. RefCell is a wonderful tool that allows you to change the innards of an immutable object and the RefCell ensures only a single mutable version exists at any given moment at runtime.

However, RefCell also comes with a cost. Firstly, you need to create a fairly large interface of accessor methods for complex data structures, which gets annoying quick. Just because I write Java for my university courses doesn’t mean I like writing getters and setters. The second cost is that it can introduce fragility to your code base, because now some compile-time guarantees need to be upheld at runtime. More than a few times I ended up writing some code and getting it to compile only to realize when testing the program that what I had written was broken beyond all hope.

However, RefCells did allow me to get the basic functionality working and through enough testing, I did have objects interacting in such a way that the program wouldn’t be panicking constantly.

A pile of state too big for a shovel

My second bigger issue that got pretty close to toppling the entire project was how much centralized state I had ended up. Basically, I had a super-class of sorts in the form of my gamestate/mod.rs file, which was responsible for dealing with the level data, the player, all of my NPC creatures and now I was at a stage where I needed to bolt a menu system of some sort to allow the player to manage their inventory, along with other similar actions.

In my Java project I had just slapped this into my GameState.java class, but it was a big mess even there, complicating my rendering code and making things extremely inflexible. Doing it for a university course project was fine, since I had no intention of ever looking at that code after the course, but inflicting that pain on myself in my free time seemed unreasonable.

It’s a good thing I had done a course in game programming, because that course taught me about a tool that suited this particular purpose quite well. Namely, State Machines.

The idea is as follows: rather than trying to handle every possible case in a single place, we can code these various states in different places and write a system that allows transitions from one state to another. I also decided that each state should handle its own rendering and updating, so that I wouldn’t need to create a universal renderer that knew exactly what should be drawn and when.

So, I defined a State as follows:

pub trait State {
    fn draw(&self, graphics: &mut Graphics) -> Result<String, String>;
    fn update(&mut self, key: Keycode) -> Transition;
}

Then I wrote a Pushdown Automaton, which is a state machine that maintains a stack of States in a list and draws and updates them in the desired order.

pub struct StateManager {
	stack: LinkedList<Box<State>>,
}

impl StateManager {
    pub fn new() -> StateManager {
		return StateManager { stack: LinkedList::new()  };
    } 
        
    pub fn push(&mut self, new_state: Box<State>) {
		self.stack.push_back(new_state);
    }

    pub fn draw(&self, graphics: &mut Graphics) -> Result<String, String> {
        for state in self.stack.iter() {
			state.draw(graphics)?;
        }

		return Ok(String::from("ok"));
                    
    }

    pub fn update(&mut self, key: Keycode) {
		let mut state_opt = self.stack.pop_back();
                        
		match state_opt {
			Some(mut state) => {
				let transition = state.update(key);

				match transition {
					Transition::Switch(new_state) => self.stack.push_back(new_state),
					Transition::Push(new_state) => {
						self.stack.push_back(state);
						self.stack.push_back(new_state);
					},
					Transition::Continue => self.stack.push_back(state),
					Transition::Destroy => (),
                                                                                                                            
				}
                                                    
			},
			None => (),
		}
    }
}

This code remained mostly unchanged throughout the rest of the project and it was helpful in decoupling different aspects of game. The usefulness of the state machine was hampered by the strict hierarchy of my game code, though. Because the states were now separated, I needed to share data between them. An InventoryState, for example, needed access to the player’s inventory, but the inventory was also owned by the player. And then some state would need access to something else and I’d need to write a method for accessing THAT piece of data etc…

So, I was basically at war with my entire hierarchy every time I needed to add a new feature. I needed a way to be able to access basically any data anywhere as easily as possible without introducing unsafety, because what’s the point in using Rust if we are just going to go unsafe {} every time we run into difficulties?

ECS to the rescue

The solution came to me when I stumbled upon a talk by Catherine West called Using Rust for Game Development which talked about some of the same problems I was facing and how to use ECS, Entity-Component-System pattern for solving them. If you browse blog posts about Rust game dev you encounter the term “ECS” quite often and I was aware such a pattern existed, but until I watched the talk above I didn’t really understand how the ECS pattern worked, so I was really hoping to just make do with my hierarchy and work around the issues. But, as it turns out, the ECS pattern made everything significantly easier at least for this particular project.

The basic gist of the ECS idea is that rather than trying to create complex classes and class hierarchies, you focus on the individual pieces of data that constitute your World. This data forms your Components, which store the properties of your individual Entities. You then create Systems, which operate on the Components of your various Entities to transform them.

I could have picked a ready-made ECS library, like specs, but I decided that in the interests of learning how to use ECS, I should first make my own simple implementation. There’s a few ways people have gone about implementing the ECS pattern, but I settled for a similar design to the one in the talk I linked above: Entities are merely a unique identifier, more specifically a generational index (index number and a generation value) and my Components would be stored in a number of dynamic arrays. To access a given Component for a specific Entity, I would need to simply index into that Component type’s array with the Entity’s ID and check that the generation of the stored component matches the Entity’s generation.

To simplify the storing of a number of dynamic arrays, I pulled in AnyMap which simply allows me to store any data into a single place, so that I could more easily cope with the creation of new Component types.

The basic structure of the ECS I settled on after a few attempts was as follows:

pub struct ECS {
    pub entities: HashSet<Entity>,  
    recycle: RefCell<Vec<Entity>>,
    destroy_list: RefCell<Vec<Entity>>,
    create_list: RefCell<Vec<Entity>>,
    next_index: Cell<usize>,
    pub registered_components: Vec<TypeId>,
    pub components: AnyMap,
    pub delayed_components: AnyMap,
    pub delayed_removed_components: RwLock<HashMap<TypeId, Vec<Entity>>>,
    sync_closures: Vec<Box<Fn(&AnyMap, &AnyMap, &RwLock<HashMap<TypeId, Vec<Entity>>>)>>,
}

#[derive(Clone, PartialEq, Eq, Hash)]
pub struct GenerationalIndex {
    index: usize,
    generation: usize,
}

pub type Entity = GenerationalIndex;

pub struct GenerationalVec<T> {
    pub storage: Vec<Option<(GenerationalIndex, T)>>,
}

I wrote a small Clusterbomb example that demonstrates how the ECS is used. The ClusterbombSystem goes through every entity with a Clusterbomb component, detonates the bomb when the timer reaches zero, and creates additional sub-bombs around the bomb’s location:

pub struct ClusterbombSystem {

}

impl System for ClusterbombSystem {
    fn run(&mut self, world: &mut World) {
        let ecs = &mut world.ecs;

        let mut clusterbomb_components = ecs.get_components::<Clusterbomb>().unwrap().write().unwrap();

        let mut position_components = ecs.get_components::<Position>().unwrap().write().unwrap();
        
        clusterbomb_components.storage.iter_mut().for_each(|component| {
            if let Some((entity, clusterbomb)) = component {
                if ecs.is_alive(entity) {
                    let position = position_components.get(entity).unwrap(); 
                    clusterbomb.timer -= 1;

                    if (clusterbomb.timer <= 0) {
                        println!("BOOM!");
                        let mut rng = thread_rng();

                        for i in 0..clusterbomb.submunitions {
                            let x_delta = rng.gen_range(-5, 5);
                            let y_delta = rng.gen_range(-5, 5);

                            let mut sub_bomb = ecs.create_entity();
                    
                            ecs.set_component_delayed(&sub_bomb, Position { 
                                x: position.x + x_delta, 
                                y: position.y + y_delta });

                            ecs.set_component_delayed(&sub_bomb, Clusterbomb { timer: 1, 
                                submunitions: clusterbomb.submunitions - 1 });
                            ecs.set_component_delayed(&sub_bomb, Renderable { 
                                sprite: String::from("red_potion.png") 
                            });
                        }

                        ecs.destroy_entity(&entity);
                    }
                }

            }
                    
        });
    }

    fn post_run(&mut self, world: &mut World) {
        world.ecs.maintain();
    }
}

You can probably notice that there is a fair bit of extra complexity I had to introduce to the system that I didn’t mention when I briefly explained the basic workings of an ECS. The biggest problem with writing the ECS was how to handle the deletion and creation of entities and components during the execution of Systems. To ensure data safety, you cannot iterate over a Vec<> while modifying the contents of said Vec<>, so I had to create new lists for entities which had been queued for deletion or creation.

The Component side proved to be a fair bit more complex to manage. I wanted to have the ability to add Components or remove them from Entities during the execution of Systems. This allowed me to tag certain Entities in order to group them or to give them transient qualities, such as being unable to move. Because the Components were stored in the AnyMap, there was no easy way to synchronize the “delayed_components” AnyMap with the “components” AnyMap. So, I came up with a solution I was quite proud of with the “sync_closures” member.

Basically, rather than keeping track of which Component types I had stored in the ECS, upon registering a Component, I would also add a closure to the ECS that told the ECS what it needed to do in order to add or remove that particular Component type to the current components. In practice the registering function looked as follows:

    pub fn register_component<C: 'static + Clone>(&mut self) {
        self.registered_components.push(TypeId::of::<C>());
        self.components.insert(RwLock::new(EntityMap::<C>::new()));
        self.delayed_components.insert(RwLock::new(EntityMap::<C>::new()));

        self.sync_closures.push(Box::new(|components, new_components, removed_components| {
            let mut current_comps = components.get::<RwLock<EntityMap<C>>>().unwrap().write().unwrap();
            let mut new_comps = new_components.get::<RwLock<EntityMap<C>>>().unwrap().write().unwrap();
            let mut removed_comps = removed_components.write().unwrap();
            
            for component in new_comps.storage.iter() {
                if let Some((entity, comp)) = component {
                    current_comps.set(entity, comp.clone());
                }
            }
            
            if let Some(entities) = removed_comps.get_mut(&TypeId::of::<C>()) {
                for entity in entities.iter() {
                    current_comps.unset(&entity);
                }

                entities.clear();
            }

            new_comps.clear();       
        }));
    }

The code is a bit spiky with all of the locking and closure code, but I was quite proud of it regardless. I haven’t really used first-class functions that often and this was the first time I tried storing closures in a data structure in Rust, so I’m quite happy with it.

Because now the game data was laid out in flat arrays and all hierarchies were essentially eliminated or were at least made optional, I could relatively easily just create new Systems that operated on any pieces of datat I happened to need in any State that I happened to be in. This allowed me to prototype new functionality with ease, since Systems were isolated from one another and thus changes in one would rarely affect another. Just about the only time I needed to make changes across multiple Systems was when I modified my structure of some of my Components, but even then only minor changes were needed.

I did have to break this neat structure in a few places, however. I decided that writing separate AI Systems for the different creatures would have resulted in too much repeated code, so I wrote one AI System and stored a “brain” for the NPC in a Component that would decide the NPC’s next action:

#[derive(Clone)]
pub struct NPC {
    pub brain: Rc<Brain>,  
}


pub trait Brain {
    fn next_action(&self, world: &World, entity: &Entity) -> Action;
}

This meant that I had essentially two or more nested systems running when I processed my NPC entities, which meant I needed to be a little bit more careful about what data I could and could not modify. But, since I was able to partition my changes by Component, I ran into way fewer borrow checker issues and overall the process went smoothly.

Epilogue

Writing this roguelike game was definitely an interesting journey, even if the game doesn’t really measure to much compared to the other wonderful roguelikes people have made. In the end, the project weighed in at 5400 lines of code, making it the bigget project I’ve done in Rust to date and one of the biggest projects I have written in general. It was also a very good learning experience, since I was able to gain insight into Rust programming, game programming and even a bit of sprite drawing.

However, thinking back to the problems I went through, I think it probably would have been a bit more beneficial to start out with a slightly less complex genre of games, since writing a roguelike often introduces a rather wide scope. Although, it could also be that the inherent complexity of writing a roguelike dungeon crawler like this forced me to face the task of coming up with a usable and extendable architecture more so than hacking together a simple platformer might have.

This project also became my introduction to coding livestreams, which I think has been a positive for my confidence in my coding skills and I definitely will continue streaming my coding adventures well beyond this project. In fact, I have recently begun to stream the development of a twin-stick tank game I’m making in Godot Engine over at my Twitch channel. Hint hint.

Regardless, I don’t think I will really be working more on this project. The biggest flaws I see with the current version of the game is that it does not have nearly enough content to offer enough variety of experiences and playstyles and in order to fix that, I would need to spend more time coming up with new items and monsters than I am willing to invest right now.

However, should I one day decide to get a bit more serious about writing a roguelike, I am sure that this project has given me enough tools and understanding to make such a project a reality.

I hope this blog post has been of some interest to you and thanks to all the people who have tuned in to my coding streams and helped me come up with some of the functionality in Rust Roguelike.

Onward to new adventures!

>> Home