游戏编程入门(8):管理子画面

子画面相互作用的主要方式是通过碰撞发生的,这涉及物体的彼此碰撞。本文重点介绍一个子画面管理器的设计和分开,这个管理器允许建立一个子画面系统内部的作用和反作用。

本文内容包括:

  • 子画面管理对游戏的重要性
  • 如何设计子画面管理器
  • 如何修改游戏引擎,以便支持子画面管理
  • 如何使用双缓冲技术消除动画中的闪烁
  • 如何开发一个使用新的子画面特性(如碰撞检测)的例子

接上文 游戏编程入门(7):使用子画面动画移动对象


了解管理子画面的需求

在上一篇文章中,我们开发了一个Sprite 子画面类,它建立了可以移动的图形对象的基本物理性质模型。然后,创建了一个名为Planets 的例子,在同一个太空中漂浮着多个行星子画面。虽然Planets 程序中的子画面看起来像是在同一个太空中,但是它们之间并不存在真正的联系。与真是的世界不同,行星子画面不会彼此碰撞并相应地做出反应。这种局限性源自于这样一个事实:Sprite 子画面类没有考虑到子画面之间的关系,因此需要一个能够监视子画面系统并管理相互作用的子画面系统。

子画面管理器的思路是,将系统中的所有子画面都组织在一起,这样就可以一起更新和绘制它们。此外,子画面管理器还必须能够比较子画面彼此的位置,并确定是否发生了碰撞。如果发生了碰撞,那么随后管理器必须以某种方式通知系统发生了碰撞,这样,程序就可以相应地做出反应。

子画面管理器的另一个优点是,它提供了一种方法来支持额外的边界动作,Die(消失)。Die 边界动作会导致在子画面遇到一个边界时破坏这个子画面。这在射击游戏中应用很广阔,在子弹子画面到达游戏屏幕边缘时破坏这个子弹子画面。在Sprite 类中很难直接支持Die 边界动作,因为这种动作假定是要消除子画面,但是很难让子画面消除它自己。最好将这项任务留给外部的一方,它的工作就是监视游戏中的所有子画面,这就是子画面管理器。

设计子画面管理器

可能有人认为,要像在上一章中创建Sprite 子画面类那样讲子画面管理器设计为一个类。但是,子画面管理器与游戏引擎是紧密相连的,因此将子画面管理器与游戏引擎直接集成会更有益。我们实际上会将子画面管理器创建为GameEngine 类中的一组方法。

即使创建子画面管理器是作为对游戏引擎的修改,也需要在GameEngine 类外部做一些改动。更具体的说,为了使用子画面管理器平滑地应用子画面,需要在Sprite 子画面类做一些改动。

第一个改动涉及支持子画面动作,它用来通知子画面管理器应该对特定的子画面执行动作。子画面动作类似于边界动作,只不过他们更为灵活。例如,要支持的第一个子画面动作是Kill ,它用来通知子画面管理器将要破坏一个子画面。Kill 子画面动作类似于Die 边界动作,但是发出Kill 可以是出于各种不同的原因。例如,在发生碰撞时通常就会激活子画面动作,这样一枚导弹就可以在发生冲击时破坏一辆坦克。

除了子画面动作之外,对Sprite 子画面类 和子画面管理器的另一个主要要求是碰撞检测。在上一章中介绍过,碰撞检测包括检查两个子画面是否彼此碰撞了。读者还了解了一项名为缩小矩形碰撞检测的技术,它使用一个稍小于子画面的矩形来作为碰撞检测的基础。因为这种碰撞检测形式需要它自己的矩形,所以添加一个碰撞矩形作为Sprite 子画面类的一个成员是有意义的,还要包括一些支持方法,以便计算这个矩形并测试与另一个子画面的碰撞。

子画面管理器本身是直接集成到游戏引擎中的,它主要包括添加一个成员变量来跟中子画面列表。这个成员变量可以是一个固定大小的数组(这个大小是允许的最大子画面数量),也可以是一个更高级的数据结构,例如可以动态扩大的矢量,以便存储额外的子画面。

不论子画面列表具体是如何建立的,子画面管理器都必须提供几个方法来与所管理的子画面交互。下面是子画面管理需要使用这些方法完成的主要任务列表

  • 向子画面列表添加新的子画面
  • 绘制子画面列表中的所有子画面
  • 更新子画面列表中的所有子画面
  • 清除子画面列表中的所有子画面
  • 测试一个点是否在子画面列表中的某个子画面内部

除了必须在游戏引擎上完成这些任务之外,为游戏提供一个在任何时候发生子画面碰撞时调用的函数也是很重的。对于这一点,因为处理子画面碰撞的工作与具体的游戏情况密切相关,所以让游戏代码来进行处理会更好,而不是在游戏引擎中包括这个函数。因此,使用子画面管理器的任何游戏都必须提供一个子画面碰撞通知函数,这样游戏引擎就能够响应子画面碰撞。当然,子画面管理器还必须确保在任何时候发生碰撞,都调用了这个函数。

向游戏引擎添加子画面管理器

改进 Sprite 子画面类

在Sprite 子画面类中,需要更改的第一部分代码是在成员变量中添加一个碰撞矩形,它用来确定一个子画面是否与另一个子画面相撞。

RECT m_rcCollision;

碰撞矩形需要一个单独的访问方法,这样子画面管理器就可以在检测碰撞时访问这个矩形,这个方法名为GetCollision( )

 RECT&   GetCollision()            
  {
      return m_rcCollision; 
  };

此外我们还需要一个根据位置矩形计算碰撞矩形的方法,CalcCollisionRect( )。它我是一个虚拟函数,因此派生类可以重写这个函数,使用它们自己特定的碰撞矩形计算方法。

virtual void  CalcCollisionRect();

下面列出CalcCollisionRect( ) 方法的代码,它计算子画面的碰撞矩形的方法是将位置矩形减去子画面大小的1/6。

inline void Sprite::CalcCollisionRect()
{
  int iXShrink = (m_rcPosition.left - m_rcPosition.right) / 12;
  int iYShrink = (m_rcPosition.top - m_rcPosition.bottom) / 12;
  CopyRect(&m_rcCollision, &m_rcPosition);
  InflateRect(&m_rcCollision, iXShrink, iYShrink);
}

这段有一点容易让人误解的地方:子画面在X和Y方向上收缩的值首先计算为子画面大小的1/12。探究将这些值传入 Win32 InflateRect( )函数,这个函数使用各个值在每一个方向收收缩子画面。最后的结果就是碰撞矩形比位置矩形小1/6(因为对子画面的每一遍都应用了收缩值)。

对于碰撞,Sprite 子画面类提供了一个名为TestCollision( )方法,用来检查子画面是否与另一个子画面碰撞,如下所示。

 BOOL          TestCollision(Sprite* pTestSprite);

下面是这个方法的具体实现,这个方法检查两个子画面碰撞矩形是否有任何部分存在重叠的情况。

inline BOOL Sprite::TestCollision(Sprite* pTestSprite)
{
  RECT& rcTest = pTestSprite->GetCollision();
  return m_rcCollision.left <= rcTest.right &&
         rcTest.left <= m_rcCollision.right &&
         m_rcCollision.top <= rcTest.bottom &&
         rcTest.top <= m_rcCollision.bottom;
}

如果这两个子画面之间确实发生了碰撞,那么TestCollision( )方法将返回TRUE,否则返回FALSE。

此外,在Sprite 子画面类中添加的碰撞矩形m_rcCollision,必须在构造函数中初始化它。而且所有的构造函数(3个)都必须包括了对CalcCollisionRect( )的调用,CalcCollisionRect()将根据子画面的位置矩形设置碰撞矩形。

Sprite 子画面类中的另一个重大变化涉及添加子画面动作,这允许子画面管理器在发生像子画面碰撞这样的事件时操作子画面。使用一个自定的数据类型SPRITEACTION 来表示子画面动作

typedef WORD        SPRITEACTION;
const SPRITEACTION  SA_NONE   = 0x0000L, //不做任何事情
                    SA_KILL   = 0x0001L; //从子画面列表删除一个子画面并破坏它

SPRITEACTION 数据类型,只定义了两个子画面动作,以后可以根据需要再添加新的动作,以便扩展子画面管理器的任务。这些子画面动作的真实含义是在Update( )方法中指定的,现在这个方法定义为返回一个SPRITEACTION 值,指定对这个子画面执行的任何操作。

Update( )方法的一大变化是,它现在支持BA_DIE 边界动作,这会导致在一个子画面碰到一个边界时破坏它。这个边界动作是由SA_KILL子画面动作实现的,在发生BA_DIE边界动作时,Update( )方法就会返回SA_KILL子画面动作。因此,对于BA_DIE 边界动作,Update( )方法返回SA_KILL子画面动作,导致破坏子画面并从子画面列表中删除它。

增强游戏引擎

对于子画面列表来说,相对于使用数组来存储,使用STL中的vector集合类会更方便与灵活。STL vector 类允许存储和管理任何类型的对象列表。然后使用一组方便的方法来操作它们。

使用STL 中的任何数据集合类的第一步就是正确包括这个类的头文件及其名称空间。

#include <vector>
using namespace std;

要想使用一个STL 集合类,例如vector 类,只需要声明一个类型为vector 的变量即可,但是还需要在尖括号中包括想要在矢量中存储的数据类型。下面的代码展示了如何创建Sprite指针的矢量。

vector<Sprite*>     m_vSprites;

这段代码创建了一个包含Sprite 指针的矢量,这正是在游戏引擎中跟踪子画面列表所需要的。现在可以使用m_vSprites 矢量来管理子画面列表并在需要时与他们交互。设置vector 变量的属性是有益的,这样它在游戏中的效率就会高一些。

为矢量保留的内存数量,决定了在分配更多的内存之前能够在矢量中存储多少个子画面指针。这并不是限制能够在矢量中存储的子画面数量,而是确定vector 类必须为新的子画面分配内存的频率。因为分配内存会好费时间,所以尽量减少这种操作是有益的。对于大多数游戏的需求来说,在需要额外分配内存之前为100个子画面留出空间就足够了。这项内存分配工作是在GameEngine::GameEngine( )构造函数中进行的。

我们需要在游戏引擎中添加一个新的游戏函数为子画面管理器提供支持,它是游戏特有的代码一部分。这个函数被命为SpriteCollision( )。它的工作是以游戏特有的形式响应子画面碰撞

BOOL SpriteCollision(Sprite* pSpriteHitter, Sprite* pSpriteHittee);

要记住,创建的每一个游戏都必须提供SpriteCollision( ),游戏引擎中的CheckSpriteCollision( )方法调用了SpriteCollision( ) 函数。CheckSpriteCollision( ) 方法遍历子画面列表,并检查是否有任何子画面发生了碰撞。

 BOOL                CheckSpriteCollision(Sprite* pTestSprite);

CheckSpriteCollision( )方法调用了SpriteCollision( ) 函数来处理单个子画面碰撞。

CheckSpriteCollision( )方法首先遍历整个子画面列表,在循环内部,首先执行一次检查,以便确保没有将子画面与它自身相比较。然后调用TestCollision( )方法,在两个子画面之间执行碰撞测试,如果检测到了碰撞,则调用SpriteCollision( )函数,这样游戏就可以恰当地响应碰撞。

CheckSpriteCollision( )方法返回TRUE会导致将一个子画面还原到更新之前的原始位置,而返回值为FALSE则允许子画面继续沿着它的路线前进。

还需要向GameEngine类添加一组公共的子画面管理方法,它们用来与子画面管理器交互。

  void                AddSprite(Sprite* pSprite);     //向子画面列表添加子画面
  void                DrawSprites(HDC hDC);           //绘制子画面列表的所有子画面
  void                UpdateSprites();                //更新子画面的位置
  void                CleanupSprites();               //释放子画面并清空子画面矢量
  Sprite*             IsPointInSprite(int x, int y);  //查看一个点是否位于子画面列表中的某个子画面内部

使用双重缓存消除闪烁

现在,子画面管理器已经完成了,可以在例子中使用了。不过,在继续进行之前需要提及一点未完成的工作。读者可能已经注意到了,到目前为止,在本书中的所有动画例子都存在很烦人的闪烁。导致这种闪烁的原因是,在绘制动画图形之前重新绘制了游戏屏幕上的背景图像。

换句话说,每次移动动画图形对象时都要删除并重新绘制。因为擦除和重新绘制过程是直接在游戏屏幕上进行的,所以图像看起来就存在闪烁。

可以使用一种名为双重缓存的技术来解决子画面动画的闪烁问题。在双重缓存中,所有擦除和绘制工作都是在屏幕外的一个绘图表面上完成的,用户看不到这个表面。在完成所有绘制工作之后,一次性地将最终结果直接绘制到游戏屏幕上。因为没有发生可见的擦除,因此最终的动画是没有闪烁的。

这里写图片描述

缓冲区就是内存中的一个区域,我们在这里绘制图形。在传统的单缓存动画中,缓冲区就是游戏屏幕本身,而双重缓存还添加了一个屏幕外的内存缓冲区。

在游戏中使用双重缓存并不是一件很困难的事情。第一步是创建两个全局变量来记录屏幕外设备以及作为屏幕外缓冲区的位图。

HDC         g_hOffscreenDC;
HBITMAP     g_hOffscreenBitmap;

有了这两个变量之后,需要创建屏幕外的设备环境,然后使用它创建一个屏幕外位图。这个位图的大小与游戏屏幕相同。然后,需要将屏幕外位图选入屏幕外设备环境中。

  g_hOffscreenDC = CreateCompatibleDC(GetDC(hWindow));
  g_hOffscreenBitmap = CreateCompatibleBitmap(GetDC(hWindow),
  g_pGame->GetWidth(), g_pGame->GetHeight());
  SelectObject(g_hOffscreenDC, g_hOffscreenBitmap);

现在已经有了一个大小与游戏屏幕相同的屏幕外位图,并且已经选入了一个可以绘制的屏幕外设备环境。下面的代码展示了如何使用屏幕外设备环境和位图,向游戏中的绘制代码添加双重缓存支持。

 // 获得用于重新绘制游戏的设备环境
  HWND  hWindow = g_pGame->GetWindow();
  HDC   hDC = GetDC(hWindow);

  // 在屏幕外设备环境上绘制游戏
  GamePaint(g_hOffscreenDC);

  // 将屏幕外位图位块传送到游戏屏幕上
  BitBlt(hDC, 0, 0, g_pGame->GetWidth(), g_pGame->GetHeight(),
    g_hOffscreenDC, 0, 0, SRCCOPY);

这段代码非常适合放在GameCycle( )函数中。向我们熟悉的GamePaint( )函数传递屏幕外设备环境,这意味这所有游戏绘制工作都是在屏幕外发生的,然后立刻在游戏屏幕的设备环境上绘制得到的图像,这就消除了产生闪烁的可能性。

开发Planets 2 示例

Planets 2 示例和上一个Planets 示例唯一的区别就是,增加了碰撞检测机制(子画面管理)以及双重缓存动画。

注意:若出现编译错误,请在项目设置->连接->对象/库模块中 加入 msimg32.lib winmm.lib

Planets 2 目录结构和效果图

Planets 2 目录结构:

这里写图片描述

Planets 2 效果图:

这里写图片描述

Resource.h 源代码

//-----------------------------------------------------------------
// Planets Resource Identifiers
// C++ Header - Resource.h
//-----------------------------------------------------------------

//-----------------------------------------------------------------
// Icons                    Range : 1000 - 1999
//-----------------------------------------------------------------
#define IDI_PLANETS         1000
#define IDI_PLANETS_SM      1001

//-----------------------------------------------------------------
// Bitmaps                  Range : 2000 - 2999
//-----------------------------------------------------------------
#define IDB_GALAXY          2000
#define IDB_PLANET1         2001
#define IDB_PLANET2         2002
#define IDB_PLANET3         2003

Planets.h 源代码

#pragma once

//-----------------------------------------------------------------
// 包含的文件
//-----------------------------------------------------------------
#include <windows.h>
#include "Resource.h"
#include "GameEngine.h"
#include "Bitmap.h"
#include "Sprite.h"

//-----------------------------------------------------------------
// 全局变量
//-----------------------------------------------------------------
HINSTANCE   g_hInstance;       //程序实例句柄
GameEngine* g_pGame;           //游戏引擎指针
HDC         g_hOffscreenDC;    //屏幕外设备环境(双重缓存)
HBITMAP     g_hOffscreenBitmap;//屏幕外缓冲区的位图(双重缓存)
Bitmap*     g_pGalaxyBitmap;   //背景位图
Bitmap*     g_pPlanetBitmap[3];//行星位图
Sprite*     g_pDragSprite;     //被拖曳的子画面

Planets.cpp 源代码

//-----------------------------------------------------------------
// 包含的文件
//-----------------------------------------------------------------
#include "Planets.h"

//-----------------------------------------------------------------
// 游戏事件函数
//-----------------------------------------------------------------

// 初始化游戏
BOOL GameInitialize(HINSTANCE hInstance)
{
  // Create the game engine
  g_pGame = new GameEngine(hInstance, TEXT("Planets 2"),
    TEXT("Planets 2"), IDI_PLANETS, IDI_PLANETS_SM, 600, 400);
  if (g_pGame == NULL)
    return FALSE;

  // Set the frame rate
  g_pGame->SetFrameRate(30);

  // Store the instance handle
  g_hInstance = hInstance;

  return TRUE;
}

// 开始游戏
void GameStart(HWND hWindow)
{
  // 设置随机数生成种子
  srand(GetTickCount());

  // 创建屏幕外设备环境和位图
  g_hOffscreenDC = CreateCompatibleDC(GetDC(hWindow));
  g_hOffscreenBitmap = CreateCompatibleBitmap(GetDC(hWindow),
    g_pGame->GetWidth(), g_pGame->GetHeight());
  SelectObject(g_hOffscreenDC, g_hOffscreenBitmap);

  // 创建并加载位图
  HDC hDC = GetDC(hWindow);
  g_pGalaxyBitmap = new Bitmap(hDC, IDB_GALAXY, g_hInstance);
  g_pPlanetBitmap[0] = new Bitmap(hDC, IDB_PLANET1, g_hInstance);
  g_pPlanetBitmap[1] = new Bitmap(hDC, IDB_PLANET2, g_hInstance);
  g_pPlanetBitmap[2] = new Bitmap(hDC, IDB_PLANET3, g_hInstance);

  // 创建行星子画面
  RECT    rcBounds = { 0, 0, 600, 400 };
  Sprite* pSprite;
  pSprite = new Sprite(g_pPlanetBitmap[0], rcBounds, BA_WRAP);
  pSprite->SetVelocity(3, 2);
  g_pGame->AddSprite(pSprite);
  pSprite = new Sprite(g_pPlanetBitmap[1], rcBounds, BA_WRAP);
  pSprite->SetVelocity(4, 1);
  g_pGame->AddSprite(pSprite);
  rcBounds.right = 200; rcBounds.bottom = 160;
  pSprite = new Sprite(g_pPlanetBitmap[2], rcBounds, BA_BOUNCE);
  pSprite->SetVelocity(-4, 2);
  g_pGame->AddSprite(pSprite);
  rcBounds.left = 400; rcBounds.top = 240;
  rcBounds.right = 600; rcBounds.bottom = 400;
  pSprite = new Sprite(g_pPlanetBitmap[2], rcBounds, BA_BOUNCE);
  pSprite->SetVelocity(7, -3);
  g_pGame->AddSprite(pSprite);

  // 设置初始化拖动信息
  g_pDragSprite = NULL;
}

// 结束游戏
void GameEnd()
{
  // 清理屏幕外设备环境和位图
  DeleteObject(g_hOffscreenBitmap);
  DeleteDC(g_hOffscreenDC);  

  // 清理位图
  delete g_pGalaxyBitmap;
  for (int i = 0; i < 3; i++)
    delete g_pPlanetBitmap[i];

  // 清理子画面
  g_pGame->CleanupSprites();

  // 清理游戏引擎
  delete g_pGame;
}

void GameActivate(HWND hWindow)
{
}

void GameDeactivate(HWND hWindow)
{
}

// 绘制子画面
void GamePaint(HDC hDC)
{
  // 绘制星系背景
  g_pGalaxyBitmap->Draw(hDC, 0, 0);

  // 绘制子画面
  g_pGame->DrawSprites(hDC);
}

// 更新子画面列表中的子画面,在更新游戏屏幕之前,在屏幕外内存缓冲区绘制它们
void GameCycle()
{
  // 更新位图
  g_pGame->UpdateSprites();

  // 获得用于重新绘制游戏的设备环境
  HWND  hWindow = g_pGame->GetWindow();
  HDC   hDC = GetDC(hWindow);

  // 在屏幕外设备环境上绘制游戏
  GamePaint(g_hOffscreenDC);

  // 将屏幕外位图位块传送到游戏屏幕上
  BitBlt(hDC, 0, 0, g_pGame->GetWidth(), g_pGame->GetHeight(),
    g_hOffscreenDC, 0, 0, SRCCOPY);

  // 清理
  ReleaseDC(hWindow, hDC);
}

// 键盘处理函数
void HandleKeys()
{
}

// 按下鼠标
void MouseButtonDown(int x, int y, BOOL bLeft)
{
  //  查看是否使用鼠标左键单击了一颗行星
  if (bLeft && (g_pDragSprite == NULL))
  {
    if ((g_pDragSprite = g_pGame->IsPointInSprite(x, y)) != NULL)
    {
      // 捕获鼠标
      SetCapture(g_pGame->GetWindow());

      // 模拟鼠标移动
      MouseMove(x, y);
    }
  }
}

// 释放鼠标
void MouseButtonUp(int x, int y, BOOL bLeft)
{
  // 释放鼠标
  ReleaseCapture();

  // 停止拖动
  g_pDragSprite = NULL;
}

// 移动鼠标
void MouseMove(int x, int y)
{
  if (g_pDragSprite != NULL)
  {
    // 将子画面移动到鼠标指针的位置
    g_pDragSprite->SetPosition(x - (g_pDragSprite->GetWidth() / 2),
      y - (g_pDragSprite->GetHeight() / 2));

    // 强制重新绘制,以便重画子画面
    InvalidateRect(g_pGame->GetWindow(), NULL, FALSE);
  }
}

// 子画面碰撞函数
BOOL SpriteCollision(Sprite* pSpriteHitter, Sprite* pSpriteHittee)
{
  // 交换了碰撞的子画面的速度,使他们看起来彼此弹开
  POINT ptSwapVelocity = pSpriteHitter->GetVelocity();
  pSpriteHitter->SetVelocity(pSpriteHittee->GetVelocity());
  pSpriteHittee->SetVelocity(ptSwapVelocity);
  return TRUE;
}

源代码 下载

http://pan.baidu.com/s/1ge2Vzr1

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值