Kitty Kart

A Multiplayer Racing Game

Download the game here!

Background

Kitty Kart is a multiplayer racing game that uses AI technology to procedurally generate new racetracks. I teamed up with 4 friends to make this game for our senior game dev capstone project.

You can view the game design doc here.

Responsibilities

Kitty Kart Trailer

The Systems Outline of Kitty Kart

For my senior capstone project, my team and I were tasked with creating a video game that used AI tools, such as Chat GPT. 

My team and I decided to make a multiplayer racing game, using AI to procedurally generate racetracks and create endless cosmetics.

To begin, I created a systems flowchart to atomize the components of the game that make Kitty Kart. This way, we knew exactly what to build and how they should interact. 

I was solely responsible for the procedural generation system. There were four main challenges I faced: Terrain Creation, Track Generation, Track Boundaries, and Decoration. I also made the procedural generation system designer friendly.

Terrain Creation

The first step in procedural generation was to generate the geometry of the world. I decided to go with Perlin noise terrain because of how simple and customizable it is.

This is simply done by generating some noise and offsetting a plane mesh's vertices based on the value of the noise.

I also divided the terrain into chunks, allowing us to create terrains of any size while bypassing Unity's vertex limit for meshes.

Track Generation

I needed to somehow create a smooth racetrack from a seed image and have it perfectly conform to randomly generated terrain. 

I went through many solutions, but the one I landed on is easily the most elegant.

I was already coloring the terrain based on the Perlin noise. All I needed to do was add an extra parameter to the shader.

Interpolate between the surrounding color and the color of the track wherever there are black pixels on the seed image.

Track Boundaries

Without a doubt the hardest part was creating smooth invisible track boundaries that the player wouldn't get stuck on. 

The best solution I came up with was to use the seed image itself to generate the track boundaries.

The boundaries for the outside edge are simple: use a contour detection algorithm to generate a list of points that describe the outer-edge of the track.

Then, I can interpolate between each point on the contour and place an invisible collider along each interpolation iteration.

The boundaries for the inside edge were harder to get, as I had no clear way of obtaining the inner contour. However, after tinkering around in Photoshop I had an epiphany.

We can get the inner edge by effectively coloring black any pixel that's outside the black line that makes up the track. 

This is done by applying a flood-fill to the image, similar to how the paint bucket tool in Photoshop works. 

With that, we now have an image to use the same contour detection algorithm on. We can also use the exact same interpolation method to place colliders on the inner contour.

Decoration

Decorating the terrain is easy now that we know where the race track is and where the boundaries are. The first step is to color the terrain. 

As explained before, we can interpolate the terrain's color based on where the racetrack should be. This time, instead of it being a single color, we can interpolate between multiple textures.

The next step is to populate all the empty space with some cool stuff like trees, vegetation, or rocks. We can store a list of prefabs for these objects.

To place the objects this, I used a for loop to fire raycasts down at a randomly generated point on the terrain. 

If the racetrack/boundaries isn't there or if the raycast hits only the terrain, then a random object is picked from the list, instantiated, and oriented to match the terrain.

This results in tons of non-overlapping scenery being generated.

Artist-friendly Tooling

I made the procedural generation super easy to use by making it use ScriptableObjects. This allowed new biome variations to be created very quickly.

The ScriptableObject has control over every aspect of generation, with easily tunable parameters. 

An artist or designer can control anything from the textures the terrain uses, to the size/shape of the terrain, to the decoration objects seen in the level.

Using ScriptableObjects made selecting which biome the players want to race in super easy as well.

What I'd Change

Overall, the procedural generation turned out great! However I'd make a few changes if I were to do this again: