Behind the scenes of the high-speed, high-intensity driving game that utilizes a homemade 3D graphics engine built on top of PixiJS's 2D rendering engine. Developed by Nick Baker, 2020.
Play the game here!In late September, I started experimenting with writing my own 3D graphics rendering engine in C#. I spent about a week bodging together the correct vector and matrix math required to make 3D projections work with a free-moving, free-rotating camera. The idea had potentional, but it was going to take a lot of work to turn it into a full game.
Early test of perspective projections with a wireframe cube.
My first attempts at rasterizing textures were really inefficient, but it did look cool when polygons got twisted.
I rewrote this projection code to work in JavaScript in late November, but I also added more features to it. These included rasterization: taking points on the screen and filling in the shape they create with colored pixels, back-face-culling: only drawing the polygons/faces of a mesh that are facing towards the camera, and depth buffering: storing a depth value per pixel that helps determine which pixel of each polygon is closest to the camera and should therefore me rendered in front. I created a little "cube demo" to show off this work, of which ran at a resolution of 128x72.
All of these features were tough, geometrical challenges in their own right, but I learned a lot from solving them. Specifically, when dealing a lot with triangles (tris), I had to learn how barycentric coordinates worked, and how to convert between space coordinates and barycentric coordinates. I use barycentric coordinates during the rasterization process, as it tells me where on a 3D tri a specific screen pixel is pointing to. This means I can interpolate between the depth values of each vertex of a tri, but it also means I can convert a pixel on screen to a pixel in a texture map. I did not end up implementing texture maps in the final product, but the foundation is there for my system to be scaled up to that level.
I did a lot of research to figure all of this out, and I don't have all of the resources I learned from saved, but here are some pages I remember looking at:
To create the randomly generated racetracks, I loosely followed Gustavo Maciel's article: "Generating Procedural Racetracks". The basic idea is this: I start by placing points randomly within a 2D space, and then I build what is known as a convex hull around those points. To do this, I followed the Jarvis March Algorithm, published by R. A. Jarvis in 1973. Starting at the point closest to one of the edges of the 2D space, connect that point to the rest of the points, and find the line formed by 2 points that requires the least amount of right turn. Move to that next point that forms that line segment, and repeat, all the way until hitting the starting point again. This forms the basis for the racetrack, just a very simple, closed polygon.
One of the first tracks I generated. I originally extruded the points inward rather than outward, and that caused them to overlap much more.
A track somehow generated with an overlaping loop-d-loop. I really want to do more complex tracks with designs like this in the future.
I then add points onto line segments that are very long, basically cutting them in half, and I move these points around to create indents in the track that add some variation and barriers the player can drift around. I also push the points away from eachother to make sure the track doesn't overlap itself, though there are times where it still will overlap. I then create a mesh out of these points by extruding the points outwards. "Outwards" is defined by the clockwise-perpendicular normal direction between the 2 points surrounding a point. I also generate a heightmap, which is just height values on the edges of the track that get interpolated between, and I apply those heights to the vertices of the track mesh.
An even crazier track that features even more loops.
Jackson Pollock calls this one "Turning Up the Randomness Slider a Bit Too Much".
After this track mesh has been generated, and the track 'segments' (the quads that make up the track, you can see this in game as they are all a slightly different shade of grey) have been defined, the track is ready for racing! The physics and 'racing game logic' are all done in a 2D sensse, meaning this game basically acts like a top-down driving game. The height variation of a track is just a visual element, and does not actually contribute any complexity to the physics.
Even just barely adjusting the 'rules' for track generation can produce monstrosities.
Finally, a functional track with some cool height variation!
Along with the actual coding challenges I faced, I needed to design car controls that were fun. There is no set guide on how driving physics should work in games, so there was a lot of trial and error in getting the acceleration amount, sensitivity of the steering, and drift ability feeling just right. I found videos like the one below particularly helpful to reference. The drift in "The Urban Underdog" is not quite the same as in real life, or as racer Leona Chin describes it in the video, but it adds a lot of depth to the game, and fixes some of its crucial issues.
First off, the drift fixes the issue with the random racetracks having some very sharp turns: just give the driver the ability to turn really sharply with a manual drift. Along with that, drifting increases your top speed depending on how much you are turning, so an experienced player can string togther multiple drifts, even on straitaways, to pick up tremendous speed. This comes at a cost of more sensitive steering, and one misplaced drift can send you right into a barrier, ruining any time you might have saved. I find this risk vs. reward system super satisfying and keeps the racing very dynamic!
There are a couple, very simple, quality of life features that I really like. Customizing your car color from the full range of color values is really cool in my opinion, and I utilize local storage to save the users car color! Also, being able to copy and share track codes is a feature I really wanted to implement, and I'm glad I took the time to do it. My friends and I have shared some track codes with each other, and are seeing who can get the best lap time on them. I saved and labeled a couple of my favorites, such as "Drift Heavy", "Craziness", and "The High Ground".
What is even cooler than just sharing these codes with friends however, is that because these codes are just coordinates for vertices on the track, a very dedicated user could create their own custom tracks by editing a randomly generated track code, or by writing one from scratch. Someone could create tracks to practice specific tricks in the game like drifting, or something could make one huge gauntlet track that takes minutes to complete! It makes me excited just thinking about all of the possibilities such a simple system as shareable track codes brings to the table.
I spent a while getting the camera in just the right spot to be close to the action but high enough to show the track in front of them.
Before I spent the time to manually enter vertex coordinates for a racecar mesh, I just used the 'Rubik's Cube' model I made for the cube demo shown earlier.
In hindsight, some of these ideas were way out there, such as "simple textures and lighting" or even computer controlled racers. I managed to make do with flat colored polygons rather than textured ones, but with how I wrote my rasterization code, it could be scaled up to work with a model's UV map to display textures, or any other kind of data maps such as normal maps! Currently, I just do not have time, energy, or need to expand the graphics system to that level.
With computer controlled racers (bots), I thought up many ideas on how they could work, but they just did not end up fitting the final design of the game. In the final version of the game, the real challenge is going as fast as you can with a slippery car on race tracks that can have very tight spaces and sharp corners. Bots would just end up clogging up the track. Not only that, but simulating bot AI, calculating physics for that many players, and displaying that many models on screen would probably bring the frame rate to a halt. Overall, bots would have taken away more from the experience than added.
You can read my initial ambitions below, in my 'Initial Proposal':
Drive your high speed racecar in randomly generated tracks and dethrone the champions of the urban racing scene as the Urban Underdog!
Racing, Arcade. The game is about driving a racecar, and will act similarly to those racing arcade cabinets.
The game is meant to be played on Desktop with keyboard and mouse.
In Urban Underdog, you play as the underdog driver that no one bets on or believes in. It is time to prove them wrong. The game will be designed so that you start in last place, and work your way up to first place, and by the end of the race you just barely make it. Players will feel the thrill. Other than that, there is no actual story to follow.
The game will be set on a pixelated display, with pseudo 3D graphics. There will be very simple textures and lighting on some of the objects (such as the player's car), but overall almost everything will be represented with colorful, flat shading. Before starting the race, the player will be able to customize the color of their car with RGB sliders.
Players will drive multiple laps around randomly generated race tracks moving the mouse left and right to steer, pressing the W key as the gas pedal, and Space Bar as the breaks. They will also be able to press Shift to turn the camera around and see behind them. The current goal is to make the player racer against 7 CPU-controlled opponents, but if there is not enough time to develop that, the player can just go for their best time in a Time Trials gamemode.
I've already started developing the pseudo-3D graphics 'engine' for the game. I'm using vector, matrix, and projection math I learned last semester, as well as what we've learned of PixiJS in the Circle Blast HW. You can view a demo of my 3D graphics here: Cube Demo.
On the Left: a mock up of what the player will see in game. The camera will be behind the player, and the HUD will show speed, place in the race, a map, lap count, and the player's hands on the wheel that will rotate when the player steers by moving the mouse left or right. If they are playing the Time Trials gamemode, display stopwatch time instead of their place in the race.
On the Right: after the game generates the track, it will show the user a 3D version of the track that rotates around. If they like it and want to play it, they can click 'Play'. If not, they can regenerate the track!
While the overall project is complicated, it is made up of rather simple smaller parts. I've already gotten started on my 3D graphics (see demo), and I have a solid foundation with 3D projections, back-face-culling, and mesh creation. By the due date, I will have a fun, working version ready, but if I have extra time, then I will be able to implement some more complex features such as CPU controlled players, more complicated race tracks, saving/sharing generated race tracks, and other visual and audio effects.
I'm Nick Baker, a second year Game Design and Development major and 3D Digital Design minor at RIT. I'm interested in pushing my art, programming, and design skills to make unique and expressive works.