开发 Smartphone 游戏
Andy Sjostrom
businessanyplace
2003年1月
适用于:
Microsoft Smartphone 2002 软件
摘要
概要介绍 Microsoft Smartphone 2002 软件的游戏开发并提供一些相关技巧。
目录
- 游戏市场概述
- 游戏开发
- 可以移植到 Smartphone 中的游戏
- 编写高效的游戏
- 内存访问和带宽
- 内存管理器
- 消息系统
- 资源管理器
- 不要使用浮点运算
- 不要使用除法
- 使用精度刚好足以解决问题的数据类型
- 使用查找表完成所有任务
- 使用汇编完成所有运算
- 关闭硬件按钮单击声效
- 游戏屏幕快照
- 小结
游戏市场概述
伴随着新的 Microsoft® Windows® Powered Smartphone 呈现在我们面前的是一些非常有吸引力的游戏,而在未来还会有更多这样的游戏问世。接下去,您将了解这些电话为什么能吸引那些“重量级”的游戏玩家,以及开发成功的游戏需要注意的事项。
计算机游戏市场走过了一条漫长的发展之路,这条路的起点可以追溯到上个世纪 70 年代后期第一款基于字符的简单游戏的诞生。从金融角度来讲,一些分析家声称:仅在美国,游戏市场加上游戏机和 PC 软件的销售,预计到 2004 年将带来 180 亿美元的销售额;而在全世界范围内,该项销售总值已经超过了整个好莱坞电影产业的产值。从技术角度来讲,游戏市场带动了消费硬件的发展,而在过去几年中,甚至带动了 Internet 的普及和连通。很显然,那些字处理软件、电子表格软件或者业务流程应用程序并不象计算机游戏那样,需要功能更强大的三维图形芯片、大容量存储设备以及速度更快的处理器。
据预测,新兴的无线游戏产业的产值到 2006 年将达到 40 亿美元到 150 亿美元,当然不同的人有不同的算法(英文)。无论游戏产业朝什么方向发展,其前景都将非常美好。
游戏开发
移动电话的游戏开发与基于 PC 的游戏开发的发展道路非常相似。PC 游戏开发的进步表现在从非图形到图形、从单人游戏到多人游戏、从互不相连到 Internet 互联等各个方面。经过数年的发展,移动电话中的游戏已经达到那些较低级的图形 PC 游戏的水平了。而有些移动电话制造商已经开始在产品中包含更好的图形支持,尽管现在市场上出售的一些移动电话的内置游戏几乎没有任何图形。
有了 Smartphone 的支持,移动电话产业将能取得象在 PC 游戏市场中那样的巨大飞跃。虽然其发展会更加迅猛,但最先发布的 Smartphone 游戏可能仍然是现有的 Windows 和 Pocket PC 游戏。由于游戏开发人员可以使用相同的开发工具、编程语言和操作系统 API(应用程序编程接口),因此将这些游戏移植到 Smartphone 的工作量非常小。
可以移植到 Smartphone 中的游戏
下表是为 Smartphone 2002 开发和移植游戏的公司。
表 1:为 Smartphone 2002 开发和移植游戏的公司。
公司 | 游戏 |
---|---|
Ideaworks3D(英文) | “Rebound!”以及很多其他游戏,有些是与 Eidos Interactive(英文)等合作出版商共同进行的。 |
Hexacto(英文) | “Tennis Addict”、“Full Hand Casino”和“Slurp”。更多的游戏将相继问世,包括“Lemonade Inc.”、“Baseball Addict”和“Bob The Pipe Fitter”。其中,有几个游戏将具备多人游戏的功能,充分利用了 Smartphone 的无线功能。 |
Incagold(英文) | “Slamtilt”,一种三维弹球游戏 |
Pixel Technologies(英文) | “MobilePlay 1”,一个包含国际象棋、跳棋、纸牌、锄大地、军棋和黑白棋等多人游戏的在线游戏包 |
Xen Games(英文) | “Interstellar Flames”,一款让用户驾驶战斗机保卫地球的动作游戏 |
Terra Mobile-iobox(英文) | “Defender”以及其他将具有无线功能的游戏 |
游戏开发工作和开发出的游戏的质量很大程度上取决于目标平台的性能以及可用的游戏引擎。除了 Smartphone 软件开发工具包中的 API 以外,预计还会有以下游戏引擎可供使用:
单击这些链接查看丰富的功能,以及它们带给 Smartphone 用户的引人入胜的游戏体验。
编写高效的游戏
为了深入了解如何编写高效的游戏,我们请教了 Sven Myhre。Sven Myhre 是 Amazing Games(英文)的 CEO,同时也是一名游戏开发人员。
手持设备用户和开发人员中间普遍存在一个错误概念,即现在的 ARM 处理器与同一速度级别的 Pentium 处理器功能相当,但 ARM 处理器并未很好地体现出这一点。一个早期的 Pentium 处理器就可以胜过当前基于 ARM 的所有 Smartphone 和 Pocket PC,这是由处理器自身及其支持系统决定的。
Pentium 处理器具有超标量体系结构(能够在一个时钟周期内执行多条指令),有五个并行执行单元和一个集成的浮点运算单元。一般情况下,大多数以 Pentium 处理器为核心构建的 PC 都有一个内部 L1 缓存和一个大型外部 L2 缓存。
当前基于 ARM 的 Windows Powered Smartphone 和 Pocket PC 具有标量体系结构(它们可以在一个时钟周期内完成一条指令)。但是指令集受到很大的限制,只包含了最基本的指令,而不包含一些相对高级的指令(比如除法),这些指令必须在软件中进行模拟。
另外一个问题是向处理器提供指令和数据以保持其全速运行的能力。大多数设计方案都使用一条 16 位总线来获取代码指令和数据。由于所有指令都是 32 位的,总线的运行速度就应当是处理器速度的两倍,这样才能满足代码指令管道的需要。但实际并不是这样的。因为总线速度要比处理器速度慢,所以实际的解决方案是另外一种方式。处理器与总线的速度比在 2x 到 4x 之间。一条 66-MHz、16 位的总线最多只能为 33-MHz 的 ARM 处理器提供足够的代码指令以使其保持全速运行,这其中还不包括您需要处理的数据。为了解决这个问题,大多数 ARM 处理器都包含一个指令缓存和一个数据缓存,通常这两个缓存的大小均为 8 Kb,有些大的可以达到 32 Kb。只要缓存中出现请求的代码指令或数据,CPU 就能以全速直接从缓存中获得它们,而不必经过缓慢的内存总线。而一旦需要访问尚未加载到缓存中的代码和数据,您就会切换回 < 33 MHz(假定总线为 66 MHz、16 位)的速度。实际上,将一个 132 MHz ARM 处理器的速度降低为 2 MHz 是相当容易的。只需要采用一种效率非常低的方式来组织数据,使缓存失去作用就可以实现这个目的。
即便是这样,也比您使用错误数据类型和低效率编码的工作速度要快。执行大量除法运算,或者滥用浮点数据类型,将使 CPU 为达到每秒 20 万条代码指令的速度而“筋疲力竭”。
内存访问和带宽
Smartphone 可以配备各种基于 ARM 的处理器,每一种处理器的内存访问开销都不同。但可以肯定的是,由于缓存容量太小、存储总线速度太慢,因此您无法忽略这个问题。
比如,您的处理器运行速度为 132 MHz,内存总线为 16 位、运行速度为 66 MHz。每次读取一个尚未到达处理器缓存中的字节时,处理器都要先填充一整条缓存线。一条缓存线可能是 16 个字(在 ARM 体系结构中,一个字相当于 32 位或 4 字节),也就是说一条缓存线为 16 * 4 = 64 字节。由于您的内存总线是 16 位的,因此在操作结束之前它将被占用 32 个周期,而且更糟的是,由于总线运行的速度只有处理器的一半,所以处理器将在您获得请求的一个字节之前延迟 64 个周期。因此,要确保值得花时间去等待填充缓存线。同样,还要确保您的内存尽可能紧密,同时还要检查内存访问模式,以判断是否可以调整结构以使运行效率更高。如果您需要定期访问某一结构中的一个数据成员并要处理大量这样的结构,则请您考虑将这个特殊的数据成员移到它自己的数组中。
基于同样的原因,应该尽可能使用字节(8 位)或双字节(16 位),这是因为 ARM 处理器在从内存到寄存器加载信息的过程中,可以将无符号和有符号的字节和双字节扩展为字。不过,在将有符号值从寄存器存储到字节或双字节的内存位置时,编译器将生成两条额外的移位指令,以确保即使由于寄存器中的值过大而无法保存在指定的内存位置,该值也能保留其符号位。在 ARM 上,其中一条移位指令可能不需要任何系统开销(几乎每一条“常规”指令都可以与一条移位指令配对),这取决于编译器的效率。这是您在处理内部循环时应该了解的一条信息。每次使用单字节或双字节的变量时,都要确保尽可能使用无符号的数据类型。
内存管理器
您可能已经对此有所了解,但是一般的内存管理器和函数,如 malloc、realloc 和 new (通常只是 malloc 的一种包装),速度都很慢。您通常需要预先分配所需的全部内存,并使用自己的内存管理器。这是游戏项目中一个最重要的子系统。在开发过程中,您很容易加入一致性检查、测试坏指针等功能,并确保所有的 free 和 delete 操作都与 alloc 和 new 一一对应。
分配同样大小的结构的数组,并仅仅通过位屏蔽(或其他方式)来处理分配和解除分配,速度也会相当快。
消息系统
第二个最重要的子系统是一个可靠的消息处理系统,而不是传统的 Windows 消息泵系统。游戏对象之间的所有交互都应该通过您自己的专用消息系统来完成。这包括为消息设置传递时间的选项,这样您就能向自己或其他对象发送消息,以便在未来某个时刻传递该消息。例如,当游戏角色拾到一个加血物品时,您就可以简单地向自己发布一条“解除加血”的消息,并在 5 秒钟之内传递。由于对象并不能保证会在传递消息时仍然存活,因此您任何时候都不应该使用指针,而应该采取句柄(静态标识符)的方式。对于这样的系统,很容易添加回复功能,因为中央消息系统很容易游戏进行期间将所有消息保存到一个文件中。然后回复系统就能从保存文件读取所有消息并按顺序调度。这对于调试来说也是一种非常不错的功能:您可以添加一个控制台或日志文件,以便实时观察所有消息;也可以重复播放相同的消息流;直到到达游戏的崩溃点。
对象的创建和删除,以及玩家输入的处理,也应当使用消息来进行。唯一缺少的就是对象所有权的信息,您可以扩展消息系统使其与另一台计算机连接起来 - 这是多人游戏系统的基础。由于所有交互都要经过消息系统,因此您的游戏逻辑将不对真人玩家(硬件按钮)、AI 玩家或远程网络玩家发送的消息进行区分。
资源管理器
要重申的是,永远不要使用指针来访问对象。对所有资源的引用都应当用句柄来进行。为了提高效率,您可以考虑在一小段代码片断中锁定/解锁这种资源,但是不能在一段时间内始终锁定资源(不要让锁定持续多个帧)。通过添加一个使用计数器,您可以反复使用只读资源并节省内存。同样,即便使用计数器复原为零,您也不需要真正去释放资源。将资源保留在内存中,在下次需要资源的时候,所需的加载时间就可以达到几乎为零。一种好的做法是,分配足够多的内存以存储您同时需要的全部资源,或者分配系统可用内存的 75%,取两者中的较大值。通过这种方式,您就可以使用额外的内存对资源进行缓存,从而更充分地利用设备的强大功能。
您的资源管理器应当知道如何重新加载每个资源项。具备这种能力之后,它就能够释放资源项占用的内存并重新加载资源项,而无需涉及其余的游戏代码。释放全部资源内存是对焦点切换到其他应用程序的自然反映,再次获得焦点时将重新加载应用程序。这种操作应该自动完成,并且对其余代码来说应该是完全透明的。
不要使用浮点运算
ARM 处理器本身不支持浮点运算。所有的浮点运算都在一个特殊的浮点模拟器中运行,并且速度很慢,经常需要进行数千个时钟周期才能完成浮点函数的计算。这就是为什么游戏项目通常都使用定点格式来代替浮点格式的原因。定点数实际上就是一个整数,在这个整数中您指定一个假想(但固定)的位数作为数值的小数部分。这就好比所有 1000 以下的数字都是这个数的小数部分。要表示 0.500 这个数,只要简单地乘以 1000 就可以得到 500,难点在于始终要想象这一不可见的小数点。加法和减法的计算不会有问题:500 + 500 = 1000(或用心算:0.500 + 0.500 = 1.000)。乘法和除法则是另一回事:500 * 500 = 250000(或用心算:0.500 * 0.500 = 250.000)将是不正确的。两个定点值完成一次乘法运算之后,需要去除结果。如果把结果除以 1000 就对了(250.000 / 1000 = 0.250 是正确的)。因此对于乘法运算,您只需进行通常的乘法运算然后去除结果,使其规范化。
这带来了另外一个有意思的问题。在对中间结果进行规范化之前,其数据范围是什么样的?在上面的例子中,您在进行乘法运算的时候可能会超出可用的位数范围,这就意味着将产生一个溢出并丢失结果中最重要的部分。诀窍就是,确保在中间结果中使用一种能够保留最大的可能结果的数据格式。进行两个 32 位数值的乘法运算时,您的中间值就必须是 64 位。规范化(并截位)之后,位数将再次成为 32。
int Multiply16_16_by_16_16( int a16_16, int b16_16 ) { __int64 tmp32_32; int result16_16; tmp32_32 = a16_16; tmp32_32 *= b16_16; // 现在结果是 32:32 tmp32_32 >>= 16; // 截去低端 16 位 result16_16 = ( int ) tmp32_32; // 截去高端 16 位。 // 现在结果变回 16:16 return result16_16; }
要做除法,就要进行相反操作,先做乘法,然后再除。
一般的定点格式是 16:16,其中第一个 16 位是整数部分,后一个 16 位是小数部分。我在目前开发的游戏项目中使用了大量的不同格式,为的是涵盖游戏引擎的各个不同部分用到的各种数值范围。总的来说,我使用的有 2:30、8:24、16:16、24:8、28:4、2:14、8:8、11:5、2:8 和 4:4。其中大多数都是 32 位数值,但也有一些是 16 位或 10 位的,甚至有些只有 8 位。
不要使用除法
您的游戏项目不应该执行单独的除法运算。ARM 处理器本身不支持除法运算。每次您进行除法运算的时候,它都会消耗数千个时钟周期。132 MHz 的 ARM 在理论上可以每秒执行 1.32 亿条指令(或者 2.64 亿条指令,如果其中一半运算是移位运算)。但是每秒 7 万次的除法运算就会超过 CPU 的最大能力。也就是说,如果您的游戏运行速度是每秒 70 帧,则对所绘制的每一帧进行 1,000 次除法运算就会达到处理器的最大能力。
将所有除法都替换成移位和/或乘法运算。被 16 除可以改写为右移 4 位。更复杂的除法运算可以用移位和/或乘法的组合运算实现。
也可以用查找表来执行除法运算。不过,如果分子和分母都是 32 位,所需的二维查找表就远远超出了可用的内存容量。一种解决方案就是缩小问题的范围。
在表达式 a / b 中,可以在任何需要的位置插入一个“乘以 1”而不改变最终结果。这样 a * 1 / b 将产生完全相同的结果。另外也可以改变乘除法的顺序,将其写为 a * ( 1 / b ),这样,除法运算就被简化为 1 / b,它只需要一个一维的查找表。您还可以进一步简化查找表,方法是降低精度。我们先假设在大多数情况下 16 位的精度就已经足够,您的查找表只需要 64K 的项数。通过查找 b 中的 MSB(最高有效位),您可以使用此信息对 b 和结果进行向上或向下的移位以调整您的 32 位数值,同时保持最高的可能精度。即使加上由于随机访问除法查找表而造成的填充一条缓冲线的时间,最差的情况也将少于 100 个时钟周期。这个结果是编译器提供的标准除法效率的 20 倍,付出的代价是精度的一点点损失。
使用精度刚好足以解决问题的数据类型
内存访问是实现高性能代码的一个最重要方面。这意味着您应当使用尽可能少的数据类型来涵盖您的问题范围。您真的需要在网格中使用 16 位索引吗?有没有可能将位数降至 8 位?在最多有 256 条边、256 个顶点、256 种光照/阴影值、256 个多边形、256 条法线以及 256 种纹理的坐标中,可能要将一些网格划分成多个较小的网格。这样做的好处是可以加快内存访问的速度。
您是如何使用法向向量的?它们主要用于光照计算还是可见性测试?我的法向向量组件用的是 2:8 定点格式,也就是说我可以使用从 -1.99609375 到 +1.99609375 范围内的定点数据,精度为 0.00390625。换句话说,我的小数精度为 8 位,角度精度为 1.4 度。在小屏幕(如 176x220)上,如果一个点的光照方向有 +/- 0.7 度的偏差(最坏情况下),最终效果的差别是无法分辨的。而好处就是我可以将 x、y 和 z 分量存储到一个单独的字中。
使用查找表完成所有任务
为可以提前计算的项创建查找表,只需要您访问一次内存。与复杂的、需要执行数千条指令的数学函数相比,这是一笔相当划算的“买卖”,尽管您将必须“牺牲”一小块内存来存储查找表。
典型的查找表有倒数 (1 / x)、正弦值计算、色彩混合以及光照等。在我目前开发的游戏项目中,渲染器中的几乎所有环境贴图和光照/阴影管道都是用两个提前计算的查找表实现的。
游戏最动人的地方在于,我们正在努力“说服”人们,让他们相信自己置身于一个鲜活、生动的世界中。不管我们所采用何种方式,只要这个世界看起来很完美并且感觉良好,我们就达到了目的。“足够好”是我们的优化口号。
使用汇编完成所有运算
如果检查上述有关定点数乘法的代码示例中的编译器输出,就会发现即使是最优化的代码输出,其效率也不是非常高:
stmdb sp!, {r11, lr} ; stmfd mov r11, r0 mov r2, r1, asr #31 mov r3, r0, asr #31 mul r2, r11, r2 mul r11, r3, r1 add r3, r2, r11 umull r11, r2, r0, r1 mul r1, r0, r1 add r0, r3, r2 mov r3, r0, lsl #16 orr r0, r3, r1, lsr #16 ldmia sp!, {r11, pc} ; ldmfd
由于 C 和 C++ 代码没有办法精确表达我们代码的目标,编译器不得不采用我们编写的语句并将它们转换为纯粹的汇编代码。
作为程序员,我们非常清楚要达到什么样的目标,以及如何让微处理器通过最佳方式来达到目标,结果通常更加紧凑:
smull r2, r3, r0, r1 mov r0, r3, lsl #16 orr r0, r0, r2, lsr #16 mov pc, lr
四个乘法运算被替换成一个。很多在寄存器之间的前后移位都可以被删除,并且由于只使用四个可变寄存器 (r0-r3),因此我们就不必设置和恢复堆栈块。
关闭硬件按钮单击声效
您可能已经注意到,Smartphone 上的很多游戏都具有一种“参差不齐”的帧速率,但是如果将硬件按钮单击声效关闭,游戏就会运行得更流畅些。
由于某种原因,在您每次按下按钮、操作系统播放按钮单击声效时,设备都会冻结不到一秒钟。
幸运的是,几乎用户界面的所有方面都可以用 XML 进行配置。通过设置一小段配置脚本,您就能够让配置管理器改变任何需要的内容。
<wap-provisioningdoc> <characteristic type="Sounds"> <characteristic type="ControlPanel\Sounds\KeyPress"> <parm name="Mode" value="1"/> <!-- 0=无,1=音调,2=单击 --> </characteristic> </characteristic> </wap-provisioningdoc>
使用 DMProcessConfigXML() 函数将以上 XML 配置数据传递到配置管理器。
请记住,在执行期间可能会丧失焦点,因此要确保在将控制转交给另一个应用程序之前恢复最初的配置。由于用户可能已经在我们的应用程序处于非活动状态期间更改了设置,所以在恢复焦点时要读回设置。
游戏屏幕快照
下面这些漂亮的屏幕快照来自 Smartphone 游戏开发实验室。图 1 的屏幕快照来自 Hexacto(英文)。
图 1:开发中的 Hexacto 游戏
图 2 的屏幕快照来自 Ideaworks3D(英文)的“Rebound!”。
图 2:开发中的 Ideaworks3D 游戏
图 3 的屏幕快照来自使用 Fathammer 的 X-Forge(tm) 三维游戏引擎(英文)的游戏。屏幕快照是从相同游戏的 Pocket PC 和 Smartphone 版本中得到的。
图 3:开发中的 Fathammer 的 X-Forge™ 三维游戏引擎游戏
小结
Smartphone 是第一种具有足够的处理能力、图形功能以及连通性的移动电话,它能将 PC 平台上的丰富游戏体验带入移动电话世界。将游戏进行到底!