-
Notifications
You must be signed in to change notification settings - Fork 154
/
Copy path4.5.html
1497 lines (974 loc) · 150 KB
/
4.5.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
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
<!DOCTYPE HTML>
<html lang="" >
<head>
<meta charset="UTF-8">
<meta content="text/html; charset=utf-8" http-equiv="Content-Type">
<title>编写不朽的传奇 · GitBook</title>
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="description" content="">
<meta name="generator" content="GitBook 3.2.3">
<link rel="stylesheet" href="gitbook/style.css">
<link rel="stylesheet" href="gitbook/gitbook-plugin-intopic-toc/style.css">
<link rel="stylesheet" href="gitbook/gitbook-plugin-page-footer-ex/style/plugin.css">
<link rel="stylesheet" href="gitbook/gitbook-plugin-callouts/plugin.css">
<link rel="stylesheet" href="gitbook/gitbook-plugin-highlight/website.css">
<link rel="stylesheet" href="gitbook/gitbook-plugin-search/search.css">
<link rel="stylesheet" href="gitbook/gitbook-plugin-fontsettings/website.css">
<link rel="stylesheet" href="gitbook/gitbook-plugin-theme-comscore/test.css">
<link rel="stylesheet" href="styles.css">
<meta name="HandheldFriendly" content="true"/>
<meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=no">
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="black">
<link rel="apple-touch-icon-precomposed" sizes="152x152" href="gitbook/images/apple-touch-icon-precomposed-152.png">
<link rel="shortcut icon" href="gitbook/images/favicon.ico" type="image/x-icon">
<link rel="next" href="blank.html" />
<link rel="prev" href="4.4.html" />
</head>
<body>
<div class="book">
<div class="book-summary">
<div id="book-search-input" role="search">
<input type="text" placeholder="Type to search" />
</div>
<nav role="navigation">
<ul class="summary">
<li class="chapter " data-level="1.1" data-path="index.html">
<a href="index.html">
Introduction
</a>
</li>
<li class="chapter " data-level="1.2" data-path="PA0.html">
<a href="PA0.html">
PA0 - 世界诞生的前夜: 开发环境配置
</a>
<ul class="articles">
<li class="chapter " data-level="1.2.1" data-path="0.1.html">
<a href="0.1.html">
Installing GNU/Linux
</a>
</li>
<li class="chapter " data-level="1.2.2" data-path="0.2.html">
<a href="0.2.html">
First Exploration with GNU/Linux
</a>
</li>
<li class="chapter " data-level="1.2.3" data-path="0.3.html">
<a href="0.3.html">
Installing Tools
</a>
</li>
<li class="chapter " data-level="1.2.4" data-path="0.4.html">
<a href="0.4.html">
Configuring vim
</a>
</li>
<li class="chapter " data-level="1.2.5" data-path="0.5.html">
<a href="0.5.html">
More Exploration
</a>
</li>
<li class="chapter " data-level="1.2.6" data-path="0.6.html">
<a href="0.6.html">
Getting Source Code for PAs
</a>
</li>
</ul>
</li>
<li class="chapter " data-level="1.3" data-path="PA1.html">
<a href="PA1.html">
PA1 - 开天辟地的篇章: 最简单的计算机
</a>
<ul class="articles">
<li class="chapter " data-level="1.3.1" data-path="1.1.html">
<a href="1.1.html">
在开始愉快的PA之旅之前
</a>
</li>
<li class="chapter " data-level="1.3.2" data-path="1.2.html">
<a href="1.2.html">
开天辟地的篇章
</a>
</li>
<li class="chapter " data-level="1.3.3" data-path="1.3.html">
<a href="1.3.html">
RTFSC
</a>
</li>
<li class="chapter " data-level="1.3.4" data-path="1.4.html">
<a href="1.4.html">
基础设施
</a>
</li>
<li class="chapter " data-level="1.3.5" data-path="1.5.html">
<a href="1.5.html">
表达式求值
</a>
</li>
<li class="chapter " data-level="1.3.6" data-path="1.6.html">
<a href="1.6.html">
监视点
</a>
</li>
<li class="chapter " data-level="1.3.7" data-path="1.7.html">
<a href="1.7.html">
如何阅读手册
</a>
</li>
</ul>
</li>
<li class="chapter " data-level="1.4" data-path="PA2.html">
<a href="PA2.html">
PA2 - 简单复杂的机器: 冯诺依曼计算机系统
</a>
<ul class="articles">
<li class="chapter " data-level="1.4.1" data-path="2.1.html">
<a href="2.1.html">
不停计算的机器
</a>
</li>
<li class="chapter " data-level="1.4.2" data-path="2.2.html">
<a href="2.2.html">
RTFSC(2)
</a>
</li>
<li class="chapter " data-level="1.4.3" data-path="2.3.html">
<a href="2.3.html">
程序, 运行时环境与AM
</a>
</li>
<li class="chapter " data-level="1.4.4" data-path="2.4.html">
<a href="2.4.html">
基础设施(2)
</a>
</li>
<li class="chapter " data-level="1.4.5" data-path="2.5.html">
<a href="2.5.html">
输入输出
</a>
</li>
</ul>
</li>
<li class="chapter " data-level="1.5" data-path="PA3.html">
<a href="PA3.html">
PA3 - 穿越时空的旅程: 批处理系统
</a>
<ul class="articles">
<li class="chapter " data-level="1.5.1" data-path="3.1.html">
<a href="3.1.html">
最简单的操作系统
</a>
</li>
<li class="chapter " data-level="1.5.2" data-path="3.2.html">
<a href="3.2.html">
穿越时空的旅程
</a>
</li>
<li class="chapter " data-level="1.5.3" data-path="3.3.html">
<a href="3.3.html">
用户程序和系统调用
</a>
</li>
<li class="chapter " data-level="1.5.4" data-path="3.4.html">
<a href="3.4.html">
文件系统
</a>
</li>
<li class="chapter " data-level="1.5.5" data-path="3.5.html">
<a href="3.5.html">
精彩纷呈的应用程序
</a>
</li>
</ul>
</li>
<li class="chapter " data-level="1.6" data-path="PA4.html">
<a href="PA4.html">
PA4 - 虚实交错的魔法: 分时多任务
</a>
<ul class="articles">
<li class="chapter " data-level="1.6.1" data-path="4.1.html">
<a href="4.1.html">
多道程序
</a>
</li>
<li class="chapter " data-level="1.6.2" data-path="4.2.html">
<a href="4.2.html">
虚实交错的魔法
</a>
</li>
<li class="chapter " data-level="1.6.3" data-path="4.3.html">
<a href="4.3.html">
超越容量的界限
</a>
</li>
<li class="chapter " data-level="1.6.4" data-path="4.4.html">
<a href="4.4.html">
来自外部的声音
</a>
</li>
<li class="chapter active" data-level="1.6.5" data-path="4.5.html">
<a href="4.5.html">
编写不朽的传奇
</a>
</li>
</ul>
</li>
<li class="chapter " data-level="1.7" data-path="blank.html">
<a href="blank.html">
杂项
</a>
<ul class="articles">
<li class="chapter " data-level="1.7.1" data-path="FAQ.html">
<a href="FAQ.html">
常见问题(FAQ)
</a>
</li>
<li class="chapter " data-level="1.7.2" data-path="why.html">
<a href="why.html">
为什么要学习计算机系统基础
</a>
</li>
<li class="chapter " data-level="1.7.3" data-path="linux.html">
<a href="linux.html">
Linux入门教程
</a>
</li>
<li class="chapter " data-level="1.7.4" data-path="man.html">
<a href="man.html">
man入门教程
</a>
</li>
<li class="chapter " data-level="1.7.5" data-path="git.html">
<a href="git.html">
git入门教程
</a>
</li>
<li class="chapter " data-level="1.7.6" data-path="nemu-isa-api.html">
<a href="nemu-isa-api.html">
NEMU ISA相关API说明文档
</a>
</li>
<li class="chapter " data-level="1.7.7" data-path="changelog.html">
<a href="changelog.html">
更新日志
</a>
</li>
<li class="chapter " data-level="1.7.8" data-path="i386-intro.html">
<a href="i386-intro.html">
i386手册指令集阅读指南
</a>
</li>
<li class="chapter " data-level="1.7.9" data-path="exec.html">
<a href="exec.html">
指令执行例子
</a>
</li>
</ul>
</li>
<li class="divider"></li>
<li>
<a href="https://www.gitbook.com" target="blank" class="gitbook-link">
Published with GitBook
</a>
</li>
</ul>
</nav>
</div>
<div class="book-body">
<div class="body-inner">
<div class="book-header" role="navigation">
<!-- Title -->
<h1>
<i class="fa fa-circle-o-notch fa-spin"></i>
<a href="." >编写不朽的传奇</a>
</h1>
</div>
<div class="page-wrapper" tabindex="-1" role="main">
<div class="page-inner">
<div id="book-search-results">
<div class="search-noresults">
<section class="normal markdown-section">
<h2 id="编写不朽的传奇">编写不朽的传奇</h2>
<p>在PA的最后, 我们来做些好玩的事情.</p>
<h3 id="展示你的计算机系统">展示你的计算机系统</h3>
<p>目前Nanos-lite中最多可以运行4个进程, 我们可以把这4个进程全部用满.
具体地, 我们可以加载仙剑奇侠传, Flappy Bird, NSlider和hello程序,
然后通过一个变量<code>fg_pcb</code>来维护当前的前台程序, 让前台程序和hello程序分时运行.
具体地, 我们可以在Nanos-lite的<code>events_read()</code>函数中让<code>F1</code>, <code>F2</code>, <code>F3</code>这3个按键来和3个前台程序绑定,
例如, 一开始是仙剑奇侠传和hello程序分时运行, 按下<code>F2</code>之后, 就变成Flappy Bird和hello程序分时运行.</p>
<p>特别地, 你还可以加载3份开机菜单程序或者NTerm, 它们可以在VME的支持下自由切换,
就好像在同时玩3个红白机游戏一样, 很酷!</p>
<div class="panel panel-warning"><div class="panel-heading"><h5 class="panel-title" id="展示你的计算机系统"><i class="fa fa-edit"></i> 展示你的计算机系统</h5></div><div class="panel-body"><p>添加前台程序及其切换功能, 展示你亲手创造的计算机系统.</p></div></div>
<h3 id="运行onscripter模拟器">运行ONScripter模拟器</h3>
<div class="panel panel-success"><div class="panel-heading"><h5 class="panel-title" id="在新分支中尝试"><i class="fa fa-lightbulb-o"></i> 在新分支中尝试</h5></div><div class="panel-body"><p>以下均为选做内容, 且可能会对必做部分造成不兼容的改动,
如果你想要体验以下内容, 请创建一个新分支, 并在新分支中进行代码的修改.</p></div></div>
<p>ONScripter是一个开源的NScripter脚本引擎, 可以用来运行用相应脚本编写的文字冒险游戏.
我们从<a href="https://github.com/LasmGratel/onscripter-jh" target="_blank">这个repo</a>克隆了项目, 并将其移植到Navy中.
在<code>navy-apps/apps/onscripter/</code>目录下运行<code>make init</code>, 将会从github上克隆移植后的项目.
这个移植后的项目仍然可以在Linux native上运行:
在<code>repo/</code>目录下执行<code>make</code>, 将ONScripter编译到Linux native,
但你可能还需要安装一些库, 请STFW自行解决;
然后下载一些较小的ONS游戏进行测试(例如约100MB的"星之梦", 请自行STFW),
对游戏进行解压缩后, 运行如下命令启动游戏:</p>
<pre><code>./repo/build/onscripter -r 游戏解压路径
</code></pre><p>由于是在Linux native上运行, 你可以使用鼠标进行游戏,
也可以通过方向键移动光标, 通过<code>ENTER</code>/<code>ESC</code>键进行确定/取消的操作.</p>
<p><img src="images/onscripter.png" alt="onscripter"></p>
<p>为了将ONScripter移植到Navy, 我们首先需要了解ONScripter依赖于哪些库.
你可以通过阅读<code>navy-apps/apps/onscripter/repo/Makefile</code>中的<code>LDFLAGS</code>变量来了解依赖的库:</p>
<ul>
<li><code>SDL</code> - 在Navy中, 我们通过libminiSDL来提供功能兼容的API.</li>
<li><code>SDL_ttf</code> - 这是一个truetype字体库,
在Navy中, 我们通过libminiSDL和<a href="https://github.com/nothings/stb" target="_blank">stb项目</a>中的truetype字体解析器,
提供和<code>SDL_ttf</code>兼容的部分API, 主要用于实现truetype字体的光栅化.</li>
<li><code>SDL_image</code> - 这是一个图像库,
在Navy中, 我们通过libminiSDL和<a href="https://github.com/nothings/stb" target="_blank">stb项目</a>中的图像解码器,
提供和<code>SDL_image</code>兼容的部分API, 主要用于实现JPG, PNG, BMP等图像格式的解码.</li>
<li><code>SDL_mixer</code> - 这是一个音频混声库,
在Navy中, 我们通过libminiSDL和libvorbis,
提供和<code>SDL_mixer</code>兼容的部分API, 主要用于实现多通道的OGG音频混声.</li>
<li><code>bz2</code> - 这是一个压缩库,
在Navy中, 我们去掉了ONScripter中压缩文件访问相关的功能,
因此编译到Navy时, ONScripter的运行不依赖于压缩库.</li>
<li><code>m</code> - 这是glibc的数学库, 在Navy中通过libfixedptc提供一些实数的运算.</li>
</ul>
<div class="panel panel-info"><div class="panel-heading"><h5 class="panel-title" id="缓存和字体光栅化"><i class="fa fa-question-circle"></i> 缓存和字体光栅化</h5></div><div class="panel-body"><p><code>navy-apps/libs/libSDL_ttf/src/ttf.c</code>中的<code>TTF_RenderGlyph_Shaded()</code>
函数用于对字体库中的一个字符进行光栅化, 生成相应的像素信息和调色板.
代码在生成调色板的时候添加了一个小小的缓存, 你知道它是如何工作的吗?
为什么它可以提升性能? (虽然提升的幅度不是特别明显)</p></div></div>
<p>需要说明的是, 由于Navy中上述库的一些缺陷, 我们并不能在Navy上完美地运行所有的ONS游戏.
具体地:</p>
<ul>
<li><a href="https://github.com/nothings/stb" target="_blank">stb项目</a>中的truetype字体解析器使用的光栅化算法可能无法很好地对部分truetype字体进行光栅化,
导致文字间距较小, 显示效果不如Linux本地的<code>SDL_ttf</code>库</li>
<li>Navy中的库只支持对OGG音频的解码, 因此不支持需要播放其它格式音频(如MP3)的ONS游戏</li>
</ul>
<h4 id="补充sdl相关库">补充SDL相关库</h4>
<p>我们先考虑在Navy native上运行ONScripter.
由于ONS游戏一般需要更大的屏幕才能运行, 我们需要在
<code>navy-apps/libs/libos/src/native.cpp</code>中定义宏<code>MODE_800x600</code>.
这样以后, 我们就可以尝试在Navy native上运行ONScripter了:</p>
<pre><code>cd navy-apps/apps/onscripter
make ISA=native run mainargs="-r 游戏解压路径"
</code></pre><p>如果你看到终端输出类似以下的信息, 就说明ONScripter已经成功地读取到游戏信息了:</p>
<pre><code>ONScripter-Jh version 0.7.6 (20181218, 2.96)
Display: 800 x 600 (32 bpp)
Audio: -937182692 Hz 0 bit stereo
</code></pre><p>其中显示分辨率和具体的ONS游戏相关, 音频信息的输出是一些垃圾信息, 这是因为我们还没有实现混声库.
接下来ONScripter可能会在miniSDL库中触发assertion fail, 或者报告无法打开文件而退出.</p>
<p>为了让ONScripter成功运行, 我们需要对miniSDL库的实现进行补充.
需要补充的功能包括(具体细节请务必RTFM):</p>
<ul>
<li><code>timer.c</code>: 添加计时器的注册和移除功能.
其中回调函数的实现可以参考PA3中填充音频数据的实现方式.</li>
</ul>
<pre><code class="lang-c"><span class="hljs-comment">// 注册一个计时器, 经过interval毫秒后调用callback</span>
<span class="hljs-function">SDL_TimerID <span class="hljs-title">SDL_AddTimer</span><span class="hljs-params">(uint32_t interval, SDL_NewTimerCallback callback, <span class="hljs-keyword">void</span> *param)</span></span>;
<span class="hljs-comment">// 移除一个已经注册的计时器</span>
<span class="hljs-function"><span class="hljs-keyword">int</span> <span class="hljs-title">SDL_RemoveTimer</span><span class="hljs-params">(SDL_TimerID id)</span>
</span></code></pre>
<ul>
<li><code>event.c</code>: 添加事件的入队和检查.
SDL支持用户添加自定义的事件, 因此我们需要通过一个队列来缓冲未处理的事件.
实现事件队列后, 我们可以把从NDL中读到的按键事件也通过<code>SDL_PushEvent()</code>添加到事件队列中,
这样就可以对各种事件进行统一处理了.</li>
</ul>
<pre><code class="lang-c"><span class="hljs-comment">// 往事件队列中添加一个事件ev</span>
<span class="hljs-function"><span class="hljs-keyword">int</span> <span class="hljs-title">SDL_PushEvent</span><span class="hljs-params">(SDL_Event *ev)</span></span>;
<span class="hljs-comment">// 对事件队列中前numevents个类型与mask匹配的事件进行action操作</span>
<span class="hljs-comment">// 关于mask的含义, 可以阅读`SDL_EVENTMASK()`这个宏</span>
<span class="hljs-comment">// 在ONScripter中, numevents总是1且action总是SDL_GETEVENT, 这样可以简化实现</span>
<span class="hljs-function"><span class="hljs-keyword">int</span> <span class="hljs-title">SDL_PeepEvents</span><span class="hljs-params">(SDL_Event *ev, <span class="hljs-keyword">int</span> numevents, <span class="hljs-keyword">int</span> action, uint32_t mask)</span></span>;
</code></pre>
<ul>
<li><code>file.c</code>: SDL库向用户程序提供了<code>SDL_RWread()</code>, <code>SDL_RWwrite()</code>等统一的接口,
并通过函数指针对各种平台的文件操作进行抽象, 使得用户无需关心具体文件操作的细节.
这样的思想和我们在PA3中介绍的VFS非常相似, 具体可以参考<a href="https://wiki.libsdl.org/SDL_RWops" target="_blank">这里</a>.
在ONScripter中, 我们只会用到普通文件和"内存文件", 你需要分别为它们实现相应的文件操作.
其中, "内存文件"是指把内存中的一段空间看作字节序列, 并通过文件操作来访问它.
注意这些操作的语义和返回值可能与libc中对应的库函数有所不同, 请仔细RTFM.</li>
</ul>
<div class="panel panel-info"><div class="panel-heading"><h5 class="panel-title" id="glibc对内存文件的支持"><i class="fa fa-comment-o"></i> glibc对内存文件的支持</h5></div><div class="panel-body"><p>事实上, glibc提供了一个<code>fmemopen()</code>的库函数来支持内存文件的操作,
你可以通过<code>fmemopen()</code>大幅简化你的实现, 具体用法请RTFM.</p></div></div>
<p>除了miniSDL库之外, 我们还需要实现SDL_image库中的两个API. 第一个API是</p>
<pre><code class="lang-c"><span class="hljs-function">SDL_Surface* <span class="hljs-title">IMG_Load_RW</span><span class="hljs-params">(SDL_RWops *src, <span class="hljs-keyword">int</span> freesrc)</span></span>;
</code></pre>
<p>它和你在PA3中实现的<code>IMG_Load()</code>非常类似,
只不过是从SDL抽象文件中解码出图像的像素信息.
ONScripter使用的时候, <code>src</code>的类型总是内存文件, 而<code>freesrc</code>则总是<code>0</code>,
这些信息可以帮助你简化实现.</p>
<p>第二个API是</p>
<pre><code class="lang-c"><span class="hljs-function"><span class="hljs-keyword">int</span> <span class="hljs-title">IMG_isPNG</span><span class="hljs-params">(SDL_RWops *src)</span></span>;
</code></pre>
<p>它用于判断SDL抽象文件<code>src</code>是否为PNG格式的图像文件.
判断文件类型的最好方法就是检查文件中存储的魔数(magic number):
回顾一下, 我们认为一个文件是ELF文件, 是因为它的魔数是<code>0x7f 0x45 0x4c 0x46</code>,
PNG文件也有类似的魔数, 具体请STFW.</p>
<div class="panel panel-info"><div class="panel-heading"><h5 class="panel-title" id="运行onscripter"><i class="fa fa-edit"></i> 运行ONScripter</h5></div><div class="panel-body"><p>实现上述内容后, 你就可以在Navy native上运行ONScripter了.
虽然还没声音, 但你还是可以看到图像并且进行操作.</p><p>由于Navy不支持鼠标, 你只能通过键盘进行操作.
我们在ONScripter的代码中添加了一个<code>4x4</code>像素的简易光标,
键入方向键的时候你可以看到光标的移动.
但这个简易光标的实现并不完美, 有时候你会看到屏幕上出现多个光标.</p></div></div>
<h4 id="实现混声库sdlmixer">实现混声库SDL_mixer</h4>
<div class="panel panel-success"><div class="panel-heading"><h5 class="panel-title" id="此部分为选做内容"><i class="fa fa-flag"></i> 此部分为选做内容</h5></div><div class="panel-body"><p>前置任务: 在PA3中实现NDL和miniSDL的音频播放功能.</p><p>需要说明的是, 即使不实现混声库, ONScripter仍然可以在无音频的情况下运行(但无法支持自动阅读功能).
不过对文字冒险游戏来说, 音频会占据游戏体验的很大比例.</p></div></div>
<p>游戏中一般会同时出现多个音频片段, 例如在战斗的过程中,
首先会有背景音乐BGM, 施放法术的过程中会播放法术的音效,
法术对敌方造成伤害的时候会播放敌方受伤的音效.
在玩家来看, 这些音频片段都是同时播放的,
而混声库SDL_mixer就是用于对多个音频进行管理并通过混声实现同时播放的效果.</p>
<p>首先我们提供<a href="https://www.libsdl.org/projects/SDL_mixer/docs/SDL_mixer.html" target="_blank">混声库SDL_mixer的手册</a>.
虽然手册中描述了大量的API, 但我们不必全部实现它们.
ONScripter用到的API已经在<code>navy-apps/libs/libSDL_mixer/src/mixer.c</code>中列出,
我们也无需实现文件中的所有API, 有一些API只是用于链接,
ONScripter在运行部分游戏的时候并不会调用它们.
需要注意的是, 实现这些API的时候请务必通过RTFM阅读相应的细节.</p>
<p>SDL_mixer可以基于SDL的音频API进行实现.
具体地, 你可以在<code>Mix_OpenAudio()</code>中调用<code>SDL_OpenAudio()</code>和<code>SDL_PauseAudio()</code>,
在<code>Mix_CloseAudio()</code>中调用<code>SDL_CloseAudio()</code>.
回顾<code>SDL_OpenAudio()</code>需要提供一个填充音频数据的回调函数,
因此SDL_mixer的实现可以分为两部分, 一部分是用于维护音频状态的各种API,
另一部分是在回调函数中实现真正的混声效果.</p>
<h5 id="播放bgm">播放BGM</h5>
<p>播放BGM的功能相对简单一些, 我们可以先来实现它.
具体地, 你需要实现如下的API:</p>
<pre><code class="lang-c"><span class="hljs-function">Mix_Music *<span class="hljs-title">Mix_LoadMUS_RW</span><span class="hljs-params">(SDL_RWops *src)</span></span>;
<span class="hljs-function"><span class="hljs-keyword">void</span> <span class="hljs-title">Mix_FreeMusic</span><span class="hljs-params">(Mix_Music *music)</span></span>;
<span class="hljs-function"><span class="hljs-keyword">int</span> <span class="hljs-title">Mix_PlayMusic</span><span class="hljs-params">(Mix_Music *music, <span class="hljs-keyword">int</span> loops)</span></span>;
<span class="hljs-function"><span class="hljs-keyword">int</span> <span class="hljs-title">Mix_VolumeMusic</span><span class="hljs-params">(<span class="hljs-keyword">int</span> volume)</span></span>;
<span class="hljs-function"><span class="hljs-keyword">int</span> <span class="hljs-title">Mix_HaltMusic</span><span class="hljs-params">()</span></span>;
<span class="hljs-function"><span class="hljs-keyword">int</span> <span class="hljs-title">Mix_PlayingMusic</span><span class="hljs-params">()</span></span>;
</code></pre>
<p>由于此时只有BGM的音频, 不存在其它音效的音频,
因此我们需在回调函数中进行混声, 就可以实现BGM的播放.</p>
<div class="panel panel-info"><div class="panel-heading"><h5 class="panel-title" id="在onscripter中播放bgm"><i class="fa fa-edit"></i> 在ONScripter中播放BGM</h5></div><div class="panel-body"><p>实现上述API, 在ONScripter中播放ONS游戏的BGM.</p><p>在这些API中, <code>Mix_PlayMusic()</code>用于开始播放一段音乐,
它可以通过libvorbis中的<code>stb_vorbis_open_memory()</code>打开音频文件,
然后在回调函数中通过<code>stb_vorbis_get_samples_short_interleaved()</code>对音频进行解码,
具体用法可以阅读<code>navy-apps/libs/libvorbis/include/vorbis.h</code>中的文档, 或者参考NPlayer的实现.
其它的API都和音频状态的维护相关, 你可以自行决定如何实现状态的维护.</p></div></div>
<!-- -->
<div class="panel panel-info"><div class="panel-heading"><h5 class="panel-title" id="在onscripter中实现bgm的自动重放"><i class="fa fa-edit"></i> 在ONScripter中实现BGM的自动重放</h5></div><div class="panel-body"><p>SDL_mixer库支持音频的自动重放: 当音频播放完毕之后,
就会自动从头开始重新播放, 无需用户手动播放.
这是通过<code>Mix_PlayMusic()</code>中的<code>loops</code>参数实现的.
此外, 你可以通过<code>stb_vorbis_get_samples_short_interleaved()</code>的返回值得知相应音频是否播放完毕,
以及通过<code>stb_vorbis_seek_start()</code>让音频从头开始播放, 具体用法请RTFM.</p><p>实现正确后, 你就可以在ONScripter中欣赏到自动重放的BGM了.</p></div></div>
<h5 id="播放音效">播放音效</h5>
<p>为了支持多个音效的同时播放, SDL_mixer库把一个音效的对象称为"片段"(chunk), 并且引入了"通道"(channel)的概念.
用户可以把一个片段放在任意通道上进行播放, 多个通道中的片段会与BGM进行混声,
从而实现BGM与多个音效同时播放的效果.
注意这里的"通道"和音频文件中的"声道"虽然都叫channel, 但它们的含义并不同, 我们可以通过下图理解它们:</p>
<pre><code> +---> Left Channel
+++-+
* = |L|R|
+-+++
+---> Right Channel
+-----+------------------------------------------------------------+-----+
Channel 1 | |************************ Chunk 1 ***************************| |
+-----+------------------------------------------------------------+-----+
+-----+-------------+--------------------+-------------------+-----------+
Channel 2 | |** Chunk 2 **| |***** Chunk 3 *****| |
+-----+-------------+--------------------+-------------------+-----------+
+---------------------------------+-------------+------------------------+
Channel 3 | |** Chunk 2 **| |
+---------------------------------+-------------+------------------------+
</code></pre><p>上图展示了3个通道, 每个通道按照时间播放各自的片段,
当同一时刻多个通道中都有片段需要播放时, 回调函数需要对它们进行混声.
而考虑每个片段自身, 它们又是双声道的, 即每个采样样本都具有左声道和右声道的数据.</p>
<p>理解上述概念之后, 你就可以来实现和音效播放相关的如下API了:</p>
<pre><code class="lang-c">Mix_Chunk *Mix_LoadWAV_RW(SDL_RWops *src, int freesrc)
void Mix_FreeChunk(Mix_Chunk *chunk)
int Mix_AllocateChannels(int numchans);
int Mix_PlayChannel(int channel, Mix_Chunk *chunk, int loops);
int Mix_Volume(int channel, int volume);
void Mix_Pause(int channel);
void Mix_ChannelFinished(void (*channel_finished)(int channel));
</code></pre>
<p>其中<code>Mix_AllocateChannels()</code>用于分配相应数量的通道,
<code>Mix_ChannelFinished()</code>用于设置一个通知用户"片段播放结束"的回调函数,
ONScripter通过这个回调函数来实现文字冒险游戏的自动阅读功能.
其余API与BGM的相应API很相似, 不过操作对象变成了通道, 具体行为请RTFM.</p>
<p>最后你还需要在填充音频数据的回调函数中实现混声的功能:
你需要检查BGM和各个通道是否处于播放状态, 若是, 则对相应的音频数据进行混声.</p>
<div class="panel panel-info"><div class="panel-heading"><h5 class="panel-title" id="在onscripter中播放音效"><i class="fa fa-edit"></i> 在ONScripter中播放音效</h5></div><div class="panel-body"><p>实现音效播放相关的API和混声功能.
实现正确后, 你就可以在ONScripter中比较完整地运行ONS游戏了.</p></div></div>
<h5 id="采样频率和声道数量的转换">采样频率和声道数量的转换</h5>
<p>ONScripter默认会以"44100Hz双声道"的方式初始化音频设备,
音频文件符合这一属性的ONS游戏可以正常地进行, 如"CLANNAD".
但如果将要播放的音频文件属性并不是"44100Hz双声道", 直接播放它们就会产生失真的现象.
例如在某个ONS版本的"Ever 17"中, BGM采样频率是22050Hz;
而在"星之梦"中, 有少量音效甚至是单声道的.
为了解决这些音频文件的播放失真问题, 我们需要对音频数据进行一些转换操作.</p>
<p>转换操作非常简单, 如果播放的音频文件的采样频率或声道数量小于音频设备打开方式,
就对音频文件进行"扩展". 具体地, 我们只需要对解码后的每个样本进行复制即可:</p>
<pre><code> +-A-+-B-+-C-+
Before +-+-+-+-+-+-+
convertion |L|R|L|R|L|R|
22050Hz +-+-+-+-+-+-+
+-A-+-A-+-B-+-B-+-C-+-C-+
After +-+-+-+-+-+-+-+-+-+-+-+-+
convertion |L|R|L|R|L|R|L|R|L|R|L|R|
44100Hz +-+-+-+-+-+-+-+-+-+-+-+-+
=====================================
+A+B+C+
Before +-+-+-+
convertion |S|S|S|
mono +-+-+-+
+-A-+-B-+-C-+
After +-+-+-+-+-+-+
convertion |L|R|L|R|L|R|
stereo +-+-+-+-+-+-+
</code></pre><p>如果播放的音频文件的采样频率或声道数量大于音频设备打开方式, 就对音频文件进行"截断".
但绝大部分游戏中的音频质量都不会高于"44100Hz双声道", 我们可以不处理这种情况.</p>
<p>最后一个问题是, 我们应该如何得到音频文件的属性?
libvorbis库中提供了一个叫<code>stb_vorbis_get_info()</code>的API, 可以读出音频文件的属性,
具体用法可以参考NPlayer的实现, 或者RTFM.</p>
<div class="panel panel-info"><div class="panel-heading"><h5 class="panel-title" id="在onscripter中播放音效2"><i class="fa fa-edit"></i> 在ONScripter中播放音效(2)</h5></div><div class="panel-body"><p>实现音频数据的转换操作, 从而让ONScripter支持播放不同质量的音频.</p></div></div>
<h4 id="实现磁盘">实现磁盘</h4>
<p>补充了上述函数库的实现后, ONScripter就可以比较完美地在Navy native上运行了.
为了让ONScripter运行在AM native或者NEMU上, 我们还需要考虑存储空间的问题.
AM native和NEMU都假设物理内存只有128MB, 但ONS游戏的数据文件一般都占用比较多的空间,