I’m going to attempt a more complex game in p5.js using sprites and multiple levels. My game will be about a bee who needs to collect honey from flowers before they go back to their hive. It will be played on a grid.

The first thing I did was prepare a set of sprites.
Grid Setup

I first set up a grid using nested (or two dimensional) arrays. And tested drawing sprites to the screen, aligned to the grid.
The flowers seen in the image are generated using p5’s noise function. I made it scroll by shifting the noise coordinates by the frameCount. It makes a cool effect but I’m not going to use it for my game.
Input Setup
p5 has a built-in KeyIsPressed() function but I won’t be able to use it within a class method. So I’ve decided to make my own input variables keypress and keydown
//declare variables
let keydown = false;
let keypress = false;
function draw(){
[...]
keypress = false;
if(keyIsPressed && !keydown){
keydown = true;
keypress = true;
}
if(!keyIsPressed){ keydown = false; }
[...]
}
keypress is true on the first frame the key is registered whilst keydown is true while the key is held down.
Player Object
I’m using a familiar structure for game objects with constructor, step and drawself methods. These mirror ‘Events’ in GameMaker Studio.
I declare the Player object as a class with x, y, and moves. x and y represent the player’s grid coordinates not the screen coordinates.
class player {
constructor(x,y,moves){
this.x = x;
this.y = y;
this.tx = x;
this.ty = y;
this.moves = moves;
}
}
I can then use the step event to let the user move the player character around by pressing the arrow keys on their keyboard.
step(){
//handle inputs
let keyUp = keypress && keyCode == 38;
let keyDown = keypress && keyCode == 40;
let keyLeft = keypress && keyCode == 37;
let keyRight = keypress && keyCode == 39;
this.x += keyRight - keyLeft;
this.y += keyDown - keyUp;
this.drawself();
}
I then draw the character to the screen, translating the grid coordinates to screen coordinates.
drawself(){
image(img_bee,this.x*gridElW,this.y*gridElH-hover);
}

The player now moves around the screen, but it feels very jumpy. I’m adding two new variables to solve this target x and target y. Instead of directly changing the bee’s x and y coordinates we will change their target and interpolate them to make a smooth animation.
[...]
this.tx += keyRight - keyLeft;
this.ty += keyDown - keyUp;
this.x = lerp(this.x,this.tx,0.5);
this.y = lerp(this.y,this.ty,0.5);
[...]
I will also add a slight hover to the bee so that it ‘flies’ over the ground. This is purely visual so it can go directly into the drawself() method.
drawself(){
let hover = sin(frameCount*0.1)*2+2
image(img_bee,this.x*gridElW,this.y*gridElH-hover);
}

The bee’s movement is now far smoother and much more pleasing to look at. This also helps the player track their character as well.
Collision Checks
I want to implement a quick and simple collision check when the player inputs a movement key. This can be done by checking the grid element the player is attempting to move to to see if it’s collidable.
let checkX = min(max(0,floor(this.tx)+dx),grid.length-1);
let checkY = min(max(0,floor(this.ty)+dy),grid[checkX].length-1);
I need to clamp the checked values so that I don’t try to access values outside the grid array.
let collisionCheck = grid[checkX][checkY];
if(collisionCheck == img_rock || collisionCheck == img_rock1){
dx = 0;
dy = 0;
}
If the position checked is a rock, then it stops the movement from happening.
Loading Levels
As a side-project I did some more tinkering with using the p5 noise function to generate levels.

I used different values of perlin noise to decide whether to generate grass, flowers, water or rocks.
for(let x = 0; x < gridSizeX; x++){
grid[x] = [];
for(let y = 0; y < gridSizeY; y++){
grid[x][y] = img_grass;
if(noise(x*0.2,y*0.2) < 0.35){ grid[x][y] = random([img_rock,img_rock1,img_grass]); }
if(noise(x*0.2,y*0.2) > 0.50){ grid[x][y] = img_flower; }
if(noise(x*0.2,y*0.2) > 0.60){ grid[x][y] = img_water; }
}
}
Actually loading a level is far simpler. I wrote a function to match a pixel to an array.
function matchPixel(arr1,arr2) {
return (arr1[0] == arr2[0] && arr1[1] == arr2[1] && arr1[2] == arr2[2] && arr1[3] == arr2[3])
}
This has to be done because JavaScript does not allow for comparing two arrays directly.

Levels are stored as images with each pixel representing a tile on the grid.
| Colour | Tile |
| GREEN (‘#00FF00’) | Grass |
| WHITE (‘#FFFFFF’) | Flower |
| BLACK (‘#000000’) | Rock |
| YELLOW (‘#FFFF00’) | Hive |
I use a row of if statements to load the correct tile for each pixel.
function loadLevel(level) {
let gridSizeX = level.width;
let gridSizeY = level.height;
for(let x = 0; x < gridSizeX; x++){
grid[x] = [];
for(let y = 0; y < gridSizeY; y++){
let getpixel = level.get(x,y);
grid[x][y] = img_grass;
if(matchPixel(getpixel,[0,0,0,255])){
grid[x][y] = img_rock;
}
if(matchPixel(getpixel,[1,255,0,255])){
grid[x][y] = img_grass;
}
if(matchPixel(getpixel,[255,255,0,255])){
grid[x][y] = img_hive;
}
if(matchPixel(getpixel,[255,255,255,255])){
grid[x][y] = img_flower;
}
}
}
}

This makes it very easy to create new levels and new tile types can be created (up to 16.7 million tile types).
Interacting with Tiles
I already check the tile the player is moving to and store it in the collisionCheck variable. I can use an if statement to check when the player moves to interact with tiles that the player character moves on to.
if(dx+dy != 0){
if(collisionCheck == img_flower){
this.moves += 4;
}
if(this.moves <= 0){
this.die();
dx = 0;
dy = 0;
}
if(collisionCheck == img_hive){
currentlvl++;
loadLevel(lvls[currentlvl]);
this.die();
dx = 0;
dy = 0;
}
}
I check if the player is on a flower and add 5 movement points,
I check if the player has 0 or less moves remaining and kill it, as well as stopping it from moving.
I check if the player is on a hive and load the next level.
die(){
this.tx = 1;
this.ty = 1;
this.moves = 5;
}
The death function simply resets the bee’s position and movement points.
Final Game

There are many areas of potential improvement, but the game is basically complete. I used just a few simple levels to show off the basic gameplay but more complex features and better designed levels can easily be added in.
