How to build a racing game - curves

Earlier I published a simple outrun-style pseudo-3d racing game
and followed up with an article showing how to get started with
straight roads.

Today I’m going to go into more detail on how the curves work.

If you followed along with the previous article you will know that we built
up our road geometry as an array of segments, each of which has world coordinates
which get translated relative to the camera and then projected into the screen.

We only needed a z world coordinate for each point because, for straight roads, both
x and y were zero.

If we were building a full-blown 3d system we might implement curves by
calculating x and z coordinates in a kind of fan-strip as shown on
the left. However that kind of geometry can be a little complex to calculate
and would require us to add a 3d rotation step to our projection equations…

… if we wanted to go down that path we would be better off using WebGL or
its equivalent, but that’s not really what this project is about. We just want
to use some old-school ‘good enough’ pseudo 3d tricks to fake our curves.

So you might be surprised to learn that we wont be calculating x coordinates
for our road segments at all…

“To curve a road, you just need to change the position of the
center-line in a curve shape… starting at the bottom of the
screen, the amount that the center of the road shifts left or right
steadily increases”

In our case, the center-line is the cameraX value we pass to the projection
calculations. This means that as we render() each segment of the road, we can
fake curves by offsetting the cameraX value by a steadily increasing amount.

In order to know how much to offset we need to store a curve value in each
segment. This value represents how much the segment should be offset from the
camera’s center line, and will be:

negative for left hand curves

positive for right hand curves

smaller for easy curves

larger for harder curves

The actual values are somewhat arbitrary, and through trial and error we can find good
values to make our curves ‘feel’ right:

In addition to defining good curve values. We want to avoid any jarring
transitions when a straight turns into a curve (or vice versa) by easing
into and out of the curves. We do this by slowly incrementing (or decrementing) the
curve value for each segment until it reaches our desired value using traditional
easing functions such as:

Rendering curves

Earlier we said we could render fake curves by offsetting the cameraX value
used in the projection calculations as we render() each segment of the road.

To do that we maintain an accumulating dx variable that increases by the
amount of curve for each segment, along with an x variable that will be
used as the offset to the cameraX value used in the projection calculations.

To implement curves we need to:

offset each segments p1 projection by x

offset each segments p2 projection by x + dx

increase x by dx for the next segment

Finally, in order to avoid jarring transitions when crossing segment boundaries
we must ensure dx is initialized with an interpolated value for the current
base segments curve.

Hmmm. If I was brutally honest, I’d have to admit that this made a lot more sense
when I was writing the code than it does now trying to explain it for others. Looking back
now it looks suspiciously like I have a double accumulation going on and I can’t really
justify the need for both x and dx ? That’s a terrible admission as a
programmer!!… You know what, forget I said anything, there’s nothing to see here, pretend
you didn’t read this note and lets move on…

UPDATE. Thanks to PeteB in comments below for reminding me that a curve is a 2nd
order equation, and that I do need to maintain a separate dx as the rate of change of x.
I started second guessing myself when writing this article, and I was also in a dazed and
confused state of mind due to England getting knocked out of Euro2012 - on penalties - AGAIN!
So its ok, there was nothing to worry about, this code is correct (at least as correct as
a ‘fake’ curve can be!)

Parallax scrolling background

Finally, we need to scroll the parallax background layers by maintaining an offset for each layer…

Conclusion

Most of the code we added for curves revolves around constructing the road geometry with the
appropriate curve value. Once we have that, providing a centrifugal force during update()
is easy.

Rendering the curves is only a few lines of code, but they can be conceptually hard to understand
(and describe) exactly what’s happening. There are many approaches to faking curves and its easy
to go down some dead ends, and even easier to get side tracked trying to do the ‘correct’ thing and
before you know it you are implementing a full blown 3d system with matrices, rotation and true
3-d geometry… which I’ve already said is not the point here.

In writing this article I’m pretty sure that I actually have some problems with my curve
implementation. In trying to visualize the algorithm for this article I can’t help but wonder
why I need 2 accumulating values dx and x instead of just one… and if I’m not able
to fully explain it then something, somewhere is wrong…

… but my time on this ‘weekend’ (ha!) project is pretty much up and, to be honest, the
curves look pretty good to me - and really that’s what matters at the end of the day.