top of page

The other

Singleplayer - Unreal Engine C++

The other is a game that was made as part of the course 8GIF225 – Video game production workshop I. To do this project, we were a large team of 4 programmers as well as a dozen artists from NAD-UQAC .

 

This game is a serious 2.5D platformer style game with bipolarity as its main theme. The game contains two characters, Juliet who is controlled by the player and her friend Romeo who is controlled by an artificial intelligence. In addition, Romeo is a bipolar person who has to go through several hardships. Juliette must help her friend Romeo to go through the various puzzles that will be on their way and this, during different seasons. Each season will affect Romeo differently in his bipolarity like the majority of bipolar people. During the fall, Romeo is in a state of "mania" and will take the lead without thinking too much and it will be up to Juliette to accompany him and help him continue their journey together. Then, during the winter, Romeo will be in a depressed state. He will be slow and unmotivated. He will have to follow Juliette and she will once again have to help him through the various obstacles on their way.    

​

As a programmer, I mainly dealt with the programming of the artificial intelligence (AI) of Romeo in the different seasons as well as the different actions performed by it such as: jumping, bending, waiting, walking, etc.  

Romeo's AI

AI initialization

After making some prototypes before starting the project, our team of programmers agreed to use a "behavior tree" as well as a "blackboard" to make Romeo's AI in C++. So I took care of the configuration and initialization of these elements. In particular by adding the necessary modules in the build file and by adding the keys for the blackboard. I then created the "AI Controller class" of Romeo's artificial intelligence as well as its "character class". I also made functions which consist of initializing the behavior tree and the blackboard at the start of the game.

Challenges encountered

The main challenge I encountered for initialization was that I had never worked with behavior trees or blackboards before. So I had to do some research online to find out how to set this all up in c++. This process was relatively difficult because the majority of the information on the subject is for systems in blueprint and not in c++.

Romeo's AI

The PatrolPath and Waypoints System

During the fall season, Romeo must move energetically and semi-autonomously. Indeed, during Romeo's period of mania, Juliet must be able to follow him throughout the level. This is why I developed a system of waypoints. Romeo will have a "patrolPath" assigned to him. This path will consist of several points that I named "waypoints". Waypoints are placed manually on the map and will constitute Romeo's journey. In addition, when the AI will reach the last waypoint, it will return to the first waypoint of the route and redo the route or wait at the last one if desired. It will be up to Juliette to hold his hand to help him progress and to help him continue on his way by taking a different path, because the fall level will have several small paths.

First, I started by creating a class for the patrol path and making a reference to it in the AI character. The patrol path has several features to allow it to be adapted to different puzzles directly in the editor. Indeed, in addition to being able to add as many waypoints as you want in the table, it is also possible to decide whether or not you want to "loop" the AI through the path. This is achieved using a "checkbox" which represents a Boolean value. The patrol path also has a "collision sphere" that moves to each waypoint location and it moves to the AI's next path when the AI collides with it. The goal being that when Romeo crosses one of these "collision spheres" while holding Juliet's hand, the index of the next waypoint to go will increase. This makes it possible to update the waypoints when Romeo performs another task in the behavior tree, that is to hold the hand. Without such functionality, Romeo would attempt to retrace his steps to get to the waypoint position he was at before holding Juliet's hand. The size of the "collision sphere" can also be modified according to the path, to adapt it to the environment and optimize the chances that the "event is triggered" when the two characters will pass near a waypoint while holding each other hand. Finally, iIt is also possible to observe the current index of the AI from the editor to be able to observe its path and better understand its next destination by testing the paths during production.  Also, it's important to note that I decided to increment the path index by 2 and not 1 when colliding with the handhold. I did this in order to make the path more permissive if the player doesn't collide with all the spheres. 

Then I had to make 2 classes that were going to be in my behavior tree. That is, a class to identify the waypoint of the path as well as a class to control the incrementation of the index of my path each time the AI reaches a waypoint to then indicate the next one to it. Indeed, the index corresponds to a waypoint located in a table and it is to this waypoint that the AI must go.  Also, between these two classes, I use the function " move to" from unreal engine. 

behavior tree patrol path .png

Behavior tree for the waypoint system

In the code of my class used to find the position in the world of the next waypoint in the index, I made myself two very useful blackboard keys, the "patrol_path_index" which is an "int" which corresponds to the current index in my behavior tree as well as the key "patrol_path_vector" which is a "vector" which corresponds to the position in (x,y,z) of the current waypoint in the world. It is the value of this key which is then sent to the "move to" function so that the AI goes to this position using the pathfinding of unreal. 

It is finally after having found the waypoint and having successfully performed the "move to" of the AI at the position of the vector that I increment my path index for the next waypoint. When the last waypoint is reached, I look at the boolean value of the path which indicates if the path "loop" or not. If the value is false then the AI will wait at that last index, as the direction will now be equal to "Wait" and this will cause the index to no longer increment. If true, then the direction remains equal to "forward" and the index will continue to increment.

Challenges encountered

The challenges encountered for this system revolved around the mechanics with the holding of the hand. Indeed, I had to adapt my mechanic according to another mechanic (hand holding) which was in the same behavior tree as my waypoint mechanic. As mentioned earlier, to solve this problem, I added a "collision sphere" and moved it from one waypoint to another. The challenge was then to manage if the collision of the AI with the sphere was during its "standard" patrol path navigation, or during a hand hold with Juliette. To then perform the desired behavior accordingly. The last challenge was to properly manage the incrementation of my index so as not to create cases of "array out of bound" and that the path is incremented correctly when colliding with the tenage of main. Finally, it is this system which turned out to be the most difficult in this project and which caused me the most bugs and adjustments. However, it is also the system I am most proud of and find the most versatile in many different situations. So I'm very proud of what I managed to accomplish with this system. 

Romeo's AI

The Wait gate system

I created a system of "wait gates" which allows to control the waiting time of the AI when it arrives at a specific place. I created this system for several reasons, such as: to make it easier to control Romeo's behavior in the various puzzles and to give Juliet the opportunity to catch up with Romeo if he gets too far ahead (or if she takes too late). The "wait gates" are also managed by the behavior tree and the AI blackboard. 

In the header of my "WaitAI" class, I added a "collision box" which launches an "event" when a character will collide with it. I also put a "float" which allows to specify the AI waiting time in the "blueprint instance" of the "wait gate" in the editor. 

Wait AI capture set time.png

In the source code, I initialize my "collision box" and its "onBeginOverlap" event. If the AI collides with the "gate", then I access the AI's "controller" and call the "OnBeginTime" function with the desired time as a parameter. If Juliet enters the "collision box" and the "controller" is not equal to "NULL", it means that Romeo passed through the "gate" before her.   Then I call the "OnEndWaiting" function with the value "0" as a parameter to stop making Romeo wait. If Juliette is the first to enter the "collision box", then nothing happens. 

In my code, I use an "FTimerHandle" in the "NPC_AIController" class (Romeo's "controller" class) to manage the AI wait time. Indeed, I start and stop the stopwatch in my functions according to the time received as a parameter and I "set" the time in the blackboard key "is_waiting". When this value changes it starts the event in the behavior tree. The sequence of waypoints then continues when the time of this blackboard key is equal to zero again. 

Challenges encountered

The challenges with this system were to make it as smooth as possible with player behavior. Indeed, depending on the player's way of playing, the waiting time was never perfectly adapted. Because if the player took their time in a section and the AI waited for X number of seconds, then the AI would run out at the end of their timer and the slower player would lose sight of Romeo. However, if the player was faster and arrived at the "wait gate" it delayed the player, because he had to wait for Romeo to finish "his little break". To fix this, I made it so that no matter how long the wait gate was waiting, it would be destroyed when the player collided with it. This way, the AI can wait a very long time if necessary and as soon as the player gets near Romeo, he stops waiting. Also, if the player arrives before Romeo in the "wait gate" it is destroyed even before Romeo arrives, which will ensure that he will not wait at this same section and will continue his journey without incident and without having to make the player wait unnecessarily.

Romeo's AI

The AI crouching system

I also made the code allowing Romeo to lean ("crouch"). To do this, when the "StartCrouching" function is called, I halve the size of Romeo's "collision capsule" and reduce its speed by half. I then call the function of the same name in its parent class, the "ActiveCharacter" class. I then do the reverse process to get him to stop bending over.  

preuve crouching romeo.png
Challenges encountered

The only notable challenge with this system was fixing the bug where the nav mesh wouldn't go below the lower spots the AI had to lean under. Then, after some research on online forums, I discovered that it was enough to uncheck the "can ever affect navigation" checkbox which is normally checked by default in unreal engine. Obviously, without unchecking this box, the AI could not have passed below because an AI can only work on the nav mesh.

Romeo's AI

The AI path change system

The fall level is made up of several small "patrol paths" with their own peculiarities. However, it needed a mechanic to change Romeo's path dynamically during the game when Juliet takes him to a certain place while holding his hand. That's why I created the "ChangePatrolPathAI" class. The basic principle is substantially the same as the "Wait gate" class seen previously. However, instead of specifying a time, we specify a path to the instance of this class in the editor as shown in the screenshot below. The indicated path will replace the AI's current path when it enters the "collision box" regardless of whether it is holding Juliette's hand or not. 

preuve changement path.png

In the code, after the collision, I "set" the new path to the NPC (to Romeo). Then, using the "controller" of the AI, I "set" the blackboard key of the current index of the waypoint to 0. This makes Romeo start with the first index (first waypoint) of the new path no matter the index in which it was rendered in the previous path. This ensures that the new path is performed in the correct order.  

Challenges encountered

This system was the simplest to achieve during this project. The only challenge was to make sure the AI went to the first index of the new path assigned to it every time no matter where it was in the previous path. Even if in the end this system was the simplest, it remains one of the most important for the game and one of the most useful.  

Romeo's AI

Romeo's Winter AI

During the winter season, Romeo is in a depressive state. So he has a completely different AI system. Indeed, instead of taking the lead and following waypoints in predefined paths, this time it is always behind Juliette and it follows her throughout the level. He therefore has no assigned path and cannot jump or lean. He also walks much more slowly than in autumn and he waits there if he loses sight of Juliette. So it's up to her to go get it if it gets lost. The only resemblance to Romeo's AI during the fall is that he can still hold Juliet's hand. He also has a walking acceleration when Juliet asks him to hold her hand and Romeo is further away. It is important to note that when the two are side by side during the handshake, the two characters walk at the same speed. 

​

To do this, I use the same behavior tree as the other seasons, but I added a condition that checks if the AI has a patrol path at the start of the level. If it doesn't, then that means it's in the winter season and that's when I run the functions corresponding to this new behavior. I also added a "sight perception system" to Romeo's AI controller, so that it detects Juliet in a 360 degree circle around him. As long as Juliet is in this detection circle, Romeo will move towards Juliet's position. I also added a condition in the class corresponding to the AI's grip that checks if the current season is winter or not. If the season is winter, then I give a different acceleration speed than in autumn for the grip. This way I also set up all of Romeo's shift states (before, during, and after) during the winter grip.

Challenges encountered

The challenges for this new behavior of the AI were to incorporate it well into the system so that it could work well in the same behavior tree without impacting the rest as well as to make the necessary speed modifications with the same grip as the other seasons. This system required a lot of calibration to make sure it all worked well. I had initially thought of having to make a completely different behavior tree and another controller for this. However, I managed to put the two systems together efficiently and it saved me a lot of time that way. I was also happy to be able to do a more conventional style of AI, an AI that follows the movement of another continuously. Finally, I find that this system makes my AI even more versatile and I am very satisfied with the end result. 

Romeo's AI Design Challenges

During the fall season, the AI brought me several challenges. First, I had never used a behavior tree or a blackboard. Then, making an AI that moves "alone" without following a character or an object brings a lot of complexity. Also, in addition to making it move alone, I had to make sure that it could also have different behaviors and go through a series of obstacles while keeping the whole system functional and dynamic. I also had to adapt my systems to the different changes that can occur in the behavior tree as well as update some AI elements when respawning in the level (the respawn was created by one of my teammates). I also created a class to blast the AI to a specific destination, but chose not to include it in my portfolio as I had to take inspiration from an online blog to achieve a functional jump with my AI. 

​

I also found during the realization of my various systems, that there was very little documentation in c++ relating to the subjects that I worked on. It allowed me to develop my skills and learn a lot of new things on my own. Also, the AI has undergone a lot of behavioral changes over the course of production, which took me a lot of time to often modify the AI for the new needs of the project, or to imagine alternatives to solve new problems. that occurred along the way. 

During the winter season, I was able to produce the new AI behavior in record time by properly pairing it with my fall AI system. This way I could use the already existing AI elements and ignore the ones I didn't need during the winter season. This saved me from starting from scratch and therefore allowed me to spend more time on fixing bugs in my mechanics. It also made me realize the power of behavior trees and how good use of them can help us solve many problems that may seem complex at first glance.

bottom of page