电脑技术学习

编写安全的Symbian C++游戏代码

dn001

字体缓冲区类 - 还是使用Gc的DrawText函数绘制文字。但是同时用一张位图作为一个缓冲区存储最近绘制的文字。既能支持大字符集合,速度也很快。
如果需要学习图形和直接写屏的基础,请参考Programming Games in C++ v1.0(www.forum.nokia.com/main/1.6566.21.00.html)。本文主要针对图像类和直接写屏类讲几个容易被忽略的问题。

1.4.1.;图像类的直接内存访问

贴图是2D游戏最主要的画面操作。为了实现快速的贴图,或者实现某种混合效果,就不能再使用CFbsBitGc的BitBlt或者BitBltMasked进行贴图,而必须自己得到图片的内存地址,直接读写其中的数据。在读写图片内存地址的过程中,有几点需要加以注意。
首先,只有当源图片和目标图片色深相等时,才更容易进行贴图操作。所以,再载入图片的过程中,我习惯把非4k色的图片转化为4k色。之所以选择4k色是因为它也是后台缓冲区的色深。下面的代码通过转换可以保证iImage是4k色的图像。
// Make sure that we have a 4K color depth image in iImage
;if (iImage->DisplayMode() != EColor4K)
;{
;;// Create 4k color image
;;CFbsBitmap* image = new (ELeave) CWsBitmap();
;;CleanupStack::PushL(image);
;;User::LeaveIfError(image->Create(iSize, EColor4K));

// Create device
;;CFbsBitmapDevice* device = CFbsBitmapDevice::NewL(image);
;;CleanupStack::PushL(device);
;;CFbsBitGc* gc;
;;User::LeaveIfError(device->CreateContext(gc));
;;CleanupStack::PushL(gc);

// Bitblt to new color depth
;;gc->BitBlt(TPoint(0,0), iImage);

// Destroy context and device;
;;CleanupStack::PopAndDestroy();;// gc
;;CleanupStack::PopAndDestroy();;// device
;;CleanupStack::Pop();;;// image
;;delete iImage;
;;iImage = image;
;}
其次,Symbian系统在内存匮乏时会进行碎片整理。所以如果简单的用CFbsBitmap::DataAddress获取内存首地址并开始读写,那么可能在你读写的过程中,图片已经被悄悄的移动了位置,你读写的就是一块无效的内存区域。解决这个问题的办法是在获取首地址前,必须先锁定图像内存区域。在高版本的60系列SDK中(比如2.0,2.1),有LockHeap和UnlockHeap函数可以完成这个操作。但是在低版本的SDK中(比如0.9,1.0),这两个函数是私有的。我们必须通过TBitmapUtil锁定内存。但是不一定必须使用TBitmapUtil的SetPixel和GetPixel函数进行位操作。下面是最基本的没有关键色和Alpha通道的简单贴图代码。
void CImage::RenderToBitmapL(CFbsBitmap* aBmp, TPoint aPos, const TRect& aRect)
{
;// 在此计算贴图目标矩形区域
;// 代码略去

// 没有关键色和蒙板的最简单、最快情况
;if (!iKey && iMask == NULL)
;{
;;// 锁定
;;TBitmapUtil bmpUtil1(ImageL());
;;TBitmapUtil bmpUtil2(aBmp);
;;bmpUtil1.Begin(TPoint(0,0));
;;bmpUtil2.Begin(TPoint(0,0), bmpUtil1);

// 获取首地址
;;TUint16* addr2 = (TUint16*)ImageL()->DataAddress();;// source image
;;TUint16* addr = (TUint16*)aBmp->DataAddress();;// target bmp
;;TInt line = aBmp->ScanLineLength(
;;;aBmp->SizeInPixels().iWidth,
;;;EColor4K) / 2;
;;TInt line2 = iImage->ScanLineLength(;;// line length in 16bit word
;;;iImage->SizeInPixels().iWidth,
;;;EColor4K) / 2;

// 计算扫描持续量和跳跃量
;;TInt jump = line - rectw;
;;TInt lasting2 = rectw;
;;TInt jump2 = line2 - lasting2;

// 获取贴图首地址
;;TUint16* p = addr + fromY * aBmp->SizeInPixels().iWidth + fromX;
;;TUint16* p2 = addr2 + line2 * recty + rectx;
;;
;;// The first pixel out of interest
;;TUint16* p2end = p2 + line2 * (toY - fromY - 1) + lasting2 + jump2;

// 开始扫描
;;while(p2 != p2end)
;;{
;;;// 开始一个扫描行
;;;TUint16* p2endline = p2 + lasting2;
;;;while(p2 != p2endline)
;;;{
;;;;// 复制一个像素
;;;;*p = *p2;
;;;;// 移动到下一个像素
;;;;p++; p2++;
;;;}
;;;// 跳到下一行
;;;p += jump; p2 += jump2;
;;}

// 解锁
;;bmpUtil2.End();
;;bmpUtil1.End();

return;
;}

// 其它情况。有关键色等等.
;// ...

最后告诉大家几个优化的小窍门:
;;使用While循环直接把指针的比较作为循环结束条件。不要再多用一个整数来控制循环。
;;贴图是个两重循环,如果你的代码需要判断是否支持关键色和Alpha通道等,尽量把判断外移到循环之外。每个象素都进行好几个if判断的开销太不值得。比如上面的代码,处理最简单的情况时,while循环内一个if都没有。
;;4k色时,RGB内存排列如下图。所以未被使用的4位正巧可以用来存储alpha通道。

1.4.2.;直接写屏和特殊系统事件

游戏软件一般用CDirectScreenAccess进行直接写屏。大家都知道,WindowServer会在需要停止直接写屏时回调MDirectScreenAccess::AbortNow接口函数,在可以重新启动时回调MDirectScreenAccess::Restart接口函数。可是具体在这两个函数中做什么,SDK没有过多的介绍。我在此说一下我的做法。如果你合理的处理了这两个函数,就可以轻松应对来电、屏保、程序切换等事件。
我们先说AbortNow,它的处理比较简单。你之需在其中停止驱动游戏逻辑的计时器(一般是个CPeriodic对象),停止声音模块(一般是一个CActive任务)就可以了。
值得费些力气的是Restart函数,它并不是在应用程序回到前台,并且可以进行全屏直接写屏时才被回调。所以不能在此时武断的恢复游戏逻辑,开始游戏。
首先,你要调用CDirectScreenAccess::StartL( )恢复直接写屏。但是必须给这个函数加上TRAP保护。因为它很可能抛出KErrNotReady异常。如果遇到这个异常,那你就直接返回好了,因为直接写屏此时并不能开始。接下来你需要检查一下绘图区域,看是否整个屏幕都可以被使用。如果不是,那也无需启动游戏逻辑,只需要用最后保留的后台缓冲区的内容更新直接写屏区域即可。第三种情况,如果直接写屏成功启动,并且整个屏幕都可以被绘制,才启动游戏逻辑,启动声音等其它模块。
完整的代码如下:
void CEngine::AbortNow(RDirectScreenAccess::TTerminationReasons /*aReason*/)
{
;// Cancel timer and display
;if (iGameTimer->IsActive())
;;iGameTimer->CancelTimer();
;if (!iGameWorldPaused)
;{
;;iGameWorldPaused = ETrue;
;;iGameWorld->PauseGame();;// Pause audio stream etc.
;}
;iPaused = ETrue;
}

void CEngine::Restart(RDirectScreenAccess::TTerminationReasons /*aReason*/)
{
;TRAPD(error, SetupDirectScreenAccessL());
;switch(error)
;{
;case KErrNone:
;;break;
;case KErrNotReady:
;;if (iDirectScreenAccess->IsActive())
;;;iDirectScreenAccess->Cancel();
;;if (iGameTimer->IsActive())
;;;iGameTimer->CancelTimer();
;;if (!iGameWorldPaused)
;;{
;;;iGameWorldPaused = ETrue;
;;;iGameWorld->PauseGame();
;;}
;;return;
;default:
;;User::Panic(_L("Setup DSA Error"), error);
;}
;
;if(iPaused)
;{
;;if(iGameDawingArea == iRegion->BoundingRect())
;;{
;;;iPaused = EFalse;;
;;;if(!iGameTimer->IsActive())
;;;{
;;;;iGameWorldPaused = EFalse;
;;;;iGameWorld->ResumeGame();
;;;;iGameTimer->Restart();
;;;}
;;}
;;else
;;{
;;;PauseFrame();
;;}
;}
;else
;{
;;if(!iGameTimer->IsActive())
;;{
;;;iGameTimer->Restart();
;;}
;}
}

void CEngine::SetupDirectScreenAccessL()
{
;// Initialise DSA
;iDirectScreenAccess->StartL();

// Get graphics context for it
;iGc = iDirectScreenAccess->Gc();
;
;// Get region that DSA can draw in
;iRegion = iDirectScreenAccess->DrawingRegion();
;
;// Set the display to clip to this region
;iGc->SetClippingRegion(iRegion);
}

void CEngine::PauseFrame()
{
;// Force screen update: this is required for WINS, but may
;// not be for all hardware:
;iDirectScreenAccess->ScreenDevice()->Update();
;
;// and draw from unchanged offscreen bitmap
;iGc->BitBlt(TPoint(0,0), &(iDoubleBufferedArea->GetDoubleBufferedAreaBitmap()));
;
;iClient.Flush();
}
};

1.5.;声音处理

我的引擎中使用CMdaAudioOutputStream和MMdaAudioOutputStreamCallback完成声音播放功能。它主要有三个类组成:
;;CAudioStreamPlayer。它复合CMdaAudioOutputStream,继承CActive,实现MMdaAudioOutputStreamCallback接口。我们需要小心的维持缓冲区的大小以获得低延迟播放。CActive不断的建立新的任务,在RunL函数中估算缓冲区中的剩余数据,向其中追加适当的数据,维持缓冲区的预期大小。
;;CSimpleMixer。它实现CAudioGenerator接口。因为CMdaAudioOutputStream是一个单一的流式播放器,所以需要写一个混音器进行波形混合。这里波形混合就是简单的数据相加。混音器有许多的声道(channel)。每个channel记录了其中的CAudio指针和当前播放位置。
;;CAudio。包含一个音频缓冲区。对每个声音文件,我们还需要一个类把它载入到内存缓冲区中。
我不会在此讲解如何实现音频播放,那需要单独的一篇文章。如果你也使用这种方法实现声音播放,我只想在此和大家讨论两个问题。
需要学习声音基础的话,可以参考www.newlc.com/article.php?id_article=113。(可惜我当时学习声音时那篇文章和代码找不到了)

1.5.1.;声音的关闭和开启
因为整个音频系统是一个拉的结构,音频流从混音器那里拉数据,混音器从音频缓冲区中拉数据。所以,只要把CMdaAudioOutputStream和写数据的CActive对象delete掉,声音播放就全部停止了。在我的实现中,也就是delete CAudioStreamPlayer对象即可。再想要开启声音,只需要重新创建这个对象。
这个实现的好处是程序的其它部分不需要保存声音是否开启这个状态。因为CAudio和CSimpleMixer对象是存在的,CAudio就可以把自己插入到Mixer的channel中,觉得自己好像在播放一样。其实因为CAudioStreamPlayer根本没有从Mixer向外拉数据,声音设备是完全停止的。
但是在恢复声音播放时有一点需要注意,恢复前需要清空混音器中的声音数据。因为经过了长时间的运行,混音器中的各个channel中已经塞满了各种声音。如果此时突然打开,会传出各种延迟了的杂音。

1.5.2.;特殊错误处理
MMdaAudioOutputStreamCallback接口中的几个回调函数MaoscOpenComplete、MaoscBufferCopied和MaoscPlayComplete都有一个错误码参数。你不能忽略这个参数。
比如MaoscPlayComplete函数,是在音频停止播放时被调用。停止播放的原因可能是多种多样的。我们都知道要处理KErrUnderflow这个情况,这个错误吗意味着混音器没有及时的供给它音频数据。此时需要重新启动声音流。但是还有一些情况比如KErrDied和KErrInUse很容易被忽略。KErrDied发生在接听电话时,此时声音线程已经死了,那么就需要重建整个音频系统。KErrInUse发生在收到短信时,此时声音设备被抢占,用来播放短信提示音。此时你也需要重建整个声音系统,但是此时不能立刻重建,否则还是一样的结果。你应该等待几秒钟之后才重建它。
上面说的重启声音流和重建声音系统深度不同。重启声音流在稍后的代码中可以看到。其中RunAudioL向音频流写入了第一个声音缓冲区。重建声音系统在我的实现中就是指先delete 再NewL创建CAudioStreamPlayer对象。
这三个错误的处理代码如下:
// Audio stream API callback: Called when playback has finished.
void CAudioStreamPlayer::MaoscPlayComplete(TInt anError)
{
;if (m_bInDelay)
;;return;
;// If we finish due to an underflow, we'll need to restart playback.
;// Normally KErrUnderlow is raised at stream end, but in our case the API
;// should never see the stream end -- we are continuously feeding it more
;// data! Many underflow errors mean that the latency target is too low.
;if ( anError == KErrUnderflow ) {
;;iObserver->MasoMessage(_L("Play Underflow"));
;;// The number of samples played gets resetted to zero when we restart
;;// playback after underflow
;;iBaseSamplesPlayed = iSamplesWritten;

// Stop and restart
;;iStream->Stop();
;;Cancel();
#ifdef RATE_16K
;;iStream->SetAudioPropertiesL(TMdaAudioDataSettings::ESampleRate16000Hz,
; TMdaAudioDataSettings::EChannelsMono);
#else
;;iStream->SetAudioPropertiesL(TMdaAudioDataSettings::ESampleRate8000Hz,
; TMdaAudioDataSettings::EChannelsMono);
#endif
;;iStream->SetVolume(iStream->MaxVolume() / 4);
;;TRAPD(error, RunAudioL());
;;if ( error != KErrNone ) {
;;;User::Panic(KPlay, error);
;;}

return;;;;
;}
;else if ( anError == KErrDied )
;{
;;m_bInDelay = ETrue;
;;m_RebuildDelay = 0;;// no delay
;}
;else if ( anError == KErrInUse )
;{
;;m_bInDelay = ETrue;
;;m_RebuildDelay = 3000;;// delay 3 second
;}
;else if ( anError != KErrNone ) {
;;// Some other error, panic!
;;User::Panic(KPlayComplete, anError);
;}
}
由外界发现m_RebuildDelay标志,重建CAduioStreamPlayer这个对象。
除了MaoscPlayComplete,我在MaoscBufferCopied中忽略了KErrUnderflow和KErrAbort错误。在MaoscBufferCopied和MaoscOpenComplete也处理了KErrInUse错误。
经过上面的处理,我的程序已经可以安全的应对来电、短信、切换程序等特殊情况了。


作者简介:
姓名:冯兆麟
网民:Simba
E-mail:kingsimba@tom.com
个人主页:www.fsgame.net; cosoft.org.cn/projects/fslib
本文来自:http://blog.csdn.net/emag_mobile/archive/2005/02/23/298840.aspx

标签: