Overview

Tenebris is a 2D platform endless-runner (procedurally generated) in which a cultist fails an invocation ritual and is now chased by Tenebris, a giant entity with the only purpose of killing him.
The gameplay features classic systems and mechanics of its genre, a player in an endless run for its life while avoiding obstacles using its abilities, the Dash and the Grapple.

What I have learned

Tenebris was my first big project using Unity and it’s the one where I had the opportunity to really hone my skills about C# and knowledge about the engine by the application of everything I learned, in particular:

  • Game design patterns:
    • Singleton pattern;
    • Factory pattern;
    • Object Pool pattern;
    • Publisher/Subscriber pattern;
  • Procedural Content Generation - WFC Algorithm

Event Driven Approach - Event Manager

During the development of this project I made extensive use of the Publisher/Subscriber design pattern.
For this I implemented an EventManager (Singleton) class (click here to see the script) as the broker. This holds a dictionary of string (eventNames as keys) and Action (C# delegate); through it objects can subscribe their callbacks methods to it using the correct event name string.
I found myself really comfortable using this event driven approach because it made the development of the project much faster and I achieved a good level of decoupling, making the whole architecture more modular and maintainable.

Here you can find a more recent version of my event manager.

I decided to subdivide the event manager into three different classes, one that triggers the event without passing any parameters, one that accept a parameter and the last one that accepts two parameters.

Two Parameter version

public class EventManagerTwoParams<T1, T2>
{
    private static EventManagerTwoParams<T1, T2> instance;
    public static EventManagerTwoParams<T1, T2> Instance
    {
        get
        {
            if (instance == null)
            {
                instance = new EventManagerTwoParams<T1, T2>();

                instance.Init();
            }
            return instance;
        }
    }

    private Dictionary<string, Action<T1, T2>> paramEventDictionary;

    private EventManagerTwoParams() { }

    private void Init()
    {
        if (paramEventDictionary == null)
        {
            paramEventDictionary = 
              new Dictionary<string, Action<T1, T2>>();
        }
    }

Publisher example

EventManagerTwoParams<bool, int>.
  Instance.TriggerEvent("onExample", true, 5);

Subscriber example

OnEnable()
{
    EventManagerTwoParams<bool, int>.
      Instance.StartListening("onExample", ExampleFunction);
}

OnDisable()
{
    EventManagerTwoParams<bool, int>.
      Instance.StopListening("onExample", ExampleFunction);
}

ExampleFunction(bool exBool, int exInt) { }

Procedural Content Generation

Simple implementation of the Wave Function Collapse algorithm

After studying about the WFC algorithm I wanted to try and make something with it so I decided to take care of the PCG during this project.
Unfortunately, due to the simple nature of the game, in the end I opted for a really simple implementation of it, that still follows the general idea of the WFC algorithm, but works only in the right direction.
First some facts about the game,the player does not move but we give the illusion of movement by moving the background and the “chunks”.

Tenebris chunk movement

This is a container in which the LevelAssembler (the script that handles the procedural generation), places the level asset chosen by the algorithm (click here to see the script).
Starting from the initial level asset, the algorithm takes all the possible level assets (possible neighbors) that this level can be connected to and picks one according to its probability to spawn.
I then reset the probability of the chosen level asset and increase the probability of all the level assets that were not chosen; this allows a good distribution of all the level assets among each run.
The chosen one is then spawned beside the previous one and the process repeats itself starting from all the possible neighbors this newly created level has.

Level Scriptable Object

A level asset is a scriptable object with which the design team was able to create new levels in a quick and easy way; in particular what they had to do was create a new asset and specify a LevelID (an enum that held every level created so far), the level prefab, its intended difficulty and probability and a list of all the possible level it could connect to.

Game Systems

Game Progression System

The game progression system is strictly related to both the procedural content generation and the enemies since those are the two areas of the game that changes over time:

  • Every X seconds all the enemies “level up”, becoming faster at falling or at chasing the player;
  • Every Y minutes, the base probability of all levels of easy and medium difficulty decreases and the probability of all levels of hard and insane difficulty increases.

Both increase and decrease until a cap value and all these values are easily customizable by the design team.

Corruption System

The corruption is the counterpart of health for the player, it is represented by a bar that fills a bit each time the player gets hit by an enemy or gets too close to the shadow that is chasing it but also each time the player uses one of his abilities.
The value of corruption slowly decreases if it has not incremented for some time but only if it did not reach its max value, when this happens the player’s abilities are less effective and most important another hit from an enemy could kill it, causing game over. The corruption value stays at its max for some time before resetting to zero.

Key Mapping System

During the final stages of the development I wanted to challenge myself and I started researching a way to implement a system to let the user remap the keys at will, since Unity does not have a native one. It requires:

  • an enum for each “action type” (ex. “jump”, “dash”),
  • a dictionary of action types and key codes
  • a Coroutine that, when the rebinding starts for a specific action type, waits for the key code input by the user and exchanges that in the dictionary. Before ending the coroutine the input controller gets updated with the new key code for that action type.

I decided to go store everything into a dictionary to always have an updated reference on what key code is bound to an action type, since during the tutorial the text needs to show the correct key when explaining to the player how to perform a specific action.

If you want to take a look at the code you can find it here!

Tenebris rebind keys

Enemies

After finishing the PCG I decided to focus on the enemies of the game and in particular on their class architecture, how to spawn them and their behaviors.
Since the scope of the project was small I decided to just make an abstract class containing the basics:

  • the logic to apply the damage to the player;
  • the self-destruct coroutine.

I developed three kinds of enemies all with simple but entertaining behaviors:

  • The Runner, that runs towards the player copying its movements from the right side of the screen;
Tenebris Runner
  • The Chaser, that chases the player from the left side of the screen slowly accelerating until it outruns it;
Tenebris Chaser
  • The Lurker, that lurks over the player’s head before crashing to the ground.
Tenebris Lurker

Factory and Object Pool Game design pattern

To optimize the spawn and destruction of the enemies I used the Factory game design pattern in conjunction with the Object Pool pattern.
I created an object called ManifestationFactory that at the start of the game creates three object pools, one for each enemy type and handles the activation/deactivation of the enemy gameobject when requested.
The spawn request is made by the spawn of a specific trigger that the design team placed in the level during the creation of the prefab, this allowed them complete and autonomous control over where each enemy should be spawned.

You can find the link for my implementation of these patterns here for the Factory one and here for the Pool one (line 10 - 95).

Dialogue Tool

I also took care of most of the UI in the game (main menu, pause and gameover screen) in regards to transitions, sounds and logic.
In particular I developed a dialogue box that, given a list of lines to show, cycles through each line and prints them letter by letter.
To aid the design team in the creation of the tutorial dialogues I prepared two objects to use in conjunction:

  • A GameObject with a simple trigger and a “tag” string;
  • A ScriptableObject where the design team should input;
    • A “tag” (that should match the trigger tag)
    • A position on where to spawn the dialogue box
    • A list of all the lines that dialogue box should show
Tenebris Dialogue