aboutsummaryrefslogtreecommitdiff
path: root/files/zh-cn/games/anatomy/index.html
blob: 8964577ef2fd33e20aefe1468cd4512c1fe0a693 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
---
title: 剖析游戏结构
slug: Games/Anatomy
tags:
  - Games
  - JavaScript
  - Main Loop
  - requestAnimationFrame
translation_of: Games/Anatomy
---
<div>{{GamesSidebar}}</div>

<p>{{IncludeSubnav("/en-US/docs/Games")}}</p>

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

<h2 id="呈现,接收,解释,计算,重复">呈现,接收,解释,计算,重复</h2>

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

<p>具体情况取决于游戏本身。</p>

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

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

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

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

<h2 id="在JavaScript中构建一个主循环">在JavaScript中构建一个主循环</h2>

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

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

<pre class="brush: js notranslate">window.main = function(){
  window.requestAnimationFrame(main);

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

main(); //开始循环</pre>

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

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

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

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

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

<h2 id="在Javascript中构建一个更好的主循环">在Javascript中构建一个更好的主循环</h2>

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

<pre class="brush: js notranslate" dir="rtl">/*
*
*/

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

    //你的主循环内容。
  }

  main(); //开始循环
})();</pre>

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

<div class="blockIndicator note">
<p>注意:在实践中,更常见的终止下一个<code>requestAnimationFrame()</code>方式是使用if语句,而不是调用<code>cancelAnimationFrame()</code></p>
</div>

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

<pre class="brush: js notranslate">/*
* 让我们假设MyGame是之前定义的。
*/

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

    //你的主循环内容
  }

  main(); //开始循环
})();</pre>

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

<pre class="notranslate">window.cancelAnimationFrame(MyGame.stopMain);</pre>

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

<h2 id="用JavaScript构建一个更优化的主循环">用JavaScript构建一个更优化的主循环</h2>

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

<p>现代版的Firefox和Google Chrome(可能还有其他版本)试图在框架的时间片的开始时将请求AnimationFrame回调与它们的主线程进行连接。因此,浏览器的主线程看起来就像下面这样:</p>

<ol>
 <li>启动一个新帧(而之前的帧由显示处理)。</li>
 <li>遍历requestAnimationFrame回调并调用它们。</li>
 <li>当上面的回调停止控制主线程时,执行垃圾收集和其他帧任务。</li>
 <li>睡眠(除非事件打断了浏览器的小睡),直到显示器准备好你的图像(VSYNC)并重复。</li>
</ol>

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

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

<p>注意:系统(硬件或软件)不能达到微秒精度,可以提供毫秒精度的最小值然而,如果他们能够做到这一点,他们就应该提供0.001的准确性。</p>

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

<pre class="brush: js notranslate">var tNow = window.performance.now();
</pre>

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

<pre class="brush: js notranslate">/*
* 以分号开始,以上例子中的代码行都是这样的
* 依靠自动分号插入(ASI)。浏览器可能会意外
* 认为这个整个例子从上一行继续。领先分号
* 标记我们的新行的开始,如果前一个不是空或终止。
*
* 我们还假设MyGame是以前定义的。
*/

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

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

  main(); //开始循环
})();</pre>

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

<h2 id="决定......时间">决定......时间</h2>

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

<p>注意:改变主循环如何处理时间是非常困难的,在进行主循环之前,请仔细考虑您的需求。</p>

<h3 id="大多数浏览器游戏应该是什么样的">大多数浏览器游戏应该是什么样的</h3>

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

<pre class="notranslate">/*
* 以分号开始,以上例子中的代码行都是这样的
* 依靠自动分号插入(ASI)。浏览器可能会意外
* 认为这个整个例子从上一行继续。领先分号
* 标记我们的新行的开始,如果前一个不是空或终止。
*
* 我们还假设MyGame是以前定义的。
*/

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

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

  main(); //开始循环
})();</pre>

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

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

<h3 id="处理可变刷新率需求的其他方法">处理可变刷新率需求的其他方法</h3>

<p>存在其他解决问题的方法。</p>

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

<ul>
 <li>绘制<code>requestAnimationFrame</code>并更新 {{ domxref("window.setInterval") }}{{domxref("window.setTimeout")}}<ul>
   <li>即使在未聚焦或最小化的情况下,使用处理器时间,也可能是主线程,并且可能是传统游戏循环的工件(但是很简单)。</li>
  </ul>
 </li>
 <li>绘制<code>requestAnimationFrame</code>和更新一个<code>setInterval</code><code>setTimeout</code>一个<a href="/en-US/docs/Web/Guide/Performance/Using_web_workers">Web工作者</a><ul>
   <li>这与上述相同,除了更新不会使主线程(主线程也没有)。这是一个更复杂的解决方案,并且对于简单更新可能会有太多的开销。</li>
  </ul>
 </li>
 <li>绘制<code>requestAnimationFrame</code>并使用它来戳一个包含更新方法的Web Worker,其中包含要计算的刻度数(如果有的话)。
  <ul>
   <li>这个睡眠直到<code>requestAnimationFrame</code>被调用并且不会污染主线程,加上你不依赖于老式的方法。再次,这比以前的两个选项更复杂一些,并且<em>开始</em>每个更新将被阻止,直到浏览器决定启动rAF回调。</li>
  </ul>
 </li>
</ul>

<p>这些方法中的每一种都有类似的权衡:</p>

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

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

<p><em>注意:这个例子,具体来说,需要进行技术审查。</em></p>

<pre class="brush: js notranslate">/*
* 以分号开始,以上例子中的代码行都是这样的
* 依靠自动分号插入(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 &lt;nextTick,则需要更新0个ticks(对于numTicks,默认为0)。
    //如果tFrame = nextTick,则需要更新1 tick(等等)。
    //注意:正如我们在总结中提到的那样,您应该跟踪numTicks的大小。
    //如果它很大,那么你的游戏是睡着了,或者机器无法跟上。
    if(tFrame&gt; nextTick){
      var timeSinceTick = tFrame - MyGame.lastTick;
      numTicks = Math.floor(timeSinceTick / MyGame.tickLength);
    }

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

  function queueUpdates(numTicks){
    for(var i = 0; i &lt;numTicks; i ++){
      MyGame.lastTick = 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()); //开始循环
})();</pre>

<p>另一个选择是简单地做一些事情不那么频繁。如果您的更新循环的一部分难以计算但对时间不敏感,则可以考虑缩小其频率,理想情况下,在延长的时间段内将其扩展成块。这是一个隐含的例子,在火炮博物馆的炮兵游戏中,他们<a href="http://blog.artillery.com/2012/10/browser-garbage-collection-and-framerate.html">调整垃圾发生率</a>来优化垃圾收集。显然,清理资源不是时间敏感的(特别是如果整理比垃圾本身更具破坏性)。</p>

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

<h2 id="概要">概要</h2>

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

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

<ul>
 <li>考虑差距“暂停”并跳过时间。

  <ul>
   <li>你可能会看到这对大多数多人游戏来说都是有问题的。</li>
  </ul>
 </li>
 <li>你可以模拟差距赶上。
  <ul>
   <li>这可能是长时间丢失和/或复杂更新的问题。</li>
  </ul>
 </li>
 <li>您可以从联机设备或服务器恢复游戏状态。
  <ul>
   <li>除非联机设备/服务器的游戏状态已经过期,或者这是一个没有没服务器的纯单机游戏。</li>
  </ul>
 </li>
</ul>

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