Y0/FMCC/W4 – Bee Game

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.

Made using Aseprite

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

Grid Setup

Made using p5.js

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);
}
Made using p5.js

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);
}
Made using p5.js

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.

Made using p5.js

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.

ColourTile
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.

https://editor.p5js.org/woojinJang_/sketches/eIPFKNUar

Comments

Leave a Reply

Your email address will not be published. Required fields are marked *