I’ve been working on some fun little p5.js sketches recently and wanted somewhere to host them. And so this: InsomniacPhysicist’s code-y compSci-y sister site. Everything’s still under construction, and I’m mostly just using this post to figure out how it’s all going to work. If you want to entertain yourself in the meantime, you can play some snake or minesweeper.
I want my sketch.js to look something like this
1 2 3 4 5 6 7 8 9 10 |
let s; function setup() { createCanvas(800, 600); s = new Snake(width, height); } function draw() { s.show(); } |
We’re going to need a snake class to achieve this
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
class Snake { constructor(playWidth, playHeight) { // Map resolution in pixels this.mapX = playWidth; this.mapY = playHeight; } show() { // Update the game state this.update(); //Draw all the various elements here } update() { } |
Nailed it. Snake is played on a grid (which I’d like to be visible), so a reasonable place to start is to define the grid size and draw the background. We add this.gridSize = 20; to the constructor, and call this.drawBoard(); in show()
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
// Draws the background and faint lines on the play area drawBoard() { // Fill the background background(10); // Set the line colour stroke(30); // Vertical lines for (let i = 0; i < this.mapX / this.gridSize; i++) { line(i * this.gridSize, 0, i * this.gridSize, this.mapY); } // Horizontal lines for (let i = 0; i < this.mapY / this.gridSize; i++) { line(0, i * this.gridSize, this.mapX, i * this.gridSize); } } |
Next, our snake is going to need some food. I’m going to use a P5 vector to store its position, write a function to randomize its position on the board (for when it gets eaten), and add a couple of lines to draw() to place it as a white rectangle. Click the bar to see the full code.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 |
class Snake { constructor(playWidth, playHeight) { this.mapX = playWidth; this.mapY = playHeight; // Pixel size of board squares this.gridSize = 20; // These are just for convenience; number of gridsquares in x and y this.gridX = this.mapX / this.gridSize; this.gridY = this.mapY / this.gridSize; // Position of Food this.food = createVector(0, 0); this.randomizeFood(); } // Update positions and then draw everything show() { // Update the game state this.update(); //Draw all the various elements here this.drawBoard(); // Draw the Food fill(255); rect(this.food.x, this.food.y, this.gridSize, this.gridSize) } // Update the snake's position, check for collisions etc update() { } // Draws the background and faint lines on the play area drawBoard() { // Fill the background background(10); // Set the line colour stroke(30); // Vertical lines for (let i = 0; i < this.mapX / this.gridSize; i++) { line(i * this.gridSize, 0, i * this.gridSize, this.mapY); } // Horizontal lines for (let i = 0; i < this.mapY / this.gridSize; i++) { line(0, i * this.gridSize, this.mapX, i * this.gridSize); } } // Place the food at a random position on the grid randomizeFood() { this.food.x = floor(random(this.gridX)) * this.gridSize; this.food.y = floor(random(this.gridY)) * this.gridSize; } } |

So now we’re here.
This looks good, but we still don’t have a snake! We need a vector for the head, an array for the tail, and an integer length. We also need a vector for its current velocity. Then, we need a function to give all that good stuff some reasonable initial values:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
// Initially place food and snake initializePositions() { // Place the snake's head at least a tail length away from the side this.head.x = floor(random(this.gridX - this.length)) * this.gridSize; this.head.y = floor(random(this.gridY - this.length)) * this.gridSize; // Give the snake a random travel direction let v1 = createVector(0, 1); let v2 = createVector(1, 0); let v3 = createVector(0, -1); let v4 = createVector(-1, 0); this.velocity = random([v1, v2, v3, v4]); // Create the tail for (let i = 1; i < this.length + 1; i++) { this.tail.push(createVector( this.head.x + this.velocity.x * i * this.gridSize, this.head.y + this.velocity.y * i * this.gridSize)); } // Place the food this.randomizeFood(); } } |
For movement this was the simplest solution I could come up with for having the tail “follow” the head:
- Destroy last tail segment
- Add the head to the front of the tail
- Move the head one square in the direction of its velocity
1 2 3 4 5 6 |
moveSnake() { this.tail.pop(); this.tail.unshift(createVector(this.head.x, this.head.y)); let shift = p5.Vector.mult(this.velocity, this.gridSize); this.head.add(shift); } |
Next we need a way to control the snake. I decided to use a switch, like this:
1 2 3 4 5 6 7 8 9 |
keyPressed() { switch(keyCode) { case LEFT_ARROW: this.vx = 1; this.vy = 0; break; //...etc for the other directions } } |
And then in my sketch.js file I simply need to call this whenever a key is pushed
1 2 3 |
function keyPressed() { s.keyPressed(); } |
However, this implementation has an issue: if we’re going left and hit
RIGHT_ARROW, our snake is going to turn 180 degrees and die. Adding an if statement to check for this partially solves the problem, but if the user inputs two commands during a single frame, they can still turn the snake 180 degrees. The way I chose to fix this was an “acceleration” vector; essentially storing the user’s command and then using it to update the velocity once per frame (an equivalent method would have been some sort of “allowInput” flag, that gets set to false when the velocity is changed).
So, the keyPressed switch now looks like this
1 2 3 4 5 6 7 8 9 10 11 |
keyPressed() { switch (keyCode) { case LEFT_ARROW: if (this.velocity.x != 1) { this.acceleration.x = -1; this.acceleration.y = 0; } break; //...etc for the other directions } } |
And the update function pushes the “acceleration” to the snake’s velocity once per frame
1 2 3 4 5 |
// Update the snake's position, check for collisions etc update() { this.velocity = createVector(this.acceleration.x, this.acceleration.y); this.moveSnake(); } |
Since the snake’s velocity is tied to how often we update, I set the frame rate to 15 in sketch.js’s setup() function to get a reasonable speed.
Woohoo! Moving, controllable snake. Now we need collision.
There’s two types of collision event we need to handle – the snake’s death (by hitting the wall or itself), and the snake eating the food and growing longer. We’ll start with the latter, since it’s considerably more straightforward.
1 2 3 4 5 6 7 8 |
// If the head is on the food, move the food and add a tail piece eat() { if (this.head.equals(this.food)) { this.length ++; this.tail.push(this.tail[0]); this.randomizeFood(); } } |
Here, we just check if the head’s position vector is the same as the food’s position vector, and if it is, we increment the length, add a tail segment (by duplicating one of the existing ones; the update function will set its position the next frame anyway), and randomize the food’s position. Easy peasy. To kill the snake, we need to reset its length and randomize its position (I’ve changed the starting length to 8 in order to make it easier to test collision; hitting yourself with a length 5 snake is extremely difficult).
1 2 3 4 5 6 7 8 9 10 11 12 13 |
reset() { this.length = 8; this.tail = []; // Place the snake's head at least a snake length away from the side this.head.x = floor(random(this.gridX - this.length)) * this.gridSize; this.head.y = floor(random(this.gridY - this.length)) * this.gridSize; // Create the tail segments for (let i = 1; i < this.length + 1; i++) { this.tail.push(createVector( this.head.x - this.velocity.x * i * this.gridSize, this.head.y - this.velocity.y * i * this.gridSize)); } } |
Now we need to check if the snake has it itself or a wall, and call reset if it has.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
// Check if the head is touching any tail pieces selfCollision() { for (let i = 0; i < this.tail.length; i++) { if (this.tail[i].equals(this.head)) { return true; } } return false; } // Check for collision or leaving the board collision() { if ( this.selfCollision() || this.head.x < 0 || this.head.y < 0 || this.head.x > (this.mapX - this.gridSize) || this.head.y > (this.mapY - this.gridSize)) { this.reset(); } } |
Now all that’s left to do is call eat() and collision() in the update function and we’re done!
1 2 3 4 5 6 7 |
// Check if the head is touching any tail pieces update() { this.velocity = createVector(this.acceleration.x, this.acceleration.y); this.moveSnake(); this.collision(); this.eat(); } |
There are a few things I still want to add to this: first, considering the size of the play area the game is much too easy – I think it’d be fun to add a set of increasingly difficult obstacles like the original snake campaign mode. Secondly, particularly if I’m making the game harder, I’d like to make the controls more responsive. Currently running at 15 fps is necessary so the snake moves at a reasonable speed, but I’d like to decouple its speed from the framerate so I can run the game at 30 or 60fps instead. This would allow the snake’s speed to increase with increasing length, and also make the controls feel much more responsive. I’ll tackle these in a future post.
Thanks for reading!