TrainDev is an open world train delivery game that focuses on the links that railways between communities provide. Taking place in the “Pre-apocalypse” or the run up to an apocalyptic event, the main gameplay loop is driven by rail navigation, resource collection, accomplishing tasks, and talking to NPCs. The vertical slice demo needed to contain some form of all of these elements, within a tight timespan of 2 months.
From a gameplay engineering standpoint, this requires a handful of necessary features, those being:
dialogue system
train character controller
third person character controller
quest system
custom character class
interaction system
In order to satisfy our stakeholders (team members, mentors, our “mock CEO”, etc), the game must have a handful of crucial elements.
The game must have a retro visual aesthetic calling back specifically to the dreamcast era (1999-2001) utilising a mixture of low-poly models and low-res photobashed textures
The setting of the game must be in the “Pre-apocalypse”, or the runup to an apocalyptic event (climate change, large scale industrial accident, de-industrialization)
The game must have a focus on building community and fostering human connection, rather than committing acts of violence and cruelty
Many post-apocalyptic games focus on the apocalypse being the “end of civilization” and ushering in an age of individualism where the player acts as an agent of chaos, we wanted to focus on the development of community that most post-apocalyptic settings take for granted.
one phrase that drove the design process forward was from our “mock CEO” Emily Rose, saying “the issue I have with apocalypse fiction is that it is more interested in exploring the end of the world before the end of capitalism”
Trains/Public Transit must be involved in some fashion
I faced a number of challenges attempting to implement all of these features in the three months we had to finish the project. There was a major gameplay shift halfway through development that could have significantly hampered our progress. In addition to this, I was also balancing development of the game with delivery work on the side to help bring some money in while I work on the project.
To accomplish this seemingly impossible task, I took the advice of one of the other programmers on the project, Alex, and used the open source dialogue system SUDS. SUDS provided a way to parse dialogue scripts and present them in an accessible data structure, as well as providing a way to create custom events to interact with external systems. Writing and debugging a parser and a custom dialogue markup language would have increased development time to an unacceptable degree, considering our development timeframe. To compliment this, I also created a quest subsystem to store and structure information about tasks the player had accepted, as well as to check quest requirements. In order to store all of the necessary data, I spent a significant amount of time modifying the custom character class made for the game, making it as generic as possible as it would be used for both NPCs and the player.
One of the main mechanics of the game involves talking to NPCs and exchanging resources between them and the player. For the first few sprints, my goal was to get this set up in the ATrainDevCharacter class so the core of the game could be established as soon as possible.
To trigger these functions in the game, I created custom SUDS Events to trigger within dialogue scripts, as resources can only transferred between characters. Using SUDS Events would prove to be a huge help in the future, as it reduced our reliance on hardcoded events and gave our quest designers more freedom to work independently of programmers and level designers.
For the dialogue system of the game, I decided to use an off the shelf open source plugin called SUDS (Steve's Unreal Dialogue System). SUDS provided a markup language for dialogue, an event system, global and dialogue specific variables, conditional statements, and a debug panel in the editor. To display the dialogue on screen, I grabbed the UI elements from a demo project using SUDS and hooked it up to display only when the player interacts with an NPC.
Note: The gif shown here is from early in development
To accomplish tasks such as advancing quests, adding gameplay tags to participants, exchanging resources, modifying player reputation, etc. I would make heavy use of the event system within SUDS.
All that the event system does is handle event names and parameters and sends them to the participant of the conversation that initiated the dialogue. These events then call methods on the respective participant (a Character) that then interact with either the quest system (through a reference to said system) or by calling methods on the character itself.
Characters are the only objects that are able to interact with each other to start dialogues. However in game, the towns that you interact with are just proxies that serve as a way for the player to interact with an NPC hidden within the town. This is an artifact of an early version of the game which had a third person perspective and saw the player exploring individual towns rather than exploring a larger world map via rail. These NPCs have a component with an attached SUDS script containing all of their dialogue. The only job of the component is to initiate dialogue on interaction. Events are only read by the participant who started the dialogue (the Player) and any other participant will not trigger events if they are defined. For the purposes of this demo, this is ideal since there are no multi-participant dialogues with 3 or more participants. Triggering events on multiple participants could prove to be problematic if this was the default behavior, so i'm glad that it isn't the default behavior.
For the Quest SubSystem, I had the option to use another open source plugin that would integrate neatly alongside SUDS, but opted instead to create a custom solution. The reason being I wanted to try my hand at writing a quest system from scratch since I had never done so before, and it seemed like a reasonably accomplishable task compared to making a custom dialogue system. The system works by reading a datatable of defined quest steps, and giving those quests to the player by keeping track of them in a TMap. The SubSystem has a handful of functions defined to add quests, update their progress, and check if their requirements have been satisfied.
To accomplish this, I would make the subsystem a GameInstanceSubsystem since the system only needs to be active during the lifetime of the play session. The characters in the world would then change the state of the subsystem to reflect the player’s quest status. Using a subsystem for this has a number of advantages over using a regular UObject. Getting a reference to the subsystem was easy to do in both C++ and Blueprints, and made updating the quest status both easy (integrated well with our existing code) and flexible (able to be called from blueprints).
The aim of the game here is to maximize code reuse, and minimize the amount of hard-coded elements related to quests and quest progression in the game world. Keeping this in mind, I also wanted level designers and quest designers to be able to call subsystem methods on a per-actor basis.
To accomplish this, Datatables are used as a designer-friendly way to define quests in a spreadsheet and import them into the game with as little friction as possible. A quest designer would define the steps of the quest in excel or google sheets (alongside resource requirements, reputation requirements, etc), export the file to CSV, and import the file as a datatable in engine. The system reads the datatable into a new data structure, and refers to said data structure throughout the game session. This was not initially done for any potential performance benefit, but rather because I did not know that there were multiple functions for accessing data from a datatable.
The structure of the datatable has an entry for each quest step, rather than an entry for each quest. Initially, the plan was to have each quest keep an internal counter of the number of steps required to complete the quest, and define the quest requirements in dialogue scripts. However, this proved to be problematic, as we couldn’t display any text that corresponds to the specified quest step without creating another external file, and negate any of the spatial benefits that a counter-based approach would provide.
As a way to enhance the designer experience of creating quests, I toyed around with the idea of making a Google Form as a way to ingest quest data through a series of questions. The solution was primitive, extremely limited by the capabilities of Google Forms, and was only somewhat effective at reducing the time it took to create quests. We never used it that much outside of testing, and it was only meant to be used if we decided to scale up production in the future. Whenever we made a change to the datatable structure, it would have required us to make changes to the form as well, and add a time cost to maintaining this tool that wouldn’t have made sense during our dev cycle.
However, this is an area I would like to explore in more depth in the future. Google forms can provide some useful metrics in the responses tab on the types of content being made by designers, and that data can be compared with data gathered from playtests and QA to show what players are actually engaging with.
Initially, quest dependencies were going to be checked in each relevant dialogue script through SUDS events. However, the limitations of SUDS events led me to take a different approach entirely. In order to gate progress through dialogue events: - an event would be called - the event would call a quest system function - the quest system function would return a value - the event handler would set a local variable - the dialogue would execute a conditional statement based on the event just called
For operations like checking gameplay tags, this works. You can even run the event at the start of the script, and refer to the variables changed throughout the script. These are all variables that are not going to change often within a single dialogue. However for quest dependencies, this can change within a dialogue, and can cause some serious issues if you aren’t careful. It makes more sense to move this check into the AdvanceQuest event and handle a fail state rather than perform it separately.
Halfway through development, we decided to make a radical change to the game. We shifted from the initial third person over-the-shoulder perspective with humanoid control scheme that we started development with to the railway traversal and train control scheme that defines the game in its current form. For most projects, a change this drastic would have ramifications for the entire project, and derail the production schedule entirely. However the impact of this change on my work was minimal due to quest and dialogue systems not being directly tied to the shift in perspective. The largest change I had to make due to the perspective shift at this period of time was changing how the interact action worked from the trace based solution to the current collision based solution.
The last thing of note that I did for this project was create a build of the game to demo at GDC on my steam deck. There isn't much to say about it apart from how easy it was to do after fixing a few blocking errors. Being able to demo the game anywhere was a huge benefit, because I could go from talking about the game all the way to demonstrating the game on a dime.
OpenGameDev was an incredible opportunity for me. Getting a chance to work with some incredibly talented friends of mine on a well organized project for 3 months, simulating the studio environment, and showing off a demo at GDC was exactly what I needed at that point. Development on my other projects had slown down significantly, and I felt little incentive to work on them any further. This project is exactly what the doctor ordered for me. If I could start it all over again, I’d take a closer look into datatables, and i’d make sure that I had a better understanding of how to get data out of them and into the game. I’d also use a switch statement for the event handler. Even though it works perfectly fine as is, I cringe a little bit whenever I use an if-else statement more than 3 times in a row.
I’m definitely going to be forking SUDS in the future so I can make some significant changes to it. Making the dialogue scripts more code-like, overhauling events to be able to return values and be evaluated in conditional statements, adding support for gameplay tags (and more importantly, custom participant types).
I learned a lot about Unreal Engine 5 while working on TrainDev, and I feel way more confident in my ability to use the engine than I did when I was first learning it in late 2022. This project felt like a real test of my ability to communicate effectively within a team, solve problems where they crop up, and create a product that people love. This is what I always wanted my senior capstone project to be, make a game that feels unique and tells a story from a perspective that no one else has in the industry right now. It just so happens that this is exactly what the industry needs right now.