目前
mobile phone 游戏API简称GAPI为手机上的游戏开发者提供了强有力的高效率的编程接口,当然GAPI不仅仅使用在游戏方面,需要高效率图形显示处理的地方都可以使用GAPI。
GAPI是基于动态连接库方式,应用
程序直接调用动态库里的函数,一般GAPI库的
文件名为GX.dll,目前mobile phone里都提供了gx.dll
文件。
一个典型的游戏或者应用程序使用下列GAPI函数:
OpenDisplay (fullscreenflag)
打开GAPI显示功能。
OpenInput
打开直接响应硬件
键盘输入消息功能
GetDisplayProperties
获得VFB详细结构信息
GetDefaultKeys
获得缺省的键值
操纵GAPI
开始一个游戏编写,首先要打开GAPI显示功能,获得控制视频显示缓存的控制权限。可以调用
GXOpenDisplay(HWND hwnd, DWORD dwFlags)
hwnd参数是游戏程序的窗口句柄,dwFlags定义了显示模式,宏GX_FULLSCREEN定义全屏模式,就能对设备的全屏区域进行控制。返回值1说明打开成功,0是失败。
虽然都是使用mobile phone系统但是不同系列的产品可能使用不同的显示设备,那么对于不同的显示设备就可能有不同的显示性能参数,不同的分辨率率,不同的色深,不同的颜色显示能力。当在编写一个为mobile phone 系列运行的游戏程序时不得不考虑这些问题,以使程序能适应在不同的显示环境下达到程序所希望的显示
效果。
如何得到这些相关显示信息?可以调用下面函数:
GXDLL_API GXDisplayProperties GXGetDisplayProperties ();
它能得到显示设备的所有相关细节信息,这些都是在开发基于GAPI游戏时需要的。所有信息被返回到GXDisplayProperties结构中,其结构如下:
struct GXDisplayProperties {
DWORD cxWidth;
DWORD cyHeight;
long cbxPitch;
long cbyPitch;
long cBPP;
DWORD ffFormat;
};
这个结构提供了显示设备的信息,也就是当前视频缓存区域的参数指标。
cxWidth和cyHeight是显示设备的宽和高的像素值,提供了显示设备横、纵能显示的像素个数;cBPP是每个像素点需要的位数,总是等于2的n次方。cbxPitch和cbyPitch提供了相邻两个像素间从
内存数据上相差的字节数,cbxPitch表示的是左右两个像素间的差值,当cBPP大于等于8时,cbxPitch表示相差的字节个数,当cBPP小于8时,cbxPitch已经不能真实的反映出相差的字节数,事实上必须自己计算得到相邻的地址:
比如:
cBPP = 4;
Leftpointaddr = pb + (((current_x+1) * cBPP) >> 3)
+ (current_y * cbyPitch)
cbyPitch表示的上下方向间两个像素的差值,计算时通过加减cbyPitch来的到上下方向的像素点的地址:
downpointaddr = currentpointaddr + cbyPitch;
ffFormat参数说明显示设备对色深的处理方式及显示的
格式:
当ffFormat 等于 KfLandscape 说明当前显示方式是横屏方式,即原点(0,0)变成了左下角。
ffFormat 等于 KfPalette 说明色彩显示是基于调色板方式。
ffFormat 等于 KfDirect 说明色彩显示是直接映射,不引用调色板。
ffFormat 等于 KfDirectInverted 说明颜色显示是反转的。
ffFormat 等于 kfDirect555 、kfDirect565、kfDirect888 说明映射颜色显示时,数字表示红绿蓝所占的位信息。
计算每一个像素坐标地址方法如下(x,y):
unsigned char * pb;
if (cBPP < 8) {
address = pb + ((x * cBPP) >> 3) + (y * cbyPitch)
}
else
{
address = pb + (x * cbxPitch) + (y * cbyPitch);
}
判断是否是标准显示设备
可以使用函数GXIsDisplayDRAMBuffer (),返回值为TRUE说明是非标准显示设备,返回值为FLASE说明是标准显示设备。当是非标准显示设备时,需要使用函数GXSetViewport来定义显示
屏幕的区域,在标准显示设备上使用GXSetViewport是无效的。
GXDLL_API int GXSetViewport (
DWORD dwTop,
DWORD dwHeight,
DWORD dwReserved1,
DWORD dwReserved2)
dwTop定义了屏幕显示区域的y坐标,dwHeight显示区域的高度,dwReserved1、dwReserved被保留,必须需设置为0。
开始绘制像素
现在就可以准备对缓存区进行操作绘制图形,通过GXBeginDraw得到缓存区的首地址:
void * GXBeginDraw ();
函数返回值就是需要的首地址,如果是NULL说明显示缓存区得不到。然后就可以进行一些列像素的操作,操作完毕后需要调用GXEndDraw 结束一次操作:
int GXEndDraw ();
返回1说明调用成功,0说明错误。
提交绘制的信息,已使变化的
画面生效。当程序失去焦点时必须调用GXSuspend ()挂起所有GAPI的操作,把屏幕控制交给其他程序,当接收到获得焦点信息时,程序必须调用GXResume ()使得程序继续运行GAPI函数。
当退出程序时,必须释放GAPI资源,可以调用:
int GXCloseDisplay ();
GAPI高效贴图
在开发一些图像处理或游戏时我们可以使用GDI制作出满意的产品,但是开发复杂高速的图形显示或高效率的动态游戏时,往往对GDI的显示效率不高而感到沮丧,虽然可以使用双缓存等技术,但是GDI层接口毕竟效率低,无法满足要求。
GAPI对显示缓存区的直接操作,使显示效率大大提高,所以在目前mobile phone上当需要处理高速贴图时GAPI就当之无愧了。
虽然GAPI高效强大,提供了对显示缓存区直接的读写权限,但是基于如此低级的功能函数,在编写一个稍微复杂的程序时,就会花费大量的时间和精力在处理对显示缓存区的操作,因为此缓存区并不像GDI提供的绘制缓存区对图片显示一样操作容易,为了显示一幅bmp图就需要编写好几页的代码,这是非常令人厌倦的事。
在这里主要介绍一下一个使用GAPI编写的第三方贴图类STGapiBuffer。STGapiBuffer提供了类似于GDI方式的接口操作简单,很容易就构造出了需要显示的缓存内容,最后只要简单的把它们拷贝进显示缓存区,就可以显示出来了。
只要简单的把STGapiBuffer.h 和 STGapiBuffer.cpp加入到工程里面就可以方面的使用了。
为了需要绘制一个jpg图片,首先需要把图片加载入此类里,并创建适合STGapiBuffer处理的数据,使用CreateNativeBitmap函数,需要如下操作:
HBITMAP hBitmap = SHLoadDIBitmap(_T("//image.bmp" );
g_pNativeBitmap = g_gapiBuffer.CreateNativeBitmap(hBitmap);
: eleteObject(hBitmap);
接下去需要为绘制到哪里设置目标对象,可以是另一个STGapiBuffer的缓存区,也可以是显示的显示设备缓存区,调用函数SetBuffer,代码如下:
g_gapiBuffer.SetBuffer(pDisplayBuffer);
最好可以使用CSTGapiBuffer::BitBlt来把需要的数据绘制到缓存区里,类似于GDI函数BitBlt,代码如下:
g_gapiBuffer.BitBlt(0, 0, 100, 100, g_pNativeBitmap);
这样就把一幅图片显示到了屏幕上。
CSTGapiBuffer还提供了绘制透明图片的函数,在绘图时经常会遇到这样的情况,使用CSTGapiBuffer::MaskedBlt方便的绘制指定透明色的图案。当然我在用GDI绘图时经常使用CreateMemoryDC创建一个临时内存DC来绘制,CSTGapiBuffer也提供了类似的功能的函数CSTGapiBuffer:: CreateMemoryBuffer
CSTGapiBuffer类使用示例(部分代码):
CNativeBitmap* pAsteroidBitmap = NULL;
CNativeBitmap* pAsteroidMask = NULL;
CSTGapiBuffer gapiBufferBackground; // 背景
CSTGapiBuffer gapiBufferMemory;
CSTGapiBuffer gapiBufferScreen;
HBITMAP hBackground = ::LoadBitmap(hInst, MAKEINTRESOURCE(IDB_BACKGROUND));
CNativeBitmap * pBackgroundBitmap =
gapiBufferMemory.CreateNativeBitmap(hBackground);
: eleteObject(hBackground);
gapiBufferBackground.CreateMemoryBuffer();
gapiBufferBackground.BitBlt(0, 20, dwDispWidth, dwDispHeight,
pBackgroundBitmap);
delete pBackgroundBitmap;
pBackgroundBitmap = NULL;
////////////////////////////////////
HBITMAP hAsteroid = ::LoadBitmap(hInst, MAKEINTRESOURCE(IDB_ASTEROID));
pAsteroidBitmap = gapiBufferMemory.CreateNativeBitmap(hAsteroid);
: eleteObject(hAsteroid);
HBITMAP hAsteroidMask = ::LoadBitmap(hInst, MAKEINTRESOURCE(IDB_ASTEROID_MASK));
pAsteroidMask = gapiBufferMemory.CreateNativeBitmap(hAsteroidMask);
::DeleteObject(hAsteroidMask);
/////////////////////////////////////////
//
dwTransparentColor = gapiBufferMemory.GetNativeColor(RGB(249, 57, 198));
// create an offscreen buffer
gapiBufferMemory.CreateMemoryBuffer();
gapiBufferMemory.BitBlt(&gapiBufferBackground);
///////////////////////////////////////////////////
//
gapiBufferMemory.TransparentBltEx(0, nOffset%dwDispHeight,
50, 60, pAsteroidBitmap, dwTransparentColor);
gapiBufferMemory.TransparentBltEx(100, (nOffset*2)%dwDispHeight,
50, 60, pAsteroidBitmap, dwTransparentColor);
gapiBufferMemory.TransparentBltEx(nOffset%dwDispWidth,
50, 50, 60, pAsteroidBitmap, dwTransparentColor);
gapiBufferMemory.TransparentBltEx((nOffset*2)%dwDispWidth,
150, 50, 60, pAsteroidBitmap, dwTransparentColor);
gapiBufferMemory.MaskedBlt(dwDispWidth-(nOffset*2)%dwDispWidth,
(nOffset*8)%dwDispHeight, 50, 60, pAsteroidBitmap, pAsteroidMask);
gapiBufferMemory.MaskedBlt((nOffset%dwDispWidth*5), (nOffset*3)%
dwDispHeight, 50, 60, pAsteroidBitmap, pAsteroidMask);
gapiBufferMemory.MaskedBlt((nOffset*3)%dwDispWidth, (nOffset*4)%
dwDispHeight, 50, 60, pAsteroidBitmap, pAsteroidMask);
gapiBufferMemory.MaskedBlt((nOffset%dwDispWidth*2), dwDispHeight-
(nOffset*5)%dwDispHeight, 50, 60, pAsteroidBitmap, pAsteroidMask);
RECT rc = { 0, 20, dwDispWidth/3, 10 };
FillRect(&rc, RGB(255, 0, 0));
rc.left = dwDispWidth/3;
rc.right = 2*dwDispWidth/3;
FillRect(&rc, RGB(0, 255, 0));
rc.left = 2*dwDispWidth/3;
rc.right = dwDispWidth;
FillRect(&rc, RGB(0, 0, 255));
void* pBuffer = GXBeginDraw();
gapiBufferScreen.SetBuffer(pBuffer);
gapiBufferScreen.BitBlt(&gapiBufferMemory);
GXEndDraw();
Gapi键盘消息
使用GXOpenInput()函数获得键盘的控制权,调用GXGetDefaultKeys(GX_NORMALKEYS)函数来获得默认键盘的消息映射。然后在Windows消息处理函数中我们就能收到由GAPI发送过来的键盘消息,当你按下某一个键,程序会收到WM_KEYDOWN,wParam参数包含GAPI映射的这个键的消息,通过与得到的GXKeyList结构中的字段定义来判断当前收到的键是不是定义的功能键。
GXKeyList结构如下:
struct GXKeyList {
short vkUp;
POINT ptUp;
Short vkDown;
POINT ptDown;
Short vkLeft;
POINT ptLeft;
Short vkRight;
POINT ptRight;
Short vkA;
POINT ptA;
Short vkB;
POINT ptB;
Short vkC;
POINT ptC;
Short vkStart;
POINT ptStart;
};
游戏的振动感
在游戏中提供振动效果,是让玩家非常兴奋的事情,可以提升游戏的吸引程度。Mobile Phone SDK 提供了振动效果的API,允许你控制振动,类似与控制声音一样简单。
获得振动设备属性
要获知手机是否支持振动,振动的性能,当前的设置情况,可以调用下面的函数:
int VibrateGetDeviceCaps(VIBRATEDEVICECAPS vdc);
VIBRATEDEVICECAPS是个枚举类型,结构如下
typedef enum {
VDC_AMPLITUDE,
VDC_FREQUENCY,
VDC_LAST
} VIBRATEDEVICECAPS
VDC_AMPLITUDE 查询振动设备所能支持的振幅大小
VDC_FREQUENCY 查询振动设备所能支持的振动频率大小
VDC_LAST 查询振动设备所能支持的振幅大小
如果函数成功,它将返回数字0到7,数字0说明设备没有提供振动功能,1说明设备具有振动功能,并且可以使用,但是仅仅具有打开关闭震动,无法对振动进行调节,2到7说明了设备提供了不同等级的振动功能,数字越大提供的调节能力越强。当设备具有不同等级振动能力时,我就可以通过VIBRATENOTE结构做详细设置。
怎么才能开始真正的使用振动功能呢?mobile phone SDK提供Vibrate函数:
HRESULT Vibrate(
DWORD cvn,
const VIBRATENOTE * rgvn,
BOOL fRepeat,
DWORD dwTimeout
);
它能提供不同振幅,不同频率,并可以调节需要振动时间。cvn参数是第二个参数rgvn数组的维数,rgvn是指向一组VIBRATENOTE结构的指针。
VIBRATENOTE结构如下:
typedef struct {
WORD wDuration;
BYTE bAmplitude;
BYTE bFrequency;
} VIBRATENOTE
wDuration说明了震动持续的时间,bAmplitude定义了振动的振幅大小,允许设置0-7级,如果等于0xff系统使用缺省值作为参数,bFrequency定义了振动的频率高低,允许设置0-7级,如果0xff系统使用缺省值作为参数。
当你需要停止当然的振动时,可以调用VibrateStop()函数,返回S_OK说明成功调用,E_FAIL说明调用失败。
下面是代码示例:
int caps = -1;
caps = VibrateGetDeviceCaps(VDC_AMPLITUDE);
if(caps<=0)
return FALSE; //振幅返回失败,说明不支持振动功能
HRESULT hr = Vibrate(0, NULL, TRUE, INFINITE); //设定为无时间限制
if(hr == E_FAIL)
{
MessageBox(NULL,L"E_FAIL",L"",MB_OK);
}
else if(hr == E_NOTIMPL)
{
MessageBox(NULL,L"E_NOTIMPL",L"",MB_OK);
}
Sleep(1000); //振动所花时间
VibrateStop();
开始第一手机游戏历程
在这里使用GAPI模拟一个贪食者游戏,它非常简单。主要注重怎么具体使用GAPI,在使用中怎么对视频缓存区操作演示,并不去美化外表。
初始化GAPI库,在InitInstance函数里我们对GAPI的显示和输入进行了初始化。
if (GXOpenDisplay( hWnd, GX_FULLSCREEN) == 0)
return FALSE;
gx_displayprop = GXGetDisplayProperties();
if (gx_displayprop.cBPP != 16)
{
// Only dealing with 16 bit color in this code
GXCloseDisplay();
MessageBox(hWnd,L"Sorry, only supporting 16bit color",L"Sorry!", MB_OK);
return FALSE;
}
framebuf = (unsigned short*) malloc(sizeof(short)*gx_displayprop.cxWidth*gx_displayprop.cyHeight);
if(framebuf==NULL)
return FALSE;
ClearScreen(framebuf,0xff,0xff,0xff);
GXOpenInput();
// Get default buttons for up/down etc.
gx_keylist = GXGetDefaultKeys(GX_NORMALKEYS);
初始化工作完成后,我们就需要对游戏的内容进行必要准备工作。我们首先初始化贪食者对象,我们为它建立蛇头和它的身体。为了使贪食者不停的游动,必须有一个事件触发。在这个我们使用定时器,以100ms的间隔发送消息,这样将得到一秒10帧的效果,这足以满足普通游戏的效果。
为了对定时器发送过来的消息进行处理,我们调用Run函数
void Run(HWND hwnd)
{
if(1 == JudgeDeath(framebuf))
{
KillTimer(hwnd,1);
RunVibrate(1000);
MessageBox(hwnd,_T("Snake has died!" ,_T("died" ,
MB_OK | MB_ICONINFORMATION);
SendMessage(hwnd,WM_PAINT,0,0);
InvalidateRect(hwnd,NULL,TRUE);
}
ChangeDirection();
SortAll();
RedrawSnake();
}
JudgeDeath每次都会判断是否已经死亡(蛇的任何部位有重叠),一旦满足死亡的条件,就取消定时器,以便中止蛇的游动。ChangeDirection判断方向是否发生了改变。在这里我对蛇部位的重叠利用了两个象素是否相同的颜色,如果颜色一直说明重叠发生。
void Get16Pixel(unsigned short *buffer,int x, int y,int *r, int *g, int *b)
{
unsigned short *pixeladd;
int address = (x * gx_displayprop.cbxPitch>>1)
+ (y * gx_displayprop.cbyPitch>>1);
pixeladd = (buffer+address);
if (gx_displayprop.ffFormat & kfDirect565)
{
unsigned short PixelCol;
PixelCol = (*pixeladd);
*r = (PixelCol & 0xf800) >> 11;
*g = (PixelCol & 0x07e0) >> 5;
*b = (PixelCol & 0x001f);
}
else//555
{
unsigned short PixelCol;
PixelCol = (*pixeladd);
*r = (PixelCol & 0x7c00) >> 11;
*g = (PixelCol & 0x03e0) >> 5;
*b = (PixelCol & 0x001f);
}
}
第三方开发库介绍
GapiDraw的设计与DirectDraw非常相似,而且将更加容易使用,极大限度的为掌上设备进行了优化。下面是一些DriectDraw中的一般功能,以及如何在GapiDraw中实现这些功能。
打开显示设备
计算机的显示内存是一块包含了图像数据的内存区域。为了直接向这块区域进行写操作,DirectDraw和GapiDraw都需要你创建一个指定的称之为主界面的界面。直接绘制到这个主界面来影响屏幕的可见内容。
创建主界面的第一步是打开显示器,设置一个显示模式。下面的步骤是使用DirectDraw创建主界面的最少步骤。
DirectDraw
LPDIRECTDRAW lpDD;
HRESULT ddrval;
//创建主Direct Draw对象
ddrval = DirectDrawCreate(NULL, &lpDD, NULL);
if(ddrval != DD_OK)
{
return(false);
}
//设置合作级别以允许Direct Draw全屏运行
ddrval = lpDD->SetCooperativeLevel(hwnd, DDSCL_EXCLUSIVE | DDSCL_FULLSCREEN);
if(ddrval != DD_OK)
{
lpDD->Release();
return(false);
}
//设置显示模式为320x240x16
ddrval = lpDD->SetDisplayMode(320, 240, 16);
if(ddrval != DD_OK)
{
lpDD->Release();
return(false);
}
使用GapiDraw是比较容易的。因为界面是对象而不是COM接口,所以根本就不用手工进行释放。下面的GapiDraw例子只使用了一条命令打开显示设备,设置默认显示模式。
GapiDraw
CGapiDisplay display;
HRESULT gdrval;
//使用标准Pocket PC240x320x16模式打开显示
gdrval = display.OpenDisplay(hwnd, GDOPENDISPLAY_FULLSCREEN);
if(gdrval != GD_OK)
{
return(false);
}
取回主界面和后背缓冲
使用Direct Draw,你必须手工请求创建一个指定界面的主Direcr Draw对象,主界面主要用于直接在显示器上绘制。在Direct Draw中只有一个界面接口用于双内存界面和显示。这可以简单地解释为过去使用的COM模式中的子类化缺陷。下面的例子创建一个主界面,使用Direct Draw取回它的后背缓冲。
DirectDraw
LPDIRECTDRAWSURFACE lpDDSPrimary; // DirectDraw 主界面
LPDIRECTDRAWSURFACE lpDDSBack; // DirectDraw 后背缓冲
DDSURFACEDESC ddsd;
DDSCAPS ddscaps;
HRESULT ddrval;
memset(&ddsd, 0, sizeof(ddsd));
ddsd.dwSize = sizeof(ddsd);
ddsd.dwFlags = DDSD_CAPS | DDSD_BACKBUFFERCOUNT;
ddsd.ddsCaps.dwCaps = DDSCAPS_PRIMARYSURFACE | DDSCAPS_FLIP | DDSCAPS_COMPLEX;
ddsd.dwBackBufferCount = 1;
//创建主界面
ddrval = lpDD->CreateSurface(&ddsd, &lpDDSPrimary, NULL);
if(ddrval != DD_OK)
{
lpDD->Release();
return(false);
}
// Get the back buffer获得背后缓冲
ddscaps.dwCaps = DDSCAPS_BACKBUFFER;
ddrval = lpDDSPrimary->GetAttachedSurface(&ddscaps, &lpDDSBack);
if(ddrval != DD_OK)
{
lpDDSPrimary->Release();
lpDD->Release();
return(false);
}
相反,使用GapiDraw 事情会更容易。一旦调用了CGapiSurface::OpenDisplay,CGapiDisplay 对象自动地变成主界面。因为CGapiDisplay 是CGapiSurface的子类,所有的位图传输(blit)和绘制操作都已经是可用的了。为了从CGapiDisplay得到后背缓冲,使用如下代码:
GapiDraw
CGapiSurface backbuffer; // GapiDraw 后背缓冲
HRESULT gdrval;
//得到后背缓冲
gdrval = display.GetBackBuffer(&backbuffer);
if(gdrval != GD_OK)
{
return(false);
}
失败的界面
DirectDraw的界面通常储存在图像存储器中,可以在任何时候被覆盖(在用户切换程序或者启动另外一个使用GDI的程序的情况下)。这是因为对界面的每个操作在任何时候都有可能失败,简单地说是因为界面数据被覆盖。因此所有使用Direct Draw的操作必须每次检查界面是否失败,然后从失败处手工恢复和重新创建界面。下面的例子说明了这一点。
DirectDraw
ddrval = lpDDSBack->Blt(&rcRectDest, lpDDSMySurf, &rcRectSrc, DDBLT_WAIT, NULL);
if(ddrval == DDERR_SURFACELOST)
{
//界面被覆盖,现在你必须手工恢复和重新创建所有的界面
}
ddrval = lpDDSPrimary->Flip(NULL, DDFLIP_WAIT);
if(ddrval == DDERR_SURFACELOST)
{
//界面被覆盖,现在你必须手工恢复和重新创建所有的界面
}
Pocket PC不使用图像存储器,所有界面数据被储存在RAM物理内存,只有调用CGapiDisplay::Flip时才被复制到显示区域。如果Pocket PC设备访问了显示区域的缓冲,那么Pocket PC可能会移动它的后背缓冲的位置。到目前为止,还没有设备被告之可以做到这点,但是它始终是最好的设计。可以使用下面的代码捕获在GapiDraw中丢失的后背缓冲。
GapiDraw
//普通操作中的界面不能丢失
gdrval = backbuffer.Blt(&rcRectDest, &mysurf, &rcRectSrc, 0, NULL);
gdrval = display.Flip();
if(gdrval == GDERR_BACKBUFFERLOST)
{
//显示缓冲被移动,刚好获得一个更新的后背缓冲
display.GetBackBuffer(&backbuffer);
}
结论
上面提及的是GapiDraw和Direct Draw之间的主要不同。其它的特性,象blit、颜色值、矩形坐标等都是有差异的。GapDraw也包含一个巨大的扩展特性,这些特性在Direct Draw不可用,象高级的blit影响、快速旋转、从文件或内存装载位图图像、绘制工具、冲突掩码、界面交叉、线程定时器、位图字体支持以及更多。