NAME
Thijs Bouwhuis
ROLE
Games Programmer
RESUME
Arid
A Hardcore Survival game set in the Atacama desert, made in Unreal Engine 4 using C++ and blueprints.
School project, 32 weeks (september 2020 - june 2021) with 22 team members (4 programmers)
Roles : Lead programming, Games programmer
Arid is a gritty, exploration-survival experience that challenges players in surviving the most arid place in the world. By using your skills and adaptation, you must face the loneliness, the extreme temperatures, and mysteries of the Atacama Desert.
Systems and features I worked on: Narrative System, Saving and loading, Interaction system, Animation implementation and the Day/Night cycle.
Narrative System
Arid is a story-driven game and needed a narrative system to allow writers and designers to create narrative sequences without any need to code or use blueprints. I created a data-driven narrative system linked to a google sheets document, to allow writers to more easily work on adding narrative and not having to have the engine open while writing narrative.
I started out with defining a struct which included the data that would be needed for a single narrative line.
Line is the subtitle text that needs to be shown when the narrative line is played.
Delay is a time in seconds that will be used to wait after the audio has completed before the next narrative line will be ran. This will allow designers to give the player more time to read the subtitles.
Audio is the name of the fmod event that will be played for this narrative line.
AudioEvent is the fmod event that will be played for this narrative line. This event will be filled in automatically through C++ based on what is filled in int he audio FString.
EventName is the name of the blueprint event that will be activated at the start of this narrative line. This allows designers to add additional functionality and fire events from narrative lines.
BlueprintEvent is a bit confusing, It is an array of blueprint event classes. This variable is an array because google sheets can't link blueprint events to a single class(for some reason), but it can add blueprint events to an array, so this will always be an array with 0 or 1 event. This event will be executed when the narrative line they are linked to is ran (this will be showed later on).
This struct can be used to create datatables which will represent narrative sequences (Multiple narrative lines ran after each other). These datatables will be filled by data written out in a google sheet document, the filling of these datatables will be explained later on in this post.
Now I needed something to actually process the narrative data, show the subtitles and run the events. I created a NarrativeManagerComponent which will be a component of the player. This way narrative sequences can easily be ran from every blueprint by getting the player, then the component and then calling the BeginNarrative function.
The BeginNarrative accepts a datatable with with the previously struct as data. I first make sure that the player is alive and that there's no narrative currently running. If there's none currently running, we fire the PlayLine function which is recursive and will be ran over and over until all the lines of the narrative have been ran
(Click on the image to zoom in)
I first set the subtitle of the current narrative line to the narrative widget, then play the Fmod audio event of the current narrative line and
I check if the narrative line has a valid narrative event. If it's valid, I spawn the actor with no collisions and Run it's narrative event(at the end of it's event it destroys itself). Finally I set a timer with as time(how long to wait before the function delegate gets executed) the duration of the audio file added to the delay for the current narrative line with as function EndLine.
EndLine is the function that will be executed after the timer runs out. This function will increment the current line, check if the next line is valid and execute the run next line if it is, making the LoadNarrative function recursive. If the next line is invalid, the narrative component will be cleared and made ready to accept the next narrative sequence.
Now I'll explain how I filled the data from a Google Sheets document into the datatable we defined at the start of this post. The google sheet structure is as follows:
The Audio and eventname are string variables that I have added to the datatable. These contain the names of the audio files and blueprint event actors that need to be put into the datatable, but can't be added directly to the sheet. We will use their names to find them in the editor later on.
I start by running a tool made by the tools programmer of the team that works on this project, link here. This tool downloads the google sheet as a csv file and converts it into a json file.
then we import the json file into the datatable using the unreal native FDataTableImporterJSON class. After this is done, all values except for the audio files and events have been added to the narrative datatable. To put the audio and blueprint files into the datatable we need to use the asset registry.
Using the asset registry we get all rows inside the datatable, then iterate through them and through reflection we cast each individual row to an array of dialogue lines. Each row equals a dialogue sequence.
Then we iterate through each array element and get the string that represents the name of the audio file for each element. Then we iterate through all files inside the audio folder and try to find an Fmod event with a corresponding name.
If we can find an Fmod event with the same name as the "audio" cell in the google sheets document, we fill this Fmod event into the AudioEvent slot inside the narrative datatable. After that we continue with the next dialogue line until an audio file was tried to be found for every dialogue line.
Finding and filling in the blueprint events is a little more complicated.
First we get all blueprint subclasses for the event baseclass that we defined and save it in an array.
Then we get all the rows inside the datatable and iterate through each individual dialogue line, just like we did for the fmod events.
After that we iterate through the array of all subclasses of the event baseclass, load them in and check if they have the same name as our eventname variable defined in the Google Sheet document. If we can find a subclass with the same name as the eventname cell in the Google Sheet document we add this blueprint
subclass into the blueprint event array inside the narrative datatable (can't directly set a class as variable, for some reason it needs to be an array). After that we continue with the next dialogue line until an event tried to be found for every dialogue line.
Climbing animation system
In this video I tapped the up and down keys instead of holding them. Even though I tapped keys the movement was still fully completed.
I created a state machine for the climbing animations to ensure that the climbing animations will always be fully played to their end and won't blend weirdly halfway when the player stops movement input. I think that this can be extended into locomotion to achieve a responsive start/stop system with smooth blending (e.g. using the correct foot in a turn) and I will be working on creating this in the near future for my personal project.
Full Blueprint Code:
(embed is a little buggy at times, the page might need to be refreshed if it doesn't load properly)
I started out with defining the states and the animator in my team made an animation for each state. The states for this state machine are as follows:
1. None, The first state when the player enters climbing.
2. IdleLeft, Idle with left hand resting above the right.
3. IdleRight, Idle with right hand resting above the left.
4. LeftSwingUp, Moving left hand from under to above the right.
5. RightSwingUp, Moving right hand from under to above the left.
6. LeftSwingDown, Moving left hand from above to under the right.
7. RightSwingDown, Moving right hand from above to under the left.
This state machine will be the base of the climbing system. There is one animation for every state.
When we enter the state machine, the first state of the machine is None. Since the first state is None, we play a ClimbStart animation.
The ClimbStart animation has an animation notify at the end of it. This notify will call a function that makes the state machine go into idle left (set the state to idle left and play the according animation), since the climb start animation ends with the player's left hand above the right.
The idle left animation is a fairly long animation which has an animation notify every few frames. Whenever this notify gets triggered we call the following function.
This function checks if the player wants to move up or down on the rope based on a variable called climb movement, which is a number between 1 and -1, depending on if the player input is up or down. If there's no input the variable is 0, which will cause it to continue playing the idle animation (which is a looping animation).
If there is input, the next state depends on if the player wants to move up or down. In this example the player wants to move up, so the Climb Movement variable is 1, which causes the Right Swing Up function to be called.
This function sets the state to RightSwingUp, plays the according animation and starts a movement timeline.
This movement timeline is a graph, with as X value time that the animation takes to play and on Y value the amount the player moves up (negative value for the player to move down). This makes the animation have realistic movement, since it's not a constant movement speed and allows the animator to ensure that the movement and animation are synced up.
At the end of each swing animation there is an animation notify which calls the main function of this system, ClimbMontageEnded.
This function runs the current animation state through a switch case. The same functionality as described earlier in the idle function exists each swing state in the switch case. In this example the animation state is
Right swing up. after the switch case running through the switch case we check if the player input is still up, or changed to no input or down.
If it's still up, we call the LeftSwingUp function which in turn sets the animation state to LeftSwingUp, plays the according animation and starts the movement timeline.
If the player stopped giving input, the state machine goes into IdleRight(Which behaves exactly like IdleLeft, but goes into different states) because the right hand is above the left.
Finally, if the player wants to move down, we call the RightSwingDown function, which again sets the animation state to RightSwingDown, plays the according animation and starts the down movement timeline.
Player Animation system
The animation system that is used in the gameplayis a big state machine. Based on the current state of the player certain actions can or can not be performed(E.g. The player can't take up the torch while inside the sandstorm).
The animation state dictates what the player can and can't do and what functionality is executed. For example, if the player tries to drink, the player presses the drink button, which calls the drink function in the animation blueprint.
Then the animation blueprint checks if it's in a state which the player can drink in. If this is true, the animation plays and drinking functionality executes(the player gains hydration and the flask loses a water charge). The pro of creating functionality that is
blueprint driven in this way ensures that functionality will never play if the animation doesn't play. So the player will always get proper visual feedback if an action is happening or not through the animation. The con of this system is that if the animation system gets bugged or locked
the player can't perform actions anymore because the player is stuck in a state that they can't perform other actions in.
A good example of this system is putting the torch away, called TorchDisappear which is called by pressing the torch button while holding the torch.
This function first checks if there player is in the torch animation state, then puts the montage the player is currently in(torch holding montage) to it's final section which is putting away the torch and updates the animation state to torch appearing/disappearing.
Finally we bind an event to when the montage is finished(when the player has fully put away the torch).
When the torch is fully put away in the animation we also extinguish the torch and unequip it in terms of backend functionality. After that we unbind the previously bound event.
Finally we check if the player is inside a sandstorm. If the player is, the animation state should go into sandstorm state and the player should play the sandstorm animation, if not we go into locomotion, from which the player is allowed to perform a multitude of actions.