devlog #0 - A Roguelike for 2024 ?

Posted on Dec 29, 2023

Introduction

Over the last two months, I’ve been hooked on Tales of Maj’Eyal, playing it almost on a daily basis. Developed in 2012, its gameplay and engine derive directly from classical roguelikes, particularly Angband which is itself based on Moria.

The game is quite straightforward – you guide a top-down character through a 2D world. Navigating randomly generated levels, your goal is to defeat the final boss. Despite its apparent simplicity, its richness lies in the sheer number of playable classes and races, its difficulty modes and its various campaigns (some DLCs, arena mode, infinite dungeon mode, etc).

You can play up to 37 classes and 16 races

The game consists of inanimated sprites and an UI similar to many RPGS

I won’t delve more into that game, but I highly suggest checking it out. While playing, I pondered the idea of creating my own roguelike game, drawing inspiration primarily from it, but not only as I’m an avid player of related games. Here’s a list I recommend:

Project Overview

The project’s scope isn’t entirely clear at the moment, but my primary aim is to explore the extent to which I can develop my own roguelike. As it’s currently in the prototype phase, there’s no set plan for a release.

In terms of the tech stack, I went for Rust and Bevy for a few reasons:

  1. I’ve been eager to learn more about Bevy and the ECS paradigm since my last silly pet project.
  2. Many popular game engines come with intricate UIs which require time to get used to.
  3. Distributing the game could become an issue, as seen in the recent Unity controversy.

About the devlog

The idea with this devlog is to showcase recent changes made to the game. I think it can be interesting for others to see the process I am going through for creating a game and how new content/features are added. That being said, I won’t go too deep in the code itself, but I’ll try to provide links to the GitHub repository for the interesting parts.

This first entry is a little bit special. I didn’t want to start the devlog completely from scratch since I felt it would lack actual content to talk about. Therefore, I dedicated the month of December to building a first version that can be expanded upon (Hence the #0 for the entry). This way, I can tackle 2024 with a very fresh perspective! Regarding the posting frequency, I’ll try to post once a month, but I can’t promise anything.

First version

In every project, the most difficult part is the first step. Aiming too big from the start is the best way to give up, so I aimed very small.

My very first objective was to display a tileset image in a window. I spent about 5-10 minutes crafting 3 rather rudimentary 64x64px tiles in Gimp, arranging them into a single image (a tileset). Hopefully, Bevy provides its own TextureAtlas for working with this kind of resources (see related commit).

Step 1: Display a tileset image

For the next step, I created a first 10x10 map by spawning tile entities (with Bevy’s SpriteSheetBundle). The map was not displayed properly as Bevy’s coordinates origin is the middle of the screen (see related commit).

Step 2: Display individual tiles, forming a 10x10 map

I fixed the rendering by shifting all tiles in relation with the origin. This blog post written by Mike Chambers details how Bevy’s coordinates system work. Besides the map, there’s also the character’s tile showing up (see related commit).

Step 3: Fix the map rendering and display character sprite

The next logical step is to make the character evolves in its environment. I created a new tile to depict a stone, which functions as an impassable obstacle. Moreover, I implemented keyboard input capture for movement control, allowing the player to move in the specified direction. It includes obstacle detection, considering elements like stones or map borders (see related commit).

Step 4: The character moving around in the environment

I also took advantagee of Bevy’s states, which allows to execute some systems only when a state is active. I defined two type of states:

  • Application States: They differentiate between the loading assets state and the in-game state. This separation is crucial as there’s no need to run the game until all assets are fully loaded.
  • Game States: They are used for differentiating between various game phases, including level initialization, player turns, enemmy turns, etc.
use bevy::prelude::*;

#[derive(Debug, Clone, Copy, Default, Eq, PartialEq, Hash, States)]
pub enum AppState {
    #[default]
    LoadingAssets,
    InGame,
    Finished,
}

#[derive(Debug, Clone, Copy, Default, Eq, PartialEq, Hash, States)]
pub enum GameState {
    #[default]
    Uninitialized,
    Initializing,
    PlayerTurn,
    EnemyTurn,
}

Moving from one state to another is also trivial using the set method, for example once all assets are loaded:

fn check_assets(
    mut app_next_state: ResMut<NextState<AppState>>,
    mut game_next_state: ResMut<NextState<GameState>>,
    mut events: EventReader<AssetEvent<LoadedFolder>>,
) {
    for event in events.read() {
        match event {
            AssetEvent::LoadedWithDependencies { id: _ } => {
                println!("asset loaded!");
                app_next_state.set(AppState::InGame);
                game_next_state.set(GameState::Initializing);
            }
            _ => {}
        }
    }
}

Finally, the last worth mentioning things I added to this version are:

  • zoom-in/zoom-out using the mouse wheel
  • map’s grid displaying when pressing G
  • current’s turn displaying

Step 5: Display the turn number and the grid

Final result

Here’s a quick video showcasing all mechanics and features.

The source code is available on GitHub.

Closing Thoughts

If you want to support the project, drop a ⭐ on GitHub, which can directly make me stop procrastinating. Also, writing about my projects is still a new thing for me, so if you think I can improve in some way, please reach out!

Happy new year 2024!