运行游戏
我们在第十四章中看到的requestAnimationFrames
函数是一种产生游戏动画的好方法。但该函数的接口有点过于原始。该函数要求我们跟踪上次调用函数的时间,并在每一帧后再次调用requestAnimationFrame
方法。
我们这里定义一个辅助函数来将这部分烦人的代码包装到一个名为runAnimation
的简单接口中,我们只需向其传递一个函数即可,该函数的参数是一个时间间隔,并用于绘制一帧图像。当帧函数返回false
时,整个动画停止。
function runAnimation(frameFunc) {
let lastTime = null;
function frame(time) {
let stop = false;
if (lastTime != null) {
let timeStep = Math.min(time - lastTime, 100) / 1000;
if (frameFunc(timeStep) === false) return;
}
lastTime = time;
requestAnimationFrame(frame);
}
requestAnimationFrame(frame);
}
我们将每帧之间的最大时间间隔设置为 100 毫秒(十分之一秒)。当浏览器标签页或窗口隐藏时,requestAnimationFrame
调用会自动暂停,并在标签页或窗口再次显示时重新开始绘制动画。在本例中,lastTime
和time
之差是隐藏页面的整个时间。一步一步地推进游戏看起来很傻,可能会造成奇怪的副作用,比如玩家从地板上掉下去。
该函数也会将时间单位转换成秒,相比于毫秒大家会更熟悉秒。
runLevel
函数的接受Level对象和显示对象的构造器,并返回一个Promise
。runLevel
函数(在document.body
中)显示关卡,并使得用户通过该节点操作游戏。当关卡结束时(或胜或负),runLevel
会多等一秒(让用户看看发生了什么),清除关卡,并停止动画,如果我们指定了andThen
函数,则runLevel
会以关卡状态为参数调用该函数。
function runLevel(level, Display) {
let display = new Display(document.body, level);
let state = State.start(level);
let ending = 1;
return new Promise(resolve => {
runAnimation(time => {
state = state.update(time, arrowKeys);
display.setState(state);
if (state.status == "playing") {
return true;
} else if (ending > 0) {
ending -= time;
return true;
} else {
display.clear();
resolve(state.status);
return false;
}
});
});
}
一个游戏是一个关卡序列。每当玩家死亡时就重新开始当前关卡。当完成关卡后,我们切换到下一关。我们可以使用下面的函数来完成该任务,该函数的参数为一个关卡平面图(字符串)数组和显示对象的构造器。
async function runGame(plans, Display) {
for (let level = 0; level < plans.length;) {
let status = await runLevel(new Level(plans[level]),
Display);
if (status == "won") level++;
}
console.log("You've won!");
}
因为我们使runLevel
返回Promise
,runGame
可以使用async
函数编写,如第十一章中所见。它返回另一个Promise
,当玩家完成游戏时得到解析。
在本章的沙盒的GAME_LEVELS
绑定中,有一组可用的关卡平面图。这个页面将它们提供给runGame
,启动实际的游戏:
<link rel="stylesheet" href="css/game.css">
<body>
<script>
runGame(GAME_LEVELS, DOMDisplay);
</script>
</body>