forked from tkchu/Game-Programming-Patterns-CN
-
Notifications
You must be signed in to change notification settings - Fork 0
/
state.html
793 lines (727 loc) · 55.7 KB
/
state.html
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
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
<head>
<meta http-equiv="Content-type" content="text/html;charset=UTF-8" />
<title>State · Design Patterns Revisited · Game Programming Patterns</title>
<!-- Tell mobile browsers we're optimized for them and they don't need to crop
the viewport. -->
<meta name="viewport" content="width=device-width, initial-scale=1"/>
<link rel="stylesheet" type="text/css" href="style.css" />
<link href="http://fonts.googleapis.com/css?family=Merriweather:400,400italic,700,700italic|Source+Code+Pro|Source+Sans+Pro:200,300,400,600,400italic,600italic|Rock+Salt" rel="stylesheet" type="text/css">
<script>
(function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){
(i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o),
m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m)
})(window,document,'script','//www.google-analytics.com/analytics.js','ga');
ga('create', 'UA-42804721-1', 'gameprogrammingpatterns.com');
ga('send', 'pageview');
</script>
<script src="jquery-1.10.1.min.js"></script>
<script src="script.js"></script>
</head>
<body id="top">
<div class="page sidebar">
<div class="content">
<nav class="top">
<span class="prev">← <a href="singleton.html">Previous Chapter</a></span>
<span class="next"><a href="sequencing-patterns.html">Next Chapter</a> →</span>
<span class="toc">≡ <a href="/">The Book</a></span>
</nav>
<h1>State</h1>
<h1 class="book"><a href="/">Game Programming Patterns</a><span class="section"><a href="design-patterns-revisited.html">Design Patterns Revisited</a></span></h1>
<p>忏悔时间:我有些越界,将太多的东西打包到了这章中。
它表面上关于<a href="http://en.wikipedia.org/wiki/State_pattern" class="gof-pattern">状态模式</a>,但我无法只讨论它和游戏,而不涉及更加基础的<em>有限状态机</em>(FSMs)。
但是一旦讲了那个,我发现也想要介绍<em>层次状态机</em>和<em>下推自动机</em>。</p>
<p>有很多要讲,我会尽可能简短,这里的示例代码留下了一些你需要自己填补 的细节。
我希望它们仍然足够清晰的让你获取一份全景图。</p>
<p>如果你从来没有听说过状态机,不要难过。
虽然在<span name="two-camps">AI和编译器</span>程序员界很知名,在其他编程圈就没那么知名了。
我认为应该有更多人知道它,所以我在这里将其运用在不同的问题。</p>
<aside name="two-camps">
<p>这来自人工智能的早期时代。
在五十年代到六十年代,很多AI研究关注于语言处理。
很多现在用于分析程序语言的技术在当时是发明出来分析人类语言的。</p>
</aside>
<h2><a href="#感同身受" name="感同身受">感同身受</a></h2>
<p>我们在完成一个卷轴平台游戏。
工作是实现玩家在游戏世界中操作的女英雄。
这就意味着她需要对玩家的输入做出响应。按B键她应该跳跃。简单实现如下:</p>
<div class="codehilite"><pre><span class="kt">void</span> <span class="n">Heroine</span><span class="o">::</span><span class="n">handleInput</span><span class="p">(</span><span class="n">Input</span> <span class="n">input</span><span class="p">)</span>
<span class="p">{</span>
<span class="k">if</span> <span class="p">(</span><span class="n">input</span> <span class="o">==</span> <span class="n">PRESS_B</span><span class="p">)</span>
<span class="p">{</span>
<span class="n">yVelocity_</span> <span class="o">=</span> <span class="n">JUMP_VELOCITY</span><span class="p">;</span>
<span class="n">setGraphics</span><span class="p">(</span><span class="n">IMAGE_JUMP</span><span class="p">);</span>
<span class="p">}</span>
<span class="p">}</span>
</pre></div>
<p>看到漏洞了吗?</p>
<p>没有东西阻止“空气跳”——在她空中时狂按B,她就会浮空。
简单的<span name="landing">修复方法</span>是给<code>Heroine</code>增加<code>isJumping_</code>布尔字段,追踪它跳跃的状态。然后这样做:</p>
<div class="codehilite"><pre><span class="kt">void</span> <span class="n">Heroine</span><span class="o">::</span><span class="n">handleInput</span><span class="p">(</span><span class="n">Input</span> <span class="n">input</span><span class="p">)</span>
<span class="p">{</span>
<span class="k">if</span> <span class="p">(</span><span class="n">input</span> <span class="o">==</span> <span class="n">PRESS_B</span><span class="p">)</span>
<span class="p">{</span>
<span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="n">isJumping_</span><span class="p">)</span>
<span class="p">{</span>
<span class="n">isJumping_</span> <span class="o">=</span> <span class="nb">true</span><span class="p">;</span>
<span class="c1">// Jump...</span>
<span class="p">}</span>
<span class="p">}</span>
<span class="p">}</span>
</pre></div>
<aside name="landing">
<p>这也是在英雄接触到地面的时候将<code>isJumping_</code>设回<code>false</code>的代码。
我在这里为了简明而没有写。</p>
</aside>
<p>下面,当玩家按下键时,我们想要她在地上时卧倒,而松开下键的时候站起来:</p>
<div class="codehilite"><pre><span class="kt">void</span> <span class="n">Heroine</span><span class="o">::</span><span class="n">handleInput</span><span class="p">(</span><span class="n">Input</span> <span class="n">input</span><span class="p">)</span>
<span class="p">{</span>
<span class="k">if</span> <span class="p">(</span><span class="n">input</span> <span class="o">==</span> <span class="n">PRESS_B</span><span class="p">)</span>
<span class="p">{</span>
<span class="c1">// Jump if not jumping...</span>
<span class="p">}</span>
<span class="k">else</span> <span class="k">if</span> <span class="p">(</span><span class="n">input</span> <span class="o">==</span> <span class="n">PRESS_DOWN</span><span class="p">)</span>
<span class="p">{</span>
<span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="n">isJumping_</span><span class="p">)</span>
<span class="p">{</span>
<span class="n">setGraphics</span><span class="p">(</span><span class="n">IMAGE_DUCK</span><span class="p">);</span>
<span class="p">}</span>
<span class="p">}</span>
<span class="k">else</span> <span class="k">if</span> <span class="p">(</span><span class="n">input</span> <span class="o">==</span> <span class="n">RELEASE_DOWN</span><span class="p">)</span>
<span class="p">{</span>
<span class="n">setGraphics</span><span class="p">(</span><span class="n">IMAGE_STAND</span><span class="p">);</span>
<span class="p">}</span>
<span class="p">}</span>
</pre></div>
<p>这次看到了错误了吗?</p>
<p>用这个代码,玩家可以:</p>
<ol>
<li>按下键卧倒。</li>
<li>按B从卧倒状态跳起。</li>
<li>在空中放开下键。</li>
</ol>
<p>英雄会在跳跃的半路上变成站立图片。是时候增加另一个标识了……</p>
<div class="codehilite"><pre><span class="kt">void</span> <span class="n">Heroine</span><span class="o">::</span><span class="n">handleInput</span><span class="p">(</span><span class="n">Input</span> <span class="n">input</span><span class="p">)</span>
<span class="p">{</span>
<span class="k">if</span> <span class="p">(</span><span class="n">input</span> <span class="o">==</span> <span class="n">PRESS_B</span><span class="p">)</span>
<span class="p">{</span>
<span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="n">isJumping_</span> <span class="o">&&</span> <span class="o">!</span><span class="n">isDucking_</span><span class="p">)</span>
<span class="p">{</span>
<span class="c1">// Jump...</span>
<span class="p">}</span>
<span class="p">}</span>
<span class="k">else</span> <span class="k">if</span> <span class="p">(</span><span class="n">input</span> <span class="o">==</span> <span class="n">PRESS_DOWN</span><span class="p">)</span>
<span class="p">{</span>
<span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="n">isJumping_</span><span class="p">)</span>
<span class="p">{</span>
<span class="n">isDucking_</span> <span class="o">=</span> <span class="nb">true</span><span class="p">;</span>
<span class="n">setGraphics</span><span class="p">(</span><span class="n">IMAGE_DUCK</span><span class="p">);</span>
<span class="p">}</span>
<span class="p">}</span>
<span class="k">else</span> <span class="k">if</span> <span class="p">(</span><span class="n">input</span> <span class="o">==</span> <span class="n">RELEASE_DOWN</span><span class="p">)</span>
<span class="p">{</span>
<span class="k">if</span> <span class="p">(</span><span class="n">isDucking_</span><span class="p">)</span>
<span class="p">{</span>
<span class="n">isDucking_</span> <span class="o">=</span> <span class="nb">false</span><span class="p">;</span>
<span class="n">setGraphics</span><span class="p">(</span><span class="n">IMAGE_STAND</span><span class="p">);</span>
<span class="p">}</span>
<span class="p">}</span>
<span class="p">}</span>
</pre></div>
<p>下面,如果玩家在跳跃途中按下下键,英雄能够做速降攻击就太酷了:</p>
<div class="codehilite"><pre><span class="kt">void</span> <span class="n">Heroine</span><span class="o">::</span><span class="n">handleInput</span><span class="p">(</span><span class="n">Input</span> <span class="n">input</span><span class="p">)</span>
<span class="p">{</span>
<span class="k">if</span> <span class="p">(</span><span class="n">input</span> <span class="o">==</span> <span class="n">PRESS_B</span><span class="p">)</span>
<span class="p">{</span>
<span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="n">isJumping_</span> <span class="o">&&</span> <span class="o">!</span><span class="n">isDucking_</span><span class="p">)</span>
<span class="p">{</span>
<span class="c1">// Jump...</span>
<span class="p">}</span>
<span class="p">}</span>
<span class="k">else</span> <span class="k">if</span> <span class="p">(</span><span class="n">input</span> <span class="o">==</span> <span class="n">PRESS_DOWN</span><span class="p">)</span>
<span class="p">{</span>
<span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="n">isJumping_</span><span class="p">)</span>
<span class="p">{</span>
<span class="n">isDucking_</span> <span class="o">=</span> <span class="nb">true</span><span class="p">;</span>
<span class="n">setGraphics</span><span class="p">(</span><span class="n">IMAGE_DUCK</span><span class="p">);</span>
<span class="p">}</span>
<span class="k">else</span>
<span class="p">{</span>
<span class="n">isJumping_</span> <span class="o">=</span> <span class="nb">false</span><span class="p">;</span>
<span class="n">setGraphics</span><span class="p">(</span><span class="n">IMAGE_DIVE</span><span class="p">);</span>
<span class="p">}</span>
<span class="p">}</span>
<span class="k">else</span> <span class="k">if</span> <span class="p">(</span><span class="n">input</span> <span class="o">==</span> <span class="n">RELEASE_DOWN</span><span class="p">)</span>
<span class="p">{</span>
<span class="k">if</span> <span class="p">(</span><span class="n">isDucking_</span><span class="p">)</span>
<span class="p">{</span>
<span class="c1">// Stand...</span>
<span class="p">}</span>
<span class="p">}</span>
<span class="p">}</span>
</pre></div>
<p>又是检查漏洞的时间了。找到了吗?</p>
<p>我们检查了跳跃时不能做空气跳,但是速降时没有。又是另一个字段……</p>
<p>我们的实现方法有很明显的<span name="se">错误</span>。
每次我们改动代码时,就破坏了什么东西。
需要增加更多动作——<em>行走</em>都还没有加入呢——但以这种速度,完成之前就会遇到一堆错误。</p>
<aside name="se">
<p>那些你崇拜的看上去永远能写出完美代码的程序员不是超人。
相反,他们有<em>哪种</em>代码易于出错的直觉,他们会避开他们。</p>
<p>复杂分支和可变状态——随时间改变的字段——是两种易错代码,上面的例子覆盖了两者。</p>
</aside>
<h2><a href="#有限状态机前来救援" name="有限状态机前来救援">有限状态机前来救援</a></h2>
<p>在挫败之后,你把桌子一扫而空,只留下纸笔开始画流程图。
你给英雄每件能做的事情都画了一个盒子:站立,跳跃,俯卧,速降。
当她在这些状态中能响应按键时,你从那个盒子画出一个箭头,标记上按键,然后连接到她改变到的状态。</p>
<p><img src="images/state-flowchart.png" alt="A flowchart containing boxes for Standing, Jumping, Diving, and Ducking. Arrows for button presses and releases connect some of the boxes." /></p>
<p>祝贺,你刚刚建好了一个<em>有限状态机</em>。
它来自计算机科学的分支<em>自动理论</em>,那里有很多著名的数据结构,包括著名的图灵机。
FSMs是其中最简单的成员。</p>
<p><span name="adventure">要点</span>是:</p>
<ul>
<li>
<p><strong>你有机器可以处于的固定数量的<em>状态</em>集合。</strong>在我们的例子中,是站立,跳跃,俯卧和速降。</p>
</li>
<li>
<p><strong>机器同时只能在<em>一个</em>状态。</strong>英雄不可能同时处于跳跃和站立。事实上,防止这点是使用FSM的理由之一。</p>
</li>
<li>
<p><strong>一连串的<em>输入</em>或<em>事件</em>被发送给机器。</strong>在我们的例子中,就是按键按下和松开。</p>
</li>
<li>
<p><strong>每个状态都有<em>一系列的转换</em>,转换与输入和另一状态相关。</strong>当输入进来,如果它与当前状态的某个转换匹配,机器转为转换所指的状态。</p>
</li>
</ul>
<p>举个例子,在站立状态时,按下下键转换为俯卧状态。在跳跃时按下下键转换为速降。如果输入在当前状态没有定义转换,输入就被忽视。</p>
<p>在它们的核心,这就是全部了:状态,输入,和转换。
你可以用一张流程图把它画出来。不幸的是,编译器不认我们的画,
所以我们如何<em>实现</em>一个?
GoF的状态模式是一个方法——我们会谈到的——但先从简单的开始。</p>
<aside name="adventure">
<p>对FSMs我最喜欢的类比是那种老式文字冒险游戏,比如Zork。
你有一世界的屋子,彼此通过出口相连。你通过输入像“去北方”的指令探索他们。</p>
<p>这直接指向了状态机:每个屋子都是一个状态。
你进入的屋子是当前状态。每个屋子的出口是它的转换。
导航指令是输入。</p>
</aside>
<h2><a href="#枚举和分支" name="枚举和分支">枚举和分支</a></h2>
<p>我们<code>Heroine</code>类的问题来自将布尔字段不合法的绑在了一起:
<code>isJumping_</code>和<code>isDucking_</code>,不会同时为真。
但有些标识同时只有一个是<code>true</code>,提示是你真正需要的其实是<code>enum</code>。 </p>
<p>在这个例子中 <code>enum</code>就是FSM的状态的集合,所以让我们定义它:</p>
<div class="codehilite"><pre><span class="k">enum</span> <span class="n">State</span>
<span class="p">{</span>
<span class="n">STATE_STANDING</span><span class="p">,</span>
<span class="n">STATE_JUMPING</span><span class="p">,</span>
<span class="n">STATE_DUCKING</span><span class="p">,</span>
<span class="n">STATE_DIVING</span>
<span class="p">};</span>
</pre></div>
<p>不用一堆标识,<code>Heroine</code>只有一个<code>state_</code>状态。
我们同样改变了分支顺序。在前面的代码中,我们先在输入上做分支,<em>然后</em>是状态。
这让代码统一处理某个按键,但一个状态分布到了代码各处。
我们想让它们聚在一起,所以先对状态做分支。这样的话:</p>
<div class="codehilite"><pre><span class="kt">void</span> <span class="n">Heroine</span><span class="o">::</span><span class="n">handleInput</span><span class="p">(</span><span class="n">Input</span> <span class="n">input</span><span class="p">)</span>
<span class="p">{</span>
<span class="k">switch</span> <span class="p">(</span><span class="n">state_</span><span class="p">)</span>
<span class="p">{</span>
<span class="k">case</span> <span class="nl">STATE_STANDING</span><span class="p">:</span>
<span class="k">if</span> <span class="p">(</span><span class="n">input</span> <span class="o">==</span> <span class="n">PRESS_B</span><span class="p">)</span>
<span class="p">{</span>
<span class="n">state_</span> <span class="o">=</span> <span class="n">STATE_JUMPING</span><span class="p">;</span>
<span class="n">yVelocity_</span> <span class="o">=</span> <span class="n">JUMP_VELOCITY</span><span class="p">;</span>
<span class="n">setGraphics</span><span class="p">(</span><span class="n">IMAGE_JUMP</span><span class="p">);</span>
<span class="p">}</span>
<span class="k">else</span> <span class="k">if</span> <span class="p">(</span><span class="n">input</span> <span class="o">==</span> <span class="n">PRESS_DOWN</span><span class="p">)</span>
<span class="p">{</span>
<span class="n">state_</span> <span class="o">=</span> <span class="n">STATE_DUCKING</span><span class="p">;</span>
<span class="n">setGraphics</span><span class="p">(</span><span class="n">IMAGE_DUCK</span><span class="p">);</span>
<span class="p">}</span>
<span class="k">break</span><span class="p">;</span>
<span class="k">case</span> <span class="nl">STATE_JUMPING</span><span class="p">:</span>
<span class="k">if</span> <span class="p">(</span><span class="n">input</span> <span class="o">==</span> <span class="n">PRESS_DOWN</span><span class="p">)</span>
<span class="p">{</span>
<span class="n">state_</span> <span class="o">=</span> <span class="n">STATE_DIVING</span><span class="p">;</span>
<span class="n">setGraphics</span><span class="p">(</span><span class="n">IMAGE_DIVE</span><span class="p">);</span>
<span class="p">}</span>
<span class="k">break</span><span class="p">;</span>
<span class="k">case</span> <span class="nl">STATE_DUCKING</span><span class="p">:</span>
<span class="k">if</span> <span class="p">(</span><span class="n">input</span> <span class="o">==</span> <span class="n">RELEASE_DOWN</span><span class="p">)</span>
<span class="p">{</span>
<span class="n">state_</span> <span class="o">=</span> <span class="n">STATE_STANDING</span><span class="p">;</span>
<span class="n">setGraphics</span><span class="p">(</span><span class="n">IMAGE_STAND</span><span class="p">);</span>
<span class="p">}</span>
<span class="k">break</span><span class="p">;</span>
<span class="p">}</span>
<span class="p">}</span>
</pre></div>
<p>这看起来很琐碎,但是比起前面的代码是一个很大的进步了。
我们仍有条件分支,但简化了<span name="invalid">变化</span>状态,将其变成了字段。
处理同一状态的所有代码都聚到了一起。
这是实现状态机最简单的方法,对于某些使用情况也不错。</p>
<aside name="invalid">
<p>特别的,英雄不再处于<em>不合法</em>状态。
使用布尔标识,很多值的集合是可能但是不合法的。
通过<code>enum</code>,每个值都是合法的。</p>
</aside>
<p>但是,你的问题也许超过了这个解法的控制。
假设我们想增加动作,我们的英雄可以速降一段时间来充能,之后释放一次特殊攻击。
当她速降时,我们需要追踪它的充能时间。</p>
<p>我们为<code>Heroine</code>添加了<code>chargeTime_</code>字段,保存攻击充能的时间大小。
假设我们已经有一个每帧都会调用的<code>update()</code>方法。在那里,我们添加:</p>
<div class="codehilite"><pre><span class="kt">void</span> <span class="n">Heroine</span><span class="o">::</span><span class="n">update</span><span class="p">()</span>
<span class="p">{</span>
<span class="k">if</span> <span class="p">(</span><span class="n">state_</span> <span class="o">==</span> <span class="n">STATE_DUCKING</span><span class="p">)</span>
<span class="p">{</span>
<span class="n">chargeTime_</span><span class="o">++</span><span class="p">;</span>
<span class="k">if</span> <span class="p">(</span><span class="n">chargeTime_</span> <span class="o">></span> <span class="n">MAX_CHARGE</span><span class="p">)</span>
<span class="p">{</span>
<span class="n">superBomb</span><span class="p">();</span>
<span class="p">}</span>
<span class="p">}</span>
<span class="p">}</span>
</pre></div>
<aside name="update">
<p>如果你猜这就是<a href="update-method.html" class="pattern">更新方法</a>模式,你答对了!</p>
</aside>
<p>我们需要在她开始速降的时候重置计时器,所以我们修改<code>handleInput()</code>:</p>
<div class="codehilite"><pre><span class="kt">void</span> <span class="n">Heroine</span><span class="o">::</span><span class="n">handleInput</span><span class="p">(</span><span class="n">Input</span> <span class="n">input</span><span class="p">)</span>
<span class="p">{</span>
<span class="k">switch</span> <span class="p">(</span><span class="n">state_</span><span class="p">)</span>
<span class="p">{</span>
<span class="k">case</span> <span class="nl">STATE_STANDING</span><span class="p">:</span>
<span class="k">if</span> <span class="p">(</span><span class="n">input</span> <span class="o">==</span> <span class="n">PRESS_DOWN</span><span class="p">)</span>
<span class="p">{</span>
<span class="n">state_</span> <span class="o">=</span> <span class="n">STATE_DUCKING</span><span class="p">;</span>
<span class="n">chargeTime_</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span>
<span class="n">setGraphics</span><span class="p">(</span><span class="n">IMAGE_DUCK</span><span class="p">);</span>
<span class="p">}</span>
<span class="c1">// Handle other inputs...</span>
<span class="k">break</span><span class="p">;</span>
<span class="c1">// Other states...</span>
<span class="p">}</span>
<span class="p">}</span>
</pre></div>
<p>总而言之,为了增加这个充能攻击,我们需要修改两个方法,
添加一个<code>chargeTime_</code>字段到<code>Heroine</code>,哪怕它只在速降时有意义。
我们更喜欢的是让所有的代码和数据都待在同一个地方。GoF完成了这个。</p>
<h2><a href="#状态模式" name="状态模式">状态模式</a></h2>
<p>对于那些深深沉浸在面向对象思维方式的人,每个<span name="branch">条件分支</span>都是使用动态分配的机会(在C++中叫做虚方法调用)。
我觉得那就在兔子洞里挖得太深了。有时候一个<code>if</code>就能满足你的需要了。</p>
<aside name="branch">
<p>这里有个历史遗留问题。
原先的面向对象传教徒,比如<em>设计模式</em>的GoF和<em>重构</em>的Martin Fowler都使用Smalltalk。
那里,<code>ifThen:</code>是你使用条件的方法,<code>true</code>和<code>false</code>对象以不同的方式实现了它。</p>
</aside>
<p>但是在我们的例子中,我们抵达了面向对象更好的转折点。
这带领我们走向状态模式。在GoF中这样描述:</p>
<blockquote>
<p>允许对象在当内部状态改变时改变其行为。就好像对象改变了自己的类一样。</p>
</blockquote>
<p>这可没太多帮助。我们的<code>switch</code>也完成了这一点。
它们描述的东西应用在我们的英雄身上是这样的:</p>
<h3><a href="#一个状态接口" name="一个状态接口">一个状态接口</a></h3>
<p>首先,我们为状态定义接口。
状态相关的行为——我们之前用<code>switch</code>的每一处——成为了接口中的虚方法。
对于我们来说,那是<code>handleInput()</code>和<code>update()</code>:</p>
<div class="codehilite"><pre><span class="k">class</span> <span class="nc">HeroineState</span>
<span class="p">{</span>
<span class="k">public</span><span class="o">:</span>
<span class="k">virtual</span> <span class="o">~</span><span class="n">HeroineState</span><span class="p">()</span> <span class="p">{}</span>
<span class="k">virtual</span> <span class="kt">void</span> <span class="n">handleInput</span><span class="p">(</span><span class="n">Heroine</span><span class="o">&</span> <span class="n">heroine</span><span class="p">,</span> <span class="n">Input</span> <span class="n">input</span><span class="p">)</span> <span class="p">{}</span>
<span class="k">virtual</span> <span class="kt">void</span> <span class="n">update</span><span class="p">(</span><span class="n">Heroine</span><span class="o">&</span> <span class="n">heroine</span><span class="p">)</span> <span class="p">{}</span>
<span class="p">};</span>
</pre></div>
<h3><a href="#为每个状态写类" name="为每个状态写类">为每个状态写类</a></h3>
<p>对于每个状态,我们定义一个类实现接口。它的方法定义了英雄在状态的行为。
换言之,从之前的<code>switch</code>中取出每个<code>case</code>,将它们移动到状态类中。举个例子:</p>
<div class="codehilite"><pre><span class="k">class</span> <span class="nc">DuckingState</span> <span class="o">:</span> <span class="k">public</span> <span class="n">HeroineState</span>
<span class="p">{</span>
<span class="k">public</span><span class="o">:</span>
<span class="n">DuckingState</span><span class="p">()</span>
<span class="o">:</span> <span class="n">chargeTime_</span><span class="p">(</span><span class="mi">0</span><span class="p">)</span>
<span class="p">{}</span>
<span class="k">virtual</span> <span class="kt">void</span> <span class="n">handleInput</span><span class="p">(</span><span class="n">Heroine</span><span class="o">&</span> <span class="n">heroine</span><span class="p">,</span> <span class="n">Input</span> <span class="n">input</span><span class="p">)</span> <span class="p">{</span>
<span class="k">if</span> <span class="p">(</span><span class="n">input</span> <span class="o">==</span> <span class="n">RELEASE_DOWN</span><span class="p">)</span>
<span class="p">{</span>
<span class="c1">// Change to standing state...</span>
<span class="n">heroine</span><span class="p">.</span><span class="n">setGraphics</span><span class="p">(</span><span class="n">IMAGE_STAND</span><span class="p">);</span>
<span class="p">}</span>
<span class="p">}</span>
<span class="k">virtual</span> <span class="kt">void</span> <span class="n">update</span><span class="p">(</span><span class="n">Heroine</span><span class="o">&</span> <span class="n">heroine</span><span class="p">)</span> <span class="p">{</span>
<span class="n">chargeTime_</span><span class="o">++</span><span class="p">;</span>
<span class="k">if</span> <span class="p">(</span><span class="n">chargeTime_</span> <span class="o">></span> <span class="n">MAX_CHARGE</span><span class="p">)</span>
<span class="p">{</span>
<span class="n">heroine</span><span class="p">.</span><span class="n">superBomb</span><span class="p">();</span>
<span class="p">}</span>
<span class="p">}</span>
<span class="k">private</span><span class="o">:</span>
<span class="kt">int</span> <span class="n">chargeTime_</span><span class="p">;</span>
<span class="p">};</span>
</pre></div>
<p>注意我们也将<code>chargeTime_</code>移出了<code>Heroine</code>,放到了<code>DuckingState</code>类中。
这很好——那部分数据只在这个状态有用,现在我们的对象模型直接反映了这一点。</p>
<h3><a href="#状态委托" name="状态委托">状态委托</a></h3>
<p>然后,我们给<code>Heroine</code>指向她的当前状态的指针,放弃巨大的<code>switch</code>,转而委托给状态。</p>
<p><span name="delegate"></span></p>
<div class="codehilite"><pre><span class="k">class</span> <span class="nc">Heroine</span>
<span class="p">{</span>
<span class="k">public</span><span class="o">:</span>
<span class="k">virtual</span> <span class="kt">void</span> <span class="n">handleInput</span><span class="p">(</span><span class="n">Input</span> <span class="n">input</span><span class="p">)</span>
<span class="p">{</span>
<span class="n">state_</span><span class="o">-></span><span class="n">handleInput</span><span class="p">(</span><span class="o">*</span><span class="k">this</span><span class="p">,</span> <span class="n">input</span><span class="p">);</span>
<span class="p">}</span>
<span class="k">virtual</span> <span class="kt">void</span> <span class="n">update</span><span class="p">()</span>
<span class="p">{</span>
<span class="n">state_</span><span class="o">-></span><span class="n">update</span><span class="p">(</span><span class="o">*</span><span class="k">this</span><span class="p">);</span>
<span class="p">}</span>
<span class="c1">// Other methods...</span>
<span class="k">private</span><span class="o">:</span>
<span class="n">HeroineState</span><span class="o">*</span> <span class="n">state_</span><span class="p">;</span>
<span class="p">};</span>
</pre></div>
<p>为了“改变状态”,我们只需要将<code>state_</code>声明指向不同的<code>HeroineState</code>对象。
这就是状态模式的整体。</p>
<aside name="delegate">
<p>这看上去有些像<a href="http://en.wikipedia.org/wiki/Strategy_pattern" class="gof-pattern">策略</a>模式和<a href="type-object.html"
class="pattern">类型</a>对象模式。
在三者中,你都有一个主要对象委托给下属。区别在于<em>意图</em>。</p>
<ul>
<li>
<p>在策略模式中,目标是解耦主类和它的部分行为。</p>
</li>
<li>
<p>在类型对象中,目标是通过<em>共享</em>一个对相同类型对象的引用,让一<em>系列</em>对象行为相近。</p>
</li>
<li>
<p>在状态模式中,目标是让主对象通过<em>改变</em>委托的对象,来<em>改变</em>它的行为。</p>
</li>
</ul>
</aside>
<h2><a href="#状态对象在哪里?" name="状态对象在哪里?">状态对象在哪里?</a></h2>
<p>我这里掩盖了一点。为了改变状态,我们需要声明<code>state_</code>指向新的,
但那个对象又是从哪里来呢?
在<code>enum</code>实现中,这都不用过脑子——<code>enum</code>实际上就像数字一样。
但是现在状态是类了,意味着我们需要指向实例。通常这有两种回答:</p>
<h3><a href="#静态状态" name="静态状态">静态状态</a></h3>
<p>如果状态对象没有其他<span name="fn">字段</span>,
那么它存储的唯一数据就是指向虚方法表的指针,这样可以调用它的方法。
在这种情况下,没理由产生多个实例。毕竟每个实例都完全一样。</p>
<aside name="fn">
<p>如果你的状态没有字段,只有<em>一个</em>虚方法,你可以再简化这个模式。
将每一个状态<em>类</em>替换成状态<em>函数</em>——只是一个普通的顶层函数。
然后,主类中的<code>state_</code>字段变成一个简单的函数指针。</p>
</aside>
<p>在那种情况下,你可以用一个<em>静态</em>实例。
哪怕你有一堆FSM同时在同一状态上运行,它们都能接触到<span name="flyweight">同一实例</span>,因为没有状态机特定的部分。</p>
<aside name="flyweight">
<p>这是<a href="flyweight.html" class="gof-pattern">享元</a>模式。</p>
</aside>
<p>在<em>哪里</em>放置静态实例取决于你。找一个合理的地方。
没什么特殊的理由,把我们的放在状态基类中。</p>
<div class="codehilite"><pre><span class="k">class</span> <span class="nc">HeroineState</span>
<span class="p">{</span>
<span class="k">public</span><span class="o">:</span>
<span class="k">static</span> <span class="n">StandingState</span> <span class="n">standing</span><span class="p">;</span>
<span class="k">static</span> <span class="n">DuckingState</span> <span class="n">ducking</span><span class="p">;</span>
<span class="k">static</span> <span class="n">JumpingState</span> <span class="n">jumping</span><span class="p">;</span>
<span class="k">static</span> <span class="n">DivingState</span> <span class="n">diving</span><span class="p">;</span>
<span class="c1">// Other code...</span>
<span class="p">};</span>
</pre></div>
<p>每个静态字段都是游戏状态的一个实例。为了让英雄跳跃,站立状态会做些这样的事情:</p>
<div class="codehilite"><pre><span class="k">if</span> <span class="p">(</span><span class="n">input</span> <span class="o">==</span> <span class="n">PRESS_B</span><span class="p">)</span>
<span class="p">{</span>
<span class="n">heroine</span><span class="p">.</span><span class="n">state_</span> <span class="o">=</span> <span class="o">&</span><span class="n">HeroineState</span><span class="o">::</span><span class="n">jumping</span><span class="p">;</span>
<span class="n">heroine</span><span class="p">.</span><span class="n">setGraphics</span><span class="p">(</span><span class="n">IMAGE_JUMP</span><span class="p">);</span>
<span class="p">}</span>
</pre></div>
<h3><a href="#实例化状态" name="实例化状态">实例化状态</a></h3>
<p>有时,没那么容易。静态状态对俯卧不起作用。
它有一个<code>chargeTime_</code>字段,与正在俯卧的英雄特定相关。
在游戏中如果只有一个英雄那也能工作,但是如果我们要添加双人合作,同时在屏幕上有两个英雄,我们就遇到麻烦了。</p>
<p>在那种情况下,转换时需要<span name="fragment">创建</span>状态对象。
这让每个FSM拥有它自己的状态实例。当然,如果我们分配<em>新</em>状态,
那意味着我们需要释放<em>当前的</em>。
在这里要小心,由于触发变化的代码是当前状态中的方法。我们不想自己删除<code>this</code>。</p>
<p>相反,我们允许<code>HeroineState</code>中的<code>handleInput()</code>随机的返回一个新状态。
如果它那么做了,<code>Heroine</code>会删除旧的,然后换成新的,就像这样:</p>
<div class="codehilite"><pre><span class="kt">void</span> <span class="n">Heroine</span><span class="o">::</span><span class="n">handleInput</span><span class="p">(</span><span class="n">Input</span> <span class="n">input</span><span class="p">)</span>
<span class="p">{</span>
<span class="n">HeroineState</span><span class="o">*</span> <span class="n">state</span> <span class="o">=</span> <span class="n">state_</span><span class="o">-></span><span class="n">handleInput</span><span class="p">(</span><span class="o">*</span><span class="k">this</span><span class="p">,</span> <span class="n">input</span><span class="p">);</span>
<span class="k">if</span> <span class="p">(</span><span class="n">state</span> <span class="o">!=</span> <span class="nb">NULL</span><span class="p">)</span>
<span class="p">{</span>
<span class="k">delete</span> <span class="n">state_</span><span class="p">;</span>
<span class="n">state_</span> <span class="o">=</span> <span class="n">state</span><span class="p">;</span>
<span class="p">}</span>
<span class="p">}</span>
</pre></div>
<p>这样,我们直到从之前的状态返回,才需要删除它。
现在,站立状态可以通过创建一个新实例转换为俯卧状态:</p>
<div class="codehilite"><pre><span class="n">HeroineState</span><span class="o">*</span> <span class="n">StandingState</span><span class="o">::</span><span class="n">handleInput</span><span class="p">(</span><span class="n">Heroine</span><span class="o">&</span> <span class="n">heroine</span><span class="p">,</span>
<span class="n">Input</span> <span class="n">input</span><span class="p">)</span>
<span class="p">{</span>
<span class="k">if</span> <span class="p">(</span><span class="n">input</span> <span class="o">==</span> <span class="n">PRESS_DOWN</span><span class="p">)</span>
<span class="p">{</span>
<span class="c1">// Other code...</span>
<span class="k">return</span> <span class="k">new</span> <span class="n">DuckingState</span><span class="p">();</span>
<span class="p">}</span>
<span class="c1">// Stay in this state.</span>
<span class="k">return</span> <span class="nb">NULL</span><span class="p">;</span>
<span class="p">}</span>
</pre></div>
<p>如果可以,我倾向于使用静态状态,因为它们不会在状态转换时消耗太多的内存和CPU。
但是,对于更多的状态,额,<em>多状态的</em>,这是一条可选的路。</p>
<aside name="fragment">
<p>当你动态分配状态时,你也许会担心碎片。<a href="object-pool.html" class="pattern">对象池</a>模式可以帮上忙。</p>
</aside>
<h2><a href="#入口行为和退出行为" name="入口行为和退出行为">入口行为和退出行为</a></h2>
<p>状态模式的目标是将状态的行为和数据封装到单一类中。
我们部分的完成了这一点,但是还有一些未了之事。</p>
<p>当英雄改变状态,我们也改变她的图片。
现在,那部分代码在她转换<em>前</em>的状态上。
当她从俯卧转为站立,俯卧状态设置了她的图片:</p>
<div class="codehilite"><pre><span class="n">HeroineState</span><span class="o">*</span> <span class="n">DuckingState</span><span class="o">::</span><span class="n">handleInput</span><span class="p">(</span><span class="n">Heroine</span><span class="o">&</span> <span class="n">heroine</span><span class="p">,</span>
<span class="n">Input</span> <span class="n">input</span><span class="p">)</span>
<span class="p">{</span>
<span class="k">if</span> <span class="p">(</span><span class="n">input</span> <span class="o">==</span> <span class="n">RELEASE_DOWN</span><span class="p">)</span>
<span class="p">{</span>
<span class="n">heroine</span><span class="p">.</span><span class="n">setGraphics</span><span class="p">(</span><span class="n">IMAGE_STAND</span><span class="p">);</span>
<span class="k">return</span> <span class="k">new</span> <span class="nf">StandingState</span><span class="p">();</span>
<span class="p">}</span>
<span class="c1">// Other code...</span>
<span class="p">}</span>
</pre></div>
<p>我们想做的是每个状态控制她自己的图像。我们可以给状态一个<em>入口行为</em>来实现:</p>
<div class="codehilite"><pre><span class="k">class</span> <span class="nc">StandingState</span> <span class="o">:</span> <span class="k">public</span> <span class="n">HeroineState</span>
<span class="p">{</span>
<span class="k">public</span><span class="o">:</span>
<span class="k">virtual</span> <span class="kt">void</span> <span class="n">enter</span><span class="p">(</span><span class="n">Heroine</span><span class="o">&</span> <span class="n">heroine</span><span class="p">)</span>
<span class="p">{</span>
<span class="n">heroine</span><span class="p">.</span><span class="n">setGraphics</span><span class="p">(</span><span class="n">IMAGE_STAND</span><span class="p">);</span>
<span class="p">}</span>
<span class="c1">// Other code...</span>
<span class="p">};</span>
</pre></div>
<p>在<code>Heroine</code>中,我们修改了处理状态改变的代码,在新的状态上调用:</p>
<div class="codehilite"><pre><span class="kt">void</span> <span class="n">Heroine</span><span class="o">::</span><span class="n">handleInput</span><span class="p">(</span><span class="n">Input</span> <span class="n">input</span><span class="p">)</span>
<span class="p">{</span>
<span class="n">HeroineState</span><span class="o">*</span> <span class="n">state</span> <span class="o">=</span> <span class="n">state_</span><span class="o">-></span><span class="n">handleInput</span><span class="p">(</span><span class="o">*</span><span class="k">this</span><span class="p">,</span> <span class="n">input</span><span class="p">);</span>
<span class="k">if</span> <span class="p">(</span><span class="n">state</span> <span class="o">!=</span> <span class="nb">NULL</span><span class="p">)</span>
<span class="p">{</span>
<span class="k">delete</span> <span class="n">state_</span><span class="p">;</span>
<span class="n">state_</span> <span class="o">=</span> <span class="n">state</span><span class="p">;</span>
<span class="c1">// Call the enter action on the new state.</span>
<span class="n">state_</span><span class="o">-></span><span class="n">enter</span><span class="p">(</span><span class="o">*</span><span class="k">this</span><span class="p">);</span>
<span class="p">}</span>
<span class="p">}</span>
</pre></div>
<p>这让我们将俯卧代码简化为:</p>
<div class="codehilite"><pre><span class="n">HeroineState</span><span class="o">*</span> <span class="n">DuckingState</span><span class="o">::</span><span class="n">handleInput</span><span class="p">(</span><span class="n">Heroine</span><span class="o">&</span> <span class="n">heroine</span><span class="p">,</span>
<span class="n">Input</span> <span class="n">input</span><span class="p">)</span>
<span class="p">{</span>
<span class="k">if</span> <span class="p">(</span><span class="n">input</span> <span class="o">==</span> <span class="n">RELEASE_DOWN</span><span class="p">)</span>
<span class="p">{</span>
<span class="k">return</span> <span class="k">new</span> <span class="n">StandingState</span><span class="p">();</span>
<span class="p">}</span>
<span class="c1">// Other code...</span>
<span class="p">}</span>
</pre></div>
<p>它做的所有事情就是转换状态为站立,站立状态控制图形。
现在我们的状态真的封装了。
关于入口行为的好事就是,当你进入状态时,不必关心你是从哪个状态转换<em>来的</em>。</p>
<p>大多数现实世界的状态图都有从多个转换进入同一个状态。
举个例子,英雄在它跳跃或速降后进入站立状态。
这意味着我们在转换发生的最后复制了相同的代码。
入口行为给了一个好地方来巩固那一点。</p>
<p>我们能,当然,扩展并支持<em>离开行为</em>。
这是在我们<em>离开</em>现有状态,转换到新状态之前调用的方法。</p>
<h2><a href="#有什么收获?" name="有什么收获?">有什么收获?</a></h2>
<p>我花了这么长时间向您推销FSMs,现在我要从你脚下抽走地毯了。
我到现在讲的都是真的,FSM能很好解决一些问题。但它们最大的优点也是它们最大的缺点。</p>
<p>状态机通过使用<span name="turing">有约束的</span>结构来理清杂乱的代码。
你得到的是固定状态集合,单一的当前状态,和一些硬编码的转换。</p>
<aside name="turing">
<p>一个有限状态机甚至不是<em>图灵完全的</em>。
自动理论用一系列抽象模型描述计算,每种都比之前的复杂。
<em>图灵机</em>是其中最要表现力的模型之一。</p>
<p>“图灵完全”意味着一个系统(通常是编程语言)足以在内部实现一个图灵机,
也就意味着,在某种程度上,所有的图灵完全机有同样的表现力。
FSMs不够灵活,不是它们中的一员。</p>
</aside>
<p>如果你需要为更复杂的东西,比如游戏AI使用状态机,你的脸会撞到这个模型的限制上。
感谢上天,我们的前辈找到了一些方法来闪避这些障碍。我会浏览一些来结束这一章。</p>
<h2><a href="#并发状态机" name="并发状态机">并发状态机</a></h2>
<p>我们决定给英雄持枪的能力。
当她拿着枪的时候,她还是能做她之前的任何事情:跑,跳,速降,等等。
但是她也需要在做这些的同时能开火。</p>
<p>如果我们绑定在FSM上,我们需要<em>翻倍</em>现有状态。
对于每个现有状态,我们需要另一个她武装状态:站立,持枪站立,跳跃,持枪跳跃,
你知道我的意思。</p>
<p>多加几种武器,状态就会指数爆炸。
这不但有大量的状态,这也是大量的冗余:武装和非武装状态集合是完全一致的,只是多了一点负责射击的代码。</p>
<p>问题在于我们将两种状态<span name="combination">绑定</span> ——她<em>做的</em>和她<em>携带的</em>——到了一个状态机上。
为了处理所有可能的结合,我们需要为每一<em>对</em>写一个状态。
修复方法很明显:使用两个单独的状态机。</p>
<aside name="combination">
<p>如果她在做什么有<em>n</em>个状态,而她携带了什么有<em>m</em>个状态,要塞到一个状态机中,
我们需要<em>n × m</em>个状态。使用两个状态机,就只有<em>n + m</em>个。</p>
</aside>
<p>我们保留原来记录她在做什么的状态机,不再管它。
然后定义她携带了什么的单独状态机。
<code>Heroine</code>将会有<em>两个</em>“状态”引用,每个对应一个状态机,就像这样:</p>
<p><span name="equip-state"></span></p>
<div class="codehilite"><pre><span class="k">class</span> <span class="nc">Heroine</span>
<span class="p">{</span>
<span class="c1">// Other code...</span>
<span class="k">private</span><span class="o">:</span>
<span class="n">HeroineState</span><span class="o">*</span> <span class="n">state_</span><span class="p">;</span>
<span class="n">HeroineState</span><span class="o">*</span> <span class="n">equipment_</span><span class="p">;</span>
<span class="p">};</span>
</pre></div>
<aside name="equip-state">
<p>为了演示目标,她的装备也使用了状态模式。
在实践中,由于它只有两个状态,一个布尔标识就够了。</p>
</aside>
<p>当英雄把输入委托给了状态,她两个都会委托:</p>
<p><span name="consume"></span></p>
<div class="codehilite"><pre><span class="kt">void</span> <span class="n">Heroine</span><span class="o">::</span><span class="n">handleInput</span><span class="p">(</span><span class="n">Input</span> <span class="n">input</span><span class="p">)</span>
<span class="p">{</span>
<span class="n">state_</span><span class="o">-></span><span class="n">handleInput</span><span class="p">(</span><span class="o">*</span><span class="k">this</span><span class="p">,</span> <span class="n">input</span><span class="p">);</span>
<span class="n">equipment_</span><span class="o">-></span><span class="n">handleInput</span><span class="p">(</span><span class="o">*</span><span class="k">this</span><span class="p">,</span> <span class="n">input</span><span class="p">);</span>
<span class="p">}</span>
</pre></div>
<aside name="consume">
<p>有更多特性的系统也许能让状态机<em>销毁</em>输入,这样其他状态机就不会收到了。
这能阻止两个状态机响应同一输入。</p>
</aside>
<p>每个状态机之后都能响应输入,发生行为,独立于其它机器改变状态。
当两个状态集合几乎没有联系的时候,它的结果不错。</p>
<p>在实践中,你会发现有些时候状态在交互。
举个例子,也许她在跳跃时不能开火,或者她在武装时不能速降攻击。
为了完成这个,你也许会在状态的代码中做一些粗糙的<code>if</code>测试<em>其他</em>状态来协同,
这不是最优雅的解决方案,但这可以搞定工作。</p>
<h2><a href="#分层状态机" name="分层状态机">分层状态机</a></h2>
<p>再充实我们英雄的行为,她可能会有更多相似的状态。
举个例子,她也许有站立,行走,奔跑,和滑铲状态。在每一个中,按B跳,按下蹲。</p>
<p>通过简单的状态机实现,我们在每个状态中的都重复了代码。
如果我们能够实现一次,在多个状态间重用就好了。</p>
<p>如果这是面向对象的代码而不是状态机,在这些状态间分享代码的方式是通过<span name="inheritance">继承</span>。
我们可以为“在地面上”定义一个类处理跳跃和速降。
站立,行走,奔跑和滑铲都会从它继承,然后增加它们的附加行为。</p>
<aside name="inheritance">
<p>它的影响有好有坏。继承是一种有力的代码重用工具,但也在两块代码间建立了非常强的耦合。这是重锤,所以小心使用。</p>
</aside>
<p>你会发现,这是一个被称为<em>分层状态机</em>的通用结构。
状态可以有<em>父状态</em>(这让它变为<em>子状态</em>)。
当一个事件进来,如果子状态没有处理,它就会交给链上的父状态。
换言之,它像重载的继承方法那样运作。</p>
<p>事实上,如果我们使用状态模式实现FSM,我们可以使用继承来实现层次。
定义一个基类作为父状态:</p>
<div class="codehilite"><pre><span class="k">class</span> <span class="nc">OnGroundState</span> <span class="o">:</span> <span class="k">public</span> <span class="n">HeroineState</span>
<span class="p">{</span>
<span class="k">public</span><span class="o">:</span>
<span class="k">virtual</span> <span class="kt">void</span> <span class="n">handleInput</span><span class="p">(</span><span class="n">Heroine</span><span class="o">&</span> <span class="n">heroine</span><span class="p">,</span> <span class="n">Input</span> <span class="n">input</span><span class="p">)</span>
<span class="p">{</span>
<span class="k">if</span> <span class="p">(</span><span class="n">input</span> <span class="o">==</span> <span class="n">PRESS_B</span><span class="p">)</span>
<span class="p">{</span>
<span class="c1">// Jump...</span>
<span class="p">}</span>
<span class="k">else</span> <span class="k">if</span> <span class="p">(</span><span class="n">input</span> <span class="o">==</span> <span class="n">PRESS_DOWN</span><span class="p">)</span>
<span class="p">{</span>
<span class="c1">// Duck...</span>
<span class="p">}</span>
<span class="p">}</span>
<span class="p">};</span>
</pre></div>
<p>每个子状态继承它:</p>
<div class="codehilite"><pre><span class="k">class</span> <span class="nc">DuckingState</span> <span class="o">:</span> <span class="k">public</span> <span class="n">OnGroundState</span>
<span class="p">{</span>
<span class="k">public</span><span class="o">:</span>
<span class="k">virtual</span> <span class="kt">void</span> <span class="n">handleInput</span><span class="p">(</span><span class="n">Heroine</span><span class="o">&</span> <span class="n">heroine</span><span class="p">,</span> <span class="n">Input</span> <span class="n">input</span><span class="p">)</span>
<span class="p">{</span>
<span class="k">if</span> <span class="p">(</span><span class="n">input</span> <span class="o">==</span> <span class="n">RELEASE_DOWN</span><span class="p">)</span>
<span class="p">{</span>
<span class="c1">// Stand up...</span>
<span class="p">}</span>
<span class="k">else</span>
<span class="p">{</span>
<span class="c1">// Didn't handle input, so walk up hierarchy.</span>
<span class="n">OnGroundState</span><span class="o">::</span><span class="n">handleInput</span><span class="p">(</span><span class="n">heroine</span><span class="p">,</span> <span class="n">input</span><span class="p">);</span>
<span class="p">}</span>
<span class="p">}</span>
<span class="p">};</span>
</pre></div>
<p>这当然不是唯一实现层次的方法。
如果你没有使用GoF的状态模式,这不会有用。
相反,你可以显式的使用状态<em>栈</em>而不是单一状态来表示当前状态的父状态链。</p>
<p>当前状态是栈顶的状态,在他下面是它的直接父状态,
然后是<em>那个</em>状态的父状态,一直下去。
当你发现了一些特定状态的行为,你从栈的顶端开始,
然后向下移动直到某一个状态处理了它。(如果没有,你忽视它。)</p>
<h2><a href="#下推自动机" name="下推自动机">下推自动机</a></h2>
<p>还有一种有限自动机的扩展也用了状态栈。
令人困惑的是,栈表示的是完全不同的事物,被用作解决不同的问题。</p>
<p>问题是有限自动机没有任何<em>历史</em>的概念。
你知道你<em>在</em>什么状态中,但是不记得你<em>曾在</em>什么状态。
没有回到上一状态的简单办法。</p>
<p>这里是个例子:早先,我们让无畏英雄武装到了牙齿。
当她开火时,我们需要新状态播放开火动画,产生子弹和视觉效果。
所以我们拼凑了一个<code>FiringState</code>,让<span name="shared">所有状态</span>都能在按下开火按钮时跳转过去。</p>
<aside name="shared">
<p>由于这个行为在多个状态间重复,也许是用层次状态机重用代码的好地方。</p>
</aside>
<p>有技巧的部分是她开火<em>后</em>转换到的状态。
她可以在站立,奔跑,跳跃,速降时开火。
当开火序列完成了,应该转换为她之前的状态。</p>
<p>如果我们固执于纯粹的FSM,我们就已经忘了她之前所处的状态。
为了继续追踪,我们定义了很多几乎完全一样的类——站立开火,跑步开火,跳跃开火,诸如此类——每个都有硬编码的转换,用来回到刚刚在做的事。</p>
<p>我们真正喜欢的是,它会<em>存储</em>开火前所处的状态,然后之后能<em>回想</em>起来。
又一次,自动理论来帮忙了,相关的数据结构被称为<a href="http://en.wikipedia.org/wiki/Pushdown_automaton"><em>下推自动机</em></a>。</p>
<p>有限自动机有<em>一个</em>指针指向状态,下推自动机有<em>一栈</em>指针。
在FSM中,转换到新状态<em>代替</em>了之前的那个。
下推状态机不仅能完成那个,还能给你两个额外操作:</p>
<ol>
<li>
<p>你可以将新状态<em>推入</em>栈中。“当前的”状态总是在栈顶,所以这会转到新状态。但它让之前的状态待在栈中而不是销毁它。</p>
</li>
<li>
<p>你可以<em>弹出</em>最上面的状态。这个状态会被销毁,它下面状态成为新状态。</p>
</li>
</ol>
<p><img src="images/state-pushdown.png" alt="The stack for a pushdown automaton. First it just contains a Standing state. A Firing state is pushed on top, then popped back off when done." /></p>
<p>这正是我们开火时需要的。我们创建<em>单一</em>开火状态。
当开火按钮在其他状态按下时,我们<em>推入</em>开火状态。
当开火动画结束,我们<em>弹出</em>开火状态,然后下推自动机自动转回我们之前的状态。</p>
<h2><a href="#所以它们有多有用呢?" name="所以它们有多有用呢?">所以它们有多有用呢?</a></h2>
<p>即使状态机有这些常见的扩展,它们还是很受限制。
这让今日游戏AI移向了更加激动人心的领域,比如<em><a href="http://web.archive.org/web/20140402204854/http://www.altdevblogaday.com/2011/02/24/introduction-to-behavior-trees/">行为树</a></em>和<em><a href="http://web.media.mit.edu/~jorkin/goap.html">计划系统</a></em>。
如果你关注复杂AI,这一整章只是勾起了你的食欲。
你需要阅读其他书来满足它。</p>
<p>这不意味着有限自动机,下推自动机,和其他简单的系统无用。
它们是特定问题的好工具。有限自动机在以下情况有用:</p>
<ul>
<li>
<p>你有个实体,它的行为基于一些内在状态。</p>
</li>
<li>
<p>状态可以被严格的分割为相对较少的不相干项目。</p>
</li>
<li>
<p>实体响应一系列输入或事件。</p>
</li>
</ul>
<p>在游戏中,它们因在AI中使用而闻名,但是它也常用于其他实现,
比如处理玩家输入,导航菜单界面,分析文字,网络协议以及其他异步行为。</p>
<nav>
<span class="prev">← <a href="singleton.html">Previous Chapter</a></span>
<span class="next"><a href="sequencing-patterns.html">Next Chapter</a> →</span>
<span class="toc">≡ <a href="/">The Book</a></span>
</nav>
</div>
</div>
<footer>© 2009-2015 Robert Nystrom</footer>
</body>
</html>