Growing up I absolutely loved arcade top-down shooters, I’d spend hours running and gunning trying to best my previous score. In particular, I’d play a lot of zombie shooters, with the goal of simply surviving for as long as possible.
In this blog, we’re going to create our own top-down-run-and-gun-zombie-shooter. So buckle up and get your pistols ready.
The goal
By the end of this blog, we should have a working game that’ll give us the foundation to extend and create something special! The main goal here is to outline the fundamental game-development concepts for creating a top-down shooter. This blog is suitable for all levels of programming and is an appropriate follow on from the Top 5 BEST games to code as a beginner, but if you haven’t read it, no worries, you should be fine. We’ll be using p5.js to code the game for ease of interaction with the HTML canvas – to jump right in, I’d suggest opening up the online p5.js editor.
The main protagonist
The first thing we’re going to do is create our hero and give them the ability to walk.
Create a new file, call it Player.js
Then we’ll need to reference that file in our index.html
<!DOCTYPE html>
<html lang="en">
<head>
<script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.4.0/p5.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.4.0/addons/p5.sound.min.js"></script>
<link rel="stylesheet" type="text/css" href="style.css">
<meta charset="utf-8" />
</head>
<body>
<script src="sketch.js"></script>
<script src="Player.js"></script> <!-- Add this -->
</body>
</html>
We’ll create a Player class that’ll have one field pos
to represent the position of the player on the screen (pos
is a vector that contains the x and the y location). Also, we’ll add a draw()
function that’ll just contain a rect
(rectangle) for the moment to be our player.
class Player {
constructor() {
this.pos = createVector(width / 2, height / 2)
}
draw() {
rect(this.pos.x, this.pos.y, 20, 20);
}
}
Now we need to update the sketch.js
to create the player
object and draw them:
let player;
function setup() {
createCanvas(700, 700);
player = new Player();
}
function draw() {
background(100, 100, 100);
rectMode(CENTER);
player.draw();
}
Which, when played should give us:

There we have our zombie destroyer.
Player movement
In most top-down shooters I’ve played you’d use WASD to control the player’s up/down, left/right movement and then the mouse to determine where the player’s facing. You could also move at the same speed in all directions, so let’s do that.
Let’s add an update
method to our Player
class that’ll update the pos
vector depending on which keys are currently held down.
class Player {
constructor() {
this.pos = createVector(width / 2, height / 2)
}
draw() {
rect(this.pos.x, this.pos.y, 20, 20);
}
update() { // add this
let xSpeed = 0;
let ySpeed = 0;
if (keyIsDown(65)) {
xSpeed = -2;
}
if (keyIsDown(68)) {
xSpeed = 2;
}
if (keyIsDown(87)) {
ySpeed = -2;
}
if (keyIsDown(83)) {
ySpeed = 2;
}
this.pos.add(xSpeed, ySpeed);
}
}
And update sketch.js
to call the update
method on the player
object.
let player;
function setup() {
createCanvas(700, 700);
player = new Player();
}
function draw() {
background(100, 100, 100);
rectMode(CENTER);
player.draw();
player.update(); // add this
}
Hit play, and you should have something like this:

Rotating the player to the mouse position
Given the position of the mouse we need to determine the angle from the player’s position to that specified point and then rotate the player so that they’re facing the cursor. To do this we can use the atan2 function which makes our life a bit easier.
So the two functions that are changing here are within the Player
class, the draw()
and the update()
. Here’s the code, I’ll explain in more detail underneath.
class Player {
constructor() {
this.pos = createVector(width / 2, height / 2)
this.angle = 0;
}
draw() { // update draw
push();
translate(this.pos.x, this.pos.y);
rotate(this.angle);
rect(0, 0, 20, 20);
pop();
}
update() {
let xSpeed = 0;
let ySpeed = 0;
if (keyIsDown(65)) {
xSpeed = -2;
}
if (keyIsDown(68)) {
xSpeed = 2;
}
if (keyIsDown(87)) {
ySpeed = -2;
}
if (keyIsDown(83)) {
ySpeed = 2;
}
this.pos.add(xSpeed, ySpeed);
this.angle = atan2(mouseY - this.pos.y, mouseX - this.pos.x); // add this
}
}
So each frame in our update()
function we’re setting the angle using the atan2
we spoke about above, mouseY
and mouseX
are two handy properties p5 provides us with to track the position of the cursor.
In the draw()
function you’ll notice we’ve added a few things here:
push()
andpop()
functions are always used together, it allows us to add transformation settings that doesn’t cascade to other objects.translate()
allows us to displace objects within the window. So we’re translating to the position of the player to make it our point of origin. This allows us to rotate from that point. Notice that because we’ve translated to the position of the player, we set thex
andy
in therect(x, y
.. to 0 (Because we’ve translated to that position).rotate()
does exactly what you’d expect, rotates.
You should now have something like this:

Adding zombies
Our game is feeling a little bit empty, let’s add a horde of zombies.
Taking similar steps as we did before, let’s create a new file Zombie.js
(Remember to add the reference in the index.html
as we did earlier)
The blue areas in the image below are our zombie spawning zones. They can either come from the top or from the bottom.

We’re also going to provide the zombie with a speed
argument that’ll be used later on to make the game more difficult.
Here’s the code for the Zombie
class, I’ll explain in more detail underneath:
class Zombie {
constructor(speed) {
this.speed = speed;
let y;
if (random(1) < 0.5) {
// from the top
y = random(-300, 0);
} else {
// from the bottom
y = random(height, height + 300);
}
let x = random(-300, width + 300);
this.pos = createVector(x, y);
}
draw() {
push();
fill(100, 255, 100);
rect(this.pos.x, this.pos.y, 20, 20);
pop();
}
update() {
let difference = p5.Vector.sub(player.pos, this.pos);
difference.limit(this.speed);
this.pos.add(difference);
}
}
When a zombie
object is created we pass in a speed (To begin with we’ll just hardcode it to some value). random(1) < 1
creates a random number between 0 and 1 and if it’s less than 0.5 we spawn the zombie from the top else we’ll spawn it from the bottom.
The draw()
function just creates a green rectangle, notice how we’re using push()
and pop()
again, so that the colour green doesn’t cascade – try it out for yourself, remove the push()
and pop()
to see what happens.
The update()
function subtracts the vector for the position of the player and the position of the zombie, the difference is added to the zombie position, we limit this by the zombie speed. This allows the zombie to move towards the player.
Next, let’s open up sketch.js
and create the array of zombies:
let player;
let zombies = []; // add this
function setup() {
createCanvas(700, 700);
player = new Player();
}
function draw() {
background(100, 100, 100);
rectMode(CENTER);
player.draw();
player.update();
for (let zombie of zombies) { // add this
zombie.draw();
zombie.update();
}
if (frameCount % 200 == 0) { // add this
zombies.push(new Zombie(2));
}
}
Notice how we’re creating a zombie
object every 200 frames and setting the speed to 2, these will both change later.
You should now have something that resembles the following:

Rotating the zombies towards us
At the moment the zombies aren’t facing us, they’re just rigid rectangles that don’t fancy rotating. Let’s add some rotation so it really looks as though they’re focusing on us.
This is similar code to before where we rotated the player’s position to the mouse cursor. Update the draw()
function in the Zombie
class to be the following:
draw() {
push();
fill(100, 255, 100);
let angle = atan2(player.pos.y - this.pos.y, player.pos.x - this.pos.x);
translate(this.pos.x, this.pos.y);
rotate(angle);
rect(0, 0, 20, 20);
pop();
}
Now we should have the zombie’s undivided attention:

Adding the ability to shoot the zombies!
We’ve got zombies, we’ve got their attention, we’ve not got a gun. Let’s fix that.
Let’s create a new Bullet.js
file (Make sure to add it to the index.html
as done previously)
class Bullet {
constructor(x, y, angle) {
this.x = x;
this.y = y;
this.angle = angle;
this.speed = 16;
}
draw() {
push();
fill(0);
circle(this.x, this.y, 5);
pop();
}
update() {
this.x += this.speed * cos(this.angle);
this.y += this.speed * sin(this.angle);
}
}
We provide the bullet class with an x
and a y
which represents the player position at the point of shooting the bullet. The angle of the player is also passed in. The update()
function moves the bullet along the trajectory.
Let’s create an array of bullet
objects in the Player
class and have a function called shoot()
which’ll create the bullets:
class Player {
constructor() {
this.pos = createVector(width / 2, height / 2)
this.angle = 0;
this.bullets = []; // add this
}
draw() {
push();
translate(this.pos.x, this.pos.y);
rotate(this.angle);
rect(0, 0, 20, 20);
pop();
for (let bullet of this.bullets) { // add this
bullet.update();
bullet.draw();
}
}
update() {
let xSpeed = 0;
let ySpeed = 0;
if (keyIsDown(65)) {
xSpeed = -2;
}
if (keyIsDown(68)) {
xSpeed = 2;
}
if (keyIsDown(87)) {
ySpeed = -2;
}
if (keyIsDown(83)) {
ySpeed = 2;
}
this.pos.add(xSpeed, ySpeed);
this.angle = atan2(mouseY - this.pos.y, mouseX - this.pos.x); // add this
}
shoot() { // add this
this.bullets.push(new Bullet(this.pos.x, this.pos.y, this.angle));
}
}
Notice how we are updating and drawing the bullets all within the draw()
function. Usually you’d separate this so that you’d only do updating in the update()
function and drawing in the draw()
function, but for simplicity I’ve added both here.
Next, in sketch.js
we need to call the shoot()
function when the player clicks the mouse button:
function mouseClicked() {
player.shoot();
}
Bullet collision

Bullet collision
It’s all well and good being able to shoot, but if the bullets ain’t gonna hit, what’s the point?
Let’s add some code to detect if a bullet has hit a zombie and if it has we remove the zombie from the array.
We can determine if a bullet has hit a zombie looking at the following image. Given half the width of the player and the radius of the bullet. If the distance between the two objects is less than or equal to the sum of those values. We have a hit.

In the player
class let’s add a new method hasShot(zombie)
:
hasShot(zombie) {
for (let i = 0; i < this.bullets.length; i++) {
if (dist(this.bullets[i].x, this.bullets[i].y, zombie.pos.x, zombie.pos.y) < 15) {
this.bullets.splice(i, 1);
return true;
}
}
return false;
}
This method returns true if we’ve shot the passed in zombie and returns false if we haven’t. If we have shot them, we remove the bullet from the bullets
array.
I’ve just hardcoded the summed radius here to 15. Ideally, you’d the radius of the player and the radius of the bullet as variables.
Next, we need to update the sketch.js
so that we remove the zombie if we hit them:
let player;
let zombies = [];
function setup() {
createCanvas(700, 700);
player = new Player();
}
function draw() {
background(100, 100, 100);
rectMode(CENTER);
player.draw();
player.update();
// add this
for (let i = zombies.length - 1; i >= 0; i--) {
zombies[i].draw();
zombies[i].update();
if (player.hasShot(zombies[i])) {
zombies.splice(i, 1);
}
}
if (frameCount % 200 == 0) {
zombies.push(new Zombie(2));
}
}
function mouseClicked() {
player.shoot();
}
Notice how we are looping through the zombies backwards, this is good practice when removing items from an array so we don’t mess up the indexes.

Making the game progressively more difficult
At the moment the game is too easy. The longer we’re alive the more difficult the game should get. We’re going to increase the difficulty in two ways:
- Increase the number of zombies
- Increase the speed of the zombies
Increasing zombie count
Let’s create two new variables in sketch.js
, zombieSpawnTime
and frame
.
We will increment the frame each time draw()
is executed and spawn a zombie if frame >= zombieSpawnTime
Then we will decrease the zombieSpawnTime
so that zombies are more frequent. sketch.js
should now look like:
let player;
let zombies = [];
// add these
let zombieSpawnTime = 300;
let frame = 0
function setup() {
createCanvas(700, 700);
player = new Player();
}
function draw() {
background(100, 100, 100);
rectMode(CENTER);
player.draw();
player.update();
for (let i = zombies.length - 1; i >= 0; i--) {
zombies[i].draw();
zombies[i].update();
if (player.hasShot(zombies[i])) {
zombies.splice(i, 1);
}
}
// add this
if (frame >= zombieSpawnTime) {
zombies.push(new Zombie(2));
zombieSpawnTime *= 0.95;
frame = 0;
}
frame++;
}
function mouseClicked() {
player.shoot();
}
After 20 seconds

After 1 minute

After 2 minutes 😱

You’ll likely want to add a cap on the number of zombies that spawn 😂
Increasing zombie speed
As the game goes on the zombies should increase their speed. A fun way to do this would be to generate a random speed for a zombie. But, increase the maximum potential speed the longer you survive.
For this, we need one new variable in sketch.js
, zombieMaxSpeed
:
let player;
let zombies = [];
let zombieSpawnTime = 300;
let zombieMaxSpeed = 2; // add this
let frame = 0
function setup() {
createCanvas(700, 700);
player = new Player();
}
function draw() {
background(100, 100, 100);
rectMode(CENTER);
player.draw();
player.update();
for (let i = zombies.length - 1; i >= 0; i--) {
zombies[i].draw();
zombies[i].update();
if (player.hasShot(zombies[i])) {
zombies.splice(i, 1);
}
}
if (frame >= zombieSpawnTime) {
zombies.push(new Zombie(random(zombieMaxSpeed)));
zombieSpawnTime *= 0.95;
frame = 0;
}
if (frameCount % 1000 == 0) { // add this
zombieMaxSpeed += 0.1;
}
frame++;
}
function mouseClicked() {
player.shoot();
}
So every 1000 frames, we increase the maximum speed of a zombie.

Good luck.
Adding a point system
The first thing we need to do is make it possible for zombies to eat you.
Add the following ateYou()
method to the Zombie
class:
ateYou() {
return dist(this.pos.x, this.pos.y, player.pos.x, player.pos.y) < 20;
}
This will return true if the zombie touches you.
Now let’s restart the game if the zombie eats you, your sketch.js
should look like the following:
let player;
let zombies = [];
let zombieSpawnTime = 300;
let zombieMaxSpeed = 2;
let frame = 0
function setup() {
createCanvas(700, 700);
player = new Player();
}
function draw() {
background(100, 100, 100);
rectMode(CENTER);
player.draw();
player.update();
for (let i = zombies.length - 1; i >= 0; i--) {
zombies[i].draw();
zombies[i].update();
if (zombies[i].ateYou()) { // add this
restart();
break;
}
if (player.hasShot(zombies[i])) {
zombies.splice(i, 1);
}
}
if (frame >= zombieSpawnTime) {
zombies.push(new Zombie(random(zombieMaxSpeed)));
zombieSpawnTime *= 0.95;
frame = 0;
}
if (frameCount % 1000 == 0) {
zombieMaxSpeed += 0.1;
}
frame++;
}
// add this
function restart() {
player = new Player();
zombies = [];
zombieSpawnTime = 300;
zombieMaxSpeed = 2;
}
function mouseClicked() {
player.shoot();
}
And your game:

Adding a score
Adding a score is really simple. We’ve already got code that tells us when we’ve shot a zombie, let’s just increment a score when that happens.
let player;
let zombies = [];
let zombieSpawnTime = 300;
let zombieMaxSpeed = 2;
let frame = 0
let score = 0; // add this
function setup() {
createCanvas(700, 700);
player = new Player();
}
function draw() {
background(100, 100, 100);
rectMode(CENTER);
player.draw();
player.update();
for (let i = zombies.length - 1; i >= 0; i--) {
zombies[i].draw();
zombies[i].update();
if (zombies[i].ateYou()) {
restart();
break;
}
if (player.hasShot(zombies[i])) {
score++; // add this
zombies.splice(i, 1);
}
}
if (frame >= zombieSpawnTime) {
zombies.push(new Zombie(random(zombieMaxSpeed)));
zombieSpawnTime *= 0.95;
frame = 0;
}
if (frameCount % 1000 == 0) {
zombieMaxSpeed += 0.1;
}
frame++;
// add these
textAlign(CENTER);
textSize(40);
text(score, width/2, 100);
}
function restart() {
player = new Player();
zombies = [];
zombieSpawnTime = 300;
zombieMaxSpeed = 2;
score = 0; // don't forget to reset the score :D
}
function mouseClicked() {
player.shoot();
}
Notice the use of the text
functions, these are p5.js functions and make it really easy to display text in the format we want on the canvas.

What next?
There are so many things we can add to this now we’ve got down the basic mechanics.
- Add new weapons
- Add zombies that are harder to kill
- Weapon upgrades at the expense of points
- Increase player speed at the expense of points
- Add images to represent the characters (Below is my attempt)

Here’s a link to the code shown in this blog. And here’s a link to the extra bits I’ve done above (The code in this one may be a little different to the original).