【HTML5小游戏】一. 开始游戏的基础 - 一个永不停息的死循环

为什么要说死循环

几乎所有的游戏,都会有一个死循环,所以我们也从这个东西来作为这次主题的开端,讲讲用来写游戏的 - 死循环。

为什么要有一个死循环

游戏的本质,仔细想想,似乎就是一个可以交互的动画片。
而动画片是由一秒几十帧的画面组成的。

那么游戏的这几十帧画面从何而来呢?

其实,就是来自于这个死循环,是它孜孜不倦的把画面画到了的画布(Canvas)上。
(如果你还不清楚 Canvas 是什么,建议先自行了解一下 Canvas ,这里我就不再赘述了)

除了渲染画面,游戏逻辑,对用户输入的监听,其实也都是需要在这个循环内完成的。

一遍一遍又一遍,周而复始。

怎么写这个死循环

开始循环

由于是 HTML5 小游戏,所以这里直接使用了 Javascript 的 setInterval 来循环执行 main 函数
我们期望这个循环执行速度尽量快一些,所以使用了最小单位 1毫秒 来执行。

setInterval(main, 1);

function main() {
  // TODO
}

main 函数里要做什么?

就像前面说的,这个循环很重要的一部分内容,就是要绘制游戏画面。所以 main 函数里的首要任务也是绘制游戏画面
当然除了画面之外,还需要处理游戏逻辑,用户输入甚至音频音效。
这里先偷个懒,先把除了画面之外的内容暂时放到一个函数里

function main() {
  update(); // 处理游戏逻辑
  drawImg(); // 绘制游戏图像
}

function drawImg() {
  // TODO
}

function update() {
  // TODO
}

这样这个循环的框架就完成了,但是还缺少一个很重要的东西,时间。

在执行中,你可能会逐渐发现,游戏的每次循环间隔时间,并不是预期的 1 毫秒。
这是因为游戏逻辑的处理和画面的绘制的耗时可能不止 1 毫秒,所以循环间隔其实是不可控的。
这时候就需要引入一些变量,来记录循环之间实际的间隔,然后传递给循环内部,以便游戏逻辑处理时使用。

如何计算循环间隔时间呢?其实很简单,只要在每次循环开始的时候记录一下开始时间就好了。

let then = new Date()
function main() {
  let now = new Date()
  let delta = now - then;
  update(delta); // 将 delta 传入 update 函数,方便游戏逻辑得知真正的循环间隔时间
  drawImg(); // 绘制游戏图像
  then = now;
}

如何绘制图片

绘制图片需要用到 Html5 的标签 Canvas,先把这个 Canvas 放到页面上

const canvas = document.createElement("canvas");
const ctx = canvas.getContext("2d");
canvas.width = width;
canvas.height = height;
canvas.style.margin = "0 auto";
const screen = document.getElementById("screen"); // 我 html 代码里放了一个 id 为 screen 的 div,但这不重要
screen.appendChild(canvas);

然后是绘制图片函数,记得每次循环绘制之前先把画布清空,这个很重要。
就简单画几个文字上去吧。
(gameData 的值的来源,稍后会在 update 函数中说明)

function drawImg() {
  ctx.clearRect(0, 0, cvWidth, cvHeight); // 清除画面
  ctx.font = "20px 微软雅黑"; // 设置字体
  ctx.fillStyle = "#000"; // 设置颜色
  ctx.fillText("当前时间戳为: " + gameData.currentTimeStamp, 260, 50); // 绘制文字
  ctx.fillText("当前循环耗时(秒): " + gameData.updateDelta, 260, 120); // 绘制文字
  ctx.fillText("当前帧数: " + gameData.fps, 260, 180); // 绘制文字
}

游戏逻辑更新

由于这次重点是在讲这个主循环,所以游戏逻辑这块,就只更新一点基础数据吧。

const gameData = {}

function update(modifer) {
  gameData.currentTimeStamp = new Date().getTime()
  gameData.updateDelta = modifer
  gameData.fps = Math.floor(1 / modifer)
}

最终的效果图

cover

完整的代码

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <title></title>
  </head>
  <body>
    <div id="screen"></div>
  </body>
  <script>
    // 创建 canvas
    const canvas = document.createElement("canvas");
    const ctx = canvas.getContext("2d");

    // 改变量用于存储上一帧开始的时间
    let then;

    // Canvas 宽高
    const cvWidth = 600;
    const cvHeight = 500;

    // 存储游戏数据的变量
    const gameData = {
      currentTimeStamp: new Date().getTime(),
    };

    function init() {
      then = Date.now();
      createCanvas(cvWidth, cvHeight);
      // 开启死循环
      setInterval(main, 1);
    }

    // 添加 canvas 到网页里
    function createCanvas(width, height) {
      canvas.width = width;
      canvas.height = height;
      canvas.style.margin = "0 auto";
      let screen = document.getElementById("screen");
      screen.appendChild(canvas);
    }

    // 主循环函数
    function main() {
      const now = Date.now();
      const delta = now - then; // 计算每次循环的实际耗时,方便在代码中统一时间相关的参数(例如速度之类)
      update(delta / 1000); // 处理游戏逻辑
      drawImg(); // 绘制游戏图像
      then = now;
    }

    // 可以处理一下游戏逻辑,这次来用它来更新一些基本变量
    function update(modifier) {
      gameData.currentTimeStamp = new Date().getTime();
      gameData.updateDelta = modifier;
      gameData.fps = Math.floor(1 / modifier);
    }

    // 绘制游戏图像, 这次就简单的绘制一些文字就好
    function drawImg() {
      ctx.clearRect(0, 0, cvWidth, cvHeight); // 清除画面
      ctx.font = "20px 微软雅黑"; // 设置字体
      ctx.fillStyle = "#000"; // 设置颜色
      ctx.fillText("当前时间戳为: " + gameData.currentTimeStamp, 260, 50); // 绘制文字
      ctx.fillText("当前循环耗时(秒): " + gameData.updateDelta, 260, 120); // 绘制文字
      ctx.fillText("当前帧数: " + gameData.fps, 260, 180); // 绘制文字
    }

    window.onload = init(); // 页面加载后执行 init 函数
  </script>
</html>