Sketch 2026-03-06 23:20

This is a 3D first-person shooter game built with three.js where players shoot banana projectiles at 'Among Us' characters across 15 progressively harder rounds. The game combines FPS controls, collision detection, AI pathfinding, and procedural sound effects to create an interactive combat experience.

🧪 Try This!

Experiment with the code by making these changes:

  1. Make enemies faster to start — Increase the initial speed in the spawnAmongUsCharacters() function so round 1 is more challenging
  2. Change enemy color to blue — In the AmongUsCharacter3D class, change the body material from red (0xff0000) to blue (0x0000ff)
  3. Double the shooting speed — Reduce the fire rate (reloadTime) so the player can shoot twice as fast
  4. Paint the floor green — Change the floor color from grey (0x808080) to bright green (0x00FF00) in initThreeJS()
  5. Award more points per kill — In updateBananaProjectiles(), increase the points awarded from 10 to 25 per enemy
Prefer the full editor? Open it there →

📖 About This Sketch

This sketch creates a complete 3D first-person shooter game called 'Escape Benson Island.' Players move through a house-like environment using WASD keys, aim with the mouse, and fire banana projectiles to defeat red 'Among Us' characters that patrol and chase them. The game combines three major p5.js and three.js techniques: procedural sound synthesis using p5.sound oscillators and PolySynths for shoot, hit, win, and lose sounds; three.js 3D rendering with meshes, lighting, and materials; and real-time collision detection using bounding boxes for player-wall, enemy-wall, projectile-wall, and projectile-enemy interactions.

The code is organized into five major sections: game state management (tracking rounds, score, and UI), audio setup using p5.sound, three.js scene initialization with lighting and geometry, FPS controls with keyboard movement and mouse look, and game logic that spawns enemies, updates their AI, handles shooting mechanics, and detects collisions. By reading this sketch, you will learn how to combine p5.js (primarily for audio) with three.js (for 3D graphics), implement an FPS camera controller with pointer lock, create procedurally generated enemy AI that chases the player, fire projectiles from a first-person viewpoint, and manage complex game state across multiple screens and rounds.

⚙️ How It Works

  1. When the sketch loads, preload() initializes procedural sounds by creating p5.Oscillator objects (sine and triangle waves) and p5.PolySynth objects for multi-note win and lose sounds, all started in silent state. setup() creates a minimal 1×1 p5.js canvas to establish the audio context, calls userStartAudio() to enable sound playback, initializes the three.js renderer and scene with sky-blue background, cameras, ambient and directional lighting, creates a floor and room walls using BoxGeometry, spawns the player's invisible collision sphere and yellow banana weapon mesh, and starts the animation loop.
  2. The game displays a START_SCREEN UI prompting the player to click. When clicked, gameState changes to 'PLAYING', the pointer locks to enable FPS controls, and spawnAmongUsCharacters() populates the scene with 5 + (currentRound * 2) red enemy characters at random spawn positions.
  3. Every frame, the animate() function calculates delta time for frame-rate-independent movement. If gameState is 'PLAYING', updateFPSControls() moves the camera based on WASD key presses and mouse movement (cameraYaw and cameraPitch), performs collision detection with walls, and keeps the player collision mesh synchronized with the camera.
  4. updateAmongUsCharacters() moves each enemy toward the player's position using simple vector subtraction, handles enemy-wall collisions by reverting to the previous position, and checks if any enemy has touched the player—if so, gameState becomes 'GAME_OVER' and a three-note losing sound plays.
  5. When the mouse is clicked during gameplay, playerShoot() creates a new cylinder mesh representing a banana projectile at the camera position, gives it a velocity in the camera's forward direction, and plays a short shoot sound. updateBananaProjectiles() updates each projectile's position every frame, removes it if it travels beyond 200 units or hits a wall, and when a projectile hits an enemy, both are destroyed, the hit sound plays, the score increases by 10, and the enemy is removed from the scene.
  6. When all enemies are defeated, gameState becomes 'ROUND_WON' (or 'GAME_WON' if round 15 is complete), a three-note winning sound plays, and clicking advances to the next round with more and faster enemies. The game ends in 'GAME_WON' or 'GAME_OVER' states, both of which can be reset by clicking to return to the START_SCREEN.

🎓 Concepts You'll Learn

3D rendering with three.jsProcedural audio synthesis with p5.soundFPS camera controls and pointer lockBounding box collision detectionAI pathfinding and chasing behaviorGame state machinesVector math for direction and velocityMesh geometry and materialsFrame-rate-independent movement (delta time)

📝 Code Breakdown

preload()

preload() runs before setup() and is the ideal place to load sounds, images, or other assets. Using p5.Oscillator and p5.PolySynth allows us to create procedural sounds without needing external audio files—perfect for keeping the sketch self-contained. The amp(0) and stop() calls ensure sounds are silent until explicitly triggered during gameplay.

function preload() {
  shootSound = new p5.Oscillator('sine');
  shootSound.freq(800);
  shootSound.amp(0);
  shootSound.stop();

  hitSound = new p5.Oscillator('triangle');
  hitSound.freq(400);
  hitSound.amp(0);
  hitSound.stop();

  winSound = new p5.PolySynth();
  winSound.amp(0);
  winSound.stop();

  loseSound = new p5.PolySynth();
  loseSound.amp(0);
  loseSound.stop();
}
Line-by-line explanation (8 lines)

🔧 Subcomponents:

calculation Shoot sound oscillator shootSound = new p5.Oscillator('sine');

Creates a sine-wave oscillator for the weapon fire sound

calculation Hit sound oscillator hitSound = new p5.Oscillator('triangle');

Creates a triangle-wave oscillator for enemy hit feedback

calculation Win sound polyphonic synth winSound = new p5.PolySynth();

Creates a multi-note synthesizer for round/game win sounds

shootSound = new p5.Oscillator('sine');
Creates a new oscillator that generates a sine-wave (smooth, pure tone) for the shooting sound
shootSound.freq(800);
Sets the oscillator's frequency to 800 Hz, a fairly high pitch suitable for a weapon fire sound
shootSound.amp(0);
Starts the oscillator completely silent (amplitude 0) so it only makes noise when explicitly triggered
shootSound.stop();
Ensures the oscillator is in stopped state before the sketch begins playing any sounds
hitSound = new p5.Oscillator('triangle');
Creates a triangle-wave oscillator (slightly brighter than sine) for the hit feedback sound
hitSound.freq(400);
Sets the hit sound frequency to 400 Hz, lower than the shoot sound to provide sonic contrast
winSound = new p5.PolySynth();
Creates a PolySynth object that can play multiple notes simultaneously, enabling the three-note win arpeggio
loseSound = new p5.PolySynth();
Creates a PolySynth object for the three-note losing sound, using the same multi-note capability

setup()

setup() runs once when the sketch loads. This sketch uses setup() to initialize both p5.js (audio context) and three.js (3D graphics), then delegates rendering to the animate() function. The 1×1 canvas is a clever approach that lets p5.js handle only sound while three.js handles all visuals.

function setup() {
  createCanvas(1, 1);
  userStartAudio();
  uiDiv = document.getElementById('ui-overlay');
  updateUI();
  initThreeJS();
  initFPSControls();
  animate();
}
Line-by-line explanation (7 lines)

🔧 Subcomponents:

calculation Canvas initialization createCanvas(1, 1);

Creates a minimal p5.js canvas for audio context only—three.js renders to a separate canvas

calculation Audio context initialization userStartAudio();

Enables p5.sound's audio context so procedural sounds can play on user interaction

createCanvas(1, 1);
Creates a 1×1 pixel canvas purely to initialize p5.js—the actual game renders via three.js in a larger canvas added to the DOM
userStartAudio();
Required call that activates the Web Audio context, allowing p5.sound to produce audio after user interaction (browser security requirement)
uiDiv = document.getElementById('ui-overlay');
Grabs the HTML element where game UI text (score, round number, instructions) will be displayed
updateUI();
Populates the UI div with the initial START_SCREEN message
initThreeJS();
Builds the entire 3D scene: renderer, camera, lights, floor, walls, player mesh, and weapon mesh
initFPSControls();
Attaches keyboard and mouse event listeners for FPS movement and shooting
animate();
Starts the three.js animation loop via requestAnimationFrame, which runs continuously at ~60 FPS

draw()

In this sketch, draw() is kept but left empty. Normally in p5.js, draw() is where all animation happens—but since we're using three.js for 3D rendering, we delegate that responsibility to the animate() function, which uses requestAnimationFrame instead of p5's frame loop. The p5.js canvas is only needed to provide the audio context for p5.sound.

function draw() {
  // This p5.js draw function is now minimal, as three.js handles the main rendering.
  // We keep it to maintain the p5.js sketch structure and ensure p5.sound continues working.
  // The three.js renderer will cover this canvas.
  // background(173, 216, 230);
}
Line-by-line explanation (1 lines)
// This p5.js draw function is now minimal...
The draw() function is intentionally left empty because three.js's animate() loop (called from setup) handles all frame-by-frame rendering, making the p5.js draw loop unnecessary

spawnAmongUsCharacters()

spawnAmongUsCharacters() is called when the game starts and after each round is won. It uses random() to create varied spawn positions, making each game feel different. The scaling formula (5 + currentRound * 2) is a simple yet effective difficulty curve—observe how the game feels increasingly challenging as you progress through 15 rounds.

🔬 These lines pick random spawn positions in a 80×80 unit square. What happens if you change -40, 40 to -20, 20 to force enemies to spawn closer to the player?

    const x = random(-40, 40);
    const z = random(-40, 40);
function spawnAmongUsCharacters() {
  const numCharacters = 5 + currentRound * 2;
  const characterSpeed = 1 + currentRound * 0.2;
  for (let i = 0; i < numCharacters; i++) {
    const x = random(-40, 40);
    const z = random(-40, 40);
    const y = PLAYER_HEIGHT/2;
    amongUsCharacters.push(new AmongUsCharacter3D(x, y, z, characterSpeed));
  }
}
Line-by-line explanation (7 lines)

🔧 Subcomponents:

for-loop Enemy spawning loop for (let i = 0; i < numCharacters; i++) {

Iterates numCharacters times, creating a new enemy each iteration at a random position

const numCharacters = 5 + currentRound * 2;
Calculates how many enemies to spawn: starts at 5, then adds 2 more for each round completed, making the game progressively harder
const characterSpeed = 1 + currentRound * 0.2;
Calculates enemy movement speed: starts at 1 unit/frame, then increases by 0.2 each round, making enemies faster as you progress
for (let i = 0; i < numCharacters; i++) {
Loop that repeats numCharacters times, creating a new enemy on each iteration
const x = random(-40, 40);
Picks a random x position between -40 and 40 (horizontal), keeping enemies within the game room
const z = random(-40, 40);
Picks a random z position between -40 and 40 (depth), ensuring enemies spawn at varied distances
const y = PLAYER_HEIGHT/2;
Sets y to half the player height (0.9 units), positioning the enemy's center roughly at ground level where it looks natural
amongUsCharacters.push(new AmongUsCharacter3D(x, y, z, characterSpeed));
Creates a new AmongUsCharacter3D object at the calculated position with the current round's speed, then adds it to the array

mousePressed()

mousePressed() is a p5.js built-in function that fires whenever the user clicks the mouse. This sketch delegates to handleInput() to keep logic centralized, but during gameplay the onMouseDown() event listener (defined in initFPSControls) is what actually handles shots.

function mousePressed() {
  handleInput();
}
Line-by-line explanation (1 lines)
handleInput();
Calls the handleInput() function, which checks the current gameState and performs the appropriate action (start game, shoot, advance round, etc.)

touchStarted()

touchStarted() is the p5.js equivalent of mousePressed() for touch devices (mobile, tablets). By returning false, we prevent the browser from acting on the touch event, giving the game complete control.

function touchStarted() {
  handleInput();
  return false;
}
Line-by-line explanation (2 lines)
handleInput();
Calls the input handler, enabling touch input to start the game or advance rounds (same as mouse click)
return false;
Prevents the browser's default touch behavior (like scrolling or zooming), ensuring touches only trigger game actions

handleInput()

handleInput() is the central dispatcher for all input events. By checking gameState, it ensures that clicks do different things depending on whether the player is on the menu, playing, or viewing a game-over screen. This pattern—using a state variable to control behavior—is fundamental to game development and makes code much easier to organize and debug.

🔬 These three lines initialize game state when starting. What if you changed currentRound = 1 to currentRound = 10 to start on a hard round?

    gameState = 'PLAYING';
    currentRound = 1;
    score = 0;
function handleInput() {
  if (gameState === 'START_SCREEN') {
    gameState = 'PLAYING';
    currentRound = 1;
    score = 0;
    amongUsCharacters = [];
    amongUsMeshes = [];
    bananaProjectileMeshes = [];
    spawnAmongUsCharacters();
    document.body.requestPointerLock();
  } else if (gameState === 'ROUND_WON') {
    currentRound++;
    amongUsCharacters = [];
    amongUsMeshes = [];
    bananaProjectileMeshes = [];
    spawnAmongUsCharacters();
    gameState = 'PLAYING';
    document.body.requestPointerLock();
  } else if (gameState === 'GAME_OVER' || gameState === 'GAME_WON') {
    gameState = 'START_SCREEN';
    document.exitPointerLock();
  } else if (gameState === 'PLAYING' && pointerLocked) {
    playerShoot();
  }
  updateUI();
}
Line-by-line explanation (17 lines)

🔧 Subcomponents:

conditional Start screen handler if (gameState === 'START_SCREEN') {

Begins the game, resets all variables, spawns enemies, and locks pointer for FPS controls

conditional Round won handler } else if (gameState === 'ROUND_WON') {

Advances to the next round, clears old enemies, spawns new harder ones, and re-locks pointer

conditional Game end handler } else if (gameState === 'GAME_OVER' || gameState === 'GAME_WON') {

Returns to START_SCREEN and unlocks pointer for menu interaction

conditional Shoot handler } else if (gameState === 'PLAYING' && pointerLocked) {

Fires a projectile if the game is active and pointer is locked (during gameplay only)

if (gameState === 'START_SCREEN') {
Checks if we're on the title screen; if so, start the game
gameState = 'PLAYING';
Transitions to the playing state, triggering gameplay logic
currentRound = 1;
Resets the round counter to 1 for a fresh game
score = 0;
Resets the score to 0
amongUsCharacters = [];
Clears the array of enemy objects, removing references to old enemies
amongUsMeshes = [];
Clears the array of enemy three.js meshes, preparing for fresh enemy visuals
bananaProjectileMeshes = [];
Clears any leftover projectile meshes from the previous game
spawnAmongUsCharacters();
Creates 5 new enemies (plus scaling) at random spawn points
document.body.requestPointerLock();
Locks the mouse cursor to the window and enables mouse movement events, activating FPS controls
} else if (gameState === 'ROUND_WON') {
If a round was just won, increment the round and prepare the next one
currentRound++;
Advances to the next round (e.g., from round 1 to round 2)
} else if (gameState === 'GAME_OVER' || gameState === 'GAME_WON') {
If the game ended (win or loss), return to the start screen
gameState = 'START_SCREEN';
Transitions back to the title screen UI
document.exitPointerLock();
Unlocks the mouse so the player can click menu buttons freely
} else if (gameState === 'PLAYING' && pointerLocked) {
If currently playing and the mouse is locked, treat the input as a shoot command
playerShoot();
Fires a banana projectile from the camera's current position and direction
updateUI();
Updates the displayed UI text to reflect the new game state

windowResized()

windowResized() is a p5.js built-in function called whenever the browser window is resized. In this sketch, it ensures the three.js renderer and camera stay properly sized and proportioned on mobile devices or when users resize their desktop window. Without this, the game would look stretched or have black bars on the sides.

function windowResized() {
  if (renderer) {
    renderer.setSize(window.innerWidth, window.innerHeight);
    camera.aspect = window.innerWidth / window.innerHeight;
    camera.updateProjectionMatrix();
  }
}
Line-by-line explanation (4 lines)
if (renderer) {
Checks that the three.js renderer exists before attempting to resize it (prevents errors on initial page load)
renderer.setSize(window.innerWidth, window.innerHeight);
Resizes the three.js canvas to match the window dimensions when the browser window is resized
camera.aspect = window.innerWidth / window.innerHeight;
Updates the camera's aspect ratio to match the new window proportions, preventing stretched or squished visuals
camera.updateProjectionMatrix();
Tells the camera to recalculate its internal projection matrix based on the new aspect ratio

initThreeJS()

initThreeJS() is the foundation of the entire 3D environment. It creates the renderer (draws to the screen), scene (3D container), camera (player's viewpoint), lights (for visibility and mood), floor, walls, player mesh, and weapon mesh. Understanding this function means understanding how three.js transforms code into a navigable 3D world.

function initThreeJS() {
  renderer = new THREE.WebGLRenderer({ antialias: true });
  renderer.setSize(window.innerWidth, window.innerHeight);
  document.body.appendChild(renderer.domElement);

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

  camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
  camera.position.set(0, PLAYER_HEIGHT, 0);

  const ambientLight = new THREE.AmbientLight(0xffffff, 0.7);
  scene.add(ambientLight);
  const directionalLight = new THREE.DirectionalLight(0xffffff, 0.5);
  directionalLight.position.set(5, 10, 7);
  scene.add(directionalLight);

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

  createWall(0, PLAYER_HEIGHT/2, -50, 100, PLAYER_HEIGHT, 2);
  createWall(0, PLAYER_HEIGHT/2, 50, 100, PLAYER_HEIGHT, 2);
  createWall(-50, PLAYER_HEIGHT/2, 0, 2, PLAYER_HEIGHT, 100);
  createWall(50, PLAYER_HEIGHT/2, 0, 2, PLAYER_HEIGHT, 100);

  createWall(0, PLAYER_HEIGHT/2, 0, 2, PLAYER_HEIGHT, 50);
  createWall(-25, PLAYER_HEIGHT/2, 25, 50, PLAYER_HEIGHT, 2);
  createWall(25, PLAYER_HEIGHT/2, -25, 50, PLAYER_HEIGHT, 2);

  const playerGeometry = new THREE.SphereGeometry(1, 8, 8);
  const playerMaterial = new THREE.MeshBasicMaterial({ color: 0x00ff00, wireframe: true, transparent: true, opacity: 0 });
  player = new THREE.Mesh(playerGeometry, playerMaterial);
  player.position.set(0, PLAYER_HEIGHT, 0);
  scene.add(player);

  const bananaGeometry = new THREE.CylinderGeometry(0.5, 0.7, 5, 8);
  const bananaMaterial = new THREE.MeshLambertMaterial({ color: 0xffff00 });
  playerBananaMesh = new THREE.Mesh(bananaGeometry, bananaMaterial);
  camera.add(playerBananaMesh);
  playerBananaMesh.position.set(3, -2, -5);
  playerBananaMesh.rotation.z = Math.PI / 4;
}
Line-by-line explanation (23 lines)

🔧 Subcomponents:

calculation WebGL renderer setup renderer = new THREE.WebGLRenderer({ antialias: true });

Creates a three.js WebGL renderer with antialiasing enabled for smooth edges

calculation Scene creation scene = new THREE.Scene();

Creates an empty 3D scene container that will hold all game objects (lights, meshes, camera)

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

Creates a perspective camera with 75° field of view, matching window aspect ratio, and rendering objects between 0.1 and 1000 units away

calculation Lighting setup const ambientLight = new THREE.AmbientLight(0xffffff, 0.7);

Adds ambient light that evenly illuminates all surfaces from all directions, preventing pitch-black shadows

calculation Floor mesh const floor = new THREE.Mesh(floorGeometry, floorMaterial);

Creates a 100×100 unit grey floor plane to define the game space's ground level

for-loop Wall spawning createWall(0, PLAYER_HEIGHT/2, -50, 100, PLAYER_HEIGHT, 2);

Creates the outer and inner walls of the game room by calling createWall() multiple times with different parameters

calculation Player collision sphere player = new THREE.Mesh(playerGeometry, playerMaterial);

Creates an invisible collision mesh for the player's body, used to detect hits from enemies

calculation Player weapon mesh playerBananaMesh = new THREE.Mesh(bananaGeometry, bananaMaterial);

Creates the yellow banana weapon visible in the player's hand/first-person view

renderer = new THREE.WebGLRenderer({ antialias: true });
Creates a WebGL renderer (uses GPU to draw 3D graphics) with antialiasing enabled for smooth, non-jagged edges
renderer.setSize(window.innerWidth, window.innerHeight);
Sets the renderer to cover the entire browser window
document.body.appendChild(renderer.domElement);
Adds the renderer's canvas to the HTML page, making it visible to the player
scene = new THREE.Scene();
Creates an empty 3D scene—a container that will hold the camera, lights, and all game objects
scene.background = new THREE.Color(0x87CEEB);
Sets the scene's background color to sky blue (hex color 0x87CEEB), giving a pleasant outdoor atmosphere
camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
Creates a perspective camera (mimics human vision with depth). 75° is the field of view, 0.1–1000 is the visible range in units
camera.position.set(0, PLAYER_HEIGHT, 0);
Places the camera at the center of the room (x=0, z=0) and 1.8 units up (eye height), so the player sees from the correct perspective
const ambientLight = new THREE.AmbientLight(0xffffff, 0.7);
Creates ambient light (white, brightness 0.7) that softly illuminates all surfaces equally, preventing pure black shadows
const directionalLight = new THREE.DirectionalLight(0xffffff, 0.5);
Creates a directional light (like sunlight) from a specific direction, adding highlights and shadows for depth
directionalLight.position.set(5, 10, 7);
Positions the directional light in space so shadows fall naturally across the scene
const floorGeometry = new THREE.PlaneGeometry(100, 100);
Creates a 100×100 unit flat plane geometry—the blueprint for the floor mesh
const floorMaterial = new THREE.MeshLambertMaterial({ color: 0x808080 });
Creates a material (visual surface) with grey color and Lambert shading (matte, not shiny), suitable for a floor
floor.rotation.x = -Math.PI / 2;
Rotates the plane 90 degrees around the x-axis, turning it from vertical to horizontal so it lies flat as a floor
floor.position.y = 0;
Places the floor at y=0, the ground level of the game world
createWall(0, PLAYER_HEIGHT/2, -50, 100, PLAYER_HEIGHT, 2);
Creates the back wall of the room: centered at (0, 0.9, -50), 100 units wide, 1.8 units tall, and 2 units thick
const playerGeometry = new THREE.SphereGeometry(1, 8, 8);
Creates a sphere geometry (1 unit radius, 8×8 segments) as a collision shape for the player's body
const playerMaterial = new THREE.MeshBasicMaterial({ color: 0x00ff00, wireframe: true, transparent: true, opacity: 0 });
Creates an invisible material: wireframe shows the mesh structure if ever visible, transparent and opacity 0 ensures it's never seen
player.position.set(0, PLAYER_HEIGHT, 0);
Positions the player collision sphere at the world origin, 1.8 units up (matching camera height)
const bananaGeometry = new THREE.CylinderGeometry(0.5, 0.7, 5, 8);
Creates a banana-like shape: a cylinder 5 units long, thinner at top (0.5) and thicker at bottom (0.7), with 8 vertical segments
const bananaMaterial = new THREE.MeshLambertMaterial({ color: 0xffff00 });
Creates a yellow material for the banana with Lambert shading, giving it a matte appearance
camera.add(playerBananaMesh);
Makes the banana mesh a child of the camera, so it moves and rotates with the player's head—a first-person weapon view
playerBananaMesh.position.set(3, -2, -5);
Positions the banana 3 units right, 2 units down, and 5 units forward in the camera's local space (visible in the player's hand)
playerBananaMesh.rotation.z = Math.PI / 4;
Tilts the banana 45 degrees (π/4 radians), giving it a natural held-gun appearance

createWall()

createWall() is a helper function that abstracts the process of creating a 3D box-shaped wall. By parametrizing position and dimensions, it avoids code repetition and makes the wall layout easy to modify. Each wall is added to the walls array so that collision detection can loop through and check for player/enemy/projectile collisions.

function createWall(x, y, z, width, height, depth) {
  const wallGeometry = new THREE.BoxGeometry(width, height, depth);
  const wallMaterial = new THREE.MeshLambertMaterial({ color: 0xaaaaaa });
  const wall = new THREE.Mesh(wallGeometry, wallMaterial);
  wall.position.set(x, y, z);
  scene.add(wall);
  walls.push(wall);
}
Line-by-line explanation (6 lines)
const wallGeometry = new THREE.BoxGeometry(width, height, depth);
Creates a box-shaped geometry (rectangular solid) with the given dimensions
const wallMaterial = new THREE.MeshLambertMaterial({ color: 0xaaaaaa });
Creates a grey material using Lambert shading, which looks matte and natural for stone/concrete walls
const wall = new THREE.Mesh(wallGeometry, wallMaterial);
Combines the geometry and material into a complete 3D mesh object
wall.position.set(x, y, z);
Places the wall at the specified (x, y, z) coordinates in 3D space
scene.add(wall);
Adds the wall mesh to the scene so it will be rendered each frame
walls.push(wall);
Adds the wall to the walls array for later collision detection checking

initFPSControls()

initFPSControls() sets up all the input handlers that make the FPS feel responsive. By attaching listeners directly to the document, this sketch captures inputs from anywhere on the page, not just within a specific element. The five event types work together to manage movement (keydown/keyup), shooting (mousedown), and cursor behavior (pointerlockchange/error).

function initFPSControls() {
  document.addEventListener('keydown', onKeyDown, false);
  document.addEventListener('keyup', onKeyUp, false);
  document.addEventListener('mousedown', onMouseDown, false);
  document.addEventListener('pointerlockchange', onPointerlockChange, false);
  document.addEventListener('pointerlockerror', onPointerlockError, false);
}
Line-by-line explanation (5 lines)
document.addEventListener('keydown', onKeyDown, false);
Registers a listener for keydown events, calling onKeyDown() whenever a key is pressed
document.addEventListener('keyup', onKeyUp, false);
Registers a listener for keyup events, calling onKeyUp() whenever a key is released
document.addEventListener('mousedown', onMouseDown, false);
Registers a listener for mouse clicks, calling onMouseDown() whenever the mouse button is pressed
document.addEventListener('pointerlockchange', onPointerlockChange, false);
Listens for pointer lock state changes (when the mouse locks or unlocks), triggering onPointerlockChange()
document.addEventListener('pointerlockerror', onPointerlockError, false);
Listens for pointer lock errors (if the browser denies the lock request), triggering onPointerlockError()

onKeyDown()

onKeyDown() sets movement flags that are read every frame by updateFPSControls(). The flags remain true until the key is released, allowing smooth, continuous movement as long as the player holds WASD keys. Using event.code ensures consistency across keyboard layouts worldwide.

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 (5 lines)

🔧 Subcomponents:

switch-case Keyboard input mapping switch (event.code) {

Maps each key to a movement direction by checking the event code and setting the corresponding movement flag

switch (event.code) {
Checks which key was pressed by looking at event.code (the physical key code), which is more reliable than event.key for international keyboards
case 'KeyW': moveForward = true;
If the W key is pressed, sets moveForward to true, signaling that the player wants to move forward
case 'KeyA': moveLeft = true;
If the A key is pressed, sets moveLeft to true
case 'KeyS': moveBackward = true;
If the S key is pressed, sets moveBackward to true
case 'KeyD': moveRight = true;
If the D key is pressed, sets moveRight to true

onKeyUp()

onKeyUp() complements onKeyDown() by clearing the movement flags when keys are released. This dual-flag approach is better than checking the current key state every frame, as it's more responsive and handles multi-key presses (e.g., moving forward-left) naturally.

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 (4 lines)

🔧 Subcomponents:

switch-case Key release mapping switch (event.code) {

Clears movement flags when keys are released, stopping movement in that direction

case 'KeyW': moveForward = false;
When W is released, sets moveForward to false, stopping forward movement
case 'KeyA': moveLeft = false;
When A is released, stops leftward movement
case 'KeyS': moveBackward = false;
When S is released, stops backward movement
case 'KeyD': moveRight = false;
When D is released, stops rightward movement

onMouseDown()

onMouseDown() acts as a context-aware input handler: during gameplay it shoots, on menus it advances the game. The pointerLocked check is crucial—if the pointer is unlocked (mouse visible), clicks should only affect menus, not trigger shots in the game world.

function onMouseDown(event) {
  if (gameState === 'PLAYING' && pointerLocked) {
    playerShoot();
  } else if (gameState === 'START_SCREEN' || gameState === 'ROUND_WON' || gameState === 'GAME_OVER' || gameState === 'GAME_WON') {
    handleInput();
  }
}
Line-by-line explanation (4 lines)

🔧 Subcomponents:

conditional Gameplay shooting if (gameState === 'PLAYING' && pointerLocked) {

Fires a projectile when the player clicks during active gameplay (only if pointer is locked)

if (gameState === 'PLAYING' && pointerLocked) {
Checks if the game is active AND the mouse is locked (meaning the player is focused on the game canvas)
playerShoot();
Fires a banana projectile from the camera's position and direction
} else if (gameState === 'START_SCREEN' || gameState === 'ROUND_WON' || gameState === 'GAME_OVER' || gameState === 'GAME_WON') {
If we're on any menu screen, treat the click as a menu interaction
handleInput();
Calls handleInput(), which starts the game, advances to the next round, or returns to the menu depending on the current state

onPointerlockChange()

onPointerlockChange() fires whenever pointer lock is acquired or released (by the player pressing ESC or by a script calling exitPointerLock). This function synchronizes the pointerLocked flag and intelligently adds/removes the mousemove listener only when the pointer is locked, improving efficiency and clarity.

function onPointerlockChange() {
  if (document.pointerLockElement === renderer.domElement) {
    console.log('Pointer locked');
    pointerLocked = true;
    document.addEventListener('mousemove', onMouseMove, false);
  } else {
    console.log('Pointer unlocked');
    pointerLocked = false;
    document.removeEventListener('mousemove', onMouseMove, false);
  }
  updateUI();
}
Line-by-line explanation (7 lines)

🔧 Subcomponents:

conditional Pointer lock status check if (document.pointerLockElement === renderer.domElement) {

Determines whether pointer lock is currently active by checking if the renderer canvas has the lock

if (document.pointerLockElement === renderer.domElement) {
Checks if the mouse pointer is currently locked to the three.js canvas; if so, we're in gameplay mode
pointerLocked = true;
Sets the local flag to true, indicating the mouse is locked
document.addEventListener('mousemove', onMouseMove, false);
Attaches the mousemove listener now that the pointer is locked, enabling camera rotation from mouse movement
} else {
If the pointer is not locked...
pointerLocked = false;
Sets the flag to false, indicating the mouse is free
document.removeEventListener('mousemove', onMouseMove, false);
Removes the mousemove listener to prevent camera rotation when the pointer is unlocked (on menus)
updateUI();
Updates the UI display (the UI changes based on pointer lock state)

onPointerlockError()

onPointerlockError() handles the rare case where the browser denies a pointer lock request (e.g., permissions, browser settings). Logging and updating the UI helps the player understand what went wrong and can try again.

function onPointerlockError() {
  console.error('Pointer lock error');
  updateUI();
}
Line-by-line explanation (2 lines)
console.error('Pointer lock error');
Logs an error message to the browser console for debugging—useful if pointer lock fails for technical reasons
updateUI();
Updates the UI so the player knows the lock request failed (they can try again)

onMouseMove()

onMouseMove() is the core of FPS camera controls. event.movementX and event.movementY give the delta (change) in mouse position since the last frame, making it frame-rate independent. The yaw/pitch system and rotation order prevent gimbal lock—a crucial detail that separates smooth FPS controls from broken ones.

🔬 These lines multiply mouse movement by mouseSensitivity. What happens if you change the subtraction (-=) to addition (+=)?

    cameraYaw -= event.movementX * mouseSensitivity;
    cameraPitch -= event.movementY * mouseSensitivity;
function onMouseMove(event) {
  if (pointerLocked) {
    cameraYaw -= event.movementX * mouseSensitivity;
    cameraPitch -= event.movementY * mouseSensitivity;

    cameraPitch = Math.max(-Math.PI / 2, Math.min(Math.PI / 2, cameraPitch));

    camera.rotation.order = 'YXZ';
    camera.rotation.y = cameraYaw;
    camera.rotation.x = cameraPitch;
  }
}
Line-by-line explanation (7 lines)

🔧 Subcomponents:

calculation Horizontal camera rotation cameraYaw -= event.movementX * mouseSensitivity;

Rotates the camera left/right (yaw) based on horizontal mouse movement

calculation Vertical camera rotation cameraPitch -= event.movementY * mouseSensitivity;

Rotates the camera up/down (pitch) based on vertical mouse movement

calculation Pitch constraint cameraPitch = Math.max(-Math.PI / 2, Math.min(Math.PI / 2, cameraPitch));

Prevents the camera from rotating more than 90° up or down, stopping the player from flipping upside down

if (pointerLocked) {
Only processes mouse movement if the pointer is locked (in gameplay mode)
cameraYaw -= event.movementX * mouseSensitivity;
Subtracts the horizontal mouse movement (multiplied by sensitivity) from cameraYaw, rotating the camera left/right. Negative because moving the mouse right should look right (yaw increases)
cameraPitch -= event.movementY * mouseSensitivity;
Subtracts vertical mouse movement from cameraPitch, rotating the camera up/down. Moving the mouse down tilts the camera down (pitch decreases)
cameraPitch = Math.max(-Math.PI / 2, Math.min(Math.PI / 2, cameraPitch));
Constrains cameraPitch to a range of -90° to +90° (−π/2 to +π/2 radians), preventing neck-twisting camera flips
camera.rotation.order = 'YXZ';
Sets the rotation order to YXZ, meaning yaw (Y) is applied first, then pitch (X). This order prevents gimbal lock (a rotation quirk) in first-person cameras
camera.rotation.y = cameraYaw;
Applies the calculated yaw rotation to the camera's Y axis (left/right turn)
camera.rotation.x = cameraPitch;
Applies the calculated pitch rotation to the camera's X axis (up/down look)

updateFPSControls()

updateFPSControls() runs every frame and implements the complete FPS movement system: reading key presses as directions, applying acceleration and friction for smooth motion, moving the camera, updating the collision mesh, and checking for wall collisions with bounding box overlap tests. The friction system makes movement feel responsive without being twitchy, and the collision detection prevents walking through walls.

🔬 These lines convert boolean key presses into a normalized movement direction. What happens if you remove the direction.normalize() call—will diagonal movement feel faster?

  direction.z = Number(moveForward) - Number(moveBackward);
  direction.x = Number(moveRight) - Number(moveLeft);
  direction.normalize();
function updateFPSControls(delta) {
  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 * PLAYER_SPEED * delta;
  if (moveLeft || moveRight) velocity.x -= direction.x * PLAYER_SPEED * delta;

  const prevPosition = camera.position.clone();

  camera.translateX(velocity.x * delta);
  camera.translateZ(velocity.z * delta);

  player.position.copy(camera.position);

  const playerBBox = new THREE.Box3().setFromObject(player);

  for (const wall of walls) {
    const wallBBox = new THREE.Box3().setFromObject(wall);
    if (playerBBox.intersectsBox(wallBBox)) {
      camera.position.copy(prevPosition);
      player.position.copy(prevPosition);
      break;
    }
  }
}
Line-by-line explanation (18 lines)

🔧 Subcomponents:

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

Gradually slows the player to a stop when no keys are pressed, creating natural, responsive movement

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

Combines forward/backward input into a single direction value (-1 for back, 0 for neither, 1 for forward)

conditional Velocity updates if (moveForward || moveBackward) velocity.z -= direction.z * PLAYER_SPEED * delta;

Accelerates the player in the desired direction when movement keys are pressed

conditional Wall collision check if (playerBBox.intersectsBox(wallBBox)) {

Detects whether the player collided with a wall and reverts movement if so

velocity.x -= velocity.x * 10.0 * delta;
Applies friction to the player's x (left/right) velocity. Each frame, velocity decreases by 10 times itself (scaled by delta time), causing smooth deceleration
velocity.z -= velocity.z * 10.0 * delta;
Applies friction to the z (forward/backward) velocity in the same way
direction.z = Number(moveForward) - Number(moveBackward);
Converts boolean flags into a number: if forward=true and backward=false, result is 1; if both false, result is 0; if backward=true, result is -1
direction.x = Number(moveRight) - Number(moveLeft);
Similar logic for left/right: 1 for right, -1 for left, 0 for neutral
direction.normalize();
Scales the direction vector so its length is 1, ensuring constant speed in all directions (prevents diagonal movement from being faster)
if (moveForward || moveBackward) velocity.z -= direction.z * PLAYER_SPEED * delta;
If any forward/backward key is pressed, adds acceleration in that direction to the velocity. PLAYER_SPEED controls how quickly the player speeds up
if (moveLeft || moveRight) velocity.x -= direction.x * PLAYER_SPEED * delta;
Similar acceleration logic for left/right movement
const prevPosition = camera.position.clone();
Saves the camera's current position so we can revert to it if a collision occurs
camera.translateX(velocity.x * delta);
Moves the camera horizontally (left/right) by the current x velocity
camera.translateZ(velocity.z * delta);
Moves the camera forward/backward by the current z velocity
player.position.copy(camera.position);
Updates the player's collision mesh to match the camera position so collision detection works correctly
const playerBBox = new THREE.Box3().setFromObject(player);
Calculates the bounding box (axis-aligned box) around the player's collision mesh
for (const wall of walls) {
Loops through all walls in the scene to check for collisions
const wallBBox = new THREE.Box3().setFromObject(wall);
Calculates the bounding box around the wall
if (playerBBox.intersectsBox(wallBBox)) {
Checks if the two bounding boxes overlap, indicating a collision
camera.position.copy(prevPosition);
If collision detected, reverts the camera to its previous position (before the move)
player.position.copy(prevPosition);
Also reverts the collision mesh to its previous position
break;
Stops checking further walls once a collision is found (optimization: no need to check all walls if one blocks the path)

playerShoot()

playerShoot() is the core shooting mechanic. It checks the fire rate, plays a sound, creates a projectile mesh at the camera position pointing in the camera's direction, stores a velocity for movement, and adds the projectile to the scene and tracking array. The sound and projectile rotation both use the camera's orientation, creating a cohesive first-person weapon feel.

function playerShoot() {
  if (millis() - lastShotTime > reloadTime) {
    shootSound.amp(0.5, 0.05);
    shootSound.start();
    shootSound.amp(0, 0.2);

    const projectileGeometry = new THREE.CylinderGeometry(0.5, 0.7, 5, 8);
    const projectileMaterial = new THREE.MeshLambertMaterial({ color: 0xffff00 });
    const projectileMesh = new THREE.Mesh(projectileGeometry, projectileMaterial);

    const cameraDirection = new THREE.Vector3();
    camera.getWorldDirection(cameraDirection);
    projectileMesh.position.copy(camera.position).add(cameraDirection.multiplyScalar(5));

    projectileMesh.velocity = cameraDirection.clone().normalize().multiplyScalar(bananaProjectileSpeed);

    projectileMesh.rotation.z = Math.PI / 4;
    projectileMesh.rotation.x = camera.rotation.x;
    projectileMesh.rotation.y = camera.rotation.y;

    scene.add(projectileMesh);
    bananaProjectileMeshes.push(projectileMesh);
    lastShotTime = millis();
  }
}
Line-by-line explanation (17 lines)

🔧 Subcomponents:

conditional Fire rate limiting if (millis() - lastShotTime > reloadTime) {

Ensures the player cannot fire faster than the reload time allows, preventing instant machine-gun spam

calculation Shoot sound effect shootSound.amp(0.5, 0.05);

Plays the procedural shoot sound by ramping up amplitude, then fading out

calculation Projectile mesh creation const projectileMesh = new THREE.Mesh(projectileGeometry, projectileMaterial);

Creates the visible banana projectile that will travel through the scene

calculation Projectile direction camera.getWorldDirection(cameraDirection);

Gets the camera's forward direction so the projectile travels where the player is looking

if (millis() - lastShotTime > reloadTime) {
Checks if enough time (reloadTime milliseconds) has passed since the last shot. If not, the function returns early and no shot fires
shootSound.amp(0.5, 0.05);
Ramps the shoot sound's amplitude to 0.5 over 0.05 seconds, creating a quick attack to the sound
shootSound.start();
Starts playing the oscillator
shootSound.amp(0, 0.2);
Fades the sound's amplitude back to 0 over 0.2 seconds, creating a brief 'pop' effect
const projectileGeometry = new THREE.CylinderGeometry(0.5, 0.7, 5, 8);
Creates a banana-shaped cylinder (same shape as the player's weapon) for the projectile
const projectileMaterial = new THREE.MeshLambertMaterial({ color: 0xffff00 });
Creates a yellow material for the projectile
const projectileMesh = new THREE.Mesh(projectileGeometry, projectileMaterial);
Combines the geometry and material into a complete mesh
const cameraDirection = new THREE.Vector3();
Creates an empty vector that will store the camera's forward direction
camera.getWorldDirection(cameraDirection);
Fills the cameraDirection vector with the camera's forward direction in world space
projectileMesh.position.copy(camera.position).add(cameraDirection.multiplyScalar(5));
Places the projectile 5 units in front of the camera. .copy() sets position to camera position, .add() moves it 5 units forward
projectileMesh.velocity = cameraDirection.clone().normalize().multiplyScalar(bananaProjectileSpeed);
Creates a velocity vector: clones the direction, ensures it's unit length via normalize(), then scales by projectile speed
projectileMesh.rotation.z = Math.PI / 4;
Tilts the projectile 45 degrees (π/4 radians) so it looks naturally rotated
projectileMesh.rotation.x = camera.rotation.x;
Rotates the projectile to match the camera's pitch (up/down look angle)
projectileMesh.rotation.y = camera.rotation.y;
Rotates the projectile to match the camera's yaw (left/right look angle)
scene.add(projectileMesh);
Adds the projectile to the 3D scene so it will be rendered
bananaProjectileMeshes.push(projectileMesh);
Adds the projectile to the tracking array so updateBananaProjectiles() can update it each frame
lastShotTime = millis();
Records the current time as the last shot time, used to enforce the reload delay on the next shot

animate()

animate() is the master game loop. It uses requestAnimationFrame for optimal frame timing, calculates delta time for frame-rate-independent movement, renders the scene, and runs all gameplay logic if the game is active. The win condition check (all enemies defeated) is elegant: simply check if the array is empty. This is the heartbeat of the entire game.

🔬 These three lines run every frame during gameplay to update movement, AI, and projectiles. What happens if you comment out the second line (AI updates)?

    updateFPSControls(delta);
    updateAmongUsCharacters(delta);
    updateBananaProjectiles(delta);
function animate() {
  requestAnimationFrame(animate);

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

  renderer.render(scene, camera);

  if (gameState === 'PLAYING') {
    updateFPSControls(delta);
    updateAmongUsCharacters(delta);
    updateBananaProjectiles(delta);

    if (amongUsCharacters.length === 0) {
      if (currentRound < MAX_ROUNDS) {
        gameState = 'ROUND_WON';
        winSound.amp(0.3, 0.1);
        winSound.play('C5', 0.1, 0, 0.1);
        winSound.play('E5', 0.1, 0.1, 0.1);
        winSound.play('G5', 0.1, 0.2, 0.1);
        winSound.amp(0, 0.3, 0.3);
      } else {
        gameState = 'GAME_WON';
        winSound.amp(0.5, 0.1);
        winSound.play('C5', 0.1, 0, 0.1);
        winSound.play('E5', 0.1, 0.1, 0.1);
        winSound.play('G5', 0.1, 0.2, 0.1);
        winSound.play('C6', 0.1, 0.3, 0.1);
        winSound.amp(0, 0.5, 0.4);
      }
      document.exitPointerLock();
      updateUI();
    }
  }

  updateUI();
}
Line-by-line explanation (22 lines)

🔧 Subcomponents:

calculation Animation loop recursion requestAnimationFrame(animate);

Schedules animate() to run again on the next browser frame, creating a continuous game loop

calculation Frame delta time const delta = (time - prevTime) / 1000;

Calculates how many seconds have passed since the last frame, enabling frame-rate-independent movement

calculation Scene rendering renderer.render(scene, camera);

Draws the entire 3D scene to the screen using the current camera view

conditional Gameplay logic execution if (gameState === 'PLAYING') {

Runs movement, enemy AI, and projectile updates only during active gameplay

conditional Round/game completion if (amongUsCharacters.length === 0) {

Checks if all enemies are defeated and advances the round or declares game victory

requestAnimationFrame(animate);
Schedules this function to run again on the next frame (typically ~60 times per second), creating the continuous animation loop
const time = performance.now();
Gets the current high-resolution timestamp in milliseconds
const delta = (time - prevTime) / 1000;
Calculates the time elapsed since the last frame in seconds. Dividing by 1000 converts milliseconds to seconds
prevTime = time;
Stores the current time so we can calculate delta on the next frame
renderer.render(scene, camera);
Draws the 3D scene from the camera's perspective to the canvas
if (gameState === 'PLAYING') {
Only runs gameplay logic (movement, AI, projectiles) if the game is active
updateFPSControls(delta);
Updates player movement, rotation, and collision detection
updateAmongUsCharacters(delta);
Updates all enemy positions, AI behavior, and checks if they've hit the player
updateBananaProjectiles(delta);
Updates projectile positions and checks collisions with walls and enemies
if (amongUsCharacters.length === 0) {
Checks if all enemies have been defeated (array is empty)
if (currentRound < MAX_ROUNDS) {
If this isn't the final round, transition to ROUND_WON state
gameState = 'ROUND_WON';
Sets state to ROUND_WON, triggering the UI to show the round completion screen
winSound.amp(0.3, 0.1);
Ramps the win sound amplitude to 0.3 over 0.1 seconds for the attack
winSound.play('C5', 0.1, 0, 0.1);
Plays a C5 note for 0.1 seconds starting immediately
winSound.play('E5', 0.1, 0.1, 0.1);
Plays an E5 note (third of the chord) starting 0.1 seconds after the C5
winSound.play('G5', 0.1, 0.2, 0.1);
Plays a G5 note (fifth of the chord) starting 0.2 seconds after C5, completing a major chord
winSound.amp(0, 0.3, 0.3);
Fades the win sound to silence over 0.3 seconds with a delay of 0.3 seconds
} else {
If all rounds are complete...
gameState = 'GAME_WON';
Transitions to GAME_WON state, showing the final victory screen
winSound.play('C6', 0.1, 0.3, 0.1);
Adds a high C6 note to celebrate the complete victory, extending the arpeggio
document.exitPointerLock();
Unlocks the pointer so the player can click the restart button
updateUI();
Updates the UI to display the round/game completion message

updateAmongUsCharacters()

updateAmongUsCharacters() updates all enemies each frame and checks if any have caught the player. The reverse loop pattern is a best practice when removing items during iteration (though no removals happen here, it's future-proofed). The lose sound is a descending arpeggio (C-A-F) which sounds 'sad' and contrasts with the ascending win arpeggio (C-E-G), providing clear audio feedback.

function updateAmongUsCharacters(delta) {
  for (let i = amongUsCharacters.length - 1; i >= 0; i--) {
    amongUsCharacters[i].update(delta, player.position);

    const bboxAmongUs = new THREE.Box3().setFromObject(amongUsCharacters[i].mesh);
    const bboxPlayer = new THREE.Box3().setFromObject(player);

    if (bboxAmongUs.intersectsBox(bboxPlayer)) {
      gameState = 'GAME_OVER';
      loseSound.amp(0.3, 0.1);
      loseSound.play('C5', 0.1, 0, 0.1);
      loseSound.play('A4', 0.1, 0.1, 0.1);
      loseSound.play('F4', 0.1, 0.2, 0.1);
      loseSound.amp(0, 0.3, 0.3);
      document.exitPointerLock();
      updateUI();
      return;
    }
  }
}
Line-by-line explanation (14 lines)

🔧 Subcomponents:

for-loop Reverse iteration for (let i = amongUsCharacters.length - 1; i >= 0; i--) {

Loops through enemies backwards, safe for removing items during iteration

conditional Enemy-player collision if (bboxAmongUs.intersectsBox(bboxPlayer)) {

Detects if any enemy has reached the player, triggering game over

for (let i = amongUsCharacters.length - 1; i >= 0; i--) {
Loops through all enemies in reverse (from last to first). Reverse iteration is safe if items are removed during the loop
amongUsCharacters[i].update(delta, player.position);
Calls the update method on each enemy, passing delta time and the player's position so they can chase
const bboxAmongUs = new THREE.Box3().setFromObject(amongUsCharacters[i].mesh);
Calculates the bounding box around the enemy mesh
const bboxPlayer = new THREE.Box3().setFromObject(player);
Calculates the bounding box around the player mesh
if (bboxAmongUs.intersectsBox(bboxPlayer)) {
Checks if the two bounding boxes overlap—if so, the enemy has reached the player
gameState = 'GAME_OVER';
Transitions to GAME_OVER state, ending the game
loseSound.amp(0.3, 0.1);
Ramps the lose sound amplitude to 0.3 for a dramatic entrance
loseSound.play('C5', 0.1, 0, 0.1);
Plays a C5 note, starting the 'sad' descending arpeggio
loseSound.play('A4', 0.1, 0.1, 0.1);
Plays a lower A4 note, continuing the descending pitch to sound sad
loseSound.play('F4', 0.1, 0.2, 0.1);
Plays an even lower F4 note, completing the descending sad chord
loseSound.amp(0, 0.3, 0.3);
Fades the lose sound to silence
document.exitPointerLock();
Unlocks the pointer so the player can click to restart
updateUI();
Updates the UI to display the game over message
return;
Exits the function early to avoid further enemy updates (the game is over)

updateBananaProjectiles()

updateBananaProjectiles() is the projectile system. Each frame it moves projectiles, culls those that are too far away, checks collisions with walls and enemies, and cleans up memory when objects are destroyed. The pattern of checking collisions in sequence (distance → walls → enemies) with early exits via flags and continue is efficient: once a projectile hits something, we immediately stop checking and move to the next one.

🔬 This code checks if the projectile hit any wall. What happens if you change intersectsBox to a different method name (try distanceToPoint)?

    const projectileBBox = new THREE.Box3().setFromObject(projectile);
    let hitWall = false;
    for (const wall of walls) {
      const wallBBox = new THREE.Box3().setFromObject(wall);
      if (projectileBBox.intersectsBox(wallBBox)) {
function updateBananaProjectiles(delta) {
  for (let i = bananaProjectileMeshes.length - 1; i >= 0; i--) {
    const projectile = bananaProjectileMeshes[i];
    projectile.position.add(projectile.velocity.clone().multiplyScalar(delta));

    if (projectile.position.length() > 200) {
      scene.remove(projectile);
      projectile.geometry.dispose();
      projectile.material.dispose();
      bananaProjectileMeshes.splice(i, 1);
      continue;
    }

    const projectileBBox = new THREE.Box3().setFromObject(projectile);
    let hitWall = false;
    for (const wall of walls) {
      const wallBBox = new THREE.Box3().setFromObject(wall);
      if (projectileBBox.intersectsBox(wallBBox)) {
        scene.remove(projectile);
        projectile.geometry.dispose();
        projectile.material.dispose();
        bananaProjectileMeshes.splice(i, 1);
        hitWall = true;
        break;
      }
    }
    if (hitWall) continue;

    let hitAmongUs = false;
    for (let j = amongUsCharacters.length - 1; j >= 0; j--) {
      const amongUs = amongUsCharacters[j];
      const amongUsBBox = new THREE.Box3().setFromObject(amongUs.mesh);

      if (projectileBBox.intersectsBox(amongUsBBox)) {
        hitSound.amp(0.3, 0.05);
        hitSound.start();
        hitSound.amp(0, 0.1);

        score += 10;
        scene.remove(projectile);
        projectile.geometry.dispose();
        projectile.material.dispose();
        bananaProjectileMeshes.splice(i, 1);

        scene.remove(amongUs.mesh);
        amongUs.mesh.geometry.dispose();
        amongUs.mesh.material.dispose();
        amongUsCharacters.splice(j, 1);
        amongUsMeshes = amongUsMeshes.filter(mesh => mesh !== amongUs.mesh);

        hitAmongUs = true;
        break;
      }
    }
    if (hitAmongUs) continue;
  }
}
Line-by-line explanation (37 lines)

🔧 Subcomponents:

calculation Projectile position update projectile.position.add(projectile.velocity.clone().multiplyScalar(delta));

Moves each projectile forward based on its velocity and the time elapsed

conditional Projectile distance limit if (projectile.position.length() > 200) {

Removes projectiles that travel too far (200 units) to prevent infinite memory use

conditional Projectile-wall collision if (projectileBBox.intersectsBox(wallBBox)) {

Detects if a projectile hit a wall and removes it

conditional Projectile-enemy collision if (projectileBBox.intersectsBox(amongUsBBox)) {

Detects if a projectile hit an enemy, destroying both and awarding points

for (let i = bananaProjectileMeshes.length - 1; i >= 0; i--) {
Loops through all projectiles in reverse, safe for removing items
const projectile = bananaProjectileMeshes[i];
Gets the current projectile for easier reference
projectile.position.add(projectile.velocity.clone().multiplyScalar(delta));
Moves the projectile forward by multiplying its velocity by delta time, ensuring frame-rate-independent motion
if (projectile.position.length() > 200) {
Checks if the projectile has traveled more than 200 units from the origin (arbitrary culling distance)
scene.remove(projectile);
Removes the projectile mesh from the 3D scene so it's no longer rendered
projectile.geometry.dispose();
Frees the projectile's geometry memory (best practice for memory cleanup)
projectile.material.dispose();
Frees the projectile's material memory
bananaProjectileMeshes.splice(i, 1);
Removes the projectile from the tracking array
continue;
Skips the rest of the loop iteration (no need to check collisions for a removed projectile)
const projectileBBox = new THREE.Box3().setFromObject(projectile);
Calculates the projectile's bounding box for collision detection
let hitWall = false;
Flag to track if this projectile hit a wall
for (const wall of walls) {
Loops through all walls to check for collision
const wallBBox = new THREE.Box3().setFromObject(wall);
Calculates the wall's bounding box
if (projectileBBox.intersectsBox(wallBBox)) {
Checks if the projectile's bounding box overlaps the wall's bounding box
hitWall = true;
Sets the flag to true so we skip enemy collision checks
break;
Stops checking further walls (projectile is destroyed)
if (hitWall) continue;
If the projectile hit a wall, skip the rest of the loop (no need to check enemies)
for (let j = amongUsCharacters.length - 1; j >= 0; j--) {
Loops through all enemies in reverse, checking for collision with this projectile
const amongUs = amongUsCharacters[j];
Gets the current enemy for easier reference
const amongUsBBox = new THREE.Box3().setFromObject(amongUs.mesh);
Calculates the enemy's bounding box
if (projectileBBox.intersectsBox(amongUsBBox)) {
Checks if the projectile and enemy bounding boxes overlap
hitSound.amp(0.3, 0.05);
Ramps the hit sound amplitude to 0.3 for audio feedback
hitSound.start();
Plays the hit sound
hitSound.amp(0, 0.1);
Fades the hit sound to silence over 0.1 seconds
score += 10;
Awards 10 points for hitting an enemy
scene.remove(projectile);
Removes the projectile from the scene
projectile.geometry.dispose();
Frees projectile geometry memory
projectile.material.dispose();
Frees projectile material memory
bananaProjectileMeshes.splice(i, 1);
Removes the projectile from the tracking array
scene.remove(amongUs.mesh);
Removes the enemy mesh from the scene
amongUs.mesh.geometry.dispose();
Frees enemy geometry memory
amongUs.mesh.material.dispose();
Frees enemy material memory
amongUsCharacters.splice(j, 1);
Removes the enemy from the character tracking array
amongUsMeshes = amongUsMeshes.filter(mesh => mesh !== amongUs.mesh);
Removes the enemy mesh from the mesh tracking array using filter()
hitAmongUs = true;
Sets the flag to true so we don't process more enemies for this projectile
break;
Stops checking further enemies (projectile is destroyed)
if (hitAmongUs) continue;
If the projectile hit an enemy, skip the rest of this iteration

updateUI()

updateUI() is the presentation layer. It doesn't contain game logic—it purely reads gameState, score, and currentRound, then displays appropriate text. This separation keeps the code clean: game logic is in animate(), updateFPSControls(), etc., and presentation is isolated in updateUI(). The switch statement makes it trivial to add new game states (e.g., PAUSED, SETTINGS) by just adding a new case.

function updateUI() {
  let uiText = '';
  switch (gameState) {
    case 'START_SCREEN':
      uiText = "<h1>Escape Benson Island</h1><p>Shoot Among Us characters!</p><p>Click anywhere to start!</p>";
      if (!pointerLocked) {
        uiText += "<p><em>(Click anywhere to lock mouse and play)</em></p>";
      }
      break;
    case 'PLAYING':
      uiText = `Round: ${currentRound}/${MAX_ROUNDS}<br>Score: ${score}<br>WASD to move, Mouse to look, Click to shoot`;
      break;
    case 'ROUND_WON':
      uiText = `<h2>Round ${currentRound} Complete!</h2><p>Score: ${score}</p><p>Click to continue to next round!</p>`;
      break;
    case 'GAME_WON':
      uiText = `<h2>CONGRATULATIONS!</h2><p>YOU ESCAPED BENSON ISLAND!</p><p>Final Score: ${score}</p><p>Click to play again!</p>`;
      break;
    case 'GAME_OVER':
      uiText = `<h2>GAME OVER!</h2><p>Final Score: ${score}</p><p>Click to try again!</p>`;
      break;
  }
  uiDiv.innerHTML = uiText;
}
Line-by-line explanation (15 lines)

🔧 Subcomponents:

switch-case UI state rendering switch (gameState) {

Changes the displayed UI text based on the current game state

let uiText = '';
Initializes an empty string that will hold the HTML UI text
switch (gameState) {
Checks the current game state and builds appropriate UI text for that state
case 'START_SCREEN':
If the game hasn't started yet...
uiText = "<h1>Escape Benson Island</h1><p>Shoot Among Us characters!</p><p>Click anywhere to start!</p>";
Sets the title, game description, and start instruction
if (!pointerLocked) {
If the pointer is not currently locked...
uiText += "<p><em>(Click anywhere to lock mouse and play)</em></p>";
Adds a helpful hint about locking the mouse
case 'PLAYING':
During active gameplay...
uiText = `Round: ${currentRound}/${MAX_ROUNDS}<br>Score: ${score}<br>WASD to move, Mouse to look, Click to shoot`;
Displays the current round, score, and gameplay controls
case 'ROUND_WON':
After defeating all enemies in a round...
uiText = `<h2>Round ${currentRound} Complete!</h2><p>Score: ${score}</p><p>Click to continue to next round!</p>`;
Shows the round completion message and invitation to continue
case 'GAME_WON':
After completing all 15 rounds...
uiText = `<h2>CONGRATULATIONS!</h2><p>YOU ESCAPED BENSON ISLAND!</p><p>Final Score: ${score}</p><p>Click to play again!</p>`;
Shows a celebratory victory message with the final score
case 'GAME_OVER':
If an enemy reaches the player...
uiText = `<h2>GAME OVER!</h2><p>Final Score: ${score}</p><p>Click to try again!</p>`;
Shows the game over message and invitation to retry
uiDiv.innerHTML = uiText;
Updates the HTML element with the appropriate UI text

AmongUsCharacter3D class

AmongUsCharacter3D is a class that represents a single enemy. The constructor sets up properties and creates the visual mesh from three geometric parts grouped together. The update() method, called every frame, calculates the direction to the player and moves the enemy toward them while avoiding walls—a simple but effective chasing AI. The lookAt() call makes the enemy visually orient toward the player, adding personality to the enemies.

class AmongUsCharacter3D {
  constructor(x, y, z, speed) {
    this.x = x;
    this.y = y;
    this.z = z;
    this.speed = speed;
    this.mesh = this.createMesh();
    scene.add(this.mesh);
    amongUsMeshes.push(this.mesh);
  }

  createMesh() {
    const bodyGeometry = new THREE.CylinderGeometry(2, 2, 8, 8);
    const bodyMaterial = new THREE.MeshLambertMaterial({ color: 0xff0000 });
    const bodyMesh = new THREE.Mesh(bodyGeometry, bodyMaterial);
    bodyMesh.position.y = 4;

    const backpackGeometry = new THREE.BoxGeometry(1.5, 4, 1);
    const backpackMaterial = new THREE.MeshLambertMaterial({ color: 0x880000 });
    const backpackMesh = new THREE.Mesh(backpackGeometry, backpackMaterial);
    backpackMesh.position.set(0, 4, 2);

    const visorGeometry = new THREE.SphereGeometry(1.5, 16, 8, 0, Math.PI);
    visorGeometry.scale(1, 0.5, 1);
    const visorMaterial = new THREE.MeshLambertMaterial({ color: 0x99ccff });
    const visorMesh = new THREE.Mesh(visorGeometry, visorMaterial);
    visorMesh.position.set(0, 5, 1.5);

    const group = new THREE.Group();
    group.add(bodyMesh);
    group.add(backpackMesh);
    group.add(visorMesh);
    group.position.set(this.x, this.y, this.z);
    return group;
  }

  update(delta, playerPosition) {
    const directionToPlayer = new THREE.Vector3().subVectors(playerPosition, this.mesh.position);
    directionToPlayer.y = 0;
    directionToPlayer.normalize();

    const prevPosition = this.mesh.position.clone();

    this.mesh.position.x += directionToPlayer.x * this.speed * delta;
    this.mesh.position.z += directionToPlayer.z * this.speed * delta;

    this.mesh.lookAt(playerPosition.x, this.mesh.position.y, playerPosition.z);

    for (const wall of walls) {
      const bboxAmongUs = new THREE.Box3().setFromObject(this.mesh);
      const bboxWall = new THREE.Box3().setFromObject(wall);
      if (bboxAmongUs.intersectsBox(bboxWall)) {
        this.mesh.position.copy(prevPosition);
        break;
      }
    }
  }
}
Line-by-line explanation (35 lines)

🔧 Subcomponents:

calculation Constructor initialization constructor(x, y, z, speed) {

Sets up position and speed, creates the visual mesh, and adds it to the scene

calculation Mesh creation createMesh() {

Builds the 3D enemy character from three parts: body (cylinder), backpack (box), and visor (half-sphere)

calculation Enemy update update(delta, playerPosition) {

Each frame, moves the enemy toward the player and handles wall collisions

constructor(x, y, z, speed) {
Called when a new AmongUsCharacter3D is created, initializing the enemy at position (x, y, z) with a given speed
this.x = x;
Stores the x position (not used after initialization, but good practice)
this.speed = speed;
Stores the speed value, used in the update() method to control how fast the enemy chases
this.mesh = this.createMesh();
Calls createMesh() to build the visual 3D representation and stores it in this.mesh
scene.add(this.mesh);
Adds the mesh to the three.js scene so it will be rendered each frame
amongUsMeshes.push(this.mesh);
Adds the mesh to the global tracking array for reference in other functions
const bodyGeometry = new THREE.CylinderGeometry(2, 2, 8, 8);
Creates a cylinder (2 unit radius, 8 units tall) for the enemy's main body
const bodyMaterial = new THREE.MeshLambertMaterial({ color: 0xff0000 });
Creates a red material for the body
const bodyMesh = new THREE.Mesh(bodyGeometry, bodyMaterial);
Combines the geometry and material into a mesh
bodyMesh.position.y = 4;
Positions the body 4 units up so its bottom sits on the ground (the cylinder extends from y=0 to y=8)
const backpackGeometry = new THREE.BoxGeometry(1.5, 4, 1);
Creates a box-shaped backpack (1.5 units wide, 4 units tall, 1 unit deep)
const backpackMaterial = new THREE.MeshLambertMaterial({ color: 0x880000 });
Creates a dark red material for the backpack
backpackMesh.position.set(0, 4, 2);
Positions the backpack at y=4 (middle of the body) and z=2 (behind the body)
const visorGeometry = new THREE.SphereGeometry(1.5, 16, 8, 0, Math.PI);
Creates a half-sphere (0 to π radians covers the top hemisphere) for the visor
visorGeometry.scale(1, 0.5, 1);
Flattens the hemisphere vertically (y scale 0.5) to make it look more like a helmet visor
visorMesh.position.set(0, 5, 1.5);
Positions the visor at the top front of the head
const group = new THREE.Group();
Creates a container (Group) to hold all three parts as one object
group.add(bodyMesh);
Adds the body to the group
group.add(backpackMesh);
Adds the backpack to the group
group.add(visorMesh);
Adds the visor to the group
group.position.set(this.x, this.y, this.z);
Positions the entire group at the spawn coordinates
return group;
Returns the group (containing all three parts) to be stored as this.mesh
const directionToPlayer = new THREE.Vector3().subVectors(playerPosition, this.mesh.position);
Calculates a vector pointing from the enemy toward the player
directionToPlayer.y = 0;
Clears the y component so the enemy only moves horizontally (doesn't try to jump at the player)
directionToPlayer.normalize();
Scales the direction vector to unit length (magnitude 1) so speed is consistent in all directions
const prevPosition = this.mesh.position.clone();
Saves the current position so we can revert if a wall collision occurs
this.mesh.position.x += directionToPlayer.x * this.speed * delta;
Moves the enemy horizontally toward the player by multiplying the direction by speed and delta time
this.mesh.position.z += directionToPlayer.z * this.speed * delta;
Moves the enemy forward/backward toward the player in the same way
this.mesh.lookAt(playerPosition.x, this.mesh.position.y, playerPosition.z);
Rotates the enemy to face the player, making it look like they're chasing rather than sliding sideways
for (const wall of walls) {
Loops through all walls to check for collisions
const bboxAmongUs = new THREE.Box3().setFromObject(this.mesh);
Calculates the enemy's bounding box
const bboxWall = new THREE.Box3().setFromObject(wall);
Calculates the wall's bounding box
if (bboxAmongUs.intersectsBox(bboxWall)) {
Checks if the enemy collided with the wall
this.mesh.position.copy(prevPosition);
Reverts the enemy to its previous position (before the move)
break;
Stops checking further walls (the collision has been handled)

📦 Key Variables

gameState string

Tracks the current game phase (START_SCREEN, PLAYING, ROUND_WON, GAME_WON, or GAME_OVER), controlling which UI is displayed and which game logic runs

let gameState = 'START_SCREEN';
currentRound number

Tracks which round (1-15) the player is currently on, used to scale enemy count and speed, and to determine win condition

let currentRound = 1;
score number

Accumulates points (10 per enemy defeated) displayed in the UI

let score = 0;
MAX_ROUNDS number

The total number of rounds needed to complete the game (15), used to determine if the current round is the last

const MAX_ROUNDS = 15;
shootSound, hitSound, winSound, loseSound p5.Oscillator / p5.PolySynth

Procedurally generated sound objects that play feedback audio for shooting, hitting enemies, winning rounds, and losing the game

let shootSound = new p5.Oscillator('sine');
renderer THREE.WebGLRenderer

The three.js graphics engine that renders the 3D scene to the canvas each frame

let renderer = new THREE.WebGLRenderer();
scene THREE.Scene

The 3D container holding all game objects (meshes, lights, camera) that the renderer draws

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

The player's viewpoint in 3D space; position and rotation determine what the player sees

let camera = new THREE.PerspectiveCamera(75, ...);
pointerLocked boolean

True if the mouse pointer is locked to the game canvas (in gameplay mode), false if on menus

let pointerLocked = false;
moveForward, moveBackward, moveLeft, moveRight boolean

Flags set by keyboard events (WASD) that indicate which directions the player wants to move

let moveForward = false;
cameraYaw, cameraPitch number

Angles (in radians) that control camera rotation for FPS mouse look (yaw = left/right, pitch = up/down)

let cameraYaw = 0;
velocity THREE.Vector3

The player's current movement velocity, used in acceleration and friction calculations

const velocity = new THREE.Vector3();
amongUsCharacters array of AmongUsCharacter3D

Tracks all active enemy objects, their positions, and AI state; when empty, the round is won

let amongUsCharacters = [];
bananaProjectileMeshes array of THREE.Mesh

Tracks all active projectile meshes for position updates and collision detection

const bananaProjectileMeshes = [];
lastShotTime number

Timestamp (in milliseconds) of the last shot fired, used to enforce reload delay between shots

let lastShotTime = 0;
PLAYER_HEIGHT number

The height of the player's eyes above the ground (1.8 units), used for camera and collision positioning

const PLAYER_HEIGHT = 1.8;
PLAYER_SPEED number

How fast the player accelerates when moving (units per second)

const PLAYER_SPEED = 15.0;
bananaProjectileSpeed number

How fast projectiles travel after being fired

const bananaProjectileSpeed = 50.0;
reloadTime number

Minimum milliseconds between shots, controlling the fire rate

const reloadTime = 200;
mouseSensitivity number

Multiplier for mouse movement to camera rotation sensitivity

const mouseSensitivity = 0.002;

🔧 Potential Improvements (6)

Here are some ways this code could be enhanced:

PERFORMANCE updateBananaProjectiles() and updateAmongUsCharacters()

Bounding boxes are recalculated every frame for every object, even if they haven't moved. This wastes CPU cycles.

💡 Cache bounding boxes or use simpler distance-based checks (e.g., Vector3.distanceTo()) for quick early-exit collision detection before computing full bounding boxes.

BUG playerShoot() fire rate check

Using millis() from p5.js but the sketch is designed around three.js. If p5 draw() doesn't run frequently, fire rate timing could be unreliable.

💡 Use performance.now() (same as animate() does) for frame-independent timing consistency.

FEATURE Game design

Once a player loses, the only option is to restart from round 1. There's no pause or difficulty selection.

💡 Add a 'PAUSED' gameState and allow players to resume mid-game, or add a difficulty selection menu at START_SCREEN to scale enemy speed and count.

STYLE HTML/CSS

UI text is rendered directly as HTML with inline styling (text-shadow in CSS only). This approach mixes presentation and data.

💡 Create a separate UI manager class or use a template system (e.g., building HTML from variables) to make UI easier to style and maintain.

BUG AmongUsCharacter3D.update()

The variable name bboxWall was mentioned in a comment (FIX: Corrected wallBBox to bboxWall) but the actual code uses bboxWall correctly—no actual bug, but the comment suggests confusion that could affect future maintenance.

💡 Use consistent naming: either bboxWall or wallBBox throughout, and remove confusing comments.

PERFORMANCE Memory management in updateBananaProjectiles()

When a projectile or enemy is destroyed, dispose() is called, but the scene may still contain old geometry/material references if dispose is called after remove.

💡 Ensure dispose() is called before remove(), or consider a resource pool pattern for frequently created/destroyed objects like projectiles.

🔄 Code Flow

Code flow showing preload, setup, draw, spawnamonguscharacters, mousepressed, touchstarted, handleinput, windowresized, initthreejs, createwall, initfpscontrols, onkeydown, onkeyup, onmousedown, onpointerlockchange, onpointerlockererror, onmousemove, updatefpscontrols, playershoot, animate, updateamonguscharacters, updatebananaprojercles, updateui, amonguscharacter3d

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

graph TD start[Start] --> setup[setup] setup --> draw[draw loop] draw --> subcomponents[Subcomponents] subcomponents --> canvas-creation[Canvas Creation] subcomponents --> audio-context[Audio Context Initialization] subcomponents --> initthreejs[initThreeJS] initthreejs --> renderer-init[WebGL Renderer Setup] initthreejs --> scene-init[Scene Creation] initthreejs --> camera-init[Camera Setup] initthreejs --> lighting[Lighting Setup] initthreejs --> floor-creation[Floor Mesh Creation] initthreejs --> wall-creation[Wall Spawning] initthreejs --> player-mesh[Player Collision Sphere] initthreejs --> weapon-mesh[Player Weapon Mesh] draw --> spawnamonguscharacters[spawnAmongUsCharacters] spawnamonguscharacters --> spawn-loop[Enemy Spawning Loop] draw --> mousepressed[mousePressed] mousepressed --> handleinput[handleInput] handleinput --> start-screen-branch[Start Screen Handler] handleinput --> round-won-branch[Round Won Handler] handleinput --> game-end-branch[Game End Handler] handleinput --> shoot-branch[Shoot Handler] draw --> touchstarted[touchStarted] draw --> windowresized[windowResized] draw --> animate[animate] animate --> delta-time-calc[Frame Delta Time Calculation] animate --> rendering[Scene Rendering] animate --> gameplay-updates[Gameplay Logic Execution] gameplay-updates --> updateamonguscharacters[updateAmongUsCharacters] updateamonguscharacters --> reverse-loop[Reverse Iteration] updateamonguscharacters --> collision-check[Enemy-Player Collision Detection] gameplay-updates --> updatebananaprojercles[updateBananaProjectiles] updatebananaprojercles --> projectile-movement[Projectile Position Update] updatebananaprojercles --> distance-cull[Projectile Distance Limit] updatebananaprojercles --> wall-collision[Projectile-Wall Collision Detection] updatebananaprojercles --> enemy-collision[Projectile-Enemy Collision Detection] draw --> updateui[updateUI] updateui --> state-switch[UI State Rendering] click setup href "#fn-setup" click draw href "#fn-draw" click canvas-creation href "#sub-canvas-creation" click audio-context href "#sub-audio-context" click initthreejs href "#fn-initthreejs" click renderer-init href "#sub-renderer-init" click scene-init href "#sub-scene-init" click camera-init href "#sub-camera-init" click lighting href "#sub-lighting" click floor-creation href "#sub-floor-creation" click wall-creation href "#sub-wall-creation" click player-mesh href "#sub-player-mesh" click weapon-mesh href "#sub-weapon-mesh" click spawnamonguscharacters href "#fn-spawnamonguscharacters" click spawn-loop href "#sub-spawn-loop" click mousepressed href "#fn-mousepressed" click handleinput href "#fn-handleinput" click start-screen-branch href "#sub-start-screen-branch" click round-won-branch href "#sub-round-won-branch" click game-end-branch href "#sub-game-end-branch" click shoot-branch href "#sub-shoot-branch" click touchstarted href "#fn-touchstarted" click windowresized href "#fn-windowresized" click animate href "#fn-animate" click delta-time-calc href "#sub-delta-time-calc" click rendering href "#sub-rendering" click gameplay-updates href "#sub-gameplay-updates" click updateamonguscharacters href "#fn-updateamonguscharacters" click reverse-loop href "#sub-reverse-loop" click collision-check href "#sub-collision-check" click updatebananaprojercles href "#fn-updatebananaprojercles" click projectile-movement href "#sub-projectile-movement" click distance-cull href "#sub-distance-cull" click wall-collision href "#sub-wall-collision" click enemy-collision href "#sub-enemy-collision" click updateui href "#fn-updateui" click state-switch href "#sub-state-switch"

❓ Frequently Asked Questions

What visual elements does the Sketch 2026-03-06 23:20 create?

The sketch primarily sets up a game environment, including a start screen and various game states, although detailed visual elements are not provided in the code snippet.

How can users interact with the Sketch 2026-03-06 23:20?

Users can interact with the sketch by engaging in gameplay, which involves progressing through rounds, scoring points, and experiencing sound feedback for actions like shooting and hitting targets.

What creative coding concepts are demonstrated in Sketch 2026-03-06 23:20?

The sketch demonstrates game state management, sound synthesis using p5.js, and basic interactive game mechanics.

Preview

Sketch 2026-03-06 23:20 - p5.js creative coding sketch preview
Sketch Preview
Code flow diagram showing the structure of Sketch 2026-03-06 23:20 - Code flow showing preload, setup, draw, spawnamonguscharacters, mousepressed, touchstarted, handleinput, windowresized, initthreejs, createwall, initfpscontrols, onkeydown, onkeyup, onmousedown, onpointerlockchange, onpointerlockererror, onmousemove, updatefpscontrols, playershoot, animate, updateamonguscharacters, updatebananaprojercles, updateui, amonguscharacter3d
Code Flow Diagram