survive ef5 tornado

This sketch creates an intense survival game where a tiny red player character must navigate a stormy field while dodging a swirling, animated tornado and debris of various sizes. The player must reach a safe shelter zone while managing health that depletes from tornado proximity and debris collisions, with the goal of surviving as long as possible.

🧪 Try This!

Experiment with the code by making these changes:

  1. Speed up the survivor — The player moves faster with higher speed values—watch them zip across the screen to outrun debris more easily
  2. Make the tornado smaller — Reduce the visual size and damage zone of the tornado, making it easier to get close without taking damage
  3. Disable tornado damage — Comment out the damage line so approaching the tornado doesn't hurt you, focusing the challenge on debris alone
  4. Make houses more common — Higher house spawn rate means more knockback physics to dodge, making the game more chaotic
  5. Spawn debris faster — Lower the spawn interval from 60 frames (1 second) to 30 frames (0.5 seconds) for more obstacles
  6. Make the shelter bigger — Expand the safe zone so it's easier to find refuge without being able to complete the game too easily
Prefer the full editor? Open it there →

📖 About This Sketch

This sketch builds a complete arcade-style survival game where you guide a red dot through increasingly chaotic tornado winds and flying debris. It's visually striking because it combines an organic, noise-based tornado animation with multiple types of moving obstacles (small debris, cars, and houses), realistic audio that changes based on proximity to the storm, and UI elements that track your health and survival time. Under the hood, it demonstrates several advanced p5.js techniques: Perlin noise for natural-looking shapes, the p5.sound library for dynamic audio feedback, collision detection with different damage values, and object-oriented design using a Debris class.

The code is organized into a game state machine (start screen, gameplay, game over), a setup and draw loop that manages sixty frames per second of animation, a Debris class that handles all flying objects with different properties and drawing logic, and several helper functions for collision checking, audio updates, and UI rendering. By studying it, you'll learn how to structure a complete game with multiple systems working together, how to make objects interact with predictable physics, and how sound can enhance interactivity when tied to game events.

⚙️ How It Works

  1. When the sketch loads, setup() creates a full-screen canvas, initializes the survivor (player) at the center, spawns a tornado at a random location, creates an initial pool of small debris objects, and sets up two p5.sound oscillators—one for wind tone and one for white noise—that will play based on game proximity.
  2. The draw loop runs at 60 frames per second. It first checks if the game has started or ended, displaying appropriate screens. During active gameplay, it clears the background, draws the green shelter zone and the survivor, updates and displays all debris objects while checking for collisions with the player, draws the tornado using Perlin noise for organic swirling animation, and checks whether the survivor touches the tornado or goes off-screen for instant death conditions.
  3. The survivor responds to WASD or arrow key input, moving at a constant speed across the canvas. The tornado stays fixed in place but animates its shape using noise functions that evolve over time, creating a hypnotic swirling effect.
  4. Debris objects (small pieces, cars, and houses) spawn every second with random types weighted toward smaller debris. Each debris item is pulled toward the tornado center with physics-like acceleration while also applying random turbulence, creating the illusion of chaotic wind.
  5. Collision detection works in two ways: gradual damage from proximity to the tornado (checked every frame, reduced if you're in shelter), and discrete damage from hitting debris (which also resets that debris). Houses deal heavy damage and 'throw' the player with momentum, creating a secondary hazard of being flung off-screen.
  6. Audio updates every frame based on the distance between survivor and tornado—closer proximity increases both wind tone volume and debris noise, creating an immersive audio landscape that mirrors the visual danger. The game ends when health reaches zero, the survivor enters the tornado's instant death zone, or goes off-screen while thrown.

🎓 Concepts You'll Learn

Game state managementCollision detectionObject-oriented programmingPerlin noise animationAudio synthesis with p5.soundPhysics simulationUser input handling

📝 Code Breakdown

setup()

setup() runs once when the sketch starts. It initializes all game objects, establishes the canvas, and prepares the audio system. Notice how objects (survivor, tornado, shelter) are defined as plain JavaScript objects with properties rather than class instances—this is a flexible approach when objects don't need methods. The debris array, however, uses a class (Debris) because each debris item needs update(), display(), and checkCollision() methods.

function setup() {
  createCanvas(windowWidth, windowHeight);
  noStroke();

  // Initialize survivor properties
  survivor = {
    x: width / 2,
    y: height / 2,
    size: 20,
    speed: 5,
    isThrown: false, // New: flag for when survivor is being thrown
    thrownVx: 0,     // New: horizontal velocity when thrown
    thrownVy: 0,     // New: vertical velocity when thrown
    thrownDuration: 0 // New: how long the thrown state lasts
  };

  // Initialize tornado properties
  tornado = {
    x: random(width),
    y: random(height),
    size: 150, // Max width of the tornado visual
    active: true,
    noiseOffset: 0, // Used to animate the tornado shape
    instantDeathZone: 30 // New: radius for instant death from tornado center
  };

  // Define shelter area
  shelter = {
    x: width / 4,
    y: height / 4,
    w: width / 2,
    h: height / 2
  };

  // Create initial debris objects (fewer initially, as new ones will spawn)
  for (let i = 0; i < 10; i++) {
    debris.push(new Debris('small')); // Initial debris are small
  }

  // p5.sound setup
  // Wind sound using a sine wave oscillator, modulated by tornado proximity
  windOsc = new p5.Oscillator('sine');
  windOsc.freq(400); // Base frequency
  windOsc.amp(0); // Start silent
  windOsc.start();

  // Debris sound using white noise, modulated by tornado proximity
  debrisSound = new p5.Noise('white');
  debrisSound.amp(0); // Start silent
  debrisSound.start();

  fft = new p5.FFT(); // For potential future audio visualization

  // User must interact with the page to start audio playback
  // This is a browser security measure to prevent unsolicited sound
  userStartAudio();
}
Line-by-line explanation (17 lines)

🔧 Subcomponents:

object-initialization Survivor object setup survivor = {

Defines all properties of the player character including position, speed, and thrown state for physics interaction

object-initialization Tornado object setup tornado = {

Stores tornado position, visual size, animation state, and the radius where instant death occurs

object-initialization Shelter zone definition shelter = {

Defines the safe rectangular area where the player is protected from tornado damage and instant death

for-loop Initial debris spawning for (let i = 0; i < 10; i++) {

Creates ten small debris objects at the start to have obstacles ready immediately

function-call p5.sound initialization windOsc = new p5.Oscillator('sine');

Creates and starts two audio sources (sine wave and white noise) that will be modulated during gameplay based on proximity

createCanvas(windowWidth, windowHeight);
Creates a full-screen canvas that responds to the window size, making the game responsive to screen dimensions
noStroke();
Removes outlines from all shapes drawn afterward, creating cleaner visuals for the game
survivor = {
Starts defining the survivor as an object with multiple properties rather than separate variables
x: width / 2,
Places the survivor horizontally at the center of the canvas
isThrown: false,
A boolean flag that tracks whether the survivor is currently being knocked back by a house collision
tornado = {
Defines the tornado as an object with properties that control its appearance and behavior
x: random(width),
Spawns the tornado at a random horizontal position to make each game different
noiseOffset: 0,
Stores a value that animates over time to drive the Perlin noise function, creating swirling motion
shelter = {
Defines a rectangular safe zone where the player avoids all tornado damage and instant death
w: width / 2,
Sets the shelter width to half the screen, making it a sizable target to reach
h: height / 2
Sets the shelter height to half the screen, creating a centered square safe area
debris.push(new Debris('small'));
Creates a new Debris object of type 'small' and adds it to the debris array using the push method
windOsc = new p5.Oscillator('sine');
Creates a sine wave oscillator that will produce the howling wind sound effect when its amplitude is increased
windOsc.freq(400);
Sets the base frequency of the wind sound to 400 Hz, a mid-range pitch that sounds like wind
windOsc.start();
Starts the oscillator playing (silently at first since amplitude is 0)
debrisSound = new p5.Noise('white');
Creates white noise (all frequencies at once) that represents the chaotic sound of debris flying
userStartAudio();
Calls p5.sound's function to request browser permission to play audio—required by modern browsers for security

draw()

The draw() function is the heartbeat of the game, called 60 times per second. Notice the early returns when the game hasn't started or is over—this is a common pattern for managing different screens or states. The debris loop demonstrates iteration and object method calls. The order of operations matters: you move the survivor before checking instant death, so the latest position is evaluated. The audio fade at the end shows how p5.sound can smoothly transition parameters over time.

🔬 This loop runs once for every debris object every frame. What happens if you comment out d.update() so the debris stops moving but still collides and draws? Which line makes the debris move?

  for (let d of debris) {
    d.update();
    d.display();
    d.checkCollision(survivor);
  }

🔬 This line counts how many seconds you've survived. What happens if you change deltaTime / 1000 to deltaTime / 500—does time count faster or slower?

  survivalTime += deltaTime / 1000;
function draw() {
  background(220, 230, 240); // Light blue/gray for cloudy sky

  // Check if the game has started
  if (!gameStarted) {
    drawStartScreen();
    return;
  }

  // Check if the game is over
  if (gameOver) {
    drawGameOverScreen();
    return;
  }

  // Update survival time
  survivalTime += deltaTime / 1000;

  // Spawn new debris periodically (once every second)
  if (frameCount % 60 == 0) {
    spawnNewDebris();
  }

  // Draw shelter area
  fill(180, 200, 180, 100); // Semi-transparent green
  rect(shelter.x, shelter.y, shelter.w, shelter.h);
  fill(0);
  textSize(16);
  textAlign(CENTER, CENTER);
  text("SHELTER", shelter.x + shelter.w / 2, shelter.y + shelter.h / 2);

  // Draw survivor
  fill(255, 0, 0); // Red circle for the survivor
  ellipse(survivor.x, survivor.y, survivor.size);

  // Update and display debris, check for collisions
  for (let d of debris) {
    d.update();
    d.display();
    d.checkCollision(survivor);
  }

  // Draw tornado and check for collisions
  drawTornado();
  checkTornadoCollision(survivor); // Handles gradual tornado damage

  // Update survivor position based on keyboard input or thrown state
  handleInput();

  // New: Check for instant death conditions after survivor's position is updated
  checkInstantDeath(survivor);

  // Display UI elements (health, time)
  drawUI();

  // Update audio effects based on game state
  updateAudio();

  // Check game over condition (if not already set by instant death)
  if (health <= 0) {
    gameOver = true;
  }

  if (gameOver) {
    // Fade out audio when game ends
    windOsc.amp(0, 0.5);
    debrisSound.amp(0, 0.5);
  }
}
Line-by-line explanation (22 lines)

🔧 Subcomponents:

conditional Game state branching if (!gameStarted) {

Routes the draw loop to different screens depending on whether the game is running or not

conditional Debris spawning condition if (frameCount % 60 == 0) {

Triggers new debris creation once per second (every 60 frames at 60 fps)

for-loop Debris update and collision loop for (let d of debris) {

Iterates through all debris objects, updating their physics and checking if they hit the player

conditional Game over condition if (health <= 0) {

Ends the game when health is fully depleted

conditional Audio fade-out if (gameOver) {

Smoothly reduces audio volume over 0.5 seconds when the game ends for a polished transition

background(220, 230, 240);
Clears the canvas with a light blue-gray color every frame, simulating a cloudy sky and erasing previous drawings
if (!gameStarted) {
Checks if the game hasn't started yet—if true, shows the start screen instead of gameplay
return;
Exits the draw function early, skipping all gameplay logic if the game hasn't started
if (gameOver) {
Checks if the game has ended—if true, shows the game over screen instead of continuing gameplay
survivalTime += deltaTime / 1000;
Increases the survival time counter by the time elapsed since the last frame (converted from milliseconds to seconds)
if (frameCount % 60 == 0) {
Uses the modulo operator to check if frameCount is divisible by 60, triggering every 60 frames (once per second at 60fps)
fill(180, 200, 180, 100);
Sets the fill color to semi-transparent green (the fourth number 100 is the alpha/transparency value)
rect(shelter.x, shelter.y, shelter.w, shelter.h);
Draws the green rectangular shelter zone at the position and dimensions stored in the shelter object
text("SHELTER", shelter.x + shelter.w / 2, shelter.y + shelter.h / 2);
Draws text at the center of the shelter box to label the safe zone
ellipse(survivor.x, survivor.y, survivor.size);
Draws a red circle at the survivor's current position with diameter equal to survivor.size
for (let d of debris) {
Uses a for...of loop to iterate through each debris object in the debris array
d.update();
Calls the update() method on each debris object to move it toward the tornado and apply physics
d.display();
Calls the display() method on each debris object to draw its shape on the canvas
d.checkCollision(survivor);
Calls the checkCollision() method on each debris object to see if it touches the player
drawTornado();
Calls the drawTornado() function to render the animated tornado at the canvas
checkTornadoCollision(survivor);
Calls the function that checks if the survivor is in the tornado's damage zone and reduces health accordingly
handleInput();
Calls the function that moves the survivor based on keyboard input or applies physics if they're being thrown
checkInstantDeath(survivor);
Calls the function that checks whether the survivor has entered instant death conditions (tornado center or screen edge)
drawUI();
Calls the function that renders the health bar and survival time display on screen
updateAudio();
Calls the function that adjusts wind and debris sound volumes based on distance to the tornado
if (health <= 0) {
Checks if health has dropped to zero or below, triggering the game over state
windOsc.amp(0, 0.5);
Fades the wind sound volume to zero over 0.5 seconds using the second parameter (ramp time)

drawTornado()

This function demonstrates advanced p5.js rendering: nested loops to create layers, Perlin noise (noise()) for organic-looking distortion, trigonometry (cos/sin) to trace circular paths, and the push/pop stack for coordinate transformations. The two loops work together—the outer loop creates 10 rings, and the inner loop creates vertices around each ring. Perlin noise makes the edges wavy rather than perfectly circular, creating the illusion of a living, swirling storm. This is the visual heart of the sketch.

🔬 This inner loop traces points around a circle. What happens if you change radians(5) to radians(10)—do you draw fewer points, creating a more jagged outline? How would the tornado look then?

    for (let a = 0; a < TWO_PI; a += radians(5)) { // Draw a polygon
      let r = map(i, 0, numShapes, 0, maxOffset); // Radius for this layer
      // Use Perlin noise to create organic offsets for the tornado shape
      let xOffset = map(noise(tornado.noiseOffset + cos(a) * detail, tornado.noiseOffset + sin(a) * detail, i * 0.1), 0, 1, -r * 0.5, r * 0.5);
function drawTornado() {
  let detail = 0.01; // Detail level for noise sampling
  let numShapes = 10; // Number of concentric shapes to draw
  let maxOffset = tornado.size / 2; // Max radius of the tornado

  tornado.noiseOffset += 0.01; // Animate the noise over time

  push();
  translate(tornado.x, tornado.y); // Center drawing at tornado's position

  for (let i = 0; i < numShapes; i++) {
    let alpha = map(i, 0, numShapes, 20, 200); // Vary transparency
    fill(100, 100, 150, alpha); // Darker blue/gray for tornado
    beginShape();
    for (let a = 0; a < TWO_PI; a += radians(5)) { // Draw a polygon
      let r = map(i, 0, numShapes, 0, maxOffset); // Radius for this layer
      // Use Perlin noise to create organic offsets for the tornado shape
      let xOffset = map(noise(tornado.noiseOffset + cos(a) * detail, tornado.noiseOffset + sin(a) * detail, i * 0.1), 0, 1, -r * 0.5, r * 0.5);
      let yOffset = map(noise(tornado.noiseOffset + cos(a) * detail + 100, tornado.noiseOffset + sin(a) * detail + 100, i * 0.1), 0, 1, -r * 0.5, r * 0.5);
      let x = cos(a) * r + xOffset;
      let y = sin(a) * r + yOffset;
      vertex(x, y);
    }
    endShape(CLOSE);
  }
  pop();
}
Line-by-line explanation (18 lines)

🔧 Subcomponents:

calculation Noise offset increment tornado.noiseOffset += 0.01;

Advances the Perlin noise seed each frame to create continuous animation

for-loop Concentric layers for (let i = 0; i < numShapes; i++) {

Draws 10 nested shapes from small (center) to large (edge) to create a filled tornado effect

for-loop Polygon vertices for (let a = 0; a < TWO_PI; a += radians(5)) {

Traces the outline of each layer by stepping around a circle in 5-degree increments

calculation Noise-based distortion let xOffset = map(noise(...), 0, 1, -r * 0.5, r * 0.5);

Uses Perlin noise to randomly distort the circle outline, creating organic swirling edges

let detail = 0.01;
Sets how tightly samples are taken from the noise function—lower values create smoother, less detailed shapes
let numShapes = 10;
Defines how many concentric rings to draw—more creates a denser tornado but uses more computation
let maxOffset = tornado.size / 2;
Calculates the maximum radius for the largest layer based on the tornado's defined size
tornado.noiseOffset += 0.01;
Increments the noise seed each frame so the noise pattern shifts continuously, animating the tornado
push();
Saves the current drawing state (coordinate system, fill, stroke) so we can modify it without affecting other drawings
translate(tornado.x, tornado.y);
Moves the origin point to the tornado's center, so all subsequent shapes are drawn relative to that position
for (let i = 0; i < numShapes; i++) {
Loops through 10 iterations, creating 10 layers of the tornado from center outward
let alpha = map(i, 0, numShapes, 20, 200);
Maps the layer number (0-10) to a transparency value (20-200)—inner layers are more transparent, outer more opaque
fill(100, 100, 150, alpha);
Sets the fill color to dark blue-gray with the calculated transparency for this layer
beginShape();
Starts recording vertices that will form a polygon shape
for (let a = 0; a < TWO_PI; a += radians(5)) {
Steps around a full circle (TWO_PI radians) in 5-degree increments, creating about 72 vertices per layer
let r = map(i, 0, numShapes, 0, maxOffset);
Calculates the radius for this layer—inner layers have smaller radius, outer layers larger
let xOffset = map(noise(...), 0, 1, -r * 0.5, r * 0.5);
Uses Perlin noise to generate a random but smooth x displacement from -50% to +50% of the radius
let x = cos(a) * r + xOffset;
Calculates the actual x position using cosine to trace the circle, then adds the noise-based offset
let y = sin(a) * r + yOffset;
Calculates the actual y position using sine to trace the circle, then adds the noise-based offset
vertex(x, y);
Adds the calculated point to the current shape being drawn
endShape(CLOSE);
Finishes the current polygon and closes it by drawing a line back to the starting vertex
pop();
Restores the drawing state that was saved by push(), returning to the original coordinate system

handleInput()

This function demonstrates dual control modes: normal keyboard input and physics-based thrown state. The keyIsDown() function checks multiple keys per frame, enabling smooth multi-directional movement (pressing two keys simultaneously works). The constrain() function is a safety net for normal movement. The thrown state bypasses player input and applies velocity with decay (multiplying by a value less than 1)—this is a simple but effective physics simulation that makes collisions feel dynamic and dangerous.

🔬 These two conditionals handle left and right movement. What happens if you remove the constrain() call later? Can the survivor go off-screen? Does that create an instant death, or do they just disappear forever?

    if (keyIsDown(LEFT_ARROW) || keyIsDown(65)) { // LEFT or A
      survivor.x -= survivor.speed;
    }
    if (keyIsDown(RIGHT_ARROW) || keyIsDown(68)) { // RIGHT or D
      survivor.x += survivor.speed;
    }
function handleInput() {
  if (!survivor.isThrown) { // Only process keyboard input if not thrown
    if (keyIsDown(LEFT_ARROW) || keyIsDown(65)) { // LEFT or A
      survivor.x -= survivor.speed;
    }
    if (keyIsDown(RIGHT_ARROW) || keyIsDown(68)) { // RIGHT or D
      survivor.x += survivor.speed;
    }
    if (keyIsDown(UP_ARROW) || keyIsDown(87)) { // UP or W
      survivor.y -= survivor.speed;
    }
    if (keyIsDown(DOWN_ARROW) || keyIsDown(83)) { // DOWN or S
      survivor.y += survivor.speed;
    }

    // Constrain survivor to the canvas boundaries for normal movement
    survivor.x = constrain(survivor.x, 0, width);
    survivor.y = constrain(survivor.y, 0, height);
  } else { // If thrown, apply thrown velocity
    survivor.x += survivor.thrownVx;
    survivor.y += survivor.thrownVy;
    survivor.thrownVx *= 0.95; // Decay thrown velocity
    survivor.thrownVy *= 0.95; // Decay thrown velocity
    survivor.thrownDuration--;
    if (survivor.thrownDuration <= 0) {
      survivor.isThrown = false;
      survivor.thrownVx = 0;
      survivor.thrownVy = 0;
    }
    // DO NOT constrain while thrown, we want them to hit walls/tornado for instant death
  }
}
Line-by-line explanation (9 lines)

🔧 Subcomponents:

conditional WASD/Arrow key movement if (keyIsDown(LEFT_ARROW) || keyIsDown(65)) {

Checks if the left arrow or 'A' key is pressed and moves the survivor left if so

function-call Canvas boundary constraint survivor.x = constrain(survivor.x, 0, width);

Prevents the survivor from moving off-screen during normal movement

conditional Thrown state movement } else { // If thrown, apply thrown velocity

Applies momentum when the survivor is hit by a house, with decay to slow down over time

calculation Friction application survivor.thrownVx *= 0.95;

Reduces thrown velocity by 5% each frame, simulating friction/air resistance

if (!survivor.isThrown) {
Checks if the survivor is NOT currently being thrown—if true, process keyboard input; otherwise apply physics
if (keyIsDown(LEFT_ARROW) || keyIsDown(65)) {
Checks if either the left arrow key or the 'A' key (ASCII 65) is currently held down
survivor.x -= survivor.speed;
Decreases the survivor's x position by their speed value, moving them left
survivor.x = constrain(survivor.x, 0, width);
Clamps the survivor's x position to stay between 0 and the canvas width, preventing off-screen movement
} else { // If thrown, apply thrown velocity
If the survivor is being thrown, skip keyboard input and instead apply velocity from the collision
survivor.x += survivor.thrownVx;
Adds the horizontal thrown velocity to the x position, moving them in the direction they were hit
survivor.thrownVx *= 0.95;
Multiplies the velocity by 0.95 (95%) each frame, gradually slowing down the thrown motion
survivor.thrownDuration--;
Decrements the counter that tracks how many frames remain in the thrown state
if (survivor.thrownDuration <= 0) {
When the thrown duration expires, the survivor is no longer being thrown and can respond to keyboard input again

checkTornadoCollision(player)

This function shows two collision detection techniques: distance-based (for circular areas like the tornado) and bounding box (for rectangular areas like the shelter). Distance-based collision checks the distance between two points and compares it to a radius—simple but effective for circular objects. Bounding box collision checks if a point falls within axis-aligned rectangle boundaries—ideal for rectangular safe zones. The shelter protection demonstrates how logic can override physics: even though you're near the tornado, you take no damage if sheltered.

🔬 The damage zone is 70% of the tornado's visual size. What if you change 0.7 to 0.3—does that make the tornado's damage zone smaller or much larger? Try it and see if you can get closer before taking damage.

  let d = dist(player.x, player.y, tornado.x, tornado.y);
  let damageZone = tornado.size * 0.7;
function checkTornadoCollision(player) {
  let d = dist(player.x, player.y, tornado.x, tornado.y);
  let damageZone = tornado.size * 0.7; // Wider damage zone than visual size
  let inShelter = player.x > shelter.x && player.x < shelter.x + shelter.w &&
                  player.y > shelter.y && player.y < shelter.y + shelter.h;

  if (d < damageZone && !inShelter) {
    health -= 0.5; // Constant damage per frame outside shelter near tornado
  }
}
Line-by-line explanation (6 lines)

🔧 Subcomponents:

calculation Distance to tornado let d = dist(player.x, player.y, tornado.x, tornado.y);

Calculates the straight-line distance between the player and tornado center

conditional Shelter boundary test let inShelter = player.x > shelter.x && player.x < shelter.x + shelter.w &&

Tests whether the player is inside the rectangular shelter zone using bounding box collision

conditional Tornado damage application if (d < damageZone && !inShelter) {

Applies health reduction every frame if the player is close to the tornado AND not in shelter

let d = dist(player.x, player.y, tornado.x, tornado.y);
Uses p5.js's dist() function to calculate the Euclidean distance between player and tornado
let damageZone = tornado.size * 0.7;
Defines the damage radius as 70% of the tornado's visual size, making the damage zone larger than what you see
let inShelter = player.x > shelter.x && player.x < shelter.x + shelter.w &&
Checks if the player's x coordinate is between the left and right edges of the shelter—first part of bounding box test
player.y > shelter.y && player.y < shelter.y + shelter.h;
Checks if the player's y coordinate is between the top and bottom edges of the shelter—completes bounding box test
if (d < damageZone && !inShelter) {
Only apply damage if two conditions are both true: within damage zone AND not in shelter
health -= 0.5;
Reduces health by 0.5 points per frame while in the damage zone, gradually depleting the health bar

checkInstantDeath(player)

This function enforces instant-death conditions separate from gradual health damage. It's called after handleInput() in draw() so the latest position is checked. Notice the shelter condition again—even extreme closeness to the tornado is safe if you're sheltered. The screen edge check is especially important because the handleInput() function allows thrown players to move without constraints, creating a secondary hazard: being knocked off-screen is instant death.

function checkInstantDeath(player) {
  let d = dist(player.x, player.y, tornado.x, tornado.y);
  let inShelter = player.x > shelter.x && player.x < shelter.x + shelter.w &&
                  player.y > shelter.y && player.y < shelter.y + shelter.h;

  // 1. Direct Tornado Contact (very close to center and not in shelter)
  if (d < tornado.instantDeathZone && !inShelter) {
    gameOver = true;
    return;
  }

  // 2. Hitting Canvas Edges (being thrown off-screen)
  if (player.x <= 0 || player.x >= width || player.y <= 0 || player.y >= height) {
    gameOver = true;
    return;
  }
}
Line-by-line explanation (4 lines)

🔧 Subcomponents:

conditional Direct tornado contact check if (d < tornado.instantDeathZone && !inShelter) {

Triggers instant game over if player touches the tornado center and is not in shelter

conditional Screen boundary check if (player.x <= 0 || player.x >= width || player.y <= 0 || player.y >= height) {

Triggers instant game over if player is thrown off the screen

let d = dist(player.x, player.y, tornado.x, tornado.y);
Calculates distance between player and tornado center to check for direct contact
if (d < tornado.instantDeathZone && !inShelter) {
Ends the game immediately if player enters the tornado's instant death zone and isn't protected by shelter
return;
Exits the function after setting gameOver, avoiding unnecessary checks
if (player.x <= 0 || player.x >= width || player.y <= 0 || player.y >= height) {
Checks all four screen edges—if player goes beyond any edge, they're off-screen and dead

drawUI()

This function displays game state information to the player. The map() function converts health (0-100) to pixels (0-100) so the green bar width accurately reflects health percentage. Template literals with backticks (`) and ${} allow embedding variables directly in text strings, making UI updates simple and readable.

function drawUI() {
  fill(0);
  textSize(24);
  textAlign(LEFT, TOP);
  text(`Health: ${floor(health)}`, 10, 10);
  text(`Time: ${floor(survivalTime)}s`, 10, 40);

  // Health bar visualization
  fill(255, 0, 0); // Red background for health bar
  rect(10, 70, 100, 10);
  fill(0, 255, 0); // Green foreground for current health
  rect(10, 70, map(health, 0, 100, 0, 100), 10);
}
Line-by-line explanation (9 lines)

🔧 Subcomponents:

function-call Health and time text text(`Health: ${floor(health)}`, 10, 10);

Displays the current health value as an integer in the top-left corner

drawing Health bar visualization rect(10, 70, map(health, 0, 100, 0, 100), 10);

Draws a green rectangle whose width corresponds to the player's health percentage

fill(0);
Sets the fill color to black for text readability
textSize(24);
Sets the text size to 24 pixels for the health and time displays
textAlign(LEFT, TOP);
Aligns text so the position specified is the top-left corner, making layout predictable
text(`Health: ${floor(health)}`, 10, 10);
Displays 'Health: ' followed by the health value rounded down to an integer at position (10, 10)
text(`Time: ${floor(survivalTime)}s`, 10, 40);
Displays 'Time: ' followed by survival time rounded down, with an 's' suffix for seconds
fill(255, 0, 0);
Changes fill to red for the health bar background
rect(10, 70, 100, 10);
Draws a 100x10 pixel red rectangle as the background of the health bar
fill(0, 255, 0);
Changes fill to green for the current health indicator
rect(10, 70, map(health, 0, 100, 0, 100), 10);
Draws a green rectangle whose width maps from the health range (0-100) to pixel range (0-100), showing health percentage visually

updateAudio()

This function demonstrates dynamic audio feedback—sound changes based on game state. The map() function with the fifth parameter set to true constrains the output to the specified range, preventing audio from going negative or exceeding maximum volume. The 0.1-second ramp time in amp() smoothly transitions volume changes instead of abrupt clicks. Tying audio to proximity creates immersion: the closer you get to danger, the louder the warning sounds.

🔬 The map() function converts distance to volume on a scale of 0 to 0.4. What happens if you change 0.4 to 1.0—does the wind get louder when you're far away? Try it and listen for the difference.

  windAmp = map(d, maxTornadoDist, 0, 0, 0.4, true);
  windOsc.amp(windAmp, 0.1);
function updateAudio() {
  let windAmp;
  let debrisAmp;

  // Tornado proximity determines wind volume
  let d = dist(survivor.x, survivor.y, tornado.x, tornado.y);
  let maxTornadoDist = width / 2; // Max distance for full wind sound
  // Map distance to amplitude, constraining values between 0 and 0.4
  windAmp = map(d, maxTornadoDist, 0, 0, 0.4, true);
  windOsc.amp(windAmp, 0.1); // Smoothly change amplitude over 0.1 seconds

  // Debris sound amplitude also increases with proximity to tornado
  debrisAmp = map(windAmp, 0, 0.4, 0, 0.2, true);
  debrisSound.amp(debrisAmp, 0.1);
}
Line-by-line explanation (6 lines)

🔧 Subcomponents:

calculation Distance to tornado let d = dist(survivor.x, survivor.y, tornado.x, tornado.y);

Calculates how far the player is from the tornado

calculation Wind amplitude mapping windAmp = map(d, maxTornadoDist, 0, 0, 0.4, true);

Converts distance to volume level, with true constraining results to the output range

calculation Debris amplitude mapping debrisAmp = map(windAmp, 0, 0.4, 0, 0.2, true);

Ties debris noise volume to wind volume for cohesive audio feedback

let d = dist(survivor.x, survivor.y, tornado.x, tornado.y);
Calculates the straight-line distance between player and tornado to determine threat level
let maxTornadoDist = width / 2;
Defines the distance at which wind sound is completely silent—beyond half the screen width, no wind sound
windAmp = map(d, maxTornadoDist, 0, 0, 0.4, true);
Maps distance range (maxTornadoDist to 0) to volume range (0 to 0.4), with true constraining results; closer = louder
windOsc.amp(windAmp, 0.1);
Sets the wind oscillator's volume to windAmp, smoothly ramping over 0.1 seconds to avoid audio clicks
debrisAmp = map(windAmp, 0, 0.4, 0, 0.2, true);
Maps the wind amplitude (0-0.4) to debris amplitude (0-0.2), so debris volume follows wind volume
debrisSound.amp(debrisAmp, 0.1);
Sets the debris sound's volume with the same 0.1-second smooth ramp

spawnNewDebris()

This function shows weighted random selection: different outcomes have different probabilities. By testing the random number against different thresholds, you create a probability distribution. Spawning debris periodically (called every 60 frames in draw) keeps the game populated with obstacles without overwhelming the system immediately.

🔬 Houses deal 10 damage, cars deal 5, small debris deals 1. What happens if you change rand < 1 to rand < 50 to make houses appear much more often? Does the game become harder or easier?

  if (rand < 1) { // 1% chance for a house (rand = 0)
    debrisType = 'house';
  } else if (rand < 20) { // 19% chance for a car (rand = 1-19)
    debrisType = 'car';
function spawnNewDebris() {
  let debrisType = 'small';
  let rand = random(100); // 0-99

  if (rand < 1) { // 1% chance for a house (rand = 0)
    debrisType = 'house';
  } else if (rand < 20) { // 19% chance for a car (rand = 1-19)
    debrisType = 'car';
  } else { // 80% chance for small debris (rand = 20-99)
    debrisType = 'small';
  }
  debris.push(new Debris(debrisType));
}
Line-by-line explanation (6 lines)

🔧 Subcomponents:

conditional Debris type probability if (rand < 1) {

Uses weighted random chance to select debris type, with houses rarest and small debris most common

function-call Debris object instantiation debris.push(new Debris(debrisType));

Creates a new Debris object and adds it to the array

let debrisType = 'small';
Sets a default debris type in case none of the conditions match (though one always will)
let rand = random(100);
Generates a random number between 0 and 100 (exclusive of 100, so 0-99.999...)
if (rand < 1) {
If the random number is less than 1 (roughly 1% chance), select house type
} else if (rand < 20) {
If the random number is 1-19 (roughly 19% chance), select car type
} else {
If the random number is 20-99 (roughly 80% chance), stick with small type
debris.push(new Debris(debrisType));
Creates a new Debris object with the selected type and adds it to the debris array

class Debris

The Debris class demonstrates object-oriented design: each debris object has its own properties (x, y, vx, vy, size, color, damage) and methods (update, display, checkCollision, reset). The constructor and reset() method show DRY principles—Don't Repeat Yourself—by putting initialization logic in one place. The type system allows a single class to represent three different objects with different behaviors. The update() method shows physics: attraction to the tornado (using atan2), random turbulence, and velocity constraints. The collision detection uses circle-to-circle collision (distance between centers vs. sum of radii) and demonstrates how a collision can trigger multiple effects: damage, knockback physics, and respawning.

🔬 This code pulls debris toward the tornado using atan2() to find the direction. What happens if you change 0.05 to 0.2—do debris accelerate faster toward the tornado or slower? Try it and watch the debris behavior change.

    // Move debris towards tornado center
    let dx = tornado.x - this.x;
    let dy = tornado.y - this.y;
    let angleToTornado = atan2(dy, dx);
    this.vx += cos(angleToTornado) * 0.05;
    this.vy += sin(angleToTornado) * 0.05;

🔬 The roof peak is drawn at (0, -this.size). What happens if you change -this.size to 0—does the house look different or does the roof disappear?

      // Draw roof
      fill(this.roofColor);
      triangle(
        -this.size / 2, -this.size / 2,
        this.size / 2, -this.size / 2,
        0, -this.size // Roof peak
      );
class Debris {
  constructor(type = 'small') { // Default to 'small' debris
    this.reset(type);
  }

  // Reset debris position and properties
  reset(type = this.type) { // Allow resetting with a specific type or current type
    this.type = type;
    this.x = random(width);
    this.y = random(height);
    this.vx = random(-2, 2);
    this.vy = random(-2, 2);
    this.spin = random(-0.1, 0.1); // Rotation speed
    this.angle = 0;

    // Set properties based on debris type
    if (this.type === 'house') {
      this.size = random(80, 120); // Houses are large
      this.color = color(150, 75, 0); // Brown for house body
      this.roofColor = color(200, 0, 0); // Red for roof
      this.damage = 10; // High damage
      this.vx = random(-1, 1); // Houses move slower
      this.vy = random(-1, 1);
    } else if (this.type === 'car') {
      this.size = random(40, 60); // Cars are medium size
      this.color = color(100, 100, 100); // Gray for car body
      this.damage = 5; // Medium damage
      this.vx = random(-1.5, 1.5);
      this.vy = random(-1.5, 1.5);
    } else { // 'small' debris (default)
      this.size = random(5, 20); // Small debris
      this.color = color(139, 69, 19); // Brown for small debris
      this.damage = 1; // Low damage
      this.vx = random(-2, 2);
      this.vy = random(-2, 2);
    }
  }

  // Update debris movement and rotation
  update() {
    this.x += this.vx;
    this.y += this.vy;
    this.angle += this.spin;

    // Move debris towards tornado center
    let dx = tornado.x - this.x;
    let dy = tornado.y - this.y;
    let angleToTornado = atan2(dy, dx);
    this.vx += cos(angleToTornado) * 0.05;
    this.vy += sin(angleToTornado) * 0.05;

    // Add some random turbulence
    this.vx += random(-0.1, 0.1);
    this.vy += random(-0.1, 0.1);

    // Constrain velocity to prevent extreme speeds
    this.vx = constrain(this.vx, -5, 5);
    this.vy = constrain(this.vy, -5, 5);

    // Wrap debris around the edges of the canvas
    if (this.x < -this.size) this.x = width + this.size;
    if (this.x > width + this.size) this.x = -this.size;
    if (this.y < -this.size) this.y = height + this.size;
    if (this.y > height + this.size) this.y = -this.size;
  }

  // Display the debris based on its type
  display() {
    push();
    translate(this.x, this.y);
    rotate(this.angle);
    fill(this.color);

    if (this.type === 'house') {
      // Draw house body
      rect(-this.size / 2, -this.size / 2, this.size, this.size);
      // Draw roof
      fill(this.roofColor);
      triangle(
        -this.size / 2, -this.size / 2,
        this.size / 2, -this.size / 2,
        0, -this.size // Roof peak
      );
    } else if (this.type === 'car') {
      let carWidth = this.size;
      let carHeight = this.size * 0.6;
      let cabinHeight = carHeight * 0.4;
      rect(-carWidth / 2, -carHeight / 2, carWidth, carHeight); // Car body
      rect(-carWidth / 4, -carHeight / 2 - cabinHeight, carWidth / 2, cabinHeight); // Car cabin
    } else { // 'small' debris
      rect(-this.size / 2, -this.size / 2, this.size, this.size);
    }
    pop();
  }

  // Check for collision with the player survivor
  checkCollision(player) {
    let d = dist(this.x, this.y, player.x, player.y);
    // Collision radius is half of debris size + half of player size
    if (d < (this.size / 2 + player.size / 2)) {
      health -= this.damage; // Apply specific damage for this debris type

      // New: If it's a house, "throw" the player
      if (this.type === 'house' && !player.isThrown) {
        let pushForce = 20; // How strongly the player is pushed
        let dx = player.x - this.x;
        let dy = player.y - this.y;
        let angleOfPush = atan2(dy, dx);
        player.isThrown = true;
        player.thrownVx = cos(angleOfPush) * pushForce;
        player.thrownVy = sin(angleOfPush) * pushForce;
        player.thrownDuration = 30; // Thrown for 0.5 seconds (30 frames)
      }

      this.reset(); // Respawn debris after collision (as the same type)
    }
  }
}
Line-by-line explanation (30 lines)

🔧 Subcomponents:

function Constructor constructor(type = 'small') {

Initializes a new Debris object with a specified type, defaulting to 'small' if not provided

function Reset method reset(type = this.type) {

Sets or resets debris position and properties based on type, used for both initial creation and respawning

conditional Type-specific properties if (this.type === 'house') {

Assigns size, color, damage, and velocity based on the debris type (house, car, or small)

function Update method update() {

Moves debris toward the tornado, applies turbulence, constrains velocity, and wraps around screen edges

calculation Tornado attraction physics let angleToTornado = atan2(dy, dx);

Calculates the angle from debris to tornado and accelerates debris in that direction

function Display method display() {

Draws the debris at its current position with rotation, using different shapes for each type

function Collision detection method checkCollision(player) {

Tests if debris touches the player, applies damage, and throws the player if hit by a house

constructor(type = 'small') {
Defines the constructor function that runs when new Debris() is called, with an optional type parameter
this.reset(type);
Calls the reset method to initialize all properties, avoiding code duplication
reset(type = this.type) {
Allows resetting an existing debris object with a new type, or using its current type if none is specified
this.x = random(width);
Spawns the debris at a random horizontal position on the canvas
this.vx = random(-2, 2);
Gives the debris an initial random horizontal velocity between -2 and 2 pixels per frame
this.spin = random(-0.1, 0.1);
Sets a random rotation speed that will be added to angle each update, creating spinning motion
this.damage = 10;
Houses deal high damage (10 points) compared to cars (5) and small debris (1)
this.vx = random(-1, 1);
Houses move slower than small debris because they're heavier and harder to push around by the tornado
update() {
Called every frame to move the debris and update its physics
this.x += this.vx;
Updates the x position by adding the horizontal velocity
this.angle += this.spin;
Rotates the debris by adding the spin value, creating continuous rotation
let angleToTornado = atan2(dy, dx);
Uses atan2 to calculate the angle from debris to tornado center, accounting for both x and y differences
this.vx += cos(angleToTornado) * 0.05;
Accelerates the debris toward the tornado by adding a component of force in the tornado's direction
this.vx += random(-0.1, 0.1);
Adds random turbulence to the velocity, making debris movement chaotic and unpredictable
this.vx = constrain(this.vx, -5, 5);
Caps velocity between -5 and 5 pixels per frame to prevent debris from moving infinitely fast
if (this.x < -this.size) this.x = width + this.size;
If debris goes off the left edge, wrap it around to appear from the right side
display() {
Called every frame to draw the debris at its current position with rotation
translate(this.x, this.y);
Moves the origin to the debris's position so shapes are drawn centered on the debris
rotate(this.angle);
Rotates all subsequent drawings by the debris's current angle
if (this.type === 'house') {
Checks the debris type and draws the appropriate shape
triangle(
Draws a triangular roof on top of the house body to make it recognizable
checkCollision(player) {
Tests whether this debris object is touching the player
let d = dist(this.x, this.y, player.x, player.y);
Calculates the distance between debris and player centers
if (d < (this.size / 2 + player.size / 2)) {
Checks if the distance is less than the sum of radii—a circle-to-circle collision test
health -= this.damage;
Applies damage specific to this debris type (house=10, car=5, small=1)
if (this.type === 'house' && !player.isThrown) {
Only throws the player if hit by a house AND not already thrown (preventing double-throw)
let angleOfPush = atan2(dy, dx);
Calculates the direction from debris to player to push them away from impact
player.thrownVx = cos(angleOfPush) * pushForce;
Gives the player horizontal momentum based on the push direction and force
player.thrownDuration = 30;
Sets thrown state to last 30 frames (0.5 seconds at 60 fps)
this.reset();
Respawns the debris at a new random location after collision, keeping the same type

drawStartScreen()

This simple function displays the game's initial screen before play begins. It's called from draw() when gameStarted is false. The structure is straightforward: set colors and text sizes, then draw text at calculated positions. The vertical positions use height/2 as reference, allowing the layout to adapt to different screen heights.

function drawStartScreen() {
  background(0);
  fill(255);
  textSize(48);
  textAlign(CENTER, CENTER);
  text("Survive the EF5 Tornado!", width / 2, height / 2 - 50);
  textSize(24);
  text("Press SPACE to start", width / 2, height / 2 + 20);
  textSize(18);
  text("Controls: WASD or Arrow Keys", width / 2, height / 2 + 60);
  text("Find shelter to avoid the tornado's wrath!", width / 2, height / 2 + 90);
}
Line-by-line explanation (4 lines)
background(0);
Clears the canvas with black for a dark, dramatic start screen
fill(255);
Sets text color to white for high contrast against the black background
textSize(48);
Sets the title text size to 48 pixels for prominence
text("Survive the EF5 Tornado!", width / 2, height / 2 - 50);
Displays the title centered horizontally and positioned above the vertical center

drawGameOverScreen()

This function displays the game over screen with a semi-transparent overlay, keeping the final game state visible beneath. The use of survivalTime creates a sense of achievement—the player can see exactly how long they lasted.

function drawGameOverScreen() {
  background(0, 150); // Dark semi-transparent overlay
  fill(255);
  textSize(48);
  textAlign(CENTER, CENTER);
  text("GAME OVER", width / 2, height / 2 - 50);
  textSize(24);
  text(`You survived for ${floor(survivalTime)} seconds`, width / 2, height / 2 + 20);
  text("Press SPACE to restart", width / 2, height / 2 + 60);
}
Line-by-line explanation (2 lines)
background(0, 150);
Draws a semi-transparent black overlay (alpha=150) over the game, keeping the previous frame visible but darkened
text(`You survived for ${floor(survivalTime)} seconds`, width / 2, height / 2 + 20);
Displays the player's final survival time using a template literal and floor() to round down to an integer

keyPressed()

The keyPressed() function runs once whenever any key is pressed. By checking keyCode, we can respond to specific keys. This function demonstrates state-based control: the same key (space) has different effects depending on game state (not started vs. game over), making the UI intuitive.

function keyPressed() {
  if (keyCode === 32) { // SPACE key
    if (!gameStarted) {
      gameStarted = true;
      resetGame();
    } else if (gameOver) {
      resetGame();
      gameOver = false;
      gameStarted = true;
    }
  }
}
Line-by-line explanation (5 lines)

🔧 Subcomponents:

conditional Space bar detection if (keyCode === 32) {

Checks if the space bar (ASCII 32) was pressed

conditional Game start/restart logic if (!gameStarted) {

Routes to the appropriate action based on current game state

if (keyCode === 32) {
Checks if the pressed key is the space bar (keyCode 32)
if (!gameStarted) {
If the game hasn't started yet, this branch starts a new game
gameStarted = true;
Sets the game started flag to true, causing draw() to skip the start screen
resetGame();
Calls resetGame() to initialize all game variables for the new game
} else if (gameOver) {
If the game is over, this branch restarts the game

resetGame()

resetGame() demonstrates the importance of initialization in game loops. Every variable that changes during gameplay must be reset to start a fresh game. Notice that the survivor is always respawned at the center and the tornado at a random location—this adds variability. Clearing the debris array and repopulating it is cleaner than trying to reset individual debris objects.

function resetGame() {
  survivor.x = width / 2;
  survivor.y = height / 2;
  health = 100;
  survivalTime = 0;
  tornado.x = random(width);
  tornado.y = random(height);
  tornado.noiseOffset = 0;
  debris = [];
  // Initial debris objects (fewer initially, as new ones will spawn)
  for (let i = 0; i < 10; i++) {
    debris.push(new Debris('small'));
  }
  // Reset survivor thrown state
  survivor.isThrown = false;
  survivor.thrownVx = 0;
  survivor.thrownVy = 0;
  survivor.thrownDuration = 0;
  // Ensure audio is started again after reset
  userStartAudio();
}
Line-by-line explanation (9 lines)

🔧 Subcomponents:

calculation Survivor state reset survivor.x = width / 2;

Returns the player to the center of the canvas

assignment Debris array reset debris = [];

Clears all debris objects and creates a fresh array

calculation Thrown state reset survivor.isThrown = false;

Ensures the player is not in a thrown state at game start

survivor.x = width / 2;
Positions the survivor at the horizontal center of the canvas
health = 100;
Resets health to full for the new game
survivalTime = 0;
Resets the survival time counter to zero
tornado.x = random(width);
Places the tornado at a new random location, making each game different
tornado.noiseOffset = 0;
Resets the animation offset so the tornado starts its animation cycle fresh
debris = [];
Clears all debris objects from the array, starting with a clean slate
for (let i = 0; i < 10; i++) {
Creates 10 new small debris objects to populate the new game
survivor.isThrown = false;
Ensures the survivor starts in a normal (not thrown) state
userStartAudio();
Re-requests permission to play audio (browser security requirement)

windowResized()

The windowResized() function is called automatically by p5.js whenever the browser window is resized. It ensures the game remains responsive and visually balanced at any screen size. By recalculating all positions based on width and height, the game adapts gracefully rather than becoming distorted or broken.

function windowResized() {
  resizeCanvas(windowWidth, windowHeight);
  // Re-calculate positions for responsiveness
  survivor.x = width / 2;
  survivor.y = height / 2;
  shelter.x = width / 4;
  shelter.y = height / 4;
  shelter.w = width / 2;
  shelter.h = height / 2;
  tornado.x = random(width);
  tornado.y = random(height);
}
Line-by-line explanation (3 lines)
resizeCanvas(windowWidth, windowHeight);
Resizes the p5.js canvas to match the new window dimensions
survivor.x = width / 2;
Re-centers the survivor in the resized canvas
shelter.x = width / 4;
Recalculates the shelter position to maintain proportional layout

📦 Key Variables

survivor object

Stores the player character's position, speed, size, and thrown state for physics interaction

let survivor = { x: 200, y: 200, size: 20, speed: 5, isThrown: false, thrownVx: 0, thrownVy: 0, thrownDuration: 0 }
tornado object

Stores the tornado's position, visual size, animation state (noiseOffset), and instant death zone radius

let tornado = { x: 300, y: 300, size: 150, active: true, noiseOffset: 0, instantDeathZone: 30 }
debris array

Holds all active Debris objects in the game, allowing them to be updated and checked for collisions each frame

let debris = [new Debris('small'), new Debris('car'), new Debris('house')]
shelter object

Defines the safe rectangular zone where the player is protected from tornado damage and instant death

let shelter = { x: 100, y: 100, w: 400, h: 400 }
health number

Tracks the player's remaining health points (0-100); game ends when health reaches zero

let health = 100;
survivalTime number

Tracks how many seconds the player has survived; incremented each frame and displayed in the UI

let survivalTime = 0;
gameOver boolean

Flag indicating whether the game has ended; used to display game over screen instead of gameplay

let gameOver = false;
gameStarted boolean

Flag indicating whether the game has started; used to display start screen until player presses space

let gameStarted = false;
windOsc p5.Oscillator

A sine wave sound source that produces the howling wind effect, modulated by proximity to tornado

let windOsc = new p5.Oscillator('sine');
debrisSound p5.Noise

A white noise sound source that produces the chaotic debris sound, modulated by proximity to tornado

let debrisSound = new p5.Noise('white');
fft p5.FFT

An FFT analyzer for potential future audio visualization (initialized but not currently used in gameplay)

let fft = new p5.FFT();

🔧 Potential Improvements (5)

Here are some ways this code could be enhanced:

BUG checkTornadoCollision() and checkInstantDeath()

Shelter boundary checks are duplicated in two functions, making the code harder to maintain if shelter logic changes

💡 Create a helper function isInShelter(player) that both functions can call, reducing duplication and making changes easier

PERFORMANCE drawTornado()

The tornado is redrawn completely every frame with complex nested loops and noise calculations, which could slow down on slower devices

💡 Consider caching the tornado shape once every 10 frames or using a simpler visual representation for low-end devices

STYLE Debris class

The reset() method has three separate if/else branches for debris types, making it hard to add new types without code duplication

💡 Create a configuration object that maps type names to properties (size, color, damage, velocity), allowing new types to be added in a single line

FEATURE Game overall

There is no difficulty scaling—the game doesn't get harder as the player survives longer, so high scores plateau quickly

💡 Gradually increase debris spawn rate, tornado size, or damage rate based on survivalTime to create escalating challenge

FEATURE drawTornado()

The tornado is purely decorative and doesn't chase the player, reducing threat perception

💡 Make the tornado slowly move toward the player or toward the shelter, increasing pressure and dynamic gameplay

🔄 Code Flow

Code flow showing setup, draw, drawtornado, handleinput, checktornadocollision, checkinstantdeath, drawui, updateaudio, spawnnewdebris, debris, drawstartscreen, drawgameoverscreen, keypressed, resetgame, windowresized

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

graph TD start[Start] --> setup[setup] setup --> draw[draw loop] setup --> survivor-init[survivor-init] setup --> tornado-init[tornado-init] setup --> shelter-init[shelter-init] setup --> audio-setup[audio-setup] draw --> state-check[state-check] state-check -->|Game Running| handleinput[handleinput] state-check -->|Game Not Started| drawstartscreen[drawstartscreen] state-check -->|Game Over| drawgameoverscreen[drawgameoverscreen] handleinput --> keyboard-input[keyboard-input] handleinput --> boundary-check[boundary-check] handleinput --> thrown-physics[thrown-physics] handleinput --> velocity-decay[velocity-decay] handleinput --> debris-loop[debris-loop] debris-loop -->|Update| debris-loop[debris-loop] debris-loop --> distance-calc[distance-calc] debris-loop --> checktornadocollision[checktornadocollision] checktornadocollision --> shelter-check[shelter-check] checktornadocollision --> damage-condition[damage-condition] checktornadocollision --> tornado-contact[tornado-contact] checktornadocollision --> edge-collision[edge-collision] handleinput --> checkinstantdeath[checkinstantdeath] checkinstantdeath --> health-check[health-check] draw --> drawui[drawui] drawui --> text-display[text-display] drawui --> health-bar[health-bar] draw --> updateaudio[updateaudio] draw --> spawnnewdebris[spawnnewdebris] spawnnewdebris --> debris-spawn[debris-spawn] spawnnewdebris --> type-selection[type-selection] type-selection --> debris-creation[debris-creation] debris-creation --> constructor[constructor] constructor --> reset-method[reset-method] reset-method --> type-properties[type-properties] draw --> drawtornado[drawtornado] drawtornado --> outer-loop[outer-loop] outer-loop --> inner-loop[inner-loop] inner-loop --> perlin-offset[perlin-offset] noise-animation --> perlin-offset click setup href "#fn-setup" click draw href "#fn-draw" click survivor-init href "#sub-survivor-init" click tornado-init href "#sub-tornado-init" click shelter-init href "#sub-shelter-init" click audio-setup href "#sub-audio-setup" click state-check href "#sub-state-check" click handleinput href "#fn-handleinput" click keyboard-input href "#sub-keyboard-input" click boundary-check href "#sub-boundary-check" click thrown-physics href "#sub-thrown-physics" click velocity-decay href "#sub-velocity-decay" click debris-loop href "#sub-debris-loop" click distance-calc href "#sub-distance-calc" click checktornadocollision href "#fn-checktornadocollision" click shelter-check href "#sub-shelter-check" click damage-condition href "#sub-damage-condition" click tornado-contact href "#sub-tornado-contact" click edge-collision href "#sub-edge-collision" click checkinstantdeath href "#fn-checkinstantdeath" click health-check href "#sub-health-check" click drawui href "#fn-drawui" click text-display href "#sub-text-display" click health-bar href "#sub-health-bar" click updateaudio href "#fn-updateaudio" click spawnnewdebris href "#fn-spawnnewdebris" click debris-spawn href "#sub-debris-spawn" click type-selection href "#sub-type-selection" click debris-creation href "#sub-debris-creation" click constructor href "#sub-constructor" click reset-method href "#sub-reset-method" click type-properties href "#sub-type-properties" click drawtornado href "#fn-drawtornado" click outer-loop href "#sub-outer-loop" click inner-loop href "#sub-inner-loop" click perlin-offset href "#sub-perlin-offset"

❓ Frequently Asked Questions

What visual experience does the 'survive ef5 tornado' sketch offer?

The sketch creates a dynamic visual landscape featuring a swirling tornado, flying debris, and a tiny red survivor navigating through a stormy field towards a safe shelter.

How can players interact with the 'survive ef5 tornado' sketch?

Users can control the tiny red survivor using keyboard inputs to dodge the approaching tornado and debris while monitoring their health and survival time.

What creative coding concepts are demonstrated in the 'survive ef5 tornado' sketch?

The sketch showcases techniques such as object-oriented programming for the survivor and tornado, real-time interaction through keyboard controls, and sound modulation based on proximity to the tornado.

Preview

survive ef5 tornado - p5.js creative coding sketch preview
Sketch Preview
Code flow diagram showing the structure of survive ef5 tornado - Code flow showing setup, draw, drawtornado, handleinput, checktornadocollision, checkinstantdeath, drawui, updateaudio, spawnnewdebris, debris, drawstartscreen, drawgameoverscreen, keypressed, resetgame, windowresized
Code Flow Diagram