Roguelike Core – Open source code base for roguelike in unity

  • Post author:
  • Post category:Gamedev

Background

When searching the web for roguelike unity all you can find is unity’s own roguelike2D tutorial. While the tutorial did teach me a few things, and is overall very good, there are a few things that turned me off, and made me look elsewhere for my roguelike development. Most notably is their use of a coroutine to execute an action, and guessing how long it will take to resolve.

While searching for knowledge of how to implement better roguelike base code I came across A turn based game loop by Bob Nystrom. This comprehensive article contained everything that I needed to write my own implementation of a roguelike game loop. Since it is entirely based on someone else’s work, I immediately wanted to make this open source so everyone could benefit from it, like I benefitted from this article.

Introduction

Everything I made for this project can be found on github. What I want to do here is go over the core of the code. My aim is to make you familiar with it so you an easily use it yourself and extend it for your own projects.

I will not go into the reasoning and design of the code, since it is explained in full in Bob Nystrom’s article.

GameLoop.cs

The game loop is in charge of going over each actor and ask them which action they would like to perform. This is done in the update method, and no more than one actor will perform an action per frame. Even if you have no animations at all, this is fast enough that the player won’t even notice any wait.

private void PerformAction ()
{
    currentAction = GetCurrentActor().GetAction();
    if (currentAction == null) return; //Should only happen for player actor
    while (true)
    {
        ActionResult result = currentAction.Perform();
        if (result.Succeeded)
        {
            if (!GetCurrentActor().HasEnergyToActivate(currentAction.EnergyCost))
            {
                Debug.LogError(currentAction +  "Perform should check if actor has sufficient energy, and return an alternate if it does not. Defaulting to rest action.");
                currentAction = GetCurrentActor().GetRestAction();
            }
            GetCurrentActor().SpendEnergyForActivation(currentAction.EnergyCost);
            break;
        }
        currentAction = result.Alternate;
    }
}

This is the interesting part of the game loop, the action selection. This is pretty much a direct implementation from the article. What you need to know is that action.Perform() is in charge of checking if the actor has enough energy to perform the action, but if it does not check it properly, the game loop double checks this and defaults to a rest action. Do not rely on the game loop to do this check for you, as it means that action fallback will always be to rest action, even if a cheaper action is possible.

The game loop will then spend the energy if it is successful, and break the loop, which will finish the frame.

The last thing you need to know is that Actors are in charge of registering and unregistering themselves from the GameLoop. I’m aware this is coupled code, but it is the simplest way to make sure you don’t accidentally double up on Actors.

BoardManager.cs

BoardManager is in charge of storing the static board, along with the positions of all the Actors, and if you’ll want to add pickups and other components, here is the place to do so.

The board itself is a grid of chars, currently P denotes passable. I imagine pretty much anyone using this code will change it, so I didn’t bother making it terribly pretty. What you need to know is that IsPassable only considers static terrain on the grid, and will ignore all other Actors on the board. IsOccupied and GetOccupied checks if the spot is occupied by another Actor. This is mostly because of the way I use the A* pathfinding, up in the next section.

BoardManagerAStarCost.cs

This is the implementation of AStarCost that uses the Board Manager to find out if a spot is passable. To learn more about this implementation of A* you can see my article on how I added A* to unity’s roguelike tutorial project.

The implementation also works with diagonal movement, but this is currently disabled in AStarNode2D.cs allowDiagonal=false. Feel free to turn it on, but be aware you’ll have to modify the action cost of movement.

This implementation of A* finds a path between two points, including the starting position and end position. So if an Enemy is at 0,0 and the player is at 0,2, and the Enemy is finding a path to the player, it will return [(0,0),(0,1),(0,2)]. And this is why it needs to consider the spot the player actor is on as passable. This is also important because of the way action delegation works. The enemy will try to attack 0,1 and if it is empty it will move there. Then it will try to attack 0,2 and since the player is there, it will attack.

Note that I recalculate the pathfinding on every step. If this becomes too computationally intensive, you could cache the result, and first check if the player is still on the path, and then start finding the path from the end.

Action.cs

The base Action class which you will need to extend in order to add more actions to your game. The only method you need to overload is Perform().

When Perform() is called, you should check if the actor has enough energy to perform the action, and if not, which action would be a good alternative. Usually, if you don’t have enough energy, you’ll want to rest, which regains 1 energy. However, in the case of an attack action, I first check if I can perform the action, and if not I move instead. This is because if a move action is cheaper than an attack, then I might be able to perform it even if I don’t have energy for an attack.

The perform action should either change the state to executing, and then change to finished during the update process, or change it immediately to finished, like in rest action.

When you return a failed action result, you attach an alternate action to it. The current delegation is Attack -> Move -> rest. Make sure everything eventually defaults to rest, as it’s the action you can always perform to regain 1 energy.

RestAction.cs

When you add this script to an Actor, you can change the amount of energy it would regain from a rest. I suppose you could do some interesting things with changing this on the fly. Otherwise, it could be used to make different increments of the energy recovery loop.

AttackAction.cs

The attack action requires a weapon component to be attached to the Actor. It delegates almost all of the logic to the weapon, and as such is very flexible.

Weapon.cs and LinearWeapon.cs

A weapon needs to implement two methods. AcquireTarget(IntVector2 direction), which which takes a direction as input, and returns whether or not that attack direction is valid. And UseWeapon() which applies the attack.

Linear weapon will check if there is any destructible target in the direction and within range. And UseWeapon will apply its damage to the first destructible object in it’s path. So a LinearWeapon script can be used for bump attacks of any kind, including ranged weapons.

Destructible.cs

Don’t forget to add this to all Actors that can be dealt damage. This class also automatically notifies all other scripts that they should Unregister using SendMessage. If you wanted to implement a death animation, the Die() method would be the place to do so.

EnemyActor.cs

EnemyActor is very simple. I uses A* to find the path to the player, then tries to attack everything in it’s way, delegating to movement if there is nothing to attack. Since the delegation happens in the attack action, you might want to implement a different enemy attack action if you want enemies that cycle between movement and attacks consistently.

PlayerActor.cs

Lastly, the player actor. The first thing we do is check if it has enough energy to take an action, otherwise we rest. This is because we only have one cost and one type of action available. If you wanted, you could allow the player to take a rest action by choice each loop. That way they could make many different actions, but waiting 2-3 loops between each movement is tedious, so beware. If you allow resting by choice, you’ll also probably want to make a maximum energy cap, otherwise players could store up on energy and then go on a rampage.

Final words

Hope you find this useful, and I’ll be happy to be notified if you use this code in any of your projects!

The code is completely free to use, and under the public domain Creative Commons 0 license. Free free to share change, use, and even claim you wrote it, though that would be a dick move. If you want to give attribution, please link to www.timeshapers.com

Follow me to see how I develop this core into my own roguelike.

Aurore

Aurore is a competitive KeyForge player and the founder of Timeshapers. She's a content writer by trade and aspiring game designer. Follow @Timeshapers1