- Play - View Source Code -
Last weekend it occured to me that despite the ever-growing popularity of procedurally generated content, I haven’t seen a procedural racing game yet. Procedurally generated content would have some interesting implications in a racing game. It would reward general driving skills and the ability to improvise over rote memorization of the track.
A racetrack is conceptually a very simple object. It is essentially a ribbon, a flat surface extruded along a path. My first thought was to use splines or bezier curves (which are just a specific type of spline). I could randomly generate a series of points and interpolate curves between them to get the center line of the racetrack. Then I could offset curves on either side and draw geometry between the curves.
The big problem with splines, and it is an especially big problem in the context of a racing game, is that there is no simple way to determine their length. The simplest workaround is to approximate the curve with a series of straight line segments, but even that seemed like a lot of work for a less than ideal result.
On top of that, it turns out it is non-trivial to offset a curve from a spline such that the space in between the two curves has a constant width. It can be done, however the catch is that the offset curve of a spline cannot itself be represented as a spline. In other words, there would be no simple way to take a point on the center spline and find the corresponding point on the offset curve, which I would need to do to place vertices. (more information in this Stack Overflow discussion.)
I scrapped the idea of using splines pretty quickly.
My research led me to two articles which were absolute goldmines of information. The first article deals with the problem of drawing curved roads in a Sim City type game, the second deals with drawing curved tracks for a train simulator. Both were very similar to what I was trying to do, and both began by addressing the problems of using bezier curves.
Both articles make the case for circular arcs as an alternative to splines, and that is what I ended up using for my game. With circular arcs it is trivial to calculate distance and to make perfect offsets. They are also more visually pleasing (and actually closely resemble the curvature of real-world roads and train tracks which are modelled with Euler Spirals.)
Building a single arc segment object was quite simple. The object takes a starting point, starting angle, radius, and end angle and generates a mesh (this tutorial was very helpful for the mesh building part.)
When I tried to string several arc segments together into a track, I encountered a frustrating problem that has to do with converting between a right-handed and left-handed coordinate system. I was using simple trigonometry to calculate points on my arcs. An angle of zero degrees points toward the positive X axis, and angles increase in a counterclockwise direction. However, in Unity an angle of zero degrees on the XZ plane points toward positive Z, and angles increase going clockwise. As a result, none of my arcs were lining up properly.
At first I tried to compensate for the difference in coordinate systems by writing conversion functions between them, but by this time my code was a tangled mess. I was passing data around, manipulating it with trigonometry and then converting it into Unity rotations by using the AngleAxis and Euler functions of Quaternions. It was difficult to pinpoint exactly where the conversion problem originated from, and any time I thought I had solved it I was breaking something else that I wouldn’t notice until later.
I decided to rewrite the code from scratch. This time, to keep things simple and consistent, I didn’t apply any rotations to the arc objects whatsoever. To connect two arcs together seamlessly, I offset the starting angle of the second arc to match the ending angle of the first.
What I Learned
On the practical side, I learned two important new Unity skills. The first being the ability to generate geometry and UV coordinates from code, which is an extremely powerful tool. Secondly, a more subtle but equally powerful tool is writing extension methods to add functionality to built-in Unity classes (specifically, I needed a simple way to get just the X and Z components of a
Vector3 for certain calculations.)
I also learned, after much frustrated troubleshooting, that sometimes the best solution to a problem is to start over. It’s not fun to start over, but it’s even less fun to write code bandaids to try and fix a low-level problem that could’ve been easily solved had I noticed it at the beginning. Plus, when you write a section of code for the second time you have a better understanding of how it should work, and can salvage pieces of the old code to speed up the process.