It all started when I implemented a “Plane Wars” mini-game for a course assignment under the Windows console two semesters ago. After finishing it, I felt that such a game was far too rudimentary. Having learned Java Swing graphical interface programming last semester, I planned to rewrite the game using Swing, but I kept procrastinating and never got around to it.
At the beginning of this year, I finally decided to start. I researched materials on Swing and JavaFX (a newer Java GUI framework) and felt that JavaFX seemed somewhat lukewarm in popularity. Coupled with the extreme verbosity of GUI programming code, I began to hesitate. I suddenly realized that user interfaces are now dominated by mobile and web platforms, and I should be developing games for the web instead. I quickly found HTML Canvas. Despite having never written JavaScript code before, I learned as I went and managed to create a basically functional Snake game in just one day.
Welcome to the online demo, or visit the project’s GitHub homepage.
Canvas Rendering
Canvas is a tag introduced in HTML5. It serves as a graphic container where you can draw graphics using JavaScript scripts. In HTML5, SVG can also be used for drawing. Comparing the two, SVG is vector-based and can update automatically, while Canvas is pixel-based and renders pixel by pixel. Therefore, Canvas is more suitable for game development involving real-time animation.
The width and height of the Canvas element can be specified at the start or set dynamically via JavaScript. For development convenience, I fixed the game canvas width to 720 pixels and the height to 360 pixels. The side length of each small square in the grid is set to 24 pixels, meaning the entire screen is actually a 30x15 grid. When the snake moves on the screen, it moves one square at a time.
Updating the Canvas content requires manual refresh control by the programmer. I wrote a repaintAll function, which is called every time the snake moves to redraw the Canvas screen. The function code is shown below; readers unfamiliar with Canvas usage can skip it. For details on how to use Canvas, you can refer to W3School’s Canvas Tutorial and Canvas Reference Manual.
function repaintAll() {
// clear all contents
context.clearRect(0, 0, canvas.width, canvas.height);
// draw background color
context.fillStyle = color.background;
context.fillRect(0, 0, canvas.width, canvas.height)
// draw snake
for (var i = 0; i < snake.body.length; i++) {
paintPoint(snake.body[i], color.snakeBody);
}
paintPoint(snake.head, color.snakeHead);
// draw apple
context.fillStyle = color.apple;
context.beginPath();
context.arc(apple.x * squareSize + squareSize / 2, apple.y * squareSize + squareSize / 2,
squareSize / 2, 0, Math.PI * 2);
context.closePath();
context.fill();
}
function paintPoint(p, color) {
context.fillStyle = color;
context.fillRect(p.x * squareSize, p.y * squareSize, squareSize - 1, squareSize - 1);
}Game Logic Design
The snake is represented in the JavaScript code as a snake object. The snake object has four properties: head, body, dir, and dead. The head property records the coordinates of the snake’s head; the body property is an array of coordinates recording the points of the snake’s body; the dir property records the snake’s current movement direction; and the dead property records whether the snake has died.
When the snake moves forward, the body moves forward one square first, and then the head is moved to a new point. To move the body forward, simply copy each element of the body backward in sequence, and then copy the head to body[0], as shown in the code below:
// move snake body forward
for (var i = snake.body.length - 1; i > 0; i--) {
snake.body[i] = snake.body[i-1];
}
snake.body[0] = snake.head;
snake.head = newPoint; // move snake head forwardThis allows the snake to move normally without changing its body length. If the snake eats food and the body length needs to increase by one square, simply push an empty object to the end of the body before the movement code.
snake.body.push({}); // add a square to body
// move snake body forward
for (var i = snake.body.length - 1; i > 0; i--) {
snake.body[i] = snake.body[i-1];
}
snake.body[0] = snake.head;
snake.head = newPoint; // move snake head forwardThe above logic is encapsulated in a function called moveToNewPoint, which handles all movement logic:
function moveToNewPoint(newPoint) {
if (isOnSnake(newPoint)) {
// snake bites itself
gameOver();
return;
}
if (newPoint.x == apple.x && newPoint.y == apple.y) {
// reaches apple
addScore();
apple = generateApple();
snake.body.push({}); // add a square to body
}
// move snake body forward
for (var i = snake.body.length - 1; i > 0; i--) {
snake.body[i] = snake.body[i-1];
}
snake.body[0] = snake.head;
snake.head = newPoint; // move snake head forward
}If the point to be moved to is on the snake’s body, it means the snake has hit itself and died. Next, it checks if the target point overlaps with the food. If it eats the food, a square is added to the body. Finally, the snake moves forward. For movement in the four directions (up, down, left, right), simply pass the appropriate parameters to the moveToNewPoint function.
Implementation Challenges
There are restrictions on direction changes while the snake is moving. For example, if the snake is moving right, pressing the left or right arrow keys should not affect the snake’s direction. However, if the game is implemented incorrectly, a “sudden U-turn” bug might occur.
In the first version of the game, I used a fixed time interval to move the snake, and whenever the player pressed a direction key, the snake’s direction (snake.dir) was changed immediately. This creates a potential problem: suppose the snake is moving right, and within the time interval between two moves, the player presses the Up key and then the Left key. Both direction changes are technically valid, and snake.dir becomes “left.” Consequently, the snake will move left in the next step—a sudden reversal from right to left. According to the game logic, moving left means hitting its own body, causing an accidental death.
In reality, if a player performs that sequence, the snake should move up one square and then immediately move left. Because the direction change was controlled by directly modifying snake.dir, two rapid direction changes could not be recorded simultaneously. The solution to this problem is to use a FIFO “command queue” instrQueue. Every time a player presses a key, the direction is added to the queue; every time the snake is about to move, a command is taken from the queue to determine whether to change direction. This perfectly solves the issue.
Summary
My implementation of Snake still has several flaws. Two known issues are:
- There is a very small probability of an “undigested” visual effect where the snake eats the food, but the food remains in its original position.
- Pressing direction keys while the game is paused affects the snake’s direction once the game resumes.
As I am a JavaScript beginner, I wrote this game while constantly flipping through reference manuals. The initial code had various problems, but I later refactored it. Through this process, I gained some understanding of the JavaScript language, though I have decided to write more about it only after I have a deeper grasp of the language. The update date is TBD.
Welcome to the online demo, or visit the project’s GitHub homepage.