John Kennedy
Microsoft Corporation
2001年11月14日

 

查看本文的源代码(英文)

我是一个超级游戏编程迷。我想,这是因为我花了那么多时间来为我拥有过的几乎所有计算机平台编写游戏。别误会,我可不是专业的游戏程序员,我只是发现,这种消遣是彻底了解硬件的绝佳方式。

处理桌面 Microsoft Windows® 系统游戏时,程序员们通常会使用 DirectX®,一种游戏应用程序编程接口 (API) 库。DirectX 已经发展成为管理图形和声音(以及许多其他精彩功能)的重要工具,但是让人失望的是,目前还没有适用于 Windows CE 的 DirectX 版本。(从技术角度来说并非如此。Sega Dreamcast 就附带一个这样的版本,但不适用于手持 PC 或 Pocket PC。)没有支持三维图形的专用硬件,没有多通道声音合成器,有限的 CPU 和内存资源,面对所有这些限制,对于 CE 开发人员来说,与其开发 DirectX 的 Pocket PC 平台版本,不如做些更重要的事情。

对希望在 Pocket PC 上编写游戏的早期程序员来说,缺少好的 DirectX API 的确是一个问题。快速动作游戏需要进行快速屏幕更新,快速屏幕更新又需要对屏幕内存进行快速访问。不幸的是,对于市面上的许多硬件,访问屏幕内存是一个漫长而不可靠的过程。在 LCD 屏幕(即使是彩色 LCD)上,即使辨认红、绿、蓝像素这样基本的东西都很困难。

既然完全的 DirectX 端口行不通,那么就必然需要一种独立于设备的方式来快速访问屏幕。这就是 GAPI(游戏 API)。GAPI 提供了一系列函数,可以直接对屏幕缓冲区进行读/写访问。2000 年 4 月,GAPI 首次应用到 Pocket PC 上。GAPI 是如此有用,以至于现在市场上所有设备(至少我见到的所有设备)的 ROM 中都包含它。对程序员来说,这意味着所有低级功能现在都可以实现了。复杂部件(提供对屏幕内存的直接访问)由各个 Pocket PC 设备的 OEM 负责生产,程序员则负责软件开发,而对后者而言所有设备的情况都是一样的。

GAPI 功能

GAPI 库由三个文件组成:

  • GX.H
  • GX.LIB
  • GX.DLL

GX.DLL 位于 Windows/ 目录中。正如前面提到的,大多数 Pocket PC 设备的 ROM 中都包含它。目前它的版本为 1.2,但早期设备的版本则是 1.0。您可能需要在发布时包含最新的 DLL,以确保您的代码使用正确的版本。

GAPI 库不是特别大(不到 10 KB),主要是因为它包含的代码库不大。对,可以说 GAPI 是相当有限的,但它的确提供了您需要的核心函数,而不是许多无关紧要的东西。它不支持精致的三维图形,实际上也不支持二维图形,无论精致不精致。以下是其功能的完整列表:

结构作用
GXDisplayProperties此结构存储了有关 Pocket PC 屏幕的关键信息,例如屏幕宽度、高度和像素布局等。
GXKeyList此结构存储了有关默认硬件按钮分配的信息。这是 GAPI 的另一个功能:使其易于识别按钮的按动,而不会意外启动内置的 PIM 应用程序。
GXScreenRect由某些 GAPI 函数内部使用。

函数作用
GXBeginDraw准备显示屏以写入内容。它与大家熟悉并钟爱的早期 BeginDraw 函数类似,但它返回的是最核心的视频内存地址,而不是次之的 HDC。
GXCloseDisplay释放由 GAPI 占用的屏幕资源。在程序结束时使用。
GXCloseInput释放由 GAPI 占用的按钮资源。
GXEndDraw在完成主要的绘图代码后调用此函数。它与 GXBeginDraw 相对应。如果忘记添加这个函数,您的设备内存将在数秒内耗尽。
GXGetDefaultKeys此函数非常有用,它将返回建议在游戏中使用的键。可以将参数传递至此函数,以要求使用纵向(常规)或横向(旋转 90 度)模式。
GXGetDisplayProperties返回包含重要信息的 GXDisplayProperties 结构。
GXIsDisplayDRAMBuffer一个 GAPI V1.2 函数,它将返回 TRUE 或 FALSE(如果显示屏为“非标准”显示屏)。实际上,我们并不关心这个问题,因此可以忽略它。
GXOpenDisplay此函数非常重要,在启动程序时被调用。一旦被调用,GAPI 将控制显示屏并禁止其他程序访问。
GXOpenInput控制按键,确保将按键信息直接发送到应用程序的 Windows 消息循环,而不是发送到命令解释程序。这将禁止用户在使用您的程序时启动其他任何应用程序,这在许多情况下都非常有用。
GXResume与调用 GXSuspend 相对,用于暂时禁用所有 GAPI 功能。编写程序时,在 WM_SETFOCUS 消息后面使用它是个好习惯。
GXSetViewport又一个可能永远用不到的 GAPI V1.2 函数。
GXSuspend暂停所有 GAPI 功能。在 WM_KILLFOCUS 消息后应当调用此函数。

正如您从上表中看到的,GAPI 确实没有提供诸如 DrawEnemySpacecraft Draw3DView 之类的高级函数。但是,当您在 Pocket PC 上编写快速动作游戏时,您可能会惊奇地发现它完全能满足您的需要。通常,Pocket PC 都有一个小型而快速的处理器、良好而快捷的语言(如 Visual C++®),并可以直接访问屏幕内存,这些可以给您带来许多乐趣。不相信?我曾在 Pocket PC 上玩过 Quake,够不错的吧?

开始使用 GAPI

典型的 GAPI 程序与标准 Windows CE 程序没有太多不同,您甚至可以使用向导生成的 Pocket PC 源代码作为 GAPI 程序的基础。只有一点需要注意:SDK Pocket PC 模拟器不支持 GAPI,因此该代码需要在真实的设备上运行。

编写代码之前,您需要从 Microsoft 的 Web 站点下载最新的 GAPI 压缩文件(英文)。

您需要将 GX.H 文件复制到 Pocket PC SDK 的 Include 目录中,并把与 CPU 相关的 GX.LIB 文件移到相应的 Lib 目录中。如果您不能确定设备中是否有最新的 GX.DLL 文件,也请将该文件复制到 Pocket PC 的 windows/ 目录中。

现在一切就绪,可以编写第一个 GAPI 软件了。您需要做的事情包括:

  1. 使用 eMbedded Visual C++ 工具创建一个新的 Pocket PC 应用程序,一个“Hello World”类的程序。
  2. 从“Project Settings”中选择“Link”选项卡,并确保其中已包含 gx.lib。
  3. 为了使用 GAPI 函数,您需要在程序开始处添加以下语句行:
    #include "gx.h"
    

    我们还需要声明一些变量,GAPI 将使用它们来存储有关屏幕硬件的各种重要信息。请继续添加以下语句行:

    // 全局 GAPI 变量:
    GXDisplayProperties gx_displayprop;
    GXKeyList gx_keylist;
    
  4. 然后,在程序的 InitInstance 函数中添加四个 GAPI 函数调用。这些调用将用于管理屏幕和键盘输入。请在调用 UpdateWindows(hWnd) 的语句后添加以下内容:
        // GAPI 内容
    
        // 尝试接管屏幕
        if (GXOpenDisplay( hWnd, GX_FULLSCREEN) == 0)
           return FALSE;
    
        // 获取显示属性
        gx_displayprop = GXGetDisplayProperties();
    
        // 接管按钮处理
        GXOpenInput();
    
        // 获取(向上/向下等操作的)默认按钮
        gx_keylist = GXGetDefaultKeys(GX_NORMALKEYS);
    
  5. 现在,我们已经基本接管了系统。但是,退出程序时别忘了归还控制权。要实现这一功能,请在 WinProc 的 WM_DESTROY 中添加以下代码:
        GXCloseInput();
        GXCloseDisplay();
    

如果现在编译并运行程序,您会发现添加代码后与默认设置时没有什么明显的不同。您只是创建了一个能说“Hello World”的程序,但按硬件上的任何按键都没有响应。到目前为止一切正常!

现在,您可能还希望将 CreateRpCommandBarCommandBar_Destroy 调用注释掉,以防菜单栏出现在屏幕的底部。您可能还在想:怎样退出程序呢?要退出程序,请转到“Settings”|“Memory”并选择“Running programs”,然后,单击您的应用程序并选择“Stop”。

在 GAPI 应用程序中添加图形

完全掌握 Pocket PC 屏幕的控制权以后,让我们来绘制一些实际的图形。还记得吗?您第一次开始 Windows CE 编程时,因为不能使用 MoveToLineTo 而需要使用 PolyLine,你觉得非常难受。好啦,你会怀念那些日子的!现在,虽然您已从 Pocket PC 获得访问屏幕硬件的所有权限,但还没有开始绘制图形呢。

一旦执行 GXBeginPaint 调用(不用再处理 WM_PAINT 消息了),就可以获得屏幕内存的地址,还可以直接写入值。当然,问题是写入什么值。

Pocket PC 设备支持每像素 4 位、8 位或 16 位。目前市场上的多数彩色设备都支持每像素 16 位,为了简单起见,假定我们要开发的示例代码使用每像素 16 位。实际上,为了更保险,我们将在 GXGetDisplayProperties 调用后添加一个条件,如下所示:

// 获取显示属性
gx_displayprop = GXGetDisplayProperties();

// 检查 16 位彩色显示
if (gx_displayprop.cBPP != 16)
{
    // 此代码仅处理 16 位彩色
    GXCloseDisplay();
    MessageBox(hWnd,L"对不起,仅支持 16 位彩色",L"对不起!", MB_OK);
    return FALSE;
} 

这里稍有一点复杂。尽管这些设备使用每像素 16 位来存储颜色信息,但是使用这 16 位的方式却有两种。您肯定在叹气了,不过这其实并不复杂。只要看一眼 GXDisplayProperties 结构的字段,就知道它使用多少位来存储蓝色信息。事实证明最常见的答案是 6,但不时地确定一下也没什么不好。

了解这一信息后,我们就可以编写一个清除屏幕的简单函数。我所说的“清除”是指将许多数据放入屏幕缓冲区内存,覆盖其中已有的内容。代码如下:

// 清屏并显示由红、绿、蓝值描述的颜色
void ClearScreen(int r, int g, int b)
{

    // 由 GX 函数获取屏幕内存的开始。
    unsigned short * buffer   = (unsigned short *) GXBeginDraw();

    if (buffer == NULL) return;

    // 从 R、G、B 分量计算像素颜色。
    unsigned short PixelCol;

    if (gx_displayprop.ffFormat & kfDirect565)
    {
       PixelCol = (unsigned short) (r & 0xff) << 11 | (g & 0xff) << 6 | (b & 0xff);
    }
    else
    {
       PixelCol = (unsigned short) (r & 0xff) << 10 | (g & 0xff) << 5 | (b & 0xff);
    }

    // 通过两个循环,用所需的颜色填满整个屏幕
    for (unsigned int y=0; y> 1;
       }

       buffer += gx_displayprop.cbyPitch >> 1;
    }

    // 结束绘图代码
    GXEndDraw();
}

按下按钮!

与绘图不同,GAPI 提供了一种简单处理硬件按钮的方法。对 GXOpenInputGXGetDefaultKeys 的调用将使系统做好准备,然后只需再添加一小段代码来处理 WM_KEYDOWN 消息。以下的示例用于检查 up 按钮,如果按下该按钮就退出程序:

case WM_KEYDOWN:

if (wParam == (unsigned) gx_keylist.vkUp)
    {
       SendMessage(hWnd,WM_CLOSE,0,0);
    }
    break;

很简单,是不是?只需再使用一小段代码设置一个定时器,就可以将所有这些组成一个简单的程序以清除屏幕、使其变为随机颜色。WinProc 函数中的定时器消息将调用 ClearScreen,直至按下 up 按钮。下面即为该程序的 WinProc,您可以看看它到底是什么样:

LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
{

    switch (message) 
    {
       
       case WM_CREATE:
          SetTimer(hWnd,1,100,NULL);
          break;

       case WM_TIMER:
          ClearScreen(rand(),rand(),rand());
          break;

       case WM_KEYDOWN:
          if (wParam == (unsigned) gx_keylist.vkUp)
             SendMessage(hWnd,WM_CLOSE,0,0);
          break;

       case WM_DESTROY:
          KillTimer(hWnd,1);
          GXCloseInput();
          GXCloseDisplay();
          PostQuitMessage(0);
          break;

       case WM_KILLFOCUS:
          GXSuspend();
          break;

       case WM_SETFOCUS:
          GXResume();
          break;

       default:
          return DefWindowProc(hWnd, message, wParam, lParam);
    }
    return 0;
}

可以编游戏了吗?

在编游戏之前,还需要了解更多信息吗?答案是否定的。对 GAPI 的简单了解以及在示例程序中的内容,完全够我们编写一些基本的 Pocket PC 游戏了。但是,如果真要编写一些娱乐软件,还需要花费一些时间熟悉例程(或从 Internet 上下载的三维绘图例程),以及熟悉诸如双缓冲区和实时 WAV 文件混合等技术。当然,我有理由给您留下空间,让您自己去学习。

GAPI 并不仅限于游戏

GAPI 并不仅仅对游戏编程人员有用,许多开发人员发现需要控制 Pocket PC 设备,禁止其他所有的应用程序。在理想情况下,开发人员应该能够重新编写现有的 Pocket PC,或者为所选择的应用程序创建特定的设备。但是,为 Pocket PC 创建一个全新的操作系统版本并不是一件小事情(顺便说一句,那是操作系统构建者的任务),对多数开发人员来说,编写针对某一硬件的驱动程序即使不是不可能,也会很复杂。

GAPI 可以提供一个有用的方式,使人感觉 Pocket PC 只运行需要的程序。GXOpenInput 函数相当于与 GX.DLL 的链接,使用 GXOpenDisplay 可以确保那些使用预订应用程序的可怜的家伙不会意外启动纸牌游戏或其他可怕的程序。

告别 GAPI

在下个月的专栏到来之前,这里的几个链接会帮助您获得有关 GAPI 的信息。希望您能使用它们为这个小小的游戏平台编写一些了不起的游戏!

用于桌面 Pocket PC 模拟器的 GAPI

这是非正式的工具,但却很有用。

用于手持 PC 的 GAPI

又不是 Microsoft 的产品,但有了它,您就可以从 HP Jornada 720 中获得乐趣。

GAPI 来了

这是一篇不错的介绍性文章。

GAPI 快速入门

这是一篇关于 GAPI 编程的有用文章。

GAPI 编程链接

这个开发人员站点提供了许多精彩的资源。

Logo

开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!

更多推荐