65.9K
CodeProject 正在变化。 阅读更多。
Home

Windows Mobile DirectDraw 游戏示例

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.91/5 (24投票s)

2009年7月29日

CPOL

22分钟阅读

viewsIcon

88275

downloadIcon

2477

重制了一个旧游戏,作为 DirectDraw 的简单演示。

引言

我为某人制作了这个游戏,作为如何做某事的示例。但看了看它,我觉得代码可能包含对其他人有用的信息,所以我决定分享它。

在这个旧游戏的重制版中,我利用 DirectDraw API 来处理多种 Windows Mobile 分辨率的图形渲染。我利用一种名为 MIP MAPS 的旧技术来解决一些人遇到的分辨率问题,同时将游戏逻辑与分辨率差异隔离开来。同一个二进制文件可以在 Windows Mobile Standard/SmartPhone 和 Windows Mobile Professional/Pocket PC 上运行,无需任何修改。

该游戏还利用了三星 Windows Mobile SDK 来利用其手机提供的一些用户交互功能,包括加速度计、滚轮键、触觉反馈和通知 LED。我将分三部分讨论此程序:游戏逻辑、DirectDraw 和三星 Windows Mobile SDK。

安装

要运行游戏,您必须安装两个 cab 文件。一个 cab 文件包含三星移动 SDK 文件,另一个包含游戏。即使您不使用三星设备,也需要安装三星 SDK cab 文件。

下载三星 Windows Mobile SDK

要编译此游戏,您需要下载三星 Windows Mobile SDK(即使您不在三星设备上运行此游戏)。该 SDK 可从其工具和 SDK 页面免费获取,只需快速简单的注册。

HTC 加速度计支持

此游戏不支持 HTC 设备中的加速度计。由于我没有此类设备或访问此类设备实现,因此无法对 HTC 加速度计进行测试。但是,如果您可以访问此类设备并了解如何使用其加速度计,则需要更新的代码量很少。

游戏逻辑

这个游戏的目标是清除一个玻璃球场。当三个同色球体相互接触时,它们将被擦除。玩家可以将新球体射入游戏场,尝试匹配它们的颜色。但如果球场集合达到游戏区域底部,游戏就结束了。

Windows Mobile 设备有各种形状和尺寸,而这个游戏严重依赖游戏场的形状。我决定将游戏场设为正方形并将其适配到设备屏幕上。如果您的设备屏幕比其高,则屏幕最左侧和最右侧的区域将不被使用。如果屏幕比其宽,则屏幕顶部和底部的区域将不被使用。

游戏逻辑本身完全不知道屏幕的实际尺寸。游戏区域(由PlayArea类表示)使用浮点数来表示世界中所有对象的位置。世界中的所有物品都具有由名为PointF的结构定义的坐标。与POINT结构一样,PointF具有两个名为xy的字段。虽然POINT结构在这些字段中具有整数数据,但PointF结构使用浮点数。在这个游戏的世界中,只存在一种类型的对象,即球体。

球体类

游戏中的彩色球体由Orb类表示。所有球体的大小都相同,所以我使用了一个名为Orb::radius的静态字段来设置它们的直径(目前设置为 32.0)。一个球体有一个位置、一个速度、一个颜色,并且可以设置一个标志来指示它是否正在从游戏场上掉落。从场上掉落的球体不能与其他任何球体互动。因此,一个设置为掉落的球体,就所有互动目的而言,都已消失,除了对过去事物的一种短暂提醒之外,什么也不是。

Orb类有一个名为IsTouching(Orb*)的成员方法。给定另一个球体的引用,此函数将返回true如果球体在触摸距离内,否则返回false。如前所述,一个掉落的球体不能与任何其他物体互动。因此,即使两个球体重叠,如果其中至少一个设置为掉落,则此函数将始终返回false。球体的位置由其中心点的坐标定义。我们可以使用勾股定理来计算两个球体中心点之间的距离。如果p1是一个球体的位置,p2是另一个球体的位置,那么如果sqrt((p1.x-p2.x)^2+(p1.y-p2.y)^2)<=Orb::radius,则两个球体正在触摸。移动设备的处理器往往较弱,所以只要能使计算更简单,就应该这样做。在这种情况下,我可以通过平方两边来简化计算,得到(p1.x-p2.x)^2+(p1.y-p2.y)^2>=(Orb::radius)^2Orb::IsTouching方法使用后一个不等式来确定两个球体是否正在触摸。

要移动一个球体,它的速度需要设置为非零值。Orb::SetVelocity(float x, float y)方法用于完成此操作。速度以每秒坐标位移表示(请记住,我们的世界大小为 512 x 512)。一旦设置了球体的速度,Orb::Step(float timeDelta)方法将定期调用,以计算在timeDelta中指定的时间单位数后球体的新位置。如果一个球体开始超出世界坐标范围,那么它将要么向相反方向反弹(如果它离开世界的左侧或右侧),要么完全停止(如果它撞到世界的顶部)。球体向下移动的唯一情况是它正在下落。下落的球体允许在被删除和回收内存之前,超出世界下半部分的坐标。

#pragma once
#include "stdafx.h"
#include "common.h"
#include <list>
using namespace std;

#define ORB_RADIUS  32
#define ORB_TOUCHING_DISTANCE2 ((ORB_RADIUS*2)*(ORB_RADIUS*2))

class Orb
{
private:
    bool    falling;
    static const int touchingDistanceSquared = 
                     ORB_TOUCHING_DISTANCE2;
    static RECTF BoundingBox;
    OrbColor color;
    PointF position;
    PointF velocity;
public:
    static const int radius = ORB_RADIUS;
    Orb(OrbColor color, float x, float y);
    void SetVelocity(float x, float y);
    void GetVelocity(PointF* vel);
        
    void GetPosition(PointF* pos);
    OrbColor GetColor();
    bool IsTouching(Orb* otherOrb);
    
    void Step(float timeUnit);

    bool IsFalling() {return falling;};
    void SetFalling();
    bool IsMoving() {return (velocity.x!=0)||(velocity.y!=0);}    
    
    PointF GetPosition() { return position; }
};

PlayArea 类

游戏区域由PlayArea类表示。它包含游戏中所有球体的集合,并且与游戏控制器状态的知识解耦(稍后会详细介绍)。PlayArea类还跟踪玩家希望射击下一个球体的角度。PlayArea类提供了一些关于球体组的宝贵信息。如果玩家成功匹配了一组球体,PlayArea类将检测到匹配以及其中涉及的所有球体。受匹配球体移除影响并悬浮在半空中未连接到其他固定球体的球体,将被此类别检测到,以便将它们设置为下落状态。PlayArea::LoadLevel从文本文件加载球体的排列,并为我们设置一个级别。并且,PlayArea::Clear方法可用于从游戏场中移除所有球体。关于游戏状态的大部分信息都包含在此类中。我使用标准模板列表类来包含球体。如果您以前从未使用过标准模板库,强烈建议您熟悉它,因为了解它可能会提高生产力。

#pragma once
#include "stdafx.h"
#include <list>
#include "common.h"
#include "Orb.h"

#include "GameController.h"

using namespace std;

#define PLAY_AREA_SIZE 512

class PlayArea
{
private:
    list<orb*> orbList;
    list<orb*> orbDestructionList;
    GameController* gameController;
    Orb* loadedOrb;
    Orb* movingOrb;
    int        GetColorsInPlay();
    OrbColor        nextOrbColor;

public:
    PlayArea(GameController* controller)
      {orbList;gameController=controller;loadedOrb=NULL;movingOrb = NULL;}
            PlayArea(int playAreaSize);
    Orb*    PlaceOrb(OrbColor color, float x, float y);

    void    LoadOrb(OrbColor color);

    void    SetCannonAngle(float angle);
    void    IncrementAngle(float incrementAmount);

    void    LoadLevel(LPWSTR levelData);

    list<orb*>*    GetOrbList() { return &orbList; }
    Orb*    GetLoadedOrb() { return loadedOrb; }
    Orb*    GetMovingOrb() { return movingOrb; }
    void    FireOrb(float angle);
    void    StopMovingOrb();

    REQUIRESDELETE    MyOrbList*    GetIntersectingOrbList(Orb*);
    OrbColor GetNextOrbColor() { return nextOrbColor; }
    void    SelectNextColor();
    REQUIRESDELETE    list<Orb*>*    PlayArea::GetMatchingSet(Orb* targetOrb);
    list<Orb*>*    GetSuspendedOrbs();
    //void    DestroyOrbs(OrbLink* orbList);
    void    DestroyOrb(Orb* target);
    void Clear();

    int        GetOrbCount();
    Orb*    GetLowestOrb();

};

GameLogic 类

GameLogic类的接口是本程序中使用的最简单的一个。该类公开了两种方法:GameLogic::LoadLevel打开一个文本文件并将其内容传递给PlayArea::LoadLevel,而GameLogic::Step执行游戏处理的交互。每次调用GameLogic::Step时,移动的球体都会前进,检查匹配球体或游戏结束的条件,并对这些条件做出适当的响应。

关于移动球体与静止球体接触时发生的逻辑需要更多解释。由于球体运动是离散而非连续的,当一个球体与另一个球体接触时,很可能两个球体会有轻微重叠。当这种情况发生时,球体会被向后移动到球体仍然接触但不重叠的点。这个过程的解释很简单,但实现略微复杂。实现如下所示。

current->GetPosition(&orb2Position);
movingOrb->GetPosition(&orb1Position);
float dx = orb2Position.x-orb1Position.x;
float dy = orb2Position.y-orb1Position.y;

//Get the distance by which we need to move the orb backwards
float targetDistance = Orb::radius*2-(float)sqrt((double)(dx*dx+dy*dy));

if(targetDistance!=0.0)
{
    //get the (linear) speed of the ball
    movingOrb->GetVelocity(&velocity);
    float speed = (float)sqrt((velocity.x*velocity.x)+
                              (velocity.y*velocity.y));
    
    //calculate the needed time over which the reverse velocity should be applied
    float adjTime = targetDistance/speed;
    //reverse the velocity
    movingOrb->SetVelocity(-velocity.x,-velocity.y);
    //Step Backwards
    movingOrb->Step(adjTime);
    //undo the reverse velocity
    movingOrb->SetVelocity(velocity.x,velocity.y);
    
}

GameController 类

游戏控制器类作为实际输入设备的一种抽象层而存在。玩此游戏的人可能会通过方向键、滚轮键(例如三星 Blackjack II SGH-i617 上的滚轮键)、加速度计或屏幕提供部分输入。我创建了一个类来封装游戏需要知道的信息,而不是将游戏逻辑与这些输入设备的每种可能状态混淆。用户必须提供的输入的一个简化视图是动作按钮是否被按下(用于发射球体)以及球体将以何种角度发射的选择。因此,GameController类包含两条信息:angleactionButtonPressed。在加速度计的情况下,用户可以通过倾斜设备直接选择一个角度。在滚轮键或控制面板的情况下,用户逐步选择一个角度。GameController::SetAngle允许直接设置一个角度,而GameController::IncrementAngle递增(或递减)输入角度。对于动作按钮,GameController::SetActionButtonPressed将设置标志,通知游戏逻辑动作按钮已被按下。由游戏负责使用GameController::ClearActionButton清除标志。当游戏需要读取控制器状态时,GameController::IsActionButtonPressed返回动作按钮标志的状态,GameController::GetAngleDeg返回以度为单位的角度,而GameController::GetAngleRad返回以弧度为单位的角度。

#pragma once
#include "Stdafx.h"
#include "common.h"

#define MAX_CONTROLLER_ANGLE 80.0f
//    The GameController represents the information that we 
//    need to get from the player (as opposed to a physical
//    controller). New methods of interaction added to the
//    game should manipulate this class. This layer of 
//    separation allows for easier adaptation to new control
//    methods reducing/removing the need to modify game logic
//    for a new controller type.
class GameController
{
private:
    float angle ;
    bool actionButtonPressed;

    void CheckAngleBoundaries();
public:
            GameController();
    float    IncrementAngle(float amount);
    float    GetAngle();
    float    GetAngleRad();

    float    SetAngle(float newAngle);

    void    PressActionButton();
    void    ClearActionButton();

    bool IsActionButtonPressed();
};

PlayAreaRenderer 类和 DisplayList 类

从游戏逻辑的角度来看,PlayAreaRenderer类负责绘制游戏对象。实际上,这个类会创建一个待办事项列表,其中包含渲染游戏场景所需的所有操作。必须执行的项目列表存储在DisplayList类中。与此程序中使用的大多数列表不同,DisplayList类包含一个静态大小的列表。在此类中使用动态列表会对性能和内存碎片化产生负面影响,因为它每秒会重新生成多次(大约 30-40 次,具体取决于设备的性能)。

#pragma once
#include "stdafx.h"
#include "common.h"
#include "PlayArea.h"

#include "Orb.h"
#include "DisplayList.h"
#include "OrbRenderer.h"

class PlayAreaRenderer
{
private:
    OrbRenderer* orbRenderer;
    PlayArea* playArea;
    DisplayList* displayList;
    GameController* gameController;

public:
    PlayAreaRenderer(OrbRenderer * or, PlayArea* pa, 
                     GameController *gc, DisplayList* dl)
    {
        playArea = pa;
        orbRenderer = or;
        displayList = dl;
        gameController = gc;
    }

    void Render();
    
};

#pragma once
#include "stdafx.h"
#include "common.h"

struct DisplayItem
{
    RECT    itemSource;
    RECT    itemDestination;
};

class DisplayList
{
private:
    DisplayItem    itemList[100];
    int itemCount;
public:
            DisplayList(){itemCount=0;}
    void    Clear() {itemCount=0;}
    void    AddItem(RECT* itemSource, RECT* itemDestination);
    void    Render(IDirectDrawSurface* spriteSource, 
                   IDirectDrawSurface* destinationSurface, POINT offset);
};

GameEventNotification 类

GameEventNotification类充当一个解耦层,用于向用户发送通知。它在以下四种事件之一发生时被调用:球体被发射、球体已停止、决定了球体的下一个颜色,或者用户成功匹配了颜色。如果我想做任何事情来通知用户这些操作之一,则通知的实现将从此类中调用。此处提供的代码将在球体发射或停止时生成触觉反馈(有关触觉反馈的更多信息,请参阅下面的“三星 Windows Mobile SDK”部分),或更改通知 LED 的颜色以传达下一个球体颜色。

#pragma once
#include "Stdafx.h"
#include "common.h"
#include "SmiHaptics.h"

class GameEventFeedback
{
    SmiHapticsNote    launchNote[1];

    bool hasNotificationLed;
    bool canBlink;
    bool hasHapticFeedback;
    SMI_HAPTICS_HANDLE hapticsHandle;
public:
    GameEventFeedback();
    ~GameEventFeedback();

    void    OrbFired();
    void    NextColorDecided(OrbColor);
    void    OrbStopped();
    void    ColorMatchMade();
};

DirectDraw 和游戏渲染

我之前提到,这个游戏可以在具有不同分辨率的多个 Windows Mobile 设备上渲染。我通过一种称为 mipmap 的技术处理了多分辨率渲染。Mip 是拉丁语短语multim im parvo的缩写,意思是“小地方的许多事物”。mipmap 的使用可以追溯到 20 多年前,但在今天的世界中仍然具有重要意义。许多 3D 系统都使用 mipmap 来保存纹理细节,微软的 DeepZoom 技术也广泛使用它。要解释什么是 mipmap,请看下面的球体图像。

example mipmap

图像包含我在游戏中使用的不同分辨率的球体。当一个球体被绘制到屏幕上时,将使用最接近当前需求的球体集合。一个名为OrbRenderer的类包含为给定需求选择球体的逻辑。该类的构造函数包含唯一用于根据分辨率做出决策的逻辑。游戏的其余大部分与分辨率无关。该类的构造函数根据球体半径、游戏区域的逻辑单位(世界单位)大小以及将要绘制的屏幕表面的物理大小执行计算。

OrbRenderer::OrbRenderer(
        float logicalPlayAreaSize, 
        float logicalOrbRadius, 
        float physicalPlayAreaSize, 
        DisplayList* displayList)
{
    this->targetDisplayList=displayList;
    logicalSize = logicalPlayAreaSize;
    this->scalingFactor = physicalPlayAreaSize/logicalPlayAreaSize ;
    destinationOrbSize.x=destinationOrbSize.y= 
              (int)logicalOrbRadius*2.0f*scalingFactor;
    destinationOrbRadius = destinationOrbSize.x/2;
    if(destinationOrbSize.x>=48)
    {
        sourceOrigin.x=0;
        sourceOrigin.y=0;
        orbDisplacement.x=64;
        orbDisplacement.y=0;
        orbImageSize.x=orbImageSize.y=64;
    }
    else if (destinationOrbSize.x>=24)
    {
        sourceOrigin.x=0;
        sourceOrigin.y=64;
        orbDisplacement.x=32;
        orbDisplacement.y=0;
        orbImageSize.x=orbImageSize.y=32;
    }
    else if (destinationOrbSize.x>=12)
    {
        sourceOrigin.x=0;
        sourceOrigin.y=96;
        orbDisplacement.x=16;
        orbDisplacement.y=0;
        orbImageSize.x=orbImageSize.y=16;
    }
    else
    {
        sourceOrigin.x=0;
        sourceOrigin.y=112;
        orbDisplacement.x=8;
        orbDisplacement.y=0;
        orbImageSize.x=orbImageSize.y=8;
    }    
}

VGA Screen Clip

QVGA Clip

VGA 和 QVGA 渲染并排显示,大小相同。

使用 DirectDraw

创建 DirectDraw 对象

DirectDraw 通过 COM 接口公开其功能。与任何 COM 接口一样,您必须记住在不再需要它时释放接口。不这样做可能会导致内存泄漏,因此您会听到我在整篇文章中重申释放 COM 接口的重要性。用于访问大多数 DirectDraw 功能的接口名称是IDirectDrawIDirectDraw实现的实例通过函数DirectDrawCreate获取。以下是DirectDrawCreate的调用签名

 HRESULT WINAPI DirectDrawCreate(
  GUID FAR* lpGUID, 
  LPDIRECTDRAW FAR* lplpDD, 
  IUnknown FAR* pUnkOuter
); 

对我们的用法来说,只有第二个参数是重要的。其他两个参数应设置为NULL。函数将检索一个实现IDirectDraw的对象,并将其地址分配给第二个参数中指定的接口指针。

设置 DirectDraw 模式(协作级别)

DirectDraw 程序可以在两种模式或协作级别之一中运行:独占模式或普通模式。所选模式将影响您的程序与系统其余部分的交互方式(因此称为“协作级别”)。在独占模式下,您拥有整个显示器,并且能够使用页面翻转。在普通模式下,您无法访问页面翻转,并且必须注意绘制位置。考虑到这两条信息,独占模式似乎更具吸引力。但是,通过独占模式获得的自由带来了更多的责任。在独占模式下,通知(例如来电)无法通过。因此,您必须密切关注通知并做出相应响应。在本文的大部分内容中,我们将以普通模式工作。协作级别通过IDirectDraw::SetCooperationLevel设置。此方法的调用签名如下

HRESULT SetCooperativeLevel(
  HWND hWnd, 
  DWORD dwFlags
);

第一个参数是您的应用程序的顶级窗口句柄。第二个参数是一个标志,指示要设置的模式。要使用独占模式,请同时传递DDSCL_FULLSCREEN | DDSCL_EXCLUSIVE标志。要使用普通模式,请传递DDSCL_NORMAL标志。

一旦 DirectDraw 初始化完成,我们几乎就可以开始绘制了。但是,我们需要一个可以绘制的对象。对于 DirectDraw API,作为绘制操作目标的对象称为表面。表面实现IDirectDraw表面接口。表面可以是屏幕上的,也可以是屏幕外的。屏幕上的表面与可见显示内存相关联。屏幕外表面可以存在于设备视频内存的不可见部分或主内存中。方法IDirectDraw::CreateSurface将为我们创建一个IDirectDrawSurface对象。其调用签名如下

HRESULT CreateSurface(
  LPDDSURFACEDESC lpDDSurfaceDesc, 
  LPDIRECTDRAWSURFACE FAR* lplpDDSurface, 
  IUnknown FAR* pUnkOuter 
);

第一个参数包含有关要创建的表面的信息。第二个参数是输出参数。指向新创建的表面的指针将传递到第二个参数中引用的变量中。第三个参数可以为null。第一个参数值得更多解释。DDSURFACEDESC到底是什么?

DirectX 中的许多函数都将它们的参数堆叠在一个结构中。IDirectDraw::CreateSurface也不例外。DDSURFACEDESC参数打包了有关要创建的表面的信息。某些字段是可选的,具体取决于我们尝试创建的表面类型。为了知道我们填充了哪些字段,哪些没有,一个名为dwFlags的成员必须设置标志,指示我们填充了哪些字段。另一个名为dwSize的成员函数必须设置为DDSURFACEDESC结构的大小。随着 DirectDraw 的未来版本,DDSURFACEDESC的大小可能会增加,因此 DirectDraw 使用此参数来了解我们正在使用的DDSURFACEDESC结构的大小。要在可见视频内存上创建表面,唯一必须设置的参数是ddsCaps成员。ddsCaps成员的类型为DDSCAPS。Windows Mobile 中DDSCAPS的实现有一个名为dwCaps的单个字段。该字段接受标志。要在可见视频内存中创建表面,需要DDSCAPS_PRIMARYSURFACE标志。

DirectDraw 剪裁器

在以普通协作模式运行的典型程序中,程序的无限绘制是不可取的。DirectDraw 提供了一种机制,通过 DirectDraw 剪裁器限制屏幕(或任何其他 DirectDraw 表面)上的绘制区域。DirectDraw 剪裁器实现IDirectDrawClipper接口。这些对象基于 GDI 区域。因此,您可以提供一个矩形区域列表,其中不能进行绘制。对于我们的程序,我们将使用一种更简单的方法来创建剪裁器。为了仅允许在客户端窗口占据的区域中绘制,我们可以将窗口句柄传递给IDirectDrawClipper::SetHWnd。剪裁器本身通过IDirectDraw::CreateClipper创建。

位块传输

如果您熟悉 GDI 编程,那么 Blitting 的概念就不足为奇了。BLT 代表位块传输,即将数据从一个内存块传输到另一个内存块。大多数现代图形 API 都将具有此功能的等效项。对于此程序中执行的 DirectDraw blit 操作,我将黑色设置为表示透明度的颜色。在我的原始 mipmap 图像中,黑色像素将导致不渲染任何像素。如果您想测试这一点,在我的球体图像中心绘制黑色圆圈,您将在游戏中看到甜甜圈而不是球体。

三星 Windows Mobile SDK

三星的 Windows Mobile SDK 是我遇到的第一个(也是迄今为止唯一的)OEM 厂商支持开发者访问其设备特定功能的例子。在其他设备上,如果您想访问 Windows Mobile API 未公开的功能,那么您必须进行一些逆向工程和破解(或者等待其他人这样做)才能使用它。如果该功能在设备的未来版本中以不同的方式实现,您也必须考虑到这一点。

借助三星 Windows Mobile SDK,三星设备中功能实现的细节通过一致的接口被抽象出来。他们的 SDK 允许您查询功能是否存在,如果存在,您可以获取有关其功能和限制的更多详细信息。有关 SDK 的更多信息可以在三星移动创新者网站上找到。

滚轮键

我使用 SDK 用于非常具体的目的。在三星 Blackjack 上,有一个名为滚轮键的慢跑拨号式界面。它位于显示屏正下方。对于这个游戏,滚轮键是一种控制游戏的自然方式。如果您监控与滚轮键相关的 Windows 消息,它们会以VK_UPVK_DOWN键按下形式出现。使用三星移动 SDK,我可以区分由某人按下方向键引起的VK_UPVK_DOWN消息与由滚轮键生成的消息。

case VK_UP:
    {
        UINT scanCode = (lParam & 0x00FF0000) >> 16;
        if(TRUE == SmiKeyMsgIsFromWheelKey(wParam,scanCode))
        {
           // Message is from wheelkey rotation
            g_gameController->IncrementAngle(-5);
            
        }
    }
    break;
case VK_DOWN:
    {
        UINT scanCode = (lParam & 0x00FF0000) >> 16;
        if(TRUE == SmiKeyMsgIsFromWheelKey(wParam,scanCode))
        {
           //Message is from Wheelkey Rotation
            g_gameController->IncrementAngle(5);
        }
    }
    break;

三星 Blackjack (i617) 上的滚轮键图片

加速度计

三星设备中的加速度计可以异步或同步读取。由于此游戏的离散性质,同步读取对我来说效果很好。只需两行代码即可查看游戏是否在带有加速度计的三星硬件上运行。

SmiAccelerometerCapabilities accelCaps;    
g_hasAccelerometer = (SMI_SUCCESS == SmiAccelerometerGetCapabilities(&accelCaps));

如果存在三星加速度计,则g_hasAccelerometer将设置为true。在游戏循环中,读取加速度计向量。加速度计总是返回一个指向物理向下方向的向量。如果用户更改了设备的方向,那么我也必须考虑到这一点。可以通过读取注册表键来查找用户设备的方向。该键返回 0、90、180 或 270 的值,表示设备屏幕旋转了多少度。

DWORD GetScreenOrientation()
{
    DWORD retVal ;
    HKEY hKey;
    DWORD dataSize = sizeof(DWORD);
    RegOpenKeyEx(HKEY_LOCAL_MACHINE,TEXT("System\\GDI\\Rotation"),NULL,NULL,&hKey);
    RegQueryValueEx(hKey,TEXT("Angle"),NULL,NULL,(LPBYTE)&retVal, &dataSize);
    RegCloseKey(hKey);
    return retVal;
}

此值用于重新定向我们从加速度计读取的向量。一旦我有了正确的值,我就会进行一些三角函数运算,以计算设备旋转了多少度。当在带有加速度计的设备上玩游戏时,球将始终尝试以物理上指向向上的方向发射。

if(g_hasAccelerometer)
{
    SmiAccelerometerVector vect,rotatedVect;
    if(SMI_SUCCESS==SmiAccelerometerGetVector(&vect))
    {
        switch(screenOrientation)
        {
        case 0:
            rotatedVect = vect;
            break;
        case 90:
            rotatedVect.x=-vect.y;
            rotatedVect.y=vect.x;
            rotatedVect.z=vect.z;
            break;
        case 180:
            rotatedVect.x=-vect.x;
            rotatedVect.y=-vect.y;
            rotatedVect.z=vect.z;
            break;
        case 270:
            rotatedVect.x=vect.y;
            rotatedVect.y=-vect.x;
            rotatedVect.z=vect.z;
            break;
        }
        float angle;
        float absAngle;
        float distance;
        if(rotatedVect.y==0)
            angle = 0;
        else
        {
            angle = -atan(rotatedVect.x / rotatedVect.y);
            angle = (angle/(2*3.141592))*360.0;
        }
        g_gameController->SetAngle(angle);
    }
}

通知LED

某些三星设备具有可发出七种不同颜色(红色、黄色、绿色、蓝色、紫色和白色)的通知 LED。如果此游戏在此类设备上运行,则下一个球体的颜色将由通知 LED 指示。LED 将亮起长达 4 秒,以指示下一个颜色。通知 LED 可通过SmiLedTurnOn激活。该函数需要一个COLORREF变量来指示颜色,以及其他一些参数来指示闪烁模式和 LED 应开启的时长。我在GameEventFeedback::NextColorDecided方法中使用以下代码

void GameEventFeedback::NextColorDecided(OrbColor color)
{
    SmiLedAttributes attrib;
    switch(color)
    {
        
        case Orb_Red: attrib.color=RGB(0xFF,0x00,0x00);    break;
        case Orb_Yellow: attrib.color=RGB(0xFF,0xFF,0x00);    break;
        case Orb_Green:    attrib.color=RGB(0x00,0xFF,0x00);    break;
        case Orb_Blue:    attrib.color=RGB(0x00,0x00,0xFF);    break;
        case Orb_Teal:    attrib.color=RGB(0x00,0xFF,0xFF);    break;
        case Orb_Purple: attrib.color=RGB(0xFF,0x00,0xFF);    break;
        case Orb_White:    attrib.color=RGB(0xFF,0xFF,0xFF);    break;
        default:    attrib.color=0;break;
    };
    attrib.onTime=750;
    attrib.offTime=250;
    attrib.duration=4000;
    attrib.pattern= SMI_LED_PATTERN_SOLID;
    SmiLedTurnOn(SMI_LED_ID_NOTIFICATION,&attrib);
}

通知 LED 在此处亮起,表示接下来将出现一个红色球体

触觉反馈

对于具备触觉反馈功能的三星设备,当球体被发射或撞击其他球体时,您会感受到反馈。触觉反馈通过定义一组触觉音符来调用。一个触觉音符包含关于振动强度和形状的信息。对于我的目的,一个音符就足够了,所以我创建了一个包含音符数据的单元素数组。该数组位于GameEventFeedback类中。

SmiHapticsNote    launchNote[1];    //Haptic note array of one element

GameEventFeedback类的构造函数会检测是否存在触觉反馈硬件。如果硬件存在,则根据硬件的功能填充音符。

GameEventFeedback::GameEventFeedback()
{
    hapticsHandle=NULL;
    SmiLedCapabilities ledCaps;
    SmiHapticsCapabilities hapticCaps;

    SMI_RESULT result;
    result = SmiLedGetCapabilities(SMI_LED_ID_NOTIFICATION,&ledCaps);
    if(result==SMI_SUCCESS)
    {
        

        hasNotificationLed = true;
        canBlink = ledCaps.blinkIsSupported;
    }
    result = SmiHapticsGetCapabilities(&hapticCaps);
    if(SMI_SUCCESS==result)
    {
       //Haptic feedback hardware is present
        if(SMI_SUCCESS==SmiHapticsOpen(&hapticsHandle))
        {
           //Open a handle to the haptic feedback
           //device and populate the note data 
           //according to the hardware's capability
            hasHapticFeedback = true;

            launchNote[0].duration=max(hapticCaps.minPeriod,400);
            launchNote[0].magnitude=255;
            if(hapticCaps.startEndMagIsSupported)
            {
                launchNote[0].startingMagnitude=64;
                launchNote[0].endingMagnitude=255;
            }
                launchNote[0].period=max(hapticCaps.minPeriod,0.);
            if(hapticCaps.noteStyleIsSupported)
                launchNote[0].style=SMI_HAPTICS_NOTE_STYLE_STRONG;
        }
        else
        {
            hasHapticFeedback = false;
        }
    }
}

一旦创建了音符并打开了硬件句柄,就可以通过一次调用SmiHapticsPlayNotes来播放音符序列。这在GameEventFeedback::OrbFiredGameEventFeedback::OrbStopped方法中都完成了。

void GameEventFeedback::OrbFired()
{
    SmiHapticsPlayNotes(hapticsHandle,1,launchNote,FALSE,NULL);
}

void GameEventFeedback::OrbStopped()
{
    SmiHapticsPlayNotes(hapticsHandle,1,launchNote,FALSE,NULL);
}

光学鼠标

一些三星设备(如 Epix)具有一种既可作为鼠标垫又可作为方向键的输入设备。用户可以更改垫子的模式。在鼠标模式下,它在游戏中作用不大。因此,当程序启动时,游戏将自动检测此输入设备的存在并将其切换到光标模式。

//if an optical mouse is present then 
//ensure it is in navigation mode. 
SmiOpticalMouseCapabilities mouseCaps;
//Get the optical mouse capabilities
SmiOpticalMouseGetCapabilities(&mouseCaps);
// if a mouse pad is present
if(mouseCaps.multiOperationModeIsSupported)
{
    //ensure the mouse pad is in navidation mode
    SmiOpticalMouseSetMode(SMI_OPTICAL_MOUSE_MODE_NAVIGATION);
}

三星手机上的光学鼠标垫图片

其他信息

更快的 Windows 消息处理结构

开发者在尝试编写第一个图形密集型 Windows Mobile 应用程序时常犯的一个错误是使用 Visual Studio 为您创建的默认消息处理结构。默认消息处理结构是为桌面应用程序设计的。在大多数此类应用程序中,除非发生某些事情(收到电子邮件、用户按下按钮等),否则应用程序不会执行任何操作。默认消息处理结构就是为这类场景设计的。尝试在图形密集型程序或任何视觉状态不断变化的程序中使用该结构将导致性能不佳。在使用不适当的结构后,我曾听到开发者得出结论,认为他们体验不佳的原因是 Windows 的某些不足或WM_PAINT消息没有足够高的优先级,而实际情况是他们的程序应用了错误的模式。

更理想的消息处理结构如下。在这种结构中,程序将能够持续更新其显示。虽然使用此消息处理循环可以获得更流畅的视频,但它也与更高的功耗相关联。当游戏失去焦点时,我有代码遵循旧的消息处理结构,在该结构中,线程会被阻塞,直到有消息需要处理。如果用户让游戏在后台运行,这将防止游戏耗尽电池。

while(keepRunning)
{
   if(g_bHasFocus)
   {
      if(PeekMessage(&msg,NULL,0,0,TRUE))
      {
         if(msg.message==WM_QUIT)
            keepRunning=false;
         if (!TranslateAccelerator(msg.hwnd, hAccelTable, &msg)) 
         {
            TranslateMessage(&msg);
            DispatchMessage(&msg);
         }
      }
      else
      {
         //Execute Game Logic Here
         
         //Relinquish control of the processor but 
         //stay on queue of threads ready to run
         //within the thread scheduler
         Sleep(0);
      }
   }
   else
   {
      if(GetMessage(&msg,NULL,0,0))
      {
         if (!TranslateAccelerator(msg.hwnd, hAccelTable, &msg)) 
         {
            TranslateMessage(&msg);
            DispatchMessage(&msg);
         }
      }
   }
}

级别文件格式

我只为游戏提供了一个关卡。关卡布局的格式是一个纯文本文件。每行,“ORB”后面跟着球体的 X 和 Y 坐标,然后跟着一个数字,指示球体的颜色(请参阅OrbColor枚举以获取数字到颜色的映射)。每行以分号结尾。如果您决定创建新的关卡文件或编辑我提供的文件,请注意以下事项

  • 您的所有球体都应该相互接触或接触天花板。如果它们不接触,它们将被标记为悬浮球体,并在第一个球体被射出时掉落。
  • 文件采用 16 位字符编码。如果您添加了一个使用 8 位字符的文件,它将严重失败。
  • 请记住,坐标位于一个 512x512 单位的世界中
  • 不要创建屏幕底部有球体的布局。如果这样做,游戏将在开始时结束。

最后

如前所述,我将此代码作为示例提供给某人。我不打算更新它。但是,此代码的部分内容来源于我正在进行的关于各种 Windows Mobile 图形 API 的研究。完成研究后,我希望能够提供所有图形 API 的使用示例。本文我只是粗略地介绍了 DirectDraw,没有深入细节,但我已经准备了一份更详细的指南。该指南的 DirectDraw 部分目前约有 15 页。Direct3D、GDI/GDI+、DirectShow 和 OpenGL ES 都将是该指南的一部分。我计划在未来几周内将每个部分作为独立的文章发布。

历史

  • 2009 年 7 月 30 日 - 初次发布。
© . All rights reserved.