This is a classic Snake game where players control a growing snake using arrow keys to eat food and increase their score. The game ends when the snake hits a wall or itself, and can be restarted by pressing 'R'.
Make the snake move faster — Higher frameRate values make draw() run more frequently, so the snake updates position more often and appears to move faster.
Change the grid size to make the game harder — A smaller grid means the snake moves smaller distances and the game becomes more precise and challenging.
Change the snake's color to blue — The fill() function sets the color for shapes drawn afterward—RGB values control the exact color.
Make the food spawn faster by removing collision checks — If you remove the while loop that checks for snake overlap, food can spawn on the snake (but the game still works).
This sketch implements the timeless Snake game using p5.js fundamentals. A player-controlled snake moves across a grid, eating pink food squares to grow longer and rack up points. The game combines keyboard input handling, collision detection (both with walls and the snake's own body), and dynamic array management to track the snake's segments. It demonstrates how simple rules—move forward, eat to grow, die on collision—create an engaging interactive experience.
The code is organized into a setup() function that initializes the snake and game state, a draw() function that runs the game loop, and specialized helper functions for movement, collision checking, food placement, and game-over handling. By studying this sketch, you'll learn how to manage arrays of game objects, use vectors for direction and position, implement keyboard controls with validation, and structure a complete game with win/lose conditions.
⚙️ How It Works
When the sketch loads, setup() creates a canvas, initializes a snake array with three segments positioned at the center, and picks a random food location that doesn't overlap the snake.
Every frame (10 times per second by default), draw() clears the background and checks if the game is over—if so, it displays the game-over screen and stops.
updateSnake() moves the snake forward by creating a new head in the current direction, adding it to the front of the array, and removing the tail (unless food was just eaten, which causes growth).
The sketch checks three collision conditions: if the head reaches food, the score increases and the snake grows; if the head hits a wall boundary, the game ends; if the head touches any other segment of the snake's body, the game ends.
The snake and food are drawn as colored rectangles on a grid, and the current score displays in the top-left corner.
Keyboard input (arrow keys and 'R' for restart) is handled by keyPressed(), which updates the direction vector while preventing the snake from reversing directly into itself.
🎓 Concepts You'll Learn
Game loop with draw()Array management for linked segmentsVector-based position and directionCollision detection (walls and self)Keyboard input handlingGame state management
📝 Code Breakdown
setup()
setup() runs once when the sketch starts. It prepares the canvas, initializes the snake array with three segments, and places the first food. Notice how the snake's starting position is calculated to snap to the grid—this ensures every movement stays aligned to the scale.
function setup() {
createCanvas(windowWidth, windowHeight);
// Tip: Use windowWidth/windowHeight for responsive canvas
direction = createVector(0, 0); // Initial direction (not moving)
frameRate(10); // Snake speed (frames per second)
// Initialize snake
snake = [];
let headX = floor(width / 2 / scale) * scale;
let headY = floor(height / 2 / scale) * scale;
snake.push(createVector(headX, headY)); // Head
// Add a few segments for starting length
snake.push(createVector(headX - scale, headY));
snake.push(createVector(headX - 2 * scale, headY));
// Place initial food
pickLocation();
}
Adds two more segments to the left of the head, creating a horizontal snake of length 3
pickLocation();
Calls the function that randomly places food somewhere on the grid
draw()
draw() is the game loop—it runs 10 times per second. Every frame, it checks collisions, updates movement, and redraws everything. The early return when gameIsOver is true prevents the game from continuing after you've lost.
🔬 This loop draws the entire snake. What happens if you change rect(snake[i].x, snake[i].y, scale, scale) to circle(snake[i].x + scale/2, snake[i].y + scale/2, scale)? Why does the snake look rounder?
for (let i = 0; i < snake.length; i++) {
fill(0, 255, 0); // Green snake
if (i === 0) {
fill(0, 200, 0); // Darker green for head
}
noStroke();
rect(snake[i].x, snake[i].y, scale, scale);
}
function draw() {
background(220);
if (gameIsOver) {
displayGameOver();
return; // Stop draw loop if game is over
}
// Update snake position
updateSnake();
// Check for food collision
if (snake[0].x === food.x && snake[0].y === food.y) {
score++;
pickLocation();
growSnake();
}
// Check for wall collision
checkWallCollision();
// Check for self-collision
checkSelfCollision();
// Draw snake
for (let i = 0; i < snake.length; i++) {
fill(0, 255, 0); // Green snake
if (i === 0) {
fill(0, 200, 0); // Darker green for head
}
noStroke();
rect(snake[i].x, snake[i].y, scale, scale);
}
// Draw food
fill(255, 0, 100); // Pink food
noStroke();
rect(food.x, food.y, scale, scale);
// Display score
fill(0);
textSize(24);
textAlign(LEFT, TOP);
text("Score: " + score, 10, 10);
}
Line-by-line explanation (19 lines)
🔧 Subcomponents:
conditionalGame over state checkif (gameIsOver) {
displayGameOver();
return; // Stop draw loop if game is over
}
If the game has ended, display the game-over screen and exit early, skipping all game logic
Checks if the snake's head touches food; if so, increases score, places new food, and grows the snake
for-loopDraw all snake segmentsfor (let i = 0; i < snake.length; i++) {
fill(0, 255, 0); // Green snake
if (i === 0) {
fill(0, 200, 0); // Darker green for head
}
noStroke();
rect(snake[i].x, snake[i].y, scale, scale);
}
Loops through every segment in the snake array and draws each as a green rectangle, with the head in darker green
background(220);
Clears the canvas with a light gray background, erasing the previous frame so the snake appears to move instead of leaving a trail
if (gameIsOver) {
Checks whether the game state is over
displayGameOver();
Calls the function that draws the game-over message and final score
return; // Stop draw loop if game is over
Exits the draw() function early, skipping all game updates and preventing further movement
updateSnake();
Calls the function that moves the snake forward in the current direction by one grid cell
if (snake[0].x === food.x && snake[0].y === food.y) {
Checks if the head's position (snake[0]) exactly matches the food's position
score++;
Increases the score by 1 point
pickLocation();
Randomly places new food on the grid
growSnake();
Calls the function that makes the snake grow (though the actual growth happens in updateSnake)
checkWallCollision();
Checks if the head has moved beyond the canvas boundaries and ends the game if so
checkSelfCollision();
Checks if the head has collided with any other segment of the snake's body
for (let i = 0; i < snake.length; i++) {
Loops through each segment in the snake array, counting from the head (index 0) to the tail
fill(0, 255, 0); // Green snake
Sets the fill color to bright green for all segments
if (i === 0) {
Checks if this is the head segment (the first element in the array)
fill(0, 200, 0); // Darker green for head
Changes the fill color to a darker green specifically for the head, making it visually distinct
rect(snake[i].x, snake[i].y, scale, scale);
Draws a square at the segment's position with width and height both equal to scale
fill(255, 0, 100); // Pink food
Sets the fill color to pink for the food
rect(food.x, food.y, scale, scale);
Draws the food as a pink square at the same grid size as the snake segments
text("Score: " + score, 10, 10);
Displays the current score in the top-left corner of the canvas
updateSnake()
updateSnake() is the movement engine. It creates a new head in the direction the player is moving, adds it to the front of the array with unshift(), and removes the tail with pop() unless food was just eaten. This simple push-and-pop mechanism creates the illusion of smooth motion.
🔬 This code controls growth: the tail is only removed if food wasn't eaten. What happens if you change snake.pop() to not execute—e.g., always keep the tail? Try removing the line entirely to make the snake grow every frame.
// Remove the tail segment (unless we just ate food)
if (!checkFoodCollision()) { // Check if the new head is on food
snake.pop();
}
function updateSnake() {
// Only move if a direction is set
if (direction.x === 0 && direction.y === 0) {
return;
}
// Create a new head position
let head = snake[0].copy();
head.x += direction.x * scale;
head.y += direction.y * scale;
// Add the new head to the beginning of the snake
snake.unshift(head);
// Remove the tail segment (unless we just ate food)
if (!checkFoodCollision()) { // Check if the new head is on food
snake.pop();
}
}
Prevents movement until the player presses an arrow key
calculationNew head positionlet head = snake[0].copy();
head.x += direction.x * scale;
head.y += direction.y * scale;
Copies the current head and adds the direction multiplied by scale to get the next grid position
conditionalConditional tail removalif (!checkFoodCollision()) { // Check if the new head is on food
snake.pop();
}
Removes the tail only if the head didn't land on food—if it did, the snake grows by one segment
if (direction.x === 0 && direction.y === 0) {
Checks if the direction is (0, 0), meaning no arrow key has been pressed yet
return;
Exits the function early, so the snake doesn't move until a direction is set
let head = snake[0].copy();
Creates a copy of the current head vector—.copy() ensures we don't modify the original head
head.x += direction.x * scale;
Moves the new head horizontally: if direction.x is 1, move right by scale; if -1, move left by scale; if 0, don't move horizontally
head.y += direction.y * scale;
Moves the new head vertically in the same way
snake.unshift(head);
Adds the new head to the beginning of the snake array, pushing all other segments back one position
if (!checkFoodCollision()) { // Check if the new head is on food
Calls checkFoodCollision() and only proceeds if it returns false (no food was eaten)
snake.pop();
Removes the last segment (tail) from the snake array, keeping the length constant
checkFoodCollision()
This helper function checks if the snake's head has reached the food. It's used both in draw() to trigger growth and in updateSnake() to prevent tail removal when food is eaten.
Returns true if the head's x and y coordinates exactly match the food's position, false otherwise. The && operator requires both conditions to be true.
growSnake()
growSnake() is currently empty because the actual growth logic is handled in updateSnake(). When food is eaten, we simply skip the snake.pop() line, leaving an extra segment. You could add explicit growth logic here, like snake.push(snake[snake.length - 1].copy()), to be more explicit.
function growSnake() {
// When food is eaten, the snake naturally grows because we don't pop the tail
// You could also explicitly add a new segment at the tail if desired
}
Line-by-line explanation (1 lines)
// When food is eaten, the snake naturally grows because we don't pop the tail
This comment explains that growth is actually handled in updateSnake()—by not removing the tail when food is eaten, the snake gets one segment longer
pickLocation()
pickLocation() ensures food never spawns on top of the snake by using a while loop that keeps trying random positions until it finds one that doesn't collide. This is a common game-dev pattern called 'rejection sampling'.
🔬 This code picks random grid positions. What happens if you remove the '* scale' multiplications so food can appear at any pixel position instead of aligning to the grid?
// Keep picking until food is not on the snake
while (true) {
let x = floor(random(cols)) * scale;
let y = floor(random(rows)) * scale;
food = createVector(x, y);
function pickLocation() {
let cols = floor(width / scale);
let rows = floor(height / scale);
// Keep picking until food is not on the snake
while (true) {
let x = floor(random(cols)) * scale;
let y = floor(random(rows)) * scale;
food = createVector(x, y);
let onSnake = false;
for (let i = 0; i < snake.length; i++) {
if (food.x === snake[i].x && food.y === snake[i].y) {
onSnake = true;
break;
}
}
if (!onSnake) {
break; // Found a valid location
}
}
}
Calculates how many grid cells fit horizontally and vertically on the canvas
while-loopValid food placementwhile (true) {
let x = floor(random(cols)) * scale;
let y = floor(random(rows)) * scale;
food = createVector(x, y);
let onSnake = false;
for (let i = 0; i < snake.length; i++) {
if (food.x === snake[i].x && food.y === snake[i].y) {
onSnake = true;
break;
}
}
if (!onSnake) {
break; // Found a valid location
}
}
Repeatedly picks random grid positions until one is found that doesn't overlap with the snake
let cols = floor(width / scale);
Divides the canvas width by the grid cell size to find how many columns fit
let rows = floor(height / scale);
Divides the canvas height by the grid cell size to find how many rows fit
while (true) {
Starts an infinite loop that will continue until a valid food position is found
let x = floor(random(cols)) * scale;
Picks a random integer from 0 to cols-1, then multiplies by scale to get a grid-aligned x coordinate
let y = floor(random(rows)) * scale;
Picks a random integer from 0 to rows-1, then multiplies by scale to get a grid-aligned y coordinate
food = createVector(x, y);
Creates a vector at the random position and stores it in the global food variable
let onSnake = false;
Initializes a flag to track whether this food position overlaps with the snake
for (let i = 0; i < snake.length; i++) {
Loops through every segment of the snake to check if any overlap with the food
if (food.x === snake[i].x && food.y === snake[i].y) {
Checks if this snake segment's position exactly matches the food position
onSnake = true;
Sets the flag to true, indicating that food overlaps with the snake
break;
Exits the for loop early since we've already found an overlap—no need to check further
if (!onSnake) {
Checks if onSnake is false, meaning the food position is valid
break; // Found a valid location
Exits the while loop, ending the function and keeping the current food position
checkWallCollision()
checkWallCollision() is called every frame to detect if the snake has hit a boundary. If any part of the head is outside the canvas, the game ends immediately.
function checkWallCollision() {
let head = snake[0];
if (head.x < 0 || head.x >= width || head.y < 0 || head.y >= height) {
gameOver();
}
}
Checks if the head's position is outside the canvas: x < 0 (left edge), x >= width (right edge), y < 0 (top edge), or y >= height (bottom edge). The || operator means any one of these conditions triggers the if block.
gameOver();
Calls the gameOver() function to end the game
checkSelfCollision()
checkSelfCollision() detects when the snake crashes into its own body. By starting the loop at index 1, we avoid checking the head against itself. Once a collision is found, we break out of the loop since there's no need to check further.
function checkSelfCollision() {
let head = snake[0];
for (let i = 1; i < snake.length; i++) {
if (head.x === snake[i].x && head.y === snake[i].y) {
gameOver();
break;
}
}
}
Line-by-line explanation (5 lines)
🔧 Subcomponents:
for-loopBody segment collision checkfor (let i = 1; i < snake.length; i++) {
if (head.x === snake[i].x && head.y === snake[i].y) {
gameOver();
break;
}
}
Loops through all body segments (starting from index 1, skipping the head) and checks if any overlap with the head
let head = snake[0];
Stores a reference to the snake's head for comparison
for (let i = 1; i < snake.length; i++) {
Loops through all segments starting from index 1 (the first body segment after the head). Starting at 1 prevents the head from colliding with itself.
if (head.x === snake[i].x && head.y === snake[i].y) {
Checks if the head's position exactly matches this body segment's position
gameOver();
Ends the game if a collision is detected
break;
Exits the loop early since the game is already over
gameOver()
gameOver() is called when the snake hits a wall or itself. It sets the game state flag and stops the draw loop to freeze gameplay.
function gameOver() {
gameIsOver = true;
noLoop(); // Stop the draw loop
}
Line-by-line explanation (2 lines)
gameIsOver = true;
Sets the global gameIsOver flag to true, signaling that the game has ended
noLoop(); // Stop the draw loop
Calls p5.js's noLoop() function, which stops draw() from running, freezing the game state
displayGameOver()
displayGameOver() is called from draw() when gameIsOver is true. It displays three lines of text at different sizes and positions, overlaying the game over state on the canvas.
function displayGameOver() {
fill(255, 0, 0); // Red text
textSize(64);
textAlign(CENTER, CENTER);
text("GAME OVER", width / 2, height / 2 - 40);
textSize(32);
text("Score: " + score, width / 2, height / 2 + 20);
textSize(24);
text("Press R to Restart", width / 2, height / 2 + 80);
}
Line-by-line explanation (8 lines)
fill(255, 0, 0); // Red text
Sets the text color to red
textSize(64);
Sets the font size for the next text() call to 64 pixels
textAlign(CENTER, CENTER);
Aligns the text to be centered both horizontally and vertically
text("GAME OVER", width / 2, height / 2 - 40);
Displays 'GAME OVER' in large text at the center of the canvas, positioned slightly above the vertical center
textSize(32);
Reduces the font size to 32 pixels for the next text
Reduces the font size to 24 pixels for the restart instruction
text("Press R to Restart", width / 2, height / 2 + 80);
Displays instructions to press R, positioned below the score
keyPressed()
keyPressed() is called whenever a key is pressed. It updates the direction vector based on arrow key input, but includes safety checks to prevent the snake from reversing directly into itself—a common pitfall in snake games. The 'R' key handling lets players restart after a game over.
🔬 These conditions prevent the snake from reversing (e.g., direction.x !== 1 stops a left move when moving right). What happens if you remove these reversal checks, like changing 'direction.x !== 1' to 'true'? Can you make the snake crash into itself?
Updates the direction vector based on arrow key input while preventing the snake from reversing into itself
conditionalRestart key handling} else if (keyCode === 82 && gameIsOver) { // 'R' key to restart
restartGame();
}
Detects when 'R' is pressed after game over and resets the game
if (keyCode === LEFT_ARROW && direction.x !== 1) {
Checks if the left arrow was pressed AND the snake is not currently moving right (direction.x !== 1). This prevents the snake from instantly reversing and colliding with itself.
direction.set(-1, 0);
Sets the direction vector to (-1, 0), meaning move left (negative x) with no vertical movement
Checks if the down arrow was pressed AND the snake is not moving up, then sets direction to (0, 1)
} else if (keyCode === 82 && gameIsOver) { // 'R' key to restart
Checks if the 'R' key (keyCode 82) was pressed AND the game is currently over
restartGame();
Calls the restartGame() function to reset the game state and resume play
restartGame()
restartGame() is called when the player presses 'R' after a game over. It mirrors the initialization in setup(), resetting all game state variables and restarting the draw loop with loop().
function restartGame() {
score = 0;
gameIsOver = false;
direction.set(0, 0); // Reset direction
frameRate(10); // Reset speed
// Reinitialize snake
snake = [];
let headX = floor(width / 2 / scale) * scale;
let headY = floor(height / 2 / scale) * scale;
snake.push(createVector(headX, headY));
snake.push(createVector(headX - scale, headY));
snake.push(createVector(headX - 2 * scale, headY));
pickLocation(); // Place new food
loop(); // Resume the draw loop
}
Line-by-line explanation (10 lines)
🔧 Subcomponents:
calculationGame state initializationscore = 0;
gameIsOver = false;
direction.set(0, 0); // Reset direction
frameRate(10); // Reset speed
Resets all game variables to their starting values
Adds three new segments to create the starting snake at the center of the canvas
pickLocation(); // Place new food
Calls pickLocation() to place food on the grid
loop(); // Resume the draw loop
Calls p5.js's loop() function to resume the draw loop, which was stopped by noLoop() when the game ended
windowResized()
windowResized() is a p5.js callback that runs whenever the browser window is resized. In this sketch, it resizes the canvas and re-centers the snake and food if the game is active, ensuring the game remains playable at any window size.
function windowResized() {
resizeCanvas(windowWidth, windowHeight);
// Called when preview panel is resized
// Re-center snake and food if the canvas size changes drastically
if (!gameIsOver) {
let headX = floor(width / 2 / scale) * scale;
let headY = floor(height / 2 / scale) * scale;
snake[0].set(headX, headY);
pickLocation();
}
}
Re-centers the snake's head and places new food if the window is resized during active gameplay
resizeCanvas(windowWidth, windowHeight);
Calls p5.js's resizeCanvas() function to update the canvas dimensions to match the new window size
if (!gameIsOver) {
Only re-centers the snake if the game is active (not in a game-over state)
let headX = floor(width / 2 / scale) * scale;
let headY = floor(height / 2 / scale) * scale;
Recalculates the center position based on the new canvas dimensions
snake[0].set(headX, headY);
Moves the snake's head to the new center position
pickLocation();
Places food at a new random location on the resized canvas
📦 Key Variables
snakearray
An array of p5.Vector objects representing each segment of the snake's body. The first element (snake[0]) is the head.
let snake = [];
foodobject
A p5.Vector storing the current position of the food on the grid. Updated by pickLocation().
let food;
scorenumber
Tracks the player's score, incremented by 1 each time food is eaten.
let score = 0;
scalenumber
The size of each grid cell in pixels. Controls the size of snake segments, food, and movement distance per frame.
let scale = 20;
directionobject
A p5.Vector storing the current movement direction. x and y are each -1, 0, or 1.
let direction;
gameIsOverboolean
A flag indicating whether the game has ended. When true, the game-over screen is displayed instead of normal gameplay.
let gameIsOver = false;
🔧 Potential Improvements (5)
Here are some ways this code could be enhanced:
BUGkeyPressed() and updateSnake() interaction
If the player presses two arrow keys quickly (e.g., UP then LEFT), the second input can reverse the snake into itself before the next updateSnake() call, causing immediate self-collision. This happens because the direction is set before the next movement occurs.
💡 Store the next intended direction separately and only apply it in updateSnake() after checking for valid moves. This is called 'input buffering' and is standard in Snake games.
BUGcheckWallCollision()
The collision boundary check uses >= for the right edge but < 0 for the left. For consistency and clarity, both should check against the grid-aligned boundaries to avoid off-by-one errors as the snake grows.
💡 Consider using a more explicit boundary check: if (head.x + scale > width || head.x < 0 || head.y + scale > height || head.y < 0) to account for the snake segment's size.
PERFORMANCEpickLocation() while loop
As the snake grows longer (especially near 400+ segments), the while loop in pickLocation() may iterate many times before finding a valid spot, slowing down the game.
💡 Consider stopping the game when the snake reaches a certain length, or use a more efficient algorithm (e.g., maintain a list of occupied cells instead of looping through the entire snake each time).
STYLEcheckSelfCollision() and checkWallCollision()
Both functions set head = snake[0] at the start. This logic could be simplified or merged since they're called sequentially.
💡 Consider creating a single 'updateGameState()' function that checks all collisions at once, making the code more modular and easier to maintain.
FEATURErestartGame() and setup()
Snake initialization code is duplicated in both setup() and restartGame(). Any changes to starting conditions must be updated in two places.
💡 Extract the snake initialization into a separate helper function like initializeSnake() and call it from both setup() and restartGame() to reduce code duplication.