Torchlife

Software Used: Unity C#, Blender 2.8
Solo Project

Torchlife is a game of endless adventure with a dangerous twist. While exploring unlimited dungeon rooms, you must keep your torch lit or face being plunged into complete darkness! At the start of each level there is a fireplace to relight your torch, but beware, monsters and traps lurk at every corner.

Torchlife has evolved from an older project called "Escape From Odin" when I decided to improve on old code. I updated code with many challenges, but in the process learned many valuable skills.

Procedural Dungeon Room Generation System

In every Torchlife level, the rooms are procedurally generated. It uses an additive generation system where each room has the ability to create another room at each of it's exits. This continues until a certain number of rooms is reached, which then leads to all open rooms sealing off it's exits with a wall. Most of the work went into room collision detection (detecting if any rooms intersected with each other), which was one of the greatest challenges of this system. The generating part was by far the simplest part while the collision detection system was the trickiest part. The finished product involved passing a test where a 100,000 room building was generated with zero room collisions. The video on the right shows how rooms are generated (top down and perspective views shown). Each room exit has a spawner which determines which room to spawn next. If there are no more rooms to be spawned, the spawner will spawn a wall to seal off the dungeon.

A more detailed look on how rooms are generated.

This is a spawner object. It is responsible for spawning a room or a wall off of the exit of another room. It was created as child object of the room to the right of it. If the spawner is inside of another room, then the spawner is destroyed because there is already a room in that location. If the spawner is inside of another spawner, then every spawner, except for one, is destroyed.

This is the sensor that is attached to the spawner in the first note. It is responsible for scanning in front of the spawner to determine what the maximum size is for spawning a room. If the sensor collides with another room, then only a single size room can be spawned. Else, a double size room can be spawned. As larger room sizes are created in the future, more sensors will be added.

This is a spawner who’s sensor (see 4.) is inside of another room. The sensor sends back information to the spawner telling the spawner to limit generation options to only single size rooms.

This is a sensor which is inside of another room. Because of this, it will send information back to it’s spawner (see 3.) about the maximum size that a spawner can generate.

Several scripts were used to make the dungeon generation system. Below is one method from the spawner script. This method determines which room type to spawn in this particular area. The room type depends on multiple factors: If any rooms need to be force generated, if there is enough space to spawn all room sizes, and weighted chance.

 1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
    /* This method determines what room to spawn. 
     * Details: When a spawner determines what room to spawn, it first references
     * it's sensors to determine what the maximum size that it can spawn is. 
     * If the sensor has detected a collision, then the spawner can only
     * spawn a single size room. Then, it determines a room based on weighted
     * chance. 
     * This happens unless force generation is detected at this 
     * particular sequence. (more details on that in the method)
    */
    public GameObject CalcRoom(GenData genData)
    {
        /*
         * This handles if any rooms need to be force generated.
         * forceGenData takes data from genData about force generation.
         * Force generation generates rooms that are specified to be generated in a specific sequence.
         */
        List<Vector2> forceGenData = genData.forceGen.forceGenRooms;

        if (forceGenData.Count != 0)
        {
            foreach (Vector2 data in forceGenData)
            {
                //If the sequence count matches the force generation sequence.
                if (genData.currentRooms == data.y)
                {   
                    /*
                     * Determines the room ID to be force generated.
                     * 0-99     : Single room
                     * 100-199  : Double Room
                     * 200-299  : Trap Room (single size)
                     * 1000-inf : Special Room
                     * */
                    if (data.x < 100 && data.x >= 0)
                    {
                        return genData.roomGeneration.singleRooms[(int)data.x];
                    }
                    else if (data.x >= 100 && data.x < 200)
                    {
                        return genData.roomGeneration.doubleRooms[(int)(data.x - 100)];
                    }
                    else if (data.x >= 200 && data.x < 300)
                    {
                        return genData.roomGeneration.trapRooms[(int)(data.x - 200)];

                    } else if(data.x >= 1000) {

                        return genData.roomGeneration.specialRooms[(int)(data.x - 1000)];
                    }                    
                }
            }
        }

        //If the sequence number is the final sequence number, return the final room.
        if (genData.currentRooms == genData.maxRooms)
        {            
            return genData.roomGeneration.specialRooms[1];
        }

        //Weighted chances based on GenData chances which were specified in the inspector. 
        int dat0 = genData.roomGeneration.chances[0];
        int dat1 = genData.roomGeneration.chances[1];

        //This is the value which will be compared against the weighted chances.
        int randomChance = Random.Range(1, 101);

        //Single Room
        if (randomChance <= dat0)
        {
            int size = genData.roomGeneration.singleRooms.Length;

            //There are different types of single rooms, so pick a random one. 
            int retInt = Random.Range(0, size);

            return genData.roomGeneration.singleRooms[retInt];
        }
        else if (randomChance <= dat1) //Double Room
        {
            if (maxSize == 1)
            {
                int size = genData.roomGeneration.doubleRooms.Length;

                //There are different types of double rooms, so pick a random one. 
                int retInt = Random.Range(0, size);

                return genData.roomGeneration.doubleRooms[retInt];
            }
            int size2 = genData.roomGeneration.singleRooms.Length;

            //There are different types of single rooms, so pick a random one. 
            int retInt2 = Random.Range(0, size2);

            return genData.roomGeneration.singleRooms[retInt2];
        } else //Trap room
        {
            int size = genData.roomGeneration.trapRooms.Length;

            //There are different types of trap rooms, so pick a random one. 
            int retInt = Random.Range(0, size);

            return genData.roomGeneration.trapRooms[retInt];
        }
    }