运行游戏

我们在第十四章中看到的requestAnimationFrames函数是一种产生游戏动画的好方法。但该函数的接口有点过于原始。该函数要求我们跟踪上次调用函数的时间,并在每一帧后再次调用requestAnimationFrame方法。

我们这里定义一个辅助函数来将这部分烦人的代码包装到一个名为runAnimation的简单接口中,我们只需向其传递一个函数即可,该函数的参数是一个时间间隔,并用于绘制一帧图像。当帧函数返回false时,整个动画停止。

  1. function runAnimation(frameFunc) {
  2. let lastTime = null;
  3. function frame(time) {
  4. let stop = false;
  5. if (lastTime != null) {
  6. let timeStep = Math.min(time - lastTime, 100) / 1000;
  7. if (frameFunc(timeStep) === false) return;
  8. }
  9. lastTime = time;
  10. requestAnimationFrame(frame);
  11. }
  12. requestAnimationFrame(frame);
  13. }

我们将每帧之间的最大时间间隔设置为 100 毫秒(十分之一秒)。当浏览器标签页或窗口隐藏时,requestAnimationFrame调用会自动暂停,并在标签页或窗口再次显示时重新开始绘制动画。在本例中,lastTimetime之差是隐藏页面的整个时间。一步一步地推进游戏看起来很傻,可能会造成奇怪的副作用,比如玩家从地板上掉下去。

该函数也会将时间单位转换成秒,相比于毫秒大家会更熟悉秒。

runLevel函数的接受Level对象和显示对象的构造器,并返回一个PromiserunLevel函数(在document.body中)显示关卡,并使得用户通过该节点操作游戏。当关卡结束时(或胜或负),runLevel会多等一秒(让用户看看发生了什么),清除关卡,并停止动画,如果我们指定了andThen函数,则runLevel会以关卡状态为参数调用该函数。

  1. function runLevel(level, Display) {
  2. let display = new Display(document.body, level);
  3. let state = State.start(level);
  4. let ending = 1;
  5. return new Promise(resolve => {
  6. runAnimation(time => {
  7. state = state.update(time, arrowKeys);
  8. display.setState(state);
  9. if (state.status == "playing") {
  10. return true;
  11. } else if (ending > 0) {
  12. ending -= time;
  13. return true;
  14. } else {
  15. display.clear();
  16. resolve(state.status);
  17. return false;
  18. }
  19. });
  20. });
  21. }

一个游戏是一个关卡序列。每当玩家死亡时就重新开始当前关卡。当完成关卡后,我们切换到下一关。我们可以使用下面的函数来完成该任务,该函数的参数为一个关卡平面图(字符串)数组和显示对象的构造器。

  1. async function runGame(plans, Display) {
  2. for (let level = 0; level < plans.length;) {
  3. let status = await runLevel(new Level(plans[level]),
  4. Display);
  5. if (status == "won") level++;
  6. }
  7. console.log("You've won!");
  8. }

因为我们使runLevel返回PromiserunGame可以使用async函数编写,如第十一章中所见。它返回另一个Promise,当玩家完成游戏时得到解析。

本章的沙盒GAME_LEVELS绑定中,有一组可用的关卡平面图。这个页面将它们提供给runGame,启动实际的游戏:

  1. <link rel="stylesheet" href="css/game.css">
  2. <body>
  3. <script>
  4. runGame(GAME_LEVELS, DOMDisplay);
  5. </script>
  6. </body>