Sketch 2026-02-13 17:42

Poubelle Shooter is a first-person shooter game that combines three.js 3D graphics with p5.js UI overlays. Players navigate a 3D environment using WASD keys, aim with the mouse, and shoot randomly spawning garbage cans to earn points within a 60-second timer while managing a health system.

🧪 Try This!

Experiment with the code by making these changes:

  1. Make enemies spawn faster — Lower the spawn delay so poubelles appear more frequently and the game gets harder
  2. Increase points per hit — Award more points for each successful shot to make high scores feel more rewarding
  3. Change the sky color — Alter the 3D arena's background color to create a completely different mood
  4. Make the game longer — Extend the time limit so players have more time to rack up points
  5. Increase player speed — Make WASD movement faster so navigation around the arena feels snappier
  6. Make misses more forgiving — Reduce the health penalty for missing shots so the game is less punishing
Prefer the full editor? Open it there →

📖 About This Sketch

This sketch creates a fully playable first-person shooter game called Poubelle Shooter, where you navigate a 3D arena and shoot at garbage cans to score points. The game combines three.js for rendering a 3D environment, raycasting for detecting successful shots, pointer lock controls for FPS-style camera movement, and p5.js for drawing the UI overlay (crosshair, score, health, timer). It demonstrates how two graphics libraries can work together seamlessly—three.js handles the immersive 3D world while p5.js renders transparent 2D UI elements on top.

The code is organized into three main sections: p5.js setup and draw functions that manage UI rendering and game state transitions; three.js initialization and animation loop that handles 3D rendering, player movement, and enemy spawning; and game logic functions that handle shooting, hit detection, scoring, and health management. By studying this sketch, you will learn how to build a complete interactive game loop, manage 3D object spawning and removal, detect ray-object intersections for hit detection, apply physics-like friction and acceleration to player movement, and coordinate two rendering systems in one application.

⚙️ How It Works

  1. When the sketch loads, setup() initializes both p5.js and three.js: it creates a transparent p5 canvas for UI overlays, sets up three.js with a camera positioned at player height, creates a green floor plane, adds lighting, and initializes PointerLockControls for first-person movement. The scene starts in the 'start' game state.
  2. The p5.js draw loop runs continuously and displays different screens based on gameState: on the start screen it shows instructions; during gameplay it draws a crosshair in the center and a HUD showing score, health, and remaining time; when the game ends it displays the final score.
  3. The three.js animation loop (animateThreeJS) runs in parallel and handles all 3D updates: it applies friction to player velocity, reads keyboard input (WASD) to calculate movement direction, moves the camera, and keeps the player within the arena bounds.
  4. Every 1.5 seconds (controlled by spawnRate), a new poubelle (garbage can) spawns at a random position in the arena, up to a maximum of 15 enemies at once. Each poubelle is a merged three.js mesh made from a cylinder body and a flat lid.
  5. When the player clicks while the pointer is locked, the shoot() function fires a ray from the camera center and checks for intersections with all poubelles. If a hit is detected, the score increases by 10 and the enemy is removed; if the shot misses, the player loses 5 health.
  6. The game ends when either 60 seconds elapse (checked in drawHUD) or health reaches zero. A click in the game-over state restarts the entire game by resetting score, health, enemies array, and spawn timer.

🎓 Concepts You'll Learn

Three.js 3D graphics libraryRaycasting for collision detectionPointer lock controls (FPS-style camera)Game state managementObject spawning and poolingMesh geometry mergingVelocity and friction-based movementOverlay UI rendering with p5.js

📝 Code Breakdown

setup()

setup() runs once when the sketch starts and is where you initialize everything: create canvases, load assets, set up the 3D scene, and register event listeners. In this game, setup() bridges p5.js and three.js by creating both rendering systems and connecting them to the same container.

function setup() {
  p5Canvas = createCanvas(windowWidth, windowHeight);
  p5Canvas.parent('three-canvas-container');
  p5Canvas.style('position', 'absolute');
  p5Canvas.style('top', '0');
  p5Canvas.style('left', '0');
  p5Canvas.style('z-index', '1');

  shootSound = new p5.Oscillator();
  shootSound.setType('sawtooth');
  shootSound.freq(300);
  shootSound.amp(0);
  shootSound.start();

  initThreeJS();

  document.addEventListener('keydown', onKeyDown, false);
  document.addEventListener('keyup', onKeyUp, false);
  document.addEventListener('click', onDocumentClick, false);

  document.addEventListener('pointerlockchange', onPointerLockChange, false);
  document.addEventListener('mozpointerlockchange', onPointerLockChange, false);
  document.addEventListener('webkitpointerlockchange', onPointerLockChange, false);
}
Line-by-line explanation (13 lines)

🔧 Subcomponents:

function-call p5 Canvas Setup p5Canvas = createCanvas(windowWidth, windowHeight);

Creates a transparent p5.js canvas that overlays the three.js 3D scene for rendering UI elements

calculation Absolute Positioning p5Canvas.style('position', 'absolute');

Positions the p5 canvas absolutely so it sits directly on top of the three.js renderer without affecting layout

function-call Shoot Sound Initialization shootSound = new p5.Oscillator();

Creates a sawtooth-wave oscillator that will play a harsh sound when the player shoots

function-call Input Event Listeners document.addEventListener('keydown', onKeyDown, false);

Registers keyboard and mouse input handlers to detect player movement (WASD) and shooting (click)

p5Canvas = createCanvas(windowWidth, windowHeight);
Creates a p5.js canvas that fills the entire window; this will be used for drawing UI elements (score, crosshair, etc.)
p5Canvas.parent('three-canvas-container');
Attaches the p5 canvas to the same DOM container as the three.js renderer, allowing both to share the same space
p5Canvas.style('position', 'absolute');
Makes the p5 canvas positioned absolutely, so it overlays the three.js canvas without affecting page layout
p5Canvas.style('z-index', '1');
Sets the p5 canvas to appear on top of the three.js renderer (which has z-index 0), so UI is visible over the 3D game
shootSound = new p5.Oscillator();
Creates an oscillator object that will generate sound when the player shoots
shootSound.setType('sawtooth');
Sets the oscillator to a sawtooth waveform, which produces a harsh, buzzy sound suitable for a shooter game
shootSound.freq(300);
Sets the oscillator frequency to 300 Hz, which is a low-mid pitch for the shooting sound
shootSound.amp(0);
Starts the oscillator at zero volume (silent) so it's ready to play but not audible until needed
shootSound.start();
Starts the oscillator running; it will remain silent until amp() is changed to a positive value
initThreeJS();
Calls the function that sets up the three.js scene, camera, renderer, lights, floor, and controls
document.addEventListener('keydown', onKeyDown, false);
Registers a listener for keyboard key-down events so movement keys (WASD) can be detected
document.addEventListener('click', onDocumentClick, false);
Registers a listener for mouse clicks, which trigger both shooting (if in-game) and pointer lock requests (to start the game)
document.addEventListener('pointerlockchange', onPointerLockChange, false);
Detects when pointer lock is gained or lost, allowing the game to respond to pointer lock state changes (e.g., pressing Escape)

draw()

The p5.js draw() loop runs 60 times per second and handles all 2D UI rendering by checking the current game state and drawing the appropriate screen. It runs independently from the three.js animation loop, allowing both rendering systems to update in parallel. The clear() call is critical—without it, old text and shapes would pile up and create visual noise.

function draw() {
  clear();

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

🔧 Subcomponents:

function-call Clear Canvas clear();

Erases the previous frame's p5 drawings so the canvas stays transparent and doesn't accumulate visual artifacts

conditional Game State Branch if (gameState === 'start') { drawStartScreen(); }

Directs which UI screen to draw based on the current game state (start, playing, or gameOver)

clear();
Clears the p5 canvas so drawings from the previous frame disappear, preventing trails or overlapping text
if (gameState === 'start') {
Checks if the game is in the 'start' state, meaning the player hasn't clicked play yet
drawStartScreen();
If starting, draws the title, instructions, and controls on the p5 canvas overlay
} else if (gameState === 'playing') {
Checks if the game is actively being played
drawCrosshair();
Draws a crosshair in the center of the screen to help the player aim
drawHUD();
Draws the heads-up display showing score, health, and remaining time in the top corners
} else if (gameState === 'gameOver') {
Checks if the game has ended (time ran out or health hit zero)
drawGameOverScreen();
Displays the 'GAME OVER' message along with the final score and restart instruction

drawStartScreen()

This function draws the start screen overlay. It's called from draw() when gameState === 'start', which happens before the player clicks. The screen displays the game title, instructions, and control scheme all centered in the middle of the screen.

function drawStartScreen() {
  textAlign(CENTER, CENTER);
  textSize(48);
  fill(255);
  text("POUBELLE SHOOTER", width / 2, height / 2 - 50);
  textSize(24);
  text("Click to play!", width / 2, height / 2 + 50);
  text("W A S D to move, Mouse to aim and shoot", width / 2, height / 2 + 100);
}
Line-by-line explanation (7 lines)

🔧 Subcomponents:

function-call Title Text text("POUBELLE SHOOTER", width / 2, height / 2 - 50);

Displays the large game title centered on the screen

function-call Instruction Text text("Click to play!", width / 2, height / 2 + 50);

Prompts the player to click to start the game

function-call Controls Display text("W A S D to move, Mouse to aim and shoot", width / 2, height / 2 + 100);

Teaches the player how to move and shoot before the game begins

textAlign(CENTER, CENTER);
Sets text alignment to center both horizontally and vertically so text positions are at the text's center point
textSize(48);
Sets the font size to 48 pixels for the large title
fill(255);
Sets the text color to white (255 in grayscale means full brightness)
text("POUBELLE SHOOTER", width / 2, height / 2 - 50);
Draws the game title centered horizontally and positioned 50 pixels above the screen's vertical center
textSize(24);
Reduces font size to 24 pixels for the instructions below the title
text("Click to play!", width / 2, height / 2 + 50);
Displays the start instruction centered on the screen, 50 pixels below center
text("W A S D to move, Mouse to aim and shoot", width / 2, height / 2 + 100);
Shows the control scheme to teach the player how to interact with the game

drawCrosshair()

drawCrosshair() draws a simple plus-sign aiming reticle in the center of the screen during gameplay. It's called every frame from draw() when the game is playing, providing visual feedback for where the player's shots will go (straight from the camera center).

function drawCrosshair() {
  stroke(255);
  strokeWeight(2);
  const size = 10;
  line(width / 2 - size, height / 2, width / 2 + size, height / 2);
  line(width / 2, height / 2 - size, width / 2, height / 2 + size);
}
Line-by-line explanation (5 lines)

🔧 Subcomponents:

function-call Horizontal Crosshair Line line(width / 2 - size, height / 2, width / 2 + size, height / 2);

Draws the horizontal line of the crosshair through the center of the screen

function-call Vertical Crosshair Line line(width / 2, height / 2 - size, width / 2, height / 2 + size);

Draws the vertical line of the crosshair through the center of the screen

stroke(255);
Sets the line color to white so the crosshair is visible against any background
strokeWeight(2);
Sets the line thickness to 2 pixels, making the crosshair visible but not too thick
const size = 10;
Defines the length of each crosshair arm as 10 pixels from the center
line(width / 2 - size, height / 2, width / 2 + size, height / 2);
Draws a horizontal line from 10 pixels left of center to 10 pixels right of center at the vertical middle of the screen
line(width / 2, height / 2 - size, width / 2, height / 2 + size);
Draws a vertical line from 10 pixels above center to 10 pixels below center at the horizontal middle of the screen

drawHUD()

drawHUD() renders the heads-up display showing score, health, and a countdown timer. It's called every frame during gameplay and also checks if time has run out, ending the game if the timer reaches zero. The function demonstrates real-time calculation and display of game state variables.

🔬 These lines calculate how much time is left. What happens if you divide by 2000 instead of 1000? How would that change the timer display?

  const elapsedTime = millis() - gameStartTime;
  const timeLeft = max(0, (gameDuration - elapsedTime) / 1000);
function drawHUD() {
  textAlign(LEFT, TOP);
  textSize(24);
  fill(255);
  text(`Score: ${score}`, 20, 20);
  text(`Health: ${health}`, 20, 50);

  const elapsedTime = millis() - gameStartTime;
  const timeLeft = max(0, (gameDuration - elapsedTime) / 1000);
  textAlign(RIGHT, TOP);
  text(`Time: ${timeLeft.toFixed(1)}s`, width - 20, 20);

  if (timeLeft <= 0) {
    endGame();
  }
}
Line-by-line explanation (11 lines)

🔧 Subcomponents:

function-call Score Text text(`Score: ${score}`, 20, 20);

Shows the current score in the top-left corner

function-call Health Text text(`Health: ${health}`, 20, 50);

Shows the current health in the top-left corner below the score

calculation Time Remaining Calculation const timeLeft = max(0, (gameDuration - elapsedTime) / 1000);

Calculates the remaining game time in seconds, clamping it to never go below zero

function-call Timer Text text(`Time: ${timeLeft.toFixed(1)}s`, width - 20, 20);

Shows the remaining time in the top-right corner with one decimal place

conditional Time Up End Game if (timeLeft <= 0) { endGame(); }

Ends the game when the timer reaches zero

textAlign(LEFT, TOP);
Aligns text to the top-left corner for score and health display
textSize(24);
Sets font size to 24 pixels for the HUD text
fill(255);
Sets text color to white for visibility
text(`Score: ${score}`, 20, 20);
Displays the current score 20 pixels from the left edge and 20 pixels from the top using template literal syntax
text(`Health: ${health}`, 20, 50);
Displays the current health 20 pixels from the left edge and 50 pixels from the top (30 pixels below score)
const elapsedTime = millis() - gameStartTime;
Calculates how many milliseconds have elapsed since the game started
const timeLeft = max(0, (gameDuration - elapsedTime) / 1000);
Subtracts elapsed time from total duration, divides by 1000 to convert ms to seconds, and uses max() to ensure it never goes negative
textAlign(RIGHT, TOP);
Switches text alignment to the top-right for the timer display
text(`Time: ${timeLeft.toFixed(1)}s`, width - 20, 20);
Displays remaining time 20 pixels from the right edge in the top-right corner, formatted to 1 decimal place
if (timeLeft <= 0) {
Checks if the game timer has reached zero
endGame();
Calls the endGame function to transition to the game over state

drawGameOverScreen()

drawGameOverScreen() displays the game-over UI when gameState === 'gameOver'. It shows the final score and prompts the player to click to restart, creating a clean end-of-game experience.

function drawGameOverScreen() {
  textAlign(CENTER, CENTER);
  textSize(48);
  fill(255);
  text("GAME OVER", width / 2, height / 2 - 50);
  textSize(36);
  text(`Final Score: ${score}`, width / 2, height / 2 + 10);
  textSize(24);
  text("Click to restart", width / 2, height / 2 + 80);
}
Line-by-line explanation (8 lines)

🔧 Subcomponents:

function-call Game Over Title text("GAME OVER", width / 2, height / 2 - 50);

Displays the large 'GAME OVER' message

function-call Final Score Display text(`Final Score: ${score}`, width / 2, height / 2 + 10);

Shows the player's final score achieved before the game ended

function-call Restart Instruction text("Click to restart", width / 2, height / 2 + 80);

Prompts the player to click to restart the game

textAlign(CENTER, CENTER);
Centers text both horizontally and vertically
textSize(48);
Sets large font size for the 'GAME OVER' title
fill(255);
Sets text color to white
text("GAME OVER", width / 2, height / 2 - 50);
Displays 'GAME OVER' centered horizontally and positioned 50 pixels above the screen center
textSize(36);
Reduces font size to 36 pixels for the score display
text(`Final Score: ${score}`, width / 2, height / 2 + 10);
Displays the final score using a template literal to insert the score variable, positioned near the center
textSize(24);
Reduces font size to 24 pixels for the restart instruction
text("Click to restart", width / 2, height / 2 + 80);
Displays the restart prompt 80 pixels below screen center

initThreeJS()

initThreeJS() sets up the entire three.js 3D environment: it creates the scene, camera at player eye height, WebGL renderer, lighting (ambient and directional), the arena floor, pointer lock controls for FPS movement, and a raycaster for hit detection. This function is called once from setup() to initialize all 3D systems.

function initThreeJS() {
  scene = new THREE.Scene();
  scene.background = new THREE.Color(0x87CEEB);

  camera = new THREE.PerspectiveCamera(75, windowWidth / windowHeight, 0.1, 1000);
  camera.position.set(0, 1.6, 0);

  renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true });
  renderer.setSize(windowWidth, windowHeight);
  document.getElementById('three-canvas-container').appendChild(renderer.domElement);

  const ambientLight = new THREE.AmbientLight(0xffffff, 0.8);
  scene.add(ambientLight);
  const directionalLight = new THREE.DirectionalLight(0xffffff, 0.6);
  directionalLight.position.set(5, 10, 5).normalize();
  scene.add(directionalLight);

  const floorGeometry = new THREE.PlaneGeometry(100, 100);
  const floorMaterial = new THREE.MeshLambertMaterial({ color: 0x228B22 });
  const floor = new THREE.Mesh(floorGeometry, floorMaterial);
  floor.rotation.x = -Math.PI / 2;
  floor.position.y = -0.01;
  scene.add(floor);

  controls = new THREE.PointerLockControls(camera, renderer.domElement);
  scene.add(controls.getObject());

  raycaster = new THREE.Raycaster();

  animateThreeJS();
}
Line-by-line explanation (21 lines)

🔧 Subcomponents:

function-call Scene Setup scene = new THREE.Scene();

Creates the three.js scene that will contain all 3D objects

function-call Camera Setup camera = new THREE.PerspectiveCamera(75, windowWidth / windowHeight, 0.1, 1000);

Creates a perspective camera with a 75-degree field of view positioned at player eye height

function-call Renderer Setup renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true });

Creates a WebGL renderer with anti-aliasing enabled and alpha transparency for p5.js overlay

function-call Lighting Setup const ambientLight = new THREE.AmbientLight(0xffffff, 0.8);

Adds ambient light so all objects in the scene are evenly illuminated

function-call Floor Geometry const floorGeometry = new THREE.PlaneGeometry(100, 100);

Creates a 100x100 unit plane that serves as the arena floor

function-call Pointer Lock Controls controls = new THREE.PointerLockControls(camera, renderer.domElement);

Initializes first-person controls that lock the mouse cursor and allow camera rotation

function-call Raycaster Initialization raycaster = new THREE.Raycaster();

Creates a raycaster used to detect what objects the player's shots hit

scene = new THREE.Scene();
Creates a new three.js scene, which is the container for all 3D objects, lights, and the camera
scene.background = new THREE.Color(0x87CEEB);
Sets the scene background to sky blue using a hex color code
camera = new THREE.PerspectiveCamera(75, windowWidth / windowHeight, 0.1, 1000);
Creates a perspective camera with 75° FOV, matching the window aspect ratio, with near clip at 0.1 and far clip at 1000
camera.position.set(0, 1.6, 0);
Positions the camera at 1.6 units high (approximately human eye height) and centered on the ground plane
renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true });
Creates a WebGL renderer with anti-aliasing for smooth edges and alpha transparency to blend with the p5.js canvas
renderer.setSize(windowWidth, windowHeight);
Sets the renderer resolution to fill the entire browser window
document.getElementById('three-canvas-container').appendChild(renderer.domElement);
Attaches the three.js renderer canvas to the HTML container div
const ambientLight = new THREE.AmbientLight(0xffffff, 0.8);
Creates ambient light (white light at 80% intensity) that illuminates all objects equally
scene.add(ambientLight);
Adds the ambient light to the scene
const directionalLight = new THREE.DirectionalLight(0xffffff, 0.6);
Creates a directional light (like sunlight) at 60% intensity
directionalLight.position.set(5, 10, 5).normalize();
Positions the directional light above and to the side of the scene, then normalizes the direction
const floorGeometry = new THREE.PlaneGeometry(100, 100);
Creates a 100x100 unit plane geometry for the arena floor
const floorMaterial = new THREE.MeshLambertMaterial({ color: 0x228B22 });
Creates a Lambert material (matte) with forest green color for the floor
const floor = new THREE.Mesh(floorGeometry, floorMaterial);
Combines the geometry and material into a mesh, which is a 3D object that can be rendered
floor.rotation.x = -Math.PI / 2;
Rotates the plane 90 degrees around the x-axis so it lies flat on the ground instead of standing vertical
floor.position.y = -0.01;
Slightly lowers the floor to prevent z-fighting (visual artifacts) with objects placed on it
scene.add(floor);
Adds the floor mesh to the scene
controls = new THREE.PointerLockControls(camera, renderer.domElement);
Creates PointerLockControls, which enables first-person camera control by hiding and locking the mouse
scene.add(controls.getObject());
Adds the controls' camera group to the scene, allowing the controls to update camera position
raycaster = new THREE.Raycaster();
Creates a raycaster object that will cast rays from the camera to detect collisions with objects
animateThreeJS();
Calls the animation loop function to start the continuous rendering and update cycle

onDocumentClick()

onDocumentClick() is an event listener callback registered in setup(). It handles all mouse clicks in the game: starting/restarting when on the start or game-over screens, and shooting when actively playing with pointer lock enabled. The controls.isLocked check ensures the player can't shoot before requesting pointer lock.

function onDocumentClick() {
  if (gameState === 'start' || gameState === 'gameOver') {
    startGame();
  } else if (gameState === 'playing' && controls.isLocked) {
    shoot();
  }
}
Line-by-line explanation (5 lines)

🔧 Subcomponents:

conditional Start/Restart Game if (gameState === 'start' || gameState === 'gameOver') {

Detects if the player clicked to start or restart the game

conditional Shoot Action } else if (gameState === 'playing' && controls.isLocked) {

Allows shooting only when the game is active and pointer is locked

function onDocumentClick() {
Event handler called whenever the document (browser window) is clicked
if (gameState === 'start' || gameState === 'gameOver') {
Checks if the game is in the start screen or game-over screen state
startGame();
If true, calls startGame() to begin a new game or restart after game over
} else if (gameState === 'playing' && controls.isLocked) {
Checks if the game is actively playing AND the pointer is locked (mouse captured)
shoot();
If true, calls shoot() to fire a projectile and detect hits

startGame()

startGame() initializes a fresh game by resetting all game state variables, cleaning up enemies from any previous game, recording the start time for the timer, and requesting pointer lock. It's called whenever the player clicks during the start or game-over screens.

function startGame() {
  score = 0;
  health = 100;
  enemies.forEach(enemy => {
    scene.remove(enemy);
    enemy.geometry.dispose();
    enemy.material.dispose();
  });
  enemies = [];
  lastSpawnTime = millis();
  gameStartTime = millis();
  gameState = 'playing';
  controls.lock();
}
Line-by-line explanation (11 lines)

🔧 Subcomponents:

calculation Score Reset score = 0;

Resets the score to zero at game start

calculation Health Reset health = 100;

Resets the health to full

for-loop Enemy Cleanup Loop enemies.forEach(enemy => { scene.remove(enemy); enemy.geometry.dispose(); enemy.material.dispose(); });

Removes all existing enemies from the scene and frees their memory

calculation Clear Enemies Array enemies = [];

Empties the enemies array to start fresh with no tracked enemies

calculation Timer Reset gameStartTime = millis();

Records the current time as the game start, so the 60-second timer begins counting

calculation Game State Change gameState = 'playing';

Transitions from start/gameOver state to active playing state

function-call Request Pointer Lock controls.lock();

Requests pointer lock to hide the mouse and enable FPS-style camera control

score = 0;
Resets the score variable to zero for a fresh game
health = 100;
Resets the player's health to full (100 points)
enemies.forEach(enemy => {
Loops through each enemy in the enemies array to clean up from any previous game
scene.remove(enemy);
Removes the enemy mesh from the three.js scene so it's no longer rendered
enemy.geometry.dispose();
Frees the memory used by the enemy's geometry (3D shape data)
enemy.material.dispose();
Frees the memory used by the enemy's material (color and shading data)
enemies = [];
Clears the enemies array, resetting it to an empty state for the new game
lastSpawnTime = millis();
Records the current time as the spawn timer baseline so enemies don't immediately spawn
gameStartTime = millis();
Records the current time as when the game started, used to calculate the countdown timer
gameState = 'playing';
Changes the game state to 'playing' so draw() will render gameplay UI instead of start/game-over screens
controls.lock();
Requests pointer lock, which hides the mouse and locks it to the game window for FPS-style camera control

endGame()

endGame() is a simple state transition function called when the game ends (either time runs out or health hits zero). It changes the game state and releases pointer lock to show the cursor, allowing the player to click to restart.

function endGame() {
  gameState = 'gameOver';
  controls.unlock();
}
Line-by-line explanation (2 lines)

🔧 Subcomponents:

calculation Game Over State gameState = 'gameOver';

Transitions the game state to game over, triggering the game-over UI to display

function-call Release Pointer Lock controls.unlock();

Releases the pointer lock, showing the mouse cursor again

gameState = 'gameOver';
Sets the game state to 'gameOver' so draw() will render the game-over screen on the next frame
controls.unlock();
Releases pointer lock, making the mouse cursor visible again so the player can click to restart

onPointerLockChange()

onPointerLockChange() is an event listener that responds when pointer lock is gained or lost. It handles the case where a player presses Escape to exit pointer lock during gameplay, automatically ending the game to prevent an unfair situation where the player can't aim but the game keeps running.

function onPointerLockChange() {
  if (document.pointerLockElement === renderer.domElement ||
      document.mozPointerLockElement === renderer.domElement ||
      document.webkitPointerLockElement === renderer.domElement) {
  } else {
    if (gameState === 'playing') {
      endGame();
    }
  }
}
Line-by-line explanation (7 lines)

🔧 Subcomponents:

conditional Pointer Lock Status Check if (document.pointerLockElement === renderer.domElement ||

Detects if the pointer is locked (and handles browser vendor prefixes)

conditional Unlock Response } else { if (gameState === 'playing') { endGame(); } }

Ends the game if pointer lock is lost while playing (e.g., user pressed Escape)

function onPointerLockChange() {
Event handler called whenever the pointer lock state changes (lock gained or lost)
if (document.pointerLockElement === renderer.domElement ||
Checks if the pointer is locked to the three.js renderer element (standard API)
document.mozPointerLockElement === renderer.domElement ||
Checks for Firefox-specific pointer lock (vendor prefix)
document.webkitPointerLockElement === renderer.domElement) {
Checks for Chrome/Safari-specific pointer lock (vendor prefix)
} else {
If none of the above are true, the pointer is not locked
if (gameState === 'playing') {
Checks if the game is currently being played
endGame();
If the pointer lock was lost while playing (e.g., user pressed Esc), end the game

onKeyDown()

onKeyDown() is an event listener that detects keyboard key presses. It uses a switch statement to map WASD keys to movement direction flags (moveForward, moveLeft, moveBackward, moveRight). These flags are checked in the animation loop to update player position each frame.

🔬 This switch statement maps WASD keys to movement flags. What happens if you change 'KeyW' to 'KeyI' so the I key moves you forward instead of W? Try remapping the controls to arrow keys or other letters.

  switch (event.code) {
    case 'KeyW':
      moveForward = true;
      break;
    case 'KeyA':
      moveLeft = true;
      break;
    case 'KeyS':
      moveBackward = true;
      break;
    case 'KeyD':
      moveRight = true;
      break;
  }
function onKeyDown(event) {
  switch (event.code) {
    case 'KeyW':
      moveForward = true;
      break;
    case 'KeyA':
      moveLeft = true;
      break;
    case 'KeyS':
      moveBackward = true;
      break;
    case 'KeyD':
      moveRight = true;
      break;
  }
}
Line-by-line explanation (8 lines)

🔧 Subcomponents:

switch-case Key Code Switch switch (event.code) {

Routes different keyboard keys to different movement flags

switch-case W Key Handler case 'KeyW': moveForward = true; break;

Sets moveForward flag when W is pressed

switch-case A Key Handler case 'KeyA': moveLeft = true; break;

Sets moveLeft flag when A is pressed

switch-case S Key Handler case 'KeyS': moveBackward = true; break;

Sets moveBackward flag when S is pressed

switch-case D Key Handler case 'KeyD': moveRight = true; break;

Sets moveRight flag when D is pressed

function onKeyDown(event) {
Event handler called when any keyboard key is pressed
switch (event.code) {
Uses a switch statement to check which key was pressed by its event.code value
case 'KeyW':
If the W key was pressed...
moveForward = true;
...set the moveForward flag to true, signaling that forward movement should occur
break;
Exits the switch statement for this case
case 'KeyA':
If the A key was pressed, set moveLeft to true
case 'KeyS':
If the S key was pressed, set moveBackward to true
case 'KeyD':
If the D key was pressed, set moveRight to true

onKeyUp()

onKeyUp() is the complement to onKeyDown(). It's called whenever a key is released and sets the corresponding movement flag to false. Together, onKeyDown and onKeyUp allow smooth, responsive movement: the flags stay true while a key is held down and become false when released.

function onKeyUp(event) {
  switch (event.code) {
    case 'KeyW':
      moveForward = false;
      break;
    case 'KeyA':
      moveLeft = false;
      break;
    case 'KeyS':
      moveBackward = false;
      break;
    case 'KeyD':
      moveRight = false;
      break;
  }
}
Line-by-line explanation (6 lines)

🔧 Subcomponents:

switch-case Key Code Switch switch (event.code) {

Routes different keyboard key releases to stop movement flags

function onKeyUp(event) {
Event handler called when a keyboard key is released
switch (event.code) {
Checks which key was released using its event.code
case 'KeyW': moveForward = false; break;
If W is released, set moveForward to false to stop forward movement
case 'KeyA': moveLeft = false; break;
If A is released, set moveLeft to false
case 'KeyS': moveBackward = false; break;
If S is released, set moveBackward to false
case 'KeyD': moveRight = false; break;
If D is released, set moveRight to false

animateThreeJS()

animateThreeJS() is the three.js animation loop that runs 60 times per second. It handles all 3D updates: calculating delta time for frame-rate-independent movement, applying friction and acceleration physics to player velocity, moving the camera based on WASD input, spawning enemies periodically, and rendering the scene. This function runs in parallel with p5.js's draw() loop, demonstrating how two rendering systems can work together.

🔬 This block converts movement flags into a normalized direction vector. What happens if you remove the direction.normalize() call? The player would move faster diagonally. Why does normalize() prevent that?

    direction.z = Number(moveForward) - Number(moveBackward);
    direction.x = Number(moveRight) - Number(moveLeft);
    direction.normalize();

🔬 These lines accelerate the player in the direction they're holding. What happens if you increase moveSpeed from 0.15 to 0.5? Try changing it to see how much faster the player moves.

    if (moveForward || moveBackward) velocity.z -= direction.z * moveSpeed * 60.0 * delta;
    if (moveLeft || moveRight) velocity.x -= direction.x * moveSpeed * 60.0 * delta;
function animateThreeJS() {
  requestAnimationFrame(animateThreeJS);

  const time = performance.now();
  const delta = (time - prevTime) / 1000;

  if (controls.isLocked === true && gameState === 'playing') {
    velocity.x -= velocity.x * 10.0 * delta;
    velocity.z -= velocity.z * 10.0 * delta;

    direction.z = Number(moveForward) - Number(moveBackward);
    direction.x = Number(moveRight) - Number(moveLeft);
    direction.normalize();

    if (moveForward || moveBackward) velocity.z -= direction.z * moveSpeed * 60.0 * delta;
    if (moveLeft || moveRight) velocity.x -= direction.x * moveSpeed * 60.0 * delta;

    controls.moveRight(-velocity.x * delta);
    controls.moveForward(-velocity.z * delta);

    camera.position.x = Math.max(-49, Math.min(49, camera.position.x));
    camera.position.z = Math.max(-49, Math.min(49, camera.position.z));

    if (millis() - lastSpawnTime > spawnRate && enemies.length < maxEnemies) {
      spawnPoubelle();
      lastSpawnTime = millis();
    }
  }

  prevTime = time;

  renderer.render(scene, camera);
}
Line-by-line explanation (20 lines)

🔧 Subcomponents:

function-call Request Next Frame requestAnimationFrame(animateThreeJS);

Schedules this function to run again on the next display refresh (~60fps)

calculation Delta Time Calculation const delta = (time - prevTime) / 1000;

Calculates elapsed time since last frame in seconds for frame-rate-independent movement

calculation Friction/Damping velocity.x -= velocity.x * 10.0 * delta;

Applies friction to slow the player down when no input is given

calculation Movement Direction direction.z = Number(moveForward) - Number(moveBackward);

Converts boolean movement flags into a normalized direction vector

conditional Acceleration Application if (moveForward || moveBackward) velocity.z -= direction.z * moveSpeed * 60.0 * delta;

Accelerates the player in the input direction

function-call Apply Movement to Camera controls.moveRight(-velocity.x * delta);

Moves the camera using the calculated velocity

calculation Boundary Clamping camera.position.x = Math.max(-49, Math.min(49, camera.position.x));

Keeps the player within the arena bounds

conditional Enemy Spawn Logic if (millis() - lastSpawnTime > spawnRate && enemies.length < maxEnemies) {

Periodically spawns new enemies up to the maximum limit

function-call Render Scene renderer.render(scene, camera);

Renders the 3D scene to the canvas

requestAnimationFrame(animateThreeJS);
Schedules this function to run again on the next browser repaint cycle, creating continuous animation
const time = performance.now();
Gets the current time in milliseconds with high precision
const delta = (time - prevTime) / 1000;
Calculates the time elapsed since the last frame, converted to seconds (divide by 1000)
if (controls.isLocked === true && gameState === 'playing') {
Only update movement and spawning if the pointer is locked AND the game is actively playing
velocity.x -= velocity.x * 10.0 * delta;
Applies friction: multiplies velocity by 10 times delta to gradually slow the player (deceleration)
velocity.z -= velocity.z * 10.0 * delta;
Applies the same friction in the forward/backward direction
direction.z = Number(moveForward) - Number(moveBackward);
Converts boolean flags to numbers (true=1, false=0) then subtracts: moving forward gives +1, backward gives -1, both gives 0
direction.x = Number(moveRight) - Number(moveLeft);
Converts boolean flags to numbers for left/right: moving right gives +1, left gives -1
direction.normalize();
Normalizes the direction vector so diagonal movement doesn't go faster than axis-aligned movement
if (moveForward || moveBackward) velocity.z -= direction.z * moveSpeed * 60.0 * delta;
If any forward/backward key is pressed, accelerate in that direction using moveSpeed
if (moveLeft || moveRight) velocity.x -= direction.x * moveSpeed * 60.0 * delta;
If any left/right key is pressed, accelerate in that direction
controls.moveRight(-velocity.x * delta);
Moves the camera right by the horizontal velocity (negative to invert the direction convention)
controls.moveForward(-velocity.z * delta);
Moves the camera forward by the forward velocity (negative to invert)
camera.position.x = Math.max(-49, Math.min(49, camera.position.x));
Clamps the camera's x position to stay between -49 and 49, preventing the player from leaving the arena
camera.position.z = Math.max(-49, Math.min(49, camera.position.z));
Clamps the camera's z position to stay within bounds
if (millis() - lastSpawnTime > spawnRate && enemies.length < maxEnemies) {
Checks if enough time has passed since the last spawn AND there are fewer enemies than the maximum
spawnPoubelle();
If both conditions are true, spawn a new enemy
lastSpawnTime = millis();
Updates the spawn timer so the next enemy won't spawn until spawnRate milliseconds have passed
prevTime = time;
Records the current time so delta can be calculated correctly on the next frame
renderer.render(scene, camera);
Renders the three.js scene to the canvas using the current camera view

createPoubelle()

createPoubelle() builds a poubelle (garbage can) as a three.js mesh by combining two cylinders (body and lid) into a single merged geometry. It demonstrates geometry merging for performance optimization: instead of rendering two separate meshes, the merged geometry renders as one, reducing draw calls and improving frameRate. After merging, the original geometries and materials are disposed to free memory. The final mesh is named 'poubelle' so the raycaster can identify hits.

function createPoubelle() {
  const bodyGeometry = new THREE.CylinderGeometry(0.5, 0.6, 1.0, 16);
  const bodyMaterial = new THREE.MeshLambertMaterial({ color: 0x444444 });
  const bodyMesh = new THREE.Mesh(bodyGeometry, bodyMaterial);
  bodyMesh.position.y = 0.5;

  const lidGeometry = new THREE.CylinderGeometry(0.65, 0.65, 0.1, 16);
  const lidMaterial = new THREE.MeshLambertMaterial({ color: 0x333333 });
  const lidMesh = new THREE.Mesh(lidGeometry, lidMaterial);
  lidMesh.position.y = 1.05;

  const bodyClone = bodyGeometry.clone();
  bodyClone.applyMatrix4(bodyMesh.matrix);
  const lidClone = lidGeometry.clone();
  lidClone.applyMatrix4(lidMesh.matrix);

  const mergedGeometry = THREE.BufferGeometryUtils.mergeBufferGeometries([bodyClone, lidClone]);
  const poubelleMaterial = new THREE.MeshLambertMaterial({ color: 0x444444 });
  const poubelle = new THREE.Mesh(mergedGeometry, poubelleMaterial);

  bodyGeometry.dispose();
  bodyMaterial.dispose();
  lidGeometry.dispose();
  lidMaterial.dispose();

  poubelle.name = 'poubelle';
  return poubelle;
}
Line-by-line explanation (21 lines)

🔧 Subcomponents:

function-call Garbage Can Body const bodyGeometry = new THREE.CylinderGeometry(0.5, 0.6, 1.0, 16);

Creates a tapered cylinder geometry for the can body (top radius 0.5, bottom radius 0.6)

function-call Garbage Can Lid const lidGeometry = new THREE.CylinderGeometry(0.65, 0.65, 0.1, 16);

Creates a flat cylinder geometry for the lid sitting on top of the body

calculation Merge Geometries const mergedGeometry = THREE.BufferGeometryUtils.mergeBufferGeometries([bodyClone, lidClone]);

Combines body and lid geometries into a single mesh for better performance

function-call Dispose Original Geometries bodyGeometry.dispose();

Frees memory from the original body geometry since it's been merged

calculation Object Naming poubelle.name = 'poubelle';

Assigns a name to the mesh for easy identification during raycasting hit detection

const bodyGeometry = new THREE.CylinderGeometry(0.5, 0.6, 1.0, 16);
Creates a cylinder with top radius 0.5, bottom radius 0.6 (tapered), height 1.0, and 16 segments for smoothness
const bodyMaterial = new THREE.MeshLambertMaterial({ color: 0x444444 });
Creates a dark gray material using Lambert shading (good for matte surfaces)
const bodyMesh = new THREE.Mesh(bodyGeometry, bodyMaterial);
Combines the geometry and material into a 3D mesh object
bodyMesh.position.y = 0.5;
Positions the body mesh so its bottom is at y=0 and top is at y=1
const lidGeometry = new THREE.CylinderGeometry(0.65, 0.65, 0.1, 16);
Creates a flat cylinder for the lid (both radii 0.65, height 0.1) slightly wider than the body
const lidMaterial = new THREE.MeshLambertMaterial({ color: 0x333333 });
Creates a slightly darker gray material for the lid
const lidMesh = new THREE.Mesh(lidGeometry, lidMaterial);
Combines lid geometry and material into a mesh
lidMesh.position.y = 1.05;
Positions the lid 1.05 units high (sitting on top of the body which ends at y=1)
const bodyClone = bodyGeometry.clone();
Clones the body geometry before merging (necessary because applyMatrix4 modifies the geometry)
bodyClone.applyMatrix4(bodyMesh.matrix);
Applies the body mesh's transformation matrix (position, rotation, scale) to the cloned geometry
const lidClone = lidGeometry.clone();
Clones the lid geometry for the same reason
lidClone.applyMatrix4(lidMesh.matrix);
Applies the lid mesh's transformation matrix to the cloned geometry
const mergedGeometry = THREE.BufferGeometryUtils.mergeBufferGeometries([bodyClone, lidClone]);
Merges the two transformed geometries into a single geometry for better rendering performance
const poubelleMaterial = new THREE.MeshLambertMaterial({ color: 0x444444 });
Creates a single material for the merged mesh
const poubelle = new THREE.Mesh(mergedGeometry, poubelleMaterial);
Combines the merged geometry and material into the final poubelle mesh
bodyGeometry.dispose();
Frees memory from the original body geometry since it's been merged into poubelleMaterial
bodyMaterial.dispose();
Frees memory from the original body material
lidGeometry.dispose();
Frees memory from the original lid geometry
lidMaterial.dispose();
Frees memory from the original lid material
poubelle.name = 'poubelle';
Names the mesh 'poubelle' so it can be identified during raycasting hit detection (checking if the shot hit a poubelle)
return poubelle;
Returns the finished poubelle mesh, ready to be added to the scene

spawnPoubelle()

spawnPoubelle() creates a new enemy by calling createPoubelle() and placing it at a random position within the arena bounds. It adds the poubelle to both the three.js scene (so it renders) and the enemies array (so it can be hit-detected and removed). This function is called periodically from animateThreeJS() based on the spawnRate variable.

function spawnPoubelle() {
  const poubelle = createPoubelle();

  const x = random(-40, 40);
  const z = random(-40, 40);
  poubelle.position.set(x, 0, z);

  scene.add(poubelle);
  enemies.push(poubelle);
}
Line-by-line explanation (6 lines)

🔧 Subcomponents:

function-call Create Poubelle Model const poubelle = createPoubelle();

Calls createPoubelle() to generate a new garbage can mesh

calculation Random Position Generation const x = random(-40, 40);

Generates a random x coordinate within the arena bounds

function-call Add to Scene scene.add(poubelle);

Adds the poubelle mesh to the three.js scene so it gets rendered

function-call Track in Enemies Array enemies.push(poubelle);

Adds the poubelle to the enemies array for raycasting hit detection and cleanup

const poubelle = createPoubelle();
Calls createPoubelle() to create a new garbage can mesh and store it in a variable
const x = random(-40, 40);
Generates a random x coordinate between -40 and 40 (near the center of the 100x100 arena)
const z = random(-40, 40);
Generates a random z coordinate between -40 and 40
poubelle.position.set(x, 0, z);
Sets the poubelle's position to the random x and z coordinates, with y=0 so it sits on the ground
scene.add(poubelle);
Adds the poubelle to the three.js scene, making it visible and part of the rendered world
enemies.push(poubelle);
Adds the poubelle to the enemies array so raycasting can detect hits on it, and so removeEnemy() can find and delete it later

shoot()

shoot() is called when the player clicks during gameplay. It plays a sound effect, casts a ray from the camera center, checks for intersections with enemies, and either awards points for a hit or deducts health for a miss. This is the core gameplay mechanic that drives the scoring and difficulty. The ray originates at (0,0), which is why the crosshair is drawn at the screen center.

function shoot() {
  shootSound.amp(0.5, 0.05);
  shootSound.amp(0, 0.2);

  raycaster.setFromCamera(new THREE.Vector2(0, 0), camera);

  const intersects = raycaster.intersectObjects(enemies);

  if (intersects.length > 0) {
    const target = intersects[0].object;
    if (target.name === 'poubelle') {
      score += 10;
      removeEnemy(target);
      console.log("Hit poubelle! Score:", score);
    }
  } else {
    health -= 5;
    if (health <= 0) {
      endGame();
    }
    console.log("Missed! Health:", health);
  }
}
Line-by-line explanation (15 lines)

🔧 Subcomponents:

function-call Play Shooting Sound shootSound.amp(0.5, 0.05);

Plays the shooting sound by jumping amplitude to 0.5 over 0.05 seconds

function-call Fade Out Sound shootSound.amp(0, 0.2);

Fades the sound back to silence over 0.2 seconds

function-call Setup Raycast raycaster.setFromCamera(new THREE.Vector2(0, 0), camera);

Creates a ray from the camera through the center of the screen (where the crosshair is)

function-call Check Intersections const intersects = raycaster.intersectObjects(enemies);

Tests which enemies the ray hits and returns them sorted by distance

conditional Hit or Miss Logic if (intersects.length > 0) {

Checks if the ray hit anything and handles hit vs miss cases differently

shootSound.amp(0.5, 0.05);
Plays the shooting sound by setting amplitude to 0.5 over 0.05 seconds (fast attack)
shootSound.amp(0, 0.2);
Fades the sound amplitude back to 0 over 0.2 seconds (release), creating a brief 'pew' sound
raycaster.setFromCamera(new THREE.Vector2(0, 0), camera);
Creates a ray that shoots from the camera through the center of the screen (0, 0 in normalized device coordinates)
const intersects = raycaster.intersectObjects(enemies);
Casts the ray against all objects in the enemies array and returns an array of intersections sorted by distance
if (intersects.length > 0) {
If the ray hit at least one object (array is not empty), it's a hit
const target = intersects[0].object;
Gets the first (closest) object hit by the ray
if (target.name === 'poubelle') {
Checks that the hit object is actually a poubelle (named 'poubelle' in createPoubelle)
score += 10;
Awards 10 points for the successful hit
removeEnemy(target);
Removes the hit poubelle from the scene and enemies array
console.log("Hit poubelle! Score:", score);
Logs the hit to the browser console for debugging
} else {
If no objects were hit, it's a miss
health -= 5;
Penalizes the player with 5 damage for missing
if (health <= 0) {
Checks if the health has dropped to zero or below
endGame();
If health is depleted, ends the game
console.log("Missed! Health:", health);
Logs the miss to the console for debugging

removeEnemy()

removeEnemy() cleans up a poubelle after it's been shot. It removes the mesh from the three.js scene, disposes its geometry and material to free GPU memory (preventing memory leaks), and removes it from the enemies array so it's no longer tracked for raycasting. Proper cleanup is critical in games where objects are constantly being created and destroyed.

function removeEnemy(enemy) {
  scene.remove(enemy);
  enemy.geometry.dispose();
  enemy.material.dispose();
  enemies = enemies.filter(e => e !== enemy);
}
Line-by-line explanation (4 lines)

🔧 Subcomponents:

function-call Remove from Scene scene.remove(enemy);

Removes the enemy mesh from the three.js scene so it stops rendering

function-call Free Geometry Memory enemy.geometry.dispose();

Deallocates GPU memory used by the enemy's geometry

function-call Free Material Memory enemy.material.dispose();

Deallocates GPU memory used by the enemy's material

function-call Remove from Tracking Array enemies = enemies.filter(e => e !== enemy);

Removes the enemy from the enemies array so it's no longer tracked for raycasting

scene.remove(enemy);
Removes the enemy mesh from the three.js scene, preventing it from being rendered
enemy.geometry.dispose();
Calls dispose on the enemy's geometry to free GPU memory (important for performance with many objects)
enemy.material.dispose();
Calls dispose on the enemy's material to free GPU memory
enemies = enemies.filter(e => e !== enemy);
Filters the enemies array, creating a new array with all enemies except the one being removed

windowResized()

windowResized() is a p5.js lifecycle function that p5.js calls automatically when the browser window is resized. It resizes both the p5 canvas (for UI) and the three.js renderer, and updates the camera's aspect ratio and projection. Properly handling window resize is essential for a responsive, fullscreen game experience.

function windowResized() {
  resizeCanvas(windowWidth, windowHeight);

  if (renderer) {
    renderer.setSize(windowWidth, windowHeight);
    camera.aspect = windowWidth / windowHeight;
    camera.updateProjectionMatrix();
  }
}
Line-by-line explanation (5 lines)

🔧 Subcomponents:

function-call Resize p5 Canvas resizeCanvas(windowWidth, windowHeight);

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

function-call Resize Renderer renderer.setSize(windowWidth, windowHeight);

Resizes the three.js renderer to match the new window dimensions

calculation Update Camera Aspect camera.aspect = windowWidth / windowHeight;

Updates the camera's aspect ratio to match the new window proportions

function-call Update Projection Matrix camera.updateProjectionMatrix();

Recalculates the camera's projection based on the new aspect ratio

resizeCanvas(windowWidth, windowHeight);
P5.js built-in function that resizes the p5 canvas to the new window dimensions
if (renderer) {
Checks if the three.js renderer has been initialized (to avoid errors if windowResized fires before setup)
renderer.setSize(windowWidth, windowHeight);
Resizes the three.js renderer's resolution to match the new window size
camera.aspect = windowWidth / windowHeight;
Recalculates the camera's aspect ratio (width/height) for the new window proportions
camera.updateProjectionMatrix();
Recalculates the camera's projection matrix based on the new aspect ratio, ensuring the perspective looks correct

📦 Key Variables

scene THREE.Scene

The three.js scene that contains all 3D objects (camera, lights, floor, poubelles)

scene = new THREE.Scene();
camera THREE.PerspectiveCamera

The player's perspective camera positioned at eye height (1.6 units) that defines what's visible in the 3D world

camera = new THREE.PerspectiveCamera(75, windowWidth / windowHeight, 0.1, 1000);
renderer THREE.WebGLRenderer

The WebGL renderer that draws the three.js scene to the HTML canvas

renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true });
controls THREE.PointerLockControls

FPS-style controls that handle pointer lock and first-person camera movement via WASD keys

controls = new THREE.PointerLockControls(camera, renderer.domElement);
raycaster THREE.Raycaster

Casts a ray from the camera center through the screen to detect shot hits on poubelles

raycaster = new THREE.Raycaster();
enemies array

Array holding all active poubelle meshes in the scene—used for raycasting hit detection and cleanup

let enemies = [];
moveSpeed number

Constant controlling how fast the player moves per frame (0.15 units per second)

const moveSpeed = 0.15;
moveForward, moveBackward, moveLeft, moveRight boolean

Flags tracking which movement keys (WASD) are currently pressed

let moveForward = false;
velocity THREE.Vector3

Three.js Vector3 storing the player's current velocity in x and z directions

const velocity = new THREE.Vector3();
direction THREE.Vector3

Three.js Vector3 storing the normalized direction the player is trying to move

const direction = new THREE.Vector3();
prevTime number

Timestamp of the previous animation frame, used to calculate delta time for frame-rate-independent movement

let prevTime = performance.now();
score number

The player's current score, increased by 10 for each successful hit

let score = 0;
health number

The player's health (starts at 100, decreases by 5 per miss, game ends at 0)

let health = 100;
gameState string

Current game state ('start', 'playing', or 'gameOver') that determines which UI to draw and what gameplay logic runs

let gameState = 'start';
maxEnemies number

Maximum number of poubelles allowed in the scene at once (15)

const maxEnemies = 15;
spawnRate number

Time in milliseconds between poubelle spawns (1500 ms = 1.5 seconds)

const spawnRate = 1500;
lastSpawnTime number

Timestamp of the last poubelle spawn, used to check if enough time has passed to spawn another

let lastSpawnTime = 0;
gameDuration number

Total game time in milliseconds (60000 = 60 seconds)

const gameDuration = 60000;
gameStartTime number

Timestamp when the game started, used to calculate remaining time for the countdown timer

let gameStartTime = 0;
p5Canvas p5.Renderer

The p5.js canvas used for drawing UI elements (crosshair, HUD, start/game-over screens)

let p5Canvas;
shootSound p5.Oscillator

p5.sound oscillator that plays a brief sawtooth-wave sound whenever the player shoots

let shootSound;

🔧 Potential Improvements (6)

Here are some ways this code could be enhanced:

BUG shoot() hit detection

If a player shoots while no poubelles exist, intersects.length === 0 is treated as a miss, penalizing the player for nothing existing to shoot

💡 Check if enemies.length > 0 before applying the miss penalty, or only penalize misses if at least one enemy exists

PERFORMANCE animateThreeJS() movement

Camera boundary clamping happens every frame even when the player isn't near boundaries, wasting computation

💡 Only clamp camera position if the new position would exceed bounds: if (camera.position.x > 49) camera.position.x = 49; etc.

FEATURE shoot()

No visual feedback (muzzle flash, screen shake, or impact effect) when the player shoots, reducing game feel

💡 Add a brief red flash or screen shake on hit, or a different visual on miss, to provide immediate tactile feedback

STYLE startGame()

Enemies cleanup loop creates temporary variables (enemy) for each disposal, adding unnecessary allocations

💡 Use a simpler pattern: for (let i = enemies.length - 1; i >= 0; i--) { removeEnemy(enemies[i]); } to avoid temporary variables

FEATURE createPoubelle()

All poubelles look identical with no variation in size, color, or difficulty

💡 Add randomized poubelle properties: vary color, scale, or even scoring multiplier to add visual interest and strategic depth

BUG onPointerLockChange()

The function has an empty if block when pointer lock is gained—no feedback to the player that the game is ready

💡 Add console.log or a brief visual indicator (flash on screen) when pointer lock is successfully acquired

🔄 Code Flow

Code flow showing setup, draw, drawstartscreen, drawcrosshair, drawhud, drawgameoverscreen, initthreejs, ondocumentclick, startgame, endgame, onpointerlockchange, onkeydown, onkeyup, animatethreejs, createpoubelle, spawnpoubelle, shoot, removeenemy, windowresized

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

graph TD start[Start] --> setup[setup] setup --> draw[draw loop] click setup href "#fn-setup" click draw href "#fn-draw" setup --> canvas-creation[Canvas Creation] setup --> canvas-positioning[Canvas Positioning] setup --> sound-init[Shoot Sound Initialization] setup --> event-listeners[Input Event Listeners] setup --> initthreejs[Initialize Three.js] click canvas-creation href "#sub-canvas-creation" click canvas-positioning href "#sub-canvas-positioning" click sound-init href "#sub-sound-init" click event-listeners href "#sub-event-listeners" click initthreejs href "#fn-initthreejs" draw --> canvas-clear[Clear Canvas] draw --> state-check[Game State Branch] draw --> drawstartscreen[Draw Start Screen] draw --> drawcrosshair[Draw Crosshair] draw --> drawhud[Draw HUD] draw --> drawgameoverscreen[Draw Game Over Screen] click canvas-clear href "#sub-canvas-clear" click state-check href "#sub-state-check" click drawstartscreen href "#fn-drawstartscreen" click drawcrosshair href "#fn-drawcrosshair" click drawhud href "#fn-drawhud" click drawgameoverscreen href "#fn-drawgameoverscreen" state-check -->|gameState === 'start'| drawstartscreen state-check -->|gameState === 'playing'| drawcrosshair state-check -->|gameState === 'gameOver'| drawgameoverscreen drawhud --> score-display[Score Text] drawhud --> health-display[Health Text] drawhud --> timer-calculation[Time Remaining Calculation] drawhud --> timer-display[Timer Text] drawhud --> time-up-check[Time Up End Game] click score-display href "#sub-score-display" click health-display href "#sub-health-display" click timer-calculation href "#sub-timer-calculation" click timer-display href "#sub-timer-display" click time-up-check href "#sub-time-up-check" drawcrosshair --> horizontal-line[Horizontal Crosshair Line] drawcrosshair --> vertical-line[Vertical Crosshair Line] click horizontal-line href "#sub-horizontal-line" click vertical-line href "#sub-vertical-line" drawgameoverscreen --> gameover-title[Game Over Title] drawgameoverscreen --> final-score[Final Score Display] drawgameoverscreen --> restart-prompt[Restart Instruction] click gameover-title href "#sub-gameover-title" click final-score href "#sub-final-score" click restart-prompt href "#sub-restart-prompt" initthreejs --> scene-creation[Scene Setup] initthreejs --> camera-setup[Camera Setup] initthreejs --> renderer-setup[Renderer Setup] initthreejs --> lighting[Lighting Setup] initthreejs --> floor-creation[Floor Geometry] initthreejs --> controls-setup[Pointer Lock Controls] initthreejs --> raycaster-setup[Raycaster Initialization] click scene-creation href "#sub-scene-creation" click camera-setup href "#sub-camera-setup" click renderer-setup href "#sub-renderer-setup" click lighting href "#sub-lighting" click floor-creation href "#sub-floor-creation" click controls-setup href "#sub-controls-setup" click raycaster-setup href "#sub-raycaster-setup" ondocumentclick --> start-check[Start/Restart Game] ondocumentclick --> shoot-check[Shoot Action] click start-check href "#sub-start-check" click shoot-check href "#sub-shoot-check" start-check -->|clicked| startgame[Start Game] start-check -->|clicked| endgame[End Game] startgame --> score-reset[Score Reset] startgame --> health-reset[Health Reset] startgame --> enemy-cleanup[Enemy Cleanup Loop] startgame --> enemy-array-clear[Clear Enemies Array] startgame --> timer-reset[Timer Reset] startgame --> state-transition[Game State Change] startgame --> pointer-lock-request[Request Pointer Lock] click score-reset href "#sub-score-reset" click health-reset href "#sub-health-reset" click enemy-cleanup href "#sub-enemy-cleanup" click enemy-array-clear href "#sub-enemy-array-clear" click timer-reset href "#sub-timer-reset" click state-transition href "#sub-state-transition" click pointer-lock-request href "#sub-pointer-lock-request" endgame --> state-change[Game Over State] endgame --> pointer-unlock[Release Pointer Lock] click state-change href "#sub-state-change" click pointer-unlock href "#sub-pointer-unlock" onpointerlockchange --> lock-check[Pointer Lock Status Check] lock-check --> unlock-response[Unlock Response] click lock-check href "#sub-lock-check" click unlock-response href "#sub-unlock-response" onkeydown --> switch-statement[Key Code Switch] onkeydown --> w-key[W Key Handler] onkeydown --> a-key[A Key Handler] onkeydown --> s-key[S Key Handler] onkeydown --> d-key[D Key Handler] click switch-statement href "#sub-switch-statement" click w-key href "#sub-w-key" click a-key href "#sub-a-key" click s-key href "#sub-s-key" click d-key href "#sub-d-key" onkeyup --> switch-statement[Key Code Switch] click switch-statement href "#sub-switch-statement" animatethreejs --> animation-frame-request[Request Next Frame] animatethreejs --> delta-time[Delta Time Calculation] animatethreejs --> friction-application[Friction/Damping] animatethreejs --> direction-calculation[Movement Direction] animatethreejs --> acceleration-application[Acceleration Application] animatethreejs --> camera-movement[Apply Movement to Camera] animatethreejs --> boundary-check[Boundary Clamping] animatethreejs --> spawn-check[Enemy Spawn Logic] animatethreejs --> render-call[Render Scene] click animation-frame-request href "#sub-animation-frame-request" click delta-time href "#sub-delta-time" click friction-application href "#sub-friction-application" click direction-calculation href "#sub-direction-calculation" click acceleration-application href "#sub-acceleration-application" click camera-movement href "#sub-camera-movement" click boundary-check href "#sub-boundary-check" click spawn-check href "#sub-spawn-check" click render-call href "#sub-render-call" spawnpoubelle --> model-creation[Create Poubelle Model] spawnpoubelle --> random-position[Random Position Generation] spawnpoubelle --> add-to-scene[Add to Scene] spawnpoubelle --> track-enemy[Track in Enemies Array] click model-creation href "#fn-createpoubelle" click random-position href "#sub-random-position" click add-to-scene href "#sub-add-to-scene" click track-enemy href "#sub-track-enemy" shoot --> sound-play[Play Shooting Sound] shoot --> raycast-setup[Setup Raycast] shoot --> intersection-check[Check Intersections] shoot --> hit-logic[Hit or Miss Logic] click sound-play href "#sub-sound-play" click raycast-setup href "#sub-raycast-setup" click intersection-check href "#sub-intersection-check" click hit-logic href "#sub-hit-logic" hit-logic -->|hit| removeenemy[Remove Enemy] hit-logic -->|miss| sound-fade[Fade Out Sound] click removeenemy href "#fn-removeenemy" click sound-fade href "#sub-sound-fade" removeenemy --> remove-from-scene[Remove from Scene] removeenemy --> dispose-geometry[Free Geometry Memory] removeenemy --> dispose-material[Free Material Memory] removeenemy --> remove-from-array[Remove from Tracking Array] click remove-from-scene href "#sub-remove-from-scene" click dispose-geometry href "#sub-dispose-geometry" click dispose-material href "#sub-dispose-material" click remove-from-array href "#sub-remove-from-array" windowresized --> p5-resize[Resize p5 Canvas] windowresized --> renderer-resize[Resize Renderer] windowresized --> camera-update[Update Camera Aspect] windowresized --> projection-update[Update Projection Matrix] click p5-resize href "#sub-p5-resize" click renderer-resize href "#sub-renderer-resize" click camera-update href "#sub-camera-update" click projection-update href "#sub-projection-update"

❓ Frequently Asked Questions

What visual experience does Sketch 2026-02-13 17:42 provide?

This sketch creates an immersive 3D environment where users navigate through a scene populated with dynamic objects, specifically 'poubelles' (trash bins), against a backdrop of changing gameplay elements.

How can users interact with the Sketch 2026-02-13 17:42?

Users can move their character using keyboard inputs to navigate through the scene, shoot at enemies, and manage their health and score during gameplay.

What creative coding techniques are showcased in this sketch?

The sketch demonstrates the integration of p5.js with three.js for 3D rendering, along with implementing game mechanics like player movement, enemy spawning, and real-time score tracking.

Preview

Sketch 2026-02-13 17:42 - p5.js creative coding sketch preview
Sketch Preview
Code flow diagram showing the structure of Sketch 2026-02-13 17:42 - Code flow showing setup, draw, drawstartscreen, drawcrosshair, drawhud, drawgameoverscreen, initthreejs, ondocumentclick, startgame, endgame, onpointerlockchange, onkeydown, onkeyup, animatethreejs, createpoubelle, spawnpoubelle, shoot, removeenemy, windowresized
Code Flow Diagram