Making Games with p5.play
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:
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:
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:
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:
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:
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:
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:
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 anonMousePressed
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.)
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:
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.
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
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—thethis
keyword keeps everything straight
Sprite groups
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
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.
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()
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
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
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
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)
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(); }