Skip to main content

Step 5: Collision Detection & Game Over

Let's add game over conditions! The game should end when the worm hits a wall or runs into itself.

Game State Variable

Add a variable to track if the game is over:

// Grid settings
let cellSize = 20;
let cols, rows;

// Worm
let worm = [];
let xSpeed = 1;
let ySpeed = 0;

// Food
let food;

// Score
let score = 0;

// Game state
let gameOver = false;

Checking Wall Collisions

The worm hits a wall when it goes outside the grid boundaries:

  • Left wall: x < 0
  • Right wall: x >= cols
  • Top wall: y < 0
  • Bottom wall: y >= rows

Add this check to updateWorm():

function updateWorm() {
// Don't update if game is over
if (gameOver) return;

// Get the current head position
let head = worm[worm.length - 1];

// Calculate new head position
let newHead = {
x: head.x + xSpeed,
y: head.y + ySpeed
};

// Check wall collision
if (newHead.x < 0 || newHead.x >= cols ||
newHead.y < 0 || newHead.y >= rows) {
gameOver = true;
return;
}

// Add new head to the front
worm.push(newHead);

// Check if worm ate the food
if (newHead.x === food.x && newHead.y === food.y) {
score++;
createFood();
} else {
worm.shift();
}
}

Breaking it down:

Early Return if Game Over

if (gameOver) return;
  • If game is already over, don't update the worm
  • return exits the function immediately
  • Worm stops moving

Wall Boundary Checks

if (newHead.x < 0 || newHead.x >= cols || 
newHead.y < 0 || newHead.y >= rows)
  • || means "OR" - if ANY condition is true, it triggers
  • newHead.x < 0 - Hit left wall
  • newHead.x >= cols - Hit right wall (cols = 20, so position 20 is out of bounds)
  • newHead.y < 0 - Hit top wall
  • newHead.y >= rows - Hit bottom wall
info

Why >= and not >?

Grid positions go from 0 to 19 (for a 20×20 grid).

  • Position 19 is valid (last cell)
  • Position 20 is OUT OF BOUNDS

So we check >= 20 to catch when we're outside the grid!

Checking Self-Collision

The worm dies if its head touches any part of its body. We need to check if the new head position matches any existing segment (except we can skip the tail since it's about to be removed):

function updateWorm() {
if (gameOver) return;

let head = worm[worm.length - 1];

let newHead = {
x: head.x + xSpeed,
y: head.y + ySpeed
};

// Check wall collision
if (newHead.x < 0 || newHead.x >= cols ||
newHead.y < 0 || newHead.y >= rows) {
gameOver = true;
return;
}

// Check self-collision
for (let i = 0; i < worm.length; i++) {
if (newHead.x === worm[i].x && newHead.y === worm[i].y) {
gameOver = true;
return;
}
}

worm.push(newHead);

if (newHead.x === food.x && newHead.y === food.y) {
score++;
createFood();
} else {
worm.shift();
}
}

Breaking it down:

Looping Through Body Segments

for (let i = 0; i < worm.length; i++) {
if (newHead.x === worm[i].x && newHead.y === worm[i].y) {
gameOver = true;
return;
}
}
  • Check every segment in the worm
  • If new head matches ANY segment position, it's a collision
  • Set gameOver = true and exit
tip

We check ALL segments because:

  • Worm might turn back on itself
  • Could hit middle segments, not just the tail
  • Safer to check everything

Displaying Game Over Screen

Update draw() to show game over message:

function draw() {
background(50);

// Update worm position
updateWorm();

// Draw grid lines (optional)
stroke(80);
strokeWeight(1);
for (let i = 0; i < cols; i++) {
line(i * cellSize, 0, i * cellSize, height);
}
for (let i = 0; i < rows; i++) {
line(0, i * cellSize, width, i * cellSize);
}

// Draw the food
fill(255, 0, 0);
noStroke();
rect(food.x * cellSize, food.y * cellSize, cellSize, cellSize);

// Draw the worm
fill(0, 255, 0);
for (let i = 0; i < worm.length; i++) {
let segment = worm[i];
rect(segment.x * cellSize, segment.y * cellSize, cellSize, cellSize);
}

// Draw score
fill(255);
textSize(20);
textAlign(LEFT);
text('Score: ' + score, 10, 25);

// Draw game over screen
if (gameOver) {
fill(0, 0, 0, 200); // Semi-transparent black overlay
rect(0, 0, width, height);

fill(255);
textSize(40);
textAlign(CENTER, CENTER);
text('GAME OVER', width/2, height/2 - 40);

textSize(20);
text('Final Score: ' + score, width/2, height/2 + 10);
text('Press ENTER to restart', width/2, height/2 + 40);
}
}

Breaking it down:

Semi-Transparent Overlay

fill(0, 0, 0, 200);
rect(0, 0, width, height);
  • fill(0, 0, 0, 200) - Black with 200/255 opacity
  • Darkens the screen but you can still see the game
  • Creates a "paused" effect

Centered Text

textAlign(CENTER, CENTER);
text('GAME OVER', width/2, height/2 - 40);
  • Centers text both horizontally and vertically
  • width/2, height/2 is the center of the canvas
  • -40 moves it up a bit to make room for other text

Adding Restart Functionality

Let's allow players to restart with the Enter key:

function keyPressed() {
// Restart game
if (keyCode === ENTER && gameOver) {
resetGame();
return;
}

// Don't change direction if game is over
if (gameOver) return;

// Right arrow
if (keyCode === RIGHT_ARROW && xSpeed !== -1) {
xSpeed = 1;
ySpeed = 0;
}
// Left arrow
else if (keyCode === LEFT_ARROW && xSpeed !== 1) {
xSpeed = -1;
ySpeed = 0;
}
// Down arrow
else if (keyCode === DOWN_ARROW && ySpeed !== -1) {
xSpeed = 0;
ySpeed = 1;
}
// Up arrow
else if (keyCode === UP_ARROW && ySpeed !== 1) {
xSpeed = 0;
ySpeed = -1;
}
}

Reset Game Function

Add a function to reset everything:

function resetGame() {
// Reset worm
worm = [];
let startX = floor(cols / 2);
let startY = floor(rows / 2);
worm.push({x: startX, y: startY});

// Reset direction
xSpeed = 1;
ySpeed = 0;

// Reset score
score = 0;

// Reset game state
gameOver = false;

// Create new food
createFood();
}

This resets:

  • Worm position and length
  • Direction to moving right
  • Score to 0
  • Game over flag
  • Creates new food

Complete Code with Collision Detection

// Grid settings
let cellSize = 20;
let cols, rows;

// Worm
let worm = [];
let xSpeed = 1;
let ySpeed = 0;

// Food
let food;

// Score
let score = 0;

// Game state
let gameOver = false;

function setup() {
createCanvas(400, 400);
frameRate(10);

cols = width / cellSize;
rows = height / cellSize;

let startX = floor(cols / 2);
let startY = floor(rows / 2);
worm.push({x: startX, y: startY});

createFood();
}

function draw() {
background(50);

updateWorm();

// Draw grid lines (optional)
stroke(80);
strokeWeight(1);
for (let i = 0; i < cols; i++) {
line(i * cellSize, 0, i * cellSize, height);
}
for (let i = 0; i < rows; i++) {
line(0, i * cellSize, width, i * cellSize);
}

// Draw food
fill(255, 0, 0);
noStroke();
rect(food.x * cellSize, food.y * cellSize, cellSize, cellSize);

// Draw worm
fill(0, 255, 0);
for (let i = 0; i < worm.length; i++) {
let segment = worm[i];
rect(segment.x * cellSize, segment.y * cellSize, cellSize, cellSize);
}

// Draw score
fill(255);
textSize(20);
textAlign(LEFT);
text('Score: ' + score, 10, 25);

// Game over screen
if (gameOver) {
fill(0, 0, 0, 200);
rect(0, 0, width, height);

fill(255);
textSize(40);
textAlign(CENTER, CENTER);
text('GAME OVER', width/2, height/2 - 40);

textSize(20);
text('Final Score: ' + score, width/2, height/2 + 10);
text('Press ENTER to restart', width/2, height/2 + 40);
}
}

function updateWorm() {
if (gameOver) return;

let head = worm[worm.length - 1];

let newHead = {
x: head.x + xSpeed,
y: head.y + ySpeed
};

// Check wall collision
if (newHead.x < 0 || newHead.x >= cols ||
newHead.y < 0 || newHead.y >= rows) {
gameOver = true;
return;
}

// Check self-collision
for (let i = 0; i < worm.length; i++) {
if (newHead.x === worm[i].x && newHead.y === worm[i].y) {
gameOver = true;
return;
}
}

worm.push(newHead);

if (newHead.x === food.x && newHead.y === food.y) {
score++;
createFood();
} else {
worm.shift();
}
}

function createFood() {
let validPosition = false;

while (!validPosition) {
food = {
x: floor(random(cols)),
y: floor(random(rows))
};

validPosition = true;
for (let i = 0; i < worm.length; i++) {
if (food.x === worm[i].x && food.y === worm[i].y) {
validPosition = false;
break;
}
}
}
}

function resetGame() {
worm = [];
let startX = floor(cols / 2);
let startY = floor(rows / 2);
worm.push({x: startX, y: startY});

xSpeed = 1;
ySpeed = 0;

score = 0;
gameOver = false;

createFood();
}

function keyPressed() {
// Restart
if (keyCode === ENTER && gameOver) {
resetGame();
return;
}

if (gameOver) return;

// Right
if (keyCode === RIGHT_ARROW && xSpeed !== -1) {
xSpeed = 1;
ySpeed = 0;
}
// Left
else if (keyCode === LEFT_ARROW && xSpeed !== 1) {
xSpeed = -1;
ySpeed = 0;
}
// Down
else if (keyCode === DOWN_ARROW && ySpeed !== -1) {
xSpeed = 0;
ySpeed = 1;
}
// Up
else if (keyCode === UP_ARROW && ySpeed !== 1) {
xSpeed = 0;
ySpeed = -1;
}
}

Test It Out!

Play the complete game:

  • ✅ Hit a wall - game over!
  • ✅ Run into yourself - game over!
  • ✅ Game over screen appears with final score
  • ✅ Press Enter to restart
  • ✅ Everything resets properly

What You Learned

  • How to detect boundary/wall collisions
  • How to check for self-collision in arrays
  • How to use boolean flags for game state
  • How to create restart functionality
  • How to draw semi-transparent overlays
  • How to use early returns to prevent code execution
  • How to reset game state completely