veck.io pls friend moon_noob on the real website veck.io

This sketch creates a hybrid 2D/3D space shooter game using p5.js for UI and game logic combined with Three.js for 3D rendering. Players control a spaceship on a procedurally generated grid arena, shooting down incoming red enemies while managing health, across three game states: start screen, active gameplay, and game over screen.

🧪 Try This!

Experiment with the code by making these changes:

  1. Make the spaceship red — The player's color is defined in the Player class constructor - change 0x0096ff (blue) to a red color code and your spaceship becomes red
  2. Make enemies yellow instead of red — The enemy color is also in the Enemy class constructor - change 0xff0000 (red) to 0xffff00 (yellow)
  3. Double the bullet damage — Each bullet currently deals 10 damage - change this to 20 so enemies die faster
  4. Make the player move twice as fast — The player currently moves 5 pixels per arrow key press - increase it to 10 for faster lateral movement
  5. Slow down enemy spawning — Enemies currently start spawning every 1000ms - increase it to 2000ms so the game starts easier
  6. Spin the grid faster — The grid rotates very slowly - increase the rotation speed from 0.001 to 0.01 for 10x faster spin
Prefer the full editor? Open it there →

📖 About This Sketch

This sketch creates a full 3D space shooter game that combines the best of two libraries: p5.js handles the 2D text UI and game logic, while Three.js renders a beautiful procedurally generated grid arena in 3D. Players control a blue spaceship at the bottom of the screen, press SPACE to shoot yellow bullets at incoming red enemies, and must manage health while accumulating score. The visual appeal comes from the rotating grid cubes in the background, smooth camera controls on the start screen, and the blend of two rendering systems working seamlessly together.

The code is organized into three main sections: game state management (start, playing, gameOver), three classes that represent game entities (Player, Bullet, Enemy), and helper functions for mapping between p5.js 2D coordinates and Three.js 3D coordinates. By studying this sketch you will learn how to layer two different rendering systems, manage complex game state across multiple screens, implement collision detection between game objects, and organize a larger sketch into reusable classes that handle both logic and rendering.

⚙️ How It Works

  1. When the sketch loads, setup() creates both a p5.js canvas for UI overlay and a Three.js renderer underneath it. The Player object is initialized at the bottom of the screen, and a procedurally generated grid of height-varying cubes is created using p5.js noise() to make the arena organic-looking.
  2. The draw() function checks gameState and either shows the start screen, plays the game, or displays the game over screen. On the start screen, camera OrbitControls are enabled so viewers can orbit around the 3D grid.
  3. During gameplay, the playGame() function updates the player position based on LEFT/RIGHT arrow keys, handles shooting when SPACE is pressed, and updates all bullets and enemies every frame.
  4. Bullets move upward on screen (toward negative Z in Three.js), and each frame the code checks if any bullet intersects an enemy's bounding box. If a hit occurs, the enemy takes damage, a hit sound plays, and the score increases.
  5. Enemies spawn at random horizontal positions and move downward toward the player. The spawn rate increases over time to make the game harder. If an enemy reaches the player's position, both lose health; if the player's health reaches zero, the game ends.
  6. An animate() function powered by requestAnimationFrame continuously renders the Three.js scene while keeping the p5.js canvas on top for text display. The grid group rotates slowly for visual appeal, and all Three.js meshes are disposed of properly when no longer needed to avoid memory leaks.

🎓 Concepts You'll Learn

Game state managementCollision detection with bounding boxesProcedural generation with noiseCanvas layering and z-indexCoordinate system mappingObject-oriented design with classesThree.js mesh and material systemMemory management with dispose()Keyboard and mouse input handlingAudio synthesis with p5.sound

📝 Code Breakdown

setup()

setup() is called once when the sketch starts. It initializes both rendering systems (p5.js and Three.js), creates all permanent objects (player, lights, grid), sets up audio, and starts the animation loop. Understanding how both libraries initialize together is key to building hybrid sketches.

🔬 These three numbers define the grid's structure: gridSize is how many cubes in each direction (20x20), cubeSize is how big each cube is, and spacing is the distance between cube centers. If you change gridSize to 10, what happens to the arena size and complexity?

  const gridSize = 20;
  const cubeSize = 3;
  const spacing = 5;

🔬 The noise values at x*0.1 and z*0.1 control how the height varies - smaller multipliers create smoother, larger features. What happens if you change 0.1 to 0.05, making the noise output change more slowly across the grid?

  for (let x = 0; x < gridSize; x++) {
    for (let z = 0; z < gridSize; z++) {
      const height = (noise(x * 0.1, z * 0.1) * 20) + threeGroundY;
function setup() {
  // 1. p5.js Setup (for UI and logic)
  const p5Canvas = createCanvas(windowWidth, windowHeight);
  p5Canvas.id('p5Canvas'); // Assign an ID for CSS styling
  // noCanvas(); // DO NOT call noCanvas() here, p5.js canvas is needed for UI

  // Initialize player
  player = new Player(width / 2, height - 50, 60, 20); // p5.js coords

  // Initialize sounds
  // Shoot sound: short, high-pitched sine wave
  shootSound = new p5.Oscillator();
  shootSound.setType('sine');
  shootSound.freq(800);
  shootSound.amp(0); // Start with 0 volume
  shootSound.start();

  // Hit sound: very short, slightly lower-pitched sine wave
  hitSound = new p5.Oscillator();
  hitSound.setType('sine');
  hitSound.freq(400);
  hitSound.amp(0);
  hitSound.start();

  // Explosion sound: short, low-pitched noise
  explosionSound = new p5.Noise('white');
  explosionSound.amp(0);
  explosionSound.start();

  // Game Over sound: sustained low-pitched square wave
  gameOverSound = new p5.Oscillator();
  gameOverSound.setType('square');
  gameOverSound.freq(100);
  gameOverSound.amp(0);
  gameOverSound.start();

  // Start audio context on user gesture (important for p5.sound)
  userStartAudio();


  // 2. three.js Setup (for 3D rendering)
  scene = new THREE.Scene();
  scene.background = new THREE.Color(0x1a1a1a); // Dark gray background

  // Camera Setup
  camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
  camera.position.set(0, 80, 70); // Position the camera to look down at the game arena
  camera.lookAt(0, threeGroundY, threeEnemySpawnZ / 2); // Look towards the center of the arena

  // Renderer Setup
  renderer = new THREE.WebGLRenderer({ antialias: true });
  renderer.setSize(window.innerWidth, window.innerHeight);
  renderer.domElement.id = 'threeCanvas'; // Give it an ID to differentiate from p5.js canvas
  document.body.appendChild(renderer.domElement); // Add the three.js canvas to the DOM
  // Move p5.js canvas on top
  document.body.appendChild(p5Canvas.elt);

  // Lighting
  const ambientLight = new THREE.AmbientLight(0x404040); // Soft white light
  scene.add(ambientLight);

  const directionalLight = new THREE.DirectionalLight(0xffffff, 0.8); // White light, medium intensity
  directionalLight.position.set(100, 100, 100); // Position it to cast light from the top-right
  scene.add(directionalLight);

  // Create Procedural Geometry (veck.io style grid)
  gridGroup = new THREE.Group(); // Use a group to hold all the cubes
  const gridSize = 20;
  const cubeSize = 3;
  const spacing = 5;

  const colors = [
    new THREE.Color(0x333333), new THREE.Color(0x555555),
    new THREE.Color(0xaaaaaa), new THREE.Color(0xdddddd),
    new THREE.Color(0xffffff)
  ];

  for (let x = 0; x < gridSize; x++) {
    for (let z = 0; z < gridSize; z++) {
      const height = (noise(x * 0.1, z * 0.1) * 20) + threeGroundY; // Use p5.js noise for organic height, ensure base is at threeGroundY
      const geometry = new THREE.BoxGeometry(cubeSize, height, cubeSize);
      const colorIndex = floor(noise(x * 0.2, z * 0.2) * colors.length);
      const material = new THREE.MeshStandardMaterial({
        color: colors[colorIndex], metalness: 0.1, roughness: 0.6, emissive: new THREE.Color(0x000000)
      });
      const cube = new THREE.Mesh(geometry, material);
      cube.position.x = (x - gridSize / 2) * spacing;
      cube.position.y = height / 2; // Position so base is on the ground plane
      cube.position.z = (z - gridSize / 2) * spacing;
      gridGroup.add(cube);
    }
  }
  scene.add(gridGroup);

  // Camera Controls (initially enabled for start screen)
  controls = new THREE.OrbitControls(camera, renderer.domElement);
  controls.enableDamping = true;
  controls.dampingFactor = 0.05;
  controls.screenSpacePanning = false;
  controls.minDistance = 10;
  controls.maxDistance = 200;
  controls.maxPolarAngle = Math.PI / 2;

  // Start the animation loop (three.js loop)
  animate();
}
Line-by-line explanation (27 lines)

🔧 Subcomponents:

calculation p5.js Canvas Setup const p5Canvas = createCanvas(windowWidth, windowHeight);

Creates a p5.js canvas matching the window size for UI and text overlay

calculation Player Initialization player = new Player(width / 2, height - 50, 60, 20);

Creates the Player object at the bottom-center of the screen with size 60x20

calculation Sound Setup shootSound = new p5.Oscillator();

Initializes four p5.sound oscillators and noise generators for game audio

calculation Three.js Scene Creation scene = new THREE.Scene();

Creates a Three.js scene that will hold all 3D objects

calculation Camera Positioning camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);

Creates a camera with 75° field of view positioned to look down at the game arena

for-loop Procedural Grid Generation for (let x = 0; x < gridSize; x++) { for (let z = 0; z < gridSize; z++) {

Creates a 20x20 grid of cubes with varying heights using p5.js noise for organic appearance

const p5Canvas = createCanvas(windowWidth, windowHeight);
Creates a p5.js canvas that fills the entire window; stored in p5Canvas so we can manipulate it later
p5Canvas.id('p5Canvas');
Assigns the HTML id 'p5Canvas' to the p5.js canvas so CSS styling can target it
player = new Player(width / 2, height - 50, 60, 20);
Instantiates a new Player object centered horizontally and positioned 50 pixels from the bottom of the screen
shootSound = new p5.Oscillator();
Creates a new oscillator that will produce the shooting sound when amplitude is increased
shootSound.setType('sine');
Sets the oscillator to produce a smooth sine wave tone
shootSound.freq(800);
Sets the oscillator's frequency to 800 Hz, a high-pitched tone for the shoot sound
shootSound.amp(0);
Starts with amplitude 0 so no sound plays until explicitly triggered later
shootSound.start();
Begins the oscillator running continuously; its volume is controlled by amp() calls later
userStartAudio();
Activates the Web Audio Context in response to user interaction, required by browsers to play sound
scene = new THREE.Scene();
Creates an empty Three.js scene container that will hold all 3D objects
scene.background = new THREE.Color(0x1a1a1a);
Sets the scene's background color to dark gray (0x1a1a1a)
camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
Creates a perspective camera with 75° field of view, aspect ratio matching the window, and viewing range from 0.1 to 1000 units
camera.position.set(0, 80, 70);
Positions the camera 80 units up and 70 units back so it looks down at the arena from above
renderer = new THREE.WebGLRenderer({ antialias: true });
Creates a Three.js renderer using WebGL with antialiasing enabled for smooth edges
renderer.setSize(window.innerWidth, window.innerHeight);
Sets the renderer output to match the full window dimensions
document.body.appendChild(renderer.domElement);
Adds the Three.js canvas to the HTML page as a background element
document.body.appendChild(p5Canvas.elt);
Adds the p5.js canvas on top of the Three.js canvas so UI text appears above the 3D scene
const ambientLight = new THREE.AmbientLight(0x404040);
Creates soft background lighting that illuminates all objects evenly from all directions
const directionalLight = new THREE.DirectionalLight(0xffffff, 0.8);
Creates a bright directional light source (like sunlight) with 80% intensity
directionalLight.position.set(100, 100, 100);
Positions the light source at (100, 100, 100) so it casts shadows from the top-right direction
const height = (noise(x * 0.1, z * 0.1) * 20) + threeGroundY;
Uses p5.js noise() to generate a random height for each cube, scaled by 20 units, ensuring all cubes sit on the same ground level
const colorIndex = floor(noise(x * 0.2, z * 0.2) * colors.length);
Uses different noise parameters to pick a color from the colors array, creating visual variation across the grid
cube.position.y = height / 2;
Positions each cube so its bottom rests on the ground plane (Three.js positions objects from their center)
gridGroup.add(cube);
Adds the cube to the gridGroup so the entire grid can be rotated as one object later
controls = new THREE.OrbitControls(camera, renderer.domElement);
Creates camera controls that let the user orbit around the scene by dragging the mouse
controls.enableDamping = true;
Enables inertial damping so the camera continues moving smoothly after the user stops dragging
animate();
Starts the Three.js animation loop that continuously renders the scene

draw()

draw() runs 60 times per second in p5.js. Here it acts as a state router, delegating to different functions based on gameState. This pattern keeps your main loop simple and each screen's logic organized separately.

🔬 These three conditions handle all possible game states. What would happen if you added a fourth state called 'gameState === "paused"' that shows a pause screen? Where would you insert this new condition?

  if (gameState === 'start') {
    drawStartScreen();
  } else if (gameState === 'playing') {
    playGame();
  } else if (gameState === 'gameOver') {
    drawGameOverScreen();
  }
function draw() {
  background(20, 20, 40); // Dark background

  if (gameState === 'start') {
    drawStartScreen();
  } else if (gameState === 'playing') {
    playGame();
  } else if (gameState === 'gameOver') {
    drawGameOverScreen();
  }
}
Line-by-line explanation (7 lines)

🔧 Subcomponents:

switch-case Game State Router if (gameState === 'start') { drawStartScreen(); } else if (gameState === 'playing') { playGame(); } else if (gameState === 'gameOver') { drawGameOverScreen(); }

Routes execution to different functions based on the current game state

background(20, 20, 40);
Clears the p5.js canvas with dark blue color every frame
if (gameState === 'start') {
Checks if the game is in the start state
drawStartScreen();
If true, draws the start screen with instructions
} else if (gameState === 'playing') {
Otherwise checks if the game is actively being played
playGame();
If true, runs all the game logic: player control, shooting, enemy spawning, collision detection
} else if (gameState === 'gameOver') {
Otherwise checks if the game has ended
drawGameOverScreen();
If true, displays the game over screen with final score

drawStartScreen()

drawStartScreen() displays the title, controls, and a prompt to start. It runs every frame when gameState is 'start'. The clear() at the beginning ensures the screen doesn't show overlapping text from previous frames.

🔬 The textSize(48) makes the title large, and fill(255) makes it white. What happens if you add fill(255, 0, 0) before the text() to make it red? How would the visual contrast change?

  textSize(48);
  fill(255);
  text("VECK.IO 3D SHOOTER", width / 2, height / 2 - 50);
function drawStartScreen() {
  clear(); // Clear p5.js canvas
  textAlign(CENTER, CENTER);
  textSize(48);
  fill(255);
  text("VECK.IO 3D SHOOTER", width / 2, height / 2 - 50);
  textSize(24);
  text("Use LEFT/RIGHT arrows to move, SPACE to shoot", width / 2, height / 2 + 20);
  textSize(32);
  text("Click to Start", width / 2, height / 2 + 100);
}
Line-by-line explanation (9 lines)

🔧 Subcomponents:

calculation Title Display text("VECK.IO 3D SHOOTER", width / 2, height / 2 - 50);

Displays the game title centered near the top of the screen

calculation Control Instructions text("Use LEFT/RIGHT arrows to move, SPACE to shoot", width / 2, height / 2 + 20);

Shows the player how to control the game

calculation Start Prompt text("Click to Start", width / 2, height / 2 + 100);

Invites the player to click to begin the game

clear();
Clears the p5.js canvas completely, removing any previous content
textAlign(CENTER, CENTER);
Sets all text to be centered both horizontally and vertically around the coordinates you provide
textSize(48);
Sets the font size to 48 pixels for the title
fill(255);
Sets the text color to white (255, 255, 255)
text("VECK.IO 3D SHOOTER", width / 2, height / 2 - 50);
Draws the title text at the horizontal center and 50 pixels above the vertical center
textSize(24);
Changes the font size to 24 pixels for the instructions
text("Use LEFT/RIGHT arrows to move, SPACE to shoot", width / 2, height / 2 + 20);
Displays instructions centered horizontally and 20 pixels below the vertical center
textSize(32);
Changes the font size to 32 pixels for the start prompt
text("Click to Start", width / 2, height / 2 + 100);
Displays the start prompt centered at 100 pixels below the vertical center

playGame()

playGame() is the core game loop. It updates all game entities, checks all collisions, spawns enemies at increasing frequency, and displays the UI. Notice it iterates through arrays in reverse (from the end backward) so removing items during iteration doesn't cause bugs. Also notice that Three.js meshes are disposed when objects die to prevent memory leaks.

function playGame() {
  clear(); // Clear p5.js canvas for text UI
  // Update and display player (updates its three.js mesh)
  player.update();
  // player.display(); // Removed, handled by update()

  // Update and display bullets
  for (let i = bullets.length - 1; i >= 0; i--) {
    bullets[i].update(); // Updates its three.js mesh

    // Check if bullet is off-screen (three.js coords)
    if (bullets[i].offScreen()) {
      bullets[i].dispose(); // Dispose of three.js mesh
      bullets.splice(i, 1);
      continue;
    }

    // Check for bullet-enemy collisions
    for (let j = enemies.length - 1; j >= 0; j--) {
      if (bullets[i].hits(enemies[j])) {
        enemies[j].takeDamage(bulletDamage);
        hitSound.amp(0.5, 0.05);
        hitSound.amp(0, 0.2);

        bullets[i].dispose(); // Dispose of three.js mesh
        bullets.splice(i, 1);
        score += 10;
        break;
      }
    }
  }

  // Update and display enemies
  if (millis() - lastEnemySpawnTime > enemySpawnRate) {
    // Spawn enemy at threeEnemySpawnZ
    enemies.push(new Enemy(random(width), 0, 40, 40)); // p5.js coords
    lastEnemySpawnTime = millis();
    enemySpawnRate = max(100, enemySpawnRate - 5);
    enemySpeed = min(5, enemySpeed + 0.01);
  }

  for (let i = enemies.length - 1; i >= 0; i--) {
    enemies[i].update(); // Updates its three.js mesh

    // Check if enemy is off-screen (three.js coords)
    if (enemies[i].offScreen()) {
      enemies[i].dispose(); // Dispose of three.js mesh
      enemies.splice(i, 1);
      playerHealth -= 10;
      if (playerHealth <= 0) {
        gameOver();
        break;
      }
      continue;
    }

    // Check for player-enemy collisions
    if (enemies[i].hits(player)) {
      playerHealth -= 20;
      explosionSound.amp(0.8, 0.05);
      explosionSound.amp(0, 0.5);

      enemies[i].dispose(); // Dispose of three.js mesh
      enemies.splice(i, 1);
      if (playerHealth <= 0) {
        gameOver();
        break;
      }
    }
  }

  // Display score and health (p5.js UI)
  textAlign(LEFT, TOP);
  textSize(24);
  fill(255);
  text("Score: " + score, 10, 10);
  text("Health: " + playerHealth, 10, 40);
}
Line-by-line explanation (27 lines)

🔧 Subcomponents:

calculation Player Update player.update();

Updates the player's position based on keyboard input and syncs the Three.js mesh

for-loop Bullet Update Loop for (let i = bullets.length - 1; i >= 0; i--) {

Iterates through all bullets in reverse to safely remove them while iterating

conditional Bullet-Enemy Collision if (bullets[i].hits(enemies[j])) {

Detects when a bullet intersects with an enemy and applies damage

conditional Enemy Spawning if (millis() - lastEnemySpawnTime > enemySpawnRate) {

Checks if enough time has passed to spawn a new enemy

for-loop Enemy Update Loop for (let i = enemies.length - 1; i >= 0; i--) {

Iterates through all enemies to update their positions and check collisions

conditional Player-Enemy Collision if (enemies[i].hits(player)) {

Detects when an enemy reaches the player and deals damage

clear();
Clears the p5.js canvas each frame so the score and health display don't trail
player.update();
Calls the player's update method to move it based on arrow key input and sync its Three.js mesh
for (let i = bullets.length - 1; i >= 0; i--) {
Loops through bullets in reverse order (from last to first) so removal during iteration doesn't skip bullets
bullets[i].update();
Updates each bullet's position and Three.js mesh
if (bullets[i].offScreen()) {
Checks if the bullet has traveled past the back of the arena
bullets[i].dispose();
Removes the bullet's Three.js mesh from the scene and frees its memory
bullets.splice(i, 1);
Removes the bullet from the bullets array
for (let j = enemies.length - 1; j >= 0; j--) {
Nested loop checking this bullet against all enemies
if (bullets[i].hits(enemies[j])) {
Calls the bullet's hits() method to check if its bounding box overlaps the enemy's
enemies[j].takeDamage(bulletDamage);
Deals damage to the enemy (10 HP per hit)
hitSound.amp(0.5, 0.05);
Plays the hit sound by ramping its amplitude to 0.5 over 50 milliseconds
hitSound.amp(0, 0.2);
Fades the hit sound to silence over 200 milliseconds
score += 10;
Increases the score by 10 points when a bullet hits an enemy
if (millis() - lastEnemySpawnTime > enemySpawnRate) {
Checks if the time since the last enemy spawn exceeds the spawn rate interval
enemies.push(new Enemy(random(width), 0, 40, 40));
Creates a new enemy at a random horizontal position
lastEnemySpawnTime = millis();
Records the current time so the next spawn waits the correct interval
enemySpawnRate = max(100, enemySpawnRate - 5);
Decreases spawn rate by 5 ms per spawn (enemies come faster), but never below 100 ms
enemySpeed = min(5, enemySpeed + 0.01);
Increases enemy speed by 0.01 each spawn, making the game progressively harder, capped at 5 pixels/frame
for (let i = enemies.length - 1; i >= 0; i--) {
Loops through all enemies in reverse for safe removal
enemies[i].update();
Updates each enemy's position and Three.js mesh
if (enemies[i].offScreen()) {
Checks if the enemy has reached the player's position
playerHealth -= 10;
Deducts 10 health points when an enemy slips past the player
if (enemies[i].hits(player)) {
Checks if the enemy's position overlaps the player's position
playerHealth -= 20;
Deducts 20 health points when the player collides with an enemy
explosionSound.amp(0.8, 0.05);
Plays the explosion sound by ramping amplitude to 0.8 over 50 milliseconds
text("Score: " + score, 10, 10);
Displays the current score in the top-left corner
text("Health: " + playerHealth, 10, 40);
Displays the current health below the score

drawGameOverScreen()

drawGameOverScreen() displays the game over message, the final score, and a prompt to restart. It runs every frame when gameState is 'gameOver'. The red color and larger text emphasize the failure state.

🔬 The text color is set to red (255, 50, 50). What happens if you swap the textSize(64) and add fill(255, 255, 0) to make the text yellow instead? How does yellow vs red change the player's emotional response?

  textSize(64);
  fill(255, 50, 50);
  text("GAME OVER!", width / 2, height / 2 - 80);
function drawGameOverScreen() {
  clear(); // Clear p5.js canvas
  textAlign(CENTER, CENTER);
  textSize(64);
  fill(255, 50, 50);
  text("GAME OVER!", width / 2, height / 2 - 80);
  textSize(32);
  text("Final Score: " + score, width / 2, height / 2);
  textSize(24);
  text("Click to Restart", width / 2, height / 2 + 60);
}
Line-by-line explanation (9 lines)

🔧 Subcomponents:

calculation Game Over Title text("GAME OVER!", width / 2, height / 2 - 80);

Displays the GAME OVER message in large red text

calculation Final Score Display text("Final Score: " + score, width / 2, height / 2);

Shows the player's final score

calculation Restart Prompt text("Click to Restart", width / 2, height / 2 + 60);

Invites the player to click to restart the game

clear();
Clears the p5.js canvas
textAlign(CENTER, CENTER);
Centers all text horizontally and vertically
textSize(64);
Sets large font size for the GAME OVER message
fill(255, 50, 50);
Sets the text color to red (255 red, 50 green, 50 blue)
text("GAME OVER!", width / 2, height / 2 - 80);
Displays GAME OVER in large red text, centered and positioned above the middle
textSize(32);
Reduces font size for the final score
text("Final Score: " + score, width / 2, height / 2);
Displays the final score value in the center of the screen
textSize(24);
Further reduces font size for the restart prompt
text("Click to Restart", width / 2, height / 2 + 60);
Displays the restart prompt below the score

gameOver()

gameOver() is called when playerHealth reaches 0. It immediately changes the game state to 'gameOver' (which switches the display to the game over screen), fades out all current sounds, and plays a low-pitched game over sound. This function is simple but critical—it's the transition point from active play to the end state.

function gameOver() {
  gameState = 'gameOver';
  shootSound.amp(0, 0.1);
  hitSound.amp(0, 0.1);
  explosionSound.amp(0, 0.1);
  gameOverSound.amp(0.5, 0.1);
}
Line-by-line explanation (5 lines)

🔧 Subcomponents:

calculation State Transition gameState = 'gameOver';

Changes the game state to trigger the game over screen

calculation Audio Fadeout shootSound.amp(0, 0.1);

Fades out all active sound effects and plays the game over sound

gameState = 'gameOver';
Sets the game state to 'gameOver' so the draw() loop will display the game over screen next frame
shootSound.amp(0, 0.1);
Fades the shoot sound to silence over 100 milliseconds
hitSound.amp(0, 0.1);
Fades the hit sound to silence over 100 milliseconds
explosionSound.amp(0, 0.1);
Fades the explosion sound to silence over 100 milliseconds
gameOverSound.amp(0.5, 0.1);
Plays the game over sound by ramping its amplitude to 0.5 over 100 milliseconds

restartGame()

restartGame() is called when the player clicks on the start screen or game over screen. It resets all game variables to their initial values, disposes of all Three.js meshes from the previous game to prevent memory leaks, and changes the game state to 'playing' to begin a fresh game. This is critical for ensuring the game can be replayed without accumulating old objects in memory.

function restartGame() {
  gameState = 'playing';
  score = 0;
  playerHealth = 100;

  // Dispose of all existing three.js meshes for bullets and enemies
  for (let b of bullets) b.dispose();
  for (let e of enemies) e.dispose();

  bullets = [];
  enemies = [];
  lastShotTime = 0;
  lastEnemySpawnTime = 0;
  enemySpawnRate = 1000;
  enemySpeed = 1.5;
  gameOverSound.amp(0, 0.1);
}
Line-by-line explanation (12 lines)

🔧 Subcomponents:

calculation State Reset gameState = 'playing';

Changes the game state back to 'playing' to resume gameplay

calculation Variable Reset score = 0; playerHealth = 100;

Resets score and health to starting values

for-loop Mesh Cleanup for (let b of bullets) b.dispose(); for (let e of enemies) e.dispose();

Removes all Three.js meshes from the previous game to free memory

calculation Array Reset bullets = []; enemies = [];

Clears the bullets and enemies arrays

gameState = 'playing';
Changes the game state to 'playing' so the next frame resumes active gameplay
score = 0;
Resets the score to 0
playerHealth = 100;
Restores player health to full (100 HP)
for (let b of bullets) b.dispose();
Loops through all remaining bullets and calls their dispose() method to remove their Three.js meshes
for (let e of enemies) e.dispose();
Loops through all remaining enemies and calls their dispose() method to remove their Three.js meshes
bullets = [];
Empties the bullets array by replacing it with a new empty array
enemies = [];
Empties the enemies array by replacing it with a new empty array
lastShotTime = 0;
Resets the shot timer so the player can shoot immediately on the next game
lastEnemySpawnTime = 0;
Resets the enemy spawn timer so enemies spawn immediately
enemySpawnRate = 1000;
Resets enemy spawn rate to the starting value (1000 ms between spawns)
enemySpeed = 1.5;
Resets enemy speed to the starting value (1.5 pixels/frame)
gameOverSound.amp(0, 0.1);
Fades the game over sound to silence

class Player

The Player class encapsulates all player-related logic: storing position and size, creating and updating a Three.js mesh, handling keyboard input, shooting bullets, and detecting collisions. The update() method is called every frame to respond to input and sync the Three.js mesh position. The getBounds() method returns a bounding box used for collision detection against enemies.

🔬 The constrain() line keeps the player on-screen by limiting x between w/2 and width - w/2. What happens if you remove the constrain line? Can the player move off the left or right edge of the screen?

  update() {
    if (keyIsDown(LEFT_ARROW)) {
      this.x -= playerSpeed;
    }
    if (keyIsDown(RIGHT_ARROW)) {
      this.x += playerSpeed;
    }
    this.x = constrain(this.x, this.w / 2, width - this.w / 2);
class Player {
  constructor(x, y, w, h) {
    this.x = x; // p5.js x
    this.y = y; // p5.js y (will be fixed for player, but kept for consistency)
    this.w = w; // p5.js width
    this.h = h; // p5.js height

    // three.js mesh for the player
    const playerGeometry = new THREE.BoxGeometry(this.w / 10, this.h / 10, this.w / 10); // Scale down p5.js dimensions for three.js units
    const playerMaterial = new THREE.MeshStandardMaterial({
      color: 0x0096ff, metalness: 0.3, roughness: 0.5, emissive: new THREE.Color(0x000000)
    });
    this.mesh = new THREE.Mesh(playerGeometry, playerMaterial);

    // Initial three.js position
    this.mesh.position.x = mapP5ToThreeX(this.x);
    this.mesh.position.y = threeGroundY; // Place it on the ground
    this.mesh.position.z = threePlayerZ; // Fixed Z position

    scene.add(this.mesh);
  }

  update() {
    if (keyIsDown(LEFT_ARROW)) {
      this.x -= playerSpeed;
    }
    if (keyIsDown(RIGHT_ARROW)) {
      this.x += playerSpeed;
    }
    this.x = constrain(this.x, this.w / 2, width - this.w / 2);

    // Update three.js mesh position
    this.mesh.position.x = mapP5ToThreeX(this.x);
  }

  shoot() {
    if (millis() - lastShotTime > playerFireRate) {
      bullets.push(new Bullet(this.x, this.y - this.h / 2)); // p5.js coords
      lastShotTime = millis();
      shootSound.amp(0.2, 0.05);
      shootSound.amp(0, 0.2);
    }
  }

  // Get three.js bounding box for collision detection
  getBounds() {
    this.mesh.geometry.computeBoundingBox();
    this.mesh.updateMatrixWorld();
    return this.mesh.geometry.boundingBox.clone().applyMatrix4(this.mesh.matrixWorld);
  }

  dispose() {
    scene.remove(this.mesh);
    this.mesh.geometry.dispose();
    this.mesh.material.dispose();
  }
}
Line-by-line explanation (30 lines)

🔧 Subcomponents:

calculation Constructor constructor(x, y, w, h) {

Initializes the Player with position and size, and creates a Three.js mesh

calculation Geometry Creation const playerGeometry = new THREE.BoxGeometry(this.w / 10, this.h / 10, this.w / 10);

Creates a cube geometry for the player's 3D model

conditional Input Handling if (keyIsDown(LEFT_ARROW)) {

Checks for arrow key input to move the player left or right

calculation Boundary Constraint this.x = constrain(this.x, this.w / 2, width - this.w / 2);

Keeps the player from moving off the sides of the screen

constructor(x, y, w, h) {
Defines the constructor method that receives initial x, y position and w, h dimensions
this.x = x;
Stores the player's x position in p5.js coordinates (0 to width)
this.w = w;
Stores the player's width (60 pixels)
this.h = h;
Stores the player's height (20 pixels)
const playerGeometry = new THREE.BoxGeometry(this.w / 10, this.h / 10, this.w / 10);
Creates a Three.js cube geometry by scaling down the p5.js dimensions by 10x (so 60 becomes 6, 20 becomes 2, etc.)
const playerMaterial = new THREE.MeshStandardMaterial({
Creates a material with realistic lighting properties
color: 0x0096ff,
Sets the color to bright blue (0x0096ff)
metalness: 0.3, roughness: 0.5,
Gives the material a slightly metallic, somewhat rough appearance
this.mesh = new THREE.Mesh(playerGeometry, playerMaterial);
Combines the geometry and material into a mesh object
this.mesh.position.x = mapP5ToThreeX(this.x);
Converts the p5.js x coordinate (0 to width) to Three.js coordinates (-gameWidth/2 to gameWidth/2)
this.mesh.position.y = threeGroundY;
Places the player on the ground plane in Three.js space
this.mesh.position.z = threePlayerZ;
Places the player at a fixed Z position (depth) in the arena
scene.add(this.mesh);
Adds the player's mesh to the Three.js scene so it renders
if (keyIsDown(LEFT_ARROW)) {
Checks if the LEFT_ARROW key is currently pressed
this.x -= playerSpeed;
If left arrow is pressed, decreases x by playerSpeed (5), moving the player left
if (keyIsDown(RIGHT_ARROW)) {
Checks if the RIGHT_ARROW key is currently pressed
this.x += playerSpeed;
If right arrow is pressed, increases x by playerSpeed (5), moving the player right
this.x = constrain(this.x, this.w / 2, width - this.w / 2);
Clamps the player's x position between w/2 (30) and width - w/2, preventing it from moving off-screen
this.mesh.position.x = mapP5ToThreeX(this.x);
Updates the Three.js mesh's x position to match the new p5.js coordinate
if (millis() - lastShotTime > playerFireRate) {
Checks if enough time has passed since the last shot (200 ms) to allow a new shot
bullets.push(new Bullet(this.x, this.y - this.h / 2));
Creates a new Bullet at the player's x position and above the player's center (y - 10)
lastShotTime = millis();
Records the current time so the next shot must wait playerFireRate milliseconds
shootSound.amp(0.2, 0.05);
Plays the shoot sound by increasing its amplitude to 0.2 over 50 milliseconds
shootSound.amp(0, 0.2);
Fades the shoot sound back to 0 over 200 milliseconds, creating a brief beep
this.mesh.geometry.computeBoundingBox();
Calculates the bounding box around the geometry
this.mesh.updateMatrixWorld();
Updates the mesh's world transformation matrix to reflect its current position and rotation
return this.mesh.geometry.boundingBox.clone().applyMatrix4(this.mesh.matrixWorld);
Returns a copy of the bounding box transformed to world coordinates, used for collision detection
scene.remove(this.mesh);
Removes the mesh from the Three.js scene so it won't render
this.mesh.geometry.dispose();
Frees the GPU memory used by the geometry
this.mesh.material.dispose();
Frees the GPU memory used by the material

class Bullet

The Bullet class represents a projectile fired by the player. Each bullet is created with a position and moves upward every frame. The offScreen() method checks if it has traveled too far and should be removed. The hits() method checks collision with enemies. Like the Player class, it has a Three.js mesh that must be disposed of to prevent memory leaks.

🔬 The bullet moves by decreasing this.y by bulletSpeed each frame. What happens if you change -= to += so bullets move downward instead of upward?

  update() {
    this.y -= bulletSpeed; // Update p5.js y

    // Update three.js mesh position
    this.mesh.position.z = mapP5ToThreeZ(this.y);
  }
class Bullet {
  constructor(x, y) {
    this.x = x; // p5.js x
    this.y = y; // p5.js y (changes)
    this.r = 5; // Radius (p5.js)

    // three.js mesh for the bullet (e.g., a small cylinder for a laser)
    const bulletGeometry = new THREE.CylinderGeometry(this.r / 5, this.r / 5, 2, 8); // Scale down for three.js units
    const bulletMaterial = new THREE.MeshStandardMaterial({
      color: 0xffd700, emissive: 0xffd700, emissiveIntensity: 1.5
    });
    this.mesh = new THREE.Mesh(bulletGeometry, bulletMaterial);

    // Initial three.js position
    this.mesh.position.x = mapP5ToThreeX(this.x);
    this.mesh.position.y = threeGroundY;
    this.mesh.position.z = mapP5ToThreeZ(this.y);

    scene.add(this.mesh);
  }

  update() {
    this.y -= bulletSpeed; // Update p5.js y

    // Update three.js mesh position
    this.mesh.position.z = mapP5ToThreeZ(this.y);
  }

  offScreen() {
    // Check three.js Z position (front of arena)
    return this.mesh.position.z > threeGameHeight / 2 + 10; // A little past the back of the arena
  }

  hits(enemy) {
    if (enemy.health <= 0) return false;
    return this.getBounds().intersectsBox(enemy.getBounds());
  }

  getBounds() {
    this.mesh.geometry.computeBoundingBox();
    this.mesh.updateMatrixWorld();
    return this.mesh.geometry.boundingBox.clone().applyMatrix4(this.mesh.matrixWorld);
  }

  dispose() {
    scene.remove(this.mesh);
    this.mesh.geometry.dispose();
    this.mesh.material.dispose();
  }
}
Line-by-line explanation (17 lines)

🔧 Subcomponents:

calculation Bullet Geometry const bulletGeometry = new THREE.CylinderGeometry(this.r / 5, this.r / 5, 2, 8);

Creates a small cylinder shape for the laser bullet

calculation Bullet Movement this.y -= bulletSpeed;

Moves the bullet upward each frame by decreasing its y position

constructor(x, y) {
Creates a new bullet at position x, y
this.x = x;
Stores the bullet's x position in p5.js coordinates
this.y = y;
Stores the bullet's y position which will decrease each frame as it moves up
this.r = 5;
Sets the bullet's radius to 5 pixels (for reference only; the actual Three.js shape is much smaller)
const bulletGeometry = new THREE.CylinderGeometry(this.r / 5, this.r / 5, 2, 8);
Creates a cylinder geometry: top radius 1 (5/5), bottom radius 1, height 2, with 8 sides for smoothness
const bulletMaterial = new THREE.MeshStandardMaterial({
Creates a material for the bullet with emissive properties (glowing)
color: 0xffd700, emissive: 0xffd700, emissiveIntensity: 1.5
Sets the bullet to gold color (0xffd700) and makes it glow with 1.5x intensity
this.mesh = new THREE.Mesh(bulletGeometry, bulletMaterial);
Combines the geometry and material into a mesh
this.mesh.position.x = mapP5ToThreeX(this.x);
Converts the bullet's p5.js x coordinate to Three.js space
this.mesh.position.y = threeGroundY;
Places the bullet slightly above the ground in Three.js space
this.mesh.position.z = mapP5ToThreeZ(this.y);
Converts the bullet's p5.js y coordinate to Three.js z coordinate
scene.add(this.mesh);
Adds the bullet mesh to the scene so it renders
this.y -= bulletSpeed;
Decreases the bullet's y position by bulletSpeed (10), moving it up the screen each frame
this.mesh.position.z = mapP5ToThreeZ(this.y);
Updates the Three.js mesh position to reflect the new y coordinate
return this.mesh.position.z > threeGameHeight / 2 + 10;
Returns true if the bullet's Z position has moved past the back of the arena, indicating it should be removed
if (enemy.health <= 0) return false;
Returns false immediately if the enemy is already dead, avoiding unnecessary collision checks
return this.getBounds().intersectsBox(enemy.getBounds());
Checks if this bullet's bounding box overlaps with the enemy's bounding box

class Enemy

The Enemy class represents hostile entities that spawn at the back of the arena and move toward the player. Each enemy has health that decreases when hit by bullets. When health reaches 0, the enemy becomes invisible immediately (for visual feedback) but the mesh isn't disposed for 500 ms to let the explosion sound finish playing. The isAlive flag prevents double-processing after death. Like other entities, enemies use Three.js meshes that must be properly disposed.

class Enemy {
  constructor(x, y, w, h) {
    this.x = x; // p5.js x
    this.y = y; // p5.js y (changes)
    this.w = w; // p5.js width
    this.h = h; // p5.js height
    this.health = enemyHealth;
    this.isAlive = true;

    // three.js mesh for the enemy
    const enemyGeometry = new THREE.BoxGeometry(this.w / 10, this.h / 10, this.w / 10); // Scale down
    const enemyMaterial = new THREE.MeshStandardMaterial({
      color: 0xff0000, metalness: 0.1, roughness: 0.7, emissive: new THREE.Color(0x000000)
    });
    this.mesh = new THREE.Mesh(enemyGeometry, enemyMaterial);

    // Initial three.js position (spawn at front of arena)
    this.mesh.position.x = mapP5ToThreeX(this.x);
    this.mesh.position.y = threeGroundY;
    this.mesh.position.z = threeEnemySpawnZ;

    scene.add(this.mesh);
  }

  update() {
    if (this.isAlive) {
      this.y += enemySpeed; // Update p5.js y
      // Update three.js mesh position
      this.mesh.position.z = mapP5ToThreeZ(this.y);
    }
  }

  offScreen() {
    // Check three.js Z position (past the player's Z)
    return this.mesh.position.z < threePlayerZ - 10; // A little past the player
  }

  takeDamage(amount) {
    this.health -= amount;
    if (this.health <= 0 && this.isAlive) {
      this.isAlive = false;
      explosionSound.amp(0.8, 0.05);
      explosionSound.amp(0, 0.5);
      // Make enemy invisible immediately
      this.mesh.visible = false;
      // Schedule disposal after a short delay to allow sound to play
      setTimeout(() => this.dispose(), 500);
    }
  }

  hits(other) {
    if (!this.isAlive) return false;
    return this.getBounds().intersectsBox(other.getBounds());
  }

  getBounds() {
    this.mesh.geometry.computeBoundingBox();
    this.mesh.updateMatrixWorld();
    return this.mesh.geometry.boundingBox.clone().applyMatrix4(this.mesh.matrixWorld);
  }

  dispose() {
    scene.remove(this.mesh);
    this.mesh.geometry.dispose();
    this.mesh.material.dispose();
  }
}
Line-by-line explanation (25 lines)

🔧 Subcomponents:

calculation Enemy Initialization this.health = enemyHealth;

Sets the enemy's health to the starting value (30 HP)

calculation Enemy Movement this.y += enemySpeed;

Moves the enemy downward each frame toward the player

conditional Enemy Death if (this.health <= 0 && this.isAlive) {

Handles what happens when an enemy's health reaches 0

constructor(x, y, w, h) {
Creates a new enemy at random horizontal position
this.x = x;
Stores the enemy's x position (randomized at spawn)
this.y = y;
Stores the enemy's y position (starts at 0, increases as it moves toward player)
this.health = enemyHealth;
Sets the enemy's health to the global enemyHealth value (30 HP)
this.isAlive = true;
Flags the enemy as alive; used to prevent double-collision checks after death
const enemyGeometry = new THREE.BoxGeometry(this.w / 10, this.h / 10, this.w / 10);
Creates a cube geometry: all dimensions are 40/10 = 4 units
const enemyMaterial = new THREE.MeshStandardMaterial({
Creates a material for the enemy
color: 0xff0000,
Sets the enemy's color to bright red (0xff0000)
metalness: 0.1, roughness: 0.7,
Makes the enemy mostly non-metallic and rough-looking
this.mesh.position.x = mapP5ToThreeX(this.x);
Converts the random p5.js x coordinate to Three.js space
this.mesh.position.y = threeGroundY;
Places the enemy on the ground
this.mesh.position.z = threeEnemySpawnZ;
Spawns the enemy at the far end of the arena (back of the screen)
if (this.isAlive) {
Only updates the enemy if it's still alive
this.y += enemySpeed;
Increases y by enemySpeed (1.5, increases over game time), moving the enemy toward the player
this.mesh.position.z = mapP5ToThreeZ(this.y);
Updates the Three.js Z position to match the new y coordinate
return this.mesh.position.z < threePlayerZ - 10;
Returns true if the enemy has moved past the player's Z position, meaning it slipped past
this.health -= amount;
Subtracts the damage amount from the enemy's health
if (this.health <= 0 && this.isAlive) {
Checks if health has dropped to 0 or below AND the enemy hasn't already died
this.isAlive = false;
Marks the enemy as dead to prevent processing it further
explosionSound.amp(0.8, 0.05);
Plays the explosion sound by setting amplitude to 0.8 over 50 ms
explosionSound.amp(0, 0.5);
Fades the explosion sound to silence over 500 ms
this.mesh.visible = false;
Makes the enemy mesh disappear immediately so the player sees the visual feedback
setTimeout(() => this.dispose(), 500);
Schedules the mesh disposal for 500 ms later, after the explosion sound finishes playing
if (!this.isAlive) return false;
Returns false immediately if the enemy is dead, so dead enemies don't collide with the player
return this.getBounds().intersectsBox(other.getBounds());
Checks if this enemy's bounding box overlaps with another object (player)

keyPressed()

keyPressed() is called by p5.js whenever a key is pressed. Here it only triggers shooting when the game is actively playing. The player.shoot() method itself handles the fire rate limiting, so holding SPACE works naturally.

function keyPressed() {
  if (gameState === 'playing' && key === ' ') { // SPACE bar
    player.shoot();
  }
}
Line-by-line explanation (2 lines)

🔧 Subcomponents:

conditional Space Bar Check if (gameState === 'playing' && key === ' ') {

Only allows shooting when the game is actively playing

if (gameState === 'playing' && key === ' ') {
Checks two conditions: the game must be playing, AND the SPACE key must be pressed
player.shoot();
Calls the player's shoot() method, which creates a bullet if enough time has passed

mouseClicked()

mouseClicked() is called by p5.js whenever the mouse is clicked. It provides an intuitive way for players to start or restart the game. Both the start and game over states call the same restartGame() function, which resets all variables and begins new gameplay.

function mouseClicked() {
  if (gameState === 'start') {
    restartGame();
  } else if (gameState === 'gameOver') {
    restartGame();
  }
}
Line-by-line explanation (4 lines)

🔧 Subcomponents:

conditional Start Screen Click if (gameState === 'start') {

Detects clicks on the start screen to begin the game

conditional Game Over Screen Click } else if (gameState === 'gameOver') {

Detects clicks on the game over screen to restart

if (gameState === 'start') {
Checks if the game is on the start screen
restartGame();
If so, calls restartGame() to begin gameplay
} else if (gameState === 'gameOver') {
Otherwise checks if the game is on the game over screen
restartGame();
If so, calls restartGame() to restart the game

windowResized()

windowResized() is called automatically by p5.js whenever the window is resized. It resizes both the p5.js canvas and the Three.js renderer, and updates the camera's aspect ratio so the 3D scene scales correctly. This ensures the game stays properly sized and visually correct at any window size.

function windowResized() {
  // Resize p5.js canvas
  resizeCanvas(windowWidth, windowHeight);
  player.y = height - 50; // Keep p5.js player y at the bottom

  // Resize three.js renderer and update camera
  camera.aspect = window.innerWidth / window.innerHeight;
  camera.updateProjectionMatrix();
  renderer.setSize(window.innerWidth, window.innerHeight);
}
Line-by-line explanation (5 lines)

🔧 Subcomponents:

calculation p5.js Canvas Resize resizeCanvas(windowWidth, windowHeight);

Resizes the p5.js canvas to match the new window size

calculation Three.js Renderer Resize renderer.setSize(window.innerWidth, window.innerHeight);

Resizes the Three.js renderer to match the new window size

resizeCanvas(windowWidth, windowHeight);
Resizes the p5.js canvas to the new window dimensions
player.y = height - 50;
Repositions the player 50 pixels from the bottom of the new canvas height
camera.aspect = window.innerWidth / window.innerHeight;
Updates the camera's aspect ratio to match the new window dimensions
camera.updateProjectionMatrix();
Recalculates the camera's projection matrix based on the new aspect ratio
renderer.setSize(window.innerWidth, window.innerHeight);
Resizes the Three.js renderer to the new window size

animate()

animate() is the Three.js animation loop, called 60 times per second via requestAnimationFrame(). It updates camera controls, rotates the grid for visual appeal, calls the appropriate p5.js game logic based on game state, and renders the Three.js scene. This function bridges the two rendering systems, ensuring both stay synchronized.

function animate() {
  requestAnimationFrame(animate);

  // Update controls (important for damping)
  controls.update();

  // Rotate the entire grid group slowly for a subtle animation
  gridGroup.rotation.y += 0.001;

  // Call p5.js game logic based on game state
  if (gameState === 'start') {
    drawStartScreen();
    controls.enabled = true; // Enable OrbitControls on start screen
  } else if (gameState === 'playing') {
    playGame();
    controls.enabled = false; // Disable OrbitControls during gameplay
  } else if (gameState === 'gameOver') {
    drawGameOverScreen();
    controls.enabled = true; // Enable OrbitControls on game over screen
  }

  // Render the three.js scene
  renderer.render(scene, camera);
}
Line-by-line explanation (13 lines)

🔧 Subcomponents:

calculation Animation Loop requestAnimationFrame(animate);

Schedules the next frame, creating continuous animation

calculation Grid Rotation gridGroup.rotation.y += 0.001;

Slowly rotates the grid cubes around the Y axis

conditional Control Enable/Disable controls.enabled = true;

Enables camera controls on start and game over screens, disables during gameplay

requestAnimationFrame(animate);
Schedules animate() to run again on the next frame (60 fps), creating the continuous animation loop
controls.update();
Updates the OrbitControls damping effect so the camera movement feels smooth and inertial
gridGroup.rotation.y += 0.001;
Rotates the grid group around the Y axis by 0.001 radians per frame (a very slow rotation)
if (gameState === 'start') {
Checks if the game is on the start screen
drawStartScreen();
Draws the start screen
controls.enabled = true;
Enables OrbitControls so the player can rotate the camera around the grid on the start screen
} else if (gameState === 'playing') {
Checks if the game is actively being played
playGame();
Runs the game logic
controls.enabled = false;
Disables OrbitControls during gameplay so mouse movement doesn't rotate the camera and interfere with the game
} else if (gameState === 'gameOver') {
Checks if the game is on the game over screen
drawGameOverScreen();
Draws the game over screen
controls.enabled = true;
Re-enables OrbitControls so the player can explore the final grid
renderer.render(scene, camera);
Renders the Three.js scene to the canvas

mapP5ToThreeX()

mapP5ToThreeX() converts a 2D p5.js x coordinate to a 3D Three.js x coordinate. In p5.js, x ranges from 0 (left) to width (right). In Three.js, the center is at 0, so we map to negative to positive. This ensures the player always appears at the correct horizontal position in the 3D scene.

function mapP5ToThreeX(p5X) {
  return map(p5X, 0, width, -threeGameWidth / 2, threeGameWidth / 2);
}
Line-by-line explanation (1 lines)
return map(p5X, 0, width, -threeGameWidth / 2, threeGameWidth / 2);
Uses p5.js's map() function to convert from p5.js x coordinates (0 to width) to Three.js x coordinates (-threeGameWidth/2 to threeGameWidth/2), centering the coordinate system at 0

mapP5ToThreeZ()

mapP5ToThreeZ() converts a 2D p5.js y coordinate (vertical on screen) to a 3D Three.js z coordinate (depth). In p5.js, y=0 is the top and y=height is the bottom. In Three.js, we want the top of the screen to appear far away (positive Z) and the bottom to appear close (negative Z), creating the perspective effect of enemies moving toward the camera.

function mapP5ToThreeZ(p5Y) {
  // p5.y=0 (top) -> three.z=threeGameHeight/2 (back)
  // p5.y=height (bottom) -> three.z=-threeGameHeight/2 (front)
  return map(p5Y, 0, height, threeGameHeight / 2, -threeGameHeight / 2);
}
Line-by-line explanation (1 lines)
return map(p5Y, 0, height, threeGameHeight / 2, -threeGameHeight / 2);
Maps p5.js y coordinates (0 at top, height at bottom) to Three.js z coordinates (positive back, negative front). The reversed range means p5.y=0 (top of screen) becomes large positive Z (far away), and p5.y=height (bottom/player) becomes negative Z (close/front).

📦 Key Variables

gameState string

Tracks which screen is currently displayed: 'start' for the start screen, 'playing' for active gameplay, or 'gameOver' for the game over screen

let gameState = 'start';
score number

Stores the player's current score, incremented by 10 points each time a bullet hits an enemy

let score = 0;
player object

Stores the Player object that represents the player's spaceship

let player;
playerHealth number

Stores the player's current health; decreases by 10 when an enemy slips past, by 20 on collision, and causes game over when it reaches 0

let playerHealth = 100;
playerSpeed number

How many pixels the player moves left or right each frame when arrow keys are held (5 pixels per frame)

let playerSpeed = 5;
playerFireRate number

Minimum time in milliseconds between consecutive shots (200 ms = 5 shots per second max)

let playerFireRate = 200;
lastShotTime number

Stores the timestamp (millis()) of the player's last shot, used to enforce fire rate limiting

let lastShotTime = 0;
bullets array

Array of all active Bullet objects currently in the game

let bullets = [];
bulletSpeed number

How many pixels each bullet moves upward per frame (10 pixels per frame)

let bulletSpeed = 10;
bulletDamage number

How many health points each bullet removes from an enemy when it hits (10 HP per hit)

let bulletDamage = 10;
enemies array

Array of all active Enemy objects currently in the game

let enemies = [];
enemySpeed number

How many pixels each enemy moves downward per frame toward the player (starts at 1.5, increases over time to difficulty)

let enemySpeed = 1.5;
enemyHealth number

How many health points each newly spawned enemy starts with (30 HP, reduced by bullets)

let enemyHealth = 30;
enemySpawnRate number

Time in milliseconds between enemy spawns (starts at 1000 ms, decreases over time to speed up the game)

let enemySpawnRate = 1000;
lastEnemySpawnTime number

Stores the timestamp (millis()) of the last enemy spawn, used to enforce spawn rate timing

let lastEnemySpawnTime = 0;
shootSound object

p5.Oscillator object that produces the shooting sound effect (800 Hz sine wave)

let shootSound;
hitSound object

p5.Oscillator object that produces the hit sound effect (400 Hz sine wave)

let hitSound;
explosionSound object

p5.Noise object that produces the explosion sound effect when enemies die

let explosionSound;
gameOverSound object

p5.Oscillator object that produces the game over sound effect (100 Hz square wave)

let gameOverSound;
scene object

THREE.Scene object that holds all 3D objects (player mesh, enemy meshes, grid, lights)

let scene;
camera object

THREE.PerspectiveCamera object that defines the viewpoint of the 3D scene

let camera;
renderer object

THREE.WebGLRenderer object that renders the Three.js scene to a canvas

let renderer;
controls object

THREE.OrbitControls object that allows camera rotation with the mouse on start and game over screens

let controls;
gridGroup object

THREE.Group object containing all the procedurally generated grid cubes, rotated as a single unit

let gridGroup;

🔧 Potential Improvements (6)

Here are some ways this code could be enhanced:

BUG playGame() - enemy spawning

Global variables scene, camera, renderer, controls, gridGroup are never declared with 'let' or 'const' - they are implicitly global, which can cause issues

💡 Add 'let' declarations at the top of the file: let scene, camera, renderer, controls, gridGroup;

PERFORMANCE Player.getBounds(), Bullet.getBounds(), Enemy.getBounds()

Every frame, getBounds() recomputes the bounding box and updates the matrix world, which is expensive. These calculations happen for every active bullet and enemy collision check

💡 Cache bounding boxes and only recompute when position changes, or use a simpler distance-based collision check for faster performance with many entities

STYLE Global variables section

Global variables are scattered and not clearly grouped - audio variables, Three.js objects, game state, etc. are mixed together

💡 Group related variables together with comments: Game State Variables, Player Variables, Bullet Variables, Enemy Variables, Audio Variables, Three.js Variables

BUG Enemy.takeDamage()

Using setTimeout() to delay dispose() is unreliable - if the player closes the browser or navigates away, the timeout may fire after the sketch is destroyed

💡 Instead of setTimeout, track dead enemies in a separate array and dispose them on the next frame after confirming they're no longer needed

FEATURE playGame()

No visual or audio feedback when the player loses health from enemies passing or colliding

💡 Add a camera shake effect, brief flash of red, or distinct sound effect when damage is taken to make the mechanic more satisfying

PERFORMANCE setup() - grid generation

The grid is 20x20 = 400 cubes, each with its own geometry and material - high memory usage

💡 Use a single BufferGeometry with instancing or merge all grid cubes into one mesh to reduce draw calls and memory

🔄 Code Flow

Code flow showing setup, draw, drawstartscreen, playgame, drawgameoverscreen, gameover, restartgame, player, bullet, enemy, keypressed, mouseclicked, windowresized, animate, mapp5tothreex, mapp5tothreez

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

graph TD start[Start] --> setup[setup] setup --> p5-canvas-creation[p5.js Canvas Setup] setup --> player-init[Player Initialization] setup --> sound-init[Sound Setup] setup --> three-scene-setup[Three.js Scene Creation] setup --> camera-setup[Camera Positioning] setup --> grid-generation[Procedural Grid Generation] setup --> animate[animate] animate --> draw[draw loop] draw --> state-routing[Game State Router] state-routing -->|start| drawstartscreen[drawStartScreen] drawstartscreen --> title-text[Title Display] drawstartscreen --> instructions-text[Control Instructions] drawstartscreen --> prompt-text[Start Prompt] state-routing -->|playing| playgame[playGame] playgame --> player-update[Player Update] playgame --> bullet-update-loop[Bullet Update Loop] bullet-update-loop --> bullet-collision-check[Bullet-Enemy Collision] playgame --> enemy-spawn-check[Enemy Spawning] playgame --> enemy-update-loop[Enemy Update Loop] enemy-update-loop --> player-enemy-collision[Player-Enemy Collision] state-routing -->|gameOver| drawgameoverscreen[drawGameOverScreen] drawgameoverscreen --> gameover-title[Game Over Title] drawgameoverscreen --> final-score-display[Final Score Display] drawgameoverscreen --> restart-prompt[Restart Prompt] state-routing -->|gameOver| gameover[gameOver] gameover --> audio-fadeout[Audio Fadeout] gameover --> state-change[State Transition] state-change --> restartgame[restartGame] restartgame --> variable-reset[Variable Reset] restartgame --> mesh-cleanup[Mesh Cleanup] restartgame --> array-reset[Array Reset] restartgame --> state-reset[State Reset] click setup href "#fn-setup" click draw href "#fn-draw" click drawstartscreen href "#fn-drawstartscreen" click playgame href "#fn-playgame" click drawgameoverscreen href "#fn-drawgameoverscreen" click gameover href "#fn-gameover" click restartgame href "#fn-restartgame" click p5-canvas-creation href "#sub-p5-canvas-creation" click player-init href "#sub-player-init" click sound-init href "#sub-sound-init" click three-scene-setup href "#sub-three-scene-setup" click camera-setup href "#sub-camera-setup" click grid-generation href "#sub-grid-generation" click state-routing href "#sub-state-routing" click title-text href "#sub-title-text" click instructions-text href "#sub-instructions-text" click prompt-text href "#sub-prompt-text" click player-update href "#sub-player-update" click bullet-update-loop href "#sub-bullet-update-loop" click bullet-collision-check href "#sub-bullet-collision-check" click enemy-spawn-check href "#sub-enemy-spawn-check" click enemy-update-loop href "#sub-enemy-update-loop" click player-enemy-collision href "#sub-player-enemy-collision" click gameover-title href "#sub-gameover-title" click final-score-display href "#sub-final-score-display" click restart-prompt href "#sub-restart-prompt" click audio-fadeout href "#sub-audio-fadeout" click state-change href "#sub-state-change" click variable-reset href "#sub-variable-reset" click mesh-cleanup href "#sub-mesh-cleanup" click array-reset href "#sub-array-reset" click state-reset href "#sub-state-reset"

❓ Frequently Asked Questions

What visual experience does the p5.js sketch 'veck.io pls friend moon_noob on the real website veck.io' create?

This sketch creates a dynamic game environment featuring a player character, enemies, and various sound effects that enhance the gameplay experience.

How can users interact with this p5.js sketch during gameplay?

Users can control the player character to shoot at enemies while avoiding damage, with shooting mechanics based on a set fire rate.

What creative coding techniques are highlighted in this p5.js sketch?

The sketch demonstrates object-oriented programming through the use of player and enemy classes, and utilizes sound synthesis for interactive audio feedback.

Preview

veck.io pls friend moon_noob on the real website veck.io - p5.js creative coding sketch preview
Sketch Preview
Code flow diagram showing the structure of veck.io pls friend moon_noob on the real website veck.io - Code flow showing setup, draw, drawstartscreen, playgame, drawgameoverscreen, gameover, restartgame, player, bullet, enemy, keypressed, mouseclicked, windowresized, animate, mapp5tothreex, mapp5tothreez
Code Flow Diagram