Simple Game

This sketch creates an interactive 'Kick The Buddy' game where players click on an animated character to damage it with three escalating weapons. The character bounces around the canvas with physics simulation, displays floating damage numbers, and can be knocked out with screen shake effects.

🧪 Try This!

Experiment with the code by making these changes:

  1. Make the bomb weapon super strong — The bomb (highest damage weapon) becomes even more devastating—watch the buddy get knocked across the screen
  2. Double the bounce energy loss
  3. Spawn way more particles on hit — Each hit creates 50 particles instead of 16, creating an explosion effect with more visual impact
  4. Make particles fall with stronger gravity — Particles are pulled downward more aggressively, creating a heavier, more dramatic burst
  5. Reduce screen shake intensity
Prefer the full editor? Open it there →

📖 About This Sketch

This sketch builds a complete mini-game called 'Kick The Buddy' that combines multiple p5.js techniques into one engaging interactive experience. A character bounces around the canvas with realistic physics (gravity, friction, wall collisions), players click to hit it with three weapons of increasing damage, and the game displays visual feedback through floating damage text, particle bursts, and screen shake effects. Learning this sketch teaches you how to layer animation, interaction, physics, and particle systems into a cohesive gameplay experience.

The code is organized into distinct systems: a physics engine that updates the buddy's position and velocity each frame, a weapon system with configurable damage and knockback values, a particle system for visual impact, and a UI layer that displays health bars and game state. By studying it, you will learn how to structure a game with multiple interacting systems, manage state across frames, and create satisfying player feedback through screen effects and visual polish.

⚙️ How It Works

  1. When the sketch loads, setup() creates a 900x600 canvas, initializes the buddy character at the center with full health, and builds three weapon buttons in the HTML UI.
  2. Every frame, draw() clears the background with a dark blue color and updates all game systems: the buddy's position and velocity are calculated with gravity and friction, particles are moved and faded out, floating damage text floats upward and disappears, and everything is drawn to screen.
  3. When the player clicks on the buddy, mousePressed() calculates distance from the click to the buddy's center; if within range, applyHit() is called which reduces health, calculates knockback direction, applies velocity to push the buddy away, and spawns particle bursts and floating damage numbers.
  4. The buddy bounces realistically off all four walls and the floor with velocity dampening (energy loss) on each collision, creating natural physics behavior.
  5. As health decreases, the buddy's color transitions from green to red using lerpColor(), and the mouth expression changes from happy (arc facing up) to sad (arc facing down), providing visual feedback of damage.
  6. When health reaches zero, buddy enters KO state, displays a red 'KO!' floating text and burst particles, and stops moving; pressing R calls resetBuddy() to restore full health and allow gameplay to resume.

🎓 Concepts You'll Learn

Physics simulation with gravity and velocityCollision detection and responseParticle systems and visual effectsState management in gamesColor interpolation and gradual changesVector math for direction and knockbackArray manipulation and cleanupScreen shake and camera effects

📝 Code Breakdown

setup()

setup() runs once when the sketch starts. It prepares the canvas, creates HTML UI elements, and initializes game state. Notice how forEach() creates multiple buttons from the weapons array in a loop instead of writing each button manually.

function setup() {
  createCanvas(900, 600);

  // Conteneur des boutons
  uiContainer = createDiv();
  uiContainer.id('ui-container');

  // Boutons d'armes
  weapons.forEach((w, i) => {
    const btn = createButton(w.name);
    btn.parent(uiContainer);
    btn.addClass('weapon-button');
    if (i === currentWeaponIndex) btn.addClass('active');
    btn.mousePressed(() => selectWeapon(i));
    weaponButtons.push(btn);
  });

  // Bouton reset
  const resetBtn = createButton('Réinitialiser Buddy (R)');
  resetBtn.parent(uiContainer);
  resetBtn.id('reset-btn');
  resetBtn.addClass('weapon-button');
  resetBtn.mousePressed(resetBuddy);

  resetBuddy();
}
Line-by-line explanation (5 lines)

🔧 Subcomponents:

for-loop Weapon Button Creation weapons.forEach((w, i) => {

Creates three weapon selection buttons, one for each weapon in the weapons array

function-call Initialize Buddy resetBuddy();

Spawns the buddy character with full health and resets all game state

createCanvas(900, 600);
Creates a 900-pixel-wide by 600-pixel-tall canvas where the game will be drawn
uiContainer = createDiv();
Creates an HTML div element to hold the weapon buttons below the canvas
weapons.forEach((w, i) => {
Loops through each weapon object in the weapons array to create a button for each one
btn.mousePressed(() => selectWeapon(i));
Attaches a click handler to each button so clicking it calls selectWeapon() with that weapon's index
resetBuddy();
Initializes the buddy object with full health at the center of the canvas

draw()

draw() runs 60 times per second. Notice the order: update physics and state first, then draw everything. The push/pop sandwich isolates the screen shake so it doesn't affect the UI. This frame-by-frame animation loop is the heartbeat of every p5.js sketch.

🔬 This code creates the screen shake effect when hitting the buddy. What happens if you change shakeMagnitude to 0? To 30? Does the shake feel more or less impactful?

  if (shakeTimer > 0) {
    translate(
      random(-shakeMagnitude, shakeMagnitude),
      random(-shakeMagnitude, shakeMagnitude)
    );
    shakeTimer--;
  }
function draw() {
  background(15, 23, 42); // bleu nuit

  // Secousse d'écran
  push();
  if (shakeTimer > 0) {
    translate(
      random(-shakeMagnitude, shakeMagnitude),
      random(-shakeMagnitude, shakeMagnitude)
    );
    shakeTimer--;
  }

  drawPlayArea();
  updateBuddy();
  drawBuddy();

  updateParticles();
  drawParticles();

  updateFloatingTexts();
  drawFloatingTexts();

  pop(); // fin secousse

  drawUI();
}
Line-by-line explanation (10 lines)

🔧 Subcomponents:

conditional Screen Shake if (shakeTimer > 0) {

Randomly offsets the entire view for a few frames when the buddy is hit, creating impact feedback

function-call Game Loop Sequence updateBuddy(); drawBuddy();

Updates physics, then draws the character—this pattern applies to particles and floating text too

background(15, 23, 42);
Clears the canvas with a dark blue-black color every frame, preventing trails and visual noise
push();
Saves the current drawing state so screen shake effects don't permanently change the canvas
if (shakeTimer > 0) {
Checks if screen shake is active (timer is still counting down)
translate(random(-shakeMagnitude, shakeMagnitude), random(-shakeMagnitude, shakeMagnitude));
Randomly offsets everything drawn after this line by a few pixels in both directions
shakeTimer--;
Decrements the shake timer, so the effect fades out after a few frames
updateBuddy(); drawBuddy();
Updates the buddy's position and velocity, then draws it at the new position
updateParticles(); drawParticles();
Moves all particles and fades them out, then draws them all to the screen
updateFloatingTexts(); drawFloatingTexts();
Moves damage numbers upward and fades them, then draws them to the screen
pop();
Restores the drawing state saved by push(), removing the screen shake effect for the UI layer
drawUI();
Draws the health bar and instructions outside the shake effect so they stay stable

resetBuddy()

resetBuddy() creates a fresh buddy object and clears all particle and text arrays. It is called when the sketch starts and whenever the player presses R. This is a simple but powerful pattern: to reset game state, create a new object with initial values.

function resetBuddy() {
  buddy = {
    x: width / 2,
    y: height / 2 - 50,
    vx: 0,
    vy: 0,
    r: 60,
    health: 100,
    maxHealth: 100,
    isKO: false
  };
  particles = [];
  floatingTexts = [];
  shakeTimer = 0;
}
Line-by-line explanation (11 lines)
buddy = {
Creates an object to store all the buddy's properties: position, velocity, size, health, and state
x: width / 2,
Places the buddy horizontally at the center of the canvas
y: height / 2 - 50,
Places the buddy slightly above the vertical center
vx: 0,
Horizontal velocity starts at 0 so the buddy doesn't move until hit
vy: 0,
Vertical velocity starts at 0, but gravity will pull it down next frame
r: 60,
The radius (half the diameter) of the buddy's body, used for drawing and collision detection
health: 100,
Current health points—decreases when hit, game ends when it reaches 0
isKO: false
Boolean flag that becomes true when health <= 0, stopping the buddy from moving
particles = [];
Clears all existing particles from previous hits
floatingTexts = [];
Clears all damage numbers from the screen
shakeTimer = 0;
Resets the screen shake countdown to zero

updateBuddy()

This is the physics engine. Every frame it applies gravity, updates position, detects collisions with all four walls and the floor, bounces the buddy back with energy loss, applies air resistance, and checks for the KO condition. Understanding this function teaches you the core of any 2D physics simulation in p5.js.

🔬 This code bounces the buddy off the left wall. The -0.6 multiplier means the velocity flips direction AND loses energy. What happens if you change -0.6 to -1? To 0.5? Does the buddy bounce higher or lower?

    // collisions murs
    if (buddy.x - buddy.r < 0) {
      buddy.x = buddy.r;
      buddy.vx *= -0.6;
    }
function updateBuddy() {
  if (!buddy) return;

  if (!buddy.isKO) {
    buddy.vy += gravity;

    buddy.x += buddy.vx;
    buddy.y += buddy.vy;

    // collisions murs
    if (buddy.x - buddy.r < 0) {
      buddy.x = buddy.r;
      buddy.vx *= -0.6;
    }
    if (buddy.x + buddy.r > width) {
      buddy.x = width - buddy.r;
      buddy.vx *= -0.6;
    }

    // plafond
    if (buddy.y - buddy.r < 0) {
      buddy.y = buddy.r;
      buddy.vy *= -0.6;
    }

    // sol
    if (buddy.y + buddy.r > height - 20) {
      buddy.y = height - 20 - buddy.r;
      buddy.vy *= -0.55;
      buddy.vx *= groundFriction;
    }

    buddy.vx *= airFriction;
    buddy.vy *= airFriction;

    if (buddy.health <= 0 && !buddy.isKO) {
      buddy.health = 0;
      buddy.isKO = true;
      spawnFloatingText('KO !', buddy.x, buddy.y - buddy.r * 1.4, color(248, 113, 113));
      spawnBurstParticles(buddy.x, buddy.y, '#f87171', 22);
    }
  }
}
Line-by-line explanation (17 lines)

🔧 Subcomponents:

calculation Gravity buddy.vy += gravity;

Pulls the buddy downward by increasing downward velocity every frame

calculation Position Update buddy.x += buddy.vx; buddy.y += buddy.vy;

Moves the buddy by adding velocity to its current position

conditional Left Wall Bounce if (buddy.x - buddy.r < 0) {

Detects when buddy's left edge passes the left canvas edge

conditional Right Wall Bounce if (buddy.x + buddy.r > width) {

Detects when buddy's right edge passes the right canvas edge

conditional Ceiling Bounce if (buddy.y - buddy.r < 0) {

Detects when buddy hits the top edge

conditional Floor Bounce if (buddy.y + buddy.r > height - 20) {

Detects when buddy hits the ground (accounting for ground visual thickness)

calculation Air Friction buddy.vx *= airFriction; buddy.vy *= airFriction;

Slows down velocity each frame to simulate air resistance

conditional KO State Check if (buddy.health <= 0 && !buddy.isKO) {

Triggers KO effects when health reaches zero

if (!buddy) return;
Exits early if buddy doesn't exist, preventing crashes
if (!buddy.isKO) {
Only update physics if the buddy is not knocked out—stops movement when KO is true
buddy.vy += gravity;
Applies gravity by increasing downward velocity (positive = down) each frame
buddy.x += buddy.vx;
Moves the buddy horizontally by adding its horizontal velocity to position
buddy.y += buddy.vy;
Moves the buddy vertically by adding its vertical velocity to position
if (buddy.x - buddy.r < 0) {
Checks if the buddy's left edge (x - radius) has crossed the left canvas edge (x = 0)
buddy.x = buddy.r;
Pushes the buddy back inside the canvas by setting its position to just inside the edge
buddy.vx *= -0.6;
Flips the horizontal velocity direction and multiplies by 0.6, losing energy on bounce (energy loss = 40%)
if (buddy.y + buddy.r > height - 20) {
Checks if buddy's bottom edge has hit the ground (height - 20 accounts for the visible ground thickness)
buddy.vy *= -0.55;
Reverses vertical direction and multiplies by -0.55, losing more energy on the floor bounce
buddy.vx *= groundFriction;
Applies extra friction while on the ground to slow down horizontal sliding
buddy.vx *= airFriction;
Applies air friction to horizontal velocity, creating drag while flying
buddy.vy *= airFriction;
Applies air friction to vertical velocity
if (buddy.health <= 0 && !buddy.isKO) {
Checks if health just hit zero and we haven't already triggered KO (prevents duplicate KO effects)
buddy.isKO = true;
Sets KO flag to true so the character stops moving
spawnFloatingText('KO !', buddy.x, buddy.y - buddy.r * 1.4, color(248, 113, 113));
Creates a red 'KO !' text that floats up from the buddy's head
spawnBurstParticles(buddy.x, buddy.y, '#f87171', 22);
Spawns 22 red particles bursting from the buddy's center for visual impact

drawBuddy()

drawBuddy() renders the character with sophisticated visual feedback: colors transition smoothly with health using lerpColor(), the mouth animates from happy to sad using map(), and the entire face changes when KO. This teaches how to create compelling character expressions with minimal code by interpolating between values.

🔬 These two lines draw both eyes of the buddy. What happens if you change 10 to 20? To 3? The eyes get bigger or smaller. Why do you think it uses buddy.r * 0.18 for positioning instead of a fixed number?

  // Yeux
    ellipse(-buddy.r * 0.18, -buddy.r * 0.6, 10, 14);
    ellipse(buddy.r * 0.18, -buddy.r * 0.6, 10, 14);
function drawBuddy() {
  if (!buddy) return;

  push();
  translate(buddy.x, buddy.y);

  // Ombre
  noStroke();
  fill(0, 0, 0, 80);
  ellipse(0, buddy.r * 1.05, buddy.r * 1.5, buddy.r * 0.4);

  // Corps
  // Couleurs en fonction de la vie
  const t = 1 - buddy.health / buddy.maxHealth;
  const bodyCol = lerpColor(color('#10b981'), color('#f97316'), t); // vert -> orange
  const headCol = lerpColor(color('#f9fafb'), color('#fecaca'), t); // peau claire -> rouge

  // Corps
  fill(bodyCol);
  rectMode(CENTER);
  rect(0, buddy.r * 0.3, buddy.r * 0.9, buddy.r * 1.4, 20);

  // Tête
  fill(headCol);
  ellipse(0, -buddy.r * 0.6, buddy.r * 0.9, buddy.r * 0.9);

  // Bras
  stroke(bodyCol);
  strokeWeight(16);
  line(-buddy.r * 0.5, buddy.r * 0.0, -buddy.r * 1.0, buddy.r * 0.3);
  line(buddy.r * 0.5, buddy.r * 0.0, buddy.r * 1.0, buddy.r * 0.3);

  // Jambes
  line(-buddy.r * 0.25, buddy.r * 0.95, -buddy.r * 0.5, buddy.r * 1.5);
  line(buddy.r * 0.25, buddy.r * 0.95, buddy.r * 0.5, buddy.r * 1.5);

  // Visage
  noStroke();
  fill(15, 23, 42);

  if (!buddy.isKO) {
    // Yeux
    ellipse(-buddy.r * 0.18, -buddy.r * 0.6, 10, 14);
    ellipse(buddy.r * 0.18, -buddy.r * 0.6, 10, 14);

    // Bouche (heureux -> triste selon la vie)
    noFill();
    stroke(15, 23, 42);
    strokeWeight(3);
    const mouthOffset = map(buddy.health, 0, buddy.maxHealth, 6, -6);
    arc(0, -buddy.r * 0.42 + mouthOffset, buddy.r * 0.4, buddy.r * 0.3, 0, PI);
  } else {
    // Yeux KO (croix)
    stroke(15, 23, 42);
    strokeWeight(3);
    const ex = buddy.r * 0.2;
    const ey = -buddy.r * 0.6;
    line(-ex - 4, ey - 4, -ex + 4, ey + 4);
    line(-ex - 4, ey + 4, -ex + 4, ey - 4);
    line(ex - 4, ey - 4, ex + 4, ey + 4);
    line(ex - 4, ey + 4, ex + 4, ey - 4);

    // Bouche KO
    noFill();
    arc(0, -buddy.r * 0.42 + 8, buddy.r * 0.45, buddy.r * 0.25, PI, TWO_PI);
  }

  pop();
}
Line-by-line explanation (15 lines)

🔧 Subcomponents:

calculation Health-Based Color Transition const t = 1 - buddy.health / buddy.maxHealth;

Calculates a value from 0 to 1 based on damage taken, used to blend colors

function-call Body Color Interpolation const bodyCol = lerpColor(color('#10b981'), color('#f97316'), t);

Smoothly transitions body color from green (healthy) to orange (damaged)

conditional Normal Face (Alive) if (!buddy.isKO) {

Draws happy eyes and a mouth that changes from happy to sad with damage

conditional KO Face (Dead) } else {

Draws X eyes and an upside-down mouth when the buddy is knocked out

if (!buddy) return;
Exits early if buddy doesn't exist, preventing crashes
push();
Saves the current drawing state so transformations don't affect other drawings
translate(buddy.x, buddy.y);
Moves the origin to the buddy's position so all coordinates are relative to the buddy
fill(0, 0, 0, 80);
Sets fill color to black with 80 alpha (semi-transparent) for the shadow
ellipse(0, buddy.r * 1.05, buddy.r * 1.5, buddy.r * 0.4);
Draws a dark ellipse below the buddy as a shadow
const t = 1 - buddy.health / buddy.maxHealth;
Calculates t: when health = 100, t = 0 (healthy); when health = 0, t = 1 (dead)
const bodyCol = lerpColor(color('#10b981'), color('#f97316'), t);
Interpolates between green (#10b981) and orange (#f97316) based on t, creating a color transition
const headCol = lerpColor(color('#f9fafb'), color('#fecaca'), t);
Interpolates between light skin (#f9fafb) and light red (#fecaca) for the head
fill(bodyCol); rectMode(CENTER); rect(0, buddy.r * 0.3, buddy.r * 0.9, buddy.r * 1.4, 20);
Draws the body as a rounded rectangle using the interpolated body color
fill(headCol); ellipse(0, -buddy.r * 0.6, buddy.r * 0.9, buddy.r * 0.9);
Draws the head as a circle using the interpolated head color, positioned above the body
const mouthOffset = map(buddy.health, 0, buddy.maxHealth, 6, -6);
Maps health (0-100) to mouth offset (6 to -6): when healthy offset is negative (sad), when damaged offset is positive (happy-like)
arc(0, -buddy.r * 0.42 + mouthOffset, buddy.r * 0.4, buddy.r * 0.3, 0, PI);
Draws an arc (half circle) for the mouth that moves up when damaged, creating a sad expression
line(-ex - 4, ey - 4, -ex + 4, ey + 4);
Draws one line of the X-shaped eye for the KO state
arc(0, -buddy.r * 0.42 + 8, buddy.r * 0.45, buddy.r * 0.25, PI, TWO_PI);
Draws an upside-down arc (PI to TWO_PI) for a dead/KO mouth
pop();
Restores the drawing state so the translate doesn't affect other drawings

drawPlayArea()

drawPlayArea() creates the visual boundary of the game arena. It draws a solid ground at the bottom (which the buddy bounces off) and a rounded border outline. This is purely visual—the actual collision detection happens in updateBuddy().

function drawPlayArea() {
  // Sol
  noStroke();
  fill(31, 41, 55);
  rect(0, height - 20, width, 20);

  // Mur léger
  stroke(55, 65, 81);
  strokeWeight(2);
  noFill();
  rect(2, 2, width - 4, height - 24, 18);
}
Line-by-line explanation (4 lines)
fill(31, 41, 55);
Sets fill color to dark gray for the ground
rect(0, height - 20, width, 20);
Draws a 20-pixel-tall rectangle at the bottom of the canvas as the visible ground
stroke(55, 65, 81);
Sets stroke color to a slightly lighter gray for the arena border
rect(2, 2, width - 4, height - 24, 18);
Draws a rounded rectangle border with 2-pixel margins, leaving space for the ground

selectWeapon()

selectWeapon() is called whenever a weapon button is clicked. It updates the global currentWeaponIndex and toggles CSS classes on the buttons to provide visual feedback. This is a simple state machine: only one weapon can be active at a time.

function selectWeapon(index) {
  currentWeaponIndex = index;
  weaponButtons.forEach((btn, i) => {
    if (i === index) btn.addClass('active');
    else btn.removeClass('active');
  });
}
Line-by-line explanation (4 lines)
currentWeaponIndex = index;
Updates the global weapon index so the next hit uses the selected weapon
weaponButtons.forEach((btn, i) => {
Loops through all weapon buttons to update their visual state
if (i === index) btn.addClass('active');
Adds the 'active' CSS class to the selected button to highlight it
else btn.removeClass('active');
Removes the 'active' class from all other buttons

mousePressed()

mousePressed() is called whenever the player clicks anywhere. It uses collision detection (dist function) to check if the click is on the buddy. If so, it triggers applyHit(). This teaches click-based interactivity and distance-based collision detection.

function mousePressed() {
  if (!buddy || buddy.isKO) return;

  const d = dist(mouseX, mouseY, buddy.x, buddy.y);
  if (d <= buddy.r * 1.1) {
    applyHit(mouseX, mouseY);
  }
}
Line-by-line explanation (4 lines)
if (!buddy || buddy.isKO) return;
Exits early if buddy doesn't exist or is knocked out—prevents hitting a dead buddy
const d = dist(mouseX, mouseY, buddy.x, buddy.y);
Calculates the distance in pixels from the mouse click to the buddy's center using p5.js dist() function
if (d <= buddy.r * 1.1) {
Checks if the click is within the buddy's hit zone (radius * 1.1 gives a slightly generous hit area)
applyHit(mouseX, mouseY);
If hit, calls applyHit() to deal damage and create effects

applyHit()

applyHit() is the core game mechanic. It deals random damage based on the weapon, calculates knockback direction using vector math, applies velocity to push the buddy, triggers screen shake and particle effects, and creates floating damage numbers. This single function demonstrates damage systems, vector math, state management, and visual feedback.

🔬 This code calculates which direction to push the buddy. What happens if you change dir.normalize() so it doesn't run? Try removing that line—without normalizing, clicks near the buddy vs far away will push different amounts. Why does normalize() make the knockback consistent?

  // Direction du coup : de la souris vers le centre du buddy (il est repoussé)
  let dir = createVector(buddy.x - mx, buddy.y - my);
  if (dir.mag() < 0.1) {
    dir = p5.Vector.random2D();
  }
  dir.normalize();
function applyHit(mx, my) {
  const w = weapons[currentWeaponIndex];

  const dmg = random(w.minDmg, w.maxDmg);
  buddy.health -= dmg;
  if (buddy.health < 0) buddy.health = 0;

  // Direction du coup : de la souris vers le centre du buddy (il est repoussé)
  let dir = createVector(buddy.x - mx, buddy.y - my);
  if (dir.mag() < 0.1) {
    dir = p5.Vector.random2D();
  }
  dir.normalize();
  const knock = w.knockback;
  buddy.vx += dir.x * knock;
  buddy.vy += dir.y * knock;

  // Secousse d'écran
  shakeTimer = 12;
  shakeMagnitude = map(knock, 10, 30, 5, 12);

  // Particules
  spawnBurstParticles(mx, my, w.color, 16);

  // Texte flottant
  spawnFloatingText('-' + nf(dmg, 0, 0), mx, my - 10, color(w.color));
}
Line-by-line explanation (13 lines)

🔧 Subcomponents:

calculation Random Damage const dmg = random(w.minDmg, w.maxDmg);

Generates a random damage value between the weapon's minimum and maximum

calculation Knockback Vector Calculation let dir = createVector(buddy.x - mx, buddy.y - my);

Creates a vector pointing from click to buddy center to push the buddy away from the hit

calculation Screen Shake Initialization shakeTimer = 12;

Activates screen shake for the next 12 frames

const w = weapons[currentWeaponIndex];
Gets the current weapon object from the weapons array
const dmg = random(w.minDmg, w.maxDmg);
Generates a random damage value between the weapon's min and max (e.g., punch does 5-12 damage)
buddy.health -= dmg;
Subtracts the damage from the buddy's health
if (buddy.health < 0) buddy.health = 0;
Clamps health to zero so it never goes negative
let dir = createVector(buddy.x - mx, buddy.y - my);
Creates a vector from the click point to the buddy's center—this is the knockback direction
if (dir.mag() < 0.1) { dir = p5.Vector.random2D(); }
If the click was exactly on the buddy center, use a random direction instead (mag < 0.1 means too small)
dir.normalize();
Scales the direction vector to length 1 so all knockbacks have the same base strength
buddy.vx += dir.x * knock;
Adds horizontal knockback velocity proportional to the weapon's knockback value
buddy.vy += dir.y * knock;
Adds vertical knockback velocity
shakeTimer = 12;
Activates screen shake for 12 frames (about 0.2 seconds at 60fps)
shakeMagnitude = map(knock, 10, 30, 5, 12);
Maps knockback strength (10-30) to shake intensity (5-12 pixels) so stronger hits shake more
spawnBurstParticles(mx, my, w.color, 16);
Creates 16 colored particles bursting from the hit location
spawnFloatingText('-' + nf(dmg, 0, 0), mx, my - 10, color(w.color));
Creates a damage number (e.g., '-8') that floats up from the hit location in the weapon color

spawnBurstParticles()

spawnBurstParticles() creates a burst effect by looping 'count' times, each time creating a particle with a random angle (spreading particles in all directions), random speed, and random lifespan. Using trigonometry (cos/sin with angles) to convert angles to x/y velocities is a classic technique in game development.

🔬 This loop creates count particles with random angles and speeds. What happens if you change random(2, 7) to random(5, 15)? Particles fly faster. What if you change it to random(0.5, 2)? How does speed change what the hit feels like?

  for (let i = 0; i < count; i++) {
    const ang = random(TWO_PI);
    const spd = random(2, 7);
function spawnBurstParticles(x, y, colHex, count) {
  const col = color(colHex);
  for (let i = 0; i < count; i++) {
    const ang = random(TWO_PI);
    const spd = random(2, 7);
    particles.push({
      x,
      y,
      vx: cos(ang) * spd,
      vy: sin(ang) * spd,
      size: random(3, 7),
      life: random(20, 40),
      col
    });
  }
}
Line-by-line explanation (9 lines)

🔧 Subcomponents:

for-loop Particle Loop for (let i = 0; i < count; i++) {

Creates 'count' individual particles with random properties

const col = color(colHex);
Converts the hex color string (e.g., '#f97373') to a p5.js color object
for (let i = 0; i < count; i++) {
Loops 'count' times to create 'count' particles (e.g., count=16 creates 16 particles)
const ang = random(TWO_PI);
Generates a random angle from 0 to TWO_PI (0 to 360 degrees), used to spread particles in all directions
const spd = random(2, 7);
Generates a random speed between 2 and 7 pixels per frame
vx: cos(ang) * spd,
Calculates horizontal velocity using cos(angle) * speed, converting angle to x direction
vy: sin(ang) * spd,
Calculates vertical velocity using sin(angle) * speed, converting angle to y direction
size: random(3, 7),
Assigns each particle a random diameter between 3 and 7 pixels
life: random(20, 40),
Assigns each particle a random lifespan between 20 and 40 frames before it disappears
particles.push({...});
Adds the new particle object to the particles array so it will be updated and drawn

updateParticles()

updateParticles() simulates physics for each particle: position updates with velocity, gravity is applied, friction slows particles down, lifespan decreases, and dead particles are removed. Notice the reverse loop—this is essential when removing items from an array during iteration, a pattern you'll use throughout your coding.

function updateParticles() {
  for (let i = particles.length - 1; i >= 0; i--) {
    const p = particles[i];
    p.x += p.vx;
    p.y += p.vy;
    p.vy += 0.2;
    p.life--;
    p.vx *= 0.98;
    p.vy *= 0.98;
    if (p.life <= 0) particles.splice(i, 1);
  }
}
Line-by-line explanation (8 lines)

🔧 Subcomponents:

for-loop Reverse Particle Loop for (let i = particles.length - 1; i >= 0; i--) {

Loops backward through particles so removing items doesn't skip any

conditional Dead Particle Cleanup if (p.life <= 0) particles.splice(i, 1);

Removes particles when their lifespan expires to prevent memory buildup

for (let i = particles.length - 1; i >= 0; i--) {
Loops backward through the particles array—important because we're removing items during iteration
p.x += p.vx;
Moves particle horizontally by adding its velocity
p.y += p.vy;
Moves particle vertically
p.vy += 0.2;
Applies a small downward force (gravity) to particles, making them fall naturally
p.life--;
Decrements the particle's lifespan by 1 frame
p.vx *= 0.98;
Applies air friction to horizontal velocity, slowing particles down each frame
p.vy *= 0.98;
Applies air friction to vertical velocity
if (p.life <= 0) particles.splice(i, 1);
When lifespan reaches 0, removes the particle from the array using splice()

drawParticles()

drawParticles() renders all particles with a fade-out effect. The map() function converts lifespan (0-40) to alpha transparency (0-255), creating the illusion of particles fading away as they age. This is a simple but effective technique for particle effects.

function drawParticles() {
  noStroke();
  for (const p of particles) {
    const a = map(p.life, 0, 40, 0, 255);
    fill(red(p.col), green(p.col), blue(p.col), a);
    circle(p.x, p.y, p.size);
  }
}
Line-by-line explanation (5 lines)
noStroke();
Removes outlines from circles so particles are solid
for (const p of particles) {
Loops through every particle in the particles array
const a = map(p.life, 0, 40, 0, 255);
Maps particle lifespan (0-40 frames) to alpha transparency (0-255)—particles fade as they age
fill(red(p.col), green(p.col), blue(p.col), a);
Extracts red, green, blue components from the particle color and uses the mapped alpha
circle(p.x, p.y, p.size);
Draws a circle at the particle's current position with its size

spawnFloatingText()

spawnFloatingText() creates a floating damage number object and adds it to the array. Similar to spawnBurstParticles(), it stores initial state (position, velocity, opacity, color) and pushes to an array for the update/draw loop to manage.

function spawnFloatingText(txt, x, y, col) {
  floatingTexts.push({
    text: txt,
    x,
    y,
    vy: -0.5,
    alpha: 255,
    col
  });
}
Line-by-line explanation (6 lines)
floatingTexts.push({
Creates a new floating text object and adds it to the floatingTexts array
text: txt,
Stores the text string (e.g., '-8' for damage)
x, y,
Stores the starting position where the text appears
vy: -0.5,
Sets vertical velocity to -0.5 (upward) so text floats up the screen
alpha: 255,
Starts with full opacity (255) before fading
col
Stores the color of the text

updateFloatingTexts()

updateFloatingTexts() moves damage numbers upward and fades them out. The alpha fades at a fixed rate (4 per frame) regardless of initial alpha, which means all damage numbers disappear at the same speed. This is memory management: removing dead objects prevents array bloat.

function updateFloatingTexts() {
  for (let i = floatingTexts.length - 1; i >= 0; i--) {
    const ft = floatingTexts[i];
    ft.y += ft.vy;
    ft.alpha -= 4;
    if (ft.alpha <= 0) floatingTexts.splice(i, 1);
  }
}
Line-by-line explanation (4 lines)
for (let i = floatingTexts.length - 1; i >= 0; i--) {
Loops backward through floating texts so removal doesn't skip items
ft.y += ft.vy;
Moves the text upward by adding its velocity (-0.5 per frame)
ft.alpha -= 4;
Decreases opacity by 4 per frame, fading the text out over ~64 frames
if (ft.alpha <= 0) floatingTexts.splice(i, 1);
Removes the text from the array when it becomes fully transparent

drawFloatingTexts()

drawFloatingTexts() renders all damage numbers with a stroke outline (black shadow) for readability. The alpha value is applied to both stroke and fill, creating a unified fade effect. This teaches text rendering and layered visual effects in p5.js.

function drawFloatingTexts() {
  textAlign(CENTER, CENTER);
  textSize(18);
  strokeWeight(2);
  for (const ft of floatingTexts) {
    const c = ft.col;
    const a = ft.alpha;
    stroke(0, 0, 0, a);
    fill(red(c), green(c), blue(c), a);
    text(ft.text, ft.x, ft.y);
  }
}
Line-by-line explanation (6 lines)
textAlign(CENTER, CENTER);
Aligns text so it's centered at the x,y position instead of starting from the corner
textSize(18);
Sets the font size to 18 pixels
strokeWeight(2);
Sets the text outline thickness to 2 pixels for better readability
stroke(0, 0, 0, a);
Draws a black outline with the same alpha as the text, creating a shadow effect
fill(red(c), green(c), blue(c), a);
Fills the text with its stored color and current alpha (which fades as text ages)
text(ft.text, ft.x, ft.y);
Draws the text string at its current position

drawUI()

drawUI() renders all on-screen information: the health bar with color interpolation (green to red), numeric health display, current weapon name, instructions, and the KO message. This demonstrates text rendering, conditional display, and color interpolation for UI design.

function drawUI() {
  if (!buddy) return;

  // Barre de vie
  const barW = 260;
  const barH = 20;
  const x = 30;
  const y = 30;

  const ratio = constrain(buddy.health / buddy.maxHealth, 0, 1);

  noStroke();
  fill(31, 41, 55);
  rect(x, y, barW, barH, 10);

  const col = lerpColor(color('#22c55e'), color('#ef4444'), 1 - ratio);
  fill(col);
  rect(x, y, barW * ratio, barH, 10);

  // contour
  noFill();
  stroke(15, 23, 42);
  strokeWeight(2);
  rect(x, y, barW, barH, 10);

  // texte vie
  noStroke();
  fill(226, 232, 240);
  textAlign(LEFT, CENTER);
  textSize(14);
  text(
    'Vie : ' + nf(buddy.health, 0, 0) + ' / ' + buddy.maxHealth,
    x + 6,
    y + barH / 2
  );

  // Infos en haut
  textAlign(RIGHT, TOP);
  textSize(14);
  const wName = weapons[currentWeaponIndex].name;
  text('Arme : ' + wName, width - 30, 20);

  textAlign(LEFT, TOP);
  text(
    'Clique sur Buddy pour le frapper.\nChange d\'arme avec les boutons.\nAppuie sur R pour le réinitialiser.',
    30,
    60
  );

  if (buddy.isKO) {
    textAlign(CENTER, CENTER);
    textSize(28);
    fill(248, 250, 252);
    text('Buddy est KO ! Appuie sur R pour le réveiller.', width / 2, 60);
  }
}
Line-by-line explanation (8 lines)

🔧 Subcomponents:

calculation Health Bar Ratio const ratio = constrain(buddy.health / buddy.maxHealth, 0, 1);

Calculates what fraction of the bar to fill based on remaining health

function-call Health Bar Color const col = lerpColor(color('#22c55e'), color('#ef4444'), 1 - ratio);

Changes bar color from green (healthy) to red (damaged)

conditional KO Message if (buddy.isKO) {

Displays a large message when the buddy is knocked out

if (!buddy) return;
Exits early if buddy doesn't exist
const ratio = constrain(buddy.health / buddy.maxHealth, 0, 1);
Calculates health as a ratio from 0 (dead) to 1 (full health), clamped to stay within 0-1
fill(31, 41, 55); rect(x, y, barW, barH, 10);
Draws the dark background of the health bar
const col = lerpColor(color('#22c55e'), color('#ef4444'), 1 - ratio);
Interpolates between green (#22c55e, healthy) and red (#ef4444, damaged) based on health ratio
rect(x, y, barW * ratio, barH, 10);
Draws the colored fill bar, with width scaled by the health ratio
text('Vie : ' + nf(buddy.health, 0, 0) + ' / ' + buddy.maxHealth, x + 6, y + barH / 2);
Displays the numeric health (e.g., 'Vie : 75 / 100')
if (buddy.isKO) {
Checks if buddy is knocked out
text('Buddy est KO ! Appuie sur R pour le réveiller.', width / 2, 60);
Displays a large centered message prompting the player to press R

keyPressed()

keyPressed() is called whenever any key is pressed. This simple implementation listens for R to reset the game. You can extend this to add more keyboard controls.

function keyPressed() {
  if (key === 'r' || key === 'R') {
    resetBuddy();
  }
}
Line-by-line explanation (2 lines)
if (key === 'r' || key === 'R') {
Checks if the pressed key is 'r' or 'R' (case-insensitive)
resetBuddy();
Calls resetBuddy() to restore full health and reset game state

📦 Key Variables

buddy object

Stores all data about the main character: position (x, y), velocity (vx, vy), radius (r), health, maxHealth, and isKO flag

let buddy = { x: 450, y: 250, vx: 0, vy: 0, r: 60, health: 100, maxHealth: 100, isKO: false };
gravity number

How much downward acceleration is applied to the buddy each frame—controls how heavy the character feels

let gravity = 0.6;
airFriction number

Multiplier applied to velocity each frame while in the air (0.99 = 1% speed loss per frame)

let airFriction = 0.99;
groundFriction number

Multiplier applied to horizontal velocity when the buddy is on the ground—slows sliding faster

let groundFriction = 0.85;
particles array

Stores all active particle objects created by weapon hits—each particle has position, velocity, size, lifespan, and color

let particles = [];
floatingTexts array

Stores all active floating damage numbers—each has text, position, velocity, alpha, and color

let floatingTexts = [];
shakeTimer number

Counts down the frames remaining in the current screen shake effect—when 0, no shake

let shakeTimer = 0;
shakeMagnitude number

How many pixels the screen randomly offsets left/right during shake—higher = more intense shake

let shakeMagnitude = 8;
weapons array of objects

Defines three weapons with name, damage range (minDmg, maxDmg), knockback strength, and color hex string

const weapons = [{ name: 'Poing', minDmg: 5, maxDmg: 12, knockback: 10, color: '#f97373' }, ...];
currentWeaponIndex number

Index (0, 1, or 2) of the currently selected weapon in the weapons array

let currentWeaponIndex = 0;
uiContainer p5.Renderer (HTML div)

HTML container div that holds the weapon selection buttons

let uiContainer;
weaponButtons array of p5 buttons

Stores references to the three weapon selection button elements for updating their active state

let weaponButtons = [];

🔧 Potential Improvements (6)

Here are some ways this code could be enhanced:

BUG updateBuddy() collision detection

Buddy can get stuck bouncing infinitely inside walls if velocity is very high—no check for overshoot distance

💡 Clamp the buddy's position to be exactly at the boundary instead of just beyond: `buddy.x = constrain(buddy.x, buddy.r, width - buddy.r);`

PERFORMANCE drawUI()

String concatenation for health display happens every frame even if health hasn't changed

💡 Cache the health text string or use a template literal; alternatively, only update it when health actually changes

STYLE applyHit()

Magic numbers for screen shake (shakeTimer = 12) are unclear without comments

💡 Define `const SHAKE_FRAMES = 12;` at the top and use it here for clarity and easy tweaking

FEATURE Game mechanics

No combo system or score multiplier for rapid hits—just damage numbers

💡 Track time since last hit; if next hit is within 0.5 seconds, increase damage multiplier (2x, 3x, etc.) to reward fast clicking

FEATURE Weapons

All three weapons are always available from the start—no progression

💡 Lock stronger weapons until buddy reaches certain damage thresholds, adding a sense of progression

PERFORMANCE updateParticles() and updateFloatingTexts()

Reverse loop prevents skips, but arrays could grow very large if spawn rate is high—no max cap

💡 Add a max particle/text limit and remove oldest entries if exceeded: `if (particles.length > 500) particles.splice(0, 100);`

🔄 Code Flow

Code flow showing setup, draw, resetbuddy, updatebuddy, drawbuddy, drawplayarea, selectweapon, mousepressed, applyhit, spawnburstparticles, updateparticles, drawparticles, spawnfloatingtext, updatefloatingtexts, drawfloatingtexts, drawui, keypressed

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

graph TD start[Start] --> setup[setup] setup -->|Initialize| resetbutton[reset-button] resetbutton -->|Create UI| weaponbuttonsloop[weapon-buttons-loop] weaponbuttonsloop --> draw[draw loop] draw -->|Game Loop| update_draw_sequence[update-draw-sequence] update_draw_sequence --> updatebuddy[updatebuddy] updatebuddy --> gravityapplication[gravity-application] gravityapplication --> positionupdate[position-update] positionupdate --> wallcollisionleft[wall-collision-left] wallcollisionleft --> wallcollisionright[wall-collision-right] wallcollisionright --> ceilingcollision[ceiling-collision] ceilingcollision --> floorcollision[floor-collision] floorcollision --> airfriction[air-friction] airfriction --> kocheck[ko-check] kocheck -->|Check KO| normalface[normal-face] kocheck -->|Check KO| koface[ko-face] normalface --> drawbuddy[drawbuddy] koface --> drawbuddy drawbuddy --> drawplayarea[drawplayarea] drawplayarea --> drawparticles[drawparticles] drawparticles --> updateparticles[updateparticles] updateparticles --> cleanup[cleanup] cleanup --> drawfloatingtexts[drawfloatingtexts] drawfloatingtexts --> updatefloatingtexts[updatefloatingtexts] updatefloatingtexts --> drawui[drawui] drawui --> draw draw -->|Loop| draw click setup href "#fn-setup" click resetbutton href "#sub-reset-button" click weaponbuttonsloop href "#sub-weapon-buttons-loop" click draw href "#fn-draw" click update_draw_sequence href "#sub-update-draw-sequence" click updatebuddy href "#fn-updatebuddy" click gravityapplication href "#sub-gravity-application" click positionupdate href "#sub-position-update" click wallcollisionleft href "#sub-wall-collision-left" click wallcollisionright href "#sub-wall-collision-right" click ceilingcollision href "#sub-ceiling-collision" click floorcollision href "#sub-floor-collision" click airfriction href "#sub-air-friction" click kocheck href "#sub-ko-check" click normalface href "#sub-normal-face" click koface href "#sub-ko-face" click drawbuddy href "#fn-drawbuddy" click drawplayarea href "#fn-drawplayarea" click drawparticles href "#fn-drawparticles" click updateparticles href "#fn-updateparticles" click cleanup href "#sub-cleanup" click drawfloatingtexts href "#fn-drawfloatingtexts" click updatefloatingtexts href "#fn-updatefloatingtexts" click drawui href "#fn-drawui"

❓ Frequently Asked Questions

What visual experience does the Simple Game sketch offer?

The Simple Game sketch creates a dynamic visual environment featuring a character (Buddy) that reacts to user interactions with animations and particle effects.

How can players engage with the Simple Game sketch?

Users can interact with the sketch by selecting different weapons to use against Buddy and resetting the game with a button press.

What creative coding concepts are showcased in the Simple Game sketch?

The sketch demonstrates concepts such as particle systems, user interface creation, and basic physics simulations like gravity and friction.

Preview

Simple Game - p5.js creative coding sketch preview
Sketch Preview
Code flow diagram showing the structure of Simple Game - Code flow showing setup, draw, resetbuddy, updatebuddy, drawbuddy, drawplayarea, selectweapon, mousepressed, applyhit, spawnburstparticles, updateparticles, drawparticles, spawnfloatingtext, updatefloatingtexts, drawfloatingtexts, drawui, keypressed
Code Flow Diagram