Made with Unreal Engine 5
The project is focused on creating a game from concept all the way until release in the future.
It serves as both a learning experience for all team members as well as a potential future commercial release
1 Programmer
1 Artist
1 Designer
7 March 2024 - Ongoing
The goal of this project is to create a detective game with a focus on the evidence board, also known as "crazy walls" often seen in many tv series, that is to be used by the player to deduce and solve mysteries based on presented evidence in order to solve the overall mystery of the narrative.
This project is a collaborative effort between Programming, Visual Art and Design.
Role: Gameplay Programmer, Technical Support
Working on all gameplay functionality & providing general technical support
I created all the relevant gameplay systems, including but not limited to: Connection System, Group Checking, String Calculation and Saving & Loading.
Smaller functionality like output log warnings were also created by me.
I used a combination of C++ and Unreal's Blueprint System throughout the project.
The Connection System is the main system that handles and stores the connection between the various pieces of evidence. Since it mostly interacts with other blueprint classes and it makes little use of information stored in C++ this system was created in Unreal Blueprints.
The system is simple, whenever a connection is created between Evidence A and Evidence B, identified by their IDs, it will first check if said connections already exists. Since the player can create a connection in both ways, it checks if connection A-B exists and if connection B-A exists. If not then the input connection, both the evidence actors and IDs, are stored in arrays that track all existing connections
If a connection is to be removed the arrays are checked for connection A-B and B-A, whichever is present is then removed from the array.
This is all rather simple, but the more interesting part of this system is the Group Checking part.
In order for the game to know if the player has solved the mystery of the game it was decided to divide the potential connections that can be created into three designations: Necessary, Allowed, Forbidden.
Since there are dozens of connections that can be created we needed to strike a fine balance in allowing the player to make their own connections, all the while preventing them from solving the question by connecting everything.
This is where the Group Checking system comes in, each "group" is essentially a question posed to the player that needs to be solved by them. Each group is tied to an array of connections, these are the ones designated as necessary and are required to be made by the player if they wish to proceed.
Every time a group is checked we go over the array to see if all of the existing connections exist in the general connection array, if so then we can set the status of the current group as solved, if not it is unsolved.
However before we check the status of a group, we first check if any of the connections designated as forbidden have been made. Just as a group has a list of connections it needs in order to be considered solved, it also has a list of connections it cannot allow if it is meant to be solved.
Every time a group is checked we first check the general connection array if any of the group's forbidden connections exists, if they do the group is unsolved.
Any connection that is neither Necessary or Forbidden can be seen as part of the Allowed category, the player is free to make this connections and it has no impact on gameplay.
In order for the player to win the game, they need to answer every single question i.e. every group needs to be considered solved. As such each time a connection is made or broken, the game system will check the status of all groups and once all are solved then the player has beaten the game.
The image above seeks to give an example of the group checking that has been explained.
There are two groups, A and B, each with a connection inside of them, meaning that these connections are considered Necessary.
Now both groups also have a connection to another piece of evidence outside of their group. How the two groups see this depends on their setting.
Group B's settings aren't concerned with this connection, making it an Allowed connection, meaning that Group B is solved. Group A' settings on the other hand are concerned with this connection as it is on their Forbidden list, meaning that Group A is unsolved.
This means that until the forbidden connection is removed Group A cannot be solved and the game cannot be won.
Given that all pieces of evidence need unique ID in order for the existing systems to function, I wanted an automated system that could both check things for me and warn a developer when needed.
In order to do this I implemented the simple function you see above, which is development only and blueprint callable.
Using this function that exposed the necessary functionality to blueprints I was able to make a system that gets all the relevant actors, such as all pieces of evidence, and then goes over all of them to check if they have a valid ID or if their ID is unique or not. If there is something wrong, the function above is called and gives out an error detailing the issue.
This function is contained in the Game Mode since that is where it is only needed as of now, but this could always be changed in the future. Since it is development only it shall have no impact on the overall performance of the game and is a useful tool that will point out mistakes for us, preventing us from having to go over every single actor to see if their data is valid.
In the project the player can move evidence all over the board and make connections regardless of distance, as not to limited the player. Given this dynamic nature of the board it was decided that we would dynamically create string actors whenever a connections was created, or the connected pieces of evidence were moved. The first step in this is calculating the points of the spline that will serve as the basis for the string's mesh.
The image above shows the code used to calculate these points.
We start out emptying any existing points and getting the direction we're working with based on world locations.
Designers have influence on the calculations through the following variables:
For every section we calculate a point, based on the starting location and direction we calculate the current world position, which is then modified to ensure we are heading towards the end location.
Then once this point is calculated, which is in a direct line, we calculate the vertical offset that needs to applied and add that to the final result.
Through this we calculate a curving spline between point A and point B, which might either be another piece of evidence or the player's cursor.
Now with the overall curve having been calculated and applied to the spline, it is time to apply the meshes to these points and make some changes to the transform as is needed.
With the string being dynamically created it would mean that dozens to hundreds of static meshes could be created for the string during a gaming session, with each and every one of these being a draw call would result in a significant effect on performance, additionally the creation and destruction of these meshes would also result in very fluctuating performance.
To prevent this each String Actor has an Instanced Static Mesh Component that loads the relevant mesh in once so that it can be reused without affecting performance too much and preventing massive fluctuations.
To begin we determine how many meshes we are going to create, and before we get to the calculations for individual meshes we calculate the general length of the mesh and based on that the minimum scale in use, this is important for its size later on.
Each mesh will get their starting and end positions and tangent, this is determined by the previous calculations of the spline.
Using this we rotate the mesh in the right direction and calculate its position between spline points, we account for the mesh's origin being in the center while doing so.
Then we come to the most difficult part, the overall scale of the mesh, at least of the Z axis. The minimum scale calculated before is the scale that would at least be needed in that section, but since it did not account for the distance between the start and end point, we need to adjust tho slightly adjust this scale to get the one we need.
The current distance between the start and end is divided by the length of the mesh to see how many times it could fit in there, and with this we get the remaining difference of scale which can then be added to the minimum scale.
Then once we know the current position for the mesh, its rotation and the scale that needs to be used we have a transform that we can use to add another instance of the mesh, and this is then done as many times is needed to create the full string.
Github
Davey Vermeeren
Linkedin
Davey Vermeeren