Hack Club

Get started with p5.js

This guide assumes you've never used p5.js. You'll learn how drawing works, draw your own ghost, and make it move.

1Set up

Select where you're completing this event!

At a club workshop, you build everything in the free p5.js web editor, so there's nothing to install.

  1. Go to editor.p5js.org. The p5.js web editor, with an arrow pointing at Sign up in the top-right corner Click Sign up in the top-right corner to make an account.
  2. Fill in a user name, email, and password, or log in with GitHub or Google. The p5.js Sign Up form It only asks for the minimum (a user name, an email, and a password), and only your user name shows up next to your sketches, so a nickname is fine.
  3. Click the pencil next to the made-up sketch name at the top and type your own. The p5.js editor, with an arrow pointing at the sketch name and its pencil icon Name your project!
  4. Here's how the editor works: The p5.js editor with five numbered parts
    1. Run: runs your code.
    2. Stop: stops it.
    3. Code: where you write your sketch.
    4. Preview: shows what your game looks like.
    5. Console: shows errors, so look here when something breaks.

2What 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, so it's where you make the canvas.
  • draw() runs again and again, about 60 times a second, and each run is one frame, like a page in a flipbook.

The p5.js web editor starts you with this code, and in VS Code you can paste it into sketch.js:

function setup() {
  createCanvas(400, 400);
}

function draw() {
  background(220);
}
Preview

Here's what each line does:

  • createCanvas(400, 400) makes a drawing area 400 pixels wide and 400 pixels tall, and it's in setup() because you only need one canvas.
  • background(220) paints the whole canvas one color, and with a single number that color is a shade of gray from 0 (black) to 255 (white), so 220 is light gray. It's in draw() so every frame starts from a clean canvas.
Press ▶ Play in the web editor, or save in VS Code with Live Server running, and you'll see a light gray square: that's your game screen.

How positions work

Every point on the canvas is an (x, y) pair, starting from (0, 0) in the top-left corner, with x growing to the right and y growing downward. On a 400 × 400 canvas the middle is (200, 200), and inside your sketch width and height hold those sizes for you.

3Draw shapes

First, set up the game screen: change the canvas to createCanvas(480, 360) (wider, like a game) and the background to background(15, 10, 25) (dark purple, for a spooky night), since the rest of the guide uses these values.

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

  • background(r, g, b) fills the whole canvas, so calling it at the start of draw() wipes the last frame.
  • fill(r, g, b) sets the color for the next shapes, with each number from 0-255 for red, green, and 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.

In setup(), make the canvas game-sized:

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

In draw(), paint the night sky and turn off outlines:

background(15, 10, 25);
noStroke();

Below that, draw a pale yellow moon, where the four numbers are its x, y, width, and height:

fill(255, 240, 200);
ellipse(400, 60, 50, 50);

Then dark purple ground along the bottom:

fill(40, 30, 50);
rect(0, 300, 480, 60);
Preview
Try it: move the moon by changing 400, 60, or change the fill numbers to make a blood-red moon.

4Draw your creature

Now make a drawing of your own and put it inside a function with x and y parameters, so you can draw it 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.

Add this function at the bottom of your sketch to draw a minty body with two dark eyes:

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);
}

Then call it in draw(), where each call draws one ghost, so this draws two:

function draw() {
  background(15, 10, 25);
  drawGhost(240, 180);
  drawGhost(100, 80);
}
Preview
zoomed in on the top-left
Your turn: make it yours by adding a mouth with another ellipse(), giving it a hat with rect(), or changing its color. Keep the body about 60 pixels wide, since the click check later expects that.

5Make it move randomly

random(a, b) gives you a different number between a and b every time you call it, which is 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, which lets one variable hold both numbers.

Option A: haunted drift

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

At the top of your sketch, store where the ghost is:

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

In draw(), nudge it a little every frame, then draw it:

function draw() {
  background(15, 10, 25);
  ghost.x += random(-3, 3);
  ghost.y += random(-3, 3);
  drawGhost(ghost.x, ghost.y);
}
Preview

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.

At the top, make the ghost and set how many frames to wait between jumps:

let ghost;
let spawnEvery = 60;

Add a function that moves the ghost to a random spot:

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

Call it once in setup() so the ghost has a starting spot:

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

In draw(), jump every 60th frame, then draw the ghost:

function draw() {
  background(15, 10, 25);
  if (frameCount % spawnEvery === 0) spawnGhost();
  drawGhost(ghost.x, ghost.y);
}
Preview
The 40 and width - 40 keep the ghost from spawning half off the edge, and your drawGhost() function from step 4 stays at the bottom of the file.

6Click to score

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

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

At the top, start the score at zero:

let score = 0;

Add mousePressed(), so a hit adds a point and moves the ghost somewhere new:

function mousePressed() {
  if (dist(mouseX, mouseY, ghost.x, ghost.y) < 30) {
    score++;
    spawnGhost();
  }
}

At the end of draw(), show the score:

fill(255);
textSize(16);
text('Score: ' + score, 12, 26);
Preview
click the ghost
Try it: click the ghost a few times and watch the score go up as the ghost jumps somewhere new.

7Add a timer

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

At the top, set how many seconds a round lasts:

let duration = 30;
let startTime;

In setup(), save when the round started:

startTime = millis();

In draw(), before drawing the ghost, work out how much time is left:

let elapsed = (millis() - startTime) / 1000;
let timeLeft = max(0, duration - elapsed);

Then show it in the top-right corner:

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

8Put it together

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

Here's the complete game, piece by piece. Clear the editor, then copy each piece in, top to bottom:

Start with the variables, where state is always one of start, play, or over:

let ghost;
let score = 0;
let duration = 30;
let startTime;
let state = 'start';
let spawnEvery = 60;

setup() makes the canvas and the first ghost:

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

draw() begins with the start and game-over screens, and return stops draw() early so nothing below it runs:

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;
  }

Still in draw(): end the round when the time runs out:

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

Then move and draw the ghost, show the score and time, and close draw():

  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);
}

mousePressed() starts a new round from the start or game-over screen, and scores hits during play:

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();
  }
}

Last, the two helper functions from earlier:

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);
}
Preview
click to play
That's Ghost Hunt, with a start screen, clicking to score, a countdown, game over, and retry. It's exactly the game on the Play page, so now make it yours.

9Make it unique

Now it's time to do your own thing! We want you to learn and try new things on your own, and it's no fun if you copy it from the guide or from AI. Here are some ideas to get you started:

Your own enemies

Swap the ghost for a bat, a skeleton, a witch, an eyeball, or something you made up, built from ellipse(), rect(), triangle(), and arc(), and try mixing 2-3 different ones.

Your own colors

Pick a palette and stick to it: toxic swamp green, blood-red moon, candy-corn orange, or icy blue, then change every fill() and background() to match.

Your own font

Use textFont('Georgia') or another font, plus textStyle(BOLD) and a bigger textSize() for your title.

Your own scene

Draw a graveyard with tombstones, a haunted house, a full moon, stars, or fog instead of a plain background.

Your own story

Give it a name and a reason to click, like catching bats before sunrise, feeding candy to a monster, or zapping zombies off your lawn, and put it on the start screen.

Your own rules

Change the timer, the ghost size, how fast it jumps, or what counts as a point, or add a creature that loses you points if you click it.

Features to add

Here are some additional features you can add! There's no code to copy here: each one points you to the p5.js docs and an example you can open, play with, and figure out for your own game. Want to see them all in action? Try the expanded demo, or pick apart the Circle Clicker example.

Haunted wobble

1-2 hours

Make your creature shiver in place by nudging its position a tiny random amount every frame.

Docs: random()
Example: Random

Floating movement

1-2 hours

Give your creature a speed in x and y so it drifts around and bounces off the edges of the canvas.

Example: Bouncing ball

A second creature

1-2 hours

Draw something new, like a pumpkin or a bat, out of arcs, triangles, and custom shapes, then use it as a bonus target or something to avoid.

See-through ghost

1-2 hours

Add a fourth number to fill() for transparency, then use sin() and map() to make your creature fade in and out.

10Already done?

Your base game works? Try these extra things to get a sticker sheet and more money for candy!

Bonus 1: Custom art & sound

Give your effects your own art, add your own sound effects, or both, like a burst you drew for when a ghost is caught or a "boo" you recorded yourself.

Bonus 2: Two more levels

Add two extra levels that each get harder than the last, by making the ghost faster, giving less time, adding more ghosts, or throwing in something new to dodge.

Finish 1 bonus: The Ghost Ship sticker sheet: jack-o-lanterns, candy, an eyeball, a BOO! sign, the ghost, a scared witch, and a Hack Club candy bar
Finish both: the sticker sheet and $10 total for candy!

Open the bonus page for hints, docs, and tools for each challenge.

Explore the p5.js docs

The p5.js reference lists every function with examples you can try, so look up these 5 and use 1-2 of them in your game:

  • loadImage() and image(): draw your own picture instead of shapes.
  • rotate(): spin or tilt your creature.
  • noise(): smooth, floaty movement that's less jittery than random().
  • lerpColor(): fade between two colors, like a sky turning blood red.
  • keyIsDown(): add keyboard controls.

11Checklist & submit

  • It's made with p5.js and is a full playable game
  • It has a start screen, gameplay, and a game-over screen
  • It's Halloween themed, with your own spooky creatures
  • It looks like your own game, not a copy of the guide
  • You wrote it yourself, using AI only to unblock a single line

Once your game is finished in the p5.js web editor, here's how to share it:

  1. Make sure you're logged in, then click File → Save so your latest changes are saved.
  2. Click File → Share and copy the Edit link, which shows your code and lets anyone press ▶ to play.
  3. Open the link in a new tab to check that the game loads and plays.
  4. Paste the link into your club's submission form, or send it to your club leader.
Built your game in VS Code instead? Switch to Submitting as an individual, follow the VS Code steps to put it on GitHub Pages, and give your club leader that link.