
Introduction
A few weeks ago I began working on a roguelike game.
It is something that I have wanted to do for quite a while, but could never find the time.
I am learning Rust right now and the best way for me to do that is to find a project that I care about.
So, this is a perfect opportunity finally make a roguelike.
Theme/Core Mechanic
I have not yet decided on a solid theme/core mechanic.
I recently read through the manga “That Time I Got Reincarnated As A Slime” in which the main character is stabbed in the first episode and as he dies there is a back and forth with a voice that responds to his thoughs granting him abilities.
The end result is him becoming a slime with the ability to consume enemies to gain their abilities and analyze things he consumes to combine or recreate them.
I want to incorporate something similar into my roguelike.
My ideas right now are:
- on death reincarnate with abilities based on how you died/what you accomplished
would this be easily cheesable? - absorb enemies to gain abilities
make non-permanent/overwrite with new abilities - mimic absorbed enemies
get their abilities and maybe enemies of same type don’t attack
Prototypes
Before I rushed into it I wanted to make prototypes of a few different systems that I though would be needed. I wanted to make some sort of name generator, map generator, and find a good library to use for graphics.
Name Generator
The most common solution I found to name generation was to get a bunch of names I like, stick them in a Markov chain, and generate names with it. This seemed to work well enough for me.
Map Generator
After reading about a bunch of different map generation algorithms I decided to use the alogorithm described by Bob Nystrom here.
1. place non-overlapping rooms randomly
2. fill the empty areas with mazes
3. fully connect the regions (rooms and mazes)
4. remove the dead ends of the mazes
I have made some tweaks to the maze fill algorithm to reduce the windy-ness since these screenshots were taken, but I didn’t want to pull out my current generator and make a wrapper to output each step again.
Graphics Library
While trying out some of the different rust terminal libraries I came across a talk by Josh Ge from Roguelike Celebration “How to Make a Roguelike”. This led me to the r/roguelikedev community and a rust version of the libtcod tutorial.
Roguelike Tutorial
I went through the first few parts of the tutorial to get familiar with the basics of libtcod and to get some ideas for how I want things to be structured.
I then made some changes so that it would be easier to add things in the future.
Generic Objects
I liked the idea of using a generic object system from the tutorial, but I did not like the idea of having to write a new function for each object and tile.
I created ObjectStore
and TileStore
structs that would load object and tile definitions from json files on creation.
When I need to add a new object or tile it is just a new line in a json file instead of a new function and recompile (which can be very slow in rust).
player object definition
{
"name":"player","x":0,"y":0,"char":"@",
"color":{"r":255,"g":255,"b":255},
"blocks":true,"alive":true,"open_doors":true,
"fighter":{"max_hp":30,"hp":30,"defense":2,"power":5}
}
wall tile definition
{
"name":"wall","blocked":true,"block_sight":true,"explored":false,
"light_color":{"r":130,"g":110,"b":50},
"dark_color":{"r":0,"g":0,"b":100}
}
Event System
I made an Event
enum that would be returned from many of the Object
methods.
Example: The Object
move method can return one or more Moved
, ReplaceTile
, Attack
, or Message
Events.
pub enum Event {
// x, y, tile
ReplaceTile(i32, i32, Tile),
// x, y
Moved(i32, i32),
// x, y, dx, dy
Moving(i32, i32, i32, i32),
// x, y, type, amount
Attack(i32, i32, String, i32),
// type, amount
TookDamage(String, i32),
// x, y, name
Died(i32, i32, String),
// player quit
Exit,
SkippedTurn,
Message(String),
// player used up stairs
GoUp,
// player used down stairs
GoDown,
}
Traits
I defined traits for things that I knew would need to be changed in the future.
pub trait MapGenerator {
fn generate(&mut self, tile_store: &TileStore,
object_store: &ObjectStore,
floor: i32) -> ((i32, i32), Vec<Vec<Tile>>);
}
pub trait Renderer {
fn draw_tile(&mut self, x: i32, y: i32, color: Color);
fn draw_char(&mut self, x: i32, y: i32, char: char, color: Color);
fn draw_bar(&mut self, x: i32, y: i32, width: i32, label: String,
value: i32, max: i32, foreground: Color, background: Color);
fn draw_message(&mut self, x: i32, y: i32, message: &String);
fn clear_panel(&mut self);
fn clear_xy(&mut self, x: i32, y: i32);
fn clear(&mut self);
fn show(&mut self);
fn closed(&self) -> bool;
fn cleanup(&mut self);
fn is_fullscreen(&self) -> bool;
fn set_fullscreen(&mut self, fullscreen: bool);
fn get_key(&mut self) -> Key;
}
pub trait AI {
fn take_turn(&self, x: i32, y: i32, map: &Map,
object_store: &ObjectStore) -> Vec<Event>;
}
The current implementations of each trait are:
Trait | Implementation | Description |
---|---|---|
MapGenerator | MazeMap | The algorithm described above |
MapGenerator | CaveMap | Cellular Automata method described here |
Renderer | TcodRenderer | Implementation for libtcod |
Renderer | TermionRenderer | Implementation for termion (terminal ui library) |
AI | RandomAI | Randomly moves around and attacks |
Game Loop
My game loop is currently:
- Render all of the tiles, objects, and ui elements (stats and messages)
- Update player effects (currently just regenerate health)
- Handle controls and return events from player actions
- Run AI for each npc object
- Process events returned by player and npc objects
- Replace tiles (happens when a door is opened)
- Apply damage to objects
- Go up or down a level
- Add all messages to the message log
Changing Levels
I have created a MapStore
struct that contains a HashMap<i32, Map>
and added a HashMap<i32, Vec<Object>>
to the ObjectStore
to store the map and objects for each level.
When the player uses an up or down stairs I store the current map and objects, check if data exists for the new level, and either retrieve it if it does or generate it if it does not.
I am now trying to decide what I want to do about levels after the player leaves them.
The options I am thinking about right now are:
- Do not do anything, keep things as they were when the player was last on the level (current behavior)
This is the least work, but I am not sure it would make for the best gameplay. - Continue processing events for all existing levels
This would allow me to have npcs change level to chase the player or run away.
Depending on how much stuff is going on this could also kill performance (dwarf fortress fps death!). - Continue processing events for just the previous, current, and next levels
Would still let me have npcs change levels, but without as much of a performance hit.
Also, I am wondering if I should pregenerate the maps and npcs intead of doing it when the player reaches a new level.
TODO
This week I will be working on the ui. I want to add a ui struct on top of the Render trait so that I don’t have have every renderable item keep track of its position on screen.
This is where I am at right now. I made this post so that I could put my thoughts in one place and hopefully get some advice on different aspects that I am having trouble with.
Here are some screenshots of the current game