Making a Racing Game in Castle

Ben Roth
Castle Games Blog
Published in
8 min readOct 26, 2019

Hello Castle community! This tutorial was written by Charlie Cheever, and he also gave a live stream covering this material. Check out the video.

We’re doing a whole set of tutorial streams as part of Castle Halloween Party. See the full stream schedule here.

Making a Racing Game in Castle

by Charlie Cheever

Let’s make a really simple game where you can race a car around a track. This will be so simple that you’ll just be able to drive around really fast, not really race anything. And we’ll leave out sounds and special effects for now. This will just be a simpler version of bridgs’ lil racer game.

Author’s note: If you’re following this tutorial to learn, I recommend that you type out all of the code listed here instead of copy/pasting it into your project. Most people seem to pick up and retain more of the details and learn faster if they do that.

To get started, first take a look at the Making your First Game in Castle tutorial and make sure you can get through it. Then let’s just create a .lua file. We'll just call it main.lua.

Drawing the Track

To start, let’s just draw the track. To do this, we’ll need to load the image of the track in the load phase and draw it in the draw phase.

local raceTrackImagefunction love.load()
raceTrackImage = love.graphics.newImage("./race-track.png")
end
function love.draw()
love.graphics.setColor(1, 1, 1)
love.graphics.draw(raceTrackImage, 0, 0)
end

And for this to work, we need an image; you can just download a good example from here.

We could just load this .lua file directly in Castle, but it will be easier to open the project and easier to tweak settings like resolution, etc. if we have a .castle file, so we'll make one of those too, but we'll keep it to pretty much the absolute minimum that we need.

---
main: main.lua
name: bridgs' Racer

If we load this, we’ll see it looks something like this.

It would be a lot nicer if the race track took up the whole available screen area, or at least most of it, so let’s fix that. There are a bunch of ways we can do this, but the easiest is just to add a dimensions key to the .castle file. Let's add this line there.

dimensions: 192x192

If you save and reload, then you should see this now.

The racetrack image is 192x192 so setting the dimensions to that means it will take up basically 100% of the screen.

Drawing the Car

Next, let’s draw the car.

OK, before we draw the car, we need to setup some basic state for it so we know where it is and how it is positioned. Let’s add this new createCar function to the bottom of our file.

function createCar()
return {
x = 95,
y = 28,
bounceVelocityX = 0,
bounceVelocityY = 0,
speed = 0,
rotation = math.pi / 2,
}
end

(95, 28) is right at the starting line of this track; math.pi / 2 means the car will be pointing left; and we start off with the car not moving at all and not bouncing at all.

Let’s initialize a car variable and carImage variable and then initialize the carImage and call the car setup function in love.load now to get the game setup.

local car
local carImage
function love.load()
raceTrackImage = love.graphics.newImage("./race-track.png")
carImage = love.graphics.newImage("./car.png")
car = createCar()
end

Now back to drawing the car. To do that, we can use this drawSprite utility function.

Since Castle lets you load code from any URL on the Internet, we don’t have to write this function ourselves and can just require it by URL like this:

local drawSprite = require "<https://raw.githubusercontent.com/ccheever/castle-utils/c35e540893e4ee0136d540f3e0ed4f13f840adb2/drawSprite.lua>"

(Put that at the top of your main.lua file)

A sprite sheet is a single image that has one or more images, or versions of images, that you might want to show in the course of a 2D game. By using different parts of that one big image, you can achieve the same effect as using lots of different images but without as much overhead as loading lots of different images into memory.

We’re only going to use a sprite sheet here to draw one thing — the car — but we’ll use a sprite sheet because we want to show different versions of it depending on how rotated it is. If you look at the source sprite sheet, you can see all the different versions of the car.

The sprite sheet for the car. All the different versions the drawing.

The signature for drawSprite is:

function drawSprite(spriteSheetImage, spriteWidth, spriteHeight, sprite, x, y, flipHorizontal, flipVertical, rotation)

So let’s add this code to love.draw:

local radiansPerSprite = 2 * math.pi / 16
local sprite = math.floor((car.rotation + radiansPerSprite / 2) / radiansPerSprite) + 1
if car.rotation >= math.pi then
sprite = 18 - sprite
end
drawSprite(carImage, 12, 12, sprite, car.x - 6, car.y - 6, car.rotation >= math.pi)

The car is 12x12, so we subtract 6 from x and y so that we're drawing it at the center of (car.x, car.y). We use some math to tell if should flip the image horizontally; but we never want to flip the image vertically because it would look weird. And we never want to rotate the image because we choose different images that already look rotated based on how much the car is turned.

If you load your project now, you should see something like this:

There’s the car!

Driving the Car

Let’s add some action to this :)

Accelerating and Braking

We’ll use the up and down keys for accelerate and brake. We’ll make it so you can go forward and backwards (but not too fast backwards). We’ll also make sure there is some sense of acceleration so it doesn’t just feel like you are either completely stopped or instantly going 30mph. We’ll make it so you can accelerate quickly to a pretty fast speed, slow down automatically but slowly if you’re just coasting, and brake at a slightly slower speed than you can accelerate, and then go in reverse at a slow speed as well.

To do this, we’ll add put some code in love.update.

function love.update(dt)
if love.keyboard.isDown("down") then
-- Press down to brake
car.speed = math.max(car.speed - 20 * dt, -10)
elseif love.keyboard.isDown("up") then
-- Press up to accelerate
car.speed = math.min(car.speed + 50 * dt, 40)
else
-- Slow down when not accelerating
car.speed = car.speed * 0.98
end
-- Apply the car's velocity
car.x = car.x + car.speed * -math.sin(car.rotation) * dt + car.bounceVelocityX * dt
car.y = car.y + car.speed * math.cos(car.rotation) * dt + car.bounceVelocityY * dt
end

In this code, we figure out the car’s speed and then we also update its position based on the combination of its speed and dt which is the elapsed time since the last time love.update was called. It's important to factor in dt here, or else your updates will happen at an inconsistent.

We can add turning in as well now. Put this code at the top of the love.update function.

-- Turn the car by pressing the left and right keys
local turnSpeed =
3 * math.min(math.max(0, math.abs(car.speed) / 20), 1) - (car.speed > 20 and (car.speed - 20) / 20 or 0)
if love.keyboard.isDown("left") then
car.rotation = car.rotation - turnSpeed * dt
end
if love.keyboard.isDown("right") then
car.rotation = car.rotation + turnSpeed * dt
end
car.rotation = (car.rotation + 2 * math.pi) % (2 * math.pi)

Now we can drive our car around!

Handling Collisions

Oh no, the car has driven on top of the barrier! =O The last thing we need to do for our simple demo is to handle collisions properly.

To do that, our game needs to know where the walls and grass on the track are. As a human, you can see that pretty easily with your eyes; but since there are different shades of grass and of barrier, and since you might change the way the track looks visually, it’s better to store the data about the track’s layout in a different way.

There are a bunch of ways we could do this. One would be to just have a big Lua table in a file somewhere and use that. But for this particular case, it would be nicer to be able to visualize it; so we’ll use a different image, that is color coded just to show where the walls vs. grass vs. track is. Here’s the source image we’ll use.

The track is black; the walls are red; and the grass is blue. Notice how its easy to visually see what’s happening here and make changes if necessary, and how you can line it up with the map we’re showing to the player. Those are some of the main ways its nice to use this way of storing that data.

We can load in this map data in love.load. Note that we use love.image.newImageData here instead of love.graphics.newImage since we're interested in getting the individual pixels of the image, not displaying it on the screen.

raceTrackData = love.image.newImageData("./race-track-data.png")

and declare raceTrackData as a local at the top level.

local raceTrackData

Now, we have to figure out where the car is exactly on that map. Let’s add this code to the bottom of love.update

-- Check what terrain the car is currently on by looking at the race track data image
local pixelX = math.min(math.max(0, math.floor(car.x)), 191)
local pixelY = math.min(math.max(0, math.floor(car.y)), 191)
local r, g, b = raceTrackData:getPixel(pixelX, pixelY)
local isInWall = r > 0 -- red = wall
local isInGrass = b > 0 -- blue = grass

Now we know if we are in the wall or on the grass and we can add behaviors to those things.

-- If the car runs off the track, it slows down
if isInGrass then
car.speed = car.speed * 0.95
end
-- If the car becomes lodged in a barrier, bounce it away
if isInWall then
local vx = car.speed * -math.sin(car.rotation) + car.bounceVelocityX
local vy = car.speed * math.cos(car.rotation) + car.bounceVelocityY
car.bounceVelocityX = -2 * vx
car.bounceVelocityY = -2 * vy
car.speed = car.speed * 0.50
end
car.bounceVelocityX = car.bounceVelocityX * 0.90
car.bounceVelocityY = car.bounceVelocityY * 0.90
-- If the car ever gets out of bounds, reset it
if car.x < 0 or car.y < 0 or car.x > 191 or car.y > 191 then
car = createCar()
end

We’re done! And we can drive the car around the track.

Future Work

There are lots of things you could add to this. bridgs’ original version of this (playable here) has sound effects and more interesting visual effects. Jason’s version of this keeps track of your lap times lets you race against your ghost from your best time. You can make the graphics look more sharply pixelated by giving different options there.

--

--