Hack Club

Guide

Get started with p5.js

This guide assumes you've never used p5.js. You'll learn how drawing works, draw your own ghost, make it move, and finish with Ghost Hunt: the same base game everyone starts from.

Needs: a browser Language: JavaScript Time: ~30 min for the base game

1What is p5.js?

p5.js is a JavaScript library for drawing and animating in the browser. You get a canvas, a rectangle of pixels, and simple functions like ellipse() and rect() to draw on it.

Every p5.js sketch has two special functions:

  • setup() runs once when the sketch starts. Make the canvas here.
  • draw() runs again and again, about 60 times a second. Each run is one frame, like a page in a flipbook.

Open editor.p5js.org, delete the starter code, and paste this:

sketch.js
function setup() {
  createCanvas(480, 360); // make a 480 x 360 pixel canvas
}

function draw() {
  background(15, 10, 25); // paint the whole canvas dark purple
}
Press the ▶ Play button. You'll see a dark purple rectangle: that's your game screen.

How positions work

Every point on the canvas is an (x, y) pair. (0, 0) is the top-left corner. x grows to the right, y grows downward. On a 480 × 360 canvas, the middle is (240, 180). Inside your sketch, width and height hold those sizes for you.

2Draw shapes

Drawing in p5.js works like painting: pick a color, then draw a shape with it. Shapes drawn later go on top.

  • background(r, g, b) fills the whole canvas. Calling it at the start of draw() wipes the last frame.
  • fill(r, g, b) sets the color for the next shapes. Each number is 0-255: red, green, blue.
  • noStroke() turns off outlines.
  • ellipse(x, y, w, h) draws a circle or oval centered at (x, y).
  • rect(x, y, w, h) draws a rectangle from its top-left corner.
sketch.js
function setup() {
  createCanvas(480, 360);
}

function draw() {
  background(15, 10, 25);

  noStroke();                // no outlines on the shapes below

  fill(255, 240, 200);       // pale yellow
  ellipse(400, 60, 50, 50);  // a moon: x, y, width, height

  fill(40, 30, 50);          // dark purple
  rect(0, 300, 480, 60);     // the ground: x, y, width, height
}
Try it: move the moon by changing 400, 60. Change the fill numbers to make a blood-red moon.

3Draw your ghost

Now make a drawing of your own. Put the drawing inside a function with x and y parameters. Then you can draw the ghost anywhere, as many times as you want, with one line.

Every shape inside uses x and y as its starting point, so the eyes move with the body.

sketch.js
function draw() {
  background(15, 10, 25);
  drawGhost(240, 180); // draw a ghost in the middle
  drawGhost(100, 80);  // and another one, top left
}

function drawGhost(x, y) {
  noStroke();
  fill(220, 255, 230);       // minty white body
  ellipse(x, y, 60, 60);

  fill(20);                  // almost-black eyes
  ellipse(x - 12, y - 5, 10);
  ellipse(x + 12, y - 5, 10);
}
Your turn: make it yours. Add a mouth with another ellipse(), give it a hat with rect(), or change its color. Keep the body about 60 pixels wide: the click check later expects that.

4Make it move randomly

random(a, b) gives you a different number between a and b every time you call it. That's the key to anything unpredictable.

First, store the ghost's position in a variable so it can change between frames. { x: 240, y: 180 } is an object: one variable holding both numbers.

Option A: haunted drift

Nudge the ghost a random amount every frame. It shivers and wanders around like it's possessed.

sketch.js
let ghost = { x: 240, y: 180 };

function draw() {
  background(15, 10, 25);

  ghost.x += random(-3, 3); // drift a little left or right
  ghost.y += random(-3, 3); // drift a little up or down

  drawGhost(ghost.x, ghost.y);
}

Option B: teleport

This is what Ghost Hunt does. Every 60 frames (about one second), the ghost jumps to a random spot. frameCount counts frames since the start, and % gives the remainder of a division, so frameCount % 60 === 0 is true once every 60 frames.

sketch.js
let ghost;
let spawnEvery = 60; // frames between jumps (60 frames = about 1 second)

function setup() {
  createCanvas(480, 360);
  spawnGhost();
}

function draw() {
  background(15, 10, 25);
  if (frameCount % spawnEvery === 0) spawnGhost(); // every 60th frame
  drawGhost(ghost.x, ghost.y);
}

function spawnGhost() {
  ghost = { x: random(40, width - 40), y: random(40, height - 40) };
}
The 40 and width - 40 keep the ghost from spawning half off the edge. Keep your drawGhost() function from step 3 at the bottom of the file.

5Click to score

p5.js calls mousePressed() every time you click. mouseX and mouseY hold where the click happened.

dist() measures the distance between two points. The ghost is 60 pixels wide, so its radius is 30: a click closer than 30 pixels to its center is a hit. An if statement runs code only when that's true.

sketch.js
let score = 0;

function mousePressed() {
  // how far was the click from the ghost's center?
  if (dist(mouseX, mouseY, ghost.x, ghost.y) < 30) {
    score++;       // hit! add a point
    spawnGhost();  // and move the ghost somewhere new
  }
}

// at the end of draw():
fill(255);
textSize(16);
text('Score: ' + score, 12, 26);
Try it: click the ghost a few times. The score goes up and the ghost jumps somewhere new.

6Add a timer

millis() is how many milliseconds have passed since the sketch started (1000 ms = 1 second). Save it when the round begins, and subtract to see how long the round has lasted.

sketch.js
let duration = 30; // seconds per round
let startTime;

// in setup():
startTime = millis();

// in draw(), before drawing the ghost:
let elapsed = (millis() - startTime) / 1000;
let timeLeft = max(0, duration - elapsed);

textAlign(RIGHT, TOP);
text('Time: ' + ceil(timeLeft), width - 12, 10);

7Put it together

One last idea: a state variable decides what the screen shows. 'start' shows a title, 'play' runs the game, 'over' shows your score. A click moves between them.

Here's the complete game. Replace everything in the editor with it:

sketch.js
let ghost;
let score = 0;
let duration = 30;
let startTime;
let state = 'start'; // 'start' | 'play' | 'over'
let spawnEvery = 60;

function setup() {
  createCanvas(480, 360);
  spawnGhost();
}

function draw() {
  background(15, 10, 25);

  if (state === 'start') {
    textAlign(CENTER, CENTER);
    fill(255);
    textSize(22);
    text('Click to start', width / 2, height / 2);
    return;
  }

  if (state === 'over') {
    textAlign(CENTER, CENTER);
    fill(255);
    textSize(22);
    text('Game over\nScore: ' + score + '\nClick to retry', width / 2, height / 2);
    return;
  }

  let elapsed = (millis() - startTime) / 1000;
  let timeLeft = max(0, duration - elapsed);
  if (timeLeft <= 0) {
    state = 'over';
    return;
  }

  if (frameCount % spawnEvery === 0) spawnGhost();
  drawGhost(ghost.x, ghost.y);

  textAlign(LEFT, TOP);
  fill(255);
  textSize(16);
  text('Score: ' + score, 12, 10);
  textAlign(RIGHT, TOP);
  text('Time: ' + ceil(timeLeft), width - 12, 10);
}

function mousePressed() {
  if (state === 'start' || state === 'over') {
    state = 'play';
    score = 0;
    startTime = millis();
    spawnGhost();
    return;
  }
  if (dist(mouseX, mouseY, ghost.x, ghost.y) < 30) {
    score++;
    spawnGhost();
  }
}

function spawnGhost() {
  ghost = { x: random(40, width - 40), y: random(40, height - 40) };
}

function drawGhost(x, y) {
  noStroke();
  fill(220, 255, 230);
  ellipse(x, y, 60, 60);
  fill(20);
  ellipse(x - 12, y - 5, 10);
  ellipse(x + 12, y - 5, 10);
}
That's Ghost Hunt. Start screen, click to score, countdown, game over, retry. It's exactly the game on the Play page. Now make it yours.

8Toolbox: make it yours

Short functions you can drop into your game. Pick 1-3 that sound fun. Want to see them all combined? Check the full demo.

Haunted wobble

~5 min

Make the ghost shiver in place while it waits to be clicked.

sketch.js
// in draw(), just before drawGhost():
ghost.x += random(-2, 2);
ghost.y += random(-2, 2);

Floating movement

~10 min

Give the ghost a speed and bounce it off the walls instead of sitting still.

sketch.js
// in spawnGhost(), after making the ghost:
ghost.vx = random(-2, 2);
ghost.vy = random(-2, 2);

// in draw(), before drawGhost():
ghost.x += ghost.vx;
ghost.y += ghost.vy;
if (ghost.x < 30 || ghost.x > width - 30) ghost.vx *= -1;
if (ghost.y < 30 || ghost.y > height - 30) ghost.vy *= -1;

Draw a pumpkin

~10 min

A second thing to draw. Swap it in for the ghost, or use it as a bonus target.

sketch.js
function drawPumpkin(x, y) {
  noStroke();
  fill(255, 140, 40);          // orange body
  ellipse(x, y, 64, 52);
  fill(60, 140, 60);           // green stem
  rect(x - 4, y - 34, 8, 12);
  fill(30);                    // spooky face
  triangle(x - 16, y - 4, x - 8, y - 12, x - 2, y - 4);
  triangle(x + 2, y - 4, x + 8, y - 12, x + 16, y - 4);
  rect(x - 12, y + 8, 24, 5);
}

See-through ghost

~5 min

A fourth number in fill() is transparency. Make the ghost fade in and out.

sketch.js
// in drawGhost(), replace the body fill with:
let alpha = map(sin(frameCount * 0.05), -1, 1, 80, 255);
fill(220, 255, 230, alpha);

Get harder over time

~10 min

Shrink the time between jumps as the round goes on.

sketch.js
// in draw(), replacing the fixed spawnEvery check:
let progress = constrain(elapsed / duration, 0, 1);
let interval = floor(lerp(60, 20, progress)); // 60 frames → 20 frames
if (frameCount % interval === 0) spawnGhost();

Particle burst on a hit

~15 min

Tiny dots fly out from the ghost when you catch it, then fade away.

sketch.js
let particles = [];

function burst(x, y) {
  for (let i = 0; i < 10; i++) {
    let a = random(TWO_PI);
    particles.push({ x: x, y: y, vx: cos(a) * 2, vy: sin(a) * 2, life: 1 });
  }
}

// call burst(ghost.x, ghost.y) on a hit, then in draw():
for (let i = particles.length - 1; i >= 0; i--) {
  let pt = particles[i];
  pt.x += pt.vx;
  pt.y += pt.vy;
  pt.life -= 0.04;
  if (pt.life <= 0) { particles.splice(i, 1); continue; }
  noStroke();
  fill(220, 255, 230, pt.life * 255);
  ellipse(pt.x, pt.y, 5);
}

9Checklist & submit

  • Your game starts from the Ghost Hunt base above
  • It has a start screen, a play state, and a game-over screen
  • It's ghost or Halloween-themed
  • You added 1-3 upgrades of your own
  • You wrote it yourself: AI only to unblock a single line

Read the full requirements once more, then submit your share link.