在游戏设计中,要注意的一个首要问题便于能够处理系统的各种事件,如一个电话或短信的到来,当这些系统时间发生时,前台的程序会失去焦点,这时我们可以通过重载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不停处理的事件。
Series60游戏设计参考(一)
最新推荐文章于 2024-08-16 23:20:20 发布