Shoot ’Em Up
Shoot ’em ups (a.k.a. shmups, STGs, shooters) are games where you pilot a ship and fire bullets. Your goal is simple: survive and defeat enemies. But with that simple goal, there’s challenge, fun, and depth. Many shmups have scoring systems, adding even more replayability to game. Shmups go back decades. I’m talking about games like Galaga, R-Type, Dodonpachi. Intense action games with an arcade lineage. They also happen to be my favorite type of game to play and make.
TODO: screenshot showing an example of the game type or what we’ll make
Shmups are great for learning how to make games because you can build something fun and challenging and begin iterating on it quickly, experimenting with systems and enemy behaviors. The core of the game is having a playable ship that can fire bullets, enemies that spawn and attack, and some sort of win state. These simple basics can be expanded upon endlessly.
For our shoot ’em up, we’re going to make a game where enemies spawn in waves. You have to defeat them (or they have to exit the screen) before the next wave spawns. You’ll have 60 seconds survive, defeat as many enemies as possible, and get the high score. Some enemies will dive down the screen, others will fire bullets at the player. By the end of this chapter, we’ll have made a shmup that you can tune, expand, and make your own. We’ll also dive deep on collision detection and finding a balance between challenging gameplay and enjoyable dodging.
This chapter builds upon the foundations from the Dodge ’Em Up chapter, so if you’re new to programming and Usagi Engine, read that first.
Moveable Player
Ensure you have Usagi Engine installed. Run
usagi init shmup to create your new project. Open your new project folder in
your code editor. Then run usagi dev in the folder to start up your game in
dev mode.
We’ll start by drawing a square to represent our player that can be moved around the screen. In the Dodge ’Em Up chapter, you may have noticed that if you press Up and Right (or any diagonal combination), the player moved faster than they did when moving in the cardinal directions. In a lot of shmups, this isn’t ideal, as you want movement to be precise. In order to make the distanced traveled in all 8 possible directions the same, we need to normalize our input.
Here’s the starting place for our game in main.lua:
local player_size = 16
local player_speed = 180 -- px/s
function _config()
---@type Usagi.Config
return {
name = "Shmup",
game_id = "com.brettmakesgames.shmuptutorial",
game_width = 320,
game_height = 320,
}
end
function _init()
State = {
player = {
x = usagi.GAME_W / 2 - player_size / 2,
y = usagi.GAME_H - 60
}
}
end
function _update(dt)
local input_delta = { x = 0, y = 0 }
if input.held(input.UP) then
input_delta.y -= 1
end
if input.held(input.DOWN) then
input_delta.y += 1
end
if input.held(input.LEFT) then
input_delta.x -= 1
end
if input.held(input.RIGHT) then
input_delta.x += 1
end
local normalized_input = util.vec_normalize(input_delta)
State.player.x += normalized_input.x * player_speed * dt
State.player.y += normalized_input.y * player_speed * dt
State.player.x = util.clamp(State.player.x, 0, usagi.GAME_W - player_size)
State.player.y = util.clamp(State.player.y, 0, usagi.GAME_H - player_size)
end
function _draw(dt)
gfx.clear(gfx.COLOR_WHITE)
gfx.rect_fill(
State.player.x, State.player.y,
player_size, player_size, gfx.COLOR_BLACK
)
end
We set player_size and player_speed variables. The local keyword is new
and worth explaining a bit, as it impacts how Usagi’s live reload works and
what’s accessible in your game’s source code as it expands into multiple files.
By default, when you create a variable in Lua, like x = 10, it is a global
variable. That means that any part of your game’s source code can read its value
and even change it. This is powerful but risky. It’s easy to accidentally
sometimes create global variables and accidentally change them when you didn’t
intend to. When Usagi live reloads your games code, it does not update
global variables unless you press Ctrl + R or F5. On the
other hand, the local keyword says: only within this file or function or chunk
of code is this variable accessible. Usagi does re-evaluate local
variables when you change them. For our player_size and player_speed, if you
change them and save main.lua, the engine will re-evaluate your new values.
This is helpful for tuning speed and trying out different values to see what
feels right.
In our _config() function, we set the name of our game and the game_id.
Change the game_id to com.usagiengine.YOURUSERNAME.shmup, where you actually
put in your username/handle. This should be a unique identifier for your game,
which is used for the save data location on people’s computers. The game_width
and game_height tell Usagi Engine to make our game field those specified
sizes. You can change these values to whatever you want, but for our game, a
square field feels good since you don’t have to worry about covering a wide
distance to reach enemies on the other side of the screen. Enemies will fly in
from the top, which will make our shoot ’em up a vertically-oriented game.
In _init(), we create a global State table with our player’s position.
State is a common way in Usagi games to have a global to contain all of the
game’s data, allowing for easy access. Since State is global, it doesn’t
change when the game is live reloaded, which is what we want. This lets our
player stay in the same position when our game code changes. You could change
player_speed and instantly test that new value without the entire game
reseting. The math in the player x and y value centers our player
horizontally and places the y value 60 pixels up from the bottom of the game.
The values of usagi.GAME_W and usagi.GAME_H correspond to what we set in
_config. Yoou could just hardcode 320 instead for each of them, but if you
decide to change the width or height of your game, you’ll be left searching for
and updating all of those old values. When possible, it’s best to not use
magic numbers for values in our game.
The _update function contains our player movement, similar to Dodge ’Em Up.
Except rather than changing the player’s x and y value in the if checks,
we update a variable called input_delta. input_delta is a Lua table that
lets us set whether or not there was movement on a given axis. By using 1,
we’re creating what’s known as a unit vector, which makes normalizing it on the
diagonals easier. Then we call util.vec_normalize(input_delta) after our input
checks. util is a collection of functions that Usagi provides to make common
operations easier. That function returns a new table with the values normalized.
When you press right and down, rather than x and y both being 1, the value
of both are: 0.7071.... This makes it so that the distance traveled is the
same in all directions. We then take that normalized value and multiply it by
the player_speed and dt (dt is delta time, the amount of time since our
last _update call). This gives us the new position for the State.player.
After that, we prevent the player from moving off the screen by calling
util.clamp on the x and y position of the player. util.clamp takes three
values: the value you want to limit, the lower limit, and the upper limit. If
the value is below the lower limit, then the lower limit is returned. If the
value is higher than the upper limit, the upper limit is return. Otherwise, the
value is returned.
Finally, in _draw, we clear the screen so we have a white background. And then
draw a black rectangle at the State.player’s position.
This was a whole lot for the first section of our chapter, but we’ve got a good
starting point to build upon. Tweak the player_speed and player_size to see
what happens.
View the source code for this section.
Firing Bullets
Let’s make our player’s ship fire bullets upward. We’ll keep track of them in a Lua table. Each frame we’ll move them upward and if they scroll off the screen, we’ll remove them from the table.
Start by setting up some local variables at the top of main.lua:
local fire_delay = 0.1 -- s
local fire_timer = 0
local bullet_speed = 420 -- px/s
local player_bullet_w = 4
local player_bullet_h = 10
We’ll use all of these variables for firing and drawing bullets.
In our State table, add a new empty table for bullets:
State = {
player = {
x = usagi.GAME_W / 2 - player_size / 2,
y = usagi.GAME_H - 60,
bullets = {}
}
}
We’ll add new bullets into that table when they’re fired and loop through it for updating the bullets and draw them on the screen.
In our _update function, below where we handle player movement, add the
following code:
fire_timer -= dt
if fire_timer <= 0 and input.held(input.BTN1) then
local bul_y = State.player.y - player_bullet_h
-- fire 3 bullets
table.insert(State.player.bullets,
{ x = State.player.x - player_bullet_w, y = bul_y })
table.insert(State.player.bullets,
{ x = State.player.x + player_size / 2 - player_bullet_w / 2, y = bul_y })
table.insert(State.player.bullets,
{ x = State.player.x + player_size, y = bul_y })
fire_timer = fire_delay
end
for i = #State.player.bullets, 1, -1 do
local bullet = State.player.bullets[i]
-- move the bullet upward
bullet.y -= bullet_speed * dt
-- remove bullets that have flown off the top of the screen
if bullet.y < -player_bullet_h then
table.remove(State.player.bullets, i)
end
end
In each frame, we subtract the dt from fire_timer to count it down. Then, if
the fire_timer is less than or equal to 0 and the player is pressing BTN1
(keyboard: Z or gamepad: A by default), then fire three bullets. The firing of a
bullet uses the Lua function table.insert, which appends a new bullet at the
x and y position to State.player.bullets table. Then, finally, we reset
the fire_timer to fire_delay, which restarts the countdown, adding a slight
gap between each time a set of bullets get fired.
The for i = #State.player.bullets, 1, -1 do line of code is a loop that goes
through the player’s bullets in reverse, moving them up the screen by
subtracting the bullet_speed * dt from each bullet’s y position. If the
bullet is so far up the screen that’s it’s no longer visible (the negative
height of the bullet), then we need to remove it from the player’s bullets
table. We have to loop through the bullets in reverse order so that if we do
remove a bullet, those in the array from that position onward will properly
shift into position. If you didn’t reverse the order of looping through the
bullets, if you removed the first bullet, they remaining would shift forward,
causing the next iteration of the loop to skip one and potentially access an
index that no longer exists.
Now we need to draw our bullets by looping through them at the bottom of
_draw() and drawing a light gray rectangle:
for _, bullet in ipairs(State.player.bullets) do
gfx.rect_fill(bullet.x, bullet.y,
player_bullet_w, player_bullet_h, gfx.COLOR_LIGHT_GRAY)
end
The for _, bullet in ipairs(State.player.bullets) do line loops through each
of the bullets in State.player.bullets. The code between the for ... do and
its end is called for each bullet in that list. ipairs returns two values,
the index of the item in the list and the actual item in the list. We set the
index variable to _, meaning we don’t use it.
In less than 100 lines of code, we’ve got a pretty good feeling player ship that moves around the screen and fires bullets. Not bad!

View the source code for this section.
Defeating Enemies
A shmup without enemies is no shmup at all! Let’s spawn enemies that fly down the screen and when they’re hit by the player’s bullet, they lose health points (HP). When their HP drops to 0, they’ll disappear.
Start by defining the local variable hit_flash_time. It’s the time in
seconds that an enemy will flash then they’re hit by a bullet:
local hit_flash_time = 0.2 -- secs
Then define a new function that returns a new enemy table at a given position. This function makes it easy to keep all of the different of an enemy close together.
function init_enemy(x, y)
return {
x = x,
y = y,
hp = 12,
w = 16,
h = 16,
speed = 44, -- px/s
color = gfx.COLOR_RED,
flash_timer = 0
}
end
We’ll call this function soon. The returned table has the width (w) and height
(h), the color, the speed, and the flash_timer to keep track of changing
the enemy’s color when they’re hit. It’s worth noting that there’s a downside to
putting all of these values in a table like this: when our game code live
reloads, the State.enemies contains the old values, not the new ones until
either new enemies spawn or you press Ctrl + R to reload your game.
Since our game is so simple right now, that’s not a big deal. But it’s worth
seeing the difference in approach compared to using local variables we use the
different player properties. Later on this chapter, we’ll break up our code into
multiple files and revise how this is handled. But for now, reutnring the table
like this works.
We’ll store our enemies in a table in State, spawning three of them with our
new init_enemy function:
function _init()
State = {
player = {
x = usagi.GAME_W / 2 - player_size / 2,
y = usagi.GAME_H - 60,
bullets = {}
},
enemies = {
init_enemy(72, -20),
init_enemy(usagi.GAME_W - 72, -20),
init_enemy(usagi.GAME_W / 2, -60),
},
}
end
Then in _update, in our bullet loop, loop through each enemy and check if the
bullet overlaps with any of the enemies:
-- check if the bullet has overlapped with any of the enemies
for _, enemy in ipairs(State.enemies) do
if util.rect_overlap(
{ x = bullet.x, y = bullet.y,
w = player_bullet_w, h = player_bullet_h },
enemy) then
bullet.dead = true
enemy.hp -= 1
enemy.flash_timer = hit_flash_time
end
end
-- remove bullets that have flown off the top of the screen
if bullet.y < -player_bullet_h or bullet.dead then
table.remove(State.player.bullets, i)
end
util.rect_overlap is a function Usagi provides that checks if two rectangles
are intersecting. It returns true if they are. Each rectangle passed to this
function must be a table with an x, y, w, and h key and value. If they
do overlap, then we set the bullet’s dead property to true, reduce the
enemy’s hp, and set the enemy’s flash_timer to the local variable we
created earlier for how long to change the color when the enemy is hit.
Then, right after that, where we were checking for bullets that fly off the
screen, we also check if bullet.dead to see if we should remove dead bullets
as well. Just add or bullet.dead to the check that previously existed.
Now, similar to bullets and still in _update, we need to move our enemies down
the screen and remove them if they’ve run out of hp or fly off the screen:
for i = #State.enemies, 1, -1 do
local enemy = State.enemies[i]
enemy.y += enemy.speed * dt
if enemy.flash_timer > 0 then
enemy.flash_timer = enemy.flash_timer - dt
end
if enemy.hp <= 0 or enemy.y > usagi.GAME_H then
table.remove(State.enemies, i)
end
end
We loop through in reverse, just like bullets. And we set enemy.flash_timer to
the previous value minus dt, reducing that timer. We’ll check that in the
_draw code to know which color to draw the enemy.
When we defeat all of our enemies, let’s spawn some more, at the end of our
_update function:
if #State.enemies == 0 then
table.insert(
State.enemies,
init_enemy(72, -20)
)
table.insert(
State.enemies,
init_enemy(usagi.GAME_W - 72, -20)
)
table.insert(
State.enemies,
init_enemy(usagi.GAME_W / 2, -60)
)
end
There’s nothing too fancy here. We check if the number of enemies is 0 and
spawn more if so.
In _draw, after we draw our player rectangle but before we draw bullets,
we’ll loop through each enemy and draw them, factoring in whether or not their
flash_timer is greater than 0. If it is, then we’ll draw the enemy as pink
instead of the red that we set in init_enemy:
for _, enemy in ipairs(State.enemies) do
local color = enemy.color
if enemy.flash_timer > 0 then
color = gfx.COLOR_PINK
end
gfx.rect_fill(enemy.x, enemy.y, enemy.w, enemy.h, color)
end
Our game is starting to have glimmers of being fun with enemies endlessly approach and bullets we can hit them with.
Enemy Bullets - Aimed Shots
Our game is a bit easy though. Sure, we could make the enemies move faster or spawn more of them. But the best way to add challenge (and fun) is to make the enemies fire back. In shoot ’em ups, there are two broad categories of enemy shot types: aimed shots that move toward the player’s position and shots that move in a specific pattern, regardless of the player’s location. We’ll focus on aimed shots in this section, making our enemies fire bullets toward the player’s position at the time of fire. This allows the player to dodge them by always needing to stay in motion. This is slightly different than a homing shot, which would follow the player where they move, requiring them to either shoot the missle down or somehow shake it off (homing shots would be a cool thing for you to add once this chapter is over!).
We’ll make our enemy bullets quite large compared to the player’s. Add a new variable at the top of the file for representing the width and height of the enemy bullets:
local enemy_bullet_size = 12
Add two new properties to our returned enemy table in init_enemy() that we can
use to track when a enemy should fire a bullet:
fire_timer = 1.5, -- seconds until first shot
fire_delay = 0.4, -- seconds between shots
shots_fired = 0,
shots_limit = 3,
fire_timer will be used to countdown 1.5 seconds and then have the enemy fire
their first bullet. We’ll reset fire_timer after each shot to fire_delay,
which will set the delay of future shots to 0.2s. We’ll use shots_fired to
count how many times the enemy has spat out a bullet and stop firing once that
number reaches shots_limit.
In the _init function’s State table, add a new key: enemy_bullets that’s
initialized to an empty table: {}:
enemy_bullets = {},
We’ll keep track of enemy bullets separate from each enemy so that even after an enemy dies or flies of their screen, their bullets live on, carrying out their mission to destroy us.
We need to make it so that our enemies fire bullets in our _update function
within the enemies loop. We’ll be calculating the linear velocity of the bullet
based on the angle of the enemy toward the player. We’ll use the power of
trigonometry to accomplish this! Right after the code where we handle updating
the enemy’s flash timer, add this:
enemy.fire_timer -= dt
if enemy.fire_timer <= 0 and enemy.shots_fired < enemy.shots_limit then
local ex = enemy.x + enemy.w / 2 - enemy_bullet_size / 2
local ey = enemy.y + enemy.h
-- bullet center positions
local bcx = ex + enemy_bullet_size / 2
local bcy = ey + enemy_bullet_size / 2
local angle = math.atan(
(State.player.y + player_size / 2) - bcy,
(State.player.x + player_size / 2) - bcx
)
table.insert(State.enemy_bullets,
{
x = ex,
y = ey,
angle = angle,
})
enemy.shots_fired += 1
enemy.fire_timer = enemy.fire_delay
end
There’s a lot here. Let’s break it down and go over what’s happening.
We subtract dt from the enemy’s fire_timer so that it counts down, just like
our other timers. Then, if the fire_timer is less than or equal to 0 and
the number of shots fired is less than the limit, we insert a new bullet into
State.enemy_bullets. In order to properly aim the bullet at the player, we
need to calculate the angle at which the bullet needs to travel based on the
enemy that’s firing the bullet’s position and the player’s position at the time
of fire. This is calculated using the arctangent of the y position delta and x
position delta. Then we increment the enemy’s shots_fired and reset the
fire_timer for future checks as to whether or not the enemy should fire
another bullet.
If you’re curious about the deeper trigonometric aspects of the arctangent calculate, check out the Wikipedia page on Inverse trigonometric functions. If you’re not curious, just accept that’s how aimed shots work and move on. For what it’s worth, these aspects of math in game programming make my head spin still (maybe a sign I should study it more!).
Right below the enemy loop in _update, in a new loop, we need to loop through
each enemy bullet, update its position, check for overlap with the player, and
remove any bullets that are dead or offscreen:
for i = #State.enemy_bullets, 1, -1 do
local bullet = State.enemy_bullets[i]
local speed = 120
bullet.x += math.cos(bullet.angle) * speed * dt
bullet.y += math.sin(bullet.angle) * speed * dt
if util.rect_overlap(
{ x = bullet.x, y = bullet.y, w = enemy_bullet_size, h = enemy_bullet_size },
{ x = State.player.x, y = State.player.y, w = player_size, h = player_size }
) then
bullet.dead = true
end
if bullet.y > usagi.GAME_H or bullet.dead then
table.remove(State.enemy_bullets, i)
end
end
We need to take the bullet.angle into account when we move the bullet. In
order to calculate the linear velocity, we pass that bullet.angle into
math.cos for the x velocity and math.sin for the y velocity, multiplying
it by enemy bullet’s speed and dt.
We then check if the bullet’s rectangle overlaps the player’s rectangle. If so, the bullet is dead. (And in the future, the player will die too.)
At the end of the enemy bullet update loop, we remove any dead bullets or those that are off screen.
Finally, loop through and draw each of the State.enemy_bullets after we draw
the player bullets:
for _, bullet in ipairs(State.enemy_bullets) do
gfx.rect_fill(bullet.x, bullet.y,
enemy_bullet_size, enemy_bullet_size, gfx.COLOR_BLUE)
end
There’s nothing particularly special about this code, we draw a blue square to represent the enemy bullets.
The Usagi _draw loop draws in order of the gfx calls. Each proceeding gfx
call draws on top of the previous ones. In shmups, it’s absolutely vital
that enemies and bullets are visible. So we draw the enemy bullets last, on top
of everything else.
Enemy bullet firing is one of the more complex parts of our shmup. Now that we’ve cleared that hurdle, we’ll be making some smaller changes to make our game more challenging and fun.
Tune some of the different values in the code to see what feels good, like try changing the bullet size, the fire delay, how many shots get fired. When making games, once you have the systems in place, you can turn the knobs and see what happens, which can often lead to some delightful surprises in your game’s design.
View the source code for this section.
Hitboxes
TODO: explain how we’ll have a player hit box smaller than the player, draw it; why we do this
Refactoring Our Code
TODO:
- Explain what refactoring is
- Break up the code into multiple functions
- Making enemy and player bullet updating code shared
- Using angular velocity
- rectangle functions for enemyes, bullets, player; DRY up that code
Game Over
TODO: when player is hit by a bullet, show game over and don’t update any longer
Waves of Enemies
TODO: build a table of enemies and have them spawn one after the other
Time Out
TODO: counting down time that remains from 60s
Scoring
TODO: killing enemies faster leads to higher score
High Score Tracking
TODO: saving it, displaying it, loading it
Sound Effects
TODO: explain how to make sfx and play them back in the game; player shot, enemy hit, enemy explosion, player death; using pitch variation and tweaking volume a bit
Sharing Our Game
Bonus Credits
TODO: list out ways to expand upon the shmup; ideas: bombs, music, sprites, explosion effects, adding homing shots/missles; player lives
Possible Future Expansions
- Sprites - I’m on the fence if I want to introduce that here or in a future game, like Sokoban or the action platformer
- Starfield
- More enemy types
- More bullet patterns, like spirals