`
totoxian
  • 浏览: 1073903 次
  • 性别: Icon_minigender_2
  • 来自: 西安
文章分类
社区版块
存档分类
最新评论

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

阅读更多

本文作者: 冯兆麟(Simba) (kingsimba@tom.com)

本文献给使用Nokia Symbian 60 SDK各个版本开发游戏软件的程序员。虽然本文主要是针对游戏软件,但是大部分内容对一般应用软件也同样适用。

1.1.声明

为了避免良心的谴责,首先我必须承认一点,我本人并不是靠Symbian C++糊口。除了forum.nokia.com上的文章和SDK,我也没有看过任何关于Symbian的书籍。只是偶然的,我在天津猛犸游戏公司(www.mammothworld.com)认识并接触了Symbian。我从零起步,写出了一个蹩脚的Symbian游戏引擎并在3650、7650上开发了一些游戏。所以我对Symbian的掌握完全是出于自己的猜测和理解,虽然本文缺乏权威,但至少都是经验之谈,容易理解。

1.2.概述

Symbian游戏是运行在手机上的游戏,它不能干扰手机正常的通讯功能,对操作系统和其它应用程序必须友善。而在首次编写Symbian C++游戏时,我遇到了无数奇怪的问题,其中大部分问题出在内存极端不足,打开太多应用程序,屏幕保护探出,接到短信、电话等特殊情况下。
其实如果养成严谨的代码风格,进行足够的错误处理,大部分问题本可以避免。为了解决它们,我很是花了一番功夫,所以在此把我的一些教训、经验写出,希望大家能避免犯同样的错误。
如果你不是业余爱好者,而是为一个认真的开发商工作,特别是如果你的产品需要通过Symbian Signed认证(www.symbiansigned.com),你就必须更加小心的对待本文提出的问题。
Symbian Signed是一个针对Symbian应用程序的认证,想要通过它,你的应用程序必须通过一系列严格的测试。认证对应用程序的文件管理、内存使用、系统事件响应、网络、资费和私人数据等都有一定的要求。如果想了解Symbian Signed认证的详细内容,可以去它们的网站下载白皮书。

1.3.异常处理

虽然我们都知道任何一个new (ELeave)或者带有L后缀的函数都可能抛出异常,但是很多业余的爱好者还是会忽视Symbian C++中异常处理的重要性。虽然有些函数只有在极其罕见的情况下才会抛出异常。但不是危言耸听,如果你不写代码捕捉并处理它们,应用程序就会遇到"系统错误"。
普通C++使用throw抛出异常。异常抛出后,栈会不停回滚,直到遇到最近一层catch为止。Symbian C++中的异常处理不使用try-catch和throw。但是它的处理机制和标准C++很是类似,区别仅仅是它只能抛出一个整数错误码,而不是一个任意对象。
我将从异常的抛出、捕捉、处理三方面讲解这部分内容。

1.3.1.抛出异常
Symbian C++中,有下面几种情况下会抛出异常:
使用静态函数User::Leave抛出异常。这个函数就是最基本的异常产生函数。下面讲的其它抛出方式都可以转化为User::Leave。
使用静态函数User::LeaveIfError把错误码转化为异常。有些函数比如CFbsBitmap::Create( )有一个TInt的返回值。如果遇到错误,这些函数就会返回非KErrNone的错误值。此时,可以使用LeaveIfError把这个返回值转化为异常。比如:User::LeaveIfError(bmp->Create(iSize, EColor4k); 其实LeaveIfError就是if (returnValue != KErrNone) User::Leave(returnValue);
使用new (ELeave)申请内存。如果没有足够内存可用,此操作产生一个KErrNoMemory异常。比如TText8* p = new (ELeave) TText8[20]; 相当于TText8* p = new TText8[20]; if (p == NULL) User::Leave(KErrNoMemory);
调用带有L后缀的函数。Symbian系统的命名规范中要求,每一个可能Leave的函数都要有后缀L。包含有带L的内层函数调用的外层函数也必须加上L。这类函数中最常见的就是NewL, NewLC和ConstructL。这个规范比你想象的要重要。因为它给其他程序员一个暗示,提示他们对这些函数进行保护。

1.3.2.捕捉异常
类似标准C++的catch语句,Symbian C++的TRAP关键字可以对一个可能产生异常的函数进行保护,并且捕获到异常值。比如:
TInt errorCode;
TRAP(errorCode, SomeDangerousFuncL( ) );// 保护执行SomeDangerousFuncL( )函数
if (errorCode != KErrNone)
{
// 捕捉到了一个异常,在这里添加处理异常的代码
}
类似的TRAPD省去了你声明一个局部变量的麻烦。头两行代码可以简写成:
TRAPD(errorCode,SomeDangerousFuncL( ) );

1.3.3.处理异常
对于不同的异常当然有不同的处理方法(废话:-))。我们以最常见的捕获到代表内存不足的KErrNoMemory异常为例讲解。
注意在Container,AppUi等类的构造过程中,你不需要加入对内存不足的保护。因为这一切系统已经为你做好了。系统会弹出一个对话框报告内存不足。根据你的操作系统版本不同,这可能是中文的,也可能是英文或者其它语言的。如果你不信,可以在AppUi或者Container的 ConstructL中写一行User::Leave(KErrNoMemory)试试看。我试验的结果如下:


除了上面说的特殊情况,你可以简单的弹出一个对话框,告诉用户没有足够的内存运行程序,并且安全的关闭程序。比如我的游戏程序就是这样处理的:
void CStageManager::DoGameFrame()
{
TRAPD(error, DoGameFrameProtectedL());
if (error == KErrNoMemory)
{
StopGame();
m_noMemoryDlg->ExecuteLD(R_KEY_INVALID_DIALOG);
Exit();// Call CAknAppUi::RunAppShutter( )
}
else if (error != KErrNone)
{
User::Panic(_L("Some other error."), error);
}
}
其中noMemoryDlg是直接或者间接在Container的ConstructL中创建的:
// in header file
CAknQueryDialog* m_noMemoryDlg;
// somewhere in ConstructL
TBuf<128> errMsg;
_LIT(formater, "Not enough memory. Please close some applications.");
errMsg.Copy(formater);
m_noMemoryDlg = new (ELeave) CAknQueryDialog(errMsg, CAknQueryDialog::EErrorTone);
当然,你也可以制作一个精美的图片来报告内存不足,等待用户按任意键再退出。不过载入这个图片也可能会失败,所以至少在这个图片成功载入之前,你还是需要系统对话框来报告的。
值得一提的是,你不一定需要退出程序,或者你可以稍后重试申请内存,幸运的话,没准第二次就能成功。这是因为Symbian系统会在内存不足时自动关闭一些应用程序。我觉得这是Symbian系统一个比较奇怪的设计。通常应用程序在AppUi的HandleCommandL中会响应EEikCmdExit消息,并且调用CAknAppUi::Exit( )函数(如下代码)。这使得应用程序可以在应用程序管理器中用C键结束掉。这也使得Symbian操作系统有机会在内存不足时通过这个渠道自动关闭一些应用程序。
// ----------------------------------------------------
// CFlyAppUi::HandleCommandL(TInt aCommand)
// takes care of command handling
// ----------------------------------------------------
//

void CFlyAppUi::HandleCommandL(TInt aCommand)
{
switch ( aCommand )
{
case EEikCmdExit:
Exit();
break;
// TODO: Add Your command handling code here
default:
break;
}
}
坦白说我没有尝试过重试申请内存这个办法,不过我想是可行的。

1.3.4.栈回滚和对象的安全析构
上面说到在遇到某些异常时,你可以选择弹出对话框并且结束程序,其实这会比你想象的要困难一些。因为C++可不像Java那样有托管堆进行垃圾收集。不过好在C++栈会自动回滚,栈上的对象会被销毁。如果你此时调用CAknAppUi::RunAppShutter( )结束程序,那么AppUi,Container的析构函数会依次被调用,引起你自己创建对象的析构函数也依次被调用。那么堆上的对象也要被销毁。可是,请记住,异常随时随处可能发生,使对象处于一种"半构造"的状态。此时析构函数被调用可能会造成对无效指针的访问错误。请看下面这个例子,它犯了两个常见的错误:
class BadExample
{
protected:
TText8* m_pBuf;
TText8* m_pBuf2;
public:
static BadExample* NewL()
{
BadExample* self = new (ELeave) BadExample();
self->ConstructL();
return self;
}

void DeleteBuf()
{
delete m_pBuf;
}

void RebuildBufL()
{
m_pBuf = new (ELeave) TText8[256];
}

private:
BadExample();
~BadExample()
{
delete m_pBuf;
delete m_pBuf2;

}
void ConstructL()
{
m_pBuf = new (ELeave) TText8[256];
m_pBuf2 = new (ELeave) TText8[256];
}
};
假设我们在AppUi的ConstructL中使用BadExample::NewL( )来构造对象,在AppUi的析构函数中delete这个对象。

下面我们分析一下可能遇到的问题:
首先,在函数NewL中,self指针没有被保护,试想如果self->ConstructL( )一句抛出异常。那么这个self指针指向的对象就没有return给外界(也就是AppUi),这个对象就永远"丢失了",造成了内存泄露。正确的做法是使用CleanupStack对它进行保护。CleanupStack至少能保证在程序退出时压入其中的对象都能销毁。
static BadExample* NewL()
{
BadExample* self = new (ELeave) BadExample();
CleanupStack::PushL(self);
self->ConstructL();
CleanupStack::Pop();
return self;
}
但是注意,此处还有一个微妙的内存泄露。仔细看看CleanupStack::PushL( )的声明:
IMPORT_C static void PushL(TAny* aPtr);
IMPORT_C static void PushL(CBase* aPtr);
IMPORT_C static void PushL(TCleanupItem anItem);
如果传入的指针是CBase指针,那么CBase的虚析构函数(virtual ~CBase( ))就能保证对象在销毁时正确的调用析构函数。可是本例中BadExample不是从CBase中派生,那么对象只能做很有限的销毁,根本不会调用析构函数。所以,如果ConstructL是由于第二个内存申请m_pBuf2失败,那么m_pBuf申请的内存就永远不会回收。所以正确的做法是,让 BadExample从CBase派生。
class BadExample : public Cbase
其次,我们并没有为 m_pBuf和m_pBuf2赋初值,在Release版中他们的值是随机的。那么,如果m_pBuf2的申请失败,析构函数还是会执行delete m_pBuf2,试图删除一个无效指针。正确的做法是在构造函数中为m_pBuf和m_pBuf2赋初值NULL。因为标准C++规定,delete一个空指针不做任何操作。不过实际上,如果对象从CBase派生,这一步是没有必要的,因为CBase能保证派生类的成员变量在构造时自动清零。
最后,动态的使用DeleteBuf和RebuildBufL是不安全的。如果你先用DeleteBuf删除了这个对象,那么m_pBuf就是一个坏指针。可是紧接着的RebuildBufL可能会失败。此时如果析构函数被调用,还是会产生delete无效指针的错误。正确的做法是,在DeleteBuf 中,把m_pBuf设为NULL。
总结上面说到的几点,完整的安全的代码是:
class BadExample : public CBase
{
protected:
TText8* m_pBuf;
TText8* m_pBuf2;
public:
static BadExample* NewL()
{
BadExample* self = new (ELeave) BadExample();
CleanupStack::PushL(self);
self->ConstructL();
CleanupStack::Pop();
return self;
}

void DeleteBuf()
{
delete m_pBuf;
m_pBuf = NULL;
}

void RebuildBufL()
{
m_pBuf = new (ELeave) TText8[256];
}

private:
~BadExample()
{
delete m_pBuf;
delete m_pBuf2;
}
void ConstructL()
{
m_pBuf = new (ELeave) TText8[256];
m_pBuf2 = new (ELeave) TText8[256];
}
};

1.4.安全的图像引擎

Symbian C++游戏的2D图像显示部分一般由下面几个类组成:
图像: 封装了一个CWsBitmap。是基本的图片资源。支持图像之间的各种贴图和混合操作。
双缓冲: 一个和屏幕分辨率、色深相等的图像。
直接写屏支持: 复合一个CDirectScreenAccess对象,实现MDirectScreenAccess接口。负责直接写屏的安全处理。比如来电、屏保时适时的停止和开启直接写屏与游戏逻辑。
绘图类: 负责在图像中绘图。它不是对Gc的封装,而是通过直接修改图像内存区进行绘图。
位图字体类: 使用预先创建的位图资源写字。如下图就是一个预先创建的位图资源。优点是速度快,缺点是无法支持大字符集合,比如中文。

字体缓冲区类: 还是使用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

分享到:
评论

相关推荐

    symbian c++ 超级玛丽简化代码

    Carbide.c++是Symbian平台上的集成开发环境,用于编写C++代码。它提供了代码编辑、编译、调试等一整套工具,对于构建Symbian应用,尤其是游戏,非常方便。熟悉Carbide.c++的基本操作和设置对于快速开发至关重要。 ...

    symbian c++游戏的可用性指南.rar

    《Symbian C++游戏的可用性指南》 在移动设备领域,Symbian操作系统曾是主流平台之一,尤其在智能手机早期,Symbian C++是开发者构建游戏和应用程序的首选语言。本指南将深入探讨如何利用Symbian C++为用户创建高...

    S60V2俄罗斯方块C++源代码,适用于6630,7610等手机

    首先,我们关注的是源代码的编写语言——C++。C++是面向对象的编程语言,对于开发高效、低内存占用的游戏来说,它是理想的选择。在S60平台上,C++ SDK提供了丰富的API,使得开发者可以直接操作硬件资源,提高游戏...

    symbian os c++ for mobile phones volume 1 电子书

    《Symbian OS C++ for Mobile Phones Volume 1》是一本深入探讨Symbian操作系统下C++编程技术的专业书籍,由Symbian自己的工程师团队撰写,旨在为移动...,帮助他们掌握在Symbian OS上编写C++应用程序的核心技能...

    设计60系列c++游戏

    ### 设计60系列C++游戏 #### 一、引言与目的 ##### 1.1 目的和范围 本文旨在深入探讨60系列C++游戏的设计与开发过程,帮助开发者理解该系列游戏的基本架构及其核心设计理念。本文档假设读者已具备C++语言的基础...

    symbian泡泡龙游戏源码

    【标题】"symbian泡泡龙游戏源码"揭示了这个资源是专门为Symbian操作系统设计的一款经典泡泡龙游戏的源代码。Symbian是一种曾经广泛应用于智能手机的操作系统,尤其在诺基亚手机中占据主导地位。源码的提供意味着...

    C++重点知识的几个程序样例附带课件

    Symbian系统曾经是智能手机市场的主流,其对C++的优化和原生支持使得开发者能够充分利用硬件资源,编写高效的代码。 通过这些程序样例和课件,学习者不仅可以理论联系实际,理解C++的核心概念,还能提升解决问题的...

    Series 60开发者平台使用C++进行游戏

    通过《Series_60_Developer_Platform_1_0_2_0_Programming_Games_v1_0_zh_ch.pdf》这份文档,开发者可以获得更详细的指导,了解如何在Series 60平台上使用C++编写游戏,包括实例代码和最佳实践。学习并掌握这些知识...

    Symbian OS C++手机应用开发(第3卷)

    《Symbian OS C++手机应用开发(第3卷)》是人民邮电出版社出版的一本专注于移动开发的专业书籍,特别关注的是Symbian操作系统上的C++编程技术。Symbian OS曾是智能手机领域的主导平台,尤其在诺基亚手机中广泛使用...

    Symbian s60 3rd 2D游戏引擎

    "Carbide.c++编译通过可以跑的"这句话表明,游戏代码已经成功通过了编译,并且可以在Symbian S60 3rd设备上运行,这是开发过程中的一个重要里程碑。 在创建2D游戏时,开发者需要考虑以下几个关键知识点: 1. **...

    symbian 官方游戏 实例源码

    Symbian游戏开发的核心在于其SDK(Software Development Kit),它包含了必要的库、工具和文档,使得开发者能够编写原生C++代码来创建应用程序。在这个实例源码中,我们可以学习到以下关键知识点: 1. **图形渲染**...

    symbian游戏实例

    8. **调试与优化**:在开发过程中,使用Symbian的调试工具进行代码调试,确保游戏无误。同时,因为Symbian设备性能有限,代码优化也是必不可少的,以保证游戏在各种设备上都能流畅运行。 9. **打包与发布**:最后,...

    Symbian俄罗斯方块源代码

    1. **编程语言**:Symbian平台通常使用C++进行开发,因此我们可以预想这个俄罗斯方块游戏的源码是用C++编写的。C++是一种强大的面向对象编程语言,能够提供高效且灵活的代码结构,非常适合游戏开发。 2. **图形用户...

    Example_game.rar_symbian_symbian 游戏_俄罗斯方块 symbian

    总之,通过研究Symbian上的“俄罗斯方块”游戏示例,开发者不仅能掌握C++游戏编程的基本原理,还能了解到Symbian平台的特性和开发技巧,这对于那些想要扩展其技能范围,进入移动游戏开发领域的程序员来说,是非常有...

    塞班入门书籍+培训讲义

    《Symbian OS C++高效编程》这本书可能是为开发者提供深入理解塞班系统底层机制和C++编程技巧的指南。Symbian C++是塞班平台的主要开发语言,它扩展了标准C++,增加了对系统服务和资源管理的特殊支持。通过这本书,...

    symbian写的贪吃蛇,供下载

    在这个特定的案例中,贪吃蛇游戏是用Symbian C++编写的。Symbian C++具有丰富的API库,包括图形界面、网络、多媒体、设备访问等功能,这些都为开发一款完整的手机游戏提供了便利。游戏的核心逻辑可能包括蛇的移动、...

    Symbian下的小游戏

    对于初学者来说,研究Mopoid的源代码可以帮助理解Symbian游戏的架构和设计模式,例如如何处理图形渲染、用户输入以及游戏循环等基础元素。 在Symbian平台上,游戏的图形渲染通常依赖于Series60提供的API,如GDI...

    Series 60 Developer 使用C++进行游戏编程

    ### Series 60 Developer 使用C++进行游戏编程 #### 一、引言 随着移动设备技术的迅速发展,特别是CPU处理能力与显示技术的进步,移动设备成为了一个极具潜力的游戏平台。Series 60 Developer Platform(S60)作为...

    C++高级软件工程师

    - **3D游戏开发**:了解3D游戏引擎的基本架构,学习OpenGL等图形库的使用。 #### 六、软件工程与项目管理 - **敏捷开发**:了解敏捷开发的理念和Scrum框架。 - **需求分析**:学习如何有效地收集和整理用户需求。 -...

Global site tag (gtag.js) - Google Analytics