在前面使用的子画面中,读者可能已经发现,它们缺少一个功能:不能改变其外观。能够四处移动当然是一个很大的有点,但是一些子画面能够频繁的改变其外观也是很不错的。
本章介绍如何向子画面添加帧动画特性,这样就可以更改它们的外观。
本章内容包括:
- 如何将帧动画结合到子画面动画中
- 如何设计帧动画支持并将其集成到现有的游戏引擎中
- 如何修改现有的游戏,以便利用帧动画子画面
接上文 游戏编程入门(12):开发 Battle Office(办公室战争)游戏
再次介绍帧动画
在前面 游戏编程入门(7):使用子画面动画移动对象 时,读者学习了有关动画的知识,包括两种基本的动画类型,基于帧的动画和基于形状(子画面)的动画。
虽然读者了解到这两种动画类型是同样有用的,但是从第7篇开始,我们只介绍了子画面动画,从现在起,我们会把帧动画与子画面动画结合。
为了更好地理解帧动画如何与子画面动画相结合,考虑一个简单的迷宫游戏,例如吃豆人。在吃豆人中,主角在屏幕上四处移动,吃圆点。为了表示主角吃到圆点的效果,它的嘴在四处移动时不断张开和闭上。角色在迷宫中的简单移动时通过子画面动画实现的,但是通过嘴的移动来表现的外观变化是通过帧动画实现的。
既然帧动画使得更改子画面的外观称为可能,那么它的工作原理是什么?回顾一下,没有帧画面的基本子画面使用一个单独的位图图像来表示其外观。在绘制子画面时,就是绘制位图图像。而使用帧动画的子画面使用一系列位图来表现子画面的多个外观。
在这幅图中,吃豆人角色包含4幅图像,将这一系列图像结合到一个游戏中并顺序播放时,效果就是吃豆人角色动嘴吃什么东西。仔细看一下,可以注意到第4帧和第2帧相同,这是因为当动画序列完成时,将返回到第一帧,因此重复第2帧提供了到第一帧的平滑转换。
与帧动画有关的一个问题是控制循环播放帧图像的速度。例如,使用了帧动画的吃豆人子画面更改外观的速度可能需要比爆炸动画子画面的速度更慢。出于这个原因,需要有一种方法来确定时间延迟,控制改变帧的速度。增加延迟将放慢帧动画,这在某些情况下正好是所需的效果。本章下面的学习中,在为游戏引擎设计和开发帧动画支持时必须要考虑帧延迟。
设计动画子画面
这里,我们将帧动画子画面简称为动画子画面。开始设计动画子画面的第一个地方就是它的位图图像。
在上一节中,读者学习了如何使用一系列图像来表示动画子画面,通过循环播放图像来获得动画效果。有各种不同的方法来存储动画子画面的一系列图像,但我发现最容易的是在一个单独的图像中垂直存储帧图像。例如,在图15.1中看到的一系列的吃豆人图像显示了几幅帧图像,它们是竖直排列的。可以像图中显示的那样将这4个帧图像一起存储为一个单独的图像。然后,动画子画面代码负责只绘制代表当前帧的图像。可以将任何特定时刻的子画面图像看作一个小窗口,它从一个帧图像移动到下一个帧图像,一次只显示一个帧图像。图15.2显示了我所说的这种情况。
这幅图显示出当前正在显示吃豆人子画面的第2帧,这幅图还解释了如何将动画子画面的位图图像放在一起。图像的宽度与普通的未使用动画的子画面图像一样,但是高度是由帧的数量决定的。因此,如果一个单独的帧为25像素高,动画一共有4帧,那么最终的图像就是100像素高。要想创建这样一个图像,只需要将帧图像紧挨着放置(中间不留任何空隙)即可。
有了动画子画面图像之后,现在就可以将注意力转向用来管理帧动画的实际子画面数据了。首先,我们知道子画面需要了解有多少个帧图像,这是决定如何循环播放帧位图图像的各个帧的关键信息。除了知道动画帧的总数之外,还需要记录当前选定的帧,也就是当前显示的帧图像。
之前我们提到过,动画子画面能够控制循环播放其帧图像的速度是非常重要的。这个速度是由一个名为帧延迟的信息控制的。实际上,子画面的帧延迟决定了在更改帧图像之前必须经历多少个游戏周期。
为了实现动画子画面的帧延迟特性,必须使用一个触发器(计数器),它将这个延迟向下计数,指出在什么时候移动到下一帧。因此,最初开始动画子画面时,将触发器设置为帧延迟,然后随着每一个游戏周期开始向下计数,当触发器到达0时,子画面移动到下一阵并再次将触发器重置为帧延迟。
总结一下,除了包含垂直放置的帧图像的位图图像之外,新的动画子画面还需要以下这些信息:
- 总帧数
- 当前帧
- 帧延迟
- 帧触发器
向游戏引擎添加动画子画面支持
为了向游戏引擎添加动画子画面支持,必须对现有代码进行一些更改。这些更改会影响到Bitmap 位图类和Sprite 子画面类,但并不会直接影响到GameEngine 类。
只绘制位图的一部分
第一处更改与表示一个位图图像的Bitmap 位图类有关。读者已经知道了,Sprite 子画面类依靠Bitmap 位图类来处理和绘制一个子画面的可视外观的各种细节。
不过,Bitmap 位图类目前只是设计来绘制一个完整的图像。对于动画子画面来说,因为帧图像全部存储在一个单独的位图图像中,所以这就存在一个问题。因此需要修改Bitmap 位图类,使之支持只绘制位图的一部分。
回顾一下,Bitmap 位图类包括了一个Draw( )方法,它接受一个设备环境和一个xy坐标以指出将要在什么位置绘制它。现在需要的新方法名为DrawPart( ),它接受我们熟悉的设备环境和xy坐标,还有帧图像在整个子画面位图中的xy位置以及宽度和高度。因此,新的DrawPart( )方法实际上允许选择并绘制位图图像的一个矩形子部分。
// 允许只绘制子画面位图图像的一部分,从而支持了帧动画
void Bitmap::DrawPart(HDC hDC, int x, int y, int xPart, int yPart,
int wPart, int hPart, BOOL bTrans, COLORREF crTransColor)
{
// 确保位图句柄有效
if (m_hBitmap != NULL)
{
// 创建一个兼容的设备环境来临时存储位图
HDC hMemDC = CreateCompatibleDC(hDC);
// 将位图选入设备环境中
HBITMAP hOldBitmap = (HBITMAP)SelectObject(hMemDC, m_hBitmap);
// 将位图画到目标设备环境中
if (bTrans)
TransparentBlt(hDC, x, y, wPart, hPart, hMemDC, xPart, yPart,
wPart, hPart, crTransColor);
else
BitBlt(hDC, x, y, wPart, hPart, hMemDC, xPart, yPart, SRCCOPY);
// 清理临时设备环境
SelectObject(hMemDC, hOldBitmap);
DeleteDC(hMemDC);
}
}
这段代码,挑选出位图图像中要绘制的一个框架图像。将这些参数传递给TransparentBlt( )函数,以便使用透明色绘制一个框架图像,或者使用BitBlt( )函数绘制一个没有透明色的框架图像。
新的Draw( )方法仍然可以用于绘制没有帧动画的子画面。
// 绘制位图
void Bitmap::Draw(HDC hDC, int x, int y, BOOL bTrans, COLORREF crTransColor)
{
DrawPart(hDC, x, y, 0, 0, GetWidth(), GetHeight(), bTrans, crTransColor);
}
对Sprite 子画面类 实现动画
之前,我们完成了帧动画子画面的设计,它需要向传统的无动画子画面添加一些重要信息。Sprite 子画面类的下面这些新成员变量就存储了这些信息。
int m_iNumFrames, m_iCurFrame; //总帧数,当前帧
int m_iFrameDelay, m_iFrameTrigger; //帧延迟,帧触发器
它们都是在Sprite( )构造函数中初始化的。
m_iNumFrames = 1;
m_iCurFrame = m_iFrameDelay = m_iFrameTrigger = 0;
m_iNumFrames 总帧数的默认值为1,因为没有使用帧动画的普通子画面只包括一幅位图图像,或者说一个帧。将其他3个变量初始化为0,折适合没有帧动画的子画面。
为了创建使用帧动画的子画面, 必须调用SetNumFrames( )方法,设置总帧数。通过设置子画面的帧数来将普通的子画面转化为动画子画面。
// 设置总帧数。通过设置子画面的帧数来将普通的子画面转化为动画子画面。
inline void Sprite::SetNumFrames(int iNumFrames)
{
// Set the number of frames
m_iNumFrames = iNumFrames;
// Recalculate the position
RECT rect = GetPosition();
rect.bottom = rect.top + ((rect.bottom - rect.top) / iNumFrames);
SetPosition(rect);
}
因为图像现在包括了多个帧,不再使用完整的图像大小,所以必须重新计算子画面的位置。
除了设置总帧数,增加了一个SetNumFrames( )方法以外。还需要设置帧延迟m_iFrameDelay。
void SetFrameDelay(int iFrameDelay)
{
m_iFrameDelay = iFrameDelay;
};
在计算子画面图像的高度时,GetHeight( )方法需要考虑到帧数。
对于无动画的子画面来说,这个高度时整个子画面图像的高度。而对于动画子画面来说,这个高度时单个帧图像的高度。
下面是新的GetHeight( )方法。
int GetHeight()
{
return (m_pBitmap->GetHeight() / m_iNumFrames);
};
还需要一个新的帮助器方法来更新当前动画帧,这个方法名为UpdateFrame( )。
//更新子画面的当前动画帧
inline void Sprite::UpdateFrame()
{
if ((m_iFrameDelay >= 0) && (--m_iFrameTrigger <= 0))
{
// 重置帧触发器
m_iFrameTrigger = m_iFrameDelay;
// 将帧加一
if (++m_iCurFrame >= m_iNumFrames)
m_iCurFrame = 0;
}
}
Update( )方法现在也要调用UpdateFrame( )方法来更新子动画的动画帧。
//根据速度更改子画面的位置,并根据其移动做出相应,从而更新子画面
SPRITEACTION Sprite::Update()
{
// 更新帧
UpdateFrame();
.....
}
添加动画支持影响到的最后一个Sprite方法是Draw( )方法。它现在必须在绘制子画面时考虑当前帧。
// 根据是否是动画帧来以不同的方式绘制子画面
void Sprite::Draw(HDC hDC)
{
// 如果没有隐藏,则绘制子画面
if (m_pBitmap != NULL && !m_bHidden)
{
// 绘制适当的帧
if (m_iNumFrames == 1)
m_pBitmap->Draw(hDC, m_rcPosition.left, m_rcPosition.top, TRUE);
else
m_pBitmap->DrawPart(hDC, m_rcPosition.left, m_rcPosition.top,
0, m_iCurFrame * GetHeight(), GetWidth(), GetHeight(), TRUE);
}
}
开发Battle Office 2(办公室战争2)游戏
之前Battle Office (办公室战争)游戏有一个缺点,在游戏屏幕顶部的走廊上移动的同事看起来像是在滑动而不是在跑动。现在我们可以通过帧动画来实现,同事腿部跑动的效果了。
注意:若出现编译错误,请在项目设置->连接->对象/库模块中 加入 msimg32.lib winmm.lib
Battle Office 2(办公室战争2)目录结构和效果图
Battle Office 2(办公室战争2)目录结构:
Battle Office 2(办公室战争2)效果图:
虽然截图上看不出来区别,但是实际运行时,同事已经有了跑动的效果。
编写程序代码
对于Battle Office 2 游戏而言,针对帧动画更改的两个子画面是两个移动的同事子画面。这两个子画面都使用4个帧图像,意味着它们都需要设置为使用4个帧。
第一个移动同事子画面的 子画面创建代码
g_pGuySprites[3] = new Sprite(g_pGuyBitmaps[3], rcBounds, BA_WRAP);
g_pGuySprites[3]->SetNumFrames(4);
g_pGuySprites[3]->SetPosition(500, 10);
g_pGuySprites[3]->SetVelocity(-3, 0);
g_pGuySprites[3]->SetZOrder(1);
g_pGuySprites[3]->SetHidden(TRUE);
g_pGame->AddSprite(g_pGuySprites[3]);
注意的是,对这个子画面没有设置帧延迟,意味着它将以最快的速度循环显示各个帧。
第2个移动同事子画面有一点不同,它选择了一个帧延迟。
rcBounds.left = 385;
g_pGuySprites[4] = new Sprite(g_pGuyBitmaps[4], rcBounds, BA_WRAP);
g_pGuySprites[4]->SetNumFrames(4);
g_pGuySprites[4]->SetFrameDelay(5);
g_pGuySprites[4]->SetPosition(260, 60);
g_pGuySprites[4]->SetVelocity(5, 0);
g_pGuySprites[4]->SetZOrder(1);
g_pGuySprites[4]->SetHidden(TRUE);
g_pGame->AddSprite(g_pGuySprites[4]);
第2个移动同事子画面的动作速度是第1个移动同事子画面的1/5。