Team Racing League is a 3D racing game created in Unity and developed by Gamious in Haarlem. I completed an internship at Gamious from september 2017 till february 2018 and with their permission I write about my experiences and tasks I was involved in. All content displayed here is their property of course. At the time I had a little over one year of experience with the Unity engine and the C# language and I still heavily leaned towards GameMaker with its GML scripting language. This internship was my stepping stone to discovering how a professional workflow takes shape with the engine and coding practises that come with it.
During my internship I followed along with their Agile Development system in the form of scrum, combined with Git source control. We used Atlassian JIRA as a development board where we could store, order and assign tasks for each “sprint” to persons, tailored for scrum. I performed many different tasks during my internship, small and big. Some of the smaller tasks included user interface adjustments or bug fixes on various areas and I usually picked them whenever there wasn’t anything with a major priority to work on. The really fun tasks were of course the big ones!
There were two major tasks I completed:
- Improving and maintaining an extensive list system designed to display or interact with player ranking, team ranking and team invites;
- A comprehensive A.I. system for bot race cars that can automatically calculate, follow and complete paths for different levels, where its goal is to make the fastest laps possible.
This was my first major task that first started by optimizing an existing list system, to the point where I was allowed to extend it with additional features later in development. First I segmented the list data into a page system to reduce the page loading overhead. The existing system loaded all player data at once and you can imagine that with hundreds or thousands of players, this would result in massive load times. This is also the moment where I first found out about coroutines and Action callbacks in Unity and how to apply them accordingly and effectively to fix this problem and many more in the near future. Eventually, I moved parts of this load to import this data in the background at a slow rate while the player was navigating the game’s menus or other performance-trivial areas of the game, ensuring much faster load times by the time the player data was requested. No more frame drops!
Making use of Dictionaries and basic sorting, it was quite easy to implement and extend the column bars to allow alphabetical sorting or numerical sorting, which was applied for name and win/lose data and later combined with search functionality for specific filtering. These were applied to all list systems although numerical sorting found its rightful place among the ranking lists. A team invite list where you can invite teamless players to join your team needed an additional upgrade: Steam Friends integration. With a small toggle below the list system, you can choose between displaying all players or just Steam Friends who have the game (and are all without a team, of course), which can then be selected and invited accordingly. These systems are currently live in the latest Steam build.
Artificial Intelligence – Racing bots
In the second half of my internship, Gamious required the game to provide bots for when players leave or lose their connection in a multiplayer match as backup. After all, it creates a significant imbalance between teams when your team loses a member! It started with a small test setup the lead programmer put together and then tasks were divided among the development team. I was tasked with making the bots complete laps as fast as possible.
In order to calculate paths independent of the level, I needed a dynamic system that can lay the foundation of what I want to accomplish. Unity’s NavMesh system proved to be ideal for the job. The NavMesh is a mesh that Unity generates on terrain according to specific rules such as maximum slope, the step height and agent (or in this case a bot car) dimensions. After configuring the ideal NavMesh settings for a level, I could bake the NavMesh and link it to the related level for the bots to refer to.
As you can see, it generates a mesh and its edges can be referenced in the form of an array. At this point we have a path, but the next problem is how are we going to make the bots navigate it? There are many situations to consider and they must be tweaked accordingly to get the fastest lap times. Simply making the bot target the closest point and steer right at it for example is nowhere near enough. We need turn anticipation, wall detection, braking and steering that takes all of this into account. Below is a detailed gif that shows certain aspects of these calculations:
I accomplished accurate braking by making the car look ahead along a straight line shooting out of the front of the car as a raycast (the long white lines). This line’s length depends on the speed of the bot where greater movement speed equals looking farther ahead. Once a wall is found, it starts to brake where the strength of braking depends on the distance between the bot and the wall. The closer it is, the harder it brakes! Although it cannot be seen above, it actually went much further than that. I also implemented a ramp separation system that evaluates whether the wall I am seeing ahead is an actual vertical wall, or a ramp up to a higher area by comparing normal angles from the contact point on the detected wall. I linked this with the slope value of the NavMesh to eventually decide whether this was a reachable area or not. If so, don’t brake and full throttle! Some levels had these ramps and the bots needed to realize this was reachable terrain. If nothing was detected or too far away, the bots simply go full throttle as well.
The steering was quite tricky to get down and it took a few tries to get it right. The way this works is the bot retrieves its NavMesh array of points or “nodes” first and targets the closest point. Instead of steering directly at the given point it actually projects its velocity onto the edges (lines) between the target node and the next node. This can be seen as the purple dots in the gif above. Then, it projects its velocity in actual world space (the small green ball). Now here comes the magic. It then mirrors the actual world space to the other side of the calculated path line around the purple ball, resulting in the steering target of the bot (the big green ball). Once the projected velocity (the purple ball) reaches the next node, it takes the next node as the current node and starts the cycle again. Combined with the braking behaviour this already results in some pretty tight laps! But wait, there’s more.
To further improve the steering behaviour of the bots I also implemented a node offset system. This system is quite simple. When the bot hits a wall (and wasn’t rammed by another player!), it registers the need for a steering adjustment around the closest nodes (the yellow lines coming out of the nodes). The next time the bot comes into this section of lap, it’ll add this offset as a vector to the actual steering target, trying to avoid more collisions. It can effectively learn to avoid walls and due to small differences or actions of players along the way, this can vary each level, adding an extra layer of realism to the bots’ racing behaviour.
Racing is not the only thing the bots or players can do. They can also block the road with blocks that can be picked up and placed among other behaviour. I also needed the bots to detect dynamic obstacles as well. This includes players and these blocks that can be moved. Although it’s hard to see, the bots actually actively try to avoid blocks from the opposite team or other cars (big gray ball). This works by picking the most efficient side of the obstacle by calculating and comparing the distance of going left or right around it. The steering target is actually a rotated vector coming out of the obstacle and aiming to the left or right around it and rotates based on the car’s angle towards it. This shifts the steering target smoothly around the obstacle!
In the end I learned so much about C# and Unity that I was able to build a system like this. Gamious was also very supportive and I recommend you to check out their games, including this one!
Members of the Team Racing League team:
Luc Schols – Lead programmer
Guido Oostermeijer – Programmer, lead networker