🔬 This loop moves each bullet and removes it if it goes off-screen. What happens if you change the condition from i >= 0 to i < bullets.length and start the loop at let i = 0 instead? (Loop forwards instead of backwards.) Why might removing items this way cause problems?
// Update and draw bullets
for (let i = bullets.length - 1; i >= 0; i--) {
bullets[i].update();
if (bullets[i].isOffScreen()) {
bullets.splice(i, 1); // Remove bullets that go off-screen
}
🔬 This code runs every time a bullet hits an enemy. What would happen if you removed the spawnExplosion() line? What if you changed spawnExplosion(enemies[j].x, enemies[j].y) to spawnExplosion(player.x, player.y)?
score += 10; // Increase score
spawnExplosion(enemies[j].x, enemies[j].y); // Create explosion particles
enemies.splice(j, 1); // Remove enemy
function updateGame() {
player.update(); // Move player
// Handle player shooting
if ((keyIsDown(32) || mouseIsPressed) && millis() - lastBulletTime > bulletDelay) { // Spacebar or mouse click
player.shoot();
lastBulletTime = millis(); // Reset bullet timer
}
// Update and draw bullets
for (let i = bullets.length - 1; i >= 0; i--) {
bullets[i].update();
if (bullets[i].isOffScreen()) {
bullets.splice(i, 1); // Remove bullets that go off-screen
} else {
// Check for bullet-enemy collisions
for (let j = enemies.length - 1; j >= 0; j--) {
if (checkCollision(bullets[i], enemies[j])) {
score += 10; // Increase score
spawnExplosion(enemies[j].x, enemies[j].y); // Create explosion particles
enemies.splice(j, 1); // Remove enemy
bullets.splice(i, 1); // Remove bullet
explosionSynth.amp(0.8, 0.05); // Play explosion sound
explosionSynth.amp(0, 0.2); // Decay explosion sound
break; // Bullet can only hit one enemy
}
}
}
}
// Spawn new enemies
if (millis() - lastEnemySpawnTime > enemySpawnDelay) {
enemies.push(new Enemy());
lastEnemySpawnTime = millis();
}
// Update and draw enemies
for (let i = enemies.length - 1; i >= 0; i--) {
enemies[i].update();
if (enemies[i].isOffScreen()) {
enemies.splice(i, 1); // Remove enemies that go off-screen
lives--; // Lose a life if enemy escapes
} else {
// Check for player-enemy collisions
if (checkCollision(player, enemies[i])) {
lives--; // Player loses a life
spawnExplosion(player.x, player.y); // Explosion at player location
explosionSynth.amp(0.8, 0.05); // Play explosion sound
explosionSynth.amp(0, 0.2); // Decay explosion sound
player.resetPosition(); // Reset player position after hit
enemies.splice(i, 1); // Remove enemy
}
}
}
// Update and draw particles (explosions)
for (let i = particles.length - 1; i >= 0; i--) {
particles[i].update();
if (particles[i].isFinished()) {
particles.splice(i, 1); // Remove finished particles
}
}
// Check for game over
if (lives <= 0) {
game_state = 'GAME_OVER';
}
}
Line-by-line explanation (34 lines)
🔧 Subcomponents:
for-loop
Bullet Update and Collision Loop
for (let i = bullets.length - 1; i >= 0; i--) {
Moves each bullet and checks if it hit an enemy or left the screen; loops backwards to safely remove bullets from the array
nested-loop
Bullet-Enemy Collision Detection
for (let j = enemies.length - 1; j >= 0; j--) {
Checks every bullet against every enemy to detect hits
conditional
Enemy Spawn Timer
if (millis() - lastEnemySpawnTime > enemySpawnDelay) {
Spawns a new enemy when enough time has passed since the last spawn
for-loop
Enemy Update and Collision Loop
for (let i = enemies.length - 1; i >= 0; i--) {
Moves each enemy and checks if it hit the player or escaped off-screen
for-loop
Particle Update Loop
for (let i = particles.length - 1; i >= 0; i--) {
Updates particle position and fades out finished particles
conditional
Game Over Condition
if (lives <= 0) {
Transitions to GAME_OVER state when the player loses all three lives
player.update(); // Move player
- Calls the player's update method, which processes keyboard input and moves the spaceship
if ((keyIsDown(32) || mouseIsPressed) && millis() - lastBulletTime > bulletDelay) {
- Checks two conditions: (1) spacebar OR mouse button is held, AND (2) at least bulletDelay milliseconds have passed since the last shot
player.shoot();
- Creates a new bullet at the player's position and plays the laser sound
lastBulletTime = millis(); // Reset bullet timer
- Records the current time so the bulletDelay timer will count down again before the next bullet is allowed
for (let i = bullets.length - 1; i >= 0; i--) {
- Loops through all bullets backwards—this is crucial because we'll be removing bullets from the array, and going backwards prevents skipping elements
bullets[i].update();
- Calls each bullet's update method to move it up the screen
if (bullets[i].isOffScreen()) {
- Checks if the bullet has traveled off the top of the canvas
bullets.splice(i, 1); // Remove bullets that go off-screen
- Deletes the bullet from the array using splice(), which removes 1 element at index i
for (let j = enemies.length - 1; j >= 0; j--) {
- Nested loop: for each bullet, checks against every enemy to see if they occupy the same space
if (checkCollision(bullets[i], enemies[j])) {
- Uses the checkCollision helper function to detect if this bullet and enemy are touching
score += 10; // Increase score
- Awards 10 points to the player for destroying an enemy
spawnExplosion(enemies[j].x, enemies[j].y); // Create explosion particles
- Creates 20 orange particles at the enemy's location to visually represent the explosion
enemies.splice(j, 1); // Remove enemy
- Removes the destroyed enemy from the enemies array
bullets.splice(i, 1); // Remove bullet
- Removes the bullet from the bullets array since it hit something and is spent
explosionSynth.amp(0.8, 0.05); // Play explosion sound
- Immediately increases the noise generator's amplitude to 0.8 over 0.05 seconds (attack), creating a burst sound
explosionSynth.amp(0, 0.2); // Decay explosion sound
- Decreases the amplitude to 0 over 0.2 seconds (decay), making the explosion sound fade away naturally
break; // Bullet can only hit one enemy
- Exits the inner enemy loop early—a bullet stops checking enemies after hitting one, so it can't hit multiple enemies
if (millis() - lastEnemySpawnTime > enemySpawnDelay) {
- Checks if enough time has passed since the last enemy was spawned
enemies.push(new Enemy());
- Creates a new Enemy object and adds it to the enemies array—the constructor automatically spawns it at a random x position and off-screen at the top
lastEnemySpawnTime = millis();
- Resets the spawn timer so the next enemy won't spawn until enemySpawnDelay milliseconds pass
for (let i = enemies.length - 1; i >= 0; i--) {
- Loops through all enemies backwards, just like the bullet loop
enemies[i].update();
- Moves the enemy down the screen
if (enemies[i].isOffScreen()) {
- Checks if the enemy has fallen past the bottom of the canvas
enemies.splice(i, 1); // Remove enemies that go off-screen
lives--; // Lose a life if enemy escapes
- Removes the escaped enemy and decrements lives by 1—the player loses a life if they fail to shoot an enemy before it reaches the bottom
if (checkCollision(player, enemies[i])) {
- Checks if the player's spaceship is touching an enemy
lives--; // Player loses a life
- Decrements lives—the player loses a life on direct collision with an enemy
spawnExplosion(player.x, player.y); // Explosion at player location
- Creates particles at the player's position to show a collision explosion
player.resetPosition(); // Reset player position after hit
- Moves the player back to the starting position so they can continue playing
enemies.splice(i, 1); // Remove enemy
- Deletes the enemy that just hit the player
for (let i = particles.length - 1; i >= 0; i--) {
- Loops through all particles and updates them
particles[i].update();
- Moves each particle and decreases its alpha (opacity) so it fades away
if (particles[i].isFinished()) {
- Checks if a particle's alpha has reached 0 and the animation is complete
particles.splice(i, 1); // Remove finished particles
- Deletes the particle from the array so it stops being drawn and updated
if (lives <= 0) {
game_state = 'GAME_OVER';
}
- Checks if the player has run out of lives and transitions to the GAME_OVER state, which will display the game over screen on the next draw() call
function drawGameOverScreen() {
fill(255, 0, 0); // Red text
noStroke();
textSize(64);
text("GAME OVER!", width / 2, height / 2 - 100);
fill(255);
textSize(32);
text(`Final Score: ${score}`, width / 2, height / 2);
text("Press Space to Restart", width / 2, height / 2 + 100);
}
Line-by-line explanation (7 lines)
fill(255, 0, 0); // Red text
- Sets text color to red for the GAME OVER message
textSize(64);
- Makes the GAME OVER text large and prominent
text("GAME OVER!", width / 2, height / 2 - 100);
- Displays the game over message centered near the top of the screen
fill(255);
- Switches text color back to white for the score and restart instructions
textSize(32);
- Reduces text size for secondary information
text(`Final Score: ${score}`, width / 2, height / 2);
- Displays the player's final score in the center of the screen
text("Press Space to Restart", width / 2, height / 2 + 100);
- Displays restart instructions near the bottom
🔬 These two if-statements handle left and right movement. What happens if you change -= to += in the first one (so left makes the player go right)? What if you change keyIsDown(65) to keyIsDown(90) to use 'Z' instead of 'A'?
if (keyIsDown(LEFT_ARROW) || keyIsDown(65)) { // Left Arrow or 'A'
this.x -= this.speed;
}
if (keyIsDown(RIGHT_ARROW) || keyIsDown(68)) { // Right Arrow or 'D'
this.x += this.speed;
}
class Player {
constructor() {
this.size = 50;
this.speed = 5;
this.resetPosition();
}
resetPosition() {
this.x = width / 2;
this.y = height - this.size * 2;
}
update() {
// Move with arrow keys or WASD
if (keyIsDown(LEFT_ARROW) || keyIsDown(65)) { // Left Arrow or 'A'
this.x -= this.speed;
}
if (keyIsDown(RIGHT_ARROW) || keyIsDown(68)) { // Right Arrow or 'D'
this.x += this.speed;
}
if (keyIsDown(UP_ARROW) || keyIsDown(87)) { // Up Arrow or 'W'
this.y -= this.speed;
}
if (keyIsDown(DOWN_ARROW) || keyIsDown(83)) { // Down Arrow or 'S'
this.y += this.speed;
}
// Keep player within canvas bounds
this.x = constrain(this.x, this.size / 2, width - this.size / 2);
this.y = constrain(this.y, this.size / 2, height - this.size / 2);
}
draw() {
fill(0, 200, 255); // Blue spaceship
noStroke();
// Simple spaceship shape (triangle with a rectangle base)
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);
rectMode(CENTER);
rect(this.x, this.y + this.size / 4, this.size * 0.8, this.size * 0.5);
rectMode(CORNER);
}
shoot() {
bullets.push(new Bullet(this.x, this.y - this.size / 2));
laserSound.amp(0.5, 0.05); // Play laser sound
laserSound.amp(0, 0.1); // Decay laser sound
}
}
Line-by-line explanation (23 lines)
🔧 Subcomponents:
calculation
Player Constructor
constructor() { this.size = 50; this.speed = 5; this.resetPosition(); }
Initializes the player's size, speed, and position when a new Player is created
conditional
Movement Input Handling
if (keyIsDown(LEFT_ARROW) || keyIsDown(65))
Checks if the player pressed left arrow or 'A' key and moves the spaceship left
calculation
Boundary Constraint
this.x = constrain(this.x, this.size / 2, width - this.size / 2);
Limits the player's position so the spaceship never goes outside the canvas
calculation
Spaceship Drawing
triangle(...); rect(...);
Draws a blue triangle (nose) and rectangle (body) to create the spaceship shape
class Player {
- Defines a class called Player, which is a blueprint for creating player objects
constructor() { this.size = 50; this.speed = 5; this.resetPosition(); }
- The constructor method runs when a new Player is created; it sets size and speed properties and calls resetPosition()
resetPosition() {
- A method that moves the player to the center-bottom of the screen, used on startup and after collisions
this.x = width / 2;
- Sets the player's x position to the horizontal center of the canvas
this.y = height - this.size * 2;
- Places the player near the bottom of the screen (height minus twice the spaceship size for padding)
if (keyIsDown(LEFT_ARROW) || keyIsDown(65)) {
- Checks if the left arrow key OR the 'A' key (ASCII 65) is currently pressed
this.x -= this.speed;
- Moves the player left by subtracting the speed value from the x position
if (keyIsDown(RIGHT_ARROW) || keyIsDown(68)) {
- Checks for right arrow or 'D' key (ASCII 68)
this.x += this.speed;
- Moves the player right by adding the speed value to x
if (keyIsDown(UP_ARROW) || keyIsDown(87)) {
- Checks for up arrow or 'W' key (ASCII 87)
this.y -= this.speed;
- Moves the player up by subtracting speed from y (remember: y=0 is at the top in p5.js)
if (keyIsDown(DOWN_ARROW) || keyIsDown(83)) {
- Checks for down arrow or 'S' key (ASCII 83)
this.y += this.speed;
- Moves the player down by adding speed to y
this.x = constrain(this.x, this.size / 2, width - this.size / 2);
- Uses p5.js's constrain() function to limit x between the left and right edges, preventing the spaceship from going off-screen
this.y = constrain(this.y, this.size / 2, height - this.size / 2);
- Constrains y similarly to keep the spaceship within the top and bottom of the canvas
fill(0, 200, 255); // Blue spaceship
- Sets the fill color to cyan blue (0 red, 200 green, 255 blue in RGB)
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 three vertices: one at the top (the nose), and two at the bottom left and right (the wings)
rectMode(CENTER);
- Changes rect drawing mode so that coordinates refer to the rectangle's center instead of its top-left corner
rect(this.x, this.y + this.size / 4, this.size * 0.8, this.size * 0.5);
- Draws a rectangle below the triangle to form the spaceship's body
rectMode(CORNER);
- Resets rect mode back to the default (CORNER) so future rectangles aren't affected
bullets.push(new Bullet(this.x, this.y - this.size / 2));
- Creates a new Bullet at the player's position (starting from the top of the spaceship) and adds it to the bullets array
laserSound.amp(0.5, 0.05); // Play laser sound
- Immediately increases the laser sound's amplitude to 0.5 over 0.05 seconds, creating an attack phase
laserSound.amp(0, 0.1); // Decay laser sound
- Decreases the amplitude back to 0 over 0.1 seconds, creating a decay that makes the laser sound short and punchy
class Bullet {
constructor(x, y) {
this.x = x;
this.y = y;
this.size = 10;
this.speed = 10;
}
update() {
this.y -= this.speed;
}
draw() {
fill(255, 255, 0); // Yellow bullet
noStroke();
ellipse(this.x, this.y, this.size, this.size);
}
isOffScreen() {
return this.y < -this.size;
}
}
Line-by-line explanation (7 lines)
constructor(x, y) {
- The constructor receives x and y coordinates from where the bullet is fired
this.x = x; this.y = y;
- Stores the bullet's starting position (usually from the player's position)
this.size = 10; this.speed = 10;
- Sets the bullet's diameter and movement speed per frame
this.y -= this.speed;
- Moves the bullet upward by subtracting speed from y each frame
fill(255, 255, 0); // Yellow bullet
- Sets the bullet's color to yellow (255 red, 255 green, 0 blue)
ellipse(this.x, this.y, this.size, this.size);
- Draws a circle (ellipse with equal width and height) at the bullet's current position
return this.y < -this.size;
- Returns true if the bullet has gone off the top of the canvas (y is less than negative its size), allowing updateGame() to remove it
class Enemy {
constructor() {
this.size = 40;
this.x = random(this.size / 2, width - this.size / 2);
this.y = -this.size; // Start off-screen at the top
this.speed = random(2, 5); // Random speed
}
update() {
this.y += this.speed;
}
draw() {
fill(255, 0, 0); // Red enemy
noStroke();
// Simple enemy shape (rectangle with small wings)
rectMode(CENTER);
rect(this.x, this.y, this.size, this.size);
rect(this.x - this.size * 0.7, this.y, this.size * 0.4, this.size * 0.2);
rect(this.x + this.size * 0.7, this.y, this.size * 0.4, this.size * 0.2);
rectMode(CORNER);
}
isOffScreen() {
return this.y > height + this.size;
}
}
Line-by-line explanation (9 lines)
this.x = random(this.size / 2, width - this.size / 2);
- Picks a random x position across the width of the canvas, ensuring the enemy stays within bounds
this.y = -this.size; // Start off-screen at the top
- Positions the enemy above the top of the canvas so it scrolls in smoothly
this.speed = random(2, 5); // Random speed
- Gives each enemy a random speed between 2 and 5 pixels per frame, adding variety to the difficulty
this.y += this.speed;
- Moves the enemy downward each frame
fill(255, 0, 0); // Red enemy
- Sets the enemy's color to red
rect(this.x, this.y, this.size, this.size);
- Draws the main body of the enemy as a square
rect(this.x - this.size * 0.7, this.y, this.size * 0.4, this.size * 0.2);
- Draws a left wing (a small rectangle to the left of the body)
rect(this.x + this.size * 0.7, this.y, this.size * 0.4, this.size * 0.2);
- Draws a right wing (a small rectangle to the right of the body)
return this.y > height + this.size;
- Returns true if the enemy has fallen past the bottom of the canvas, so updateGame() can remove it and deduct a life
class Particle {
constructor(x, y) {
this.x = x;
this.y = y;
this.size = random(5, 15);
this.vx = random(-2, 2);
this.vy = random(-2, 2);
this.alpha = 255;
this.decayRate = random(3, 8);
}
update() {
this.x += this.vx;
this.y += this.vy;
this.alpha -= this.decayRate;
}
draw() {
noStroke();
fill(255, 100, 0, this.alpha); // Orange-red fading particles
ellipse(this.x, this.y, this.size, this.size);
}
isFinished() {
return this.alpha <= 0;
}
}
Line-by-line explanation (10 lines)
this.size = random(5, 15);
- Gives each particle a random diameter between 5 and 15 pixels
this.vx = random(-2, 2);
- Sets a random horizontal velocity for the particle, allowing it to scatter left, right, or stay centered
this.vy = random(-2, 2);
- Sets a random vertical velocity so particles spray upward and downward
this.alpha = 255;
- Starts the particle fully opaque (alpha 255); it will fade as decayRate decreases it
this.decayRate = random(3, 8);
- Each particle fades at a different speed—some last longer than others, adding visual variety
this.x += this.vx; this.y += this.vy;
- Moves the particle in its velocity direction each frame
this.alpha -= this.decayRate;
- Decreases the alpha value each frame, making the particle gradually transparent
fill(255, 100, 0, this.alpha); // Orange-red fading particles
- Fills particles with orange-red color (255 red, 100 green, 0 blue) and the current alpha, so they fade as alpha decreases
ellipse(this.x, this.y, this.size, this.size);
- Draws the particle as a circle
return this.alpha <= 0;
- Returns true when the particle is fully transparent, allowing updateGame() to remove it from the particles array