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
returnexits 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 triggersnewHead.x < 0- Hit left wallnewHead.x >= cols- Hit right wall (cols = 20, so position 20 is out of bounds)newHead.y < 0- Hit top wallnewHead.y >= rows- Hit bottom wall
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 = trueand exit
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/2is the center of the canvas-40moves 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