Game Development with HTML5
HTML5 gave us the canvas element. Yes, it's been available in Safari since before, but it wasn’t globally supported until more recently.
I have been playing with the idea of using the canvas to make a game. I know this has been done many times since it was introduced, but I wanted to see if I could do it. I wanted something simplistic I could build on. I decided to try Tetris as it is simple in its shapes and play mechanics. I’ll walk you through how I set it up and some of the basics of how it works.
Setting Up the Canvas and Grid: To lay the foundation for the Tetris game, I first created an HTML5 canvas element and obtained its 2D rendering context. The canvas serves as the visual playground where all game elements will be displayed. Additionally, I defined parameters for the game grid, including block dimensions, rows, and columns. The grid represents the playing field where Tetris shapes will be manipulated and positioned.
<canvas id="tetrisCanvas" width="300" height="600"></canvas>
const canvas = document.getElementById("tetrisCanvas");
const ctx = canvas.getContext("2d");
const blockWidth = 30;
const blockHeight = 30;
const rows = 20;
const cols = 10;
const grid = [];
In the above code snippet, I've set up the canvas element and obtained its 2D rendering context. I've also defined the dimensions of each block within the grid and specified the number of rows and columns.
Defining Tetris Shapes and Colors: Central to Tetris gameplay are the seven distinct shapes, each comprising four blocks. These shapes, represented as 2D arrays of binary values, dictate the arrangement of blocks. Additionally, we assign colors to each shape to enhance visual appeal and player engagement.
const shapesColors = ['cyan', 'purple', 'orange', 'blue', 'yellow', 'red', 'green']; const shapes = [ [[1, 1, 1, 1]], // I shape (cyan) [[1, 1, 1],[0, 1, 0]], // T shape (purple) [[1, 1, 1],[1, 0, 0]], // L shape (orange) [[1, 1, 1],[0, 0, 1]], // J shape (blue) [[1, 1],[1, 1]], // O shape (yellow) [[1, 1, 0],[0, 1, 1]], // Z shape (red) [[0, 1, 1],[1, 1, 0]] // S shape (green) ];
In the code snippet above, I've defined the shapes and their corresponding colors. Each shape is represented as a 2D array, where '1' denotes a block and '0' denotes an empty space.
Initializing the Game: At the onset of the game, I initialized essential variables such as the current level, score, and game grid. The grid is populated with empty cells, signifying the initial state of the playing field.
let level = 1;
let score = 0;
for (let row = 0; row < rows; row++) {
grid[row] = [];
for (let col = 0; col < cols; col++) {
grid[row][col] = 0; // 0 indicates an empty cell
}
}
In this code, I've initialized variables for the game's level and score. Additionally, I populated the game grid with empty cells using nested loops.
Drawing Functions: To render game elements on the canvas, I defined functions for drawing individual blocks, the game grid, and the current Tetris shape. These functions utilize the canvas context to draw shapes and colors effectively.
// Function to draw a Tetris block
function drawBlock(x, y, color) {
ctx.fillStyle = color;
ctx.fillRect(x * blockWidth, y * blockHeight, blockWidth, blockHeight);
ctx.strokeStyle = 'black';
ctx.strokeRect(x * blockWidth, y * blockHeight, blockWidth, blockHeight);
}
// Function to draw the Tetris grid
function drawGrid() {
for (let row = 0; row < rows; row++) {
for (let col = 0; col < cols; col++) {
const x = col;
const y = row;
const color = grid[row][col] === 0 ? 'white' : 'blue';
drawBlock(x, y, color);
}
}
}
// Function to draw the current Tetris shape
function drawCurrentShape() {
if (!currentShape) return;
currentShape.forEach((row, i) => {
row.forEach((cell, j) => {
if (cell) {
drawBlock(
currentCol + j,
currentRow + i,
shapesColors[currentShapeIndex]
);
}
});
});
}
Moving and Rotating Shapes: Central to Tetris gameplay is the ability to maneuver and rotate falling shapes. I implemented functions for moving the current shape left, right, and down and rotating it clockwise or counterclockwise. Collision detection ensures that shapes remain within the grid boundaries and do not intersect with existing blocks.
// Function to move the current shape down
function moveDown() {
if (!checkCollision(1, 0)) {
currentRow++;
update();
} else {
lockShape();
clearLines();
generateShape();
update();
}
}
// Function to move the current shape left
function moveLeft() {
if (!checkCollision(0, -1)) {
currentCol--;
update();
}
}
// Function to move the current shape right
function moveRight() {...}
// Function to rotate the current shape clockwise
function rotateClockwise() {
const rotatedShape = [];
for (let col = 0; col < currentShape[0].length; col++) {
const newRow = [];
for (let row = currentShape.length - 1; row >= 0; row--) {
newRow.push(currentShape[row][col]);
}
rotatedShape.push(newRow);
}
if (!checkCollision(0, 0, rotatedShape)) {
currentShape = rotatedShape;
update();
}
}
function rotateCounterClockwise() { ... }
Updating the Game: The update() function orchestrates the visual updates on the canvas. It clears the canvas, draws the game grid and the current shape, and updates the score and level display.
// Function to update the game
function update() {
clearCanvas();
drawGrid();
drawScoreAndLevel();
drawCurrentShape();
}
Game Over and Restart: In the event of a game over condition, functions handle the display of a "Game Over" message and provide a restart button to initiate a new game session.
// Function to handle game over
function gameOver() {
clearInterval(intervalId); // stop the game
clearCanvas(); // Clear the canvas
drawGameOverText(); // Draw "Game Over" text
drawRestartButton(); // Draw restart button
}
// Function to draw "Game Over" text on the canvas
function drawGameOverText() {
ctx.fillStyle = "white";
ctx.font = "40px Arial";
ctx.textAlign = "center";
ctx.fillText("Game Over", canvas.width / 2, canvas.height / 2);
}
// Function to draw restart button
function drawRestartButton() {
const buttonWidth = 150;
const buttonHeight = 50;
const buttonX = (canvas.width - buttonWidth) / 2;
const buttonY = (canvas.height + buttonHeight) / 2;
ctx.fillStyle = "green";
ctx.fillRect(buttonX, buttonY, buttonWidth, buttonHeight);
ctx.fillStyle = "white";
ctx.font = "20px Arial";
ctx.textAlign = "center";
ctx.fillText("Restart", canvas.width / 2, buttonY + 30);
// Add event listener to the restart button
canvas.addEventListener("click", startRestartGame);
}
Scoring and Leveling Up: Tetris employs scoring mechanics to reward players for clearing lines. As the number of lines the player clears crosses an increment of 10, the level advances, and the speed of falling shapes intensifies. Bonus points are awarded for clearing multiple lines simultaneously and it’s compounded by the level the user is currently playing on.
I found this on the Tetris WiKi page, “All levels from 1 to 10 increase the game speed. After level 10, the game speed only increases on levels 13, 16, 19, and 29, at which point the speed no longer increases.“ and for Scoring “The score received by each line clear is dependent on the level. Each type of clear, being a single, double, triple, or Tetris, has a base value that is multiplied by the number 1 higher than the current level. For any level n, a single will give 40 points, a double will give 100 points, a triple will give 300 points, and a Tetris will give 1200 points.“
Here are the functions for leveling up and scoring.
// Function to update the speed of falling shapes based on the current level
function updateSpeed() {
clearInterval(intervalId); // Clear the current interval
// All levels from 1 to 10 increase the game speed.
// After level 10, the game speed only increases on levels 13, 16, 19, and 29, at which point the speed no longer increases.
if ((level >= 0 && level <= 10) || level === 13 || level === 16 || level === 19 || level === 29) {
currentSpeed = currentSpeed - speedIncrement; // Adjust the speed
console.log('speed adjust ' + currentSpeed);
}
intervalId = setInterval(moveDownAuto, currentSpeed); // Set a new interval with the updated speed
}
// Function to update the score when lines are cleared
function updateScore(linesCleared) {
let pointsEarned;
// Award bonus points for multiple line clears
switch (linesCleared) {
case 2:
pointsEarned = 100 * (level + 1);
break;
case 3:
pointsEarned = 300 * (level + 1);
break;
case 4: // Tetris
pointsEarned = 1200 * (level + 1);
break;
default: // Single Line Clears
pointsEarned = 40 * (level + 1);
}
score += pointsEarned; // Update the score
drawScoreAndLevel(); // Update the score display
}
// Function to update the level based on lines cleared
function updateLevel(linesCleared) {
totalLinesCleared += linesCleared;
// Check if the total lines cleared has reached the next level threshold (10 lines)
if (totalLinesCleared % levelUpThreshold === 0) {
level++; // Increment the level
updateSpeed(); // Update the speed of falling shapes based on the new level
}
}
// Function to clear filled lines
function clearLines() {
let linesCleared = 0;
for (let row = rows - 1; row >= 0; row--) {
if (grid[row].every((cell) => cell !== 0)) {
grid.splice(row, 1);
grid.unshift(Array(cols).fill(0));
linesCleared++;
row++; // Check same row again
}
}
if (linesCleared > 0) {
updateLevel(linesCleared); // Update level based on lines cleared
updateScore(linesCleared); // Update score if lines are cleared
}
}
And the last thing is to set up key tracking and make the game move automatically.
// Event listeners for keyboard controls
document.addEventListener("keydown", function (event) {
switch (event.key) {
case "ArrowLeft":
moveLeft();
break;
case "ArrowRight":
moveRight();
break;
case "ArrowDown":
moveDown();
break;
case "s": // Rotate clockwise
rotateClockwise();
break;
case "a": // Rotate counterclockwise
rotateCounterClockwise();
break;
}
});
// Function to move the piece down automatically
function moveDownAuto() {
if (!checkCollision(1, 0)) {
currentRow++;
update();
} else {
lockShape();
clearLines();
generateShape();
if (checkGameOver()) {
gameOver();
return;
}
update();
}
}
That’s the basics of how I built a Tetris game from scratch using JavaScript and HTML5 canvas. Check out the full source code on my CodePen and play a few rounds. I’ve added some extras to make it more realistic.