剖析游戏结构 - 游戏开发

剖析游戏结构
======

本文从技术角度分析了一般电子游戏的结构和工作流程，就此介绍主循环是如何运行的。它有助于初学者了解在现代游戏开发领域构建游戏时需要什么，以及如何将 JavaScript 这样的 Web 标准工具作为自己的工具使用。游戏开发经验丰富但不熟悉 Web 开发的开发者也能从中受益。

呈现、接收、解释、计算、重复
--------------

所有电子游戏的目标都是向用户**呈现**一个场景，**接收**他们的输入，将这些输入信号**解释**为动作，并**计算**出由这些动作产生的新场景。游戏不断地循环遍历，一遍又一遍，直到遇到某些终止条件（比如赢、输或者退出睡觉）。毫不奇怪，这种模式与游戏引擎的编程方式相呼应。

具体情况取决于游戏本身。

一些游戏通过**用户输入**来驱动这个循环。想象一下，你正在开发一种“大家来找茬”类型的游戏。这些游戏向用户**呈现**两张图片，游戏**接收**点击（或触摸）；将用户输入**解释**为成功，失败，暂停，菜单交互等。最后，游戏根据用户的输入**计算并更新**游戏场景。这种游戏是由用户的输入驱动，也就是说，它会在用户进行输入之后冻结画面，等待玩家进行新的输入。这是一种基于回合的游戏类型，它不需要每帧持续更新画面，只有当玩家作出反应时才会。

另一些游戏需要尽可能控制每一个细微的时间片（动画）。与上述原理有点小区别：每个动画帧都将循环遍历，并在之后第一个可用的轮次捕获玩家输入的任何变化。这种每帧一次的模型是通过一个叫**主循环**的东西实现的。如果你的游戏循环是基于时间的，则必须保证对主循环精准的模拟。

但它也可能不需要逐帧控制。你的游戏循环可能类似找不同的例子，并以输入事件作为基础。它可能需要输入和模拟时间片。它甚至可以基于其他的东西来循环。

现代 JavaScript - 正如下一节中所描述的 - 它可以轻松开发出一个高效的，逐帧执行的主循环，这很值得庆幸。当然，你的游戏只会按照你所做的那样优化。如果某些东西看起来应该被添加到一个更罕见的事件里，那么将它从主循环中剥离出来通常是个好主意（但并非总是如此）。

在 JavaScript 中构建一个主循环
---------------------

JavaScript 能很好的处理事件和回调函数。现代浏览器努力在需要的时候调用方法，并在间隙中闲下来（或做其他任务）。将你的代码附加到适合它们的时刻是一个很好的主意。考虑一下你的函数是需要在严格的时间周期内，还是每一帧，或者仅仅是在发生了其他情况之后执行。当你的函数需要被调用时，要更具体地使用浏览器，这样浏览器就可以在调用时进行优化。而且，这可能会让你的工作更轻松。

有些代码需要逐帧运行，所以应将其附加到浏览器的重绘周期中，没有比这更好的了！在 Web 中，通常 `window.requestAnimationFrame()` 方法是大多数良好的逐帧循环的基础。在调用该方法时必须传入一个回调函数，这个回调函数将在下一次重新绘制之前执行。下面是一个简单的主循环的例子：

```js
window.main = () => {
  window.requestAnimationFrame(main);

  // 无论你的主循环需要做什么
};

main(); // 开始循环
```

**备注：** 在这里讨论的每个 `main()` 方法中，在执行循环内容之前，我们会递归调用一个新的 `requestAnimationFrame`，这不是随意的，它被认为是最佳实践。如果你的当前帧错过了它的垂直同步窗口的周期，你也在下一个帧通过 `requestAnimationFrame` 尽早的调用 `main()`，从而确保浏览器能够及时地执行。

上面的代码块有两个语句。第一个语句创建一个名为 `main()` 中的全局变量的函数。这个函数做一些工作，也告诉浏览器在下一帧通过 `window.requestAnimationFrame()` 调用本身。第二个语句调用第一个语句中定义的 `main()` 函数。因为 `main()`中在第二个语句中被调用一次，而每次调用都将自身又放置到下一个帧的执行队列中，因此 `main()` 的调用与你的帧率同步。

当然，这个循环并不完美。不过在讨论如何改善之前，让我们先讨论一下它已经做好了什么。

将主循环的时机安排在浏览器每次的绘制显示中，允许你能像浏览器想要绘制的那样频繁的运行你的循环。你能够控制每一帧动画，并且 `main()` 是循环中唯一执行的函数，所以这很简单。主视角射击游戏（或类似的游戏）每一帧都会出现一个新的场景。没有比这种方法更平滑并绘制及时的了。

但是不要马上认为动画必需要帧帧控制。通过 CSS 动画或浏览器中的其他工具，可以很容易实现简单的动画，甚至 GPU 加速。有很多这样的东西，它们会让你的工作更轻松。

在 JavaScript 中构建一个更好的主循环
------------------------

在前面的主循环中有两个明显的问题：`main()` 污染了 `window` 对象（所有全局变量存储的对象），并且示例代码没有给我们留下一个停止循环的方法，除非整个浏览器选项卡被关闭或刷新。对于第一个问题，如果你希望主循环只运行，并且不需要被简单（直接）访问它，你可以将它作为一个立即调用的函数表达式（IIFE）创建。

```js
/\*
\* 以分号开头是为了以防此示例上方的代码行依赖于自动分号插入（ASI）。
\* 浏览器可能会意外地认为整个示例从上一行继续。
\* 如果前一行不为空或终止，则前面的分号标志着新行的开始。
\*/

;(() => {
  function main(){
    window.requestAnimationFrame(main);

    // 你的主循环内容。
  }

  main(); // 开始循环
})();
```

当浏览器遇到这个 IIFE 时，它将定义你的主循环，并立即将其加入下一个帧的更新队列中。`main`（或 `main()` 方法）不会被附加到任何对象，在应用程序的其他地方仍是一个有效的未使用的名称，仍可以自由地定义为其他的东西。

**备注：** 在实践中，更常见的终止下一个 `requestAnimationFrame()` 方式是使用 if 语句，而不是调用`cancelAnimationFrame()`。

对于第二个问题，要终止循环，你需要调用 `window.cancelAnimationFrame()` 来取消 `main()` 的调用。该方法需要传入你最后一次调用 `requestAnimationFrame()` 时返回的 ID。让我们假设你的游戏的函数和变量是建立在你称为 MyGame 的名称空间上。扩展我们的最后一个例子，主循环现在看起来是这样的：

```js
/\*
\* 以分号开头是为了以防此示例上方的代码行依赖于自动分号插入（ASI）。
\* 浏览器可能会意外地认为整个示例从上一行继续。
\* 如果前一行不为空或终止，则前面的分号标志着新行的开始。
\*
\* 让我们假设 MyGame 是之前定义的。
\*/

;(() => {
  function main(){
    MyGame.stopMain = window.requestAnimationFrame(main);

    // 你的主循环内容
  }

  main(); // 开始循环
})();
```

现在，我们在 MyGame 名称空间中声明了一个变量`stopMain`，其值为主循环最后调用 `requestAnimationFrame()` 时返回的 ID。任何时候，我们可以通过告诉浏览器取消与 ID 相对应的请求来停止主循环。

```js
window.cancelAnimationFrame(MyGame.stopMain);
```

在 JavaScript 的中编写主循环的关键在于，考虑到任何会驱动你行为的事件，并注意不同的系统是如何相互作用的。你可能拥有多个由多个不同类型的事件驱动的组件。这看起来像是不必要的复杂性，但它可能就是一个很好的优化（当然，不一定是这样的）。问题是，你并不是在编写一个典型的主循环。在 JavaScript 中，你使用的是浏览器的主循环，并且你正在尝试这样做。

用 JavaScript 构建一个更优化的主循环
------------------------

基本上，在 JavaScript 的中，浏览器有它自己的主循环，而你的代码存在于循环某些阶段。上面描述的主循环，试图避免脱离浏览器的控制。这种主循环附着于 `window.requestAnimationFrame()` 方法，该方法将在浏览器的下一帧中执行，具体取决于浏览器如何与将其自己的主循环关联起来。W3C 的 requestAnimationFrame 规范并没有真正定义什么时候浏览器必须执行 requestAnimationFrame 回调。这有一个好处，浏览器厂商可以自由地实现他们认为最好的解决方案，并随着时间的推移进行调整。

现代版的 Firefox 和 Google Chrome（可能还有其他版本）试图在框架的时间片的开始时尝试将 `requestAnimationFrame` 回调与它们的主线程进行连接。因此，浏览器的主线程看起来就像下面这样：

1. 启动一个新帧（而之前的帧由显示处理）。
2. 遍历 `requestAnimationFrame` 回调并调用它们。
3. 当上面的回调停止控制主线程时，执行垃圾收集和其他帧任务。
4. 睡眠（除非事件打断了浏览器的小睡），直到显示器准备好你的图像（VSync）并重复。

你可以考虑开发实时应用程序，因为有时间做工作。所有上述步骤都必须在每 16 毫秒内进行一次，以保持 60 赫兹的显示效果。浏览器会尽可能早地调用你的代码，从而给它最大的计算时间。你的主线程通常会启动一些甚至不在主线程上的工作负载（如 WebGL 的中的光栅化或着色器）。在浏览器使用其主线程管理垃圾收集，其他任务或处理异步事件时，可以在 Web Worker 或 GPU 上执行长时间的计算。

当我们讨论预算时，许多网络浏览器都有一个称为高分辨率时间的工具。`Date` 对象不再是计时事件的识别方法，因为它非常不精确，可以由系统时钟进行修改。另一方面，高分辨率的时间计算自 `navigationStart`（当上一个文档被卸载时）的毫秒数。这个值以小数的精度返回，精确到千分之一毫秒。它被称为`DOMHighResTimeStamp`，但是，无论出于什么目的和目的，都认为它是一个浮点数。

**备注：** 系统（硬件或软件）不能达到微秒精度，可以提供毫秒精度的最小值然而，如果他们能够做到这一点，他们就应该提供 0.001 毫秒的准确性。

这个值本身并不太有用，因为它与一个相当无趣的事件相关，但它可以从另一个时间戳中减去，以便准确准确地确定这两个点之间的时间间隔。要获得这些时间戳中的一个，你可以调用 `window.performance.now()` 并将结果存储为一个变量。

```js
var tNow = window.performance.now();
```

回到主循环的主题。你将经常想知道何时调用主函数。因为这是常见的，`window.requestAnimationFrame()` 总是提供一个 `DOMHighResTimeStamp` 执行时回调函数作为参数。这将导致我们之前的主循环的另一个增强。

```js
/\*
\* 以分号开头是为了以防此示例上方的代码行依赖于自动分号插入（ASI）。
\* 浏览器可能会意外地认为整个示例从上一行继续。
\* 如果前一行不为空或终止，则前面的分号标志着新行的开始。
\*
\* 我们还假设 MyGame 是以前定义的。
\*/

;(() => {
  function main(tFrame){
    MyGame.stopMain = window.requestAnimationFrame(main);

    // 你的主循环内容
    // tFrame，来自"function main(tFrame)"，现在是由 rAF 提供的 DOMHighResTimeStamp。
  }

  main(); // 开始循环
})();
```

其他一些优化是可能的，这取决于你的游戏想要完成什么。你的游戏类型显然会有所不同，但它甚至可能比这更微妙。你可以在画布上单独绘制每个像素，也可以将 DOM 元素（包括具有透明背景的多个 WebGL 的画布）放入复杂的层次结构中。每条路径都将导致不同的机会和约束。

决定......时间
----------

你将需要对你的主循环作出艰难的决定：如何模拟准确的时间进度。如果你想要控制每一帧，那么你需要确定你的游戏更新和绘制的频率，你甚至可能希望以不同的速率进行更新和绘制。你还需要考虑如果用户的系统无法跟上工作负载，那么你还需考虑如何优雅降级，以保证性能。让我们首先假定你会在每次绘制时，同时处理用户的输入，并更新游戏状态。我们稍后再细讲。

**备注：** 改变主循环如何处理时间是非常困难的，在进行主循环之前，请仔细考虑你的需求。

### 大多数浏览器游戏应该是什么样的

如果你的游戏可以达到你所支持的任何硬件的最大刷新率，那么你的工作就变得相当容易了。你可以简单地进行更新，渲染，然后在垂直同步之前什么都不用做。

```js
/\*
\* 以分号开头是为了以防此示例上方的代码行依赖于自动分号插入（ASI）。
\* 浏览器可能会意外地认为整个示例从上一行继续。
\* 如果前一行不为空或终止，则前面的分号标志着新行的开始。
\*
\* 我们还假设 MyGame 是以前定义的。
\*/

;(function(){
  function main(tFrame){
    MyGame.stopMain = window.requestAnimationFrame(main);

    update(tFrame); // 调用 update 方法。在我们的例子中，我们给它 rAF 的时间戳。
    render();
  }

  main(); // 开始循环
})();
```

如果无法达到最大刷新率，可以调整画面质量设置以保持你的时间预算。这个概念最有名的例子是 id Software 的 RAGE 游戏，这个游戏取消了用户的控制权，以使其计算时间保持在大约 16ms（或大约 60fps）。如果计算时间过长，则提交的解析度就降低，纹理和其他资源将无法加载或绘制等。这个（非网络）案例研究做了一些假设和折衷：

* 每个动画帧都占用户输入。
* 没有帧需要外推（猜测），因为每个绘图都有自己的更新。
* 仿真系统基本上可以假定每次完全更新间隔约 16ms。
* 给用户控制质量设置将是一场噩梦。
* 不同的监视器以不同的速率输入：30FPS、75FPS、100FPS、120FPS、144FPS 等
* 无法跟上 60 FPS 的系统会失去视觉质量，以保持游戏运行的最佳速度（最终如果质量太低，则会彻底失败）。
### 处理可变刷新率需求的其他方法

存在其他解决问题的方法。

一种常见的技术是以恒定的频率更新模拟，然后绘制尽可能多的（或尽可能少的）实际帧。更新方法可以继续循环，而不用考虑用户看到的内容。绘图方法可以查看最后的更新以及发生的时间。由于绘制知道何时表示，以及上次更新的模拟时间，它可以预测为用户绘制一个合理的框架。这是否比官方更新循环更频繁（甚至更不频繁）无关紧要。更新方法设置检查点，并且像系统允许的那样频繁地，渲染方法画出周围的时间。在 Web 标准中分离更新方法有很多种方法：

* 绘制 `requestAnimationFrame` 并更新 `setInterval()` 或 `setTimeout()`。
    + 即使在未聚焦或最小化的情况下，使用处理器时间，也可能是主线程，并且可能是传统游戏循环的工件（但是很简单）。
* 绘制 `requestAnimationFrame` 并在 Web Worker 的 `setInterval` 或 `setTimeout` 中对其进行更新。
    + 这与上述相同，除了更新不会使主线程（主线程也没有）。这是一个更复杂的解决方案，并且对于简单更新可能会有太多的开销。
* 绘制 `requestAnimationFrame` 并使用它来戳一个包含更新方法的 Web Worker，其中包含要计算的刻度数（如果有的话）。
    + 这个睡眠直到 `requestAnimationFrame` 被调用并且不会污染主线程，加上你不依赖于老式的方法。再次，这比以前的两个选项更复杂一些，并且*开始*每个更新将被阻止，直到浏览器决定启动 rAF 回调。

这些方法中的每一种都有类似的权衡：

* 用户可以跳过渲染帧或根据其性能内插额外的帧。
* 你可以指望所有用户以相同的恒定频率更改打嗝的更新非美容变量。
* 程序比我们前面看到的基本循环要复杂得多。
* 用户输入完全被忽略，直到下次更新（即使用户具有快速设备）。
* 强制性内插具有性能损失。

单独的更新和绘图方法可能如下面的示例。为了演示，该示例基于第三个项目符号，只是不使用 Web Workers 进行可读性（而且我们诚实地说可写性）。

**警告：** 这个例子，具体来说，需要进行技术审查。

```js
/\*
\* 以分号开头是为了以防此示例上方的代码行依赖于自动分号插入（ASI）。
\* 浏览器可能会意外地认为整个示例从上一行继续。
\* 如果前一行不为空或终止，则前面的分号标志着新行的开始。
\*
\* 我们还假设 MyGame 是以前定义的。
\*
\* MyGame.lastRender 跟踪最后提供的 requestAnimationFrame 时间戳。
\* MyGame.lastTick 跟踪最后更新时间。始终以 tickLength 递增。
\* MyGame.tickLength 是游戏状态更新的频率。这是 20 Hz(50ms)。
\*
\* timeSinceTick 是 requestAnimationFrame 回调和最后一次更新之间的时间。
\* numTicks 是这两个呈现帧之间应该发生的更新次数。
\*
\* render() 传入 tFrame, 因为 render 方法可能需要计算
\* tFrame 距离最近的更新已经过去了多久，通过外推的方式
\* 来获得场景数据。(对于快速设备，render 方法是纯表现性的)。
\* 用以绘制场景。
\*
\* update() 根据给定时间点计算游戏状态。通常需要用 tickLength
\* 作为循环参数，递增更新。来保证游戏状态的严谨。传入 DOMHighResTimeStamp
\* 格式的当前时间。（除非需要增加暂停功能，传入的时间应该总是
\* 最后更新时间 + 游戏的 tick 间隔。)
\*
\* setInitialState() 执行在运行 mainloop 之前需要的任何任务。
\* 它只是一个普通的示例函数，表示你在 mainloop 前做过的事。
\*/

;(function(){
  function main(tFrame){
    MyGame.stopMain = window.requestAnimationFrame(main);
    var nextTick = MyGame.lastTick + MyGame.tickLength;
    var numTicks = 0;

    // 如果 tFrame < nextTick，则需要更新 0 个 ticks（对于 numTicks，默认为 0）。
    // 如果 tFrame = nextTick，则需要更新 1 tick（等等）。
    // 备注：正如我们在总结中提到的那样，你应该跟踪 numTicks 的大小。
    // 如果它很大，那么你的游戏是睡着了，或者机器无法跟上。
    if (tFrame > nextTick) {
      const timeSinceTick = tFrame - MyGame.lastTick;
      numTicks = Math.floor(timeSinceTick / MyGame.tickLength);
    }

    queueUpdates(numTicks);
    render(tFrame);
    MyGame.lastRender = tFrame;
  }

  function queueUpdates(numTicks) {
    for (let i = 0; i < numTicks; i++) {
      MyGame.lastTick += MyGame.tickLength; // 现在 lastTick 是这个刻度。
      update(MyGame.lastTick);
    }
  }

  MyGame.lastTick = performance.now();
  MyGame.lastRender = MyGame.lastTick; // 假装第一次绘制是在第一次更新。
  MyGame.tickLength = 50; // 这将使你的模拟运行在 20Hz（50ms）

  setInitialState();
  main(performance.now()); // 开始循环
})();
```

另一个选择是简单地做一些事情不那么频繁。如果你的更新循环的一部分难以计算但对时间不敏感，则可以考虑缩小其频率，理想情况下，在延长的时间段内将其扩展成块。这是一个隐含的例子，在火炮博物馆的炮兵游戏中，他们调整垃圾发生率来优化垃圾收集。显然，清理资源不是时间敏感的（特别是如果整理比垃圾本身更具破坏性）。

这也可能适用于你自己的一些任务。那些是当可用资源成为关注点时的好候选人。

概要
--

我知道上述的任何一种，或许没有适合你的游戏。正确的决定完全取决于你愿意（和不愿意）做出的权衡。主要关心的是切换到另一个选项。幸运的是，我没有任何经验，但我听说这是一个令人难以置信的游戏的 Whack-a-Mole。

像 Web 这样的受管理平台，要记住的一件重要的事情是，你的循环可能会在相当长的一段时间内停止执行。当用户取消选择你的标签并且浏览器休眠（或减慢）其 `requestAnimationFrame` 回调间隔时，可能会发生这种情况。你有很多方法来处理这种情况，这可能取决于你的游戏是单人游戏还是多人游戏。一些选择是：

* 考虑差距“暂停”并跳过时间。
    + 你可能会看到这对大多数多人游戏来说都是有问题的。
* 你可以模拟差距赶上。
    + 这可能是长时间丢失和/或复杂更新的问题。
* 你可以从联机设备或服务器恢复游戏状态。
    + 除非联机设备/服务器的游戏状态已经过期，或者这是一个没有没服务器的纯单机游戏。

一旦你的主循环被开发出来，你已经决定了一套适合你的游戏的假设和权衡，现在只需要用你的决定来计算任何适用的物理、AI、声音、网络同步，以及其他任何你的游戏可能需要的。


发布游戏 - 游戏开发

发布游戏
====

HTML5 游戏在发布和分发上具有极大的优势--你可以通过网络自由地发布，推广，获利，并且不受分发商的限制。你可以实现通过多种平台获利。这一系列的案例关注在当你想要发布，推广你的游戏，并当游戏变得有名时获取一些回报。

游戏发布
----

你按照一个或两个教程（1 (en-US)，2 (en-US)）制作了一个 HTML5 游戏。Game distribution提供了你发布游戏时所需的一切知识。包括如何在线托管游戏，提交游戏到开放的游戏市场，或封闭的游戏市场，如 Google Play，iOS App Store.

游戏推广
----

开发完成游戏并不是结束。你必须让世界知道你制作了一些有趣的东西能使他们乐在其中。这些游戏推广技术— 有些是免费的 — 即使你作为一名 0 预算的独立开发者为生活奔波，你依旧可以让很多人知道你制作的这个新游戏。推广游戏或许能够使你获得收入。高效的推广非常重要。

游戏获利
----

当你开发，发布并推广你的游戏后，你可以考虑通过游戏来获得收入。游戏获利对于考虑从业余转为全职并以此为生的开发者来说是必要的一个手段。现在来看看你的选择，技术已经足够成熟，现在只差一个合适的机会了。


HTML5 游戏开发简介 - 游戏开发

HTML5 游戏开发简介
============

优点
--

1. 使用 HTML5 构建的游戏可以在智能手机，平板电脑，个人电脑和智能电视上工作。
2. 通过网络以及其他媒体广告宣传你的游戏。
3. 付款。收取你想要的，并使用任何你喜欢的付款处理服务。
4. 随时更新游戏。
5. 收集你自己的分析！
6. 更密切地与客户联系。
7. 玩家可随时随地玩游戏。
网络技术
----

| **Function** | **Technology** |
| --- | --- |
| **Audio** | Web Audio API (en-US) |
| **Graphics** | WebGL (OpenGL ES 2.0) |
| **Input** | Touch events, Gamepad API (en-US), device sensors, WebRTC (en-US), Full Screen API (en-US), Pointer Lock API (en-US) |
| **Language** | JavaScript (or C/C++ using Emscripten to compile to JavaScript) |
| **Networking** | WebRTC (en-US) and/or WebSockets (en-US) |
| **Storage** | IndexedDB or the "cloud" |
| **Web** | HTML, CSS, SVG, Social API (and much more!) |

Full Screen API (en-US)

全屏游戏。

Gamepad API (en-US)

使用游戏手柄或其他游戏控制器。

HTML and CSS

构建，样式和布局游戏的用户界面。

HTML audio

轻松播放简单的音效和音乐。

IndexedDB

将用户数据存储在他们自己的计算机或设备上。

JavaScript

快速的网页编程语言为你的游戏编写代码。轻松移植你现有的游戏 Emscripten 或 Asm.js。

Pointer Lock API (en-US)

在游戏界面中锁定鼠标或其他指针设备。

SVG (Scalable Vector Graphics)

构建能够顺利扩展的矢量图形，无论用户显示器的大小或分辨率如何。

Typed Arrays

从 JavaScript 中访问原始二进制数据; 操纵 GL 纹理，游戏数据或其他任何东西。

Web Audio API (en-US)

实时控制音频的播放，合成和操纵。

WebGL

创建高性能，硬件加速的 3D（和 2D）图形。OpenGL ES 2.0.

WebRTC (en-US)

实时通讯控制音频和视频数据，包括电话会议，并在两个用户之间来回传送其他应用数据，如聊天。

WebSockets (en-US)

将你的应用程序或站点连接到一个服务器以实时传输数据。适合多人游戏动作，聊天服务等。

Web Workers

生成后台线程为多核处理器运行自己的 JavaScript 代码。

XMLHttpRequest and File API

从一个 Web 服务器发送和接收任何你想要的数据，如下载新的游戏级别和艺术品，以便来回传送非实时游戏状态信息。


教程 - 游戏开发

教程
==

本页面包含多个教程系列，重点介绍高效创建不同类型的 Web 游戏的不同工作流程。

使用纯 JavaScript 实现 2D 打砖块游戏

在该手把手教程中，你将使用纯 JavaScript 来实现一个简单的打砖块游戏。在这个过程中，你将学习使用 `<canvas>` 元素来实现基本的游戏机制，如渲染和移动图像、碰撞检测、控制机制以及获胜和失败状态。

使用 Phaser 实现 2D 打砖块游戏

在该手把手教程中，你将实现与之前的教程系列中相同的打砖块游戏，只不过这次你将使用 Phaser HTML 游戏框架来实现。这里的想法是教授一些使用框架的基础知识（及优势），以及基本的游戏机制。

使用设备方向的 2D 迷宫游戏 (en-US)

该教程展示如何使用 HTML，结合碰撞检测及在 `<canvas>` 中放置精灵的基础知识来创建一个 2D 迷宫游戏。这是一个移动端游戏，它使用设备方向 (en-US)和振动 **API** 来增强游戏体验，并使用 Phaser 框架构建。

使用 Phaser 构建 2D 平台游戏

本教程系列展示了如何使用 Phaser 创建一个简单的平台游戏，涵盖了精灵、碰撞、物理、收集物品等基础知识。


2D breakout game using pure JavaScript - 游戏开发

2D breakout game using pure JavaScript
======================================

* 下一页 » (en-US)

在这里我们将完全使用 JavaScript 语言基于 HTML5 的 canvas 标签，一步一步的绘制一个简单的 MDN 消除游戏。

过程中的每一步都会有例子可供体验，让你更清晰的了解他的制作过程。你将学习到如何使用 canvas 标签的基本语法去实现简单游戏的渲染、动画、碰撞、控制胜负。

为了更快速高效的学习本系列教程，你需要掌握 JavaScript 的一些基础知识。学习完本教程之后你就可以创建自己的网页小游戏了。

![Gameplay screen from the game MDN Breakout where you can use your paddle to bounce the ball and destroy the brick field, with keeping the score and lives.](/zh-CN/docs/Games/Tutorials/2D_Breakout_game_pure_JavaScript/mdn-breakout-gameplay.png)

教程详情
----

全部教程 — MDN 消除小游戏 的各个版本我们正一起管理并托管到 GitHub 上：

1. 创建、绘制画布 (en-US)
2. 让球动起来 (en-US)
3. 反弹的墙 (en-US)
4. 键盘操作 (en-US)
5. 游戏结束 (en-US)
6. 创建砖块 (en-US)
7. 撞击处理 (en-US)
8. 统计得分、获得胜利 (en-US)
9. 鼠标控制 (en-US)
10. 完成 (en-US)

对于实现一个网页游戏而言，最好的方式是从纯 JavaScirpt 着手，那样可以让我们建立更坚实的基础。之后你可以在你的项目中选择你喜欢的框架。框架也只是用 JavaScript 语言实现的工具；如果你想要在项目中使用框架，你必须先了解语言本身。框架可以帮你提高开发效率并生成一些基础的内容；但是如果没有达到你的预期，你只能慢慢调试或者使用原生 JavaScript 去实现解决方案。

**备注：** 如果你对使用第三方游戏资源库开发 2d 网页游戏感兴趣，可以参考本系列教程的 2D breakout game using Phaser (en-US).

**备注：** 本系列教程可以用作游戏开发工作室的素材资源。如果你想探讨普通的游戏开发，你可以利用Gamedev Canvas Content Kit以及本教程的内容。

下一步
---

好，让我们开始吧。第一步 — 创建、绘制画布 (en-US)

* 下一页 » (en-US)


使用 Phaser 开发 2D breakout game - 游戏开发

使用 Phaser 开发 2D breakout game
=============================

* 下一页 » (en-US)

在这个手把手的教程中，我们将使用 Phaser 框架制作一个使用 JavaScript 构建简单的 MDN 消除游戏。

教程的每一步骤都会有可供修改的样品来玩，所以你可以看到开发的每一步中间步骤。你将学到如何使用 Phaser 框架来实现基础游戏机制的基本知识，诸如渲染和移动图像，碰撞检测，控制机制，框架特定的帮助器功能，动画和补间，以及获胜和失败状态等。

为了充分理解这一系列的文章，你应该确保已有基本的中级 JavaScript 知识。学完本教程，你将有能力用 Phaser 构建简单的 Web 游戏。

![Gameplay screen from the game MDN Breakout created with Phaser where you can use your paddle to bounce the ball and destroy the brick field, with keeping the points and lives.](/zh-CN/docs/Games/Tutorials/2D_breakout_game_Phaser/mdn-breakout-phaser.png)

教学清单
----

所有的课程 — 以及我们接下来将一起做的各个版本的 MDN Breakout game 都能在 GitHub上找到

1. 初始化框架 (en-US)
2. 缩放 (en-US)
3. 加载资源并在屏幕上打印 (en-US)
4. 移动小球 (en-US)
5. 物理 (en-US)
6. 从墙上弹开 (en-US)
7. 弹块和控制 (en-US)
8. 游戏结束 (en-US)
9. 建立砖块 (en-US)
10. 碰撞检测 (en-US)
11. 得分 (en-US)
12. 胜利 (en-US)
13. 额外生命 (en-US)
14. 动画与补间 (en-US)
15. 按钮 (en-US)
16. 随机游戏 (en-US)

学习路线的小提示 — 最好先熟悉使用原生 JavaScript 进行网页游戏开发，这样可以打下坚实的基础。如果你还不熟悉原生 javascript 开发，我们建议你先过一遍这个系列，使用原生 Javascript 开发 MDN 消除游戏 (en-US).

在那之后，你就能随意挑选框架并用在你的项目中;我们选择了 Phaser 这个稳定优越的框架，它有着好的支持和社区环境以及大量优秀的插件。框架加速了开发并能帮你管理无趣的部分，让你专注于有意思的事务。然而，框架也有不好的地方，所以当一些意想不到的事情发生了或者想实现一些框架没有提供的功能时，你就将需要原生的 JavaScript 知识了。

**备注：** 本系列文章可用作实际游戏开发的材料。如果你想要使用 Phaser 讨论游戏开发，你还可以使用基于本教程的 Gamedev Phaser 内容套件.

Next steps
----------

好了，那我们就开始吧！前往系列第一部分 — 初始化框架 (en-US).

* 下一页 » (en-US)


Initialize the framework - 游戏开发

Initialize the framework
========================

* « 上一页
* 下一页 »

这是Gamedev Phaser 教程 (en-US)系列的第一课。在课程完成之后，你可以在Gamedev-Phaser-Content-Kit/demos/lesson01.html找到源码。

在我们开始写游戏的功能之前，我们需要创建一个用来内部渲染游戏的基础架构。使用 HTML 就能做到 — Parser 框架将生成所需的 `<canvas>` 元素。

游戏的 HTML
--------

HTML 文档结构非常的简单，这个游戏将整个被渲染在框架生成的`<canvas>` 元素上。拿起你最爱的编辑器，挑一个好目录，创建一个 HTML 文档，存成 index.html，然后写下下面的代码：

```html
<!doctype html>
<html>
  <head>
    <meta charset="utf-8" />
    <title>Gamedev Phaser Workshop - lesson 01: Initialize the framework</title>
    <style>
 \* {
 padding: 0;
 margin: 0;
 }
 </style>
    <script src="js/phaser.min.js"></script>
  </head>
  <body>
    <script>
 var game = new Phaser.Game(480, 320, Phaser.AUTO, null, {
 preload: preload,
 create: create,
 update: update,
 });
 function preload() {}
 function create() {}
 function update() {}
 </script>
  </body>
</html>
```

下载 Phaser
---------

下面我们将下载 Phaser 的代码，并应用到我们的 HTML 文档中。

1. 进入 Phaser 下载页面.
2. 选择最适合你的下载项 — 我们建议选择 min.js，因为它最小，而且你不太可能想去看它的源码
3. 将 Phaser 的源码存到一个和 index.html 同级的 /js 的目录下
4. 在上面第一个 `<script>` 标签里写下 phaser 的路径。
捋一捋我们干了些啥
---------

这个时候我们在 `<header>` 里定义了 `charset`、`<title>` 和一些基础的 css 来重置默认的 margin 和 padding。我们也用 `<script>` 标签向页面引入了 Phaser 源码。body  里也有一个 `<script>` 标签，我们将在里面写 JavaScript 代码来渲染和控制游戏。

`<canvas>` 元素是由框架自动生成的。我们是通过 `Phaser.Game` 创建一个对象并赋给了 game 变量来完成初始化的。参数的含义是：

* width 和 height 设置了 `<canvas>` 宽高。
* 渲染方式。有三个选项分别是 `AUTO`，`CANVAS` 和 `WEBGL`。我们可以指定使用 Canvas 还是 WebGL 来渲染，如果使用了 Auto 则优先使用 WebGL，如果浏览器不支持则会选择 Canvas。
* `<canvas>` 的 id。如果该参数有值，则使用该值作为 canvas 标签的 id，我们传入 null，则 phaser 会决定 canvas 的 id 值。
* 第四个参数指定了 phaser 的三个生命周期所对应的函数。我们使用相同的名字来让程序更清晰
    + `preload` 进行资源的加载。
    + `create` 会在资源加载完成后执行一次。
    + `update` 会一直循环执行来处理每一帧动画。
完整示例
----

以下是第一章的完整代码，可以直接在 JSFiddle 中运行：

下一步
---

现在我们已经完成了一个简单的 HTML 页面，并且学习了如何安装 Phaser，让我们继续学习第二章： scaling (en-US).

* « 上一页
* 下一页 »


Animations and tweens - 游戏开发

Animations and tweens
=====================

* « 上一页 (en-US)
* 下一页 » (en-US)

这是Gamedev Phaser 教程 (en-US) 16 的**第 14 步**。你可以在Gamedev-Phaser-Content-Kit / demos / lesson14.html完成本课程后找到源代码。

为了使游戏看起来更加多汁和活泼，我们可以使用动画和补间。这将导致更好，更有趣的体验。让我们来探讨如何在游戏中实现 Phaser 动画和补间。

动画
--

在 Phaser，动画中，涉及从外部来源获取 spritesheet 并依次显示 sprites。作为一个例子，当碰到一些东西时，我们会让球摇摆。

首先，从 Github 抓取 spritesheet并将其保存在你的`/img`目录中。

接下来，我们将加载 spritesheet - 将以下行放在`preload()`函数的底部：

```js
game.load.spritesheet("ball", "img/wobble.png", 20, 20);
```

而不是加载单个图像的球，我们可以加载整个 spritesheet - 不同图像的集合。我们将按顺序显示精灵，创造动画的幻觉。该`spritesheet()`方法的两个额外的表格确定给定 spritesheet 文件中每个单个框架的宽度和高度，指示程序如何切割以获取单个框架。

加载动画
----

接下来，进入你的 create() 函数，找到加载球精灵的行，下面的调用`animations.add()`如下所示：

```js
ball = game.add.sprite(50, 250, "ball");
ball.animations.add("wobble", [0, 1, 0, 2, 0, 1, 0, 2, 0], 24);
```

要向对象添加动画，我们使用该`animations.add()`方法，其中包含以下参数

* 我们为动画选择的名称
* 一个数组，定义在动画过程中显示帧的顺序。如果你再次查看`wobble.png`图像，你会看到有三个框架。Phaser 提取它们并将它们存储在数组中 - 位置 0，1 和 2.上面的数组表示我们显示帧 0，然后是 1，然后是 0 等。
* 帧速率，以 fps 为单位。由于我们以 24fps 运行动画，有 9 帧，动画每秒将显示三次以下。
当球击中桨时应用动画
----------

在`arcade.collide()`处理球和桨（第一行内部`update()`，见下文）之间的碰撞的方法调用中，我们可以添加一个额外的参数，该参数指定每次发生碰撞时执行的功能，与该`ballHitBrick()`功能相同。更新内部的第一行`update()`，如下所示：

```js
function update() {
  game.physics.arcade.collide(ball, paddle, ballHitPaddle);
  game.physics.arcade.collide(ball, bricks, ballHitBrick);
  paddle.x = game.input.x || game.world.width \* 0.5;
}
```

然后我们可以创建`ballHitPaddle()`函数（具有`ball`和`paddle`作为默认参数），在调用时播放摆动动画。在结束`</script>`标签之前添加以下方法：

```js
function ballHitPaddle(ball, paddle) {
  ball.animations.play("wobble");
}
```

每次球击中桨时都会播放动画。你也可以`animations.play()`在`ballHitBrick()`函数内添加调用，如果你觉得它会使游戏看起来更好。

补间
--

而动画依次播放外部精灵，补间游戏中物体的属性平滑，如宽度或不透明度。

让我们在游戏中增加一个补间，使砖块在被球击中时顺利消失。转到你的`ballhitBrick()`功能，找到你的`brick.kill();`行，并将其替换为以下内容：

```js
var killTween = game.add.tween(brick.scale);
killTween.to({ x: 0, y: 0 }, 200, Phaser.Easing.Linear.None);
killTween.onComplete.addOnce(function () {
  brick.kill();
}, this);
killTween.start();
```

让我们来看看这里，看看这里发生了什么：

1. 当定义一个新的补间时，你必须指定哪些属性将被补间 - 在我们的例子中，而不是在被球击中时立即隐藏砖块，我们将把它们的宽度和高度缩放到零，所以它们将很好地消失。最后，我们使用该`add.tween()`方法，指定`brick.scale`为参数，因为这是我们想要补间。
2. 该`to()`方法定义补间结束时对象的状态。它需要一个包含所选参数的期望结束值的对象（比例取尺度值，1 为大小的 100％，0 为大小的 0％等），补间的时间（以毫秒为单位）以及使用的宽松类型补间。
3. 我们还将添加可选的`onComplete`事件处理程序，该处理程序定义了在补间程序完成时要执行的函数。
4. 最后一件事是立即开始补间`start()`。

这是补间定义的扩展版本，但是我们也可以使用速记语法：

```js
game.add
  .tween(brick.scale)
  .to({ x: 2, y: 2 }, 500, Phaser.Easing.Elastic.Out, true, 100);
```

这个补间将使用弹性宽松在半秒内将砖的比例翻倍，将自动启动，延迟 100 毫秒。

比较你的代码
------

你可以在下面的现场演示中查看本课程的完成代码，并使用它来更好地了解它的工作原理：

下一步
---

动画和 tweens 看起来很不错，但我们可以添加更多的我们的游戏 - 在下一节我们将看看处理按钮 (en-US)输入。

* « 上一页 (en-US)
* 下一页 » (en-US)


Load the assets and print them on screen - 游戏开发

Load the assets and print them on screen
========================================

* « 上一页 (en-US)
* 下一页 » (en-US)

这是Gamedev Phaser 教程 (en-US) 16 的**第三步**。你可以在Gamedev-Phaser-Content-Kit / demos / lesson03.html完成本课程后找到源代码

我们的游戏将围绕屏幕滚动，弹出一个桨，摧毁砖块赚取积分 - 熟悉吗？在本文中，我们将介绍如何将 sprite 添加到我们的 gameworld 中。

有一个球
----

我们开始创建一个 JavaScript 变量来表示我们的球 - 在游戏初始化代码（我们的`var game...`块）和`preload()`函数之间添加以下行：

```js
var ball;
```

**备注：** 为了本教程，我们将使用全局变量。**本教程的目的是教导 Phaser 特定的游戏开发方法，而不是主观的最佳方法。**

加载球精灵
-----

使用 Phaser 加载图像并将其打印在我们的画布上比使用纯 JavaScript 容易得多。要加载资产，我们将使用`game`由 Phaser 创建的对象，执行其`load.image()`方法。在`preload()`函数的底部添加以下新行：

```js
function preload() {
  // ...
  game.load.image("ball", "img/ball.png");
}
```

第一个参数是我们要提供资产的名称 - 这将在我们的游戏代码中使用，例如我们的`ball`变量名称，所以我们需要确保它是一样的。第二个参数是图形资源的相对路径。在我们的情况下，我们将加载我们的球的图像（请注意，文件名不一定是一致的，但我们建议，因为它使一切更容易遵循。）

当然，要加载图像，它需要在我们的代码目录中可用。从 Github 抓住球图像，并将其保存`/img`在与`index.html`文件相同位置的目录中。

现在，要在屏幕上显示，我们将使用另一种 Phaser 方法`add.sprite()`：在`create()`函数内添加以下新的代码行，如图所示：

```js
function create() {
  ball = game.add.sprite(50, 50, "ball");
}
```

这将添加球到游戏，并将其呈现在屏幕上。前两个参数是要添加的画布的 x 和 y 坐标，第三个是我们之前定义的资产的名称。就是这样 - 如果你加载你的`index.html`文件，你会看到已经加载并在画布上渲染的图像！

比较你的代码
------

你可以在下面的现场演示中查看本课程的完成代码，并使用它来更好地了解它的工作原理：

下一步
---

打出球很容易; 接下来我们将尝试在屏幕上移动球 (en-US)。

* « 上一页 (en-US)
* 下一页 » (en-US)


Extra lives - 游戏开发

Extra lives
===========

* « 上一页 (en-US)
* 下一页 » (en-US)

这是Gamedev Phaser 教程 (en-US) 16 的**第 13 步**。你可以在Gamedev-Phaser-Content-Kit / demos / lesson13.html完成本课程后找到源代码。

我们可以通过增加生活使游戏更愉快。在这篇文章中，我们将实施一个生活系统，以便玩家可以继续玩，直到他们失去了三个生命，而不仅仅是一个人。

新变量
---

在代码中的现有添加下面添加以下新变量：

```js
var lives = 3;
var livesText;
var lifeLostText;
```

这些分别将存储生命数，显示剩余生命数的文本标签，以及当玩家失去生命之后将在屏幕上显示的文本标签。

定义新的文本标签
--------

定义文本看起来像我们已经在分数 (en-US)课上已经做的 (en-US)。`scoreText`在`create()`函数内的现有定义下方添加以下行：

```js
livesText = game.add.text(game.world.width - 5, 5, "Lives: " + lives, {
  font: "18px Arial",
  fill: "#0095DD",
});
livesText.anchor.set(1, 0);
lifeLostText = game.add.text(
  game.world.width \* 0.5,
  game.world.height \* 0.5,
  "Life lost, click to continue",
  { font: "18px Arial", fill: "#0095DD" },
);
lifeLostText.anchor.set(0.5);
lifeLostText.visible = false;
```

在`livesText`与`lifeLostText`物体看起来非常相似的`scoreText`一个 - 它们定义在屏幕上的位置，显示实际文本和字体样式。前者被锚定在其右上边缘上，与屏幕正确对齐，后者位于中心位置，两者均使用`anchor.set()`。

该`lifeLostText`会表示，只有当生命消失，因此其知名度初始设置为`false`。

### 使我们的文字造型干燥

正如你可能已经注意到，我们使用相同的造型为三种文本：`scoreText`，`livesText`和`lifeLostText`。如果我们想要更改字体大小或颜色，我们必须在多个地方进行。为了使我们更容易维护，将来我们可以创建一个单独的变量来保存我们的样式，让我们`textStyle`将其调用并放在文本定义之前：

```js
textStyle = { font: "18px Arial", fill: "#0095DD" };
```

现在我们可以在使用文本标签的时候使用这个变量 - 更新你的代码，使文本样式的多个实例被替换为变量：

```js
scoreText = game.add.text(5, 5, "Points: 0", textStyle);
livesText = game.add.text(
  game.world.width - 5,
  5,
  "Lives: " + lives,
  textStyle,
);
livesText.anchor.set(1, 0);
lifeLostText = game.add.text(
  game.world.width \* 0.5,
  game.world.height \* 0.5,
  "Life lost, click to continue",
  textStyle,
);
lifeLostText.anchor.set(0.5);
lifeLostText.visible = false;
```

这样一来，改变一个变量中的字体将会将更改应用于每个使用的地方。

生活处理代码
------

为了在我们的游戏中实现生活，让我们先改变球对`onOutOfBounds`事件的影响。而不是执行匿名函数并立即显示警报：

```js
ball.events.onOutOfBounds.add(function () {
  alert("Game over!");
  location.reload();
}, this);
```

我们将分配一个所谓的新功能`ballLeaveScreen`; 删除以前的事件处理程序（如上所示），并将其替换为以下行：

```js
ball.events.onOutOfBounds.add(ballLeaveScreen, this);
```

我们想减少每次球离开帆布的人数。`ballLeaveScreen()`在代码末尾添加函数定义：

```js
function ballLeaveScreen() {
  lives--;
  if (lives) {
    livesText.setText("Lives: " + lives);
    lifeLostText.visible = true;
    ball.reset(game.world.width \* 0.5, game.world.height - 25);
    paddle.reset(game.world.width \* 0.5, game.world.height - 5);
    game.input.onDown.addOnce(function () {
      lifeLostText.visible = false;
      ball.body.velocity.set(150, -150);
    }, this);
  } else {
    alert("You lost, game over!");
    location.reload();
  }
}
```

而不是立即打印警报，当你失去了一生，我们首先从当前的数字减去一个生命，并检查它是否是一个非零值。如果是，那么玩家还是有一些生命剩下，可以继续玩 - 他们会看到生命中的消息，球和桨的位置将被重置在屏幕上和下一个输入（点击或触摸），消息将被隐藏球将再次开始移动。

当可用生活数量达到零时，游戏结束，并显示游戏过期警报消息。

事件
--

你可能已经注意到了`add()`，并`addOnce()`在上面的两个代码块的方法调用，并想知道它们的区别。不同之处在于，该`add()`方法绑定给定的函数，并使其在每次事件发生时执行，同时`addOnce()`当你希望绑定函数只执行一次，然后解除绑定时有用，因此不会再次执行。在我们的例子中，每个`outOfBounds`事件`ballLeaveScreen`都将被执行，但当球离开屏幕时，我们只想从屏幕上删除一次消息。

比较你的代码
------

你可以在下面的现场演示中查看本课程的完成代码，并使用它来更好地了解它的工作原理：

下一步
---

生活让游戏更加宽容 - 如果你失去一个生命，你还剩下两个，可以继续玩。现在让我们通过添加动画和补间来 (en-US)扩展游戏的外观和感觉。

* « 上一页 (en-US)
* 下一页 » (en-US)


Win the game - 游戏开发

Win the game
============

* « 上一页 (en-US)
* 下一页 » (en-US)

这是Gamedev Phaser 教程 (en-US) 16 的**第 12 步**。你可以在Gamedev-Phaser-Content-Kit / demos / lesson12.html完成本课程后找到源代码。

在我们的游戏中实现获胜是相当容易的：如果你碰巧摧毁所有的砖块，那么你赢了。

如何取胜？
-----

将以下新代码添加到你的`ballHitBrick()`函数中：

```js
function ballHitBrick(ball, brick) {
  brick.kill();
  score += 10;
  scoreText.setText("Points: " + score);

  var count_alive = 0;
  for (i = 0; i < bricks.children.length; i++) {
    if (bricks.children[i].alive == true) {
      count_alive++;
    }
  }
  if (count_alive == 0) {
    alert("You won the game, congratulations!");
    location.reload();
  }
}
```

我们循环使用组中的砖块`bricks.children`，检查每个砖块的`.alive()` 方法的活力。如果没有更多的砖块活着，那么我们会显示一个获胜的消息，一旦警报被关闭，重新启动游戏。

比较你的代码
------

你可以在下面的现场演示中查看本课程的完成代码，并使用它来更好地了解它的工作原理：

下一步
---

失败和获胜都是实施的，所以我们的游戏的核心游戏就完成了。现在让我们添加一些额外的东西 - 我们会给玩家将 3 个生活 (en-US)的，而不是一个。

* « 上一页 (en-US)
* 下一页 » (en-US)


Build the brick field - 游戏开发

Build the brick field
=====================

* « 上一页 (en-US)
* 下一页 » (en-US)

这是Gamedev Phaser 教程 (en-US) 16 的**第 9 步**。在Gamedev-Phaser-Content-Kit / demos / lesson09.html完成本课后，你可以找到源代码。

建立砖块比将单个对象添加到屏幕要复杂一点，尽管使用 Phaser 还是比纯 JavaScript 更容易。我们来探讨如何创建一组砖块，并使用循环在屏幕上打印。

定义新变量
-----

首先，我们定义所需的变量 - 在以前的变量定义中添加以下内容：

```js
var bricks;
var newBrick;
var brickInfo;
```

该`bricks`变量将用于创建一个组，`newBrick`将在循环的每次迭代中添加到组中的新对象，`brickInfo`并将存储我们需要的所有数据。

渲染砖图像
-----

接下来，我们加载砖的图像 - `load.image()`在其他地方添加以下调用：

```js
function preload() {
  // ...
  game.load.image("brick", "img/brick.png");
}
```

你还需要从 Github 抓取砖图像并将其保存在你的`/img`目录中。

画砖
--

我们将将所有用于绘制砖块的代码放在一个`initBricks`函数中，以使其与其余代码分离。`initBricks`在`create()`函数末尾添加一个调用：

```js
function create() {
  // ...
  initBricks();
}
```

现在到函数本身。`initBricks()`在我们的游戏代码末尾添加功能，就在关闭</ script>标签之前，如下所示。首先我们已经包括了这个 `brickInfo`对象，因为这很快就会派上用场：

```js
function initBricks() {
  brickInfo = {
    width: 50,
    height: 20,
    count: {
      row: 7,
      col: 3,
    },
    offset: {
      top: 50,
      left: 60,
    },
    padding: 10,
  };
}
```

这个`brickInfo`对象将包含我们需要的所有信息：单个砖的宽度和高度，我们将在屏幕上看到的砖的行数和列数，顶部和左边的偏移量（画布上我们将开始绘制的位置）砖块）和每一列和砖块之间的填充。

现在，让我们开始创建砖块 - 首先添加一个空组来包含砖块，在`initBricks()`函数底部添加以下行：

```js
bricks = game.add.group();
```

我们可以循环遍历行和列，以便在每次迭代中创建新的砖块 - 在上一行代码下面添加以下嵌套循环：

```js
for (c = 0; c < brickInfo.count.col; c++) {
  for (r = 0; r < brickInfo.count.row; r++) {
    // create new brick and add it to the group
  }
}
```

这样我们将创建我们需要的确切数量的砖，并将它们全部包含在一个组中。现在我们需要在嵌套循环结构中添加一些代码来绘制每个砖块。填写内容如下图所示：

```js
for (c = 0; c < brickInfo.count.col; c++) {
  for (r = 0; r < brickInfo.count.row; r++) {
    var brickX = 0;
    var brickY = 0;
    newBrick = game.add.sprite(brickX, brickY, "brick");
    game.physics.enable(newBrick, Phaser.Physics.ARCADE);
    newBrick.body.immovable = true;
    newBrick.anchor.set(0.5);
    bricks.add(newBrick);
  }
}
```

在这里，我们循环遍历行和列，创建新的砖块并将其放在屏幕上。新创建的砖块为 Arcade 物理引擎启用，它的身体被设置为不可移动（所以当球被击中时它不会移动），我们还将锚点放在中间并添加砖到集团。

目前的问题是，我们在一个地方绘制所有的砖，坐标（0,0）。我们需要做的是将每个砖块绘制在自己的 x 和 y 位置。更新`brickX`和`brickY`行如下：

```js
var brickX = r \* (brickInfo.width + brickInfo.padding) + brickInfo.offset.left;
var brickY = c \* (brickInfo.height + brickInfo.padding) + brickInfo.offset.top;
```

每个`brickX`位置都是`brickInfo.width`加`brickInfo.padding`号乘以行号`r`，加上`brickInfo.offset.left`; 用于所述逻辑`brickY`是不同之处在于它使用的值列号相同`c`，`brickInfo.height`和`brickInfo.offset.top`。现在每个砖都可以放置在正确的位置，每个砖块之间填充，并从左侧和顶部画布边缘偏移绘制。

检查 initBricks() 代码
------------------

这是功能的完整代码`initBricks()`：

```js
function initBricks() {
  brickInfo = {
    width: 50,
    height: 20,
    count: {
      row: 7,
      col: 3,
    },
    offset: {
      top: 50,
      left: 60,
    },
    padding: 10,
  };
  bricks = game.add.group();
  for (c = 0; c < brickInfo.count.col; c++) {
    for (r = 0; r < brickInfo.count.row; r++) {
      var brickX =
        r \* (brickInfo.width + brickInfo.padding) + brickInfo.offset.left;
      var brickY =
        c \* (brickInfo.height + brickInfo.padding) + brickInfo.offset.top;
      newBrick = game.add.sprite(brickX, brickY, "brick");
      game.physics.enable(newBrick, Phaser.Physics.ARCADE);
      newBrick.body.immovable = true;
      newBrick.anchor.set(0.5);
      bricks.add(newBrick);
    }
  }
}
```

如果你现在重新加载`index.html`，你应该看到在屏幕上打印的砖块彼此相距甚远。

比较你的代码
------

你可以在下面的现场演示中查看本课程的完成代码，并使用它来更好地了解它的工作原理：

下一步
---

有些东西丢失了 球不经停，经过砖块 - 我们需要适当的碰撞检测 (en-US)。

* « 上一页 (en-US)
* 下一页 » (en-US)


Randomizing gameplay - 游戏开发

Randomizing gameplay
====================

* « 上一页 (en-US)

这是Gamedev Phaser 教程 (en-US) 16 中的第**16 步**。你可以在Gamedev-Phaser-Content-Kit / demos / lesson16.html完成本课程后找到源代码。

我们的游戏似乎已经完成了，但是如果你看起来足够近，你会发现球在整个游戏中都以相同的角度从桨上弹起。这意味着每个游戏都非常相似。为了解决这个问题，提高可玩性，我们应该使反弹角度更加随机，在本文中我们将介绍一下如何。

让篮板更随机
------

我们可以根据撞击桨的确切位置来改变球的速度，通过使用沿着下方的线路运行功能来修改`x`速度`ballHitPaddle()`。现在添加这一行到你的代码，并尝试。

```js
function ballHitPaddle(ball, paddle) {
  ball.animations.play("wobble");
  ball.body.velocity.x = -1 \* 5 \* (paddle.x - ball.x);
}
```

这有点魔法 - 新的速度越高，桨的中心和球撞到的地方之间的距离就越大。此外，方向（左或右）由该值确定 - 如果球击中桨的左侧，则其将向左反弹，而击球右侧将向右反弹。最终这样做是因为对某些值进行了一些实验，你可以进行自己的实验，看看会发生什么。这当然不是完全随机的，但它确实使游戏玩法变得更加不可预测，因此更有趣。

比较你的代码
------

你可以在下面的现场演示中查看本课程的完成代码，并使用它来更好地了解它的工作原理：

概要
--

你已经完成了所有的课程 - 恭喜你！在这一点上，你将了解到 Phaser 的基础知识和简单 2D 游戏背后的逻辑。

### 练习跟随

你可以在游戏中做更多的事情 - 添加任何你觉得最好的东西，使它更有趣和有趣。Phaser 提供的无数有用的方法的基本介绍。以下是关于如何扩展我们的小游戏的一些建议，让你开始：

* 添加第二个球或桨。
* 改变每次命中背景的颜色。
* 更改图像并使用自己的图像。
* 如果砖块被迅速摧毁，并排排列（或你选择的其他奖金），则可获得额外的奖励积分。
* 创建不同砖块布局的水平。

一定要检查越来越多的示例列表和官方文档，如果你需要任何帮助，请访问HTML5 Gamedevs 论坛。

你也可以返回本教程系列的索引页 (en-US)。

* « 上一页 (en-US)


Bounce off the walls - 游戏开发

Bounce off the walls
====================

* « 上一页 (en-US)
* 下一页 » (en-US)

这是Gamedev Phaser 教程 (en-US)的**第 6 步**。在Gamedev-Phaser-Content-Kit / demos / lesson06.html完成本课后，你可以找到源代码。

现在已经介绍了物理引擎，我们可以开始在游戏中实现碰撞检测 - 首先我们来看看墙壁。

反弹边界
----

让我们的球从墙壁上弹起的最简单的方法是告诉框架，我们想要将 `<canvas>` 元素的边界视为墙壁，而不是让球移过它们。在 Phaser 中，可以使用该 `collideWorldsBound` 属性轻松实现。在现有 `game.physics.enable()` 方法调用之后添加此行：

```js
ball.body.collideWorldBounds = true;
```

现在球将停在屏幕的边缘，而不是消失，但它不会弹起。为了使这种情况发生，我们必须设置它的 bounciness。在上一行下面添加以下行：

```js
ball.body.bounce.set(1);
```

再次尝试重新加载 index.html - 现在你应该看到球从墙壁上弹起并在画布区域内移动。

比较你的代码
------

你可以在下面的现场演示中查看本课程的完成代码，并使用它来更好地了解它的工作原理：

下一步
---

现在开始看起来更像是一个游戏，但是我们无法以任何方式控制它 - 现在是介绍玩家挡板和控制的时候了 (en-US)。

* « 上一页 (en-US)
* 下一页 » (en-US)


The score - 游戏开发

The score
=========

* « 上一页 (en-US)
* 下一页 » (en-US)

这是Gamedev Phaser 教程 (en-US) 16 的**第 11 步**。你可以在Gamedev-Phaser-Content-Kit / demos / lesson11.html完成本课程后找到源代码。

得分也可以使游戏更有趣 - 你可以尝试击败自己的高分，或者你的朋友。在这篇文章中，我们将为我们的游戏添加一个评分系统。

我们将使用一个单独的变量来存储分数和 Phaser 的`text()`方法将其打印到屏幕上。

新变量
---

在以前定义的之后添加两个新变量：

```js
// ...
var scoreText;
var score = 0;
```

将得分文字添加到游戏显示
------------

现在在`create()`函数末尾添加这一行：

```js
scoreText = game.add.text(5, 5, "Points: 0", {
  font: "18px Arial",
  fill: "#0095DD",
});
```

该`text()`方法可以采用四个参数：

* x 和 y 坐标来绘制文本。
* 将呈现的实际文本。
* 用于呈现文本的字体样式。

最后一个参数与 CSS 样式非常相似。在我们的例子中，乐谱文字将为蓝色，大小为 18 像素，并使用 Arial 字体。

当砖块被破坏时更新分数
-----------

每当球击中砖块时，我们将增加点数，更新`scoreText`显示当前得分。这可以使用`setText()`方法 - 添加以下两行新`ballHitBrick()`功能：

```js
function ballHitBrick(ball, brick) {
  brick.kill();
  score += 10;
  scoreText.setText("Points: " + score);
}
```

这是现在 - 重新加载你的，`index.html`并检查得分更新每个砖击。

比较你的代码
------

你可以在下面的现场演示中查看本课程的完成代码，并使用它来更好地了解它的工作原理：

下一步
---

我们现在有一个得分系统，但是如果你不能赢得，那么玩和保持分数是多少？让我们看看我们如何能够增加胜利的状态，让我们赢得比赛 (en-US)。

* « 上一页 (en-US)
* 下一页 » (en-US)


Physics - 游戏开发

Physics
=======

* « 上一页 (en-US)
* 下一页 » (en-US)

这是Gamedev Phaser 教程 (en-US) 16 的**第 5 步**。你可以在Gamedev-Phaser-Content-Kit / demos / lesson05.html完成本课程后找到源代码

为了在我们的游戏中的对象之间进行正确的碰撞检测，我们将需要物理学; 本文将向你介绍 Phaser 中的可用内容，以及演示典型的简单设置。

添加物理效果
------

Phaser 与三个不同的物理引擎（Arcade Physics，P2 和 Ninja Physics）捆绑在一起，第四个选项 Box2D 可作为商业插件使用。对于像我们这样的简单游戏，我们可以使用 Arcade Physics 引擎。我们不需要任何重的几何计算 - 毕竟只是一个球从墙壁和砖块弹起来。

首先，让我们在游戏中初始化 Arcade Physics 引擎。`physics.startSystem()`在`create`函数开头添加方法（使其成为函数内的第一行），如下所示：

```js
game.physics.startSystem(Phaser.Physics.ARCADE);
```

接下来，我们需要为物理系统启用我们的球 - 默认情况下，Phaser 对象物理不启用。在`create()`函数底部添加以下行：

```js
game.physics.enable(ball, Phaser.Physics.ARCADE);
```

接下来，如果我们要在屏幕上移动我们的球，我们可以设置`velocity`它`body`。再次添加以下行`create()`：

```js
ball.body.velocity.set(150, 150);
```

删除我们以前的更新说明
-----------

记得删除添加值的我们的老方法`x`，并`y`从`update()`功能：

```js
function update() {
  ball.x += 1;
  ball.y += 1;
}
```

我们正在使用物理引擎正确处理。

最终代码检查
------

最新的代码应该如下所示：

```js
var ball;

function preload() {
  game.scale.scaleMode = Phaser.ScaleManager.SHOW\_ALL;
  game.scale.pageAlignHorizontally = true;
  game.scale.pageAlignVertically = true;
  game.stage.backgroundColor = "#eee";
  game.load.image("ball", "img/ball.png");
}

function create() {
  game.physics.startSystem(Phaser.Physics.ARCADE);
  ball = game.add.sprite(50, 50, "ball");
  game.physics.enable(ball, Phaser.Physics.ARCADE);
  ball.body.velocity.set(150, 150);
}

function update() {}
```

尝试重新加载`index.html`- 球应该在给定的方向上不断移动。目前，物理引擎的重力和摩擦力设定为零。增加重力将导致球落下，同时摩擦力最终会停止球。

物理效果趣味
------

你可以用物理学来做更多的事情，例如添加`ball.body.gravity.y = 100;`你将设置球的垂直重力。因此，它将向上发射，但是由于重力的作用而下降。

这种功能只是冰山一角 - 有各种功能和变量可以帮助你操纵物理对象。查看官方物理文档，并使用Arcade和P2物理系统查看大量示例。

比较你的代码
------

你可以在下面的现场演示中查看本课程的完成代码，并使用它来更好地了解它的工作原理：

下一步
---

现在我们可以转到下一课，看看如何让球从墙上弹起 (en-US)。

* « 上一页 (en-US)
* 下一页 » (en-US)


Collision detection - 游戏开发

Collision detection
===================

* « 上一页 (en-US)
* 下一页 » (en-US)

这是Gamedev Phaser 教程 (en-US) 16 的**第 10 步**。你可以在Gamedev-Phaser-Content-Kit / demos / lesson10.html完成本课程后找到源代码。

现在接下来的挑战 - 球和砖块之间的碰撞检测。幸运的是，我们可以使用物理引擎来检查单个对象（如球和桨）之间的碰撞，也可以检测对象和组之间的碰撞。

砖/球碰撞检测
-------

物理引擎使一切都变得更容易 - 我们只需要添加两个简单的代码。首先，在你的`update()`函数中添加一行，检查球和砖之间的碰撞检测，如下所示：

```js
function update() {
  game.physics.arcade.collide(ball, paddle);
  game.physics.arcade.collide(ball, bricks, ballHitBrick);
  paddle.x = game.input.x || game.world.width \* 0.5;
}
```

球的位置是根据组中所有砖的位置计算的。第三个可选参数是发生冲突时执行的功能`ballHitBrick()`。创建这个新功能作为代码的底部，就在结束`</script>`标签之前，如下所示：

```js
function ballHitBrick(ball, brick) {
  brick.kill();
}
```

就是这样！重新加载你的代码，你应该看到新的碰撞检测工作正常。

感谢 Phaser，有两个参数传递给函数 - 第一个是球，我们在碰撞方法中明确定义，第二个是球碰撞的砖组中的单个砖。在功能内部，我们从屏幕上删除所讨论的砖块，只需运行其`kill()`上的方法即可。

你将期望在使用纯 JavaScript (en-US)时编写更多自己的计算机来实现碰撞检测。这是使用框架的好处 - 你可以为 Phaser 留下大量无聊的代码，并专注于制作游戏中最有趣和最有趣的部分。

比较你的代码
------

你可以在下面的现场演示中查看本课程的完成代码，并使用它来更好地了解它的工作原理：

下一步
---

我们可以打砖块并删除它们，这已经是游戏的一个很好的补充。结果，更好地计算被毁砖增加得分 (en-US)。

* « 上一页 (en-US)
* 下一页 » (en-US)


Buttons - 游戏开发

Buttons
=======

* « 上一页 (en-US)
* 下一页 » (en-US)

这是Gamedev Phaser 教程 (en-US) 16 的**第 15 步**。你可以在Gamedev-Phaser-Content-Kit / demos / lesson15.html完成本课程后找到源代码

而不是立即开始游戏，我们可以通过添加他们可以按的开始按钮将该决定留给玩家。我们来调查如何做到这一点。

新变量
---

我们需要一个变量来存储表示游戏当前是否正在播放的布尔值，另一个代表我们的按钮。将以下行添加到其他变量定义之下：

```js
var playing = false;
var startButton;
```

加载按钮 spritesheet
----------------

我们可以加载按钮 spritesheet 与我们加载球的摆动动画相同的方式。将以下内容添加到`preload()`函数底部：

```js
game.load.spritesheet("button", "img/button.png", 120, 40);
```

单个按钮框架宽 120 像素，高 40 像素。

你还需要从 Github 抓取按钮 spritesheet，并将其保存在你的`/img`目录中。

将按钮添加到游戏中
---------

使用该`add.button`方法可以将新的按钮添加到游戏中。将以下行添加到`create()`函数的底部：

```js
startButton = game.add.button(
  game.world.width \* 0.5,
  game.world.height \* 0.5,
  "button",
  startGame,
  this,
  1,
  0,
  2,
);
startButton.anchor.set(0.5);
```

该`button()`方法的参数如下：

* 按钮的 x 和 y 坐标
* 要显示按钮的图形资产的名称
* 按下按钮时将执行的回调函数
* `this`指定执行上下文的引用
* 将用于*过度*，*超出*和*向下*事件的框架。

**备注：** 超越事件与悬停相同，当指针从按钮中移出时，当按下按钮时，向下移动。

现在我们需要定义`startGame()`上面代码中引用的函数：

```js
function startGame() {
  startButton.destroy();
  ball.body.velocity.set(150, -150);
  playing = true;
}
```

当按下按钮时，我们删除按钮，设置球的初始速度并将`playing`变量设置为`true`。

最后对于这一部分，回到你的`create()`函数，找到`ball.body.velocity.set(150, -150);`一行，并删除它。你只需要按下按钮时移动球，而不是之前！

在游戏开始之前仍然保持桨
------------

它按预期工作，但是当游戏尚未开始时，我们仍然可以移动桨，这看起来有点愚蠢。为了阻止这一点，我们可以利用`playing`变量，使得桨只有在游戏开始时才能移动。要做到这一点，调整`update()`功能如下所示：

```js
function update() {
  game.physics.arcade.collide(ball, paddle, ballHitPaddle);
  game.physics.arcade.collide(ball, bricks, ballHitBrick);
  if (playing) {
    paddle.x = game.input.x || game.world.width \* 0.5;
  }
}
```

这样一来，在所有的装载和准备之后，但在实际游戏开始之前，桨是不可移动的。

比较你的代码
------

你可以在下面的现场演示中查看本课程的完成代码，并使用它来更好地了解它的工作原理：

下一步
---

在本系列文章中我们将做的最后一件事情是，通过添加一些随机化 (en-US)的方式，球从球上弹起来，使游戏更有趣。

* « 上一页 (en-US)
* 下一页 » (en-US)


Game over - 游戏开发

Game over
=========

* « 上一页 (en-US)
* 下一页 » (en-US)

这是Gamedev Phaser 教程 (en-US) 16 的**第 8 步**。在Gamedev-Phaser-Content-Kit / demos / lesson08.html完成本课后，你可以找到源代码。

为了使游戏更有趣，我们可以引入失去的能力 - 如果在到达屏幕底部边缘之前没有击球，那么这个游戏将会结束。

如何输
---

为了提供丢失的能力，我们将禁用球与屏幕底部的碰撞。在`create()`函数内添加下面的代码; 刚刚定义球的属性就好了：

```js
game.physics.arcade.checkCollision.down = false;
```

这将使三个墙壁（顶部，左侧和右侧）弹回球，但是第四个（底部）将消失，如果桨错过，则球从屏幕上脱落。我们需要一种方法来检测并相应地采取行动。在以前的新的下方添加以下行：

```js
ball.checkWorldBounds = true;
ball.events.onOutOfBounds.add(function () {
  alert("Game over!");
  location.reload();
}, this);
```

添加这些行将使得球检查世界（在我们的例子中是画布）边界并执行绑定到`onOutOfBounds`事件的函数。当你点击生成的警报时，页面将被重新加载，以便你可以再次播放。

比较你的代码
------

你可以在下面的现场演示中查看本课程的完成代码，并使用它来更好地了解它的工作原理：

下一步
---

现在的基本游戏就是让我们通过引入砖块来更有趣的是 - 现在是建造砖块 (en-US)的时候了。

* « 上一页 (en-US)
* 下一页 » (en-US)


Player paddle and controls - 游戏开发

Player paddle and controls
==========================

* « 上一页 (en-US)
* 下一页 » (en-US)

这是Gamedev Phaser 教程 (en-US) 16 的**第 7 步**。你可以在Gamedev-Phaser-Content-Kit / demos / lesson07.html完成本课程后找到源代码。

我们有球从墙上移动并弹跳，但它很快变得无聊 - 没有互动！我们需要一种介绍游戏的方法，所以在这篇文章中，我们将创建一个桨来移动并击中球。

渲染桨
---

从框架的角度看，桨非常类似于球 - 我们需要添加一个变量来表示它，加载相关的图像资源，然后做出魔法。

### 装载桨

首先，添加`paddle`我们将在我们的游戏中使用的`ball`变量，就在变量之后：

```js
var paddle;
```

然后，在该`preload`功能中，`paddle`通过添加以下新`load.image()`调用来加载图像：

```js
function preload() {
  // ...
  game.load.image("ball", "img/ball.png");
  game.load.image("paddle", "img/paddle.png");
}
```

### 添加桨图形

所以我们不要忘记，在这一点上，你应该从 Github 抓住这个图形，并保存在你的`/img`文件夹中。

### 渲染桨用物理引擎

接下来，我们将通过`add.sprite()`在`create()`函数中添加以下调用来初始化我们的桨，将其添加到底部：

```js
paddle = game.add.sprite(
  game.world.width \* 0.5,
  game.world.height - 5,
  "paddle",
);
```

我们可以使用`world.width`和`world.height`值来将桨定位到我们想要的位置：`game.world.width*0.5`将在屏幕中间。在我们这个例子中，世界和画布是一样的，但是对于其他类型的游戏，例如侧滚滚，这个世界会变大，你可以修改它来创造有趣的效果。

你会注意到，如果你`index.html`在这一点上重新加载，那么桨是目前不完全在中间的。为什么？因为计算位置的锚总是从对象的左上角开始。我们可以改变它，使锚在桨的宽度的中间和它的高度的底部，所以更容易将其定位在底部边缘。添加以下新行以下的行：

```js
paddle.anchor.set(0.5, 1);
```

桨现在位于我们想要的地方。现在，为了使它与球碰撞，我们必须为桨提供物理效果。继续添加以下新行，再次在`create()`函数的底部：

```js
game.physics.enable(paddle, Phaser.Physics.ARCADE);
```

现在，魔法可以开始发生 - 该框架可以在每个框架上检查碰撞检测。要启用桨和球之间的碰撞检测，请将`collide()`方法添加到如下`update()`功能中：

```js
function update() {
  game.physics.arcade.collide(ball, paddle);
}
```

第一个参数是我们感兴趣的对象之一 - 球 - 第二个是另一个，桨。这有效，但不如我们预期的那样 - 当球击中桨时，桨从屏幕上掉下来！我们想要的就是球从跳板上跳起来，而桨子停在同一个地方。我们可以将`body`桨设置成`immovable`球，所以当球击中它时不会移动。为此，请在`create()`函数底部添加以下行：

```js
paddle.body.immovable = true;
```

现在它按预期工作。

控制桨
---

接下来的问题是我们不能移动桨。要做到这一点，我们可以使用系统的默认输入（鼠标或触摸，取决于平台），并将桨位置设置到位置的`input`位置。将以下新行添加到`update()`函数中，如下所示：

```js
function update() {
  game.physics.arcade.collide(ball, paddle);
  paddle.x = game.input.x;
}
```

现在在每个新的一帧上，桨的`x`位置将根据输入的`x`位置进行调整，但是当我们开始游戏时，桨的位置不在中间。这是因为输入位置尚未定义。要修复，我们可以将默认位置（如果输入位置尚未定义）设置为屏幕中间。更新上一行如下：

```js
paddle.x = game.input.x || game.world.width \* 0.5;
```

如果你还没有这样做，请重新加载`index.html`并尝试！

定位球
---

我们有桨按预期工作，所以我们把球放在上面。它非常类似于定位桨 - 我们需要将其放置在屏幕中间水平和垂直的底部，与底部有一点偏移。要按照我们想要的方式放置它，我们将把锚定位到球的正中间。找到现有的行，并将其替换为以下两行：`ball = game.add.sprite( ... )`

```js
ball = game.add.sprite(game.world.width \* 0.5, game.world.height - 25, "ball");
ball.anchor.set(0.5);
```

速度保持不变 - 我们只是将第二个参数的值从 150 改为 -150，所以球将通过向上移动而不是下降来开始游戏。查找现有`ball.body.velocity.set( ... )`行并将其更新为以下内容：

```js
ball.body.velocity.set(150, -150);
```

现在球将从桨的中间开始。

比较你的代码
------

你可以在下面的现场演示中查看本课程的完成代码，并使用它来更好地了解它的工作原理：

下一步
---

我们可以移动桨，并将球反弹，但是如果球从屏幕的底部边缘反弹，那又有什么意义？我们来介绍丢失的可能性 - 也称为游戏 (en-US)逻辑。

* « 上一页 (en-US)
* 下一页 » (en-US)


Scaling - 游戏开发

Scaling
=======

* « 上一页
* 下一页 »

这是Gamedev Phaser 教程 (en-US)系列的第二课。在课程完成之后，你可以在Gamedev-Phaser-Content-Kit/demos/lesson02.html找到源码。

缩放是指游戏画布如何在不同的屏幕尺寸上进行显示。我们可以在预加载阶段自动使游戏规模适合任何屏幕尺寸，之后就可以不用再担心屏幕尺寸的问题了。

Phaser 中 scale 对象
-----------------

Phaser 中有一个特殊的对象：`scale`，它包含一些特别的方法和属性。让我们来更改一下上一节中创建的的`preload()`函数：

```js
function preload() {
  game.scale.scaleMode = Phaser.ScaleManager.SHOW\_ALL;
  game.scale.pageAlignHorizontally = true;
  game.scale.pageAlignVertically = true;
}
```

`scaleMode` 有几个不同的选项来指定 Canvas 应该如何缩放：

* `NO_SCALE` — 不进行任何缩放。
* `EXACT_FIT` — 拉伸，填充屏幕，不保留长宽比。
* `SHOW_ALL` — 等比缩放，填充屏幕，保留长宽比，剩余空间用黑色填充。
* `RESIZE` — 动态，每次都会根据屏幕生成画布，所以你需要在游戏运行时动态的放置游戏元素。这是一种进阶的模式。
* `USER_SCALE` — 自定义，允许你自己计算大小和比例。这也是一种进阶的模式。

`preload()`中的其他两行代码负责水平和垂直居中画布，所以它始终以屏幕为中心，无论大小如何。

设置背景颜色
------

我们还可以给画布设置背景颜色来替代磨人的黑色背景。通过更改`stage`对象的`backgroundColor`属性来添加，我们可以使用 CSS 颜色定义语法进行设置。我们在刚才的代码下面添加以下代码：

```js
game.stage.backgroundColor = "#eee";
```

完整的代码
-----

你可以在下面的现场演示中查看本课程的完成代码，并使用它来更好地了解它的工作原理：

下一步
---

现在我们设置了我们游戏的缩放比例，让我们继续第三课，并设计出如何加载资源并将其显示在屏幕上 (en-US)。

* « 上一页
* 下一页 »


Move the ball - 游戏开发

Move the ball
=============

* « 上一页 (en-US)
* 下一页 » (en-US)

这是Gamedev Phaser 教程 (en-US) 16 的**第 4 步**。在Gamedev-Phaser-Content-Kit / demos / lesson04.html完成本课后，你可以找到源代码。

我们在屏幕上打印了我们的蓝色球，但它什么都不做，这样做会很酷。本文介绍如何做到这一点。

在每个框架上更新球的位置
------------

记住`update()`功能及其定义？其中的代码在每个框架上执行，所以它是一个完美的地方，将代码更新球的位置在屏幕上。在里面添加以下新行代码`update()`，如下所示：

```js
function update() {
  ball.x += 1;
  ball.y += 1;
}
```

上面的代码在每个框架上为表示画布上的球坐标的属性`x`和`y`属性添加了一个。重新加载 index.html，你应该看到球在屏幕上滚动。

比较你的代码
------

你可以在下面的现场演示中查看本课程的完成代码，并使用它来更好地了解它的工作原理：

下一步
---

下一步是添加一些基本的碰撞检测，所以我们的球可以从墙壁反弹。这将需要几行代码 - 一个比我们迄今为止看到的更复杂的步骤，特别是如果我们也想添加桨和砖碰撞 - 但是幸运的是 Phaser 使我们比我们想要使用纯粹的方法更容易做到这一点 JavaScript 的。

无论如何，在我们做所有的事情之前，我们将首先介绍 Phaser 的物理 (en-US)引擎，并做一些设置工作。

* « 上一页 (en-US)
* 下一页 » (en-US)


反弹的墙壁 - 游戏开发

反弹的墙壁
=====

* « 上一页 (en-US)
* 下一页 » (en-US)

本篇是 Gamedev Canvas tutorial (en-US) 10 节教程中的第三节。如果你完成了本篇教程之后，你可以从 Gamedev-Canvas-workshop/lesson3.html 看到源码。

看到我们的球动起来很惊讶吧，但是它很快就从屏幕上消失了，当然我们是可以控制它的。我们会实现一些非常简单的碰撞检测 (详细后面解释),使球在画布的四周反弹回来。

简单的碰撞
-----

我们将检查球体是否与边缘接触，如果有接触我们将相应的改变它的运动方向。

为了运算方便我们定义一个名为 ballRadius 的变量，来存储球的半径。向代码中添加一下内容：

```js
var ballRadius = 10;
```

现在更新绘制球的 drawBall() 函数：

```js
ctx.arc(x, y, ballRadius, 0, Math.PI \* 2);
```

### 从顶部和底部弹起

有四面墙壁可以让它反弹回来，我们先来看上面的那面墙。我们需要判断球运动的每一帧，球体是否与画布的顶部边缘接触。如果有接触，我们将会改变球体的运动方向，使它向相反的方向移动，并保证它在画布的可见范围之内。记住坐标系统的左上角，让我们开始并加以下代码：

```js
if (y + dy < 0) {
  dy = -dy;
}
```

如果球的纵坐标（y 轴）值小于零，我们将在球体原有的运动方向上逆转。如果球体向上移动的速度是 2 像素/帧，现在就是向上移动速度是 -2 像素。这相当于此时向下移动的速度是 2 像素/帧。

上面的代码将处理球与画布顶部边缘的反射，现在让我们思考一下底部边缘如何处理：

```js
if (y + dy > canvas.height) {
  dy = -dy;
}
```

如果球的 y 位置大于`canvas`的高度（记住，我们从左上角计算 y 值，所以顶部边缘从 0 开始，底部边缘在 480 像素），然后通过像以前那样反转 y 轴运动而离开底部边缘。

我们可以将这两句冗长的代码合二为一：

```js
if (y + dy > canvas.height || y + dy < 0) {
  dy = -dy;
}
```

如果其中一个判断为 `true`, 则反转球的运动。

### 从左边和右边反弹

我们有顶部和底部的边缘，所以我们来考虑一下左边和右边的边缘。实际上非常相似，你所要做的就是颠倒`x`而不是`y`:

```js
if (x + dx > canvas.width || x + dx < 0) {
  dx = -dx;
}

if (y + dy > canvas.height || y + dy < 0) {
  dy = -dy;
}
```

你应该把上面的代码块插入到 draw（）函数中，就在大括号之前。

### 球部分消失在墙上！

测试你的代码，你会看到我们的球碰到任一边缘都会反弹！然而，我们还发现了一个问题，当球碰撞到边缘，反弹之前：

![](/zh-CN/docs/Games/Tutorials/2D_Breakout_game_pure_JavaScript/Bounce_off_the_walls/ball-in-wall.png)

这是因为我们正在计算墙和球的中心碰撞点，而我们应该围绕它的周长来做。如果碰到墙壁，球应该会弹起来，而不是陷入墙壁一半时，所以让我们来调整一下我们的判断条件。更新你之前添加的代码：

```js
if (x + dx > canvas.width - ballRadius || x + dx < ballRadius) {
  dx = -dx;
}
if (y + dy > canvas.height - ballRadius || y + dy < ballRadius) {
  dy = -dy;
}
```

当球的中心到墙的边缘之间的距离与球的半径完全相同时，它将改变运动的方向。

比较你的代码
------

让我们再次检查这个部分的代码与你之间有何差异：

**备注：** 尝试修改你的代码，在每次碰到墙壁时都要把球的颜色改成随机的颜色。

下一步
---

现在我们已经到了我们的球正在移动和留在游戏板上的阶段。在第四章中，我们将看看如何实现一个可控制的 paddle - 参见paddle 和键盘控制 (en-US)。

* « 上一页 (en-US)
* 下一页 » (en-US)


鼠标控制 - 游戏开发

鼠标控制
====

* « 上一页 (en-US)
* 下一页 » (en-US)

本篇为 Gamedev Canvas tutorial (en-US) 10 节教程中的**第 9 节**。在你完成这篇课程之后，你可以在 Gamedev-Canvas-workshop/lesson9.html 找到我们的源代码。

这个游戏实际已经完成，现在让我们着手去润色。我们已经添加过键盘控制，而加入鼠标控制也同样简单。

监听鼠标移动
------

监听鼠标移动甚至比监听按键更简单：只需监听 `mousemove` 这个事件即可。把下面这行代码和其他事件监听代码放在一起，在 `keyup event` 的下一行：

```js
document.addEventListener("mousemove", mouseMoveHandler, false);
```

将球板移动绑定到鼠标移动
------------

我们可以根据鼠标光标位置来更新球板位置——下面这个函数正是做这件事的。把这个函数加到你的代码中，接在你刚刚加入的那行后面：

```js
function mouseMoveHandler(e) {
  var relativeX = e.clientX - canvas.offsetLeft;
  if (relativeX > 0 && relativeX < canvas.width) {
    paddleX = relativeX - paddleWidth / 2;
  }
}
```

在这个函数中，我们首先计算 `relativeX` 的值，它等于鼠标在视窗中的水平位置（`e.clientX`）减去 canvas 元素左边框到视窗左边框的距离（`canvas.offsetLeft`）——这就得到了 canvas 元素左边框到鼠标的距离。若这个值大于零，且小于 canvas 的宽度，说明鼠标指针落在 canvas 边界内，这时就把 `paddleX` （等于球板左边缘的坐标）设为 `relativeX` 减速去球板宽度的一半。这样就确保位移是相对于球板中心进行的。

现在球板将跟随鼠标指针。不过由于我们将球板移动限制在 canvas 大小范围内，它不会从两边完全消失。

比较你的代码
------

以下是我们的示例代码，以便与你进行比较：

练习：调整球板移动的范围，使得整个球板总是可见，而不是在移动到边缘时被遮住一半。

下一步
---

现在我们已经拥有一个完整的游戏。我们的系列教程将以一些细节上的调整作为结束。 (en-US)

* « 上一页 (en-US)
* 下一页 » (en-US)


球板及键盘控制 - 游戏开发

球板及键盘控制
=======

* « 上一页 (en-US)
* 下一页 » (en-US)

这是Gamedev Canvas tutorial (en-US)中的第四章。完成本课程后，你可以在Gamedev-Canvas-workshop/lesson4.html找到源码。

你可以看到球自由的、无限次的在墙壁上反弹，但是没有和我们发生任何交互。如果我们没有对它的控制操作，这仍然不是一个游戏。下面，我们新增一些用户操作：一个可以控制球的球板。

定义一个球板去接球
---------

我们需要添加一个球板去接球：为此需要先定义一些变量。在你的代码的顶部的其他变量下方添加下列代码：

```js
var paddleHeight = 10;
var paddleWidth = 75;
var paddleX = (canvas.width - paddleWidth) / 2;
```

然后定义球拍的长和宽，以及为了之后的处理同时定义 x 轴上的初始位置。新建一个方法来在页面上描绘球板。把下列代码添加到你的 `drawBall()` 方法里去：

```js
function drawPaddle() {
  ctx.beginPath();
  ctx.rect(paddleX, canvas.height - paddleHeight, paddleWidth, paddleHeight);
  ctx.fillStyle = "#0095DD";
  ctx.fill();
  ctx.closePath();
}
```

允许用户控制球板
--------

我们可以如愿的描绘出球板，也需让它听从用户的控制。是时候实现用键盘控制它了。我们需要：

* 两个变量以保存左右方向键是否被按下的信息。
* 两个事件监控器来捕捉按键的按下和松开动作。我们需要运行一些代码以在按键被按下时可以控制球拍的移动
* 两个用于处理按键被按下或松开后的事件处理方法
* 实现左右移动球拍

按键可以使用 boolean 变量来初始定义。在你的其他变量附近添加下列代码：

```js
var rightPressed = false;
var leftPressed = false;
```

这两个变量的默认值都是 false，因为在开始时按键没有被按下。为了监听按键的按下动作，我们需要添加两个监听器。把下列代码添加到底部的 `setInterval()` 的上一列去：

```js
document.addEventListener("keydown", keyDownHandler, false);
document.addEventListener("keyup", keyUpHandler, false);
```

当你按下任何键盘上的按键，按下事件被激活时 `keyDownHandler()` 方法会被调用。对于松开时的处理也是类似的：当松开按键时 keyUpHandler() 方法会被调用。把下列代码添加到 addEventListener() 下方：

```js
function keyDownHandler(e) {
  if (e.keyCode == 39) {
    rightPressed = true;
  } else if (e.keyCode == 37) {
    leftPressed = true;
  }
}

function keyUpHandler(e) {
  if (e.keyCode == 39) {
    rightPressed = false;
  } else if (e.keyCode == 37) {
    leftPressed = false;
  }
}
```

当按下一个按键，这个信息会被储存在一个变量中。每种情况下的相关变量都设置为`true`。当松开按键时，对应变量被设置回`false`。

两个函数都以一个事件作为参数，由`e`(event) 变量表示。从这里你可以得到有用的信息：keyCode 属性是被按下的键的信息。例如，keyCode 为 37 是左箭头键，而 39 是右箭头键。如果按下左键，那么 leftPressed 变量设置为 true，当松开时，leftPressed 变量设置为 false。右键同理。

### 球拍移动逻辑

我们现在有用于存储按键，事件监听器和相关功能的信息的变量。现在我们将看到实际的代码来使用这些变量，并在屏幕上移动球拍。在 draw（）函数内部，我们将检查每一帧被渲染的同时是否按下左或右键。我们的代码如下：

```js
if (rightPressed) {
  paddleX += 7;
} else if (leftPressed) {
  paddleX -= 7;
}
```

如果按一下左键，球拍将向左移动 7 个像素，如果按一下右键，球拍将向右移动 7 个像素。目前这个功能可以正常工作，但是如果我们按任意一个键的时间太长，球拍就会从画布的边缘消失。我们可以通过改变代码来改善这种情况，并且只能在画布的边界内移动球拍，如下所示：

```js
if (rightPressed && paddleX < canvas.width - paddleWidth) {
  paddleX += 7;
} else if (leftPressed && paddleX > 0) {
  paddleX -= 7;
}
```

我们使用在`Canvas`左侧的 0 和右侧的`canvas.width-paddleWidth`之间的`paddleX`位置移动，这会让球拍按预期的要求移动。

将上面的代码块添加到底部的`draw()`函数中，在右大括号的上方。

现在唯一要做的就是在`draw()`函数内调用`drawPaddle()`函数，将其实际渲染在屏幕上。在`draw()`函数内添加下面一行，就在调用`drawBall()`的那一行的下面：

```js
drawPaddle();
```

比较你的代码
------

以下是我们的示例代码，以便与你进行比较：

**备注：** 让球拍变快变慢，或者改变它的大小。

下一步
---

现在我们有一些类似于游戏的东西。唯一的麻烦就是无论如何你都可以继续用球拍击球。这一切都将在第五章中改变，游戏结束 (en-US)时，我们会为游戏添加一个最后的状态。

* « 上一页 (en-US)
* 下一页 » (en-US)


游戏结束 - 游戏开发

游戏结束
====

* « 上一页 (en-US)
* 下一页 » (en-US)

这是 Gamedev Canvas tutorial (en-US)教程的第五章。你可以在完成本课程后在这里Gamedev-Canvas-workshop/lesson5.html找到源代码。

看球从墙上反弹，并能够移动球盘是很有趣的。但除此之外，游戏什么都不做，也没有任何进展或最终目标。从游戏的角度来看，我们需要一个 game over。失败的逻辑很简单。如果你的球拍错过了球，并且球到达屏幕的底部边缘，那么游戏就结束了。

实现游戏失败
------

让我们在代码中实现，下面是第三章里的一段代码，让球从墙上反弹：

```js
if (x + dx > canvas.width - ballRadius || x + dx < ballRadius) {
  dx = -dx;
}

if (y + dy > canvas.height - ballRadius || y + dy < ballRadius) {
  dy = -dy;
}
```

我们不需要让球从四面墙上反弹，应该只允许三个 - 左，上，右。击中底部墙将结束游戏。我们将编辑第二个 if 代码块，这是一个 if else 块，当球碰撞到画布的底部边缘时，它会触发我们的“游戏结束”状态。现在我们将保持简单，显示一条警告消息，并通过重新加载页面重新开始游戏。

第一步，把你最开始使用的 `setInterval()` 函数

```js
setInterval(draw, 10);
```

替换成：

```js
var interval = setInterval(draw, 10);
```

然后将第二个 if 块替换为以下内容：

```js
if (y + dy < ballRadius) {
  dy = -dy;
} else if (y + dy > canvas.height - ballRadius) {
  alert("GAME OVER");
  document.location.reload();
}
```

让球拍接住球
------

本课中最后要做的是在球和球拍之间创建一些碰撞检测，以便它可以反弹并返回到游戏区域。最简单的方法是检查球的中心是否在球拍的左边和右边之间。再次更新你修改的代码的最后一位（第二个 if 块），如下所示：

```js
if (y + dy < ballRadius) {
  dy = -dy;
} else if (y + dy > canvas.height - ballRadius) {
  if (x > paddleX && x < paddleX + paddleWidth) {
    dy = -dy;
  } else {
    alert("GAME OVER");
    document.location.reload();
  }
}
```

如果球击中画布的底部边缘，我们需要检查它是否碰到球拍。如果是的话，就像你所期望的那样反弹。如果没有，那么游戏就像以前一样结束。

代码对比
----

这里是完整例子，对比一下代码吧！

**备注：** 当球碰到球拍时，让球移动得更快

下一步
---

到目前为止，我们的表现相当不错，游戏变得更有趣，并且现在你可以输了！但它仍然缺少一些东西。让我们继续前进到第六章 - 建造砖块 (en-US) - 并创造一些砖块来消灭它们。

* « 上一页 (en-US)
* 下一页 » (en-US)


让球动起来 - 游戏开发

让球动起来
=====

* « 上一页 (en-US)
* 下一页 » (en-US)

本篇是 Gamedev Canvas tutorial (en-US) 10 节教程中的第二节。如果你完成了本篇教程之后，你可以从 Gamedev-Canvas-workshop/lesson2.html 看到源码。

从上一节中你已经知道如何去绘制一个球。现在让我们使它动起来。从技术上讲，我们将在画布上绘制一个球，之后让它消失，然后在一个稍微不用的位置上再绘制一个一样的球。就想电影里的每一帧动起来的感觉。

我们需要定义一个绘图函数，每次使用一组不同的变量改变球体的位置；循环调用以保持画布上每一帧不断更新。你可以使用 JavaScript 时间函数 `setInterval()` (en-US) 或者 `window.requestAnimationFrame()`。

在你的 HTML 文件只保留前两行，删除其他所有的 JavaScript 代码并在 draw() 函数中添加以下内容保证每 10 毫秒执行一次 draw() 函数：

```js
function draw() {
  // drawing code
}
setInterval(draw, 10);
```

得益于 `setInterval` 的无限性，使得 `draw()` 函数将每 10 毫秒就会被调用，除非我们停止它。现在，我们来绘制小球吧，在 `draw()` 函数中添加以下内容：

```js
ctx.beginPath();
ctx.arc(50, 50, 10, 0, Math.PI \* 2);
ctx.fillStyle = "#0095DD";
ctx.fill();
ctx.closePath();
```

现在，尝试更新你的代码，球会在每一帧画面被绘制

让球动起来
-----

你不会注意到球正在不停地被重新刷，因为它没有移动。让我们改变这种情况。首先，我们不再使用固定位置 (50,50)，而是用 x 和 y 的变量来定义画布底部的起始点，然后使用这些变量来定义圆被绘制的位置。

首先，在`draw()`函数上方添加以下两行，以定义`x`和`y`：

```js
var x = canvas.width / 2;
var y = canvas.height - 30;
```

接下来更新 `draw()` 函数，在 `arc()` 方法中使用 `x` 和 `y` 变量，如下面高亮行所示：

```js
function draw() {
  ctx.beginPath();
  ctx.arc(x, y, 10, 0, Math.PI \* 2);
  ctx.fillStyle = "#0095DD";
  ctx.fill();
  ctx.closePath();
}
```

现在到了最重要的部分：我们想要在每一帧都被绘制出来之后，给 `x` 和 `y` 添加一个较小的值，让它看起来像是在移动。让我们将这些值定义为 `dx` 和 `dy`，并将它们的值分别设为 `2` 和 `-2`。在你的 x 和 y 变量声明下方添加以下内容：

```js
var dx = 2;
var dy = -2;
```

最后要做的是在每一帧上更新 `x` 和 `y`，在每一次更新中，把球画在新的位置上。将下面的两条新线添加到你的 `draw()` 函数：

```js
function draw() {
  ctx.beginPath();
  ctx.arc(x, y, 10, 0, Math.PI \* 2);
  ctx.fillStyle = "#0095DD";
  ctx.fill();
  ctx.closePath();
  x += dx;
  y += dy;
}
```

再次保存代码，并在浏览器中尝试。很好，尽管看起来球在后面留下了痕迹：

![](/zh-CN/docs/Games/Tutorials/2D_Breakout_game_pure_JavaScript/Move_the_ball/ball-trail.png)

在每一帧更新之前清空画布
------------

球移动时留下了轨迹，因为我们在每一帧上都画了一个新的圆，而没有去掉之前的一个圆。不要担心，因为有一个方法来清空画布的内容：`clearRect()`。该方法有四个参数：矩形左上角的 `x` 和 `y` 坐标，以及矩形的右下角的 `x` 和 `y` 坐标。这个矩形覆盖的整个区域里，之前所画的任何内容将被清除。

将下列高亮显示行添加到 `draw()` 函数：

```js
function draw() {
  ctx.clearRect(0, 0, canvas.width, canvas.height);
  ctx.beginPath();
  ctx.arc(x, y, 10, 0, Math.PI \* 2);
  ctx.fillStyle = "#0095DD";
  ctx.fill();
  ctx.closePath();
  x += dx;
  y += dy;
}
```

保存你的代码并再次尝试，这次你将看到球移动后没有留下轨迹。每隔 10 毫秒，画布就会被清除，蓝色的圆圈 (我们的球) 将被绘制在一个给定的位置上，而 `x` 和 `y` 的值将在下一个帧被更新。

保持代码整洁
------

在接下来的几篇文章中，我们将在 `draw()` 函数中添加越来越多的命令，因此尽可能保持简单和整洁是很好的。让我们从把绘制球的代码移至一个单独的函数。

用以下两个函数替换现有的 `draw()` 函数：

```js
function drawBall() {
  ctx.beginPath();
  ctx.arc(x, y, 10, 0, Math.PI \* 2);
  ctx.fillStyle = "#0095DD";
  ctx.fill();
  ctx.closePath();
}

function draw() {
  ctx.clearRect(0, 0, canvas.width, canvas.height);
  drawBall();
  x += dx;
  y += dy;
}
```

比较你的代码
------

你可以在下面的实时演示中查看本文的代码，并使用它来更好地了解其工作原理：

练习：尝试改变移动球的速度，或者移动球的方向。

下一步
---

我们已经画了我们的球，并将其移动，但它仍然消失在画布的边缘。在第三章中，我们将探讨如何使其 从墙壁上反弹 (en-US).

* « 上一页 (en-US)
* 下一页 » (en-US)


跟踪得分和获胜 - 游戏开发

跟踪得分和获胜
=======

* « 上一页 (en-US)
* 下一页 » (en-US)

本篇为Gamedev Canvas tutorial (en-US)10 节教程中的\*\*第 8 节。\*\*在你完成这篇课程之后，你可以在Gamedev-Canvas-workshop/lesson8.html找到我们的源代码。

破坏砖块真的很酷，但更酷的是，游戏可以给每个用户击破的砖块奖励分数，并保持总分。

计算分数
----

如果你能在整个游戏中看到你的分数，最终你会给你的朋友留下深刻印象。你需要一个变量来记录分数。在变量的其余部分之后，将下面的内容添加到 JavaScript 中：

```js
var score = 0;
```

你还需要一个 `drawScore()` 函数来创建和更新分数显示。在 `collisionDetection()` 函数之后添加以下内容：

```js
function drawScore() {
  ctx.font = "16px Arial";
  ctx.fillStyle = "#0095DD";
  ctx.fillText("Score: " + score, 8, 20);
}
```

在画布上绘制文本类似于绘制形状。字体定义看起来与 CSS 中的字体定义完全一样——可以在`font()` 方法中设置大小和字体类型。然后使用`fillStyle()` 来设置字体的颜色，`fillText()` 来设置将放置在画布上的实际文本，和其放置位置。第一个参数是文本本身——上面的代码显示当前点的数量——最后两个参数是文本将放置在画布上的坐标。

若要在每次击中砖块时评分，则在 `collisionDetection()`中添加计分规则，以在每次检测到碰撞时增加得分变量的值。将下面突出显示的行添加到代码中：

```js
function collisionDetection() {
  for (var c = 0; c < brickColumnCount; c++) {
    for (var r = 0; r < brickRowCount; r++) {
      var b = bricks[c][r];
      if (b.status == 1) {
        if (
          x > b.x &&
          x < b.x + brickWidth &&
          y > b.y &&
          y < b.y + brickHeight
        ) {
          dy = -dy;
          b.status = 0;
          score++;
        }
      }
    }
  }
}
```

从 `draw()` 函数调用 `drawScore()` ，使每一个新帧的分数都保持最新，在 `draw()`中添加下面的行，在 `drawPaddle()` 下面调用：

```js
drawScore();
```

当所有砖块被破坏时显示获胜消息
---------------

收集这些点很有效，但是你不会永远添加它们 - 当所有的砖头都被破坏的时候呢？毕竟这是游戏的主要目的，所以如果收集到所有可用的点，你应该显示一个获胜的消息。将下面突出显示的部分添加到 `collisionDetection()` 函数中：

```js
function collisionDetection() {
  for (var c = 0; c < brickColumnCount; c++) {
    for (var r = 0; r < brickRowCount; r++) {
      var b = bricks[c][r];
      if (b.status == 1) {
        if (
          x > b.x &&
          x < b.x + brickWidth &&
          y > b.y &&
          y < b.y + brickHeight
        ) {
          dy = -dy;
          b.status = 0;
          score++;
          if (score == brickRowCount \* brickColumnCount) {
            alert("YOU WIN, CONGRATULATIONS!");
            document.location.reload();
          }
        }
      }
    }
  }
}
```

谢谢你做的这些，你的用户可以真正赢得游戏时，他们也摧毁了所有的砖块。当用户来到游戏还有一点非常重要，用户一旦点击了警告按钮， `document.location.reload()`函数将重新加载页面并重新启动游戏。

比较你的代码
------

最新的代码是这样（和工程），如果你想比较和对比它与你写的：

**备注：** 在每一个砖头击破后添加更多的分数，打印出收集到的点数在游戏结束警告框中。

下一节
---

游戏到这一步看起来相当不错。在下一课中，你将通过添加鼠标控件来扩大游戏的吸引力：Mouse controls (en-US).。

* « 上一页 (en-US)
* 下一页 » (en-US)


撞击处理 - 游戏开发

撞击处理
====

* « 上一页 (en-US)
* 下一页 » (en-US)

本篇为Gamedev Canvas tutorial (en-US)10 节教程中的\*\*第 7 节。\*\*在你完成这篇课程之后，你可以在Gamedev-Canvas-workshop/lesson7.html.找到我们的源代码。

我们已经在屏幕上画出了砖块，但游戏仍然没有那么有趣，因为球通过它们。我们需要考虑增加碰撞检测，这样球就可以弹击砖块并打破它们。

当然，这是我们的决定如何实现的，但是计算球是否触及矩形是很困难的，因为在画布中没有辅助函数。为了这个教程，我们将尽可能地做到这一点。我们将检查球的中心是否与任何给定的砖块碰撞。这不会每次都给出一个完美的结果，而且有很多更复杂的方法来进行碰撞检测，但是这对指导你的基本概念很有效。

撞击侦测函数
------

踢掉这一切，我们想创建一个碰撞检测功能，将循环通过所有砖块，并比较每一个砖的位置与球的坐标，因为每个帧绘制。为了更好地理解代码，我们将定义用于在碰撞检测的每个循环中存储砖块对象的 B 变量：

```js
function collisionDetection() {
  for (c = 0; c < brickColumnCount; c++) {
    for (r = 0; r < brickRowCount; r++) {
      var b = bricks[c][r];
      // calculations
    }
  }
}
```

如果球的中心在我们的一块砖块的坐标内，我们将改变球的方向。对于球的中心在砖块内，以下四个陈述都必须是正确的：

* 球的 X 位置大于砖的 X 位置。
* 球的 X 位置小于砖的 X 位置加上它的宽度。
* 球的 Y 位置大于砖的 Y 位置。
* 球的 Y 位置小于砖块的 Y 位置加上它的高度。

让我们在代码中写下：

```js
function collisionDetection() {
  for (c = 0; c < brickColumnCount; c++) {
    for (r = 0; r < brickRowCount; r++) {
      var b = bricks[c][r];
      if (x > b.x && x < b.x + brickWidth && y > b.y && y < b.y + brickHeight) {
        dy = -dy;
      }
    }
  }
}
```

将上面的块添加到代码中，在 `keyUpHandler()` 函数下面。

让砖块在被撞击之后消失
-----------

上述代码将按需要工作，球改变其方向。问题是砖块留在原地。我们必须想出一个办法来摆脱那些我们已经用球打中的砖。我们可以通过添加一个额外的参数来指示我们是否想在屏幕上画每个砖块。在初始化砖块的代码的一部分中，让我们为每个砖块对象添加一个状态属性。更新代码的下面部分，如突出显示的行所示：

```js
var bricks = [];
for (c = 0; c < brickColumnCount; c++) {
  bricks[c] = [];
  for (r = 0; r < brickRowCount; r++) {
    bricks[c][r] = { x: 0, y: 0, status: 1 };
  }
}
```

接下来，我们将在绘制之前在 `drawBricks()` 中检查每个砖块的 `status`属性的值 - 如果 `status`是 `1`，然后画它，但是如果它是 `0`，那么它被球击中，我们不再希望它在屏幕上。更新你的 `drawBricks()` 函数如下：

```js
function drawBricks() {
  for (c = 0; c < brickColumnCount; c++) {
    for (r = 0; r < brickRowCount; r++) {
      if (bricks[c][r].status == 1) {
        var brickX = c \* (brickWidth + brickPadding) + brickOffsetLeft;
        var brickY = r \* (brickHeight + brickPadding) + brickOffsetTop;
        bricks[c][r].x = brickX;
        bricks[c][r].y = brickY;
        ctx.beginPath();
        ctx.rect(brickX, brickY, brickWidth, brickHeight);
        ctx.fillStyle = "#0095DD";
        ctx.fill();
        ctx.closePath();
      }
    }
  }
}
```

跟踪并更新在撞击侦测函数中的状态
----------------

现在我们需要将砖块 `status` 属性包含在 `collisionDetection()`函数中：如果砖块是活动的（它的状态是 `1`），我们将检查碰撞是否发生；如果发生碰撞，我们将给定砖块的状态设置为 `0`，这样它就不会被绘制在屏幕上。更新你的 `collisionDetection()` 函数，如下所示：

```js
function collisionDetection() {
  for (c = 0; c < brickColumnCount; c++) {
    for (r = 0; r < brickRowCount; r++) {
      var b = bricks[c][r];
      if (b.status == 1) {
        if (
          x > b.x &&
          x < b.x + brickWidth &&
          y > b.y &&
          y < b.y + brickHeight
        ) {
          dy = -dy;
          b.status = 0;
        }
      }
    }
  }
}
```

调用我们的撞击侦测函数
-----------

最后要做的是向 `collisionDetection()` 函数添加一个调用到我们的主要`draw()` 函数。将下面的行添加到 `draw()` 函数中，就在`drawPaddle()` 调用的下面：

```js
collisionDetection();
```

对比你的代码
------

现在，在每一帧，每一块砖上都检查了球的碰撞检测。现在我们可以毁掉砖头：-！

**备注：** 练习：当球碰到砖头时，改变球的颜色。

下一节
---

我们现在肯定到了，继续前进吧！在第八章中，我们将探讨如何跟踪得分和获胜。Track the score and win (en-US).

* « 上一页 (en-US)
* 下一页 » (en-US)


创建、绘制画布 - 游戏开发

创建、绘制画布
=======

* « 上一页 (en-US)
* 下一页 » (en-US)

本篇是 Gamedev Canvas tutorial (en-US) 10 节教程中的第一节。如果你完成了本篇教程之后，你可以从 Gamedev-Canvas-workshop/lesson1.html 看到源码。

在我们开始编写游戏功能之前，我们可以通过 HTML 的 canvas 标签创建支撑游戏的基本结构。

页面部分
----

HTML 文档的结构是非常简单的，我们的游戏将完全呈现在这个 HTML 的 canvas 标签中。你可以选择一款你最喜欢的文本编辑器，创建一个 HTML 文件，保存到你理想的位置，名称为 index.html。并添加以下代码：

```html
<!doctype html>
<html>
  <head>
    <meta charset="utf-8" />
    <title>Gamedev Canvas Workshop</title>
    <style>
 \* {
 padding: 0;
 margin: 0;
 }
 canvas {
 background: #eee;
 display: block;
 margin: 0 auto;
 }
 </style>
  </head>
  <body>
    <canvas id="myCanvas" width="480" height="320"></canvas>

    <script>
 // JavaScript code goes here
 </script>
  </body>
</html>
```

在 head 标签中我们需要定义字符集（charset），标题（title）和一些基本的样式。在 body 标签中包含 canvas 标签和 javascript 标签，我们将在 javascript 标签中使用 JavaScript 代码来控制 canvas 标签中的内容展现。在 canvas 标签有一个名为 myCanvas 的 Id，根据这个属性我们可以很容易的获取到这个元素并设置他的宽为 480 像素，高为 320 像素。之后我们会将所有的 JavaScript 代码全部写到 javascript 标签中（<script>...</script>）。

画布的基本属性
-------

要想在 canvas 标签中呈现图像内容，我们必须在 JavaScript 代码中获取到这个元素。在 javascript 标签中添加以下代码；

```js
var canvas = document.getElementById("myCanvas");
var ctx = canvas.getContext("2d");
```

在这里我们可以声明一个变量保存 canvas 标签，然后我们需要创建变量 ctx, 这个变量可用来存储 2d 的渲染上下文，我们实际上就用它在 canvas 标签上绘制内容。

让我们来看一个例子，打印在画布上的红色正方形。添加以下代码到你的 JavaScript 里，然后在浏览器中打开 index.html 来看看效果。

```js
ctx.beginPath();
ctx.rect(20, 40, 50, 50);
ctx.fillStyle = "#FF0000";
ctx.fill();
ctx.closePath();
```

把全部指令放到 ctx.beginPath() 方法 和 ctx.closePath() 方法之间。我们使用 ctx.rect() 方法创建了一个矩形。这个方法的第一组参数（20，40）是确定这个矩形的左上角在画布上的坐标，第二组参数（50, 50）是指定矩形的宽度和高度。例子中矩形左上角的 x 坐标为 20 像素，y 坐标为 40 像素；宽和高各为 50 像素，这样画出一个完美的正方形。ctx.fillStyle 属性是用于填充绘画的颜色，这里填充的颜色为红色。ctx.fill() 方法是填充路径的内容区域生成实心的图形。

我们不局限于绘制矩形 — 下面这段代码会绘制出一个绿色的圆；尝试添加到 JavaScript 代码的底部，保存并刷新：

```js
ctx.beginPath();
ctx.arc(240, 160, 20, 0, Math.PI \* 2, false);
ctx.fillStyle = "green";
ctx.fill();
ctx.closePath();
```

正如你所见，我们再一次使用了 ctx.beginPath() 方法 和 ctx.closePath() 方法；在这两个方法中间，最重要的部分是 ctx.arc() 方法。它需要六个参数：

* 圆心的 `x` 和 `y` 轴坐标
* 半径
* 起始角度，结束角度。以弧度计（弧的圆形的三点钟位置是 0 度）
* 规定应该逆时针还是顺时针绘图。false = 顺时针，true = 逆时针。（可选）

ctx.fillStyle 属性的值看起来和之前不大一样。这是因为它与 CSS 一样，颜色可以指定为十六进制值也可以指定为 rgba() 函数，或者任何其他可用的颜色值函数。

当然我们也可以不使用 ctx.fill() 函数来填充图形和颜色，用 ctx.stroke() 函数来绘制定义图形的路径。试着将这段代码添加到你的 JavaScript:

```js
ctx.beginPath();
ctx.rect(160, 10, 100, 40);
ctx.strokeStyle = "rgba(0, 0, 255, 0.5)";
ctx.stroke();
ctx.closePath();
```

上面的代码绘制出一个蓝色边框的空心矩形。由于 rgba() 函数的特性，边框为蓝色半透明的状态。

比对你的代码
------

以上是第一节的全部代码，可以运行在 JSFiddle（在线 JS 代码调试工具）上：

**备注：** 练习：尝试改变给定几何图形的大小和颜色。

下一节
---

现在我们已经创建了基本的 HTML 和关于画布的基本知识。我们继续第二节，如何让球在游戏中动起来 — 让球动起来 (en-US)。

* « 上一页 (en-US)
* 下一页 » (en-US)


收尾工作 - 游戏开发

收尾工作
====

* « 上一页 (en-US)

本篇为 Gamedev Canvas tutorial (en-US) 10 节教程中的\*\*第 10 节也是最后一节。\*\*完成这篇课程后，你可以在 Gamedev-Canvas-workshop/lesson10.html 找到我们的源代码。

不管我们做什么游戏，它总是存在优化的空间。例如，我们可以为玩家多提供几条命，让他们能在发生一两次失误的情况下顺利完成游戏。或者，我们也可以在渲染代码上下工夫。

加入生命机制
------

在游戏中实现生命机制的思路很直接。让我们先新增一个变量，用来存储其生命值。把下面这行代码和我们声明其他变量的代码放在一起：

```js
var lives = 3;
```

在 canvas 上绘制生命值计数的做法几乎和绘制分数一样——把下面的函数添加到`drawScore()` 函数后面：

```js
function drawLives() {
  ctx.font = "16px Arial";
  ctx.fillStyle = "#0095DD";
  ctx.fillText("Lives: " + lives, canvas.width - 65, 20);
}
```

当玩家失误时，我们不立即结束游戏，而是减少生命计数，直到为零。在玩家用掉一条命后，我们也可以重置小球和球板位置。那么，在函数 `draw()` 中将下面三行：

```js
alert("GAME OVER");
document.location.reload();
clearInterval(interval); // Needed for Chrome to end game
```

替换为下面的代码，注意到我们加入了一点点逻辑控制：

```js
lives--;
if (!lives) {
  alert("GAME OVER");
  document.location.reload();
  clearInterval(interval); // Needed for Chrome to end game
} else {
  x = canvas.width / 2;
  y = canvas.height - 30;
  dx = 2;
  dy = -2;
  paddleX = (canvas.width - paddleWidth) / 2;
}
```

现在，当小球碰到屏幕底边时，我们让变量`lives` 的值减一。如果生命用尽，游戏就宣告结束；否则就重置小球与球板的位置，以及小球的速度。

### 渲染生命值

现在只需在 `draw()` 函数内调用`drawLives()` 即可。让我们把它加到`drawScore()` 的下一行：

```js
drawLives();
```

用 requestAnimationFrame() 优化渲染
------------------------------

现在让我们处理一些与游戏机制无关，但与画面渲染相关的东西。和我们目前使用`setInterval()` (en-US) 实现的固定帧率渲染相比，`requestAnimationFrame` 能让浏览器更好地渲染画面。让我们把下面这行代码：

```js
var interval = setInterval(draw, 10);
```

替换为：

```js
draw();
```

再把代码中的每一处

```js
clearInterval(interval); // Needed for Chrome to end game
```

删除。然后，在 `draw()` 函数的最下方（右花括号之前）加入下面这行代码。它的作用是使 `draw()` 函数递归调用自身：

```js
requestAnimationFrame(draw);
```

现在 `draw()` 函数在 `requestAnimationFrame()` 的循环中被反复调用，之先前做法最大的不同是，我们将帧率的控制权交给浏览器，而不是固定的 10 毫秒。浏览器会在适当的时机同步帧率，并且只在必要的时候才刷新渲染的图形。这使得我们的动画比之前的 `setInterval()` 方法更加流畅且高效。

比较你的代码
------

我们的游戏的最终版本已经完成！以上。

**备注：** 试着改变生命的数目和球从球板上反弹的角度。

游戏结束——暂时看来！
-----------

祝贺你——你完成了本教程的所有小节！现在，你应该已经掌握 canvas 操纵的基础和 2D 游戏背后的逻辑了。是时候去学习一些框架，继续你的游戏开发之旅了！你可以看看本系列的姊妹篇：用 Phaser 制作 2D 打砖块游戏 (en-US) 或者 Cyber Orb built in Phaser (en-US) 。或者，你也可以在 MDN 游戏区 中获得灵感和更多知识。

你也可以回到本教程的目录页 (en-US)。祝编程愉快！

* « 上一页 (en-US)


Build the brick field - 游戏开发

Build the brick field
=====================

* « 上一页 (en-US)
* 下一页 » (en-US)

这是 Gamedev Canvas tutorial (en-US)教程 10 节的第 6 节。你可以在完成本课程后在这里Gamedev-Canvas-workshop/lesson6.html找到源代码。

在修改游戏机制后，我们可以输了 — 这样这游戏看起来终于像是一个游戏了，这真是太好了。但是，如果你总是让球与墙、板碰撞的话，很快就会感到无聊的。好游戏需要的是让球消灭砖，这就是我们即将要做的！

设置砖变量
-----

本课题的总体目标是使用一个二维数组嵌套的循环，给出砖的几行代码。首先我们需要设置一些变量定义的砖，如宽度和高度信息，行和列，等。在之前的变量声明处加入以下几行代码。

```js
var brickRowCount = 3;
var brickColumnCount = 5;
var brickWidth = 75;
var brickHeight = 20;
var brickPadding = 10;
var brickOffsetTop = 30;
var brickOffsetLeft = 30;
```

在这里，我们定义了砖的行数和列，宽度和高度，砖块之间的填充物，这样它们就不会互相接触；有一个上、左偏移量，所以它们不会从画布的边缘开始绘制。

我们将在一个二维数组容纳我们所有的砖。它将包含砖列（c），砖行（R），每一个包含一个对象，其中包含 x 和 y 位置，让每个砖显示在屏幕上。在变量下面添加以下代码：

```js
var bricks = [];
for (c = 0; c < brickColumnCount; c++) {
  bricks[c] = [];
  for (r = 0; r < brickRowCount; r++) {
    bricks[c][r] = { x: 0, y: 0 };
  }
}
```

上面的代码将通过行和列的循环和创造新砖。注意，砖块对象稍后也将用于碰撞检测。

画砖的逻辑
-----

现在让我们创建一个函数来遍历数组中的所有砖块并在屏幕上绘制它们。. 代码如下：

```js
function drawBricks() {
  for (c = 0; c < brickColumnCount; c++) {
    for (r = 0; r < brickRowCount; r++) {
      bricks[c][r].x = 0;
      bricks[c][r].y = 0;
      ctx.beginPath();
      ctx.rect(0, 0, brickWidth, brickHeight);
      ctx.fillStyle = "#0095DD";
      ctx.fill();
      ctx.closePath();
    }
  }
}
```

再次，我们遍历的行和列，给每一块砖的位置设置`X`和`Y`，我们也画布上画砖，---`brickwidth` X `brickheight` 。问题是我们都画在一个地方坐标`（0,0）`处。我们需要做的是增加一些计算，计算每个循环迭代后的砖块的 x 和 y 位置：

```js
var brickX = c \* (brickWidth + brickPadding) + brickOffsetLeft;
var brickY = r \* (brickHeight + brickPadding) + brickOffsetTop;
```

每个`brickX`位置是 `brickWidth + brickPadding`，乘以列数`C`，再加上`brickOffsetLeft`；对于砖`brickY`的逻辑相同，除了名称不同，使用行数`R`，`brickHeight`，和`brickOffsetTop`。现在，每一块砖都可以放在正确的地方，排成一排，每一块砖之间都有填充物，从左上角和顶部的帆布边缘偏移。

在设置`brickX`和`brickY`作为对应砖的坐标之后，形成了 `drawBricks()`函数的最终版本。将以下代码加在`drawPaddle()`函数后面：

```js
function drawBricks() {
  for (c = 0; c < brickColumnCount; c++) {
    for (r = 0; r < brickRowCount; r++) {
      var brickX = c \* (brickWidth + brickPadding) + brickOffsetLeft;
      var brickY = r \* (brickHeight + brickPadding) + brickOffsetTop;
      bricks[c][r].x = brickX;
      bricks[c][r].y = brickY;
      ctx.beginPath();
      ctx.rect(brickX, brickY, brickWidth, brickHeight);
      ctx.fillStyle = "#0095DD";
      ctx.fill();
      ctx.closePath();
    }
  }
}
```

到了展现真正画砖的时候了
------------

最后一件事就是在`draw()`中调用`drawBricks()`, 位置最好在函数开始处，在清除画布和画球之间。直接将下面代码加在`drawBall()` 处：

```js
drawBricks();
```

比较你的代码
------

这样，游戏变得更有趣了 :

**备注：** 练习：尝试在行或列上改变砖块数量，或者它们的位置。

下一节
---

现在，我们有砖啦！但是球根本就没有和它们互动——接下来的第七章我们将让球和砖产生碰撞：碰撞检测 (en-US)。

* « 上一页 (en-US)
* 下一页 » (en-US)


Web 中的 3D 游戏 - 游戏开发

Web 中的 3D 游戏
============

为了 web 上丰富的游戏体验，一个好武器是 webGL，它可以在 HTML `<canvas>` 元素上渲染。WebGL 基本上是 Web 的 OpenGL ES 2.0 版本——作为一个 JavaScript API，它提供了能构建丰富的交互式动画和游戏的工具。你可以使用硬件加速的 JavaScript 生成和呈现动态 3D 图像。

文档和浏览器支持
--------

WebGL 项目的文档和规范由 Khronos Group 维护，而非大部分 Web API 采用的 W3C。它很好地支持了现代浏览器甚至是移动手机，由此你无需担心太多。主流浏览器均支持 WebGL，你需要关注的仅仅是在你使用的设备上进行优化。

WebGL 2.0（基于 OpenGL ES 3.0）将在不久的将来发布，它将带来许多改进，并将帮助开发人员使用当前强大的硬件为现代 web 构建游戏。

对基本 3D 理论的解释
------------

三维理论的基本原理围绕三维空间中的图形展开，并使用坐标系来计算它们的位置。请参阅我们的解释基本 3D 理论文章，了解你需要的所有信息。

进阶概念
----

使用 WebGL，可以做更多事情。应该深入学习一些高级概念，如着色器、碰撞检测或最新的热门话题：Web 虚拟现实。

### 着色器

值得一提的是着色器，其本身有很多值得探讨的地方。着色器使用一种特殊的 OpenGL 着色语言——GLSL，其语法类似于 C 语言，由图形管道直接执行。着色器可分为顶点着色器和片段着色器（或像素着色器），前者将形状位置转换为真实的 3D 绘图坐标，后者则计算渲染颜色和其他属性。你一定要看看 GLSL 着色器这篇文章，了解更多关于它们的信息。

### 碰撞检测

很难想象没有碰撞检测的游戏会是什么样子——我们总是需要计算出物体什么时候碰撞到另外一个物体。我们有相关信息可供你学习：

* 2D 碰撞检测
* 3D 碰撞检测
### WebXR

虚拟现实的概念并不新鲜，但由于 Meta Quest、WebXR API（目前处于试验阶段）等硬件的进步，它正风靡网络，用于从 XR 硬件捕捉信息并将其提供给 JavaScript 应用程序使用。更多信息，请阅读 WebXR——Web 虚拟和增强现实 (en-US)。

还有一篇用 A-Frame 构建基本演示 (en-US)的文章，向你展示了使用 A-Frame 框架构建 3D 环境的虚拟现实是多么的简单。

库和框架的兴起
-------

编码原生 WebGL 是相当复杂的，从长远来看，随着项目更进一步，你会希望掌握它（请参阅我们的 WebGL 文档以开始学习）。在实际项目中，你可能也会使用框架来加快开发速度，并帮助你管理正在进行的项目。在 3D 游戏中使用框架还有助于优化性能，因为所使用的工具可以解决很多问题，这样就可以专注于构建游戏本身。

最流行的 JavaScript 3D 库是 Three.js，这是一个多用途工具，它使常见的 3D 技术更易于实现。还有其他一些流行的游戏开发库和框架也值得一看。A-Frame、PlayCanvas 和 Babylon.js 是其中最知名的几个，它们拥有丰富的文档、在线编辑器和活跃的社区。

### 使用 A-Frame 搭建一个基础演示

A-Frame 是一个用于搭建 3D 和 VR 体验的 Web 框架。在内部，它是一个具有已声明的实体组件模式的 three.js 框架，也就是说我们只需借助 HTML 即可搭建场景。请参阅使用 A-Frame 构建基本演示 (en-US)子页面来了解创建演示的步骤。

### 使用 Babylon.js 搭建一个基础演示

Babylon.js 是最受开发者欢迎的 3D 游戏引擎之一。与其他任何 3D 库一样，它提供了内置函数，帮助你更快地实现常见的 3D 功能。请参阅 使用 Babylon.js 构建基本演示 (en-US)子页，其中包括建立一个开发环境，构建必要的 HTML，以及编写 JavaScript 代码。

### 使用 PlayCanvas 搭建一个基础演示

PlayCanvas 是一个流行的 GitHub 开源 3D WebGL 游戏引擎，有在线编辑器和良好的文档。更多详细信息 请参阅使用 PlayCanvas 构建基本演示 (en-US) ，文章将进一步介绍如何使用 PlayCanvas 库和在线编辑器搭建演示。

### 使用 Three.js 搭建一个基础演示

与其他库一样，Three.js 也能带来巨大优势：无需编写数百行 WebGL 代码来构建任何有趣的内容，而是可以使用内置的辅助函数来更轻松、更快速地完成工作。请参阅使用 Three.js 构建基本演示子页面，了解创建演示的逐步过程。

### 其他工具

Unity 和虚幻引擎可以将你的游戏通过 asm.js (en-US) 输出到 WebGL，因此你可以自由地使用这些工具与技术来构建可被输出到 Web 上的游戏。

![三个三维几何图形的插图：灰色环形体、蓝色立方体和黄色圆柱体。](/zh-CN/docs/Games/Techniques/3D_on_the_web/shapes.png)

下一步
---

通过这篇文章，我们刚刚了解了当前可用技术所能实现的功能。使用 WebGL 以及在其基础上构建的库和框架，你可以在网络上构建身临其境、美观且快速的 3D 游戏。

### 源代码

你可以在 GitHub 上找到该系列的所有源代码演示。

### API

* Canvas API
* WebGL API
* WebVR API
### 框架

* Three.js
* PlayCanvas
* Babylon.js
* A-Frame
### 教程

* 使用 Three.js 构建基本演示
* 使用 PlayCanvas 构建基本演示 (en-US)
* 使用 Babylon.js 构建基本演示 (en-US)
* 使用 A-Frame 构建基本演示 (en-US)


实现游戏控制机制 - 游戏开发

实现游戏控制机制
========

作为游戏开发平台，HTML5 的主要优势之一是能够在各种平台和设备上运行。简化跨设备差异带来了多重挑战，尤其是在为不同环境提供适当控件时。在本系列文章中，我们将向你展示如何构建一款可以使用触摸屏智能手机、鼠标和键盘以及游戏手柄等不常用机制进行游玩的游戏。

案例分析
----

我们将使用 Captain Rogers: Battle at Andromeda 演示版作为示例。

![Captain Rogers: Battle at Andromeda——包含 Enclave Games 和 Blackmoon Design 徽标、罗杰的太空船和游戏名称的游戏封面。](/zh-CN/docs/Games/Techniques/Control_mechanisms/captainrogers2-cover.png)

Captain Rogers 是使用 Phaser 框架开发的游戏，该框架是目前 JavaScript 中最流行的简单 2D 游戏开发工具，但在使用纯 JavaScript 或其他框架构建游戏时，重复使用这些文章中包含的知识应该相当容易。如果你想了解 Phaser 的入门知识，请查看使用 Phaser 开发 2D 打砖块游戏教程。

在接下来的文章中，我们将展示如何为 Rogers 船长实现各种不同的控制机制，以支持不同的平台——从手机上的触摸、桌面上的键盘/鼠标/游戏板，到更多非常规的控制机制，如电视遥控器、对着笔记本电脑大喊或挥手，或者捏香蕉。

设置环境
----

让我们先快速浏览一下游戏的文件夹结构、JavaScript 文件和游戏中的状态，这样就能知道在哪里发生了什么。游戏的文件夹看起来是这样的：

![Captain Rogers: Battle at Andromeda——游戏项目的文件夹结构，其中包含 JavaScript 源代码、图像和字体。](/zh-CN/docs/Games/Techniques/Control_mechanisms/captainrogers2-folderstructure.png)

正如你所看到的，这里有用于存放图片、JavaScript 文件、字体和音效的文件夹。`src` 文件夹包含 JavaScript 文件，这些文件被分为不同的状态：`Boot.js`、`Preloader.js`、`MainMenu.js` 和 `Game.js`。第一个状态初始化 Phaser，第二个状态预加载所有资源，第三个状态控制欢迎玩家的主菜单，第四个状态控制实际游戏。

每个状态都有自己的默认方法：`preload()`、`create()` 和 `update()`。第一个方法用于预载所需资源，`create()` 在状态启动后执行，而 `update()` 则在每一帧上执行。

例如，你可以在 `create()` 函数中定义一个按钮：

```js
create() {
  // …
  const buttonEnclave = this.add.button(10, 10, 'logo-enclave', this.clickEnclave, this);
  // …
}
```

它将在游戏开始时创建一次，并在点击时执行分配给它的 `this.clickEnclave()` 动作，但你也可以在 `update()` 函数中使用鼠标指针值来执行动作：

```js
update() {
  // …
  if (this.game.input.mousePointer.isDown) {
      // 做些什么
  }
  // …
}
```

当鼠标按钮被按下时，它就会被执行，并且在游戏的每一帧都会根据输入的 `isDown` 布尔变量进行检查。

这应该能让你对项目结构有一些了解。我们将主要使用 `MainMenu.js` 和 `Game.js` 文件，并将在以后的文章中更详细地解释 `create()` 和 `update()` 方法中的代码。

纯 JavaScript 演示
---------------

另外还有一个小型在线演示，其完整源代码可在 GitHub 上获取，其中文章中描述的控制机制的基本支持是用纯 JavaScript 实现的。下面的文章将对其进行解释，但你已经可以用它来玩了，也可以出于学习目的随意使用这些代码。

其他文章
----

JavaScript 是移动游戏的最佳选择，因为 HTML 具有真正的多平台特性；以下所有文章都将重点放在为不同控制机制提供接口的 API 上：

1. 移动触控——第一篇文章将以触控为开篇，因为移动优先的方式非常流行。
2. 桌面鼠标和键盘控制 (en-US)——当在台式机/笔记本电脑上进行游戏时，提供键盘和鼠标控制对于为游戏提供可接受的无障碍程度至关重要。
3. 桌面游戏手柄控制 (en-US)——游戏手柄 API 允许在桌面/笔记本电脑上使用游戏手柄控制网络应用程序，以获得游戏机的感觉。
4. 非常规控制 (en-US)——最后一篇文章展示了一些非常规的控制机制，从实验性的到略显疯狂的，你可能不会相信这些机制可以用来玩游戏。


使用 Gamepad API 实现控制 - 游戏开发

使用 Gamepad API 实现控制
===================

这篇文章着眼于使用 Gamepad API 为网页游戏实现一个有效的跨浏览器控制系统，可以让你使用端游控制器来控制你的网页游戏。Hungry Fridge，就是 Enclave Games 以此制作的游戏。

网页游戏的控制
-------

在历史上，在连接主机 (console) 的电视上玩游戏和在电脑 (PC) 上玩游戏是两种完全不一样的体验，最大的区别就是它们的控制方式。后来，额外的驱动程序和插件让我们能够使用主机控制器来游玩电脑端的游戏--不论是本地游戏，还是运行在浏览器中的游戏。到现在的 HTML5 时代，我们终于有了 Gamepad API ，让我们能够在不安装任何插件的情况下，可以使用控制器来游玩基于浏览器的游戏。Gamepad API 通过提供一个接口公开按钮的按下和坐标的变化来实现这一点，在 JavaScript 中我们可以用这些变化来处理输入。这对于网页游戏来说是非常棒的特性。

API 状态与浏览器支持
------------

Gamepad API 在 W3C 的进程中仍然还是工作草案的状态，这意味着它的实现方法可能还会出现变动，但是就目前来说浏览器的支持性相当不错。Firefox 29+ 和 Chrome 35+ 对其支持得非常好。Opera 在版本 22+ 对 API 进行了支持 (一点也不奇怪，因为他们现在使用 Chrome 的引擎了。) 并且微软最近在 Edge 中对 API 实现了支持，也就是说四大主流浏览器现在都支持 Gamepad API。

哪种控制器最好？
--------

目前最受欢迎的控制器是来自 XBox 360、XBox One、PS3 和 PS4 的 — 它们经受过时间的检验，并且在浏览器跨 Windows 与 Mac OS 平台中对 Gamepad API 的实现中工作良好。

还有一些其他各种各样不同布局的控制器或多或少的支持跨浏览器的实现。本文中讨论的代码使用了一些控制器进行测试，但是笔者比较喜欢的配置是：无线 XBox 360 控制器和 Mac OS X 平台的 Firefox 浏览器。

实例分析：Hungry Fridge
------------------

GitHub Game Off II 比赛举行于 2013 年 11 月，Enclave Games 决定参加比赛。比赛的主题为“改变”(change)，所以他们提交了这样一个游戏——你需要通过点击来喂食饥饿的冰箱健康的食物 (苹果、萝卜、莴苣) 并避开“坏”的食物 (啤酒、汉堡、披萨) 。其中会有倒计时会改变接下来几秒内冰箱想吃的东西，所以你又要小心动作又要块。你可以在这里尝试 Hungry Fridge。

第二个隐藏的“改变”的实现是可以从单纯静态的冰箱改变成涡轮驱动、射击和吞食的机器能力。当你连接控制器后，游戏会有很明显的改变 (饥饿冰箱会变成超级涡轮的饥饿冰箱) 并且你可以使用 Gamepad API 来控制装甲冰箱。你需要击落食物但是你仍然需要找到冰箱目前想吃的食物，否则你会失去能量。

游戏封装了两种截然不同的“变化”（change）——好食物对坏食物，与移动端对桌面端。

示例
--

Game API 的动作展示与 JavaScript 的源代码公布是在完整版的 Hungry Fridge 构建之后才开始的，然后据此创建了一个 简单的示例。部分 Gamepad API Content Kit 在 Github 上供你分析代码并研究其如何工作。

以下讨论的代码来自于完整的 Hungry Fridge 游戏中，除了原代码需要 `turbo` 变量来决定是否启动“超级涡轮模式”以外几乎一模一样。此代码可以独立运行，就算不连接控制器也可以。

**备注：** 一个彩蛋：点击界面右上角的控制器图标有个隐藏选项——不连接控制器也能启动“超级涡轮模式” 。你可以使用键盘上的 A 和 D 控制左右旋转，W 射击，方向键移动。

实现方法
----

使用 Gamepad API 时有两个重要的事件——`gamepadconnected` 和 `gamepaddisconnected`。前者将于浏览器侦测到新控制器连接时触发；而后者则是断开连接 (不管是物理断开还是无响应了) 的时候触发。在示例中 `gamepadAPI` 对象通常存储着所有关于 API 的东西：

```js
var gamepadAPI = {
  controller: {},
  turbo: false,
  connect: function () {},
  disconnect: function () {},
  update: function () {},
  buttonPressed: function () {},
  buttons: [],
  buttonsCache: [],
  buttonsStatus: [],
  axesStatus: [],
};
```

数组 `buttons` 存储着 XBox 360 控制器的按键布局 button layout:

```js
buttons: [
  'DPad-Up','DPad-Down','DPad-Left','DPad-Right',
  'Start','Back','Axis-Left','Axis-Right',
  'LB','RB','Power','A','B','X','Y',
],
```

这可能和例如 PS3 控制器 (或者其他没名字的通用控制器) 等其他控制器有所不同，所以你需要注意并不要假设你期望的布局和你真正使用的控制器布局是一样。接下来我们设置两个事件侦听器来获取数据：

```js
window.addEventListener("gamepadconnected", gamepadAPI.connect);
window.addEventListener("gamepaddisconnected", gamepadAPI.disconnect);
```

由于安全策略，你必须先与控制器产生交互才能触发当前显示页面的事件。如果 API 在没有接收到用户交互的时候工作，那它可能会在不知情的情况下被用来识别指纹。

两个函数都十分简单：

```js
connect: function(evt) {
  gamepadAPI.controller = evt.gamepad;
  gamepadAPI.turbo = true;
  console.log('控制器已连接。');
},
```

函数 `connect()` 接受一个事件作为参数，并将其中的 `gamepad` 对象分配给 `gamepadAPI.controller` 变量。我们在这个游戏中只使用一个控制器，所以这个变量是一个单独的对象而不是控制器的数组。然后我们设置 `turbo` 属性为 `true`。 (这个可以直接用 `gamepad.connected` 实现，但我们想单独设置一个变量来控制“涡轮模式”而不需要连接控制器，原因已在前面说明过了。)

```js
disconnect: function(evt) {
  gamepadAPI.turbo = false;
  delete gamepadAPI.controller;
  console.log('控制器已断开。');
},
```

函数 `disconnect` 设置 `gamepad.turbo` 属性为 `false` 并移除存储着 `gamepad` 对象的变量。

### Gamepad 对象

对象 `gamepad` 中有包含了许多有用的信息，其中就包括按钮和坐标轴的状态等重要信息：

* `id`: 一个包含关于控制器信息的字符串。
* `index`: 一个为已连接的设备分配的唯一标识。
* `connected`: 一个布尔变量，`true` 表示设备已连接。
* `mapping`: 键位的布局类型；现在只有 `standard` 是唯一可用的值。
* `axes`: 每一个坐标轴的状态。表示为存储浮点值的数组。
* `buttons` : 每个按钮的状态，表示为一个 `GamepadButton` 对象，其包含 `pressed` 和 `value` 属性。

变量 `index` 在我们连接了多个控制器时非常有用，我们可以用此来区分它们的操作——例如我们有一个需要连接两个控制器的双人游戏。

### 查询控制器对象

除了 `connect()` 和 `disconnect()` ，`gamepadAPI` 对象中还有另外两个方法：`update()` 和 `buttonPressed()`。`update()` 会在游戏循环的每一帧中执行，来更新 gamepad 对象的实时状态：

```js
update: function() {
  // 清除按钮缓存
  gamepadAPI.buttonsCache = [];
  // 从上一帧中移动按钮状态到缓存中
  for(var k=0; k<gamepadAPI.buttonsStatus.length; k++) {
    gamepadAPI.buttonsCache[k] = gamepadAPI.buttonsStatus[k];
  }
  // 清除按钮状态
  gamepadAPI.buttonsStatus = [];
  // 获取 gamepad 对象
  var c = gamepadAPI.controller || {};

  // 遍历按键，并将按下的按钮加到数组中
  var pressed = [];
  if(c.buttons) {
    for(var b=0,t=c.buttons.length; b<t; b++) {
      if(c.buttons[b].pressed) {
        pressed.push(gamepadAPI.buttons[b]);
      }
    }
  }
  // 遍历坐标值并加到数组中
  var axes = [];
  if(c.axes) {
    for(var a=0,x=c.axes.length; a<x; a++) {
      axes.push(c.axes[a].toFixed(2));
    }
  }
  // 分配接收到的值
  gamepadAPI.axesStatus = axes;
  gamepadAPI.buttonsStatus = pressed;
  // 返回按钮以便调试
  return pressed;
},
```

在每一帧上，`update()` 都会将上一帧的按钮状态保存至数组 `buttonsCache` 中，并在 `gamepadAPI.controller` 对象提取出新的状态信息。然后它就能轮询按钮和坐标值并获得它们的实时状态和值。

### 监测按钮按下

方法 `buttonPressed()` 也位于主游戏循环中来监听按钮的按下。它有两个参数——我们想要监听的按钮和 (可选) 用来告诉游戏接收的按键是（从之前就）被按住了的。没了它你需要松开并再按一次按钮才能得到想要的结果。

```js
buttonPressed: function(button, hold) {
  var newPress = false;
  // 轮询按下的按钮
  for(var i=0,s=gamepadAPI.buttonsStatus.length; i<s; i++) {
    // 如果我们找到我们想要的按钮
    if(gamepadAPI.buttonsStatus[i] == button) {
      // 设置布尔变量（newPress）为 true
      newPress = true;
      // 如果我们想检查按住还是单次按下
      if(!hold) {
        // 从上一帧轮询缓存状态
        for(var j=0,p=gamepadAPI.buttonsCache.length; j<p; j++) {
          // 如果按钮（之前）已经被按下了则忽略新的按下状态
          if(gamepadAPI.buttonsCache[j] == button) {
            newPress = false;
          }
        }
      }
    }
  }
  return newPress;
},
```

在一个按钮中有两种动作：单次按下和按住。变量 `newPress` 布尔变量将会指出这个是不是一个按钮新的按下操作。下次我们再轮询已按下按钮的数组——如果有按钮是我们正在找的，那么设 `newPress` 变量为 `true` 。通过检查本次按下是不是新按下的，就能得出玩家是不是按住按钮了。我们从游戏循环中的上一帧轮询按钮的缓存状态，如果我们找到了，就说明按钮被按住了，所以就不是新的按下。最后 `newPress` 变量被返回。函数 `buttonPressed` 通常这样来更新游戏循环：

```js
if (gamepadAPI.turbo) {
  if (gamepadAPI.buttonPressed("A", "hold")) {
    this.turbo\_fire();
  }
  if (gamepadAPI.buttonPressed("B")) {
    this.managePause();
  }
}
```

如果 `gamepadAPI.turbo` 为 `true` 并有按钮被按下 (或被按住)，我们就会为其分配恰当的操作。在本例中，按下或按住 `A` 开火，按下 `B` 暂停游戏。

### 坐标阈值

按钮只有两种状态：`0` 或 `1`，但是摇杆可以有许多不同的值——他们在 `X` 和 `Y` 轴上都有一个范围为 `-1` 到 `1` 的浮点值。

控制器放在一边不活动时轴值也可能有一定波动 (get dusty) ，这也就是说通过判断等于绝对的 -1 或 1 来可能是会有问题的。因此对此最好是给轴值设定一个阈值来触发生效。比如说，“冰箱坦克”仅会在 `X` 值大于 `0.5` 的时候向右转：

```js
if (gamepadAPI.axesStatus[0].x > 0.5) {
  this.player.angle += 3;
  this.turret.angle += 3;
}
```

即使我们稍微误推摇杆或者摇杆没有弹回原始位置，“冰箱坦克”也不会意外转向。

规范更新
----

经过长达一年多的规范化，W3C Gamepaf API 于 2015 年 4 月更新了规范 (查看最新信息)。更新的改动并不是很大，但是我们最好了解一下到底更新了些什么——以下为更新。

### 获取控制器

`Naviagator.getGamepads()` 方法已用更长的说明和示例代码更新。现在控制器数组的长度必须为 `n+1`（`n` 是已连接设备的数量）——当设备连接且其有索引 1，数组长度为 2，那么它将会是这样：`[null, [object Gamepad]]`。如果设备被断开或不可用的话，值将被设为 `null`。

### 映射标准

布局类型现在是一个可枚举的对象而不是字符串：
```

enum GamepadMappingType {
    "",
    "standard"
};
```

此枚举中定义了已知的控制器映射集。目前只有 `standard` 布局可用，但是未来可能会有新的布局。如果布局未知，那么将会是空字符串。

### 事件

除了当前可用的 `gamepadconnected` 和 `gamepaddisconnected` 事件，其实还有其他事件也曾在规范中，但它们因为不是非常的有用所以被移出了规范。相关讨论仍在进行中，关于它们是否应该恢复规范，以及以什么形式恢复。

总结
--

Gamepad API 非常易于开发。现在它比以往更容易向浏览器提供游戏主机的体验而不需要任何插件。你可以直接在你的浏览器中游玩完整的 Hungry Fridge 游戏。你可以从 Firefox Marketplace 中安装，或者可以在 Gamepad API Content Kit 中查看示例源代码。


2D 碰撞检测 - 游戏开发

2D 碰撞检测
=======

2D 游戏中的碰撞检测算法依赖于可碰撞物体的形状（例如：矩形与矩形、矩形与圆形、圆形与圆形）。通常情况下，你使用的简单通用形状，会被称为“碰撞盒（hitbox）”的实体所覆盖，尽管发生的碰撞并不是像素那样完美契合，但它看起来也足够好，而且可跨多个实体执行碰撞。本文提供了一系列较为通用的 2D 游戏中碰撞检测技术。

轴对齐包围盒
------

碰撞检测的一种较简单的形式是在轴线对齐的两个矩形之间进行碰撞检测（这意味着没有旋转）。该算法的工作原理是确保两个矩形的 4 条边之间没有间隙。任何间隙都意味着不存在碰撞。
```

<div id="cr-stage"></div>
<p>用箭头键移动矩形。绿色表示碰撞，蓝色表示无碰撞。</p>
<script src="https://cdnjs.cloudflare.com/ajax/libs/crafty/0.5.4/crafty-min.js"></script>
```

```js
Crafty.init(200, 200);

const dim1 = { x: 5, y: 5, w: 50, h: 50 };
const dim2 = { x: 20, y: 10, w: 60, h: 40 };

const rect1 = Crafty.e("2D, Canvas, Color").attr(dim1).color("red");

const rect2 = Crafty.e("2D, Canvas, Color, Keyboard, Fourway")
  .fourway(2)
  .attr(dim2)
  .color("blue");

rect2.bind("EnterFrame", function () {
  if (
    rect1.x < rect2.x + rect2.w &&
    rect1.x + rect1.w > rect2.x &&
    rect1.y < rect2.y + rect2.h &&
    rect1.y + rect1.h > rect2.y
  ) {
    // 检测到碰撞发生！
    this.color("green");
  } else {
    // 没有碰撞
    this.color("blue");
  }
});
```

**备注：** 另一个不使用 Canvas 或外部库的示例。

圆形碰撞
----

碰撞测试的另一种形状是两个圆形间的碰撞，该算法是通过获取两个圆心点，并确定圆心距离小于两个圆形的半径和实现的。
```

<div id="cr-stage"></div>
<p>用箭头键移动圆形。绿色表示碰撞，蓝色表示无碰撞。</p>
<script src="https://cdnjs.cloudflare.com/ajax/libs/crafty/0.5.4/crafty-min.js"></script>
```

```
#cr-stage {
  position: static !important;
  height: 200px !important;
}
```

```js
Crafty.init(200, 200);

const dim1 = { x: 5, y: 5 };
const dim2 = { x: 20, y: 20 };

Crafty.c("Circle", {
  circle(radius, color) {
    this.radius = radius;
    this.w = this.h = radius \* 2;
    this.color = color || "#000000";

    this.bind("Move", Crafty.DrawManager.drawAll);
    return this;
  },

  draw() {
    const ctx = Crafty.canvas.context;
    ctx.save();
    ctx.fillStyle = this.color;
    ctx.beginPath();
    ctx.arc(
      this.x + this.radius,
      this.y + this.radius,
      this.radius,
      0,
      Math.PI \* 2,
    );
    ctx.closePath();
    ctx.fill();
    ctx.restore();
  },
});

const circle1 = Crafty.e("2D, Canvas, Circle").attr(dim1).circle(15, "red");

const circle2 = Crafty.e("2D, Canvas, Circle, Fourway")
  .fourway(2)
  .attr(dim2)
  .circle(20, "blue");

circle2.bind("EnterFrame", function () {
  const dx = circle1.x - circle2.x;
  const dy = circle1.y - circle2.y;
  const distance = Math.sqrt(dx \* dx + dy \* dy);

  const colliding = distance < circle1.radius + circle2.radius;
  this.color = colliding ? "green" : "blue";
});
```

**备注：** 另一个不使用 Canvas 或外部库的示例

分离轴定理
-----

这是一种碰撞算法，可以检测任意两个*凸*多边形之间的碰撞。它的实现比上述方法更复杂，但功能更强大。这种算法的复杂性意味着我们需要考虑性能优化，这将在下一节中介绍。

实现分离轴定理（SAT）不在本页讨论范围之内，请参阅下面推荐的教程：

1. 分离轴定理（SAT）解释
2. 碰撞检测与响应
3. 使用分离轴定理进行碰撞检测
4. SAT（分离轴定理）
5. 分离轴定理
碰撞性能
----

虽然其中一些碰撞检测算法计算起来非常简单，但要测试*每一个*实体与其他实体之间的碰撞可能会浪费很多周期。通常游戏会将碰撞分为两个阶段，即宽周期和窄周期。

### 宽周期

宽周期应该得到一个*可能*发生碰撞的实体列表。这可以通过一个空间数据结构来实现，它能让你大致了解实体存在的位置及其周围的情况。空间数据结构的一些例子包括四叉树、R 树或空间哈希图。

### 窄周期

如果要检查的实体数量不多，就需要使用窄周期算法（如上文列出的算法）来确定是否存在碰撞。


asm.js 的异步脚本 - 游戏开发

asm.js 的异步脚本
============

每个中型或大型游戏都应编译asm.js (en-US)代码作为异步脚本的一部分，以便浏览器能够最大限度地灵活地优化编译过程。在 Gecko 中，异步编译允许 JavaScript 引擎在游戏加载时缓存主线程的 asm.js，并缓存生成的机器代码，这样游戏就不需要在随后的加载中编译（从 Firefox 28 开始）。要查看差异，请切换`javascript.options.parallel_parsing` in `about:config`.

异步执行
----

获取异步编译非常简单：编写 JavaScript 时，只需使用 `async` 属性即可：

```js
<script async src="file.js"></script>
```

或者，通过脚本来做同样的事情：

```js
const script = document.createElement("script");
script.src = "file.js";
document.body.appendChild(script);
```

（从脚本中创建的脚本默认为异步。）默认的 HTML shell Emscripten 生成后者。

什么时候用 async 或者不用？
-----------------

两种常见的情况下是脚本是**非**异步的（由HTML 规范定义）

```js
<script async>code();</script>
```

以及

```js
const script = document.createElement("script");
script.textContent = "code()";
document.body.appendChild(script);
```

两者都被视为“内联”脚本，阻塞其余所有任务，进行编译，编译完成后立即执行。

如果你的代码是一个 JS 字符串呢？你应该使用带有对象 URL（object URL）的 Blob 对象，而不是使用 `eval` 或 `innerHTML`，这两者都会触发同步编译：

```js
const blob = new Blob([codeString]);
const script = document.createElement("script");
const url = URL.createObjectURL(blob);
script.onload = script.onerror = () => URL.revokeObjectURL(url);
script.src = url;
document.body.appendChild(script);
```

使用`src`而不是`innerHTML，则该`脚本是异步的。


3D 碰撞检测 - 游戏开发

3D 碰撞检测
=======

本文介绍了用于在 3D 环境中实现碰撞检测的不同包围体（bounding volume）技术。后续文章将讨论特定 3D 库中的实现。

轴对齐包围盒
------

在 2D 碰撞检测中，**轴对齐包围盒**（axis-aligned bounding box，AABB 包围盒）是判断两个物体是否重叠的最快算法。这种方法是将游戏实体包裹在一个非旋转的（因此是轴对齐的）盒中，并检查这些盒在三维坐标空间中的位置，以确定它们是否重叠。

![两个 3D 非方形物体漂浮在虚拟矩形盒子包围的空间中。](/zh-CN/docs/Games/Techniques/3D_collision_detection/screen_shot_2015-10-16_at_15.11.21.png)

由于性能原因，其存在**轴对齐约束**。两个非旋转的盒子之间是否重叠可以只通过逻辑比较进行检查，而旋转的盒子则需要三角运算，计算速度较慢。如果你有旋转的物体，可以通过修改包围盒的尺寸，这样盒子仍可以包裹物体，或者选择使用另一种边界几何类型，比如球体（旋转不改变形状）。下面的 GIF 动画显示了 AABB 的图形示例，该示例调整其大小以适应旋转实体。盒子不断改变尺寸以紧密贴合内部的实体。

![旋转线结动画显示虚拟矩形框随着线结的旋转而收缩和扩大。盒子不旋转。](/zh-CN/docs/Games/Techniques/3D_collision_detection/rotating_knot.gif)

**备注：** 参考使用 Three.js 的包围体 (en-US)，以了解该技术的实际实现。

### 点与 AABB

测试一个点是否在 AABB 内部非常简单——我们只需要测试这个点的坐标是否在 AABB 内（分别考虑每个坐标轴）。如果假设 *Px*、*Py* 和 *Pz* 是点的坐标，*BminX*–*BmaxX*、*BminY*–*BmaxY* 和 *BminZ*–*BmaxZ* 是 AABB 的每一个坐标轴的范围，我们可以使用以下公式计算两者之间的碰撞是否发生：

f
(
P
,
B
)
=
(

P
x

≥

B

m
i
n
X

∧

P
x

≤

B

m
a
x
X

)
∧
(

P
y

≥

B

m
i
n
Y

∧

P
y

≤

B

m
a
x
Y

)
∧
(

P
z

≥

B

m
i
n
Z

∧

P
z

≤

B

m
a
x
Z

)

f(P, B)= (P\_x \ge B\_{minX} \wedge P\_x \le B\_{maxX}) \wedge (P\_y \ge B\_{minY} \wedge P\_y \le B\_{maxY}) \wedge (P\_z \ge B\_{minZ} \wedge P\_z \le B\_{maxZ})

或者用 JavaScript：

```js
function isPointInsideAABB(point, box) {
  return (
    point.x >= box.minX &&
    point.x <= box.maxX &&
    point.y >= box.minY &&
    point.y <= box.maxY &&
    point.z >= box.minZ &&
    point.z <= box.maxZ
  );
}
```

### AABB 与 AABB

测试一个 AABB 是否和另一个 AABB 相交类似于测试点的方法。我们只需要基于每一条坐标轴测试盒子的边界。下图显示了我们基于 X 轴的测试——基本上是测试 *AminX*–*AmaxX* 和 *BminX*–*BmaxX* 是否重叠。

![两个矩形的手绘图，显示 A 的右上角与 B 的左下角重叠，因为 A 的 x 轴的最大坐标大于 B 的 x 轴的最小坐标](/zh-CN/docs/Games/Techniques/3D_collision_detection/aabb_test.png)

在数学上的表示就像这样：

f
(
A
,
B
)
=
(

A

m
i
n
X

≤

B

m
a
x
X

∧

A

m
a
x
X

≥

B

m
i
n
X

)
∧
(

A

m
i
n
Y

≤

B

m
a
x
Y

∧

A

m
a
x
Y

≥

B

m
i
n
Y

)
∧
(

A

m
i
n
Z

≤

B

m
a
x
Z

∧

A

m
a
x
Z

≥

B

m
i
n
Z

)

f(A, B) = (A\_{minX} \le B\_{maxX} \wedge A\_{maxX} \ge B\_{minX}) \wedge ( A\_{minY} \le B\_{maxY} \wedge A\_{maxY} \ge B\_{minY}) \wedge (A\_{minZ} \le B\_{maxZ} \wedge A\_{maxZ} \ge B\_{minZ})

在 JavaScript 中，我们可以这样：

```js
function intersect(a, b) {
  return (
    a.minX <= b.maxX &&
    a.maxX >= b.minX &&
    a.minY <= b.maxY &&
    a.maxY >= b.minY &&
    a.minZ <= b.maxZ &&
    a.maxZ >= b.minZ
  );
}
```

包围球
---

使用包围球（bounding sphere）检测碰撞比 AABB 盒子稍微复杂一点，但仍旧相当容易。球体的主要优势是它的旋转不变性，如果包裹的实体旋转，包围球仍将是相同的。它的主要缺点是，除非包裹的实体实际上是球形的，否则包裹通常不太合适（比如用包围球包裹一个人将导致一些误报，而 AABB 盒子将更合适)。

### 点与球

测试是否一个球体包含一个点，我们需要计算点和球体的中心之间的距离。如果这个距离小于或等于球的半径，这个点就在球体内部。

![笛卡尔坐标系中球体和点的 2D 投影的手绘图。该点位于圆外部的右下角。从圆心到该点的距离用虚线表示，标记为 D。较浅的线显示从圆心到圆边界的半径，标记为 R。](/zh-CN/docs/Games/Techniques/3D_collision_detection/point_vs_sphere.png)

两个点 *A* 和 *B* 之间的欧氏距离是

(

A
x

−

B
x

)
2

+
(

A
y

−

B
y

)
2

+
(

A
z

−

B
z

)
2

\sqrt{(A\_x - B\_x)^2 + (A\_y - B\_y)^2 + (A\_z - B\_z)^2}

，我们的公式指出，点与球体之间的碰撞检测应该像这样：

f
(
P
,
S
)
=

S

r
a
d
i
u
s

≥

(

P
x

−

S
x

)
2

+
(

P
y

−

S
y

)
2

+
(

P
z

−

S
z

)
2

f(P,S) = S\_{radius} \ge \sqrt{(P\_x - S\_x)^2 + (P\_y - S\_y)^2 + (P\_z - S\_z)^2}

或者用 JavaScript：

```js
function isPointInsideSphere(point, sphere) {
  // 我们使用乘法是因为这样比调用 Math.pow 更快
  const distance = Math.sqrt(
    (point.x - sphere.x) \* (point.x - sphere.x) +
      (point.y - sphere.y) \* (point.y - sphere.y) +
      (point.z - sphere.z) \* (point.z - sphere.z),
  );
  return distance < sphere.radius;
}
```

**备注：** 上面的代码有一个平方根，计算开销可能很大。一种简单的优化是将平方距离与平方半径进行比较，以避免这种情况，所以优化后的不等式涉及 `distanceSqr < sphere.radius * sphere.radius`。

### 球体与球体

球体与球体的测试类似于点与球体的。我们需要测试的是球体的中心之间的距离小于或等于半径的总和。

![两个部分重叠的圆圈的手绘图。每个圆（不同大小）都有一条从其中心到其边界的浅色半径线，标记为 R。距离由连接两个圆的中心点的虚线表示，标记为 D。](/zh-CN/docs/Games/Techniques/3D_collision_detection/sphere_vs_sphere.png)

在数学上，应该像这样：

f
(
A
,
B
)
=

(

A
x

−

B
x

)
2

+
(

A
y

−

B
y

)
2

+
(

A
z

−

B
z

)
2

≤

A

r
a
d
i
u
s

+

B

r
a
d
i
u
s

f(A,B) = \sqrt{(A\_x - B\_x)^2 + (A\_y - B\_y)^2 + (A\_z - B\_z)^2} \le A\_{radius} + B\_{radius}

或者用 JavaScript：

```js
function intersect(sphere, other) {
  // 我们使用乘法是因为这样比调用 Math.pow 更快
  const distance = Math.sqrt(
    (sphere.x - other.x) \* (sphere.x - other.x) +
      (sphere.y - other.y) \* (sphere.y - other.y) +
      (sphere.z - other.z) \* (sphere.z - other.z),
  );
  return distance < sphere.radius + other.radius;
}
```

### 球体与 AABB

测试一个球和一个 AABB 的碰撞是稍微复杂，但过程仍然简单和快速。一个合乎逻辑的方法是检查 AABB 每个顶点，计算每一个点与球的距离。然而这有点大材小用了，测试所有的顶点是不必要的，因为我们可以仅计算 AABB 的*最近点*（不一定是一个顶点）和球体的中心之间的距离，看看它是小于或等于球体的半径。我们可以通过逼近球体的中心和 AABB 的距离得到这个值：

![正方形部分重叠在圆的顶部的手绘图。半径由标记为 R 的浅色线表示。距离线是从圆的中心到正方形的最近点。](/zh-CN/docs/Games/Techniques/3D_collision_detection/sphere_vs_aabb.png)

在 JavaScript 中，我们可以像这样进行测试：

```js
function intersect(sphere, box) {
  // 通过逼近距离获得距离球体中心最近的点
  const x = Math.max(box.minX, Math.min(sphere.x, box.maxX));
  const y = Math.max(box.minY, Math.min(sphere.y, box.maxY));
  const z = Math.max(box.minZ, Math.min(sphere.z, box.maxZ));

  // 这与 isPointInsideSphere 相同
  const distance = Math.sqrt(
    (x - sphere.x) \* (x - sphere.x) +
      (y - sphere.y) \* (y - sphere.y) +
      (z - sphere.z) \* (z - sphere.z),
  );

  return distance < sphere.radius;
}
```

使用物理引擎
------

**3D 物理引擎**提供了碰撞检测算法，其中大多数也都是基于包围体。物理引擎的工作方式是创建一个**物理体**，通常附加到其可视化表示上。该物体具有诸如速度、位置、旋转、扭矩等属性，还有一个**物理形状**。这个形状是碰撞检测计算中考虑的形状。

我们准备了一个碰撞检测演示（源代码），你可以看到这些技术是如何运作的——这里使用了开源的 3D 物理引擎 cannon.js。

参见
--

MDN 上的相关文章：

* 使用 Three.js 进行边界体积碰撞检测 (en-US)
* 2D 碰撞检测

外部资源：

* Gamasutra 上的游戏的简单相交测试
* 维基百科上的包围体


解释基本的 3D 原理 - 游戏开发

解释基本的 3D 原理
===========

这篇文章解释了当你开始使用 3D 工作的时候需要的所有有用的基本理论

坐标系统
----

3D 指的是有关在 3D 空间中所有形状的表示，并且可以使用坐标系统来计算其位置。

![Coordinate system](/zh-CN/docs/Games/Techniques/3D_on_the_web/Basic_theory/mdn-games-3d-coordinate-system.png)

WebGL 使用右手坐标系统 — `x` 轴向右，`y` 轴向上 `z` 轴指向屏幕外，在上图可以看到。

物体
--

使用顶点建立不同类型的物体。 **一个顶点**是在 3D 坐标系中拥有坐标位置的一个点以及一些额外可以定义它的信息。每个点都包含这些属性：

* **位置**: 在 3D 空间用来辨认 (`x`, `y`, `z`).
* **颜色**: 包含 RGBA (R, G 和 B 分别是红，绿，蓝和 alpha 通道，alpha 通道控制透明度 — 所有通道值的范围都是 `0.0` 到 `1.0`).
* **法线：** 描述顶点朝向。
* **纹理**: 顶点用来装饰模型表面的一张 2D 图片，它是代替简单颜色的选择之一。

你可以使用这些信息建立几何体 — 这是一个立方体的例子：

![Cube](/zh-CN/docs/Games/Techniques/3D_on_the_web/Basic_theory/mdn-games-3d-cube.png)

给定形状的一个面是顶点之间的一个平面。例如，一个立方体有 8 个不同的顶点和 6 个不同的面 每个面由 4 个顶点构成。一条法线定义面的朝向。同时，连接这些点可以创建立方体的边。这个几何体通过点和面构成，材质使用的是一张纹理贴图，这里使用一个纯蓝色或一张图片。该物体的几何形状 (geometry) 由顶点和面构成，而材质 (material) 则是由纹理构成。如果我们将几何体和材质连接起来会得到一个网格 (mesh).

渲染流程
----

渲染流程是个将之前准备好的模型输出到屏幕的过程。3D 渲染流程会接受使用顶点描述 3D 物体的原始数据作为输入用于处理，并计算其片段 (fragment), 然后渲染为像素 (pixels) 输出到屏幕。

![Rendering pipeline](/zh-CN/docs/Games/Techniques/3D_on_the_web/Basic_theory/mdn-games-3d-rendering-pipeline.png)

上图中用到了如下术语：

* **原始数据**: 渲染流程中的输入 — 用顶点生成，它可能是三角形，点或线。
* **片段**: 一个像素的 3D 投射，有着和像素一样的属性。
* **像素**: 屏幕上的 2D 网格中的点布置的点，包含 RGBA.

顶点和片段处理是可编程的 — 你可以编写自己的着色器 来控制输出。

顶点处理
----

顶点处理是将独立的顶点信息组合成原始数据并设置其在 3D 空间中的坐标，方便显示器识别。就像是对你准备的场景进行拍照 — 你必须先放置物品，设置相机参数，然后开拍。

![Vertex processing](/zh-CN/docs/Games/Techniques/3D_on_the_web/Basic_theory/mdn-games-3d-vertex-processing.png)

这个过程分为四步：第一步是筹备世界坐标中的物体，也被称为**模型转换 (model transformation)**. 然后是**视图转换 (view transformation)** , 这一步只关心位置和 3D 空间中摄像机的朝向设置。摄像机有三个参数 (位置，方向和朝向), 在新创建的场景中必须定义这三个参数。

![Camera](/zh-CN/docs/Games/Techniques/3D_on_the_web/Basic_theory/mdn-games-3d-camera.png)

**投射转换**(projection transformation), 也被称作透视转换 (perspective transformation), 这一步定义摄像机设置，在此过程会设置哪些在摄像机中可见，配置包含视野 (field of view), 宽高比例 (aspect ratio) 和可选的近裁剪和远裁剪参数。阅读 Three.js 文章摄像机了解更多。

![Camera settings](/zh-CN/docs/Games/Techniques/3D_on_the_web/Basic_theory/mdn-games-3d-camera-settings.png)

最后一步是**视图窗口转换**(viewport transformation), 这一步会将一切都输出给渲染流程中的下一步。

栅格化
---

栅格化将原始数据 (从顶点信息转换过来的) 转换为一系列的片段。

![Rasterization](/zh-CN/docs/Games/Techniques/3D_on_the_web/Basic_theory/mdn-games-3d-rasterization.png)

那些片段 (2D 像素的 3D 投射) 是对应到像素网格的，所以在渲染合成阶段最后他们会被 2D 的屏幕直接打印到像素点上。

片段合成
----

片段合成关注关注的是纹理和光照 — 它会基于给定参数计算最终的颜色。

![Fragment processing](/zh-CN/docs/Games/Techniques/3D_on_the_web/Basic_theory/mdn-games-3d-fragment-processing.png)

### 纹理

纹理是在 3D 空间中用了是模型看起来更加真实的 2D 图片。纹理是由称为纹素的单个纹理元素组合，和像素组合的原理一样。如果必要的话，在渲染流程中的片段处理阶段添加纹理到模型上允许我们使用包装 (wrapping) 和过滤 (filtering) 进行适配。

纹理包装允许你在 3D 模型上重复使用 2D 图片。纹理过滤是纹理贴图的原始分辨率和将要呈现的片段的分辨率不同的时候，会根据情况对纹理进行缩放。

### 光照

我们在屏幕上看到的颜色是光照和模型颜色，材质进行交互之后的最终结果。灯光会被吸收和反射，在 WebGL 中实现的标准**Phong 光照模型** 有以下四种光照参数：

* **漫反射**: 遥远的直接光照，就像太阳。
* **高光**: 点光源，就像房间的白炽灯或闪光灯。
* **环境色**: 常量灯光，可影响场景中的所有模型。
* **自发光**: 模型自身发出的光。
输出合成
----

在输出操作阶段所有来自 3D 空间的原始数据的片段会被转换到 2D 像素网格中，然后打印到屏幕像素上。

![Output merging](/zh-CN/docs/Games/Techniques/3D_on_the_web/Basic_theory/mdn-games-3d-output-merging.png)

在输出合成阶段同样可以忽略不必要的信息，例如在屏幕外的模型参数或者被其他模型遮挡的模型，因为是不可见的所以不会被计算。

总结
--

现在你知道了 3D 操作背后的基本原理。如果你想去练习或者看学习 demo, 看看下面的教程：

* 用 Three.js 创建基本 demo
* 用 Babylon.js 创建基本 demo (en-US)
* 用 PlayCanvas 创建基本 demo (en-US)
* 用 A-Frame 创建基本 demo (en-US)

继续，去创建一些炫酷 3D 实验吧！


GLSL 着色器 - 游戏开发

GLSL 着色器
========

使用 GLSL 的着色器（shader），GLSL 是一门特殊的有着类似于 C 语言的语法，在图形管道 (graphic pipeline) 中直接可执行的 OpenGL 着色语言。着色器有两种类型——顶点着色器 (Vertex Shader) 和片段着色器（Fragment Shader）。前者是将形状转换到真实的 3D 绘制坐标中，后者是计算最终渲染的颜色和其他属性用的。

GLSL 不同于 JavaScript, 它是强类型语言，并且内置很多数学公式用于计算向量和矩阵。快速编写着色器非常复杂，但创建一个简单的着色器并不难。在这篇文章我们将介绍使用着色器的基础知识，并且构建一个使用 Three.js 的例子来加速代码编写。

你可能记得基本原理那篇文章，一个顶点 (vertex) 是在空间中有自己 3D 坐标的点，并且通常包含些被定义的其他信息。空间本身会被坐标系统定义。在那个 3D 空间中一切都是关于形状的呈现。

着色器类型
-----

一个着色器实际上就是一个绘制东西到屏幕上的函数。着色器运行在 GPU 中，它对这些操作进行了很多的优化，这样你就可以卸载很多不必要的 CPU, 然后集中处理能力去执行你自己的代码。

### 顶点着色器

顶点着色器操作 3D 空间的坐标并且每个顶点都会调用一次这个函数。其目的是设置 `gl_Position` 变量——这是一个特殊的全局内置变量，它是用来存储当前顶点的位置：

```glsl
void main() {
  gl_Position = makeCalculationsToHaveCoordinates;
}
```

这个 `void main()` 函数是定义全局`gl_Position` 变量的标准方式。所有在这个函数里面的代码都会被着色器执行。如果将 3D 空间中的位置投射到 2D 屏幕上这些信息都会保存在计算结果的变量中。

### 片段着色器

片段 (或者纹理) 着色器 在计算时定义了每像素的 RGBA 颜色 — 每个像素只调用一次片段着色器。这个着色器的作用是设置 `gl_FragColor` 变量，也是一个 GLSL 内置变量：

```glsl
void main() {
  gl_FragColor = makeCalculationsToHaveColor;
}
```

计算结果包含 RGBA 颜色信息。

示例
--

让我们构建一个简单的例子来解释这些着色器的动作。假设你已经看过Three.js 教程并掌握了场景，物体和材质的基本概念。

**备注：** 记住你没必要使用 Three.js 或者其他库来编写着色器——纯WebGL 完全够了。我们这里使用 Three.js 来制作背景代码更简单和易理解。所以你只需关注着色器代码。Three.js 和其他 3D 库给你抽象了很多东西出来——如果你想要用纯 WebGL 创建这个例子，你得写很多其他的代码才能运行。

### 环境设置

要开始编写 WebGL 着色器你不需要做太多，只需：

* 确保你在使用对 WebGL 有良好支持的现代浏览器，比如最新版的 Firefox 或 Chrome.
* 创建一个目录保存你的实验。
* 拷贝一份的 压缩版的 Three.js 库 到你的目录。
### HTML 结构

这是将用到的 HTML 结构。

```html
<!doctype html>
<html>
  <head>
    <meta charset="utf-8" />
    <title>MDN Games: Shaders demo</title>
    <style>
 body {
 margin: 0;
 padding: 0;
 font-size: 0;
 }
 canvas {
 width: 100%;
 height: 100%;
 }
 </style>
    <script src="three.min.js"></script>
  </head>
  <body>
    <script id="vertexShader" type="x-shader/x-vertex">
 // 顶点着色器代码在这里
 </script>
    <script id="fragmentShader" type="x-shader/x-fragment">
 // 片段着色器代码在这里
 </script>
    <script>
 // 场景设置在这里
 </script>
  </body>
</html>
```

其包含了一些基本信息比如文档的 `<title>`，并且设置了 `<canvas>` 元素 css 样式的宽高，Three.js 会插入到页面中占满整个可视区域。`<script>` 元素在包含 Three.js 库的 `<head>` 中。我们的代码将写在 `<body>` 标签中的 script 标签中：

1. 首先将包含顶点着色器。
2. 然后包含片段着色器。
3. 最后会包含一些生成实际场景的 JavaScript 代码。

阅读之前，复制这些代码到一个新的文本文件中，保存到你的工作目录作为 `index.html`. 我们将在这个文件中创建一个简单的立方体来解释着色器是如何工作的。

### 立方体源代码

我们可以复用Building up a basic demo with Three.js 中立方体的源代码，大多数元素例如渲染器，摄像机和灯光都没有发生改变，但是基本的材质会用到自己写的着色器。

去cube.html file on GitHub中，复制第二个`<script>`元素中所有的 JavaScript 代码，粘贴到当前例子中的第三个`<script>` 标签中。保存并运行 `index.html` — 然后你会看到一个蓝色立方体

### 顶点着色器代码

让我们继续编写顶点着色器 — 添加下面这段代码到你 body 的第一个 `<script>` 标签：

```glsl
void main() {
  gl_Position = projectionMatrix \* modelViewMatrix \* vec4(position.x+10.0, position.y, position.z+5.0, 1.0);
}
```

每次的`gl_Position` 的结果是计算 model-view 矩阵和投射矩阵和投射矩阵相乘并得到最后的顶点位置。

**备注：** 你可以在 顶点处理中学到更多关于模型，视图和投射变换，并且你可以在文末看到更多学习链接。

`projectionMatrix` 和 `modelViewMatrix` 两个函数都是 Three.js 提供的，并且传入了一个新的 3D 位置向量，转成着色器之后直接导致立方体向 `x` 轴移动 10 个单位，向`z` 轴移动了 5 个单位。我们可以忽略第四个参数并且保持为默认的`1.0` ; 这是用来控制 3D 空间中订单位置裁剪的，这个例子中不需要。

### 纹理着色器代码

现在我们将添加纹理着色器代码 — 将以下代码复制到第二个 `<script>` 标签中：

```glsl
void main() {
  gl_FragColor = vec4(0.0, 0.58, 0.86, 1.0);
}
```

这将设置一个 RGBA 颜色来重建当前的蓝色灯光 — 前三个浮点数 (范围是 0.0 到 1.0) 代表红，绿，蓝，第四个值代表 alpha 通道，控制透明度 (0.0 完全透明，1.0 是完全不透明).

### 设置着色器

实际上是创建了一个新的着色器给立方体，先用 `basicMaterial` 来定义：

```js
// var basicMaterial = new THREE.MeshBasicMaterial({color: 0x0095DD});
```

然后创建 `shaderMaterial`:

```js
var shaderMaterial = new THREE.ShaderMaterial({
  vertexShader: document.getElementById("vertexShader").textContent,
  fragmentShader: document.getElementById("fragmentShader").textContent,
});
```

这个着色器材质使用脚本中的代码并将它们赋予给材质所赋予的物体

然后，在定义立方体材质那一行我们需要替换`basicMaterial` :

```js
var cube = new THREE.Mesh(boxGeometry, basicMaterial);
```

使用新创建的 `shaderMaterial`:

```js
var cube = new THREE.Mesh(boxGeometry, shaderMaterial);
```

Three.js 编译和运行这两个这两个着色器到材质所在的网格 (mesh) 上。在这个例子中，为立方体添加了有顶点和纹理着色器。好了，你已经创建了最简单的着色器，祝贺！

下图是立方体最终效果：

![Three.js blue cube demo](/zh-CN/docs/Games/Techniques/3D_on_the_web/GLSL_Shaders/cube.png)

它看起来好像和 Three.js 的立方体 demo 一样，但不同的是，位置有点轻微变化，而且同样的蓝色使用的是着色器实现。你可以看看实际操作，这里有最终代码:

你也可以在 GitHub 看这个例子。

总结
--

本文教了你最基本的着色器实现。我们虽然只能做这么多，但你可以用着色器做很更多炫酷的事情 — 在 ShaderToy 上去看真正炫酷的例子找找灵感吧

其他链接
----

* 学习 WebGL — 基本 WebGL 知识
* WebGL 着色器和 WebGL 中的 GLSL 基础 — GLSL 特定信息