Series60游戏设计参考(一)
2010-01-05 13:20作者:天极网开发频道出处:天极网软件频道责任编辑:郑重
在游戏设计中,要注意的一个首要问题便于能够处理系统的各种事件,如一个电话或短信的到来,当这些系统时间发生时,前台的程序会失去焦点,这时我们可以通过重载CAknAppUI::HandleForegroundEventL来处理中断游戏时所要处理的事情。
为了考虑电池的节能使用,我们的游戏在长时间没有用户输入时(这时很可能是用户忘记关闭游戏了),就不应该再处理任何timer,以便机器进入休眠状态,事实上,如果程序处于这样的状态,系统会发送一个事件来通知我们,我们可以调用RTimer::Inactivity来处理。
设计游戏时我们所要考虑的另一个重要方面便是可移植性,比如到其他设备上,可能会产生屏幕大小不容的问题,因此我们在游戏中涉及屏幕大小的代码,不应该使用直接的数字数值,因该调用类似CEikAppUi::ApplicationRect()和CWsScreenDevice::GetDefaultScreenSizeAndRotation()这样的函数来获得屏幕大小等。
手机上设计游戏有很多的制约,内存,CPU,图形处理器以及运算处理器都无法和PC相比,甚至屏幕,键盘,等都对上面的开发产生了一定的制约,譬如说键盘,只有有限的数字键,不同的设备间,键的布局还可能不同,怎么办,呵呵,我们在设计游戏时就一定要考虑到游戏中操作时使用键的重定义。如上的这些制约实际上都限制了我们,因此在开发中要考虑好手机上适合开发什么样的游戏并不失其可玩性,不过目前N-Gage的横空出世使得我们在手机上玩古墓丽影都变为可能。呵呵。
内存
内存管理在一个内存受制的设备中显得尤为重要,这包括运行时的内存使用和最后生成的代码大小。大多数symbian设备中只有8MB或更少的RAM,in addition to RAM, the devices have ROM for preinstalled software and a user data area for installed applications and the systems' writeable and persistent data files。此外,设备中可以安装各种扩展卡,如CF卡或MMC卡。
使用RAM中最重要的原则就是对分配的内存要及时释放,在模拟器上开始时,它提供一个内存检查的宏(如果程序中使用一个GUI时就会缺省提供)这个宏可以用来在程序没有释放内存时示警,由此在早期设计过程中就杜绝了内存的泄漏。而在目标机器上,由核心来保证跟踪每条线程的内存使用,并在他们退出时自动的加以释放,以保证程序退出时所有的内存都得到正确的释放。
在设计一个应用程序时,堆栈的使用是值得重视的。在SymbianOS中,每个线程都有它自己的内存堆栈,而它在线程运行后是不能增加的。
Series60中一个应用程序却省的堆栈大小只有8KB,正是因为如此,我们应该更为小心的使用它。在模拟器和实际机器上,这方面是存在分歧的,在模拟器上堆栈是没有限制的,而在机器上却有。这是因为模拟器上使用了PC的堆栈。因此每个程序都应该尽早的在机器上进行调式。堆栈溢出的大部分原因是因为stack descriptors的使用,同样递归的使用也是个吃内存的大户。如果递归编程是必要的,那传递的参数大小和局部变量都要很好的控制到最小化,小糊涂曾经编写过一个程序,从windows移植过来一个核心运算部分,就是递归,非常复杂以至于不能拆解开来,结果程序在3650上经常报内存不足-_-0
由于游戏中对图形的要求,因此位图成了程序内存消耗的大方面。在这方面节约内存的有效措施并不是减少位图的使用,而是降低他们的颜色深度。Symbian OS支持24bit位图,这样可提供16777216种颜色,但实际上目标机器所能表达的颜色不是很多。因此位图的颜色不应该超过机器所能表达的范围。一般来说8bit(256色)就足够应付大多数需求了,而所有的mask都应该转为1bit位图。
As with bitmaps, any data used in game – sound, music, and video – should not consume a lot of space.
3、Structure of the Game and Game Loop
游戏的循环和游戏的结构是游戏中非常重要的部分。一个好的结构设计可以使得游戏开发更为容易些。这里给出了一个结构的示例和游戏循环的实现部分,它们是在Series60 Avkon下完成的。
Series60 Avkon应用程序的文档-视图结构是非常适合游戏使用的,因此一个游戏的典型结构如下:
AppUI:
游戏的controller,它控制了游戏的视图和菜单命令,以及其他用户输入(可传递袄active view)。
View:
拥有整个游戏的引擎(或部分引擎),displays the game state from the engine, and handles the UI controls on the view and other user input.
Document:
用来存储引擎中游戏的状态(实际中是很少使用的)。
Engine:
游戏引擎,它实际上在一个单独的类——而非UI(AppUI或view)中完成了整个游戏的逻辑部分。这表明了该类的结构和职能。游戏引擎有时会和view混合在一起,这是不能避免的,因为view处理了用户的输入和引擎的绘制部分。
实际上有2种方式可以用来实现游戏的多个场景(states)
1、使用多个views(如,intro, main menu, option screen, game playing).
2、使用单独一个view.
如果只使用一个view,那引擎比较容易实现——它只被appUI所拥有,因为appUI和view需要处理用户的输入,而view还要现实游戏的状态,因此我们通常会把一个游戏引擎的reference传递到view中(在它生成时)。这使的游戏的结构变得稍有晦涩。
使用单独的view会使用户输入和绘制部分变得凌乱,因为需要这个view根据游戏的状态做出判断,特别在一个大游戏里更是容易弄乱。
而当游戏使用多个逻辑views时,用户的输入和绘制部分可以很清楚的放入各个相关类中,如:
View for the introduction of the game
View for the main menu
View for the options screen
View for game playing
Since the Avkon architecture provides a ready-made framework for displaying multiple view, 所有上述成立可以很容易的实现,从而把结构变得更为清晰。AppUI的职能就是管理多个视图。
但每件事都有他们的正反面,开发者可以自己判断是否使用多视图,如特别在处理image containers和sound classes的实现,以及如果从各个view中访问他们而产生的问题。
一般来说游戏的循环是由一个timer来构建的,由此来更新游戏的状态,而用户输入的处理并不受timer的制约,通常timer在view中实现,它发送timer events到引擎,并周期性的更新view。
void MyGameView::MyTimer()
{
iMyEngine->NextFrame(frameTime);
UpdateDisplay();
}
另外有种情况是,游戏的引擎有个内部timer,这样会似的程序看起来比较混乱,因为引擎即要更新view,而view又要发送用户输入到引擎。
如果游戏引擎包含有Active Objects(多任务),may be necessary to use the method where the engine updates the screen’s back buffer when the active object is ready, and view always draws the last previous full frame – the variety of possibilities is almost limitless.
4、Timers
各种Timer被使用在游戏中,他们组成了游戏的循环。他们被用来周期性的重绘屏幕,更新sprites的位置,以及处理游戏中的各种事件。
symbianOS中的时间发生器是比较低的(和其他设备相比),不过仍可应付大多典型需求,只有设备中kernel端timer可以提供1/64秒的精度,而模拟器上则只能提供1/10秒了(可以使用UserHal::TickPeriod得到该实际值)。
SymbianOS中可以提供3个timer:
(1)、Simple timers,由RTimer提供,可以在每隔一段时间或在某一时间点上产生触发事件。RTimer提供了对timing services的最底层访问,完成时通常需要Active Objects的配合。如果游戏引擎是使用Active Object完成的,那使用这个timer最好。
(2)、Periodic timer(CPeriodic),可在给定周期提供触发事件,event is notified to the application through a callback (TCallBack) given when creating the timer. 这个timer因为其简单性而用得较多,通常需要一个timer来产生周期性的事件时就使用它。
(3)Heartbeat timer(CHeartBeat),它和periodic timer类似,但当一个时间事件丢失时(也就是说没有被处理)heartbeat timer可以执行一个callback以通知程序。CPeriondic timer并不是十分精确的,因为它要等待程序准备好处理一个新事件后才会发送这个事件(这个时候程序不能太忙)。
这里heartbeat timer的附属功能——即通知丢失事件(MBeating::Synchronize),很少需要。因为毕竟游戏中并不需要如此精确的时间统计。
如sprites的移动为(position = speed * time),因此轻微的时间丢失是不会产生什么危害的。
正如前面所说,CPeriodic timer是足够游戏使用的,下面的代码演示了这方面的内容:
// Starts the timer
void CMyGameView::StartTimerL()
{
// tickInterval in microseconds; 100000 equals to 1/10 seconds const TInt KTickInterval = 100000;
iPeriodic = CPeriodic::NewL(CActive::EPriorityLow);
iPeriodic->Start(KTickInterval, KTickInterval, TCallBack(Tick, this));
}
// Stops the timer
void CMyGameView::StopTimer()
{
iPeriodic->Cancel();
delete iPeriodic;
iPeriodic = NULL;
}
// Called by the timer when given interval elapsed
// This must be static method
TInt CMyGameView::Tick(TAny* aObject)
{
// call non-static method
((CMyGameEngine*)aObject)->NextTick();
// Allow the timer to continue it’s work return 1;
}
// non-static timer event; called by static Tick()
void CMyGameView::NextTick()
{
TTime currentTime;
currentTime.HomeTime();
TInt64 currentTick = currentTime.Int64();
// Calculate elapsed time from last timer event in microseconds
// iLastTick is the current time from previous timer event
TInt64 frameTime = currentTick - iLastTick;
iLastTick = currentTick;
// Update activity on the screen according to frameTime
switch(iGameState)
{
case EPlayingTheGame:
// Calculate e.g. sprite positions in game screen and
// redraw the screen
HandleGameTick(frameTime);
break;
case EWatchingTheIntro:
// Intro of the game can be handled with the same timer
HandleIntroTick(frameTime);
break;
// Other possible game states could be handled too
}
}
我们并不需要使用多个timer,这样会造成程序结构紊乱和代码的不清晰,每个timer同时也都是一个Active Object(除了RTimer,但事实上它也需要ActiveObject的配合使用),这样每timer都会产生Active Scheduler的装载和资源的耗费。
(小糊涂的五子棋就使用了一个timer,主要负责intro的loading以及一个棋盘动画的显示:)
timer的优先级和Active Scheduler的装入直接影响到timer的精确性。此外,should not swamp the active scheduler with long lasting calculations between timer events, the priority of the timer should be kept low and the timer’s interval should be realistic (not just, for example, one microsecond). These precautions should be taken into account to prevent the known ViewSrv 11 panic issue, which occurs when the active scheduler is swamped and the application’s ViewSrv Active Object cannot respond in time.
不要让active scheduler太繁忙以至于来不及调度active object:)
5、Keyboard
由于目前series 60设备的多样性,使得我们在键盘操作上要认真考虑,最理想的状态便是可以选择控制游戏的各个键。
键盘事件的处理在程序中是很简单的:
AppUI在HandleKeyEventL中接收到key event;
AppUI通过调用view的OfferKeyEventL,将key event发送到当前活动的view中。
如果使用了AddToStackL(通常在AppUI的ContructL()函数中,如AddToStackL(iAppView);),那key event就会自动的从AppUI传递此view中,key event不会被AppUI成立,除非这个key event需要的功能是全局的(也就是说不是和任何制定的view联系在一起的)
TKeyEvent和TEventCode,做为参数传递到OfferKeyEventL中,包括3个重要的数值:
Key event type
Scan code of the key
Character code of the key.
每次按键都会按顺序产生3个事件:
EEventKeyDown:当键被按下时产生
EEventKey:在键被按下后产生,如果键被保持按下的状态,那该事件则周期性的发生(发生频率可以使用RWsSession::GetKeyboardRepeatRate来查询)
EEventKeyUp:当键被释放后产生
实际设计中,通常使用EEventKeyDown来控制一次行动(如发射导弹)。如果要控制诸如飞船飞行等,那就要使用一个flag来指示该键仍处于按下状态,直到EEventKeyUp发生时才重置这个flag。
如果按键超过很长时间,那EEventkey事件就会多次传递到应用程序,重复的频率可以通过RWsSession::SetKeyboardRepeatRate来修改。这个是很有用的,因为你可以使用EEventKey每隔段时间就产生一次东西,如可以如果按键被长时间按下后,可以每隔0.5秒就发射一次导弹。
TKeyEvent为一个键准备了两个不同的代码,Scan code (TKeyEvent::iScanCode) is a practical choice for processing the key events in games, since it bypasses possible keyboard mappings and FEP. The character code in TKeyEvent::iCode may change, when, for example, a single key is pressed multiple times, depending on how the mappings and FEP are set up. When controlling the activity in the game, it is more important to know which physical key the user has pressed than the resulting character.
缺省时,同时按下多个键是不允许的,只能接受第一个被按下的键的事件。这称为key blocking,缺省时只有电源键和编辑键才是non-blocked keys,而大多数游戏都是需要处理多个键盘同时被按下的情况的(如果一边移动,一边发射武器),我们可以通过如下步骤来实现:
1、CAknAppUi提供了SetKeyBlockMode方法来取消key blocking。
2、Key blocking也可以通过系统设置来改变,但不建议这样做,因为它将对其他程序也发生影响。
下面是个切换key blocking的示例:
void CMyGameAppUi::ConstructL()
{
//Disable key blocking
SetKeyBlockMode(ENoKeyBlock);
}
而游戏中view的OfferKeyEventL的处理应该看起来如下:
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, so the ship will be
// firing periodically too.
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;
}
缺省时是不可以同时处理两个键的,但是我们现在做了ENoKeyBlock处理,那可以同时处理两个键,一个在EEventKey中的周期性响应的键事件,一个是EEventKeyDown后放置flag不停处理的事件。
Symbian中的游戏编程:第二部分
2009-12-29 16:11作者:天极网开发频道出处:天极网软件频道责任编辑:郑重
Window Server
window server是给所有拥有GUI的应用程序所使用的,它提供给应用程序一个接口,从而使它们与其他应用程序交互时更方便。这个server的主要任务就是管理系统资源,象对屏幕和键盘的访问,它使用Symbian系统中client-server体系架构来执行,因此对共享资源的控制更加有力。客户程序和server运行在不同的进程中,不可以直接访问各自的内存地址区域,因此它们之间的通信就要通过一个消息来传递,这个在client和server之间的通道就成为一个session,当session打开后,一个client可以使用session发送一个消息到server端,从而完成一次对server的请求。这个消息包括一个32bit请求类型操作码,和最多4个32bit的参数。在请求完成后,server返回一个32bit完成码到client中,server可以使用进程间通信服务来发送和接收额外的数据。
每个client应用程序与window server的通信都要使用一个window server session类:RWsSession,这个类的主要任务是协调发送到应用程序的异步事件,比如重绘事件,priority key events和标准事件包括用户输入事件等,window server会判断哪个应用程序和窗口会接受这个事件。如,一个键盘事件只能发送到拥有焦点的窗口,而一个重绘时间只能发送到应用程序中当前可见的窗口。Due to the higher process priority of the window server, the events are handled with higher priority than applications' other requests.
(一)、Client Side Buffer
应用程序对window server的请求通常通过如下的步骤来处理:
1). The window server’s client side processes the request.
2). A context switch from the client process to the server process occurs.
3). The window server processes the request,
4). A context switch back to the client process occurs.
这个方法确保了请求在正确的步骤中被处理,而当最后控制重新又回到客户端手中时,请求已经被处理过了。不管怎么说,在两个进程间的环境切换是十分繁杂的过程,会产生比较大的开销。
Even though, the window server has been implemented as a fixed process, which has a fixed virtual address area and hence does not need its address pointers to be updated, the context switch originates unfounded speed losses. The clients, raising a request, also has to wait for a synchronous response. For most of the requests, including drawing methods, this is unnecessary. Because of these drawbacks, the asynchronous function calls are buffered in a client side window server buffer.
In the GT versions of the Symbian OS the buffer size has been fixed to 640 bytes. Series 60 has grown the buffer to 6400 bytes, and added support for applications to alter the buffer size. (注意)Larger buffer size is especially valuable in applications where drawing consists of a number of drawing functions or large amount of text. This can be seen as decreased flickering. The buffer size can be changed in the ConstructL of the application's UI class:
void CMyAppUi::ConstructL()
{
BaseConstructL();
// WS buffer size can be set after the BaseContructL is called
RWsSession &ws = iEikonEnv->WsSession();
TInt bufsize = 10000;
ws.SetBufferSizeL(bufsize);
// Continue normal app UI contruction
iMyView = new ( ELeave ) CMyMainView;
iMyView->ConstructL( ClientRect() );
AddToStackL( iMyView );
}
如此一来,当缓冲被发送时,所有的methods都会在一次message中发送到window server中,这样就只有两次环境切换发生,从而减少了负荷。通常缓冲在如下情况下发生:
1、缓冲满了
2、一个同步method被调用
3、EventReady(), RedrawReady()或PriorityKeyReady()被调用
4、Flush()被调用,或
5、一个会导致缓冲溢出的method被调用
If a drawing is initiated in response to another event than a window server event, an explicit Flush should be called. This is the case, for example, in games, where drawing is usually initiated in response to a timer event.
(二)Windows
应用程序在屏幕上绘图要使用windows,它们是由window server来管理的,下图就描绘了各不同window类之间的关系:
所有的窗口都是从基类:RWindowTreeNode派生的,这个基类指定了他们的z-order。可显示的窗口也同样都是从一个抽象基类中派生的,这就是RWindowBase。RWindow是一个标准窗口,它可以根据程序的要求来绘制或重绘。RBackedUpWindow表现了这样的一种窗口,它将自己的内容都存放在一个backup bitmap中。当RBackedUpWindow的某区域有效时,the window server redraws it automatically from the backup bitmap, without requiring an application redraw. 所有从RWindowBase中派生的类,都有一个显示模式(display mode)用于指定其颜色深度。显示模式是由TDisplayMode这个枚举类型来定义的,而具体模式一般都和屏幕的一致。RWindowGroup是一个non-drawable window,它在
屏幕上是不可见的。The main function of RWindowGroup is to handle the keyboard focus and to form a group for the applications’ other windows. 一般说来应用程序都有个window group,用来联系所有的按键事件。
应用程序的窗口们都是一个树状层次,最端点是windonw group,因此每个drawable窗口都有一个父窗口以及一个或多兄弟以及子窗口。窗口的z-order就是由他们的原始位置所指定的,这个位置和他们的父窗口相关,并使之在兄弟窗口中保持唯一性。当然,我们可以通过改变这个位置从而改变其窗口的drawing order。
最前面的窗口和window group其位置为0。window group的原始位置也同它们的优先级联系在一起。有个API可以访问窗口的原始位置,定义在RWindowTreeNode。
(三)Control Environment
The window server provides a relatively low-level interface, which should be encapsulated in active objects due to the asynchronous services。应用程序可以去实现它,但系统在window server和我们的程序之间提供了一个中间层,这就是control environment(CONE),它封装了window server提供的服务。CONE运行在每个程序的进程中,并被所有拥有GUI的应用程序所使用。CONE的基类是CCoeEnv,这是一个活动对象,当从window server接受到事件后它的RunL函数就会被执行。CONE的主要任务就是评估这些事件,并把它们转发到各个相宜的组件中去处理。CONE提供了一个清除栈,一个active scheduler,以及一些实用函数。CONE使用这个active scheduler来管理从window server来的异步服务。键盘事件的优先级要超过重绘事件,以便程序能更快的响应用户的输入。
CONE提供了应用程序端的控件(control),它是symbian系统中最基本的交互单元了,每个drawable窗口都包含一个或多个控件,这是从抽象基类CCoeControl中继承的。一个拥有整个窗口的control被称为window-owning control,而只覆盖了部分的窗口则称为non-window-owning控件,如寄生的控件。为什么要介绍控件那,因为他们提供了比窗口多的优点。首先,他们比窗口更节约内存的使用,他们也减少了在一个应用程序和window server之间进行client-server通信的必要,因为重绘一个component只需要一个事件,而不管有它拥有多少control。The redraw events are generated for windows and thus a component which consists of more than one window, would need more than one redraw requests. Also the logic to detect intersections, is simpler with controls.
control可以象窗口一样嵌套,一个compound control应该重载两个函数:CountComponentControls和ComponentControl。Compound control也需要处理按键事件的分派问题。
在CONE中维护着一个控件堆栈,用来管理the channeling for the application,控件堆栈包括了那些需要按键事件的控件的列表。事件会根据他们在堆栈中的位置传递到他们那。
CONE can be accessed from the CCoeControl and CCoeAppUi derived classes using the iCoeEnv class member.如果它们都不可用,则可以通过静态函数CCoeEnv::Static()来获得使用,它会返回一个指向CCoeEnv的指针。注意,这方法只能是在没有办法访问到它的情况下才可以使用,因为这里使用了thread-local static(TLS),它是很慢的。
(四)UI Library
CONE自己并不提供任何具体的UI组件,它们是由UI库来提供的。s60提供了一个UI库,Avkon,这是从标准的symbian系统UI库——Uikon——中扩展和修改来的。Avkon提供了各种组件,这些都是专为s60屏幕量身定做的。
Avkon UI组件根据它们在屏幕上的布局可以机分为3个大类:状态pane,主pane以及control pane。可以参见下图来看:

status pane包括几个子pane:signal pane, context pane, title pane, battery pane, uni indicator pane和navi pane。每个程序都有一个状态pane的实例,它是在应用程序开始启动时自动生成的。这里signal pane, battery pane和uni indicator pane是属于一个system side server的,它们不能被应用程序所改变,其他的pane是可以的。而main则是应用程序数据正常显示的地方。Avkon UI库提供了各种组件,应用程序也可以为main pane生成它们自己的控件。Avkon UI库所提供的组件包括:list boxes, grids, form和不同类型的弹出窗口,如询问和选项菜单。而control pane包括softkeys和一个滚动指示器。
尽管main pane的区域已经能适应大部分程序的需要了,但对游戏来说,一般需要更大的空间:),更大的游戏视图空间提了游戏的可玩性。
如果要将游戏的视图覆盖整个屏幕,可以针对,偶控件调用CCoeControl::SetExtentToWholeScreen函数。这样status和control pane就被隐藏不见了。它们可以通过调用CCoeControl::MakeVisible函数(为EFalse)来完成。
Bitmaps
----------
symbian操作系统可以被认为是一个bitmap-oriented OS。
symbian系统提供了它自己的位图格式,MBM,这是个多位图的文件。MBM利用一个位图转换工具bmconv从windows位图中生成的。作为一个MBM,它可以包括多个位图,bmconv会生成洋位图header文件,MBG文件,它列举出了所有位图的ID,以便访问它们。当要从一个MBM文件中装入一个位图时,就应该include相应的头文件,并将正确的ID做为给定的参数。
bmconv可以从命令行运行,也可以在一个工程文件中进行如下定义:
START BITMAP [target-file]
HEADER
TARGETPATH [targetpath]
SOURCEPATH [sourcepath]
SOURCE [colour-depth] [source-bitmap]
END
bmconv可能生成两个不同类型的symbian位图:ROM和non-ROM位图。non-ROM的位图就是一般的文件存储位图,使用run-length encoding(RLE)进行了压缩,可以在它们要被使用时装入RAM,而为了加快绘制速度,ROM位图是不被压缩的,因此它们可以直接从ROM中使用,缺省时bmconve生成的是文件存储位图,即non-ROM的。
注意,制作sprite图时我们会用到mask位图,因为只有黑白色,因此它最好被存储为1bit的图,s60提供了一个命令行工具叫makemask,它可以从8bit bitmaps中生成1bit的mask。Makemask uses the last palette index in the original bitmap as a transparent colour.
尽管symbian系统提供了一些API来设置位图的调色板,但没有被实现。The APIs were supplemented before any support for colour displays was implemented.
位图通过CFbsBitmap类来管理,它提供了生成个装入位图的方法,并可以定义它们自己的颜色深度和大小。它使用了RFbsSession类来访问FBS,因此对用户隐藏了session类。CFbsBitmap也提供了直接访问位图数据的方法。可以通过DataAddress成员来获得指向数据地址的指针,也可以通过访问GetScanLine成员来访问到一个specific scan line。
位图也根据他们的大小而在FBS分为两个不同的堆,以4kb为个界限。大尺寸的图形可以进行自动的磁盘碎片整理。而因为要整理,因此这个堆应该可以locked,这里TBitmapUtil可以提供堆的锁定和解锁,而有当位图的image data被编辑时,我们也需要锁定堆。
drawing和拷贝都提供了自动锁定。
// Lock the heap if a large bitmap
if ( bitmap->IsLargeBitmap() )
{
TBitmapUtil bitmapUtil( bitmap );
bitmapUtil.Begin( TPoint(0,0) );
}
// Edit bitmap
TSize bitmapSize = bitmap->SizeInPixels();
TUint16* bitmapData = (TUint16*)bitmap->DataAddress();
TUint16 colour = 0;
for ( TInt y = 0; y < bitmapSize.iHeight; y++ );
{
for ( TInt x = 0; x < bitmapSize.iWidth; x++ )
{
*bitmapData++ = colour++;
}
}
// Release the heap
if ( bitmap->IsLargeBitmap() )
{
BitmapUtil.End();
}
这可以使位图的绘制比CFbsBitmaps更快,window server提供了它自己的位图类,CWsBitmap。它可以避免在window server和FBS之间额外的环境切换,通过自主掌握bitmap handle。这CWsBitmap是从CFbsBitmap中派生的。注意,当绘制速度要求很高时,我们应该使用CWsBitmap。
Drawing
---------
应用程序使用windows在屏幕上绘制,可以使用CWsScreenDevice来实现,它与CWindowGc相联系,这样graphics的context和device都有了。
The drawing methods of CWindowGc are buffered on the client side window server buffer.
Drawing可以在系统或应用程序初始化时引发,系统初始化的绘制是当窗口生成时,或窗口的内容无效时引发的。当一个窗口要被重绘时,window server就会发送一个redraw event到应用程序中。CONE就可以处理控件的重绘处理,这就是为什么每个控件都应该处理Draw方法,从而能正确完成其redraw事件。
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) );
}
上面的TRect参数就表明了具体的无效区域,大部分控件是忽略这个参数的,因为它们的重绘很简单,即使整个区域也不会太慢。
应用程序的drawing则需要应用程序数据或状态的改变。CCoeControl提供了非虚函数DrawNow()可以让window server进行控件的重绘。
CCoeCOntrol也提供了DrawDeferred方法,它可以使窗口无效,并重新从window server发出一个redraw event。这两种方法的不同之处在于,DrawNow强制控件立即重绘自己,但DrawDeffered则是低优先级的重绘,因为CONE将用户输入设定为高优先级的了,因此用户的输入事件要先处理。这些方法,不管怎么都是加重了操作的负担,通常只应该告诉它应该重绘的部分。
如下代码所示:
void CExampleControl::DrawBitmap( const TPoint& aPoint, const CFbsBitmap* aBitmap )
{
// Get the system graphics context and control rectangle
CWindowGc& gc = SystemGc();
// Establish drawing rectangle
TRect rect = TRect( aPoint, TSize( aBitmap.iWidth, aBitmap.iHeight ) );
// Activate graphics context
ActivateGc();
// Invalidate window
Window().Invalidate( rect );
Window().BeginRedraw( rect );
// Draw a bitmap
gc.DrawBitmap( aPoint, aBitmap );
Window().EndRedraw();
// Deactivate graphics context
DeactivateGc();
}
(一)Sprites
Sprites是一个masked bitmap,它用不着重绘后面的窗口就可以移动。Symbian系统中提供了两种不同类型的sprites:pointer和animated bitmaps。
如图(spritesclass.gif)
RWsSpriteBase是一个抽象的sprites基类,它拥有一个或多个TSpriteMembers,它包含了sprite的位图数据,TSpriteMember也定义mask位图,以及位图的定位,还有轮显的时间间隔。RWsSprite是sprites的一个具体类,除了构造,它只提供了一个方法,SetPostion,我们可以使用它来移动这个sprite,下面的代码就演示了以上的内容:
RWsSprite sprite = RWsSprite( iEikonEnv->WsSession() );
User::LeaveIfError( sprite.Construct( Window(), TPoint(0,0), 0 );
for ( TInt i=0; i < 8; i += 2 )
{
iMember[i/2].iBitmap = new ( ELeave ) CFbsBitmap();
User::LeaveIfError( member.iBitmap->Load( KBitmapFile, i, EFalse ) );
iMember[i/2].iBitmap = new ( ELeave ) CFbsBitmap();
User::LeaveIfError( member.iMaskBitmap->Load( KBitmapFile, i+1, EFalse ) );
iMember[i/2].iInvertMask = EFalse;
iMember[i/2].iOffset = TPoint(0,0);
iMember[i/2].iInterval = TTimeIntervalMicrosecond32(100000);
User::LeaveIfError( sprite.AppendMember( iMember[i/2] ) );
}
注意在sprite member被更新和添加后,sprite应该调用RWsSpriteBase::Activate来激活。这样sprite就可以在屏幕上显示了,并可以移动。
而sprite的内容可以通过调用RWsSpriteBase::UpdateMember来改变。CFbsBitmaps are also accessible to the window server, only the bitmap handles of a sprite are sent to the window server. This makes updating of a sprite’s bitmaps considerably fast.
当一个sprite不再需要时,我们应该调用RWsSpriteBase::Close来释放占用的window server资源。这样并不会释放客户端的成员数据,它们应该被删除掉。RWsPointerCursor则可以生成一个cursors,和RWsSprite一样使用,只有点不同,那就是当pointer已经activated后,不会显示在屏幕上,直到RWindowTreeNode::SetPointerCursor被调用。
(二)Double buffering
当游戏中包含多个需移动的图片而更新又比较频繁时,我们应该尽量填满客户端的缓冲,以便所有对象可以一次更新完毕。对用户来说,可能会发生屏幕闪烁的现象,我们可以通过双缓冲的方法来解决这一问题,图片首先可以先绘制到off-screen位图中,最后再做为一次window server操作绘制到屏幕。特别是在一秒要多次更新屏幕的,更应该使用这种方法。
一个off-screen位图可以通过使用位图graphics context和graphics device类:CFbsBitGc和CFbsBitmapDevice来生成它。
当程序要在窗口中绘制位图时,it gets converted into the same display mode than the window.这样一来,操作就会放慢。对游戏来说,如果要使用位图表现动画,则应该在动画开始时完成这种转换。可以参考下面的列子:
CFbsBitmap* CExampleControl::LoadAndConvertBitmapL(Const TDesC& aFileName, TInt aBitmapId )
{
// Load the bitmap
CFbsBitmap* originalBitmap = new ( ELeave ) CFbsBitmap();
CleanupStack::PushL( originalBitmap );
User::LeaveIfError( originalBitmap->Load( aFileName, aBitmapId, EFalse ) );
// Create a new bitmap, graphics device and context
CFbsBitmap* newBitmap = new ( ELeave ) CFbsBitmap();
CleanupStack::PushL( newBitmap );
newBitmap->Create( originalBitmap->SizeInPixels(), Window()->DisplayMode() );
CFbsBitmapDevice* graphicsDevice = CFbsBitmapDevice::NewL( bitmapConverted );
CleanupStack::PushL( graphicsDevice );
CFbsBitGc* graphicsContext;
User::LeaveIfError( graphicsDevice->CreateContext( graphicsContext ) );
TPoint zero(0,0);
// Blit the loaded bitmap to the new bitmap
bitmapContext->BitBlt( zero, originalBitmap );
CleanupStack::Pop(3);
delete bitmapContext;
delete bitmapDevice;
delete originalBitmap;
return newBitmap;
}
上面的代码从MBM加载位图后,就转换为window的显示模式,并产生一个位图用来做缓冲。如果游戏有多个位图需要转化,我们应该在初始化场景或关卡时一次做完。从而对用户隐藏这些操作。
Direct draw
---------------
在屏幕上绘制,要使用window server,这就需要场景的切换了,这样一来就减低了绘制的速度。我们能不能饶开window server,从而摆脱掉场景切换(context switch)那?可以的,程序可以直接访问屏幕,symbian中可以有两种方法在屏幕上直接绘制。
CFbsScreenDevice就是这样的一个graphics device,它可以直接定址到屏幕driver, SCDV.DLL。在生成CFbsBitGc后,它可以同其他grahics device一样使用。这样就可以直接绘制到屏幕上,而无须通过window server了。而另一个方法,就是向屏幕寻址。我们可以通过使用UserSrv类来完成:
TPckgBuf infoPckg;
TScreenInfoV01& screenInfo = infoPckg();
UserSvr::ScreenInfo(infoPckg);
TUint16* screenMemory = screenInfo.iScreenAddress + 16;
屏幕内存有一个32byte头,我们写内存的时候需要把它考虑在内。
直接写屏幕内存的方式要比使用CFbsScreenDevice来得更加快速。
In some Symbian OS based terminals the screen is automatically updated from the screen memory when the memory is changed, whereas in other terminals the drawing needs to be explicitly activated.
注意屏幕内存寻址只能在目标机器上有效,因此代码需要按实际机器还是模拟器来分别组织,参见下面代码:
#ifdef __WINS__ // Emulator environment
// Draw to an off-screen bitmap
#else // Hardware environment
// Draw directly to the screen memory
#endif
以上两种方法带来一个弊端,这就是因为屏弃了window server,所以程序不会得到通知——如其他窗口或窗口组到了前台。尽管程序会在失去焦点时收到一个事件,但这不能及时的停止绘画,屏幕可能会变乱。
这里有个未公开的API(在6.x系列前)可以很好的解决这个问题,这个API包括两个类:MDirectScreenAccess,它提供给应用程序一个回调函数,一个是CDirectScreenAccess,它提供了与window server之间的通信。下面的代码演示其是如何构造的:
iDrawer = CDirectScreenAccess::NewL(iEikonEnv->WsSession(), *iEikonEnv->ScreenDevice(), Window(), *this);
iEikonEnv->WsSession().Flush();
iDrawer->StartL();
iDrawer->ScreenDevice()->SetAutoUpdate(ETrue);
这里CDirectScreenAccess的NewL方法会需要一个window server session,CONE的graphics device,应用程序的窗口,以及一个指向从MDirectedScreenAccess派生的类做参数。在CDirectScreenAccess::StartL被调用来activate the direct draw support之前,the client side window server buffer should be flsushed.为了让屏幕自动更新,screen device的SetAutoUpdate方法应该调用(使用ETrue做为参数),这些工作完成以后直接绘制就可以开始工作了,CDirectScreenAccess生成了CFbsBitGc这个graphics context,我们可以使用下面的语句来绘制:
iDrawer->Gc()->BitBlt( TPoint(0, 0), iBitmap);
当另一个窗口前成前台时,CDirectScreenAccess会从window server得到一个事件从而终止绘制,这时CDirectScreenAccess会调用MDirectScreenAccess派生类的AbortNow方法,我们程序需要重载它以便处理终止,为了防止屏幕变乱,window server不会在重叠的窗口上绘图,知道abort drawing事件被处理完毕。
Symbian中的游戏编程:第三部分
2009-12-29 16:18作者:天极网开发频道出处:天极网软件频道责任编辑:郑重
Communications
------------------
在这个章节中,主要讨论symbain系统的连接组件。连接对游戏来说是非常重要的。
(一)Communications architecture

如上图,symbian系统支持多种连接方式,从蓝牙到GPRS。
The communications architecture of the Symbian OS is based on top of three commucnication servers: ETEL, C32 and ESOCK。由server所提供的连接服务,是种异步的操作,因此他们需要封装在活动对象之中。一般来说,一个客户应用程序会生成3个不同的活动对象:
一个用来发送数据,一个用来接收数据而另一个作为应用程序端的连接引擎。参见下图:
(communications.gif)
其中ETEL是一个电话服务,用来提供给应用程序访问各种电话硬件或服务的接口,如GSM handsets, analog modems and fax services。这个server使用的是动态装入的插件模块,telephony server modules(TSYs)。
Serial Communications Server
(主要是红外的一组API可以参见原文)
Sockets Server
The sockets server (ESOCK) provides an interface to communications protocols using sockets.
socket server使用了协议模块,如TCP/IP,IrDA和Bluetooth,都会在运行时动态装入的。一个协议模块可以包含多个协议,如IrDA模块包括低层的IrMUX,IrTinyTP,IrLAP,IrLMP和IrObex协议。Common protocol modules can be develop by application developers, correspondinglyl to TSY and CSY modules.
sockets server中的客户端API的主要类是RSocketServ,和RSocket,和RCommServ以及RComm相似,RSocketServ handles a session for the server and provides information about available protocols, 但不能提供任何数据传送服务,它们是由socket类RSocket所提供的,做为客户端的API来说,所有的协议都是一样的,一个协议的属性语义等是由TProtocolDesc结构来维护的,我们可以使用RSocketServer::GetProtocolInfo方法来访问到当前装入的协议的有关信息。
socket server至少需要2个不同的socket来完成功能,一个用来监听连接请求,一个用来建立连接并传递数据。socket server的客户端API定义在es_sock.h头文件里。
对开发者来说一个非常有用协议就是蓝牙:)它提供了快速,短距离的连接方案。而且它可以免费使用,因此很多游戏都使用了它。蓝牙的
优点在于它比红外距离更远,连接更稳定。具体可以参见相关文章或SDK:)
Game Data Receiving
---------------------
为了使游戏能够接受游戏数据,s60允许第三方游戏将他们的数据文件格式通过MIME(Multipurpose Internet Mail Extensions)注册到系统中。The MIME types are used by communication applications, like the WML browser and the messaging application, to find out the path where a file, having a specific type, should be saved.这些文件可以包括,新的关卡,武器或图片。
在s60中,游戏的MIME类型为:application/X-NokiaGameData-,这里是游戏在symbian系统中的UID的最后8位数字。这个MIME类型在symbian系统应用程序信息文件,aif中应有如下描述:
RESOURCE AIF_DATA
{
app_uid=0x12345678; // Application UID
datatype_list =
{
DATATYPE
{
priority = EDataTypePriorityHigh;
type = “application/x-NokiaGame-Data-12345678”;
}
};
}
DATATYPE的优先级,表明当前程序会怎样对待这些数据格式。EDataTypePriorityHigh表明其他程序无法处理这data formats。
接收数据的文件的目标路径在ini文件中指定,名称为.ini。这个文件应该以unicode格式编码,它包括了
SDDataDir = ,这里是接受文件的路径这个路径和游戏数据的缺省目录联系在一起,c:\nokia\games,当游戏被装入后,ini文件需要被拷贝到\System\SharedData目录中,这可以通过在游戏的pkg文件中很方便的指定。
s60为游戏数据文件指定了标准的头结构,需要附着在MIME类型才能工作。具体结构见下面表: