Making a Racing Game in Castle
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
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")
endfunction 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 carImagefunction 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 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.