Monolithic is a 1 vs 1 strategy game where you play as three gods fighting three other gods for control of the world. I joined this team as a Gameplay and Systems programmer after Incarecrum failed to make cut. I have since taken on the role of Debugging Specialist alongside my previous tasks.
My single largest responsibility on this project so far has been the task of surgically replacing the graph, pathing, and steering systems in Monolithic with a completely custom solution, due to issues with Unity’s NavMesh.
My first step in this process was to mark all in-code references made to NavMesh with a comment noting how I planned to replace them. This both served as an easy to find marker for later and a note for anyone else who might touch those sections of code that it will be undergoing changes.
Originally the plan was to use NavMesh as our graph and just implement a custom pathing and steering, but this quickly proved impossible. As it turns out, data returned from NavMesh.CalculateTriangulation is completely unusable, with the most infuriating issue being that it only attributes one or two points to each polygon, making slicing completely impossible.
After making that discovery, I set about researching options for custom solutions. I quickly reached the conclusion that I didn’t have the time to research and implement a proper navigation mesh, and that for the purposes of Monolithic a sufficiently optimized grid-based solution would serve our purposes just as well.
The grid is created in a series of steps. First, we allocate a two dimensional array of points, with a size based off of two inspector set variables: extents and granularity. Extents is a vector that controls how far in the X and Z directions the grid spans, and granularity is the distance between each node on the graph. Second, each node adds all neighboring nodes to a list of connections, checking cardinal and diagonal directions. Third, we perform a breadth first search of the graph, using an OverlapSphere to determine whether or not each point is walkable. Fourth, if the option is enabled, we use Debug.DrawLine to draw the graph in world. The graph class also contains a utility function to find the nearest node to any given location in constant time.
The pathfinding algorithm is a standard implementation of A*, using a genericized heap that takes advantage of C# interfaces, namely IEquatable and IComparable, that I created on the off chance some other part of the project will have use for it.
The steering is a path optimizing, obstacle avoiding, arrive and face steering. It leverages SphereCast to accurately judge where it can skip part of a path, and can only move in the direction it is facing. I decided that rather than using velocities, it was safer to directly translate each agent. This gives us far more control on what units will and will not have collision responses with each other, without having to add a large number of layers. The final element of this is a simple, relatively weak obstacle avoidance, which makes it so that monoliths will ‘slide’ past each other rather than clipping through.
Thanks to my preparation work, integration of this system was near-painless, with only a minor hiccup translating the system from our sandbox scene to the game scene. I’m glad to have to have taken on the replacement of an entangled system such as this, and I’ll certainly be taking the same approach to documenting any further system modifications or replacements.