Sketch 2026-05-15 16:35

This sketch creates a space shooter game with multiplayer simulation, where a player controls a spaceship to shoot down falling enemies. The game features local player control, a simulated remote player, enemy spawning and movement, bullet physics, collision detection, and score tracking—all coordinated through a simulated server system.

🧪 Try This!

Experiment with the code by making these changes:

  1. Speed up the game — Enemies spawn more frequently and move faster, making the game harder immediately.
  2. Make your player red — Your spaceship changes color from blue to red, making it look more like the enemies.
  3. Shoot faster — Lower fire rate lets you shoot more bullets per second, giving you more firepower.
  4. Change starting player position — Your spaceship spawns higher on the screen instead of near the bottom.
  5. Make enemies spawn in the center column — All enemies appear in a vertical line down the middle instead of random positions.
  6. Draw enemies as circles instead of squares — Enemies become circles instead of squares, changing the visual style.
Prefer the full editor? Open it there →

📖 About This Sketch

This sketch builds a complete space shooter game that simulates a multiplayer online architecture. You control a blue spaceship at the bottom of the screen, move it with arrow keys or mouse drag, and shoot falling red enemies by pressing space or tapping. A yellow remote player moves across the screen, showing how multiple players would appear in a real online game. The sketch demonstrates several advanced p5.js concepts: object-oriented design with Player, Enemy, and Bullet classes; game state management with the gameStarted and gameOver flags; collision detection using the distance formula; and input handling for both desktop (keyboard/mouse) and mobile (touch) platforms.

The code is organized into class definitions (Player, Enemy, Bullet), a core draw loop that simulates server updates and renders all game objects, input handlers (keyPressed, mousePressed, touchStarted), and a simulateServerUpdate() function that demonstrates server-side logic. By reading it you will learn how real multiplayer games coordinate state between clients and servers, how to manage multiple objects of the same type using object dictionaries, and how to build responsive game mechanics that work across devices.

⚙️ How It Works

  1. When the sketch loads, setup() creates a full-window canvas, generates a unique player ID, creates the local player spaceship near the bottom center, and hides the cursor for immersive gameplay.
  2. The draw loop checks if the game has started—if not, it shows a start screen; if game over, it shows a game over screen; otherwise, it simulates receiving server data every 30 frames.
  3. Server simulation (simulateServerUpdate) spawns enemies at random positions near the top, moves them downward, moves a simulated remote player left and right, and updates all bullet positions.
  4. Collision detection fires: every bullet checks its distance to every enemy; if they collide, the enemy is removed, the bullet disappears, and the score increases if the local player shot it.
  5. The draw loop renders all players (blue for local, yellow for remote), enemies (red squares), and bullets (yellow circles), then processes local player input from keyboard, mouse, or touch.
  6. If any enemy reaches the bottom of the screen, gameOver becomes true and the game over screen appears; clicking or tapping restarts the game.

🎓 Concepts You'll Learn

Object-oriented design with classesGame state managementCollision detection using distance formulaInput handling (keyboard, mouse, touch)Object dictionaries for managing multiple entitiesServer simulation and client-server architectureResponsive canvas and window resizing

📝 Code Breakdown

setup()

setup() runs once when the sketch starts. Here you initialize the canvas size, create game objects, and set up the starting state. This is where you prepare everything the draw loop will use.

function setup() {
  createCanvas(windowWidth, windowHeight);
  localPlayerId = 'player_' + floor(random(100000)); // Unique ID for this client's player
  localPlayer = new Player(localPlayerId, width / 2, height - 80);
  players[localPlayerId] = localPlayer;
  noCursor(); // Hide the cursor for better player control
  userStartAudio(); // Good practice to ensure audio works if added later
}
Line-by-line explanation (6 lines)

🔧 Subcomponents:

calculation Full-window canvas createCanvas(windowWidth, windowHeight);

Creates a canvas that fills the entire browser window so the game uses all available screen space

calculation Unique player ID localPlayerId = 'player_' + floor(random(100000));

Generates a unique identifier for this player that would be used by a real server to distinguish players

calculation Create local player localPlayer = new Player(localPlayerId, width / 2, height - 80);

Instantiates the Player object at the center-bottom of the canvas

createCanvas(windowWidth, windowHeight);
Creates a canvas matching your entire browser window size, ensuring the game fills the screen
localPlayerId = 'player_' + floor(random(100000));
Generates a unique ID like 'player_47382' that identifies this client's player to the simulated server
localPlayer = new Player(localPlayerId, width / 2, height - 80);
Creates your player spaceship object at the horizontal center and near the bottom of the canvas
players[localPlayerId] = localPlayer;
Stores the local player in the players dictionary so it can be accessed and updated later
noCursor();
Hides the mouse cursor for a cleaner, more immersive game experience
userStartAudio();
Prepares the audio system (good practice for games that might add sound effects later)

draw()

draw() runs 60 times per second and is the heartbeat of the game. Every frame it receives 'server data', clears the screen, renders all game objects, handles input, and checks win/lose conditions. In a real multiplayer game, simulatedServerData would arrive via WebSockets from an actual server.

🔬 This loop draws every player on screen. What happens if you delete the line p.show() so players don't render? What if you add a second p.show() call right after it?

    // Update and show all players (including local and remote)
    for (let id in simulatedServerData.players) {
      let playerData = simulatedServerData.players[id];
      let p = new Player(playerData.id, playerData.x, playerData.y, playerData.isLocal);
      players[id] = p;
      p.show();
    }
function draw() {
  background(20, 20, 40); // Dark space background

  if (!gameStarted) {
    showStartScreen();
  } else if (gameOver) {
    showGameOverScreen();
  } else {
    // --- Simulate receiving game state from server ---
    // In a real online game, this data would come via WebSockets from the server.
    // Here, we generate it locally for demonstration.
    if (frameCount - lastSimulatedUpdate > simulatedUpdateRate) {
      simulatedServerData = simulateServerUpdate();
      lastSimulatedUpdate = frameCount;
    }

    // --- Process received server data and render ---
    players = {}; // Clear local players and rebuild from server data
    enemies = {}; // Clear local enemies and rebuild from server data
    bullets = {}; // Clear local bullets and rebuild from server data

    // Update and show all players (including local and remote)
    for (let id in simulatedServerData.players) {
      let playerData = simulatedServerData.players[id];
      let p = new Player(playerData.id, playerData.x, playerData.y, playerData.isLocal);
      players[id] = p;
      p.show();
    }

    // Update and show all enemies
    for (let id in simulatedServerData.enemies) {
      let enemyData = simulatedServerData.enemies[id];
      let e = new Enemy(enemyData.id, enemyData.x, enemyData.y, enemyData.size, enemyData.speed);
      enemies[id] = e;
      e.show();
    }

    // Update and show all bullets
    for (let id in simulatedServerData.bullets) {
      let bulletData = simulatedServerData.bullets[id];
      let b = new Bullet(bulletData.id, bulletData.x, bulletData.y, bulletData.size, bulletData.ownerId);
      bullets[id] = b;
      b.show();
    }

    // --- Update local player's input and "send" to server (simulated) ---
    // Only update the local player's position based on local input
    localPlayer.updateInput();
    // This action would be sent to the server in a real game:
    // socket.emit('playerMove', { id: localPlayerId, x: localPlayer.x, y: localPlayer.y });

    // --- Display score ---
    fill(255);
    textSize(24);
    textAlign(LEFT, TOP);
    text(`Score: ${score}`, 10, 10);

    // Check for game over (enemies offscreen - server would handle this in real game)
    for (let id in enemies) {
      if (enemies[id].offscreen()) {
        gameOver = true;
        break;
      }
    }
  }
}
Line-by-line explanation (12 lines)

🔧 Subcomponents:

conditional Game state branching if (!gameStarted) { ... } else if (gameOver) { ... } else { ... }

Directs code flow to show start screen, game over screen, or active gameplay depending on current game state

conditional Simulated server update if (frameCount - lastSimulatedUpdate > simulatedUpdateRate) { simulatedServerData = simulateServerUpdate(); lastSimulatedUpdate = frameCount; }

Calls the server simulation every 30 frames to update game state (spawning, movement, collisions)

for-loop Render all players for (let id in simulatedServerData.players) { ... }

Iterates through all players (local and remote) from server data and draws them

for-loop Render all enemies for (let id in simulatedServerData.enemies) { ... }

Iterates through all active enemies and draws them

for-loop Render all bullets for (let id in simulatedServerData.bullets) { ... }

Iterates through all active bullets and draws them

conditional Check enemy offscreen for (let id in enemies) { if (enemies[id].offscreen()) { gameOver = true; break; } }

Ends the game if any enemy reaches the bottom of the screen

background(20, 20, 40);
Fills the entire canvas with a dark blue-gray color, erasing the previous frame to create clean animation
if (!gameStarted) { showStartScreen(); }
If the game hasn't started yet, display the start screen with instructions
} else if (gameOver) { showGameOverScreen(); }
If the game has ended, display the game over screen with final score
if (frameCount - lastSimulatedUpdate > simulatedUpdateRate) {
Checks if enough frames have passed (30 frames) since the last server update; if so, runs the simulation
simulatedServerData = simulateServerUpdate();
Calls the server simulation function to spawn enemies, move them, move players, update bullets, and detect collisions
players = {};
Clears the players dictionary so it can be rebuilt fresh from server data each frame
for (let id in simulatedServerData.players) {
Loops through each player in the server's data (local and remote)
let p = new Player(playerData.id, playerData.x, playerData.y, playerData.isLocal);
Creates a new Player object from the server data with its current position and local/remote status
p.show();
Draws the player on the canvas (blue if local, yellow if remote)
localPlayer.updateInput();
Updates the local player's position based on keyboard, mouse, or touch input from this frame
text(`Score: ${score}`, 10, 10);
Displays the current score in the top-left corner of the screen
if (enemies[id].offscreen()) { gameOver = true; break; }
Ends the game if any enemy reaches the bottom of the screen without being destroyed

simulateServerUpdate()

This function simulates what a real server would do: manage all game state, update positions, detect collisions, and return the new state. In a real multiplayer game, this would run on a remote server and send data to all clients via WebSockets. Notice how it always creates new dictionaries and returns them—this is immutable design, which prevents bugs.

🔬 This code spawns enemies at a random x position and random size. What happens if you change x: random(25, width - 25) to x: width / 2 so all enemies spawn in the center column?

  // Simulate enemy spawning and movement
  if (frameCount % enemySpawnRate === 0) {
    const enemyId = 'enemy_' + floor(random(100000));
    newEnemies[enemyId] = {
      id: enemyId,
      x: random(25, width - 25),
      y: -25,
      size: random(20, 50),
      speed: random(enemySpeedMin, enemySpeedMax)
    };
  }
function simulateServerUpdate() {
  let newPlayers = {};
  let newEnemies = {};
  let newBullets = {};

  // Copy existing local player data
  if (localPlayer) {
    newPlayers[localPlayer.id] = { id: localPlayer.id, x: localPlayer.x, y: localPlayer.y, isLocal: true };
  }

  // Simulate remote players (just one for now, moving randomly)
  if (!simulatedServerData.players['remotePlayer1']) {
    simulatedServerData.players['remotePlayer1'] = { id: 'remotePlayer1', x: width / 3, y: height - 80, isLocal: false, speed: random(-2, 2) };
  }
  let rp = simulatedServerData.players['remotePlayer1'];
  rp.x += rp.speed;
  if (rp.x < 0 || rp.x > width) rp.speed *= -1;
  newPlayers[rp.id] = rp;

  // Simulate enemy spawning and movement
  if (frameCount % enemySpawnRate === 0) {
    const enemyId = 'enemy_' + floor(random(100000));
    newEnemies[enemyId] = {
      id: enemyId,
      x: random(25, width - 25),
      y: -25,
      size: random(20, 50),
      speed: random(enemySpeedMin, enemySpeedMax)
    };
  }
  for (let id in simulatedServerData.enemies) {
    let enemy = simulatedServerData.enemies[id];
    enemy.y += enemy.speed;
    if (!enemy.offscreen()) { // Don't add if offscreen
      newEnemies[id] = enemy;
    }
  }

  // Simulate bullet movement and collision detection
  for (let id in simulatedServerData.bullets) {
    let bullet = simulatedServerData.bullets[id];
    bullet.y -= bulletSpeed;

    let hit = false;
    for (let enemyId in newEnemies) {
      let enemy = newEnemies[enemyId];
      let d = dist(bullet.x, bullet.y, enemy.x, enemy.y);
      if (d < (bullet.size / 2 + enemy.size / 2)) {
        // Collision! Increment score for the bullet's owner (simulated)
        if (bullet.ownerId === localPlayerId) {
          score++;
        }
        delete newEnemies[enemyId]; // Remove enemy
        hit = true;
        break;
      }
    }

    if (!bullet.offscreen() && !hit) {
      newBullets[id] = bullet;
    }
  }

  return { players: newPlayers, enemies: newEnemies, bullets: newBullets };
}
Line-by-line explanation (17 lines)

🔧 Subcomponents:

conditional Remote player initialization if (!simulatedServerData.players['remotePlayer1']) { ... }

Creates a remote player on first call and moves it back and forth across the screen

conditional Remote player boundary bounce if (rp.x < 0 || rp.x > width) rp.speed *= -1;

Reverses the remote player's direction when they reach the left or right edge

conditional Enemy spawn timing if (frameCount % enemySpawnRate === 0) { ... }

Creates a new enemy every 60 frames (controlled by enemySpawnRate variable)

for-loop Move and filter enemies for (let id in simulatedServerData.enemies) { enemy.y += enemy.speed; if (!enemy.offscreen()) { newEnemies[id] = enemy; } }

Moves all enemies downward and removes them if they go off the bottom

for-loop Move bullets and check collisions for (let id in simulatedServerData.bullets) { bullet.y -= bulletSpeed; for (let enemyId in newEnemies) { ... } }

Moves bullets upward and tests each one against all enemies for collision

conditional Bullet-enemy collision check if (d < (bullet.size / 2 + enemy.size / 2)) { ... }

Detects collision by comparing distance between bullet center and enemy center to their combined radii

let newPlayers = {};
Creates an empty object to store the updated player data that will be returned
if (localPlayer) { newPlayers[localPlayer.id] = { id: localPlayer.id, x: localPlayer.x, y: localPlayer.y, isLocal: true }; }
Copies the local player's current position into the new players data (this is what the server would receive from you)
if (!simulatedServerData.players['remotePlayer1']) {
Checks if the remote player exists; if not, this is the first frame, so create them
simulatedServerData.players['remotePlayer1'] = { id: 'remotePlayer1', x: width / 3, y: height - 80, isLocal: false, speed: random(-2, 2) };
Creates a remote player at one-third of the screen width with a random starting speed between -2 and 2 pixels per frame
rp.x += rp.speed;
Moves the remote player horizontally by adding their speed value
if (rp.x < 0 || rp.x > width) rp.speed *= -1;
If the remote player hit the left (< 0) or right (> width) edge, reverse their direction by flipping the sign of speed
if (frameCount % enemySpawnRate === 0) {
The modulo operator (%) returns the remainder; 0 means frameCount is evenly divisible by enemySpawnRate, so spawn an enemy
const enemyId = 'enemy_' + floor(random(100000));
Generates a unique ID like 'enemy_47382' for this enemy
x: random(25, width - 25),
Spawns the enemy at a random horizontal position with 25-pixel margins from the edges
y: -25,
Spawns enemies above the top of the canvas (-25 is above y=0)
enemy.y += enemy.speed;
Moves the enemy downward each frame by adding its speed to its y position
if (!enemy.offscreen()) { newEnemies[id] = enemy; }
Only keeps enemies in the newEnemies list if they haven't gone off the bottom; this automatically removes old enemies
bullet.y -= bulletSpeed;
Moves the bullet upward each frame by subtracting from y (lower y values = higher on screen)
let d = dist(bullet.x, bullet.y, enemy.x, enemy.y);
Calculates the distance between the bullet's center and the enemy's center using the dist() function
if (d < (bullet.size / 2 + enemy.size / 2)) {
If the distance is less than the sum of their radii, they are overlapping—collision detected!
if (bullet.ownerId === localPlayerId) { score++; }
Only increments your score if your local player shot the bullet, not a remote player
delete newEnemies[enemyId];
Removes the enemy from the game by deleting it from the newEnemies dictionary

Player class and methods

The Player class encapsulates everything about a player: their appearance (show method), their input handling (updateInput method), and their shooting (shoot method). By organizing code into classes, you can easily manage multiple players without code duplication. Notice isLocal: it lets one class represent both your player and remote players by changing colors and behavior.

🔬 These two lines let you move left and right with arrow keys. What if you add a third condition for the UP_ARROW to move the player up by changing this.y? Try it.

    // Keyboard input for movement
    if (keyIsDown(LEFT_ARROW)) {
      this.x -= playerSpeed;
    }
    if (keyIsDown(RIGHT_ARROW)) {
      this.x += playerSpeed;
    }

🔬 This code moves the player to match your finger or mouse x position. What happens if you change this.x = touches[0].x to this.x = touches[0].y so touch movement is vertical instead of horizontal?

    // Mouse/Touch input for movement
    if (touches.length > 0) { // If touch is detected
      this.x = touches[0].x;
    } else if (mouseIsPressed) { // Fallback for mouse (or if no touch)
      this.x = mouseX;
    }
class Player {
  constructor(id, x, y, isLocal = false) {
    this.id = id;
    this.size = 40; // Size of the player
    this.x = x;
    this.y = y;
    this.isLocal = isLocal; // True if this is the player controlled by this client
  }

  show() {
    if (this.isLocal) {
      fill(0, 200, 255); // Blue for local player
    } else {
      fill(200, 200, 0); // Yellow for remote players
    }
    noStroke();
    // Draw a triangle to represent a spaceship
    triangle(
      this.x, this.y - this.size / 2,
      this.x - this.size / 2, this.y + this.size / 2,
      this.x + this.size / 2, this.y + this.size / 2
    );
    // Display player ID (optional)
    fill(255);
    textSize(12);
    textAlign(CENTER, TOP);
    text(this.id, this.x, this.y + this.size / 2 + 5);
  }

  updateInput() {
    // Keyboard input for movement
    if (keyIsDown(LEFT_ARROW)) {
      this.x -= playerSpeed;
    }
    if (keyIsDown(RIGHT_ARROW)) {
      this.x += playerSpeed;
    }

    // Mouse/Touch input for movement
    if (touches.length > 0) { // If touch is detected
      this.x = touches[0].x;
    } else if (mouseIsPressed) { // Fallback for mouse (or if no touch)
      this.x = mouseX;
    }

    // Constrain player to canvas bounds
    this.x = constrain(this.x, this.size / 2, width - this.size / 2);
  }

  shoot() {
    if (frameCount - lastFireTime > fireRate) {
      // In a real online game, this would be sent to the server:
      // socket.emit('playerShoot', { id: localPlayerId, x: this.x, y: this.y - this.size });
      // For simulation, we'll just add it to the simulated data for the next update
      const bulletId = 'bullet_' + floor(random(100000));
      simulatedServerData.bullets[bulletId] = {
        id: bulletId,
        x: this.x,
        y: this.y - this.size,
        size: 10,
        ownerId: this.id
      };
      lastFireTime = frameCount;
    }
  }
}
Line-by-line explanation (19 lines)

🔧 Subcomponents:

calculation Player constructor constructor(id, x, y, isLocal = false) { ... }

Initializes a new Player object with ID, position, size, and local/remote flag

conditional Local vs remote color if (this.isLocal) { fill(0, 200, 255); } else { fill(200, 200, 0); }

Colors local players blue and remote players yellow to distinguish them visually

conditional Arrow key input if (keyIsDown(LEFT_ARROW)) { this.x -= playerSpeed; }

Moves the player left when left arrow is held down

conditional Touch/mouse input if (touches.length > 0) { this.x = touches[0].x; } else if (mouseIsPressed) { this.x = mouseX; }

Moves the player to follow finger (touch) or mouse position for cross-platform control

calculation Keep player on screen this.x = constrain(this.x, this.size / 2, width - this.size / 2);

Prevents the player from moving off the left or right edges of the canvas

conditional Fire rate limiter if (frameCount - lastFireTime > fireRate) { ... }

Prevents shooting more than once per 15 frames to limit bullet spam

constructor(id, x, y, isLocal = false) {
Defines the constructor that runs when you create a new Player. The = false sets a default value for isLocal if you don't provide one.
this.id = id;
Stores the player's unique identifier (like 'player_47382') on this object
this.size = 40;
Sets the player spaceship to 40 pixels in size
this.x = x;
Stores the starting x (horizontal) position
this.y = y;
Stores the starting y (vertical) position
this.isLocal = isLocal;
Stores whether this player is controlled by the current client (true) or is a remote player (false)
if (this.isLocal) { fill(0, 200, 255); }
Sets the fill color to cyan (0, 200, 255) if this is your player
} else { fill(200, 200, 0); }
Sets the fill color to yellow (200, 200, 0) if this is a remote player
triangle(this.x, this.y - this.size / 2, this.x - this.size / 2, this.y + this.size / 2, this.x + this.size / 2, this.y + this.size / 2);
Draws a triangle with the point at the top (y - size/2) and two points at the bottom, creating a spaceship shape
text(this.id, this.x, this.y + this.size / 2 + 5);
Displays the player's ID (like 'player_47382') below the spaceship for identification
if (keyIsDown(LEFT_ARROW)) { this.x -= playerSpeed; }
If the left arrow key is pressed, move left by subtracting playerSpeed (8) from x
if (keyIsDown(RIGHT_ARROW)) { this.x += playerSpeed; }
If the right arrow key is pressed, move right by adding playerSpeed (8) to x
if (touches.length > 0) { this.x = touches[0].x; }
If a touch is detected, move to the x position of the first finger (touches[0])
} else if (mouseIsPressed) { this.x = mouseX; }
If the mouse is clicked (and no touch), move to the mouse's x position
this.x = constrain(this.x, this.size / 2, width - this.size / 2);
Clamps the player's x position to stay between size/2 (left edge) and width - size/2 (right edge), preventing off-screen movement
if (frameCount - lastFireTime > fireRate) {
Checks if at least 15 frames have passed since the last shot; if so, allow shooting
const bulletId = 'bullet_' + floor(random(100000));
Generates a unique ID for the new bullet
simulatedServerData.bullets[bulletId] = { ... };
Adds a new bullet to the server's bullet data at the player's x position and above (y - this.size)
lastFireTime = frameCount;
Records the current frame number so the fire rate check knows when the last shot occurred

Enemy class and methods

The Enemy class is simple because enemies only need to be drawn and checked for being off-screen. All movement happens in simulateServerUpdate. This separation of concerns keeps code clean: the class stores data and draws, while the update function handles logic. offscreen() is a helper method that returns a true/false value to make the main game loop readable.

class Enemy {
  constructor(id, x, y, size, speed) {
    this.id = id;
    this.size = size;
    this.x = x;
    this.y = y;
    this.speed = speed;
  }

  show() {
    fill(255, 0, 0); // Red enemy
    noStroke();
    rectMode(CENTER);
    rect(this.x, this.y, this.size, this.size);
  }

  offscreen() {
    return this.y > height + this.size / 2;
  }
}
Line-by-line explanation (10 lines)

🔧 Subcomponents:

calculation Enemy constructor constructor(id, x, y, size, speed) { ... }

Initializes an Enemy with ID, position, size, and falling speed

conditional Check if enemy left screen return this.y > height + this.size / 2;

Returns true if enemy has moved below the bottom of the canvas

constructor(id, x, y, size, speed) {
Creates a new Enemy object with an ID, position (x, y), size, and how fast it falls
this.id = id;
Stores the enemy's unique identifier
this.size = size;
Stores the enemy's size (a random value between 20-50 pixels)
this.x = x;
Stores the starting x position (horizontal)
this.y = y;
Stores the starting y position (always starts above the screen at -25)
this.speed = speed;
Stores how fast the enemy falls down the screen (1-3 pixels per frame)
fill(255, 0, 0);
Sets the fill color to pure red (255, 0, 0)
rectMode(CENTER);
Changes rectangle drawing so the coordinates are the center, not the top-left corner
rect(this.x, this.y, this.size, this.size);
Draws a square (width = height = this.size) centered at the enemy's position
return this.y > height + this.size / 2;
Returns true if the enemy has fallen below the screen bottom (height + half its size so it's fully off)

Bullet class and methods

The Bullet class mirrors the Enemy class: simple storage and rendering. Notice ownerId—this is crucial for multiplayer: when a bullet hits an enemy, the game checks if it belongs to the local player before awarding points. In a real game, ownerId would be sent to the server for score validation.

class Bullet {
  constructor(id, x, y, size, ownerId) {
    this.id = id;
    this.x = x;
    this.y = y;
    this.size = size;
    this.ownerId = ownerId; // To know which player shot this bullet
  }

  show() {
    fill(255, 255, 0); // Yellow bullet
    noStroke();
    ellipse(this.x, this.y, this.size);
  }

  offscreen() {
    return this.y < -this.size / 2;
  }
}
Line-by-line explanation (5 lines)

🔧 Subcomponents:

calculation Bullet constructor constructor(id, x, y, size, ownerId) { ... }

Initializes a Bullet with ID, position, size, and which player owns it

conditional Check if bullet left screen top return this.y < -this.size / 2;

Returns true if bullet has moved above the top of the canvas

constructor(id, x, y, size, ownerId) {
Creates a new Bullet with an ID, position, size, and ownerId (to track which player shot it)
this.ownerId = ownerId;
Stores the ID of the player who shot this bullet, used to award score only to the shooter
fill(255, 255, 0);
Sets the fill color to yellow (255, 255, 0)
ellipse(this.x, this.y, this.size);
Draws a circle (yellow) at the bullet's position with diameter equal to this.size (10 pixels)
return this.y < -this.size / 2;
Returns true if the bullet has moved above the top of the canvas (y less than -5 for a 10-pixel bullet)

showStartScreen()

UI functions display text screens outside of gameplay. This function shows when the game hasn't started yet, giving players instructions and a call-to-action. Notice how it uses width/2 and height/2 to position text relative to the screen size—this keeps the UI centered even if the window is resized.

function showStartScreen() {
  fill(255);
  textAlign(CENTER, CENTER);
  textSize(48);
  text("SHOOTING GAME (Multiplayer Simulation)", width / 2, height / 2 - 50);
  textSize(24);
  text("Desktop: Arrow keys to move, Space to shoot", width / 2, height / 2 + 20);
  text("Touch: Drag to move, Tap to shoot", width / 2, height / 2 + 60);
  text("Click or Tap to Start", width / 2, height / 2 + 120);
}
Line-by-line explanation (6 lines)
fill(255);
Sets text color to white
textAlign(CENTER, CENTER);
Centers text horizontally and vertically around the specified coordinates
textSize(48);
Sets the title text to 48 pixels tall
text("SHOOTING GAME (Multiplayer Simulation)", width / 2, height / 2 - 50);
Displays the title at the horizontal center and 50 pixels above the vertical center
textSize(24);
Reduces text size to 24 pixels for instructions
text("Desktop: Arrow keys to move, Space to shoot", width / 2, height / 2 + 20);
Displays desktop controls 20 pixels below center

showGameOverScreen()

This function displays the game over screen with the final score. The template literal (backticks and ${}) lets you embed variables into strings—a cleaner syntax than concatenation with +.

function showGameOverScreen() {
  fill(255);
  textAlign(CENTER, CENTER);
  textSize(64);
  text("GAME OVER!", width / 2, height / 2 - 60);
  textSize(32);
  text(`Final Score: ${score}`, width / 2, height / 2 + 20);
  textSize(24);
  text("Click or Tap to Restart", width / 2, height / 2 + 80);
}
Line-by-line explanation (2 lines)
text("GAME OVER!", width / 2, height / 2 - 60);
Displays 'GAME OVER!' in large 64-pixel text above the screen center
text(`Final Score: ${score}`, width / 2, height / 2 + 20);
Displays the final score using template literal syntax to embed the score variable in the text

resetGame()

resetGame() is called from input handlers when the player clicks to start or restart. It reinitializes all game variables to their starting state. This pattern—clearing everything and rebuilding from scratch—is simple and prevents bugs that come from partially-reset state.

function resetGame() {
  score = 0;
  players = {};
  enemies = {};
  bullets = {};
  gameOver = false;
  gameStarted = true;
  lastFireTime = 0;
  localPlayer = new Player(localPlayerId, width / 2, height - 80, true);
  players[localPlayerId] = localPlayer;
  simulatedServerData = { players: {}, enemies: {}, bullets: {} };
  simulatedServerData.players[localPlayerId] = { id: localPlayerId, x: localPlayer.x, y: localPlayer.y, isLocal: true };
}
Line-by-line explanation (6 lines)
score = 0;
Resets the score back to zero
players = {}; enemies = {}; bullets = {};
Clears all game objects by resetting the dictionaries to empty objects
gameOver = false; gameStarted = true;
Sets flags so the main game loop will start running the game instead of showing screens
lastFireTime = 0;
Resets the fire timer so you can shoot immediately on game start
localPlayer = new Player(localPlayerId, width / 2, height - 80, true);
Creates a fresh local player at the center-bottom and marks it as local (true)
simulatedServerData.players[localPlayerId] = { ... };
Populates the simulated server data with the new local player so it's included in the first frame

keyPressed()

keyPressed() runs once every time you press a key. By checking keyCode you can respond to specific keys. keyCode 32 is spacebar; LEFT_ARROW and RIGHT_ARROW are named constants in p5.js. Returning false prevents default browser actions.

function keyPressed() {
  if (keyCode === 32) { // Spacebar
    if (!gameStarted) {
      resetGame();
    } else if (!gameOver) {
      localPlayer.shoot();
    }
  }
  return false; // Prevent default browser action
}
Line-by-line explanation (4 lines)

🔧 Subcomponents:

conditional Spacebar detection if (keyCode === 32)

Checks if the spacebar was pressed (keyCode 32 is spacebar)

conditional Start or shoot logic if (!gameStarted) { resetGame(); } else if (!gameOver) { localPlayer.shoot(); }

Starts the game if not started, or shoots if game is running

if (keyCode === 32) { // Spacebar
Checks if the key pressed was the spacebar (keyboard key code 32)
if (!gameStarted) { resetGame(); }
If the game hasn't started, reset it to start playing
} else if (!gameOver) { localPlayer.shoot(); }
If the game is running (not game over), make the local player shoot a bullet
return false;
Prevents the browser's default spacebar behavior (like scrolling down the page)

mousePressed()

mousePressed() runs once when you click the mouse. This function handles both UI (start/restart) and gameplay (shooting) by checking game state flags. Cross-device control is important in p5.js—your game should work on desktop and mobile.

function mousePressed() {
  if (!gameStarted) {
    resetGame();
  } else if (gameOver) {
    resetGame();
  } else {
    localPlayer.shoot();
  }
  return false; // Prevent default browser action (e.g., right-click menu)
}
Line-by-line explanation (4 lines)

🔧 Subcomponents:

conditional Start/restart on click if (!gameStarted) { resetGame(); } else if (gameOver) { resetGame(); }

Starts the game on first click or restarts after game over

if (!gameStarted) { resetGame(); }
If the start screen is showing, clicking starts the game
} else if (gameOver) { resetGame(); }
If the game over screen is showing, clicking restarts the game
} else { localPlayer.shoot(); }
If the game is running, clicking shoots a bullet
return false;
Prevents the browser's default mouse action (like showing a context menu on right-click)

touchStarted()

touchStarted() is p5.js's touch input handler. The touches array contains all active touches; checking touches.length distinguishes between a tap (1 finger) and a drag (multiple fingers). This lets one function handle both movement (drag = many touches) and shooting (tap = one touch).

function touchStarted() {
  if (!gameStarted) {
    resetGame();
  } else if (gameOver) {
    resetGame();
  } else {
    // Only shoot if the tap is not part of a drag motion (i.e., just a quick tap)
    if (touches.length === 1) { // Single tap
      localPlayer.shoot();
    }
  }
  return false; // Prevent default browser action (e.g., scrolling, zooming)
}
Line-by-line explanation (3 lines)

🔧 Subcomponents:

conditional Single tap vs drag detection if (touches.length === 1)

Shoots only on single tap, not during multi-touch drag (which is used for movement)

if (!gameStarted) { resetGame(); }
If start screen is showing, first tap starts the game
if (touches.length === 1) { localPlayer.shoot(); }
Only shoot if there's exactly one finger touching (a tap). If multiple fingers are down, it's a drag for movement, so don't shoot
return false;
Prevents default mobile browser actions like scrolling or zooming

windowResized()

windowResized() is called automatically whenever the browser window is resized. This keeps your game responsive. Always call resizeCanvas() first, then update object positions—don't forget to update simulated server data so the game state stays consistent.

function windowResized() {
  resizeCanvas(windowWidth, windowHeight);
  // Re-position the local player
  if (localPlayer) {
    localPlayer.y = height - 80;
    localPlayer.x = constrain(localPlayer.x, localPlayer.size / 2, width - localPlayer.size / 2);
    // Update simulated server data for local player
    simulatedServerData.players[localPlayerId] = { id: localPlayerId, x: localPlayer.x, y: localPlayer.y, isLocal: true };
  }
}
Line-by-line explanation (3 lines)
resizeCanvas(windowWidth, windowHeight);
Resizes the p5.js canvas to match the new window size
if (localPlayer) { localPlayer.y = height - 80; }
Repositions the player to stay 80 pixels from the (new) bottom of the canvas
localPlayer.x = constrain(localPlayer.x, localPlayer.size / 2, width - localPlayer.size / 2);
Ensures the player stays within the (new) canvas width boundaries

📦 Key Variables

localPlayer object (Player)

Stores your player object so you can update input and shoot

let localPlayer = new Player(...)
localPlayerId string

Your unique player ID (like 'player_47382') used to identify you to the server

let localPlayerId = 'player_' + floor(random(100000));
players object (dictionary)

Dictionary storing all Player objects—local and remote—indexed by their ID

let players = {};
enemies object (dictionary)

Dictionary storing all Enemy objects currently on screen, indexed by ID

let enemies = {};
bullets object (dictionary)

Dictionary storing all Bullet objects in flight, indexed by ID

let bullets = {};
score number

Your current score—incremented by 1 each time your bullet hits an enemy

let score = 0;
gameOver boolean

Flag that is true when an enemy reaches the bottom; false during active play

let gameOver = false;
gameStarted boolean

Flag that is false at startup to show the start screen; true once gameplay begins

let gameStarted = false;
playerSpeed number

How many pixels the player moves per frame with arrow keys—higher is faster

let playerSpeed = 8;
bulletSpeed number

How many pixels bullets travel upward per frame

let bulletSpeed = 10;
enemySpawnRate number

How many frames pass between each new enemy spawn (60 = 1 second at 60 fps)

let enemySpawnRate = 60;
fireRate number

Minimum frames between shots—prevents bullet spam

let fireRate = 15;
lastFireTime number

Frame count when the last shot was fired; used to enforce fire rate

let lastFireTime = 0;
simulatedServerData object

Simulates server data structure holding all current players, enemies, and bullets

let simulatedServerData = { players: {}, enemies: {}, bullets: {} };

🔧 Potential Improvements (6)

Here are some ways this code could be enhanced:

BUG draw() collision detection line (enemies reaching bottom)

Game ends even if the local player can still destroy enemies—the endgame condition is too simple. In a real game, the server would track this more carefully.

💡 Add a lives system: enemies that reach the bottom reduce a lives counter instead of instantly ending the game. End the game only when lives reach 0.

BUG simulateServerUpdate() collision detection

Multiple bullets can collide with the same enemy in one frame if they happen to all be at the same position, but only one is removed. This can award points multiple times for one kill.

💡 Mark enemies as 'destroyed' instead of immediately deleting them, so they can't be hit twice. Or move the enemy deletion outside the inner loop.

PERFORMANCE draw() - object recreation every frame

Every frame, all Player, Enemy, and Bullet objects are recreated from server data using 'new'. This wastes memory and garbage collection cycles.

💡 Keep object instances and update their properties instead: e.g., players[id].x = playerData.x rather than creating a new Player. Reuse objects when possible.

STYLE Global variables at top

Many game parameters (playerSpeed, bulletSpeed, etc.) are declared as global variables, making it hard to change difficulty settings or have different game modes.

💡 Group related variables into a 'gameConfig' object: let gameConfig = { playerSpeed: 8, bulletSpeed: 10, ... }. This makes code more organized and enables easy difficulty switching.

FEATURE Multiplayer simulation

Only one static remote player is simulated. Real multiplayer games need multiple dynamic remote players.

💡 Create an array of remote players with different IDs and random movement patterns. Iterate through them to make the simulation feel more alive.

BUG Player.updateInput() - touch movement

Touch movement sets player.x directly to the finger position. If the finger is above/below the player, sudden y-jumps could feel unresponsive.

💡 Allow vertical touch movement by also updating player.y based on touch.y. Or add a deadzone so small finger movements don't snap the player.

🔄 Code Flow

Code flow showing setup, draw, simulateserverupdate, player, enemy, bullet, showstartscreen, showgameoverscreen, resetgame, keypressed, mousepressed, touchstarted, windowresized

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

graph TD start[Start] --> setup[setup] setup --> canvas-creation[Canvas Creation] setup --> unique-id-generation[Unique ID Generation] setup --> player-instantiation[Player Instantiation] setup --> draw[draw loop] click setup href "#fn-setup" click canvas-creation href "#sub-canvas-creation" click unique-id-generation href "#sub-unique-id-generation" click player-instantiation href "#sub-player-instantiation" draw --> game-state-check[Game State Check] draw --> server-update-simulation[Server Update Simulation] draw --> render-players-loop[Render Players Loop] draw --> render-enemies-loop[Render Enemies Loop] draw --> render-bullets-loop[Render Bullets Loop] draw --> game-over-check[Game Over Check] click draw href "#fn-draw" click game-state-check href "#sub-game-state-check" click server-update-simulation href "#sub-server-update-simulation" click render-players-loop href "#sub-render-players-loop" click render-enemies-loop href "#sub-render-enemies-loop" click render-bullets-loop href "#sub-render-bullets-loop" click game-over-check href "#sub-game-over-check" server-update-simulation --> remote-player-spawn[Remote Player Spawn] server-update-simulation --> enemy-spawn-check[Enemy Spawn Check] server-update-simulation --> enemy-movement-loop[Enemy Movement Loop] server-update-simulation --> bullet-movement-loop[Bullet Movement Loop] click remote-player-spawn href "#sub-remote-player-spawn" click enemy-spawn-check href "#sub-enemy-spawn-check" click enemy-movement-loop href "#sub-enemy-movement-loop" click bullet-movement-loop href "#sub-bullet-movement-loop" render-players-loop --> constructor[Constructor] render-players-loop --> color-selection[Color Selection] click constructor href "#sub-constructor" click color-selection href "#sub-color-selection" render-enemies-loop --> enemy-constructor[Enemy Constructor] render-enemies-loop --> offscreen-check[Offscreen Check] click enemy-constructor href "#sub-enemy-constructor" click offscreen-check href "#sub-offscreen-check" render-bullets-loop --> bullet-constructor[Bullet Constructor] render-bullets-loop --> offscreen-check-bullet[Offscreen Check Bullet] render-bullets-loop --> collision-detection[Collision Detection] click bullet-constructor href "#sub-bullet-constructor" click offscreen-check-bullet href "#sub-offscreen-check-bullet" click collision-detection href "#sub-collision-detection" game-over-check --> game-over-check[Game Over Check] click game-over-check href "#sub-game-over-check" keypressed[keyPressed] --> keyboard-input[Keyboard Input] keypressed --> spacebar-check[Spacebar Check] click keypressed href "#fn-keypressed" click keyboard-input href "#sub-keyboard-input" click spacebar-check href "#sub-spacebar-check" mousepressed[mousePressed] --> start-restart-logic[Start/Restart Logic] click mousepressed href "#fn-mousepressed" click start-restart-logic href "#sub-start-restart-logic" touchstarted[touchStarted] --> tap-vs-drag[Tap vs Drag] click touchstarted href "#fn-touchstarted" click tap-vs-drag href "#sub-tap-vs-drag" windowresized[windowResized] --> resizeCanvas[Resize Canvas] click windowresized href "#fn-windowresized" click resizeCanvas href "#sub-resize-canvas"

❓ Frequently Asked Questions

What visual experience does the p5.js sketch provide?

The sketch creates a dark space-themed game environment featuring a player character, enemies, and projectiles, all rendered in a dynamic and interactive manner.

How can users engage with the sketch during gameplay?

Users can control their player character's movement and firing of bullets, aiming to survive against incoming enemies in a multiplayer setting.

What creative coding concept is showcased in this p5.js sketch?

This sketch demonstrates multiplayer game mechanics, including player movement, enemy spawning, and simulating server updates for a real-time gaming experience.

Preview

Sketch 2026-05-15 16:35 - p5.js creative coding sketch preview
Sketch Preview
Code flow diagram showing the structure of Sketch 2026-05-15 16:35 - Code flow showing setup, draw, simulateserverupdate, player, enemy, bullet, showstartscreen, showgameoverscreen, resetgame, keypressed, mousepressed, touchstarted, windowresized
Code Flow Diagram