图像 - 封装了一个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通道。