正文

开发Smartphone游戏2006-03-14 22:55:00

【评论】 【打印】 【字体: 】 本文链接:http://blog.pfan.cn/ddtme/11008.html

分享到:

 
 
游戏的开发

移动电话上的游戏开发过程与基于PC的游戏开发过程类似。PC游戏的开发过程包括从无图形到图形化,从单个玩家到多个玩家,从不相连接到Internet互联。经过这么多年,移动电话游戏已相当于从无图形发展到低层次图形化的PC游戏。尽管很多游戏开发者已经开始包含了对更好图形的支持,但是目前移动电话带有的内建游戏几乎都没有图形化。

有了Smartphone,移动电话业可能获得与PC游戏市场类似的巨大收益。预计为Smartphone发布第一批游戏是已经存在的Windows和Pocket PC游戏。因为开发人员可以使用相同的开发工具、编程语言和操作系统API(应用程序编程接口),将这些游戏转换为Smartphone游戏的成本很小。

游戏正在转换到Smartphone上


游戏的开发和质量都很依赖于目标平台的能力和可供使用的游戏引擎。作为Smartphone软件开发工具包(SDK)的补充,下面的游戏引擎是可用的:

· Fathammer's X-Forge? 3D Game Engine

· Tao's Group intent multimedia Java (J2ME MIDP) platform

· Amiga Anywhere

点击这些链接可以查看到它们为Smartphone用户提供的丰富的游戏能力。

编写高效率游戏

用户和开发者之间的一个通常的误解是现代的ARM处理器在速度上与Pentium处理器相近。可是那种比较无法正常反映ARM处理器的能力。老式的Pentium速度是基于ARM的Smartphone和Pocket PC的数倍。这归咎于处理器本身和支持它的平台。

Pentium是超级标量的(它在一个时钟周期内执行一条以上的指令),它有五个平行的执行单元和一个综合的浮点运算单元。在大多数PC中通常建有内部的L1缓存和丰富的外部L2缓存。

目前基于ARM的Smartphone和Pocket PC的标量是最好的(它们在一个周期内能执行一个指令)。但是指令集有严格的限制,只包含最基本的指令。更多的高级指令不存在,只能在软件中模拟。

另一个问题是怎样使用指令和数据填满处理器以保证它一直全速度运行。大多数的设计使用单个16位总线处理指令和数据。因为所有的指令是32位的,为了填满代码指令管道,总线的速度必须是处理器的两倍。情况不是这样的,因为总线的速度比处理器慢。处理器速度是总线的2到4倍,一个66MHZ,16位的总线最多只能给33MHZ的ARM处理器提供足够的指令(不包括你要处理的数据)。为了解决这个问题,大多数ARM处理器包括了一个指令缓存和一个数据缓存。典型的指令和数据缓存的大小都是8Kb,有一些有32Kb。只要请求的代码指令或者数据在缓存中,CPU就可以全速度直接从缓存中取,不需要通过缓慢的内存总线。但是一旦开始访问没有载入缓存的代码和数据,效率就切换到<33MHZ(给定的是66MHZ,16位总线)。实际上,将ARM处理器从133MHZ降为2MHZ很简单,只有把数据使用低效率的方式组织。

但是如果有错误的数据类型和低效率的代码,这还算是快的。执行大量的除法,或者滥用浮点数据类型,CPU将只能每秒钟执行20万条代码指令。

内存访问和带宽

Smartphone也可以装备多个基于ARM的处理器,每一个有不同的内存访问花消,但是你会发现仅有太小的缓存和太慢的内存总线,以至于你不能忽略这种花消。

例如,处理器运行频率为132MHZ,而内存总线是16位的,并运行在66MHZ。每次你读取不在处理器缓存中的字节,处理器首先要填充一个完整的缓存线(cache line)。一个缓存线也许是16个字(在ARM体系结构中一个字等于32位或者4个字节),这意味着一个缓存线是16×4=64字节。由于内存总线是16位的,在完成前它需要32个周期填充,更糟的是,总线速度只有处理器的一半,因此处理器在读取该字节前将停止64个周期。因此要确信缓存线填充是值得的。同样,要确认内存是紧密组合的,并检查内存访问模式看是否可以重新排列结构使运行效率更高。如果你有规律的访问一个结构中的一个数据成员并且处理大量的这种结构,看看是否能移动这些特定数据到自己的数组中。

因为这个原因,尽可能使用字节(8位)或者双字节(16位),因为ARM处理器在内存载入寄存器过程中能将无标记、标记字节和双字节扩展为字。但是,在从寄存器保存标记值到字节和双字节内存位置时,编译器将产生两条额外的转换指令,目的是确保如果寄存器中的值太大而无法填充指定的内存位置时,值可以保持自己的标志位。在ARM上,一个转换可能是自由的--几乎每一条正常的指令都可以与一条转换指令耦合--这依赖于编译器的效率。这是你对内部循环必须知道的信息。每次使用字节或者双字节变量,在可能的情况下都使用无标志的数据类型。

内存管理

你也许知道通用的内存管理程序和函数,例如malloc、realloc和new (通常是malloc的包装)速度都非常慢。你最好分配前台需要的内存并使用自己的内存管理程序。这是你的游戏项目中的最重要的子系统。在开发期间,你能轻易地插入一致性检查、测试错误指针,并确认所有的释放和删除与分配相匹配。

最好分配大小结构相等的数组并使用位掩码(bitmask)来处理分配和取消分配。

消息系统

最重要的第二个系统应该是可靠的消息系统,不是指的传统Windows消息泵系统。游戏对象间所有的交互都可以通过你的专用消息系统实现。这包括设置消息发送时间(time-of-delivery),这样你就可以在未来的某个时刻向对象发送消息。例如当游戏玩家选择开启电源项,你可以简单的在5秒内给自己发送一个"激活电源"消息。由于没有保证对象在消息送达时仍然是活动的,你永远不能使用指针。句柄是实现的方法。有了一个这样的系统,很容易添加重放(replay)函数,因为你的中心消息系统可以在游戏进行时简单地保存所有消息到一个文件中。接着重放系统简单地从该文件中读回所有消息并按次序发送所有消息。这对于调试很好--简单地添加一个控制台或者记录文件,在那儿你可以实时看到所有消息--或者执行准确相同的消息流直到应用程序崩溃的位置。

甚至对象的建立和删除也应该使用消息,用户的输入也是一样的。唯一丢失的是对象的所有者信息,你能把该消息系统扩展到另一个计算机--它是多用户系统的基础。因为所有的交互都通过该消息系统完成,你的游戏逻辑的消息没有什么区别,无论它是人、AI玩家还是网络玩家发送的。

资源管理

重复一遍,不要使用指针访问对象。所有的资源都要用句柄来引用。为了提高效率,你可能允许锁定/解除锁定短代码段中资源,但是资源永远不能跨越一个时段锁定(不要锁定多个页面)。通过添加一个用户计数器,你能重复使用只读资源并节约内存。而且即使用户计数器返回为零也不必真正地释放资源。通过将资源保持在内存中,你在下次需要这些资源时几乎不需要时间载入它们。一个好的办法是分配足够的内存来存放你同时需要的所有资源,或者75%的系统空闲内存,无论哪个最大。通过使用额外的内存存储资源这种途径,你能利用更强大的设备。

你的资源管理器应该知道怎样重新载入每种资源项。有了这种知识,就能够释放资源项拥有的内存并重新载入它们,而不影响剩余的游戏代码。释放所有资源内存是放弃焦点给其它应用程序的一种自然的响应,再次获得焦点时重新载入它们。这种情况应该自动发生,对于剩余的代码应该完全透明。

不要使用浮点运算

ARM处理器对于浮点数学运算没有原本(native)支持。所有的浮点运算都在浮点模拟器中运行,速度很慢。在浮点函数中需要上百万次运算的现象并不少见。这就是游戏项目使用定点(fixed point)运算代替的原因。定点实际上仅仅是一个整数,在那儿你指定了一定数量的位作为值的分数部分。通常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;
// result is now 32:32
tmp32_32 >>= 16; // chop off the lower 16 bits
result16_16 = ( int ) tmp32_32; // chop off the upper 16bits.
// result is now back at 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处理器也没有除法的原本支持。每次执行除法将花费几百万次周期。理论上一个132MHZ的ARM处理器每秒能够处理1.32亿条指令(如果50%进行了变换可以处理2.64亿条)。但是CPU每秒中最多只能处理70000次除法。如果游戏每秒显示70个页面,假设描绘每个页面执行仅仅1000次除法,处理器就满速度运行了。

试着用移位和/或乘法代替除法。除以16可以写作右移4位。更复杂的除法能用移位和/或乘法的组合完成。

除法也可以使用查找(lookup)表执行。但是如果分子和分母都是32位的,就需要一个两维的查找表,它远远超出了内存的容量。解决得方法是减小问题的范围。

在表达式a / b中,在不改变最后结果的情况下可以插入乘以1,即a * 1 / b产生相同的结果。另外,可以重新组织乘法和除法的次序,例如写为a * (1 / b )。现在除法简化为1 / b,只需要一个一维查找表了。你可以通过减小精度进一步减小查找表。在大多数情况下16位精度是足够的,查找表就只需要64K了。通过查找b中最重要的位(most significant bit,MSB),你能使用这些信息向上或者向下变换b和结果以适用于32位值,并保持最高可能的分辨率。即使包括了随机访问除法查找表来填充缓存线的时间,执行周期也少于100个--比编译器提供的标准除法速度提高了20倍--只减小了一点点精度。

使用数据类型提供足够的分辨率来处理问题的范围.

内存访问是获得高性能代码的最重要的方式。这意味着应该使用尽可能小的数据类型满足问题的需要。在网眼中真的需要16位索引吗?能不能将位数减少为8?使用最大256个边、256个顶点、256个亮度/阴影值、256个多边形、256个法线和256个相应的纹理,你也许不得不将一些网眼拆分为多个更小的网眼。它得好处是内存访问更快。

你怎样使用正常的向量?你主要用于亮度计算或者可视化测试吗?我为自己的向量组件使用2:8的定点格式,意味着可用范围是-1.99609375 到+1.99609375,分辨率为0.00390625。我使用8位小数分辨率满足1.4度分辨率下360度的需要。在小屏幕(例如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配置。通过建立一个小的配置脚本,你可以告诉配置管理器改变为你想做到的。

 







使用DMProcessConfigXML()函数通过配置管理器发送上面的XML配置数据。

要记住在执行过程中可能会失去焦点--因此在放弃控制给另一个应用程序前一定要保存原始的配置信息。由于应用程序不处于活动状态时用户可能改变了设置,再次获得焦点时要读回设置。

结论

Smartphone是第一款有足够处理能力和图形能力的移动电话,它将我们希望的PC平台的游戏经验带到了移动世界。游戏在继续!

阅读(4343) | 评论(1)


版权声明:编程爱好者网站为此博客服务提供商,如本文牵涉到版权问题,编程爱好者网站不承担相关责任,如有版权问题请直接与本文作者联系解决。谢谢!

评论

loading...
您需要登录后才能评论,请 登录 或者 注册