[游戏]原生js实现“射击俄罗斯方块”

零、前言

  1. 目的:以纯原生 HTML、JS 实现射击类的俄罗斯方块。
  2. 本文内容:从零开始一步一步介绍如何完成全部代码。
  3. 本文缺点:太多了。全局变量太多、太散;没有设计动画;没有游戏计分;游戏只能玩一次,再玩要刷新等等。
  4. 本文适合读者:有一些html、js基础或没有基础但有灵性,想探索性了解更多。
  5. 本文不适合读者:前端大佬,你们看了满脑子只想喷了,我自己看了都想喷。
  6. 游戏编写耗时:不到3小时。速成品,轻喷,多谢。
  7. 代码总行数:二百五左右,比较符合我的气质。
  8. 技术点:canvas、requestAnimationFrame
  9. 游戏预览:
    在这里插入图片描述

一、编写 HTML 和 CSS 代码

1. H5 基本代码

很简单,啥也没有:

<!DOCTYPE html>
<html>
<head>
  <meta charset="UTF-8">
  <title>Game</title>
</head>
<body>
  
</body>
</html>
2. 游戏外框、开始按钮
<!DOCTYPE html>
<html>
<head>
  <meta charset="UTF-8">
  <title>Game</title>
</head>
<body>
  <div id="container">
    <canvas id="game" width="132" height="240"></canvas>
  </div>
  <div id="start">开始游戏</div>
</body>
</html>
3. 增加一些样式
<!DOCTYPE html>
<html>
<head>
  <meta charset="UTF-8">
  <title>Game</title>
  <style>
    #container {
      width: min-content;
      margin: auto;
      padding: 1px;
      border: 1px solid #000;
    }

    #start {
      border: 1px solid #000; 
      width: 100px; 
      line-height: 30px; 
      text-align: center; 
      cursor: pointer; 
      margin: 10px auto
    }
  </style>
</head>
<body>
  <div id="container">
    <canvas id="game" width="132" height="240"></canvas>
  </div>
  <div id="start">开始游戏</div>
</body>
</html>

二、编写 js 代码

本文从这里开始的以下内容不再附全部代码,因为全是js代码。

1. 定义全局变量
const canvas = document.getElementById("game"),
      ctx = canvas.getContext("2d"),
      dataList = [],	// 存储全部数据
      w = 20, h = 20,	// 每个小方块的宽高
      hw = w * 0.5, hh = h * 0.5,	// 每个小方块的宽高的一半
      dis = 4,	// 小方块之间的间距
      dw = w + dis, dh = h + dis,	// 算上间距小方块占的宽高
      countOfOneLine = 11,	// 一行有多少个小方块
      initLineCount = 5, 	// 初始化有多少行
      maxLineCount = 20,	// 最大行数,超出则游戏失败
      difficulty = 0.5;		// 游戏难度,即每个位置有小方块的概率(范围在 0-1 之间)
let carIndex,		// 定义发射车位置
    lastTimestamp,	// 用来记录增加一行的时间戳
    status = "stop",	// 用来控制游戏状态:stop、playing、pause
    speed = 3000;		// 游戏增加一行的速度时间

  canvas.width = countOfOneLine * dw - 0.5 * dis;
  canvas.height = maxLineCount * dh + 0.5 * dis;
2. 定义数据相关的方法
// 生成一行
function generateOneLine() {
  const oneLine = [];
  for (let i = 0; i < countOfOneLine; i++) {
    oneLine[oneLine.length] = Math.random() < difficulty;
  }
  return oneLine;
}
// 真正地增加一行
function increaseOneLine() {
  while (true) {
    const oneLine = generateOneLine();
    // 避免全是 true 和全是 false
    if (!(oneLine.every(i => i) || oneLine.every(i => !i))) {
      dataList.splice(0, 0, oneLine);
      break;
    }
  }
}

// 方法里判断是否应该增加一行
function increase() {
  let timestamp = Date.now();
  if (timestamp - lastTimestamp > speed) {
    lastTimestamp = timestamp;
    increaseOneLine();
  }
}

// 清理那些可以清理的行
function clearLine() {
  for (let i = dataList.length - 1; i >= 0; i--) {
    if (dataList[i].every(data => data)) {
      dataList.splice(i, 1);
    }
  }
}

// 判断是否游戏存活
function isAlive() {
  return dataList.length < maxLineCount - 2;
}
3. 定义初始化方法(游戏开始)
function startGame() {
  dataList.length = 0;
  for (let i = 0; i < initLineCount; i++) {
    const oneLine = generateOneLine();
    // 初始化不做全空全有校验,看天意
    dataList[dataList.length] = oneLine;
  }

  carIndex = Math.floor(countOfOneLine * 0.5);
  lastTimestamp = Date.now();
  status = "playing";
}
4. 定义小车相关方法
// 发射车左移
function moveLeft() {
  if (carIndex === 0) {
    return;
  }
  carIndex--;
}

// 发射车右移
function moveRight() {
  if (carIndex === countOfOneLine - 1) {
    return;
  }
  carIndex++;
}
5. 定义 canvas 绘制相关方法

下面绘制的方法中有加 0.5,原因自行查询哦,或者你可以试试不加 0.5 看看有啥区别。

/**
 * 绘制一个小方块
 * @param {number} x
 * @param {number} y
 */
function drawOne(x, y) {
  ctx.beginPath();
  ctx.moveTo(x - hw, y - hh);
  ctx.lineTo(x + hw, y - hh);
  ctx.lineTo(x + hw, y + hh);
  ctx.lineTo(x - hw, y + hh);
  ctx.closePath();
  ctx.stroke();
}

// 绘制发射车
function drawCar() {
  drawOne(carIndex * dw + hw + 0.5, maxLineCount * dh - hh + 0.5);
  drawOne((carIndex + 1) * dw + hw + 0.5, maxLineCount * dh - hh + 0.5);
  drawOne((carIndex - 1) * dw + hw + 0.5, maxLineCount * dh - hh + 0.5);
  drawOne(carIndex * dw + hw + 0.5, (maxLineCount - 1) * dh - hh + 0.5);
}

// 绘制死亡提示线(游戏结束线)
function drawDeadline() {
  ctx.beginPath();
  ctx.moveTo(0, (maxLineCount - 3) * dh - dis * 0.5 + 0.5);
  ctx.lineTo(canvas.width, (maxLineCount - 3) * dh - dis * 0.5 + 0.5);
  ctx.stroke();
}

function pausing() {
  let x = Math.round(canvas.width * 0.5), y = Math.round(canvas.height * 0.5);
  ctx.clearRect(x - 41, y - 21, 82, 42);
  ctx.beginPath();
  ctx.moveTo(x - 40.5, y - 20.5);
  ctx.lineTo(x + 40.5, y - 20.5);
  ctx.lineTo(x + 40.5, y + 20.5);
  ctx.lineTo(x - 40.5, y + 20.5);
  ctx.closePath();
  ctx.stroke();
  ctx.textAlign = "center";
  ctx.textBaseline = "middle";
  ctx.font = "16px Arial";
  ctx.fillText("暂停中", x, y + 2);
}

function gameOver() {
  let x = Math.round(canvas.width * 0.5), y = Math.round(canvas.height * 0.5);
  ctx.clearRect(x - 41, y - 21, 82, 42);
  ctx.beginPath();
  ctx.moveTo(x - 40.5, y - 20.5);
  ctx.lineTo(x + 40.5, y - 20.5);
  ctx.lineTo(x + 40.5, y + 20.5);
  ctx.lineTo(x - 40.5, y + 20.5);
  ctx.closePath();
  ctx.stroke();
  ctx.textAlign = "center";
  ctx.textBaseline = "middle";
  ctx.font = "16px Arial";
  ctx.fillText("游戏结束", x, y + 2);
}
6. 添加一些事件
document.getElementById("start").addEventListener("click", () => status === "stop" && startGame());
document.addEventListener("keydown", e => {
  if (status !== "playing") {
    if(status === "pause" && e.key === " "){
      status = "playing";
    }
    return;
  }
  switch (e.key) {
    case "ArrowLeft":
      moveLeft();
      break;
    case "ArrowRight":
      moveRight();
      break;
    case "x":
      if (dataList.length === 0 || dataList[dataList.length - 1][carIndex]) {
        const arr = new Array(countOfOneLine).fill(false);
        arr[carIndex] = true;
        dataList[dataList.length] = arr;
      } else {
        let flag = true;
        for (let i = dataList.length - 1; i >= 0; i--) {
          if (dataList[i][carIndex]) {
            dataList[i + 1][carIndex] = true;
            flag = false;
            break;
          }
        }
        if (flag) {
          dataList[0][carIndex] = true;
        }
      }
      clearLine();
      break;
    case " ": // 空格
      status = "pause";
      break;
  }
});
window.addEventListener("blur", () => status === "playing" && (status = "pause"));
7. 重点来了
function animation() {
  switch (status) {
    case "playing":
      increase();
      ctx.clearRect(0, 0, canvas.width, canvas.height);
      dataList.forEach((data, y) => data.forEach((d, x) => d && drawOne(x * dw + hw + 0.5, y * dh + hh + 0.5)));
      drawCar();
      drawDeadline();
      if (!isAlive()) {
        gameOver();
        status = "stop";
      }
      break;
    case "pause":
      pausing();
      break;
  }
  window.requestAnimationFrame(animation);
}

animation();
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值