手把手教你,通过前端技术搭建经典游戏——俄罗斯方块

一、需要实现的功能

俄罗斯方块游戏功能比较简单,旨在借助该游戏的制作过程来帮助大家学习JavaScript在前端中的应用。

在游戏开始的时候,会出现一个空的游戏区域,以及一个正在下落的方块。使用键盘方向键来控制方块的旋转和移动,目标是为了将方块填满游戏区域的整行,使其消除。当整行被填满时,该整行将被清除并获得积分。当方块堆叠到游戏区域的顶部时无法继续移动游戏结束。(下面是游戏的演示图)

二、需求功能细化

1、游戏区域

是由许多格子组成矩形区域。初始状态下游戏区域是空的。

2、方块

由四个小方块组成,随机生成不同的形状,每个小方块可以向下、向左、向右移动,以及顺时针旋转。

3、游戏规则

方块是从游戏区域的顶部开始下落,玩家可以控制方块的旋转和移动,直到方块无法继续下落或旋转。

当方块下落到游戏区域的底部或落在其他得方块上时,将固定在该位置,成为游戏区域的一部分。

当方块固定在游戏区域时,程序检查是否有整行被填满。如果有,清除这些行,并给玩家增加相应积分。

新的方块继续从顶部生成,直到方块堆叠到游戏区域的顶部游戏结束。

可以通过增加方块的下落速度、方块的形状复杂度等来逐渐增加游戏难度。

三、H5页面设计

1、创建html文件

首先创建一个命名为tetris.html的文件,通过编辑器(我用的EditPlus)打开。在HTML文档的头部,使用<!DOCTYPE>声明HTML文档类型,确保浏览器以正确的方式渲染网页内容。同时,设置utf-8编码,以确保浏览器能够正确地解析和显示中文字符。

下面这段 html 代码定义了俄罗斯方块游戏的基本布局。

<body>
<h1>俄罗斯方块游戏</h1>
<div id="tetris">
  <div id="game-board"></div>
  <div id="score">Score: <span id="score-value">0</span></div>
</div>
</body>

<h1>俄罗斯方块游戏</h1>:这是一个 <h1> 标题标签,用于显示该页面的主题内容“俄罗斯方块游戏”,一个页面只能一个h1标签。

<div id="tetris">:这是一个 <div> 容器,用于包裹整个游戏区域。

<div id="game-board"></div>:这是一个空的 <div> 容器,它的 id 属性为 “game-board”。这个容器将用于显示俄罗斯方块游戏的主要游戏区域。

<div id="score">Score: <span id="score-value">0</span></div>:这是一个用于显示积分的 <div> 容器,它的 id 属性为 “score”。其中包含文本 "Score: ",以及一个 <span> 元素用于显示实际的积分值。初始得分为 0,它被包含在 id 属性为 “score-value” 的 <span> 元素中。

这段 HTML 代码定义了一个基本的俄罗斯方块游戏界面,包含游戏标题、游戏区域和积分显示。通过 JavaScript 代码的结合,将实现游戏逻辑和交互功能。

三、CSS样式设置

上面html已经准备好了,下面我们来设置下页面样式吧。下面这段 CSS 代码为俄罗斯方块游戏提供了样式和布局:

<style>
  h1 {
    font-size: 19px;
    text-align: center;
  }
  #tetris {
    width: 240px;
    margin: 0 auto;
    background-color: #d5d5d5;
    border-radius: 10px;
    padding: 25px;
  }
  #game-board {
    width: 200px;
    height: 400px;
    border: 4px solid #4b6014;
    position: relative;
    border-radius: 10px;
    background-color: #f4f126;
    margin: 0 auto;
  }
  #score {
    text-align: center;
    margin-top: 10px;
  }
  .block {
    width: 20px;
    height: 20px;
    position: absolute;
    background-color: #000;
    border: 1px solid #3a3a3a;
    box-sizing: border-box;
  }
</style>

这些样式规则的含义如下:

h1:设置 <h1> 标签的字体大小为 19 像素,居中对齐。

#tetris:设置俄罗斯方块游戏容器的宽度为 240 像素,水平居中对齐,背景色为 #d5d5d5,边框半径为 10 像素,内边距为 25 像素。

#game-board:设置游戏面板容器的宽度为 200 像素,高度为 400 像素,添加 4 像素宽度、颜色为 #4b6014 的实线边框,相对定位,边框半径为 10 像素,背景色为 #f4f126,水平居中对齐。

#score:设置分数容器的文本居中对齐,上外边距为 10 像素。

.block:设置方块元素的宽度和高度为 20 像素,绝对定位,背景色为 #000,边框为 1 像素宽度、颜色为 #3a3a3a 的实线边框,使用盒模型的 “border-box” 进行盒子尺寸计算。

我们看下效果,怎么样是不是感觉好多了:

四、代码逻辑,游戏实现

上面我们搭建了好了页面,下面我们通过js代码来实现我们游戏的功能。先看代码:

<script>
  document.addEventListener('DOMContentLoaded', () => {
    function createShape() {
      const shapes = [
        [[1, 1, 1, 1]],
        [[1, 1], [1, 1]],
        [[1, 1, 0], [0, 1, 1]],
        [[0, 1, 1], [1, 1, 0]],
        [[1, 1, 1], [0, 1, 0]],
        [[1, 1, 1], [1, 0, 0]],
        [[1, 1, 1], [0, 0, 1]]
      ];
      const randomIndex = Math.floor(Math.random() * shapes.length);
      const shape = shapes[randomIndex];
      currentShape = shape;
      currentRow = 0;
      currentCol = Math.floor(cols / 2) - Math.floor(shape[0].length / 2);
    }
    function drawBoard() {
      board.innerHTML = '';
      for (let row = 0; row < rows; row++) {
        for (let col = 0; col < cols; col++) {
          if (boardGrid[row][col]) {
            const block = document.createElement('div');
            block.className = 'block';
            block.style.top = row * blockSize + 'px';
            block.style.left = col * blockSize + 'px';
            board.appendChild(block);
          }
        }
      }
    }
    function drawCurrentShape() {
      for (let row = 0; row < currentShape.length; row++) {
        for (let col = 0; col < currentShape[row].length; col++) {
          if (currentShape[row][col]) {
            const block = document.createElement('div');
            block.className = 'block';
            block.style.top = (currentRow + row) * blockSize + 'px';
            block.style.left = (currentCol + col) * blockSize + 'px';
            board.appendChild(block);
          }
        }
      }
    }
    function checkCollision() {
      for (let row = 0; row < currentShape.length; row++) {
        for (let col = 0; col < currentShape[row].length; col++) {
          if (currentShape[row][col]) {
            const newRow = currentRow + row;
            const newCol = currentCol + col;
            if (
              newRow >= rows ||
              newCol < 0 ||
              newCol >= cols ||
              boardGrid[newRow][newCol]
            ) {
              return true;
            }
          }
        }
      }
      return false;
    }
    function mergeShape() {
      for (let row = 0; row < currentShape.length; row++) {
        for (let col = 0; col < currentShape[row].length; col++) {
          if (currentShape[row][col]) {
            const newRow = currentRow + row;
            const newCol = currentCol + col;
            boardGrid[newRow][newCol] = 1;
          }
        }
      }
    }
    function clearRows() {
      for (let row = rows - 1; row >= 0; row--) {
        if (boardGrid[row].every((cell) => cell)) {
          boardGrid.splice(row, 1);
          boardGrid.unshift(new Array(cols).fill(0));
          score++;
        }
      }
    }
    function updateScore() {
      scoreValue.textContent = score;
    }
    function moveDown() {
      currentRow++;
      if (checkCollision()) {
        currentRow--;
        mergeShape();
        clearRows();
        updateScore();
        createShape();
        if (checkCollision()) {
          gameOver();
        }
      }
    }
    function moveLeft() {
      currentCol--;
      if (checkCollision()) {
        currentCol++;
      }
    }
    function moveRight() {
      currentCol++;
      if (checkCollision()) {
        currentCol--;
      }
    }
    function rotateShape() {
      const rotatedShape = currentShape[0].map((_, colIndex) =>
        currentShape.map((row) => row[colIndex]).reverse()
      );
      const prevShape = currentShape;
      currentShape = rotatedShape;
      if (checkCollision()) {
        currentShape = prevShape;
      }
    }
    function gameOver() {
      alert('Game Over');
      resetGame();
    }
    function resetGame() {
      score = 0;
      boardGrid = Array.from(Array(rows), () => new Array(cols).fill(0));
      updateScore();
      createShape();
    }
    function handleKeyPress(event) {
      switch (event.key) {
        case 'ArrowDown':
          moveDown();
          break;
        case 'ArrowLeft':
          moveLeft();
          break;
        case 'ArrowRight':
          moveRight();
          break;
        case 'ArrowUp':
          rotateShape();
          break;
      }
      drawBoard();
      drawCurrentShape();
    }
    function startGame() {
      createShape();
      setInterval(() => {
        moveDown();
        drawBoard();
        drawCurrentShape();
      }, 500);
      document.addEventListener('keydown', handleKeyPress);
    }
    startGame();
  });
</script>

接下来对代码进行解析:

1、变量定义

const board = document.getElementById('game-board');
const scoreValue = document.getElementById('score-value');
const blockSize = 20;
const rows = 20;
const cols = 10;
let score = 0;
let boardGrid = Array.from(Array(rows), () => new Array(cols).fill(0));
let currentShape;
let currentRow;
let currentCol;

board: 表示游戏区域的 DOM 元素,用于显示方块的容器。

scoreValue: 显示当前积分的 DOM 元素,用于更新积分显示。

blockSize: 方块的大小,即每个小方块的宽度和高度。

rows: 游戏区域的行数,表示游戏区域的垂直方向上方块的数量。

cols: 游戏区域的列数,表示游戏区域的水平方向上方块的数量。

score: 当前的积分,表示玩家在游戏中获得的积分。

boardGrid: 表示游戏区域的二维数组,用于记录方块的位置和状态。

currentShape: 当前正在移动的方块的形状,以二维数组的形式表示。

currentRow: 当前正在移动的方块的所在行数。

currentCol: 当前正在移动的方块的所在列数。

这些变量在游戏过程中被用于存储和追踪游戏的状态和数据。通过这些变量,游戏能够实时更新和管理游戏状态。

2、函数功能

createShape(): 创建一个随机的俄罗斯方块形状,并将其设置为当前形状。还会初始化当前形状的行和列。

drawBoard(): 在游戏面板上绘制当前的方块状态和已放置的方块。通过遍历游戏面板的二维数组 boardGrid,根据数组中的值来确定是否绘制方块。

drawCurrentShape(): 在游戏面板上绘制当前的方块形状。遍历当前形状的二维数组,根据数组中的值来确定绘制方块的位置。

checkCollision(): 检查当前的方块是否与已放置的方块或游戏边界发生碰撞。遍历当前形状的二维数组,检查当前方块的每个单元格是否与已放置的方块或边界发生碰撞。

mergeShape(): 将当前方块合并到已放置方块的游戏面板中。遍历当前形状的二维数组,将当前方块的每个单元格的值设置为1,表示已放置方块。

clearRows(): 检查游戏面板的每一行是否已满。如果某一行已满,则将该行删除,并在顶部添加新的空行。同时,增加玩家的分数。

updateScore(): 更新分数显示。将分数的值更新到分数元素中。

moveDown(): 将当前方块向下移动一行。如果发生碰撞,则将当前方块合并到游戏面板中,并检查是否有已满的行需要清除。如果当前方块无法再向下移动,则生成一个新的随机方块。

moveLeft(): 将当前方块向左移动一列。如果发生碰撞,则撤销移动操作。

moveRight(): 将当前方块向右移动一列。如果发生碰撞,则撤销移动操作。

rotateShape(): 旋转当前方块的形状。通过交换二维数组的行和列来实现方块的旋转。如果旋转后发生碰撞,则撤销旋转操作。

gameOver(): 游戏结束。显示游戏结束的提示框,并重置游戏。

resetGame(): 重置游戏状态。将分数、游戏面板和已放置方块的二维数组重置为初始状态,然后创建一个新的随机方块。

handleKeyPress(event): 处理按键事件。根据按下的按键来调用相应的移动或旋转方法,并重新绘制游戏面板和当前形状。

startGame(): 启动游戏。在游戏开始时,创建一个新的随机方块,并以一定的时间间隔不断向下移动方块。同时,监听键盘按键事件。

DOMContentLoaded 事件监听器:在 HTML 文档加载完成后执行代码。这是游戏逻辑的入口点。

通过这些函数功能实现了俄罗斯方块游戏的核心逻辑,包括生成随机方块、移动方块、检测碰撞、合并方块、清除满行等。同时,它还提供了游戏的初始化、分数计算、游戏结束和重置等功能。

五、完整源代码

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>俄罗斯方块游戏 - mmt3.com</title>
  <style>
    h1 {
      font-size: 19px;
      text-align: center;
    }
    #tetris {
      width: 240px;
      margin: 0 auto;
      background-color: #d5d5d5;
      border-radius: 10px;
      padding: 25px;
    }
    #game-board {
      width: 200px;
      height: 400px;
      border: 4px solid #4b6014;
      position: relative;
      border-radius: 10px;
      background-color: #f4f126;
      margin: 0 auto;
    }
    #score {
      text-align: center;
      margin-top: 10px;
    }
    .block {
      width: 20px;
      height: 20px;
      position: absolute;
      background-color: #000;
      border: 1px solid #3a3a3a;
      box-sizing: border-box;
    }
  </style>
</head>
<body>
<h1>俄罗斯方块游戏</h1>
<div id="tetris">
  <div id="game-board"></div>
  <div id="score">Score: <span id="score-value">0</span></div>
</div>
</body>
<script>
  document.addEventListener('DOMContentLoaded', () => {
    const board = document.getElementById('game-board');
    const scoreValue = document.getElementById('score-value');
    const blockSize = 20;
    const rows = 20;
    const cols = 10;
    let score = 0;
    let boardGrid = Array.from(Array(rows), () => new Array(cols).fill(0));
    let currentShape;
    let currentRow;
    let currentCol;
    function createShape() {
      const shapes = [
        [[1, 1, 1, 1]],
        [[1, 1], [1, 1]],
        [[1, 1, 0], [0, 1, 1]],
        [[0, 1, 1], [1, 1, 0]],
        [[1, 1, 1], [0, 1, 0]],
        [[1, 1, 1], [1, 0, 0]],
        [[1, 1, 1], [0, 0, 1]]
      ];
      const randomIndex = Math.floor(Math.random() * shapes.length);
      const shape = shapes[randomIndex];
      currentShape = shape;
      currentRow = 0;
      currentCol = Math.floor(cols / 2) - Math.floor(shape[0].length / 2);
    }
    function drawBoard() {
      board.innerHTML = '';
      for (let row = 0; row < rows; row++) {
        for (let col = 0; col < cols; col++) {
          if (boardGrid[row][col]) {
            const block = document.createElement('div');
            block.className = 'block';
            block.style.top = row * blockSize + 'px';
            block.style.left = col * blockSize + 'px';
            board.appendChild(block);
          }
        }
      }
    }
    function drawCurrentShape() {
      for (let row = 0; row < currentShape.length; row++) {
        for (let col = 0; col < currentShape[row].length; col++) {
          if (currentShape[row][col]) {
            const block = document.createElement('div');
            block.className = 'block';
            block.style.top = (currentRow + row) * blockSize + 'px';
            block.style.left = (currentCol + col) * blockSize + 'px';
            board.appendChild(block);
          }
        }
      }
    }
    function checkCollision() {
      for (let row = 0; row < currentShape.length; row++) {
        for (let col = 0; col < currentShape[row].length; col++) {
          if (currentShape[row][col]) {
            const newRow = currentRow + row;
            const newCol = currentCol + col;
            if (
              newRow >= rows ||
              newCol < 0 ||
              newCol >= cols ||
              boardGrid[newRow][newCol]
            ) {
              return true;
            }
          }
        }
      }
      return false;
    }
    function mergeShape() {
      for (let row = 0; row < currentShape.length; row++) {
        for (let col = 0; col < currentShape[row].length; col++) {
          if (currentShape[row][col]) {
            const newRow = currentRow + row;
            const newCol = currentCol + col;
            boardGrid[newRow][newCol] = 1;
          }
        }
      }
    }
    function clearRows() {
      for (let row = rows - 1; row >= 0; row--) {
        if (boardGrid[row].every((cell) => cell)) {
          boardGrid.splice(row, 1);
          boardGrid.unshift(new Array(cols).fill(0));
          score++;
        }
      }
    }
    function updateScore() {
      scoreValue.textContent = score;
    }
    function moveDown() {
      currentRow++;
      if (checkCollision()) {
        currentRow--;
        mergeShape();
        clearRows();
        updateScore();
        createShape();
        if (checkCollision()) {
          gameOver();
        }
      }
    }
    function moveLeft() {
      currentCol--;
      if (checkCollision()) {
        currentCol++;
      }
    }
    function moveRight() {
      currentCol++;
      if (checkCollision()) {
        currentCol--;
      }
    }
    function rotateShape() {
      const rotatedShape = currentShape[0].map((_, colIndex) =>
        currentShape.map((row) => row[colIndex]).reverse()
      );
      const prevShape = currentShape;
      currentShape = rotatedShape;
      if (checkCollision()) {
        currentShape = prevShape;
      }
    }
    function gameOver() {
      alert('Game Over');
      resetGame();
    }
    function resetGame() {
      score = 0;
      boardGrid = Array.from(Array(rows), () => new Array(cols).fill(0));
      updateScore();
      createShape();
    }
    function handleKeyPress(event) {
      switch (event.key) {
        case 'ArrowDown':
          moveDown();
          break;
        case 'ArrowLeft':
          moveLeft();
          break;
        case 'ArrowRight':
          moveRight();
          break;
        case 'ArrowUp':
          rotateShape();
          break;
      }
      drawBoard();
      drawCurrentShape();
    }
    function startGame() {
      createShape();
      setInterval(() => {
        moveDown();
        drawBoard();
        drawCurrentShape();
      }, 500);
      document.addEventListener('keydown', handleKeyPress);
    }
    startGame();
  });
</script>
</html>

以上是俄罗斯方块游戏的完整代码,可以直接复制过去,或者点击demo链接访问。

版权声明:本文为老张的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。

原文链接:https://www.webppp.com/view/tetris.html