Adventure Rogue-like

About Game

Adventure is a 2D roguelike, complete with procedurally generated maps, perma-death, a quest system, and a party system. Originally, this project was assigned over 5 weeks in my first software development class at Guildhall. I later decided to take the game further by continuing development over an 8-week directed focus study, and during my own time outside of class. My goal with development was to move beyond the standard roguelike formula, and give the player a sense of growth from quest to quest. I worked to achieve this by developing my map generation system, and modeling gameplay after my favorite arc in the popular Dungeons and Dragons podcast “The Adventure Zone.” The breadth of this project allowed me to explore many areas of interest for me – including procedural generation, AI, UI, and system design - and will continue to be a great project to return to and polish into a finished game.

In adventure, the player spawns into a large map, filled with both friendly and hostile entities. The player must explore the map to complete a main quest, which will bring them to the next encounter. But, the player can spend more time searching to complete side quests, to grant them more equipment, stats, and even new party members. As the player moves through encounters, the maps get progressively harder, with more and more high-level enemies spawning. The player must survive through as many encounters as possible, until their entire party dies out.

Specs

  • Engine: Personal
  • Language: C++, OpenGL




Game Features


Map Generation


Maps in Adventure are generated through a sequence of generation steps, defined in data. For this system, I implemented a large number of generation step types, which, when used in tandem, can create exciting, organic 2D maps. To expand on this system, I added a masking system, which allowed generation steps to be run on a sub-area of the main map. These subareas can be defined as rectangles, circles, or perlin noise ranges. This system is extremely buildable – I can easily add new generation steps, and quickly see results on the screen.

Generation Step Description
Mutate Checks every tile, and has a random chance to change tile to a defined type (if that tile meets the parameters)
FillAndEdge Fills the area with one tile type, and edges it with another
CellularAutomata Changes a tile based on its neighbors.
SpawnItem, SpawnActor, Spawn Decoration Adds entities to the map. Can specify what kind of tiles to spawn on, or which tags the entity can or cannot spawn on.
RoomAndPath Creates rooms, and generates paths between them. Can modify how many rooms, how many times rooms can overlap, how straight the paths should be, how large the rooms are, and what tiles the rooms/paths are made of. Useful for creating man-made looking structures like villages.
FromFile Loads an image from file, and parses it as tiles, using the tiles defined chromakey.
SetSubArea Sets a subarea for all generation steps beneath it.
EndSubArea Returns the subarea to the current subarea’s parent subarea.
Submap Runs all map generation steps from another defined map. Can be used in a subarea to generate a small map within a much larger map.

Organic Tile Edges

As I worked on the map generation more, I found that the maps were starting to look ~really good~ when I was zoomed out and looking at the entire map. But on the actual game screen, while walking around, they still looked very unpolished, because of the harsh edges between tiles. The Liberated Pixel Cup tileset that I was using came with an incomplete set of cosmetic edge tiles for most tile types, so I decided to add a pass after map generation to add these cosmetic overlays appropriately to the procedural terrain.

Liberated Pixel Cup edge tile format

Conceptually, this is just a very specific cellular automata problem. However, because this tileset was missing an edge for every configuration (for instance, a grass tile with only one neighboring grass tile), I couldn't just set the rule for every configuration - there would definitely be invalid tiles on the map, and I'd have to deal with them somehow.. Additionally, I needed a way to determine which edge to place between tile types - should the grass edge go on the sand tile, or should the sand edge go on the grass tile? What about areas where three different tile types meet on one space?


The first step is to establish what kind of edge a tile should have on it. In data, I defined a terrain height for all terrain tiles, and use that to determine which direction an edge should "flow". So now I could know in data, that between grass and sand, grass edges should go on top of sand tiles. Now, for each tile, I can look at the neighboring tiles, and establish what higher-level edge is most common to determine the edge type. Once I know what kind of edge to place, I can look at a set of rules to determine which edge should be placed - or at least, which one is most suitable.

<TileDefinition
    name="Grass"
    spriteCoords="6,31|7,31|7,31|8,31|8,31|8,31|8,31|8,31"
    chromaKey ="0,255,0"
    allowsSight="true"
    allowsWalking="true"
    allowsFlying="true"
    allowsSwimming="false"
    isTerrain="true"
    terrainHeight="4"
    edgeDefinition="Grass"
    terrainLayer="Ground"
    groundLayer="Grass"
/>
  
<TileDefinition
    name="Flowers"
    spriteCoords="18,0|18,0|19,0"
    chromaKey ="255,255,0"
    allowsSight="true"
    allowsWalking="true"
    allowsFlying="true"
    allowsSwimming="false"
    isTerrain="true"
    terrainHeight="4"
    terrainDefinition="Grass"
    edgeDefinition="Grass"
    terrainLayer="Ground"
    groundLayer="Grass"
/>
.
.
.
TileDefinition.xml: Determines which layer a tile should be in, and the height of the terrain. Note that both Grass and Flower tiles point to the Grass edge definition - while flowers are cosmetically different and could easily have different affects in game, they should still be treated like grass for the purpose of placing these edges.

Now that I could find an edge to place, I had to plan for situations where there was no clear, single answer for the best edge. I decided to prioritize by the type of terrain, and have a separate sprite layer for each type - edges between ground and water/lava should be highest priority, because there is a gameplay difference between water and ground. After that, it looked best to have the grass layer be on top of any other terrain, regardless of whether it was sand or dirt. The third layer are all of the low priority edges between different types of dirt or sand - nothing necessarily looks out of place if these layers are swapped around, and small inaccuracies in the placement of edges in this layer are pretty unnoticable. To try and minimize this slim chance of strange looking tiles, I decided to add an initial pass to clear invalid configurations, so that the map started out as clean as possible.

So, my pass for edging tiles ended up looking like this:

  • Clear Invalid Tiles
  • Add "Tufts" where there are single high-level tiles entirely surrounded by lower-level tiles. (i.e., a single patch of grass)
  • Add Ground Layer->Water Layer and Ground Layer->Lava Layer edges
  • Add Grass Layer->Earth Layer edges
  • Add Earth Layer->Earth Layer edges according to terrain height
While there are still a few tiles in every map that look a little off, the benefit of having a procedurally generated world that actually looks smooth and organic is well worth it.

Adding edges: click left and right to step through the edging process for the maps.

Quest System


It was important to me to add a robust quest system to the game, to really get the feel of an RPG. The game has both main quests and side quests, where completing the main quest will progress the player to the next encounter, and completing side quests will grant the player various rewards, if completed. Quests have 3 components: Quest steps, Quest giver, and Quest Reward.

bool Quest::UpdateAndCheckIfComplete()
{
    if (!m_isComplete){         //if you haven't already completed this quest, check the steps
        bool allComplete = true;
        for (int i = 0; i < m_conditions.size(); i++){
            VictoryCondition* condition = m_conditions[i];
            if (!condition->m_complete){        //if the condition has not already been completed
                if (!condition->CheckIfComplete()){     //updates and checks for completion
                    allComplete = false;
                } else {
                    if (m_questGiver != nullptr){
                        //update the quest giver, if the quest was given by an actor.
                        m_questGiver->AdvanceQuest();
                    }
                    if (m_definition->m_isSequential){
                        //if the quest should be completed in order, unlock the next step in the quest (if there is one)
                        if (i + 1 < m_conditions.size()){
                            condition->m_active = false;
                            m_conditions[i + 1]->m_active = true;
                        }
                    }
                }
            }
        }

        if (allComplete){
            CompleteQuest();
        }
        return allComplete;
    }
    return m_isComplete;
}

Quest.cpp: quest checks for completion every frame

The quest steps are a series of conditions, which are updated every frame. These conditions can be killing a specified actor type, collecting an item, or speaking to an actor. Every frame, the game checks if any active quest steps have been completed, and advances the quest if they have. Once all steps have been finished in a quest, the quest reward is distributed. Quest steps, called “VictoryConditions” in code, are a pure virtual class, and all of the unique quest functionalities inherit from that class.

The Quest Giver is how the quest is started – now, the quest giver is either “none” (the player starts with the quest information) or an actor of a specified type. If the quest giver is an actor, the map will spawn an actor of that type on the map, and assign the quest to that actor. The player can then talk to the actor to learn more about the quest, and progress the quest.

class QuestReward{
public:
    QuestReward(){};
    QuestReward(tinyxml2::XMLElement* questRewardElement) {};

    virtual void GiveReward(Quest* quest) = 0;

    static QuestReward* CreateQuestReward( const tinyxml2::XMLElement* conditionElement);
};

class QuestReward_Item : public QuestReward{
public:
    QuestReward_Item(const tinyxml2::XMLElement* questRewardElement);

    void GiveReward(Quest* quest) override;

    ItemDefinition* m_itemToGive;
};

class QuestReward_Stats : public QuestReward{
public:
    QuestReward_Stats(const tinyxml2::XMLElement* questRewardElement);

    void GiveReward(Quest* quest) override;

    Stats* m_statsToGive;
};

class QuestReward_Ally : public QuestReward{
public:
    QuestReward_Ally(const tinyxml2::XMLElement* questRewardElement);

    void GiveReward(Quest* quest) override;     //assumes that the actor to add to the party is the quest's giver
};

QuestReward.hpp: Virtual class that different reward types inherit from and implement their own functionality

The quest reward is a triggered once all steps in the quest have been completed. QuestReward is a pure virtual class, which individual rewards inherit from. Each child class implements it's specific functionality, including how to give the player the reward for that kind of quest - currently quests can award items, permanent stat increases, or party members. This architecture allows the quest rewards to be stored ambiguously as a QuestReward* within any QuestDefinition, but maintains the correct functionality when a quest is completed.


Back To Top

Party System

In each encounter, the player can complete a special side-quest to gain an ally in their journey. Allies spawn with a quest near villages, and send the player on lengthy missions. Once you’ve completed the quest, the ally will join your party, and fight with you. The player can swap between controlling members of their party, and can equip any member of the party with equipment.

//In gameplay, the player can swap by pressing '[' or ']' (which calls this function with swapDirection as -1 or +1)
 void Party::SwapPlayer(int swapDirection)
 {
    //update the current player
    int size = m_partyMembers.size();
    //increment player index - add size to keep it >0 for mod
    m_currentPlayerIndex = (m_currentPlayerIndex + swapDirection + size) % size;		
    Actor* oldPlayer = m_currentPlayer;
    m_currentPlayer = m_partyMembers[m_currentPlayerIndex];					//set the new player
    m_currentPlayer->m_isPlayer = true;
    oldPlayer->m_isPlayer = false;

    //update all party members to be following the new player
    for (Actor* actor : m_partyMembers){
    	actor->SetFollowTarget(m_currentPlayer);
    }
 }
Party.cpp: swapping the active player updates the entire party's state

In code, the player’s party is made up of a list of Actor entities, with a pointer to the current controlled actor, and an index for where that actor is in the array. Switching between party members is as simple as incrementing that index, and modding the result to loop through the list. The player’s inventory is shared across all party members – whenever an actor joins the party, their entire inventory is added to the party. This system allows for any number of actors to join the party, and easily distribute equipment as the player sees fit. Though, for gameplay purposes, the number of party members should be limited.


Quest Dialogue


A potential ally giving a quest


An ally joining your party.


Your party in combat.


Future Plans

Adventure is a broad game, and has lots of room to grow. I plan on continuing to work on it throughout my time at guildhall, eventually polishing it into a finished game. In the immediate feature, I'm looking to accomplish the following tasks:

  • Save system
  • Melee combat
  • Character creation
  • Ally dialogue generation
  • Timed encounters
  • Tutorial level
  • Thematic elements
  • My final vision for adventure is a rogue-like game modeled after the "Stolen Century" arc in the popular dungeons and dragons podcast, "The Adventure Zone." In the arc, the players are pulled between parallel universes by a mysterious power, and are constantly pursued by an evil, enigmatic force hunting for that power. In Adventure, I will try to foster that sense of mystery by bringing the player into a new world in each encounter, each with its own characters and quests to explore. I believe that a stronger main arc for each encounter (i.e., timed encounters, a "homebase" for the player in each world, etc.) would add greatly to the player experience in adventure.