Symbian C++游戏编程 (上)

Posted on 2010-02-25 21:51 Learn 阅读(214) 评论(0)  编辑 收藏 引用

1. 介绍
1.1 S60平台的游戏编程
随着S60设备硬件的提高,在其上运行高质量的有些成为了可能。当然也同时存在一些挑战的地方就是游戏需要适应许多不同屏幕的能力。另外还有一些其他的限制如下:
被来电或是短信中断
限制电池的使用,要处理突然断电的情况
内存大小受限
显示屏幕和键盘大小的限制
相对于PC而言有限的CPU处理能力
1.2 Purpose and scope
N/A

2. S60 Platform-Specific Considerations
2.1 被系统事件中断
当一个游戏在运行的时候,如果系统事件发生了,用户应该被通知到。所以开发人员需要考虑到游戏被中断的情况。

所有的系统端事件都有这样的一个通用性质,那就是可以被一个应用程序所捕获。当一个系统事件发生时,最前面的应用程序会失去焦点。这会导致该应用程序user interface类(CAknAppUi)的HandleForegroundEventL函数被调用。如果不考虑处理这个函数,应用程序可以处理相关的动作,如暂停程序。

2.2 电池耗尽和供电失败
应用程序需要注意电池的消耗。如果一个时间段没有任何操作的话,应该进入休眠模式从而节约用电。如果一个游戏暂停了,那么所有的timerloops应该停止。如果在一个特定的时间段内,没有任何操作的话,系统的定时器会触发一个时间,应用程序可以通过RTimer::Inactivity方法来获得。

为了处理突然掉电的情况,一个游戏应该每隔一个时间保存一下数据,以便在重启的时候恢复,至少应该保存两份数据,因为很可能在你保存数据的时候会掉电。

2.3 Screen Resolution
S60手机有不同的屏幕分辨率。S60UI库,Avkon能支持不同的屏幕分辨率,然而通常情况下,游戏里面的控件都是自己描画的,所以需要我们自己根据不同的屏幕分辨率来调整,而不能hard-code

2.4 Memory
S60第三版之前的许多S60设备上最多只有8MB的内存。虽然S60第三版的设备的RAM要多些,但是相对于PC设备来说还是很少的,所以内存管理是非常重要的。

除了RAM外,设备还有ROM来存储那些预安装的软件,一个用户数据区域来存储已经安装的应用程序和系统中可写的和连续的数据。

应用程序需要注意内存的节约,这不仅包括在运行时内存的使用,还包括编译后代码的大小,总之,在处理RAM使用中最重要的一条原则就是,我们应该尽早的释放这些已分配的内存,每一步都要提醒自己这样做了吗。在模拟器上,Symbian OS提供了一个宏,如果在程序退出的时候没有释放掉分配的内存,那么这个宏会panic掉应用程序。在目标机器上,OS上的核心会保持跟踪每个线程的内存,并且在程序退出后可以重新释放它们。这就保证了当一个应用程序退出后,所有的内存都会被释放掉。如果他们没有被正确的释放掉应有资源,那结果是一定数量的资源会被保存在系统中。

Symbian OS中,每个线程都有它自己的内存堆栈,在线程已经运行时,这个堆栈大小是不可以被改变的。S60中缺省的堆栈大小仅有8KB,所以在使用时要格外的小心。项目的栈大小可以通过在mmp文件中修改epocstacksize这个标记位设定。

这里要注意的是,在模拟器上运行时,是没有这个堆栈大小限制的,因为它是根据Windows来运行的,这就是程序每个步骤都应该在硬件(手机)上试试。大部分的堆栈溢出是由堆栈里使用大栈描述符所引起的,这种情况可以通过在堆上分配描述符来避免。注意递归的使用是很耗费内存的,如果递归程序是不可避免的,那我们每次传入的参数以及部分变量都应该最小化,以防止溢出。

为了减少编译后代码的大小,我们应该遵循如下的方针:)
1、除非必要,否则不要导出method
2、不要生成没有必要的虚函数(有附加的虚函数表,增加了体积)
3、不要过分的使用TRAP捕捉陷阱
4、避免复制代码
5、找到可以分解的函数
6、使用一般的控制和组件

TRAP宏的使用要小心,并不是说避免过度使用他们是因为他们会增长代码大小。在Symbian os中由应用程序框架已经提供了不少的TRAP,我们没有必要在到处放置我们自己的TRAP了:)

在游戏中,因为会大量使用位图,所以对内存的消耗特别大,最有效的方法不是减少位图的使用,而是降低他们的颜色深度(colour depthSymbian支持24位位图,一共可以处理16777216种颜色,但实际的色彩大小是根据硬件来说的,我们应该尽量以目标硬件所能支持的色彩量为限度,举例来说,一个sprite,使用8bit colour就应该能满足大部分sprite的需要了,而mask更应该只使用1bit的位图!

2.5 其他硬件限制
处理器的能力不是很强,图形处理器很少
算术处理器也少,最好不要用浮点运算,用整型代替。相似的,避免使用64位的整数。因为他们都要影响run-time的性能和内存消耗的开销
一些设备上只有有限的屏幕解析度,大小和色深
只能手机上的键盘的数目也是有限的

2.6 应用程序开发:开发工具和过程
N/A

3. S60 应用程序框架
S60UI框架("Avkon")扩展了Symbian OS的应用程序框架(“Uikon")。典型的有如下几个部分组成:ApplicationApplication Document, Application UI, Application View

通常游戏程序还会有个game engine。通常的建议是要把game的逻辑和UI显示相分离,以便于从用,但是他们之间还是存在一定的耦合,因为UI要接受和相应用户的输入。

对于有多个screen的游戏有两种方法来做,第一就是用多个view来显示,第二是用一个view。后者如果game有很多的screen的话就会要设计一个复杂的状态机。通常我们会把游戏划分成多个逻辑上的view

4. The Game Loop and Use of Timers
4.1 The Game Loop
几乎每个游戏都有个game loop,因为这是游戏代码最重要的一个部分。

典型的一个游戏循环就是一个事件处理方法,其周期性的被应用程序主view的定时器调用。事件处理句柄调用游戏engine更新游戏的状态和显示。

4.2 Timers
SymbianOS中,最缺乏的几项服务之一就是timing services,这里OS并不支持底层的timer interrupts,它只提供一个核心端的时间发生器,其最大的发生频率是64Hz模拟器上,最大的tick rate10Hz,这使得程序的测试更加烦琐。系统中的最大tick rate可以通过访问UserHal::TickPeriod来获得。

Symbian OS v9.1之后的版本是real-time的,所以能提供一个更好的定时器。虽然标准的定时器还是1/64秒,但是你可以通过 User::AfterHighResRTimer::HighRes来获得更好的定时,其提供了1ms的定时。

Symbian OS提供了三个定时器概念,定义在e32hal.h中,可以参见下面的图表:


核心端的timer可以通过访问RTimer来获得,它是一个指向系统端服务的句柄。它提供了一个简单的API来发送三个不同的时间事件:在一段给定时间后的事件,在一个给定时间上的事件以及an event which completes at a given fraction of a second.为了更方便的使用RTimerSymbian OS提供了一个抽象的活动对象:CTimer,它封装了RTimer的使用,在上图中也能看出,CTimer中必定有RTimer的存在。

用户只需要从CTimer派生类即可,另外还需要重载RunL函数,它在一个请求被完成时进行调用。因为是非强占性的,所以在时间片上可能有延时,造成不准确,我们应该尽量减少RunL中代码运行的时间。此外我们还应该把处理时间片的活动对象的优先级提到最高,以防止其他对象切入。

在 SymbianOS中,提供了两个CTimer的派生类:CPeriodicCHeartbeatCPeriodic中,事件的间隔在 microseconds中,而CHeartbeat中,间隔在一秒的片段中,这定义在TTimerLockSpec枚举中。最小的时间片段是1/12 CHeartbeat提供了一个方法来与系统时间同步,当有timer event遗漏时,它的回调函数Synchronize就会被调用,从而给应用程序一个途径采取修正措施。

定时器的优先级和AS的负荷影响定时器的准确性。不要在定时器事件之间把AS拖入到一个长期运行的计算中去;定时器中活动对象的优先级应该低点并且间隔要切合实际一点(比如:不要是1ms)。所有的这些都必须要考虑到,否则会产生ViewSrv11错误,当一个活动对象事件处理句柄霸占了线程AS循环和应用程序的ViewSrv致使AO不能及时的相应。

5. Keypad
Symbian OS是一个事件驱动的系统,所有的应用程序和服务都可以被看作是事件处理器。如按键事件的处理,见下图:
 
以上是当一个用户按下某键后的key event流程图。

应用程序框架处理Key事件很简单:
应用程序UI通过HandleKeyEventL来接受key事件
-应用程序UI通过调用viewOfferKeyEventL方法把key事件传递到活动view中去

假如游戏的应用程序UI类调用了CCoeAppUI::AddToStackL把它创建的view都压入到栈里面去,key事件会被控件栈自动的传递到当前view。除非是某个global的事件,否则应用程序UI不应该去处理这个Key事件。

当用户按下一个键后,keyboard hardware就会生成一个中断,由keyboard driver捕捉,之后分解出这次按键事件的key code,然后driver将它发送到系统端的一个线程——被称为window server,而window server又会把它发向在window group中拥有焦点的那个应用程序中,这个步骤是使用一个control environment(CONE)来完成的,它是window serveruser interface library之间的一个API函数。

这时到了应用程序端了,这里key eventsOfferKeyEventL函数(由window server所调用)所处理,每次按下键都会产生三个不同的事件,第一个事件是EEventKeyDown,它是当一个键被按下后发生的,接着是 EEventKey,我们通常对这个事件最感兴趣,最后是当按键松开后的EEventKeyUp,这些事件的类型都在TEventCode枚举类型中被指明,它也是传递到OfferKeyEventL中的第二个参数,第一个是一个结构,TKeyEvent,它提供了更多的关于本次事件的信息。

如果一个键被按下后持续超过0.8秒,那window server就会发送另一个EEventKey到该程序,这就是长按键事件(long key press event);如按键的时间超过了那个限度,则认为这是一次重复按键事件,window server会在每隔0.25秒就发送key repeat events。这些时间的间隔定义都是s60的缺省值,可以在程序中改变的。

TKeyEvent包含一个key中两个分离的代码:scan codecharacter code。在游戏中,scan code是按键处理是一个实用的选择,因为他绕过了可能的按键映射和任何安装的FEP。而character codeTKeyEvent::iCode,会根据映射和FEP的设置而改变,比如说一个键再被按多次的时候。

TKeyEvent有一个成员变量,iRepeats,它可以用来分辨是长按键事件还是重复按键事件。总的来说,如果要分辨最后那一次按键事件的类型,可以查看iRepeats,如果为 0,那就是长按键事件,如果不是就是重复按键事件(反了吧,如果是0就是一般的按键,如果是非0就是长按键)这里iRepeats是一个32位的带符号整数。因为大部分的按键事件都会及时处理,所以这个变量并没有定义实际重复按键的次数,如果想要知道有多少键被重复按下,应用程序就要自己来计算重复按键的次数了。

游戏,对按键的处理更多,它可以设置自己的按键重复率,通过调用window serverSetKeyboardRepeatRate来调用key repeat time。它有着两个参数。

在 s60系列中,大部分的按键缺省状态下都是阻塞(blocked)的,只有电源键和编辑键是非阻塞的。因为键操作对游戏来说是非常重要的,如用户可以同时按下两个键的操作情况。这就是s60为什么提供了API来取消键阻塞的缘故了,在应用程序的UI基类中, CAknAppUi,提供了 SetKeyBlockMode方法来取消键阻塞。它使用了TAknKeyBlockMode这个枚举变量做为参数,它可以有两个可能的值 EDefaultBlockModeENoKeyBlock,注意同样这里对键的操作也是系统端的设置,我们应该在游戏到后台时恢复到原来的缺省值。
void CMyGameAppUi::ConstructL()
{
// Disable key blocking
SetKeyBlockMode(ENoKeyBlock);
}

TKeyResponse CMyGameView::OfferKeyEventL(const TKeyEvent& aKeyEvent,
TEventCode aType)
{
switch(aType)
{
case EEventKey:
if(aKeyEvent.iScanCode == EStdKeyNkp5 ||
aKeyEvent.iScanCode == EStdKeyDevice3)
{
// EEventKey is called multiple times periodically as
// long as the key is pressed down
iMyGameEngine->Fire();
return EKeyWasConsumed;
}
break;
case EEventKeyDown:
switch(aKeyEvent.iScanCode)
{
// Key pressed down; start moving the ship to left or
// right, and keep it moving as long as the key is
// pressed.
case EStdKeyNkp4:
case EStdKeyLeftArrow:
iMyGameEngine->SetShipMovingTo( ELeft );
return EKeyWasConsumed;
case EStdKeyNkp6:
case EStdKeyRightArrow:
iMyGameEngine->SetShipMovingTo( ERight );
return EKeyWasConsumed;
}
break;
case EEventKeyUp:
switch(aKeyEvent.iScanCode)
{
// Key released; stop moving the ship
case EStdKeyNkp4:
case EStdKeyLeftArrow:
case EStdKeyNkp6:
case EStdKeyRightArrow:
iMyGameEngine->SetShipMovingTo( ENowhere );
return EKeyWasConsumed;
}
break;
}
return EKeyWasNotConsumed;
}

6. Graphics
对游戏开发者来说最重要的恐怕莫过于系统对图形的支持了,下面就来讲述一下Symbian系统中对图形的支持。

6.1 Graphics Architecture Overview
Symbian系统中对图形的支持定义在系统的图形设备接口中(GDI)GDI定义了原始绘图功能,提供了函数绘制文字,分形处理以及位图处理。系统中所有的图形组件都实际上都依靠GDI来处理,参见下图:


 


SymbianOS中,绘图是通过graphics contextsgraphics devices来处理的。GDI提供了一个抽象的graphics context类,CGraphicsContext,它是所有graphics context的基类。它定义了基本的绘图设置,如笔和画刷的样式,并封装了可在应用程序中使用的GDI图形函数。实际绘画时我们是通过graphics device来完成的,而它正使用了graphics context中的设定。这里也有个device类的基类:CGraphicsDevicewhich specifies the attributes of a device the drawing is assigned to.如图:


具体的contextdevice类都是在BITGDI中完成的(参见上上幅图),which is a screen and bitmap-specific graphics component. 它使用高度优化的汇编代码编写的,可以提供良好快速的图形绘制能力。The BITGDI implements rasterising and rendering of images and it supports drawing in on- and off-screen bitmaps.

6.2 Drawing Basics
可以用CCoeControl::SystemGc的方法来描画一个控件,在控件context外用如下方法可以获取gcCEikonEnv::Static()->SystemGc()。通常情况下,描画是在控件上下文中进行的。
void CExampleControl::Draw( const TRect& /*aRect*/ ) const
{
// Get the system graphics context
CWindowGc& gc = SystemGc();
// Set drawing settings
gc.SetBrushStyle( CGraphicsContext::ESolidBrush);
gc.SetBrushColor( KRgbRed );
//Draw
gc.DrawLine( TPoint(10,10), TPoint(30,10));

Draw方法中的TRect参数定义了一个无效的需要被重画的区域(在区域外面的描画不能真的画出来,因为剪裁区域)。通常情况下这个区域是被忽略的,因为重画整个控件要简单和快的多。

Draw方法由框架来调用,不能够直接调用。Draw在以下方法中被调用:
窗口在创建时
其他事件的发生导致窗口内容的无效(比如:窗口重叠)
- DrawNow或是DrawDeferred被调用

我们也可以不通过Draw事件来描画控件,只要按照如下的步骤即可(当框架调用Draw方法时自动调用的)
1. 激活graphic contextCWindowGc::Activeate
2. 调用RWindow::BeginRedraw来通知window server关于挂起draw
3. 描画控件
4.调用RWindow::EndRedraw通知window server描画结束。
5. 调用CWindowGc::DeActiveatedeactivategraphics context

void CMyGameView::MyDrawMethod()
{
// Get the system graphics context
CWindowGc& gc = SystemGc();
gc.Activate(Window()); // Begin drawing
// Window().Invalidate(); // for backed-up Windows
Window().BeginRedraw();
// Set drawing settings
gc.SetBrushStyle( CGraphicsContext::ESolidBrush );
gc.SetBrushColor( KRgbRed );
// Draw
gc.DrawLine( TPoint(10,10), TPoint(30,10) );
Window().EndRedraw();// End drawing
gc.Deactivate();
}

 

 

 

 

 

CTIC.川科创新 3G嵌入式技术教育专家(3G送手机)

3G手机软件工程师培训 现热招中 报名即 送3G手机 一部

www.ctic.cc

posts - 41, comments - 14, trackbacks - 0, articles - 2

Copyright © Learn