运行游戏
我们在第十四章中看到的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>
