为什么要说死循环
几乎所有的游戏,都会有一个死循环,所以我们也从这个东西来作为这次主题的开端,讲讲用来写游戏的 - 死循环。
为什么要有一个死循环
游戏的本质,仔细想想,似乎就是一个可以交互的动画片。
而动画片是由一秒几十帧的画面组成的。
那么游戏的这几十帧画面从何而来呢?
其实,就是来自于这个死循环,是它孜孜不倦的把画面画到了的画布(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)
}
最终的效果图

完整的代码
<!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>