A plushy controlled game (Part 1)

A journey into p5.js

I have been fascinated by video games ever since I saw the first level of Mario. They dominated a big part of my youth and are what got me into programming in the first place. Building a website for our Call of Duty clan was probably one of the first times I can into contact with HTML and CSS.

Recently however, my fascination has shifted from playing games, to discovering how they are built. Physics are a big part of what makes a game feel natural, making things fall with gravity, being blown away by wind, etc. I had no idea how to even start coding physics, until colleague of mine told me about this great book he had read: The Nature of Code. The book describes in incredible detail how you could program physics in a pretty clear and straightforward way. Best of all, you can find it online, for free! :o

Next to the physics part, how your audience interacts with your game determines a lot of the experience. I have always loved the idea of using a camera to control your game so I decided I wanted to use my laptop camera as the main way to interact with the game.

What are we building?

So what do you get when you mix physics and computer vision? A demo concept “game” controlled by a plush animal! So fluffy! <3

In this first post we will focus on the basic elements of a game. We’ll cover how to draw items on a canvas and program in some realistic looking physics.

The basic game background

For this experiment, we need a place for the little digital version of our big friend. To make it easy for myself, I decided to use a helper library.

p5.js is a library with a ton of convenient functions to manipulate the DOM and draw on the canvas. It makes it really easy to create a nice environment for our virtual buddy.

Feel free to inspect the webpage :-) This is a canvas-element!


So how exactly do we draw this background using the p5.js library?

// main.js
let bgImg;

function preload() {
    bgImg = loadImage('assets/background.png');
}

function setup() {
    let canvas = createCanvas(640, 360);
}

function draw() {
    background(bgImg);
}

After including the p5.js library, the functions above get called automatically.

  • preload: here you load images, videos, etc.
  • setup: here you add items to your webpage, like a canvas.
  • draw: this method gets called constantly, in a loop. This is where most of our code will eventually be invoked.

To learn more about these functions and other things p5.js can do for you, check out the p5.js website.

Adding game objects

We got our little world drawn out. Time to add some life to it! A lot of elements in games consist of the same couple of properties. We will capture these similarities in a single class which each object can inherit.

There are the 3 physics related properties all objects should have.

Position: where is an object?
Velocity: where is the object going and how fast is it going there?
Acceleration: Is the object speeding up, or is it slowing down?

These 3 properties impact one another. Following the laws of physics:

Position of an object changes because of our velocity.
Velocity of an object changes because of our acceleration.
Acceleration of an object changes because of external forces being applied to them.

At any one point, in the real world, we are affected by different forces. The most famous one might be gravity but there are many others like magnetism and friction. All these forces have a direction and a magnitude, making it easy to represent them as vectors.

You can go ahead and look at the code. This blog is not meant to explain all the details of Vectors and Forces to you. If you want to lean more about that, the first 2 chapters in the Nature of Code book does a better job explaining it than I could.

// gameobject.js
class GameObject {
    constructor(x, y) {
        this.position = new createVector(x, y);
        this.velocity = new createVector(0, 0);
        this.acceleration = new createVector(0, 0);
    }

    update() { 
        this.velocity.add(this.acceleration);
        this.position.add(this.velocity);

        // After update, set acceleration to 0 for next loop
        this.acceleration = new createVector(0,0);
    }

    // Forces, like gravity can be applied here!
    applyForce(vector) {
        this.acceleration.add(vector);
    }

    // CODE OMITTED
}

Another thing we need is the ability to check if two objects touch. This is done by calculating so called bounding boxes. When the boxes of two objects overlap, the objects can be considered to be touching. For simple objects, a single BoundingBox is enough. For more complex shapes, multiple BoundingBox objects can be combined.

// gameobject.js
class GameObject {
    
    // CODE OMITTED
    
    // Abstract methods don't exist in Javascript, throwing error
    // Throwing error instead if they are not implemented
    // Each object must implement this
    boxes() {
        throw "Bounding boxes not implemented";
    }

    // checks if two groups of boxes collide
    collidesWith(other) {
        let ourBoxes = this.boxes();
        let otherBoxes = other.boxes();
        for (let a = 0; a < ourBoxes.length; ++a) {
            for (let b = 0; b < otherBoxes.length; ++b) {
                if(ourBoxes[a].intersects(otherBoxes[b])) {
                    return true;
                }
            }
        }
        return false;
    }
}

class BoundingBox {
    constructor(x, y, width, height) {
        this.x = x;
        this.y = y;
        this.width = width;
        this.height = height;
    }

    intersects(other) {
        return this.x < other.x + other.width &&
            this.x + this.width > other.x &&
            this.y < other.y + other.height &&
            this.y + this.height > other.y;
    }
}

With the basics setup here, we can finally create objects in the world. Lets start with adding our little friend!

// totz.js
class Totz extends GameObject {
    constructor(startPositionX, startPositionY, image) {
        super(startPositionX, startPositionY);
        this.drawImage = image
        this.width = image.width;
        this.height = image.height;
    }

    display() {
        // image(..) is a p5.js method to draw an image on the canvas
        image(this.drawImage, 
              this.position.x, this.position.y, 
              this.width, this.height);
    }

    boxes() {
        return [new BoundingBox(this.position.x, this.position.y, this.width, this.height)];
    }
}

Let’s see what the main Javascript file looks like now. We will load an extra image, create an instance of our little buddy and call the display method.

// main.js
let bgImg;
let totzImg;
let totz;

function preload() {
    bgImg = loadImage('assets/background.png');
    totzImg = loadImage('assets/totzSprite.png');
}

function setup() {
    let canvas = createCanvas(640, 360);
    //initialize with starting positions and image
    totz = new Totz(100, 10, totzImg); 
}

function draw() {
    background(bgImg);

    totz.display();
}

Our little friend is quite static. Let’s see what happens when we apply gravity on them. To make sure they don’t fall forever, we’ll add a floor as well!

// floor.js
class Floor extends GameObject {
    // The floor will be a straight line
    constructor(y) {
        super(0, y);
        this.width = 9000; // making floor large enough to always be covered
        this.height = 10;
    }

    display() {
        // color, fill, noStroke and rect are all p5.js functions
        let c = color(255, 204, 0); // Define color 'c'
        fill(c); // Use color variable 'c' as fill color
        noStroke(); // Don't draw a stroke around shapes
        rect(this.position.x, this.position.y, 9000, 10);
    }

    boxes() {
        return [new BoundingBox(this.position.x, this.position.y, this.width, this.height)];
    }
}

Whenever our buddy collides with the floor, we’ll make sure Totz stops falling. We’ll also add a way to make our friend jump, using a force of course!

Our main script looks like this now:

// main.js
let bgImg;
let totzImg;
let totz;
let floor;

let gravityForce;
let jumpForce;

function preload() {
    bgImg = loadImage('assets/background.png');
    totzImg = loadImage('assets/totzSprite.png');
}

function setup() {
    let canvas = createCanvas(640, 360);
    //initialize with starting positions and image
    totz = new Totz(100, 10, totzImg);
    
    // "height" is set by p5 to the height of the canvas
    floor = new Floor(height - 30); 

    // Gravity is a downward movement, so the horizontal force is 0.
    // The vertical force is 0.05, meaning down.
    gravityForce = new createVector(0, 0.05); 

    // Jumping will be a force vector as well! :-)
    jumpForce = new createVector(0, -3); 
}

function jump() {
    totz.applyForce(jumpForce);
}

// This method gets invoked by p5.js on touch.
function touchStarted() {
    jump();
    return false; // To prevent any default behavior for this event
}

function draw() {
    background(bgImg);

    // We apply the force before drawing
    totz.applyForce(gravityForce);

    // We calculate the new location by calling update()
    totz.update();

    if (totz.collidesWith(floor)) {
        // Set the entire velocity vector to 0
        totz.velocity.y = 0;
        // Set the new position to the floor
        // This is needed in case the little buddy was falling fast! 
        totz.position.y = floor.position.y - totz.height;
    }

    totz.display();
    floor.display();
}

Click / Tap to make our friend jump!


There it is! We got our little game concept ready to go! In the next post, we’ll be replacing boring clicking with some interaction with our big plushy friend! Let’s just hope our buddy doesn’t get camera shy…


Resource list


Addendum

By default, p5.js functions are defined in the the global namespace. For the purpose of this blog, it was easier to leave it that way, as it reduces some of the clutter.

However! This might not be such a great idea if you are mixing in a couple of different libraries. It also doesn’t work if you want to run multiple of these canvas-elements on a single website. Luckily, you can also run p5.js in it’s own namespace. Click here for further info on that!

comments powered by Disqus