orb-wars

Orb Run is a full arcade-style game where you steer a glowing orb with your mouse, dodge homing enemies that spawn from the screen edges, and collect pulsing orbs to raise your score before difficulty ramps up over time. It combines a menu/playing/game-over state machine, procedural gradient backgrounds, particle-style trails, and generative p5.sound audio into one self-contained sketch.

🧪 Try This!

Experiment with the code by making these changes:

  1. Give the orb a longer comet tail — Increasing maxTrailLength keeps more past positions in memory, drawing a longer fading trail behind the player.
  2. Recolor the player's gradient core — Swapping the first gradient color changes the hue the player orb blends from at its center.
  3. Ramp up difficulty twice as fast — Shrinking the divisor makes difficultyLevel climb much quicker, so enemies, spawn rates, and colors intensify sooner.
Prefer the full editor? Open it there →

📖 About This Sketch

Orb Run turns p5.js into a small arcade game: you glide a pulsing pink orb around the canvas with your mouse while enemy blobs spawn from the edges and chase you down, and you dash between glowing collectible orbs to rack up points before the difficulty curve gets too steep. Visually it layers a shifting gradient sky, a scrolling starfield, glowing gradient circles built with lerpColor, and a soft motion trail behind the player - all built from nothing but circle(), fill(), and clever math. Under the hood it also drives p5.sound oscillators and ADSR envelopes to generate every sound effect and the ambient hum live, with no audio files at all.

The code is organized around a simple state machine (gameState is 'menu', 'playing', or 'gameover') plus four ES6 classes - Star, Player, Enemy, and OrbCollectible - each responsible for its own update() and draw() logic. Studying it will teach you how to structure a real game loop in draw(), how object-oriented classes keep entities self-contained, how distance-based collision detection works, and how to synthesize game audio with oscillators and envelopes instead of loading sound files.

⚙️ How It Works

  1. When the page loads, setup() creates a full-window canvas, sets up two ambient oscillators (muted until you interact), and calls initGame() to build a fresh Player, empty enemy/orb arrays, and a field of background Star objects.
  2. Every frame, draw() first paints a smooth vertical gradient and animates the starfield, then branches based on gameState: showing the title screen, running the live game, or freezing the last frame with a 'GAME OVER' overlay.
  3. During play, updateGame() ages the difficultyLevel based on elapsed time, spawns new Enemy and OrbCollectible instances on a timer that speeds up as difficulty rises, moves the player toward the mouse, and checks distances between the player, enemies, and orbs to detect collisions.
  4. Enemies steer toward the player's current position every frame using simple vector math (direction times speed), while a sine wave makes their speed and vertical position wobble so they don't move in perfectly straight lines.
  5. Touching an orb increases the score and plays a synthesized 'bling' sound; touching an enemy ends the game, records a new best score if needed, and plays a falling tone - clicking or pressing any key afterward calls initGame() again to restart.
  6. All sound effects are generated on the fly: playBeep() spins up a fresh p5.Oscillator and p5.Envelope, slides the pitch from a start to an end frequency, and lets the envelope's attack/decay/release shape the volume before the oscillator is stopped.

🎓 Concepts You'll Learn

Game state machine (menu/playing/gameover)Object-oriented classes for game entitiesDistance-based collision detectionProcedural gradient backgrounds with lerpColorGenerative audio with p5.Oscillator and p5.EnvelopeHSB color mode for vivid collectible glowsDifficulty scaling driven by elapsed timeTrail/particle effects using an array buffer

📝 Code Breakdown

setupAudio()

This runs once in setup() to prepare two oscillators in advance. Starting them silently and fading them in later (in startAmbient()) avoids the pop/glitch of creating oscillators mid-game.

function setupAudio() {
  // Soft background space hum
  ambientOsc1 = new p5.Oscillator('sine');
  ambientOsc1.freq(80);
  ambientOsc1.amp(0);
  ambientOsc1.start();

  ambientOsc2 = new p5.Oscillator('triangle');
  ambientOsc2.freq(160);
  ambientOsc2.amp(0);
  ambientOsc2.start();
}
Line-by-line explanation (5 lines)
ambientOsc1 = new p5.Oscillator('sine');
Creates a sine-wave oscillator for the low ambient hum.
ambientOsc1.freq(80);
Sets its pitch to 80Hz, a very low background tone.
ambientOsc1.amp(0);
Starts silent so nothing plays until the user interacts (browsers block audio before a gesture).
ambientOsc1.start();
Begins the oscillator running, even though its volume is currently zero.
ambientOsc2 = new p5.Oscillator('triangle');
Creates a second oscillator with a triangle wave for a slightly richer harmony.

startAudioIfNeeded()

Modern browsers block audio playback until a user gesture (click or keypress) occurs. This function is called from mousePressed() and keyPressed() to satisfy that rule exactly once.

function startAudioIfNeeded() {
  if (!audioStarted) {
    userStartAudio(); // unlock audio context
    audioStarted = true;
    startAmbient();
  }
}
Line-by-line explanation (4 lines)

🔧 Subcomponents:

conditional First-Interaction Guard if (!audioStarted) {

Ensures the browser's audio context is only unlocked once, on the very first click or keypress.

if (!audioStarted) {
Only runs the unlock logic the first time this is called.
userStartAudio(); // unlock audio context
p5.sound helper that resumes the browser's audio context, which is required before any sound can play.
audioStarted = true;
Marks audio as unlocked so this block never runs again.
startAmbient();
Kicks off the fade-in of the ambient background hum.

startAmbient()

p5.Oscillator's amp(value, rampTime) lets you smoothly change volume over time, which is exactly how audio engineers avoid clicks and pops.

function startAmbient() {
  if (!ambientOsc1 || !ambientOsc2) return;
  // Fade in quiet ambient hum
  ambientOsc1.amp(0.03, 3.0);
  ambientOsc2.amp(0.02, 3.0);
}
Line-by-line explanation (3 lines)
if (!ambientOsc1 || !ambientOsc2) return;
Safety check - bails out if the oscillators were never created.
ambientOsc1.amp(0.03, 3.0);
Ramps the oscillator's amplitude up to 0.03 over 3 seconds, creating a slow fade-in instead of an abrupt sound.
ambientOsc2.amp(0.02, 3.0);
Fades in the second oscillator to a slightly quieter level for a layered hum.

updateAmbient()

Called every frame from draw(), this ties the ambient soundtrack directly to game difficulty - a simple way to make audio react to gameplay state.

function updateAmbient() {
  if (!ambientOsc1 || !ambientOsc2 || !audioStarted) return;
  const base1 = 60 + difficultyLevel * 3;
  const base2 = 90 + difficultyLevel * 4;
  ambientOsc1.freq(base1);
  ambientOsc2.freq(base2);
}
Line-by-line explanation (3 lines)
const base1 = 60 + difficultyLevel * 3;
Calculates a new pitch for the first oscillator that rises as difficultyLevel increases.
const base2 = 90 + difficultyLevel * 4;
Same idea for the second oscillator, rising a bit faster for more tension.
ambientOsc1.freq(base1);
Applies the new frequency every frame, making the hum's pitch creep upward as the game gets harder.

playBeep()

This is a reusable one-shot synth: rather than loading sound files, the game generates every effect live using oscillators and envelopes - a core p5.sound technique worth mastering.

function playBeep({ type = 'sine', startFreq = 440, endFreq = 440, attack = 0.01, decay = 0.1, sustain = 0.0, release = 0.1, amp = 0.4, sustainTime = 0.05 } = {}) {
  if (!audioStarted) return;
  const osc = new p5.Oscillator(type);
  osc.start();
  osc.freq(startFreq);
  osc.amp(0);

  const env = new p5.Envelope();
  env.setADSR(attack, decay, sustain, release);
  env.setRange(amp, 0);
  env.play(osc, 0, sustainTime);

  // Slide pitch
  if (startFreq !== endFreq) {
    osc.freq(endFreq, attack + decay + sustainTime + release);
  }

  // Stop after envelope
  const totalTime = attack + decay + sustainTime + release + 0.05;
  osc.stop(totalTime);
}
Line-by-line explanation (6 lines)

🔧 Subcomponents:

conditional Pitch Slide Check if (startFreq !== endFreq) {

Only slides the pitch over time if the start and end frequencies are different, saving unnecessary calls otherwise.

const osc = new p5.Oscillator(type);
Creates a brand-new oscillator each time a sound is requested, using whichever waveform type was passed in (sine, square, etc).
const env = new p5.Envelope();
An Envelope shapes volume over time using Attack-Decay-Sustain-Release (ADSR), like a synthesizer's volume knob automated over time.
env.setADSR(attack, decay, sustain, release);
Configures how quickly the sound fades in, dips to a sustain level, and fades out.
env.play(osc, 0, sustainTime);
Triggers the envelope, which controls the oscillator's amplitude according to the ADSR shape.
osc.freq(endFreq, attack + decay + sustainTime + release);
Glides the pitch from startFreq to endFreq over the total duration of the envelope, creating rising or falling tones.
osc.stop(totalTime);
Schedules the oscillator to stop completely once the sound has finished playing, freeing it up.

playStartGameSound()

This is a thin wrapper around playBeep() with a specific preset - a good pattern for keeping many distinct sound effects readable.

function playStartGameSound() {
  // Bright rising whoosh
  playBeep({
    type: 'sawtooth',
    startFreq: 300,
    endFreq: 900,
    attack: 0.01,
    decay: 0.15,
    sustain: 0,
    release: 0.2,
    amp: 0.5,
    sustainTime: 0.1
  });
}
Line-by-line explanation (3 lines)
type: 'sawtooth',
Uses a buzzy sawtooth waveform for a bright, energetic tone.
startFreq: 300,
The pitch starts at 300Hz...
endFreq: 900,
...and rises to 900Hz, giving the classic 'rising whoosh' feel of a game starting.

playCollectSound()

Tying sound parameters to game variables like difficultyLevel is a simple way to give audio feedback that scales with the action on screen.

function playCollectSound() {
  // Fast, bright bling
  playBeep({
    type: 'triangle',
    startFreq: 700 + difficultyLevel * 30,
    endFreq: 1100 + difficultyLevel * 40,
    attack: 0.005,
    decay: 0.08,
    sustain: 0,
    release: 0.1,
    amp: 0.4,
    sustainTime: 0.02
  });
}
Line-by-line explanation (2 lines)
startFreq: 700 + difficultyLevel * 30,
The collect sound's pitch rises with difficultyLevel, so orbs sound more exciting as the game speeds up.
sustainTime: 0.02
A very short sustain keeps the 'bling' snappy rather than droning on.

playEnemySpawnSound()

Falling pitches like this one are commonly used in games to signal danger - compare this to playGameOverSound() which uses the same falling-tone idea but stretched much longer.

function playEnemySpawnSound() {
  // Low thump / warning
  playBeep({
    type: 'square',
    startFreq: 180,
    endFreq: 90,
    attack: 0.005,
    decay: 0.15,
    sustain: 0,
    release: 0.15,
    amp: 0.35,
    sustainTime: 0.02
  });
}
Line-by-line explanation (3 lines)
type: 'square',
A square wave gives a harsher, more alarming tone than sine or triangle.
startFreq: 180,
Starts relatively low...
endFreq: 90,
...and drops even lower, mimicking a warning thump when a new enemy appears.

playGameOverSound()

Called from endGame(), this gives players clear audio feedback that their run has ended, using a smooth sine wave for a softer, less jarring tone than the square-wave spawn sound.

function playGameOverSound() {
  // Falling tone
  playBeep({
    type: 'sine',
    startFreq: 500,
    endFreq: 120,
    attack: 0.01,
    decay: 0.4,
    sustain: 0,
    release: 0.5,
    amp: 0.6,
    sustainTime: 0.1
  });
}
Line-by-line explanation (2 lines)
decay: 0.4,
A longer decay stretches out the falling pitch so it feels dramatic and final.
release: 0.5,
A long release lets the tone fade out slowly after the game ends, rather than cutting off abruptly.

class Star

Star is the simplest of the four classes and a good template for understanding the constructor/update/draw pattern used by Player, Enemy, and OrbCollectible too.

class Star {
  constructor() {
    this.reset();
  }

  reset() {
    this.x = random(width);
    this.y = random(height);
    this.speed = random(0.2, 1.2);
    this.size = random(1, 3);
  }

  update() {
    this.y += this.speed * (1 + difficultyLevel * 0.1);
    if (this.y > height) {
      this.y = 0;
      this.x = random(width);
    }
  }

  draw() {
    noStroke();
    fill(255, 255, 255, 120);
    circle(this.x, this.y, this.size);
  }
}
Line-by-line explanation (4 lines)

🔧 Subcomponents:

conditional Screen Wrap if (this.y > height) {

When a star scrolls past the bottom edge, it resets to the top with a new random x, creating an endless scrolling starfield.

this.speed = random(0.2, 1.2);
Each star gets its own random falling speed, giving a parallax-like sense of depth.
this.y += this.speed * (1 + difficultyLevel * 0.1);
Stars fall faster as difficultyLevel rises, subtly reinforcing that the game is speeding up.
if (this.y > height) {
Checks if the star has scrolled off the bottom of the canvas.
this.y = 0;
Resets it to the top so it can fall again, creating an infinite loop of motion.

class Player

Player is the most visually detailed class in the sketch, combining easing movement, a trail buffer, and layered circles to fake glow and gradient effects without any images or shaders.

🔬 This loop layers gradientSteps circles blended between pink and blue to fake a smooth radial gradient. What happens if you drop gradientSteps to 2? What if you push it to 30?

    const gradientSteps = 8;
    for (let i = gradientSteps; i > 0; i--) {
      const stepR = map(i, 0, gradientSteps, 0, radius);
      const col = lerpColor(
        color(255, 0, 140),
        color(120, 220, 255),
        i / gradientSteps
      );
      col.setAlpha(220);
      fill(col);
      circle(this.x, this.y, stepR * 2);
    }
class Player {
  constructor() {
    this.x = width / 2;
    this.y = height / 2;
    this.radius = PLAYER_BASE_RADIUS;
    this.trail = [];
    this.maxTrailLength = 15;
  }

  update() {
    // Move toward mouse smoothly
    let targetX = mouseX;
    let targetY = mouseY;

    // Before any mouse move, mouseX/mouseY can be NaN in some contexts
    if (isNaN(targetX) || isNaN(targetY)) {
      targetX = width / 2;
      targetY = height / 2;
    }

    let dx = targetX - this.x;
    let dy = targetY - this.y;

    const distToMouse = sqrt(dx * dx + dy * dy);
    if (distToMouse > 1) {
      const speed = min(PLAYER_MAX_SPEED, distToMouse * 0.15);
      dx = (dx / distToMouse) * speed;
      dy = (dy / distToMouse) * speed;

      this.x += dx;
      this.y += dy;
    }

    // Keep inside screen
    this.x = constrain(this.x, this.radius, width - this.radius);
    this.y = constrain(this.y, this.radius, height - this.radius);

    // Store trail
    this.trail.push({ x: this.x, y: this.y });
    if (this.trail.length > this.maxTrailLength) {
      this.trail.shift();
    }
  }

  draw() {
    // Draw trail
    noStroke();
    for (let i = 0; i < this.trail.length; i++) {
      const t = this.trail[i];
      const alpha = map(i, 0, this.trail.length - 1, 10, 120);
      const r = map(i, 0, this.trail.length - 1, 4, this.radius);
      fill(255, 80, 160, alpha);
      circle(t.x, t.y, r * 2);
    }

    // Main orb
    noStroke();
    const pulse = sin(frameCount * 0.15) * 3;
    const radius = this.radius + pulse;

    // Outer glow
    for (let i = 0; i < 3; i++) {
      const glowR = radius + i * 6;
      fill(255, 100, 200, 40 - i * 8);
      circle(this.x, this.y, glowR * 2);
    }

    // Core
    const gradientSteps = 8;
    for (let i = gradientSteps; i > 0; i--) {
      const stepR = map(i, 0, gradientSteps, 0, radius);
      const col = lerpColor(
        color(255, 0, 140),
        color(120, 220, 255),
        i / gradientSteps
      );
      col.setAlpha(220);
      fill(col);
      circle(this.x, this.y, stepR * 2);
    }

    // Small highlight
    fill(255, 255, 255, 220);
    circle(this.x - radius * 0.3, this.y - radius * 0.3, radius * 0.5);
  }
}
Line-by-line explanation (7 lines)

🔧 Subcomponents:

conditional NaN Mouse Guard if (isNaN(targetX) || isNaN(targetY)) {

Falls back to the canvas center if the mouse hasn't moved yet, preventing the orb from jumping to an invalid position.

conditional Move Toward Target if (distToMouse > 1) {

Only moves the orb if it's more than 1 pixel from the target, avoiding jittery micro-movements when already close.

for-loop Draw Trail Circles for (let i = 0; i < this.trail.length; i++) {

Draws a fading, shrinking circle for every stored past position to create a comet-like trail.

for-loop Outer Glow Rings for (let i = 0; i < 3; i++) {

Layers three semi-transparent circles of increasing size to fake a soft glow around the orb.

for-loop Gradient Core for (let i = gradientSteps; i > 0; i--) {

Draws shrinking circles blended between two colors with lerpColor to fake a smooth radial gradient.

let targetX = mouseX;
Reads the mouse's current x position as the point the orb should move toward.
const distToMouse = sqrt(dx * dx + dy * dy);
Uses the Pythagorean theorem to calculate the straight-line distance between the orb and the mouse.
const speed = min(PLAYER_MAX_SPEED, distToMouse * 0.15);
Moves faster when far from the mouse, but never faster than PLAYER_MAX_SPEED - this creates natural-feeling easing as the orb catches up.
this.x = constrain(this.x, this.radius, width - this.radius);
Keeps the orb fully inside the canvas by clamping its position based on its own radius.
this.trail.push({ x: this.x, y: this.y });
Saves the current position into an array so a trail of past positions can be drawn.
if (this.trail.length > this.maxTrailLength) {
Once the trail array gets too long, removes the oldest point so the trail has a fixed maximum length.
const pulse = sin(frameCount * 0.15) * 3;
Uses a sine wave driven by frameCount to make the orb gently grow and shrink over time - a classic p5.js pulsing effect.

class Enemy

Enemy demonstrates simple 'seek' steering: normalize a direction vector, scale it by speed, and add it to position - the same math used in flocking and pathfinding algorithms.

🔬 This loop draws 3 rings of glow around each enemy. What happens if you change the loop to run 8 times and shrink the alpha step so the glow fades more gradually?

    for (let i = 0; i < 3; i++) {
      const auraR = this.radius + i * 6;
      const c = this.color;
      const alpha = 50 - i * 10;
      fill(red(c), green(c), blue(c), alpha);
      circle(this.x, this.y, auraR * 2);
    }
class Enemy {
  constructor() {
    // Spawn on a random edge
    const edge = floor(random(4));
    if (edge === 0) {
      // top
      this.x = random(width);
      this.y = -40;
    } else if (edge === 1) {
      // right
      this.x = width + 40;
      this.y = random(height);
    } else if (edge === 2) {
      // bottom
      this.x = random(width);
      this.y = height + 40;
    } else {
      // left
      this.x = -40;
      this.y = random(height);
    }

    this.radius = random(15, 35);
    this.baseSpeed = ENEMY_BASE_SPEED + difficultyLevel * 0.2;
    this.color = color(random(40, 120), random(160, 255), random(200, 255));
    this.noiseOffset = random(1000);
  }

  update() {
    if (!player) return;

    let dx = player.x - this.x;
    let dy = player.y - this.y;
    const d = sqrt(dx * dx + dy * dy) || 1;

    // Chase behavior
    const speed =
      this.baseSpeed * (1 + 0.5 * sin(frameCount * 0.05 + this.noiseOffset));
    dx = (dx / d) * speed;
    dy = (dy / d) * speed;

    this.x += dx;
    this.y += dy;

    // Slight bobbing
    this.y += sin(frameCount * 0.06 + this.noiseOffset) * 0.3;
  }

  draw() {
    noStroke();
    // Outer aura
    for (let i = 0; i < 3; i++) {
      const auraR = this.radius + i * 6;
      const c = this.color;
      const alpha = 50 - i * 10;
      fill(red(c), green(c), blue(c), alpha);
      circle(this.x, this.y, auraR * 2);
    }

    // Main body
    fill(this.color);
    circle(this.x, this.y, this.radius * 2);

    // Eye
    fill(10, 10, 20);
    const eyeOffsetX = (player ? player.x - this.x : 0) * 0.05;
    const eyeOffsetY = (player ? player.y - this.y : 0) * 0.05;
    circle(this.x + eyeOffsetX, this.y + eyeOffsetY, this.radius * 0.7);
    fill(255);
    circle(
      this.x + eyeOffsetX * 1.3,
      this.y + eyeOffsetY * 1.3,
      this.radius * 0.25
    );
  }

  hitsPlayer() {
    if (!player) return false;
    const d = dist(this.x, this.y, player.x, player.y);
    return d < this.radius + player.radius * 0.8; // slightly forgiving
  }
}
Line-by-line explanation (6 lines)

🔧 Subcomponents:

conditional Random Edge Spawn const edge = floor(random(4));

Picks one of 4 screen edges (top/right/bottom/left) to spawn the enemy just off-screen from.

for-loop Outer Aura Rings for (let i = 0; i < 3; i++) {

Draws three fading rings around the enemy body to give it a soft glow.

calculation Collision Distance const d = dist(this.x, this.y, player.x, player.y);

Measures the distance between the enemy and the player to decide if they've collided.

this.radius = random(15, 35);
Each enemy gets a random size, so some are small and fast-feeling while others feel big and looming.
this.baseSpeed = ENEMY_BASE_SPEED + difficultyLevel * 0.2;
New enemies spawn faster as difficultyLevel rises over the course of the game.
let dx = player.x - this.x;
Calculates the direction from the enemy toward the player - the core of the 'chase' behavior.
const speed = this.baseSpeed * (1 + 0.5 * sin(frameCount * 0.05 + this.noiseOffset));
Uses a sine wave with a per-enemy phase offset so each enemy speeds up and slows down slightly out of sync with the others, feeling more organic than constant speed.
const eyeOffsetX = (player ? player.x - this.x : 0) * 0.05;
Nudges the enemy's eye slightly toward the player, making it look like it's actively watching you.
return d < this.radius + player.radius * 0.8; // slightly forgiving
Collision only counts once the distance is smaller than the combined radii (with a small forgiveness factor so near-misses don't feel unfair).

class OrbCollectible

OrbCollectible shows how switching colorMode(HSB, ...) temporarily can make it much easier to generate pleasing, related color pairs than juggling raw RGB values.

🔬 This loop blends 5 rings between two hues to build the glowing orb. What happens if you change the loop to start at 15 instead of 5 for a smoother, denser gradient?

    for (let i = 5; i > 0; i--) {
      const rr = map(i, 0, 5, 0, r * 2.3);
      const cc = lerpColor(c1, c2, i / 5);
      fill(cc);
      circle(this.x, this.y, rr);
    }
class OrbCollectible {
  constructor() {
    this.x = random(width * 0.15, width * 0.85);
    this.y = random(height * 0.15, height * 0.85);
    this.radius = random(8, 14);
    this.baseHue = random(180, 300);
    this.spawnFrame = frameCount;
  }

  update() {
    // Float slowly
    this.y += sin((frameCount - this.spawnFrame) * 0.1) * 0.2;
  }

  draw() {
    const t = frameCount * 0.12;
    const pulse = sin(t) * 2;
    const r = this.radius + pulse;

    colorMode(HSB, 360, 100, 100, 255);
    const c1 = color(this.baseHue, 80, 100, 220);
    const c2 = color((this.baseHue + 60) % 360, 100, 80, 200);

    noStroke();
    for (let i = 5; i > 0; i--) {
      const rr = map(i, 0, 5, 0, r * 2.3);
      const cc = lerpColor(c1, c2, i / 5);
      fill(cc);
      circle(this.x, this.y, rr);
    }

    colorMode(RGB, 255);
  }

  collectedBy(p) {
    const d = dist(this.x, this.y, p.x, p.y);
    return d < this.radius + p.radius * 0.6;
  }
}
Line-by-line explanation (5 lines)

🔧 Subcomponents:

for-loop HSB Gradient Rings for (let i = 5; i > 0; i--) {

Draws 5 shrinking circles blended between two hues to create a glowing gradient orb.

calculation Collection Distance const d = dist(this.x, this.y, p.x, p.y);

Determines if the player is close enough to pick up this orb.

this.baseHue = random(180, 300);
Picks a random hue in the blue/purple/magenta range for variety between orbs, using HSB color.
this.y += sin((frameCount - this.spawnFrame) * 0.1) * 0.2;
Makes the orb bob up and down slowly, offset by its own spawn time so orbs don't all bob in sync.
colorMode(HSB, 360, 100, 100, 255);
Temporarily switches p5's color mode to Hue-Saturation-Brightness, which makes picking vivid, related colors (like a hue and its complement) much easier than RGB.
const c2 = color((this.baseHue + 60) % 300, 100, 80, 200);
Note: actual code uses % 360 - creates a second color 60 degrees around the color wheel from the first, for a two-tone gradient.
colorMode(RGB, 255);
Switches color mode back to standard RGB so it doesn't affect the rest of the sketch's drawing code.

setup()

setup() runs once when the page loads. Keeping it short and delegating to setupAudio() and initGame() keeps the code organized and reusable, since initGame() is also called again every time the game restarts.

function setup() {
  createCanvas(windowWidth, windowHeight);
  setupAudio();
  initGame();
}
Line-by-line explanation (3 lines)
createCanvas(windowWidth, windowHeight);
Creates a canvas that fills the entire browser window.
setupAudio();
Prepares the ambient oscillators (silently) before anything else happens.
initGame();
Builds the player, resets score, and generates the starfield to prepare for the menu/first game.

initGame()

This function resets all game state to its starting values. It's called both from setup() (first load) and from mousePressed()/keyPressed() (restarting after game over).

function initGame() {
  player = new Player();
  enemies = [];
  orbs = [];
  score = 0;
  difficultyLevel = 1;
  startTime = millis();
  lastEnemySpawn = frameCount;
  lastOrbSpawn = frameCount;

  // Create background stars
  bgStars = [];
  const numStars = floor((width * height) / 15000);
  for (let i = 0; i < numStars; i++) {
    bgStars.push(new Star());
  }
}
Line-by-line explanation (4 lines)

🔧 Subcomponents:

for-loop Generate Starfield for (let i = 0; i < numStars; i++) {

Creates enough Star objects to fill the canvas at a consistent density based on its area.

player = new Player();
Creates a fresh Player object, resetting its position to the center of the screen.
enemies = [];
Empties the enemies array so no leftover enemies carry over between games.
startTime = millis();
Records the current time in milliseconds, used later to calculate elapsed time and difficulty.
const numStars = floor((width * height) / 15000);
Calculates how many stars to generate based on canvas area, so bigger screens get proportionally more stars.

draw()

draw() is p5.js's main animation loop, running ~60 times per second. Structuring it around a gameState variable is the simplest way to build a multi-screen app (menu, gameplay, game over) in a single sketch.

function draw() {
  backgroundGradient();

  // Draw and update starfield
  for (let star of bgStars) {
    star.update();
    star.draw();
  }

  if (gameState === 'menu') {
    drawMenu();
  } else if (gameState === 'playing') {
    updateGame();
    drawGame();
  } else if (gameState === 'gameover') {
    drawGame(); // shows frozen last state
    drawGameOver();
  }

  updateAmbient();
}
Line-by-line explanation (4 lines)

🔧 Subcomponents:

for-loop Update and Draw Stars for (let star of bgStars) {

Moves and renders every background star each frame, regardless of game state.

conditional Game State Branch if (gameState === 'menu') {

Decides what to draw based on whether the game is on the menu, actively playing, or showing the game-over screen.

backgroundGradient();
Repaints the whole canvas with a gradient sky every frame, which also clears the previous frame's drawing.
for (let star of bgStars) {
Loops through every background star, updating its position and drawing it - this runs regardless of game state so the starfield is always animated.
} else if (gameState === 'playing') {
While actively playing, both updates game logic (movement, spawning, collisions) and draws the current frame.
updateAmbient();
Adjusts the pitch of the ambient hum every frame based on the current difficulty level.

backgroundGradient()

Since p5.js doesn't have a built-in gradient fill, this function shows the common trick of drawing many thin rectangles, each interpolated with lerpColor, to simulate a smooth gradient.

🔬 This loop fakes a gradient using 40 thin bands. What happens visually if you drop steps to 5? What about pushing it to 200?

  const steps = 40;
  for (let i = 0; i < steps; i++) {
    const t = i / (steps - 1);
    const y = lerp(0, height, t);
    const h = height / steps + 2;
function backgroundGradient() {
  noFill();
  const steps = 40;
  for (let i = 0; i < steps; i++) {
    const t = i / (steps - 1);
    const y = lerp(0, height, t);
    const h = height / steps + 2;

    const base = map(difficultyLevel, 1, 10, 0, 1, true);
    // Top color
    const c1 = lerpColor(color(8, 12, 32), color(50, 0, 60), base);
    // Bottom color
    const c2 = lerpColor(color(10, 5, 25), color(10, 1, 30), base);
    const c = lerpColor(c1, c2, t);
    c.setAlpha(255);
    fill(c);
    rect(0, y, width + 1, h);
  }
}
Line-by-line explanation (5 lines)

🔧 Subcomponents:

for-loop Draw Gradient Bands for (let i = 0; i < steps; i++) {

Draws 40 thin horizontal rectangles, each a slightly different color, to fake a smooth vertical gradient.

const steps = 40;
The gradient is faked using 40 thin bands - more steps would look smoother but cost more performance.
const base = map(difficultyLevel, 1, 10, 0, 1, true);
Maps difficultyLevel (1 to 10) onto a 0-1 range, clamped (the true argument), used to blend the sky toward a more intense color as the game progresses.
const c1 = lerpColor(color(8, 12, 32), color(50, 0, 60), base);
Blends between a calm dark-blue top color and a more intense purple, based on how difficult the game currently is.
const c = lerpColor(c1, c2, t);
Blends between the top and bottom colors based on this band's vertical position (t), creating the actual top-to-bottom gradient.
rect(0, y, width + 1, h);
Draws one horizontal band of the gradient; width + 1 avoids a thin gap from floating point rounding at the right edge.

updateGame()

updateGame() is the heart of the gameplay loop - it's a great example of combining timers (spawn intervals), backward-iterating collision loops, and score accumulation in one place.

🔬 This spawns a new enemy once enough frames pass, and the interval shrinks as difficulty grows. What happens if you lower the minimum floor from max(10, ...) to max(2, ...)? The game will get chaotic much faster at high difficulty - try it.

  if (
    frameCount - lastEnemySpawn >
    max(10, ENEMY_SPAWN_INTERVAL / difficultyLevel)
  ) {
    enemies.push(new Enemy());
    lastEnemySpawn = frameCount;
    playEnemySpawnSound();
  }
function updateGame() {
  const elapsedSec = (millis() - startTime) / 1000.0;
  difficultyLevel = 1 + elapsedSec / 15.0; // gets harder over time

  player.update();

  // Spawn enemies
  if (
    frameCount - lastEnemySpawn >
    max(10, ENEMY_SPAWN_INTERVAL / difficultyLevel)
  ) {
    enemies.push(new Enemy());
    lastEnemySpawn = frameCount;
    playEnemySpawnSound();
  }

  // Spawn orbs
  if (
    frameCount - lastOrbSpawn >
    max(15, ORB_SPAWN_INTERVAL / (0.5 + difficultyLevel * 0.3))
  ) {
    orbs.push(new OrbCollectible());
    lastOrbSpawn = frameCount;
  }

  // Update enemies and check collisions
  for (let i = enemies.length - 1; i >= 0; i--) {
    const e = enemies[i];
    e.update();

    if (e.hitsPlayer()) {
      endGame();
      return;
    }

    // Remove if way off-screen
    if (
      e.x < -200 ||
      e.x > width + 200 ||
      e.y < -200 ||
      e.y > height + 200
    ) {
      enemies.splice(i, 1);
    }
  }

  // Update orbs and check collection
  for (let i = orbs.length - 1; i >= 0; i--) {
    const o = orbs[i];
    o.update();
    if (o.collectedBy(player)) {
      score += 10 + floor(difficultyLevel * 2);
      playCollectSound();
      orbs.splice(i, 1);
    }
  }

  // Small passive score over time
  score += 0.02 * difficultyLevel;
}
Line-by-line explanation (7 lines)

🔧 Subcomponents:

conditional Enemy Spawn Timer frameCount - lastEnemySpawn >

Spawns a new enemy once enough frames have passed, with the interval shrinking as difficulty rises.

conditional Orb Spawn Timer frameCount - lastOrbSpawn >

Spawns a new collectible orb on its own timer, which also speeds up with difficulty.

for-loop Update Enemies & Check Collisions for (let i = enemies.length - 1; i >= 0; i--) {

Iterates backwards through enemies so splice() can safely remove off-screen enemies mid-loop, and ends the game immediately on a hit.

for-loop Update Orbs & Check Collection for (let i = orbs.length - 1; i >= 0; i--) {

Updates each orb's floating animation and removes it (awarding score) if the player has touched it.

difficultyLevel = 1 + elapsedSec / 15.0; // gets harder over time
Difficulty rises steadily the longer you survive - after 15 seconds it's roughly level 2, after 150 seconds roughly level 11.
enemies.push(new Enemy());
Adds a brand-new Enemy instance (which picks its own random spawn edge and stats in its constructor).
for (let i = enemies.length - 1; i >= 0; i--) {
Loops backward through the array - this is the standard safe way to remove items with splice() while iterating without skipping elements.
if (e.hitsPlayer()) {
Checks if this enemy is close enough to the player to count as a hit.
endGame(); return;
Immediately ends the game and exits the function early, skipping the rest of this frame's updates.
score += 10 + floor(difficultyLevel * 2);
Awards more points per orb as difficulty rises, rewarding players who survive longer.
score += 0.02 * difficultyLevel;
Adds a tiny amount of score every single frame just for staying alive, scaled by difficulty.

drawGame()

Draw order matters in p5.js - since there's no z-index, whatever you draw last appears on top. This function carefully orders orbs, then enemies, then the player, then the HUD.

function drawGame() {
  // Draw collectibles
  for (let o of orbs) {
    o.draw();
  }

  // Draw enemies
  for (let e of enemies) {
    e.draw();
  }

  // Draw player
  player.draw();

  drawHUD();
}
Line-by-line explanation (3 lines)

🔧 Subcomponents:

for-loop Draw Orbs for (let o of orbs) {

Renders every collectible orb currently in the array.

for-loop Draw Enemies for (let e of enemies) {

Renders every active enemy on top of the orbs.

for (let o of orbs) {
Draws orbs first so they appear beneath enemies and the player visually.
player.draw();
Draws the player last so it's always visible on top of everything else.
drawHUD();
Draws the score/level/time overlay on top of the whole scene.

drawHUD()

drawHUD() shows how to build a simple game overlay using just rect() and text() - no HTML UI elements needed, everything is drawn directly onto the canvas.

function drawHUD() {
  const padding = 16;

  textFont('system-ui');
  textAlign(LEFT, TOP);
  noStroke();

  // Score box
  const scoreText = 'Score: ' + nf(score, 1, 1);
  const levelText = 'Lv ' + nf(difficultyLevel, 1, 1);
  const timeText =
    gameState === 'playing'
      ? 'Time: ' + nf((millis() - startTime) / 1000.0, 1, 1) + 's'
      : '';

  const boxWidth = 220;
  const boxHeight = 70;

  // Bg box
  fill(0, 0, 0, 140);
  rect(padding, padding, boxWidth, boxHeight, 8);

  // Accent bar
  fill(255, 80, 160, 200);
  rect(padding, padding, 5, boxHeight, 8, 0, 0, 8);

  // Text
  fill(235);
  textSize(16);
  text(scoreText, padding + 14, padding + 8);
  fill(170, 210, 255);
  textSize(14);
  text(levelText, padding + 14, padding + 30);
  if (timeText) {
    fill(200);
    text(timeText, padding + 14, padding + 48);
  }

  // Best score top-right
  const bText = 'Best: ' + nf(bestScore, 1, 1);
  textAlign(RIGHT, TOP);
  fill(255, 215, 0, 210);
  textSize(16);
  text(bText, width - padding, padding);
}
Line-by-line explanation (4 lines)

🔧 Subcomponents:

conditional Show Time Only While Playing if (timeText) {

Only draws the elapsed-time line if the game is actively being played.

const scoreText = 'Score: ' + nf(score, 1, 1);
nf() formats a number with a set number of digits - here always 1 decimal place, avoiding jittery-looking score text.
fill(0, 0, 0, 140);
A semi-transparent black rectangle acts as a readable backdrop for the HUD text.
rect(padding, padding, 5, boxHeight, 8, 0, 0, 8);
Draws a thin colored accent bar along the left edge of the HUD box, with rounded corners only on the outer edges.
textAlign(RIGHT, TOP);
Switches text alignment so the best-score text lines up against the right edge of the screen.

drawMenu()

drawMenu() shows push()/pop() combined with scale() to create an isolated pulsing animation on a single piece of text, without affecting the rest of the drawing.

🔬 This loop spaces out each instruction line by 24 pixels. What happens if you change y += 24 to y += 60?

  let y = height * 0.48;
  for (let line of lines) {
    text(line, width / 2, y);
    y += 24;
  }
function drawMenu() {
  textAlign(CENTER, CENTER);
  fill(255);
  textFont('system-ui');

  // Title
  textSize(48);
  fill(255, 120, 200);
  text('ORB RUN', width / 2, height * 0.3);

  // Subtitle
  textSize(20);
  fill(210);
  text('Dodge the hunters. Collect the light.', width / 2, height * 0.38);

  // Instructions
  fill(220);
  textSize(16);
  const lines = [
    'Move the mouse to control your orb.',
    'Avoid enemies that home in on you.',
    'Collect glowing orbs to boost your score.',
    'Survive as long as you can as difficulty increases.'
  ];

  let y = height * 0.48;
  for (let line of lines) {
    text(line, width / 2, y);
    y += 24;
  }

  // Start hint
  textSize(18);
  fill(255, 200, 255);
  const blink = sin(frameCount * 0.1) * 0.5 + 0.5;
  push();
  translate(width / 2, height * 0.7);
  scale(1 + blink * 0.05);
  text('Click or press any key to begin', 0, 0);
  pop();

  // Best score display
  if (bestScore > 0) {
    textSize(16);
    fill(255, 215, 0);
    text('Best score: ' + nf(bestScore, 1, 1), width / 2, height * 0.78);
  }
}
Line-by-line explanation (4 lines)

🔧 Subcomponents:

const blink = sin(frameCount * 0.1) * 0.5 + 0.5;
Creates a value that oscillates between 0 and 1 over time, used to animate the 'click to begin' hint.
push();
Saves the current drawing transform state so the following translate/scale only affect this one piece of text.
scale(1 + blink * 0.05);
Makes the start hint gently pulse in size between 100% and 105%, drawing attention to it.
pop();
Restores the transform state so nothing drawn after this is affected by the translate/scale.

drawGameOver()

This function is drawn on top of drawGame(), which is called first in draw() to show the frozen final moment of gameplay beneath the semi-transparent overlay.

function drawGameOver() {
  // Semi-transparent overlay
  fill(0, 0, 0, 180);
  rect(0, 0, width, height);

  textAlign(CENTER, CENTER);
  textFont('system-ui');

  textSize(40);
  fill(255, 120, 160);
  text('GAME OVER', width / 2, height * 0.35);

  textSize(20);
  fill(230);
  text('Score: ' + nf(score, 1, 1), width / 2, height * 0.45);
  fill(255, 215, 0);
  text('Best: ' + nf(bestScore, 1, 1), width / 2, height * 0.5);

  textSize(16);
  fill(220);
  text('Click or press any key to try again', width / 2, height * 0.6);
}
Line-by-line explanation (3 lines)
fill(0, 0, 0, 180);
A mostly-opaque black used to dim the frozen game scene behind the game-over text.
rect(0, 0, width, height);
Covers the entire canvas with the semi-transparent overlay.
text('Score: ' + nf(score, 1, 1), width / 2, height * 0.45);
Displays the final score from the run that just ended.

endGame()

endGame() is called the instant an enemy touches the player, from inside updateGame()'s collision loop. Keeping state transitions in small dedicated functions like this keeps the game logic easy to follow.

function endGame() {
  gameState = 'gameover';
  if (score > bestScore) {
    bestScore = score;
  }
  playGameOverSound();
}
Line-by-line explanation (3 lines)

🔧 Subcomponents:

conditional Update Best Score if (score > bestScore) {

Only overwrites bestScore if the current run beat the previous record.

gameState = 'gameover';
Switches the state machine so draw() now shows the game-over screen instead of running gameplay.
if (score > bestScore) {
Compares this run's score against the stored best.
playGameOverSound();
Plays the falling-tone sound effect to signal the run has ended.

mousePressed()

p5.js automatically calls mousePressed() whenever the mouse button is clicked, making it the natural place to handle 'start game' and 'restart' logic.

function mousePressed() {
  startAudioIfNeeded();

  if (gameState === 'menu' || gameState === 'gameover') {
    initGame();
    gameState = 'playing';
    playStartGameSound();
  }
}
Line-by-line explanation (3 lines)

🔧 Subcomponents:

conditional Start/Restart on Click if (gameState === 'menu' || gameState === 'gameover') {

Only starts or restarts the game if it isn't already in progress, preventing accidental resets mid-play.

startAudioIfNeeded();
Unlocks and starts the audio system on this first user gesture, per browser autoplay rules.
if (gameState === 'menu' || gameState === 'gameover') {
Only triggers a (re)start if the player is on the menu or the game-over screen, not mid-game.
initGame();
Resets everything - player position, score, enemies, orbs - for a fresh run.

keyPressed()

This function duplicates mousePressed() exactly, letting players start the game with either mouse click or any keyboard key - a small accessibility touch.

function keyPressed() {
  startAudioIfNeeded();

  if (gameState === 'menu' || gameState === 'gameover') {
    initGame();
    gameState = 'playing';
    playStartGameSound();
  }
}
Line-by-line explanation (2 lines)
startAudioIfNeeded();
Ensures audio is unlocked even if the player starts the game with the keyboard instead of a click.
if (gameState === 'menu' || gameState === 'gameover') {
Lets any key press restart the game from the menu or game-over screen.

windowResized()

p5.js automatically calls windowResized() when the browser window changes size. Rebuilding size-dependent state (like the starfield) here keeps the sketch responsive on any screen.

function windowResized() {
  resizeCanvas(windowWidth, windowHeight);
  // Rebuild stars to fit new size
  bgStars = [];
  const numStars = floor((width * height) / 15000);
  for (let i = 0; i < numStars; i++) {
    bgStars.push(new Star());
  }
}
Line-by-line explanation (3 lines)

🔧 Subcomponents:

for-loop Rebuild Starfield for (let i = 0; i < numStars; i++) {

Regenerates the starfield at the correct density after the canvas size changes.

resizeCanvas(windowWidth, windowHeight);
Resizes the p5.js canvas to match the browser window's new dimensions.
bgStars = [];
Clears the old star array, since old star positions may no longer suit the new canvas size.
const numStars = floor((width * height) / 15000);
Recalculates star count proportional to the new canvas area.

📦 Key Variables

gameState string

Tracks which screen is currently active: 'menu', 'playing', or 'gameover' - drives the entire state machine in draw().

let gameState = 'menu';
player object

Holds the single Player instance controlling position, trail, and rendering of the user's orb.

let player;
enemies array

Stores every active Enemy instance currently chasing the player.

let enemies = [];
orbs array

Stores every active OrbCollectible instance waiting to be picked up.

let orbs = [];
score number

Tracks the player's current score, increased by collecting orbs and by passive time-based accumulation.

let score = 0;
bestScore number

Remembers the highest score achieved across runs during this session, shown on the menu and game-over screens.

let bestScore = 0;
startTime number

Stores the millis() timestamp of when the current run began, used to calculate elapsed time and difficulty.

let startTime;
difficultyLevel number

A continuously rising number that scales enemy speed, spawn rates, background color, and audio pitch as the run goes on.

let difficultyLevel = 1;
bgStars array

Stores all Star instances used to draw the scrolling background starfield.

let bgStars = [];
PLAYER_BASE_RADIUS number

Constant controlling the player orb's base size before pulsing effects are applied.

const PLAYER_BASE_RADIUS = 20;
PLAYER_MAX_SPEED number

Constant capping how fast the player orb can move toward the mouse each frame.

const PLAYER_MAX_SPEED = 12;
ENEMY_BASE_SPEED number

Constant baseline speed for enemies before difficulty scaling is added.

const ENEMY_BASE_SPEED = 1.2;
ENEMY_SPAWN_INTERVAL number

Constant number of frames between enemy spawns at difficulty level 1.

const ENEMY_SPAWN_INTERVAL = 90;
lastEnemySpawn number

Stores the frameCount value of the last enemy spawn, used to time the next one.

let lastEnemySpawn = 0;
ORB_SPAWN_INTERVAL number

Constant number of frames between collectible orb spawns at baseline difficulty.

const ORB_SPAWN_INTERVAL = 70;
lastOrbSpawn number

Stores the frameCount value of the last orb spawn, used to time the next one.

let lastOrbSpawn = 0;
audioStarted boolean

Tracks whether the browser's audio context has been unlocked yet, so it only happens once.

let audioStarted = false;
ambientOsc1 object

A p5.Oscillator producing the low sine-wave layer of the ambient background hum.

let ambientOsc1;
ambientOsc2 object

A p5.Oscillator producing the higher triangle-wave layer of the ambient background hum.

let ambientOsc2;

🔧 Potential Improvements (4)

Here are some ways this code could be enhanced:

STYLE mousePressed() and keyPressed()

Both functions contain the exact same restart logic (startAudioIfNeeded, state check, initGame, playStartGameSound), duplicating code.

💡 Extract the shared body into a single helper like startOrRestartGame() and call it from both event handlers to keep the logic in one place.

PERFORMANCE playBeep()

Every sound effect creates a brand-new p5.Oscillator and p5.Envelope object rather than reusing existing ones, which can add up to many audio nodes over a long play session.

💡 Consider maintaining a small pool of reusable oscillators/envelopes for frequent sounds like playCollectSound(), or explicitly dispose of oscillators after they stop.

BUG Player.update() and mobile support

The player only responds to mouseX/mouseY, so the game is unplayable on touch devices where there is no persistent 'mouse' position.

💡 Add a touchMoved() function that updates the same target position from touches[0].x/y (and return false to prevent default scrolling) so mobile players can play too.

FEATURE drawHUD()

The HUD always draws a fixed 70px-tall box even during the menu or game-over screen where drawHUD() isn't called, and the time line disappears with an awkward gap when gameState isn't 'playing'.

💡 Shrink boxHeight dynamically based on which lines are actually shown, or reserve a fixed slot with placeholder text so the box size stays visually consistent.

🔄 Code Flow

Code flow showing setupaudio, startaudioifneeded, startambient, updateambient, playbeep, playstartgamesound, playcollectsound, playenemyspawnsound, playgameoversound, star, player, enemy, orbcollectible, setup, initgame, draw, backgroundgradient, updategame, drawgame, drawhud, drawmenu, drawgameover, endgame, mousepressed, keypressed, windowresized

💡 Click on function names in the diagram to jump to their code

graph TD start[Start] --> setup[setup] setup --> setupaudio[setupaudio] setup --> initgame[initgame] setup --> draw[draw loop] click setup href "#fn-setup" click setupaudio href "#fn-setupaudio" click initgame href "#fn-initgame" draw --> audio-unlock-guard[audio-unlock-guard] draw --> gamestate-switch[gamestate-switch] gamestate-switch -->|menu| drawmenu[drawmenu] gamestate-switch -->|playing| updategame[updategame] gamestate-switch -->|gameover| drawgameover[drawgameover] click draw href "#fn-draw" click drawmenu href "#fn-drawmenu" click updategame href "#fn-updategame" click drawgameover href "#fn-drawgameover" updategame --> enemy-spawn-check[enemy-spawn-check] updategame --> orb-spawn-check[orb-spawn-check] updategame --> enemy-update-loop[enemy-update-loop] updategame --> orb-update-loop[orb-update-loop] click enemy-spawn-check href "#sub-enemy-spawn-check" click orb-spawn-check href "#sub-orb-spawn-check" click enemy-update-loop href "#sub-enemy-update-loop" click orb-update-loop href "#sub-orb-update-loop" enemy-update-loop --> enemy-hit-check[enemy-hit-check] enemy-update-loop --> draw-enemies-loop[draw-enemies-loop] click enemy-hit-check href "#sub-enemy-hit-check" click draw-enemies-loop href "#sub-draw-enemies-loop" orb-update-loop --> orb-collect-check[orb-collect-check] orb-update-loop --> draw-orbs-loop[draw-orbs-loop] click orb-collect-check href "#sub-orb-collect-check" click draw-orbs-loop href "#sub-draw-orbs-loop" drawmenu --> menu-instructions-loop[menu-instructions-loop] drawmenu --> menu-best-score-check[menu-best-score-check] click menu-instructions-loop href "#sub-menu-instructions-loop" click menu-best-score-check href "#sub-menu-best-score-check" drawgameover --> drawgame[drawgame] drawgame --> drawhud[drawhud] drawhud --> hud-time-check[hud-time-check] click drawgame href "#fn-drawgame" click drawhud href "#fn-drawhud" click hud-time-check href "#sub-hud-time-check" starfield-loop --> star-generation-loop[star-generation-loop] starfield-loop --> star-wrap[star-wrap] click star-generation-loop href "#sub-star-generation-loop" click star-wrap href "#sub-star-wrap" player --> player-mouse-guard[player-mouse-guard] player --> player-move-step[player-move-step] player --> player-trail-array[player-trail-array] player --> player-glow-loop[player-glow-loop] player --> player-gradient-loop[player-gradient-loop] click player-mouse-guard href "#sub-player-mouse-guard" click player-move-step href "#sub-player-move-step" click player-trail-array href "#sub-player-trail-array" click player-glow-loop href "#sub-player-glow-loop" click player-gradient-loop href "#sub-player-gradient-loop" enemy --> enemy-edge-switch[enemy-edge-switch] enemy --> enemy-aura-loop[enemy-aura-loop] click enemy-edge-switch href "#sub-enemy-edge-switch" click enemy-aura-loop href "#sub-enemy-aura-loop" orbcollectible --> orb-gradient-loop[orb-gradient-loop] click orb-gradient-loop href "#sub-orb-gradient-loop" startaudioifneeded --> audio-unlock-guard startambient --> updateambient[updateambient] click updateambient href "#fn-updateambient" playbeep --> beep-pitch-slide[beep-pitch-slide] playstartgamesound --> playbeep playcollectsound --> playbeep playenemyspawnsound --> playbeep playgameoversound --> playbeep click playbeep href "#fn-playbeep" click playstartgamesound href "#fn-playstartgamesound" click playcollectsound href "#fn-playcollectsound" click playenemyspawnsound href "#fn-playenemyspawnsound" click playgameoversound href "#fn-playgameoversound"

Preview

orb-wars - p5.js creative coding sketch preview
Sketch Preview
Code flow diagram showing the structure of orb-wars - Code flow showing setupaudio, startaudioifneeded, startambient, updateambient, playbeep, playstartgamesound, playcollectsound, playenemyspawnsound, playgameoversound, star, player, enemy, orbcollectible, setup, initgame, draw, backgroundgradient, updategame, drawgame, drawhud, drawmenu, drawgameover, endgame, mousepressed, keypressed, windowresized
Code Flow Diagram