Making your first Castle Game, Part 3

Ben Roth
Castle Games Blog
Published in
8 min readNov 20, 2019

Hello there!

This tutorial is part of a series which helps you get started with your very first Castle game. We’re hoping to cover some basic, useful topics that can help you create the games you want. Part 2 included a written tutorial and a live stream. This segment will continue where Part 2 left off.

Tune in today at 4:30pm California time for a live stream which will cover this whole tutorial and offer an opportunity for questions.

What’s covered in Part 3

In parts 1 and 2, we learned how to create a blank Castle project, draw a space ship sprite, move the ship around with the arrow keys, populate the game with some items, collect the items, and draw some text indicating how many items were collected.

In part 3, we’ll add some terrifying enemy space ships, make them move around, cause them to hurt the player, and draw a Game Over screen.

We’ll assume you already have a project in the same state where part 2 left off. If you’d like to jump in here, grab this snapshot of the code after part 2. Let’s get started!

You can also see the finished code for Part 3 if you don’t want to write it yourself.

Add some enemies

First, we’ll add some enemy space ships to terrorize our hero ship.

We’re going to use the same approach we already used to add collectibles in the game: Make a Lua table containing a list of all our enemies. Each enemy is itself a table with a few properties like x, y, and radius, representing that enemy’s state in our game world.

Spawn the enemies by adding this in main.lua:

local enemies = {}
for index = 1, 5, 1 do
local enemy = {
x = math.random(10, 800),
y = math.random(10, 400),
radius = 10,
}
table.insert(enemies, enemy)
end

Draw a fearsome enemy graphic or pick a free one from the internet, and download the image to your game’s source directory as enemy.png.

Load the image in your game:

local enemyImage = love.graphics.newImage('enemy.png')

Add a function to draw your enemies:

local function drawEnemies()
love.graphics.setColor(1, 1, 1)
for index, enemy in pairs(enemies) do
love.graphics.draw(enemyImage, enemy.x - enemy.radius, enemy.y - enemy.radius)
end
end

And be sure to call this method inside love.draw():

function love.draw()
drawEnemies()
...

Reload your game and you should see a few enemies scattered around the world.

It’s a crowded universe.

This section of the tutorial doesn’t explain how this new code works. That’s because, so far, it’s almost exactly the same as our method of adding collectibles in Part 2. If you aren’t sure you understood this section of Part 3, go back and check out Part 2 for a deeper explanation.

Make the enemies move around

Try flying around the game world a bit. You might notice that our new enemies are pretty boring. They just sit there. And if you run into them, nothing happens. These are hardly enemies at all.

First, let’s make them do something more interesting besides hang around in space.

Update your enemy spawn code to include a velocity in the x and y direction.

local enemies = {}
for index = 1, 5, 1 do
local enemy = {
x = math.random(10, 800),
y = math.random(10, 400),
vx = math.random(-2, 2) * 60,
vy = math.random(-2, 2) * 60,

radius = 10,
}
table.insert(enemies, enemy)
end

Then write a method to update the enemies, which we will call every frame of the game.

local function updateEnemies(dt)
for index, enemy in pairs(enemies) do
enemy.x = enemy.x + enemy.vx * dt
enemy.y = enemy.y + enemy.vy * dt
end
end

Recall that velocity is the change in an object’s position over time. Here, we are iterating over our list of enemies, and during each frame, we’re adding the enemy’s velocity to its position. Since we chose a random velocity for each enemy, this is going to make the enemies roam around the world independently of each other.

You might notice that this is the same thing we’re already doing for the player’s ship in response to keyboard input, but rather than responding to the keyboard, the enemies just move all the time.

Be sure to call updateEnemies(dt) in your game’s update(dt) loop.

function love.update(dt)
updateEnemies(dt)
...

Reload your game. You should see the enemies drift around the world.

Except that one guy. Maybe out of fuel.

Notice a problem with this? The enemies do indeed move around… but they immediately get out of town. We want them to stay on the game screen, not leave the screen, so that they are actually interesting as a game element.

You could design the enemies to do whatever you want inside the updateEnemies(dt) method. One solution is to make them wrap around the screen when they reach the edge.

The default size of a Castle game is 800x450 Love units. So let’s just check the enemy position every step, and wrap it if it’s outside the bounds:

local screenWidth, screenHeight = 800, 450local function updateEnemies(dt)
for index, enemy in pairs(enemies) do
enemy.x = enemy.x + enemy.vx * dt
enemy.y = enemy.y + enemy.vy * dt
if enemy.x < -enemy.radius then
enemy.x = screenWidth + enemy.radius
elseif enemy.x > screenWidth + enemy.radius then
enemy.x = -enemy.radius
end
if enemy.y < -enemy.radius then
enemy.y = screenHeight + enemy.radius
elseif enemy.y > screenHeight + enemy.radius then
enemy.y = -enemy.radius
end

end
end

Reload again. The enemies should stay on the screen forever.

Aside: If your game is a different size from the default 800x450, or your game world is larger than the window, you’ll need to adjust the cases in this code. You can specify custom game dimensions in your Castle Project File, and you can get the current window size with love.window.getMode().

Make the enemies impede the player

Our enemies won’t truly be a challenge until they can interact with the player. One thing they can do is collide with the player and cause the player to reset.

If you remember from Part 2 of this tutorial, we already wrote some code to decide when the player is colliding with a collectible. In that case, we “collect” the collectible (delete it from the game world and increase the score).

We’re going to use the same approach for enemies, but take a different action if we notice a collision.

Near the end of love.update(dt), add this code:

for index, enemy in pairs(enemies) do
local distanceToEnemy = distance(x, y, enemy.x, enemy.y)
if distanceToEnemy < shipRadius + enemy.radius then
print('colliding with enemy!')
end
end

This looks the same as our collectible collision code, except it iterates over the enemies instead. Reload and try flying into some enemies. Notice when the console prints colliding with enemy!. If you aren’t sure how this code works, revisit Part 2 for a deeper explanation.

For now, let’s respawn the player back to their starting position if they hit an enemy. Replace the print() line with this instead:

-- reset the player                                                                                                                                                                                
x, y = 0, 0

Add a Game Over state

Right now the enemies still don’t seem very fearsome. They just move the player back to the upper left corner of the screen when disturbed.

It would be helpful if we could reset the whole game when the player hits an enemy, so let’s add a bare-bones Game Over screen.

Right now we don’t really have any notion of whether the game is “over”. We create the player, collectibles, and enemies at the global scope of the game. The only way to reset the game is to Reload it in Castle. Having a concept of “game over” would be helpful. Importantly, this will give us enough scaffolding to add more advanced game states later, like a menu, multiple lives, or more worlds in the game.

First, take all the existing code for creating the player, enemies, and collectibles, and move it inside a new method called resetGame().

local items = {}
local enemies = {}

local function resetGame()
x, y = 0, 0
itemsCollected = 0

items = {}

local NUM_ITEMS = 5
for index = 1, NUM_ITEMS, 1 do
local item = {
x = math.random(10, screenWidth),
y = math.random(10, screenHeight),
radius = 10
}
table.insert(items, item)
end

enemies = {}
for index = 1, 5, 1 do
local enemy = {
x = math.random(10, screenWidth),
y = math.random(10, screenHeight),
vx = math.random(-2, 2) * 60,
vy = math.random(-2, 2) * 60,
radius = 10,
}
table.insert(enemies, enemy)
end
end

Calling resetGame() will reset x and y back to 0, 0; It will clear the old contents (if any) of items and enemies; and it will add new items and new enemies. Lastly, it will reset the itemsCollected score back to zero.

The first place we want to call resetGame() is initially when the game first loads. Castle will call love.load() at that time, so add the love.load() method to your game:

function love.load()
resetGame()
end

If you reload now, you should see no change from earlier behavior; we are just creating the game world in a more reusable way than we were before.

Now we need a variable to decide whether the game is currently playing (in an interactable state) or whether the game is “over” (the player lost).

local gameState

local function resetGame()
gameState = 'playing'
...

And we’ll mark the game as 'over’ when the player hits an enemy:

...
local distanceToEnemy = distance(x, y, enemy.x, enemy.y)
if distanceToEnemy < shipRadius + enemy.radius then
-- the player hit an enemy, game over
gameState = 'over'

end

We’ve marked this variable to show that the game is over, so now we need to actually change the game behavior in that case. To do this, we’ll wrap love.update(dt) and love.draw() in a huge if ... else depending on gameState.

function love.draw()
if gameState == 'over' then
love.graphics.setColor(1, 1, 1, 1)
love.graphics.print('game over', 128, 128)
else

... the old contents of love.draw() ...
end
end
function love.update(dt)
if gameState == 'over' then
-- don't update anything
else

... the old contents of love.update(dt) ...
end
end

Try reloading your game and cruising straight into an enemy. If you’ve wired this all up correctly, your game should transform into a forlorn game over screen.

Space is a dangerous place.

The last step is to let the player try again if they reach the Game Over screen. So, we’ll check for the return key while we’re in the 'over’ state, and if it’s pressed, we’ll reset the game using that handy resetGame() method.

function love.draw()
if gameState == 'over' then
love.graphics.setColor(1, 1, 1, 1)
love.graphics.print('game over', 128, 128)
love.graphics.print('press return to try again', 128, 140)
else
...
end
end
function love.update(dt)
if gameState == 'over' then
if love.keyboard.isDown('return') then
resetGame()
end

else
...
end
end

Since we wrote the resetGame() method to change gameState back to 'playing’, pressing return will hide the Game Over screen and create a new game world. We fly again!

That’s it for now!

Thanks for following along! At this point you should have a space game where you can fly around, collect items, avoid enemies, and reach a Game Over screen if you accidentally hit an enemy.

Check out the full code for this tutorial.

Some interesting ways to extend this might be:

  • Adding a “success” state when the player collects all the collectibles.
  • Giving the player a health meter or multiple lives.
  • Expanding the game world to be larger than the window.

Tune in today at 4:30pm California time for a live stream which will cover this whole tutorial and offer an opportunity for questions.

--

--