Making Games with p5.play

← back to Creative Computing

By Allison Parrish

In this tutorial, I’m going to take you through Paolo Pedercini’s p5.play library. This is a very opinionated introduction, and it leaves out a lot of the functionality that makes p5.play interesting! Be sure to consult the official examples and reference documentation to get a sense of everything that the library can do.

The p5.play library provides a number of helpful objects and functions for making games and other interactive applications. The objects and functions it introduces are incorporated into p5.js just as though they were pre-programmed in the library.

Installation

Download the library here. Unzip the archive. You’ll find a directory in the archive called lib; copy the p5.play.js file from that directory into your sketch. Make sure to add the necessary <script> tag to your sketch’s index.html file as well. (More information here about installing external libraries when using the p5.js editor.)

Sprites

A “sprite” is an object in a game (or other interactive application) that knows its own size and position on the screen. Sprite objects typically expose an interface that allows the programmer to change the sprite’s position, trajectory and appearance, and to allow the programmer to easily ask questions about the sprite, such as if it intersects a particular position (or another sprite).

A single sprite

Creating a sprite in p5.play is accomplished using the createSprite() function. This function returns a sprite object, which itself has a number of attributes and methods that allow us to query and change properties of the sprite.

Here’s a simple example that creates a single sprite:

► run sketch ◼ stop sketch
let spr;
function setup() {
  createCanvas(400, 400);
  spr = createSprite(
    width/2, height/2, 40, 40);
  spr.shapeColor = color(255);
  spr.velocity.y = 0.5;
}
function draw() {
  background(50);
  drawSprites();
}
function mousePressed() {
  spr.position.x = mouseX;
  spr.position.y = mouseY;
}

The createSprite() function takes four parameters: the position of the sprite, and its width and height. The .shapeColor attribute sets the color of the rectangle that represents the sprite. In order for p5.play to display the sprite, we need to add the drawSprites() function to the end of draw().

Every sprite has a position attribute and a velocity attribute. Both of those attributes have x and y attributes, which you can set to control the position of the sprite and its velocity (in both dimensions). The p5.play library takes care of updating the position according to the velocity for you—you don’t have to do any of the math. In the example above, the sprite is constantly moving downwards, unless you click the mouse, in which case the sprite is instantly moved to the mouse position.

Sprites on the move

As mentioned above, you can set the sprite’s velocity directly with .velocity.x and .velocity.y. You can also call the sprite’s setSpeed() attribute to tell the sprite to move in a particular direction at a particular rate. In this example, use the arrow keys to control the sprite:

► run sketch ◼ stop sketch
let spr;
function setup() {
  createCanvas(400, 400);
  spr = createSprite(
    width/2, height/3, 40, 40);
  spr.shapeColor = color(255);
}
function draw() {
  background(50);
  fill(255);
  noStroke();
  textAlign(CENTER, CENTER);
  text("use arrow keys, or SPACE to stop",
    width/2, height*0.67);
  drawSprites();
}
function keyPressed() {
  if (keyCode == RIGHT_ARROW) {
    spr.setSpeed(1.5, 0);
  }
  else if (keyCode == DOWN_ARROW) {
    spr.setSpeed(1.5, 90);
  }
  else if (keyCode == LEFT_ARROW) {
    spr.setSpeed(1.5, 180);
  }
  else if (keyCode == UP_ARROW) {
    spr.setSpeed(1.5, 270);
  }
  else if (key == ' ') {
    spr.setSpeed(0, 0);
  }
  return false;
}

(The key variable in p5.js only works for alphanumeric characters. In order to detect the arrow keys, we need to use the keyCode variable.)

Adding gravity to your sketch is as easy as adding a constant downward force on every frame (using the .setSpeed() method). Here’s an example that causes a sprite to be drawn to the screen, which moves downward on every frame and then bounces when it reaches the bottom:

► run sketch ◼ stop sketch
let spr;
function setup() {
  createCanvas(400, 400);
  spr = createSprite(width/2, height/2,
    40, 40);
  spr.shapeColor = color(255);
  spr.velocity.y = 0;
}
function draw() {
  background(50);
  if (spr.position.y >= height) {
    spr.velocity.y *= -1;
    // set to height to prevent "tunneling"
    spr.position.y = height;
  }
  // constant downward speed
  // (i.e., gravity)
  spr.addSpeed(0.25, 90);
  drawSprites();
}
function mousePressed() {
  spr.position.y = mouseY;
}

Following the mouse

There are a number of ways to make a sprite follow the mouse. The first is to set the position directly:

► run sketch ◼ stop sketch
let spr;
function setup() {
  createCanvas(400, 400);
  spr = createSprite(
    width/2, height/2, 40, 40);
  spr.shapeColor = color(255);
}
function draw() {
  background(50);
  spr.position.x = mouseX;
  spr.position.y = mouseY;
  drawSprites();
}

You can also add a bit of lag to the sprite’s movement by setting the X and Y velocity to the difference between the sprite’s position and the mouse’s position:

► run sketch ◼ stop sketch
let spr;
function setup() {
  createCanvas(400, 400);
  spr = createSprite(
    width/2, height/2, 40, 40);
  spr.shapeColor = color(255);
}
function draw() {
  background(50);
  spr.velocity.x = (mouseX - spr.position.x) * 0.2;
  spr.velocity.y = (mouseY - spr.position.y) * 0.2;
  drawSprites();
}

Finally, you can use the .attractionPoint() method to set a force that pushes the sprite in the direction of the mouse’s position:

► run sketch ◼ stop sketch
let spr;
function setup() {
  createCanvas(400, 400);
  spr = createSprite(
    width/2, height/2, 40, 40);
  spr.shapeColor = color(255);
  spr.rotateToDirection = true;
  spr.maxSpeed = 2;
  spr.friction = 0.25;
}
function draw() {
  background(50);
  if (mouseIsPressed) {
    spr.attractionPoint(0.5, mouseX, mouseY);
  }
  drawSprites();
}

In this example, we also set the object’s .maxSpeed attribute (which controls how fast a sprite can move, regardless of the forces operating on it), its .friction attribute (which is a multiplier that slowly reduces the velocity of the object on each frame), and the .rotateToDirection attribute (which, when set to true, causes the object to rotate to the direction it’s moving).

Mouse events

Sprites in the p5.play framework come with a built-in mechanism for detecting whether or not the user is interacting with the sprite using the mouse. There are two ways to check for mouse interaction: callbacks or boolean attributes.

There are four attributes of a sprite object that you can assign functions to in order to define the sprite’s behavior in relation to the user’s mouse movement. The following example illustrates all four:

► run sketch ◼ stop sketch
let spr1;
let spr2;
function setup() {
  createCanvas(400, 400);

  spr1 = createSprite(width/2, height/3,
    100, 100);
  spr1.shapeColor = color(255);
  spr1.onMouseOver = function() {
    this.scale = 2;
  }
  spr1.onMouseOut = function() {
    this.scale = 1;
  }

  spr2 = createSprite(width/2, height*0.67,
    100, 100);
  spr2.shapeColor = color(0);
  spr2.onMousePressed = function() {
    this.shapeColor = color(128);
  }
  spr2.onMouseReleased = function() {
    this.shapeColor = color(0);
  }
}
function draw() {
  background(50);
  drawSprites();
}

The four attributes are:

  • onMouseOver (when the mouse cursor moves over the object)
  • onMouseOut (when the mouse cursor leaves the object)
  • onMousePressed (when the user presses the mouse button, and the mouse cursor is over the object)
  • onMouseReleased (when the user releases the mouse button, after an onMousePressed event)

The function that you assign to these attributes will be executed whenever the specified event occurs. Inside the function, the expression this refers to the object that the interaction happened to. (This is helpful for writing event handlers that can be applied to more than one object; see below.)

Every sprite object also has a mouseIsOver attribute, which has a boolean value: true if the mouse is currently over the object, and false otherwise. In the following example, the two sprites respond when the mouse is over them. (For the second sprite, the reaction behavior only happens if the mouse button is pressed as well.)

► run sketch ◼ stop sketch
let spr1;
let spr2;
function setup() {
  createCanvas(400, 400);
  spr1 = createSprite(width/2, height/3,
    100, 100);
  spr1.shapeColor = color(255);
  spr1.mouseActive = true;
  spr2 = createSprite(width/2, height*0.67,
    100, 100);
  spr2.shapeColor = color(0);
  spr2.mouseActive = true;
}
function draw() {
  background(50);
  if (spr1.mouseIsOver) {
    background(100);
  }
  if (spr2.mouseIsOver && mouseIsPressed) {
    spr2.rotation += 4;
  }
  drawSprites();
}

Note also in this example the use of the .rotation attribute, which sets the sprite’s current rotation (in degrees).

Multiple sprites

You can call the createSprite() function as many times as you want to! The p5.play framework keeps track of all the sprites you’ve added behind the scenes (so you don’t need to create your own data structure to store them). In the following example, I’ve written some code in mousePressed() that creates a new sprite whenever the user clicks the mouse:

► run sketch ◼ stop sketch
function setup() {
  createCanvas(400, 400);
}
function draw() {
  background(50);
  drawSprites();
}
function mousePressed() {
  let spr = createSprite(width/2, height/2,
    random(10, 50), random(10, 50));
  spr.shapeColor = color(255);
  spr.velocity.y = random(3);
  spr.velocity.x = random(-3, 3);
  spr.position.x = mouseX;
  spr.position.y = mouseY;
  spr.friction = 0.1;
  spr.life = 120;
}

Note here the use of the .life attribute, which is the maximum number of frames that the sprite will “live” before it’s automatically deleted by the p5.play framework.

If you want to apply changes to the sprites after they’re created, other than the changes that the p5.play framework performs on its own, you’ll need to iterate over every sprite in the draw() method. The framework supplies a built-in array called allSprites which contains every active sprite in the sketch. In the following example, we use the allSprites variable to apply “gravity” (i.e., a constant downward force) to each sprite added to the scene in mousePressed(). Another if statement checks to see if the sprite has extended beyond the height of the sketch, and causes it to “bounce” if so. Still another if statement removes any sprites that have exceeded the boundary of the sketch in the X dimension.

► run sketch ◼ stop sketch
function setup() {
  createCanvas(400, 400);
}
function draw() {
  background(50);
  for (let i = 0; i < allSprites.length; i++) {
    // gravity
    allSprites[i].addSpeed(0.1, 90);
    if (allSprites[i].position.y > height) {
      allSprites[i].velocity.y *= -1;
    }
    // any code that removes sprites should be
    // the *last* thing in the loop!
    if (allSprites[i].position.x > width ||
        allSprites[i].position.x < 0) {
      allSprites[i].remove();
    }
  }
  textAlign(RIGHT, TOP);
  text("sprite count: " + allSprites.length,
    width-10, 10);
  drawSprites();
}
function mousePressed() {
  let spr = createSprite(width/2, height/2,
    random(10, 50), random(10, 50));
  spr.shapeColor = color(255);
  spr.velocity.y = random(3);
  spr.velocity.x = random(-3, 3);
  spr.position.x = mouseX;
  spr.position.y = mouseY;
}

Examples only from this point forward—more notes TK!

Events on multiple sprites

► run sketch ◼ stop sketch
let score = 0;
function setup() {
  createCanvas(400, 400);
  for (let i = 0; i < 10; i++) {
    let spr = createSprite(
      random(width), random(height),
      random(10, 50), random(10, 50));
    spr.shapeColor = random(255);
    spr.onMouseOver = removeAndScore;
  }
}
function draw() {
  background(50);
  drawSprites();
  fill(255);
  noStroke();
  textSize(72);
  textAlign(CENTER, CENTER);
  if (score < 10) {
    text(score, width/2, height/2);
  }
  else {
    text("you win!", width/2, height/2);
  }
}
function removeAndScore() {
  score += 1;
  this.remove();
}
  • every sprite shares the same removeAndScore function—the this keyword keeps everything straight

Sprite groups

► run sketch ◼ stop sketch
let clouds;
let birds;
function setup() {
  createCanvas(400, 400);
  clouds = new Group();
  birds = new Group();

  for (let i = 0; i < 10; i++) {
    let c = createSprite(
      random(width), random(height),
      random(25, 100), random(25, 100));
    c.shapeColor = color(random(200, 255));
    clouds.add(c);
  }
  for (let i = 0; i < 5; i++) {
    let b = createSprite(
      random(width), random(height),
      random(10, 50), random(5, 25));
    b.shapeColor = color(255, 0, random(255));
    b.friction = random(0.01, 0.1);
    b.maxSpeed = random(1, 4);
    b.rotateToDirection = true;
    birds.add(b);
  }
}
function draw() {
  background(0, 150, 240);
  for (let i = 0; i < clouds.length; i++) {
    clouds[i].position.x += clouds[i].width * 0.01;
    if (clouds[i].position.x > width) {
      clouds[i].position.x = 0;
    }
  }
  for (let i = 0; i < birds.length; i++) {
    birds[i].attractionPoint(0.2, mouseX, mouseY);
  }
  drawSprites();
}
  • Group()
  • allows you to “categorize” sprites and give them different behaviors.

Collisions

► run sketch ◼ stop sketch
let spr1;
let spr2;
function setup() {
  createCanvas(400, 400);
  spr1 = createSprite(
    width/2, height/2, 150, 150);
  spr1.shapeColor = color(0);
  spr2 = createSprite(0, 0, 50, 50);
  spr2.shapeColor = color(128);
}
function draw() {
  background(50);
  spr2.velocity.x = (mouseX-spr2.position.x)*0.2;
  spr2.velocity.y = (mouseY-spr2.position.y)*0.2;
  if (spr2.overlap(spr1)) {
    spr1.shapeColor = color(255);
  }
  else {
    spr1.shapeColor = color(0);
  }
  drawSprites();
}
  • the .overlap() method returns true if one sprite overlaps another.
► run sketch ◼ stop sketch
let spr1;
let spr2;
function setup() {
  createCanvas(400, 400);
  spr1 = createSprite(
    width/2, height/2, 100, 100);
  spr1.shapeColor = color(0);
  spr2 = createSprite(0, 0, 50, 50);
  spr2.shapeColor = color(128);
}
function draw() {
  background(50);
  spr2.velocity.x = (mouseX-spr2.position.x)*0.2;
  spr2.velocity.y = (mouseY-spr2.position.y)*0.2;
  spr2.collide(spr1);
  drawSprites();
}
  • .collide()
► run sketch ◼ stop sketch
let spr1;
let spr2;
function setup() {
  createCanvas(400, 400);
  spr1 = createSprite(
    width/2, height/2, 100, 100);
  spr1.shapeColor = color(0);
  spr2 = createSprite(0, 0, 50, 50);
  spr2.shapeColor = color(128);
}
function draw() {
  background(50);
  spr2.velocity.x = (mouseX-spr2.position.x)*0.2;
  spr2.velocity.y = (mouseY-spr2.position.y)*0.2;
  spr2.displace(spr1);
  drawSprites();
}
  • .displace()

Group collisions

► run sketch ◼ stop sketch
let walls;
let boxes;
let player;
function setup() {
  createCanvas(400, 400);
  walls = new Group();
  boxes = new Group();
  player = createSprite(100, 100, 40, 40);
  player.shapeColor = color(255);
  for (let i = 0; i < 5; i++) {
    let w = createSprite(
      random(125, width-125), (height/5)*i,
      random(10, 100), random(10, 100));
    w.shapeColor = color(0);
    walls.add(w);
  }
  for (let i = 0; i < 4; i++) {
    let b = createSprite(
      random(50, 100), random(100, height-100),
      25, 25);
    b.shapeColor = color(255, 0, 0);
    boxes.add(b);
  }
}
function draw() {
  background(50);
  player.velocity.x = 
    (mouseX-player.position.x)*0.1;
  player.velocity.y = 
    (mouseY-player.position.y)*0.1;
  player.collide(walls);
  player.displace(boxes);
  boxes.collide(walls);
  boxes.displace(boxes);
  drawSprites();
}

Collision callbacks

► run sketch ◼ stop sketch
let coins;
let player;
let score = 0;
function setup() {
  createCanvas(400, 400);
  coins = new Group();
  for (let i = 0; i < 10; i++) {
    let c = createSprite(
      random(100, width-100),
      random(100, height-100),
      10, 10);
    c.shapeColor = color(255, 255, 0);
    coins.add(c);
  }
  player = createSprite(50, 50, 40, 40);
  player.shapeColor = color(255);
}
function draw() {
  background(50);
  player.velocity.x = 
    (mouseX-player.position.x)*0.1;
  player.velocity.y = 
    (mouseY-player.position.y)*0.1;
  player.overlap(coins, getCoin);
  drawSprites();
  fill(255);
  noStroke();
  textSize(72);
  textAlign(CENTER, CENTER);
  if (coins.length > 0) {
    text(score, width/2, height/2);
  }
  else {
    text("you win!", width/2, height/2);
  }
}
function getCoin(player, coin) {
  coin.remove();
  score += 1;
}

Images and animations

► run sketch ◼ stop sketch
let kitty;
let kittyImg;
function preload() {
  kittyImg = loadImage('kitty_transparent.png');
}
function setup() {
  createCanvas(400, 400);
  kitty = createSprite(width/2, height/2);
  kitty.addImage(kittyImg);
}
function draw() {
  background(255);
  kitty.position.x = mouseX;
  kitty.position.y = mouseY;
  if (mouseIsPressed) {
    kitty.rotation += 2;
  }
  drawSprites();
}

Animations

(example animation below from the p5.play examples)

► run sketch ◼ stop sketch
let spr;
let anim;
function preload() {
  anim = loadAnimation("asterisk_normal0001.png",
    "asterisk_normal0002.png",
    "asterisk_normal0003.png");
}
function setup() {
  createCanvas(400, 400);
  spr = createSprite(width/2, height/2);
  spr.addAnimation("default", anim);
}
function draw() {
  background(255);
  spr.position.x = mouseX;
  spr.position.y = mouseY;
  if (mouseIsPressed) {
    spr.rotation -= 2;
  }
  drawSprites();
}