A desktop car racing game in Go
Go Faster!
The fastest way through a curve on a racetrack is along the racing line. Instead of heading for Indianapolis, Mike Schilli trains his reflexes with a desktop application written in Go, just to be on the safe side.
A few years ago, I got to test the physical limits of my Honda Fit during a safety training session. A short time later, I discovered I was interested in car racing. It's also a more popular hobby than you might think among Silicon Valley employees, who let their tuned private cars off the leash on racetracks like Laguna Seca in California – maybe because of the strict speed limit that typically applies on freeways in the US.
While studying the topic, I was surprised to learn that it's by no means just a matter of keeping your foot on the gas. If you want to break track records, you have to take the turns exactly in line with physical formulas and always find the ideal line in order to knock those vital seconds off your time in each lap. The physical principles of racing are explained in the reference work Going Faster by Carl Lopez [1]. The book describes exactly how quickly you can enter a turn without the car starting to skid and tells you the angle and time at which the driver needs to turn the steering wheel to lose as little time as possible while cornering.
Learning How to Race
The ideal line through a turn is never going to be the shortest path, which runs along the inside. Instead, the aim is to drive through the curve on a trajectory with as large a radius as possible (Figure 1). Before the 90-degree right-hand bend shown in Figure 1, a world-class driver like Jos Verstappen will initially steer to the left-hand edge of the road and then pull sharply to the right towards the apex. This means that the race car just barely scrapes past the inside of the curve, only to run over to the left side of the road again shortly afterwards on the straight that follows the turn. This means that the radius followed by the car is far larger than that of the turn, and that the car can negotiate the turn at a far faster speed without the tires losing traction or the vehicle skidding.
World of Geometry
Figure 2 shows the simulation of a 90-degree turn as a desktop game written in Go with racing animation. The race car, depicted as a green square, speeds upwards towards the curve. The player has to steer the vehicle to the left and right with the H and L keys so that it doesn't hit the side of the road at this breakneck speed, but safely reaches the end of the turn at the top right of the game window. The stopwatch next to the two buttons runs during the animation and displays the elapsed lap time in seconds with an accuracy of two decimal figures.
With a little prior knowledge of geometry and video game technology, a simple 2D game like this can be quickly put together using Go and the Fyne framework [2]. To do this, the program runs through a number of frames per second, in each of which it computes the current position of the game figures, which it then refreshes in the graphics. At the same time, it fields user input such as keystrokes or mouse clicks and incorporates these events into the calculations, for example, by adjusting the steering.
In the Beginning Was the Circle
But how do you write a game like this in Go? First of all, you need to draw the "world" for the game. This is best done in such a way that the program can later compute the game's status at lightning speed for each video frame that it is running through. First and foremost, it needs to give feedback about whether the race car is still driving on the road or has already left the contours of the turn and is lying somewhere in the bushes.
The program draws the racetrack's right turn as an overlap of two concentric circles (Figure 3) with the radii r2
(outside) and r1
(inside). But only the upper left quadrant of this shape is of interest for the curve; this is why I used some cleverly placed rectangles in Figure 4 to mask the irrelevant parts of the circles. The blue, gray, and orange areas later disappear in the display, and two additional salmon-colored rectangles define the road at the entry to the turn and its exit. Listing 1 implements this "world," as gamers call it, using the Circle()
and Rectangle()
functions on a canvas
object provided by the Fyne framework.
Listing 1
world.go
01 package main 02 03 import ( 04 "fyne.io/fyne/v2" 05 "fyne.io/fyne/v2/canvas" 06 "fyne.io/fyne/v2/container" 07 col "golang.org/x/image/colornames" 08 "image/color" 09 ) 10 11 func drawWorld(r1, r2 float32) (fyne.CanvasObject, Car) { 12 bg := drawRectangle(col.Grey, 0, 0, 2*r2, 2*r2) 13 co := drawCircle(col.Lightsalmon, r2, r2, r2) 14 ci := drawCircle(col.Grey, r2, r2, r1) 15 mb := drawRectangle(col.Grey, 0, r2, 2*r2, r2) 16 mr := drawRectangle(col.Grey, r2, 0, r2, r2) 17 in := drawRectangle(col.Lightsalmon, 0, r2, r2-r1, r2) 18 out := drawRectangle(col.Lightsalmon, r2, 0, r2, r2-r1) 19 20 car := Car{Ava: canvas.NewRectangle(col.Green), 21 StartPos: fyne.NewPos(10, r2+r2-1), 22 } 23 24 car.Ava.Resize(fyne.NewSize(10, 10)) 25 car.Ava.Move(car.StartPos) 26 objects := []fyne.CanvasObject{bg, co, ci, mb, mr, in, out, car.Ava} 27 play := container.NewWithoutLayout(objects...) 28 29 return play, car 30 } 31 32 func drawCircle(co color.RGBA, x, y, r float32) *canvas.Circle { 33 c := canvas.NewCircle(co) 34 pos := fyne.NewPos(x-r, y-r) 35 c.Move(pos) 36 size := fyne.NewSize(2*r, 2*r) 37 c.Resize(size) 38 return c 39 } 40 41 func drawRectangle(co color.RGBA, x, y, w, h float32) *canvas.Rectangle { 42 r := canvas.NewRectangle(co) 43 r.Move(fyne.NewPos(x, y)) 44 r.Resize(fyne.NewSize(w, h)) 45 return r 46 }
Buy this article as PDF
(incl. VAT)