到此为止,这个系列的第二部分就结束了.在本文中,我们学习了如何加载图片文件并将其绘制到屏幕上,以及如何绘制动画.下一篇文章,同时也是这个系列的最后一篇文章中,我们将看到如何在屏幕上输出文本,如何管理游戏的不同状态,并最终实现一个具体的实例:一个类似俄罗斯方块的游戏.
原文链接:TetroGL: An OpenGL Game Tutorial in C++ for Win32 Platforms - Part 2
CImage类
现在我们来看看CImage类究竟是如何使用纹理图片的.早前已经看到,用户无法直接对CTexture对象进行操作.这是因为它仅仅是对一个资源文件进行包装,而这样的文件可以由多个图片组成:假设你想在游戏中显示各种类型的树,那么将它们存储在同一个文件是很方便的.因此,纹理类本身并没有任何用来在屏幕上绘制图片的函数,而仅仅只有加载文件的函数.CImage类才是负责在屏幕上绘制纹理(或者仅其一部分).多个图片可以引用同一个纹理,但使用其不同部分.
CImage类
#include "Texture.h"
#include "Rectangle.h"
#include "SmartPtr.h"
class CImage;
// Typedef of a CImage class that is wrapped inside a smart
// pointer.
typedef CSmartPtr<CImage> TImagePtr;
// An image is manipulated directly by the end user (instead of
// the texture). The main difference between an image and a texture
// is that the texture can contain multiple images (it is the
// complete file).
class CImage
{
public:
// Blit the image at the specified location
void BlitImage(int iXOffset=0, int iYOffset=0) const;
// Returns the texture that this image is using.
CTexture* GetTexture() const { return m_pTexture; }
// Helper functions to create an new image. A smart pointer
// holding the new image is returned. strFileName is the
// name of the file containing the texture and textCoord is
// the rectangle in this texture which contains the image.
static TImagePtr CreateImage(const std::string& strFileName);
static TImagePtr CreateImage(const std::string& strFileName,
const TRectanglei& textCoord);
~CImage();
protected:
// Protected constructors to avoid to be able to create a
// CImage instance directly.
CImage(const std::string& strFileName);
CImage(const std::string& strFileName, const TRectanglei& textCoord);
private:
// The texture from which this image is part of.
CTexture* m_pTexture;
// The rectangle that specifies the position of the image
// in the full texture.
TRectanglei m_rectTextCoord; //纹理坐标
};
此类有两个成员变量:图片所在的纹理,一个表明指明了图片占了纹理的那些部分的矩形.下面再来看BlitImage函数如何实现的,它将在参数指定的位置处绘制纹理:
绘制纹理
void CImage::BlitImage(int iXOffset, int iYOffset) const
{
if (m_pTexture)
{
m_pTexture->Bind();
// Get the coordinates of the image in the texture, expressed
// as a value from 0 to 1.
float Top = ((float)m_rectTextCoord.m_Top)/m_pTexture->GetHeight();
float Bottom = ((float)m_rectTextCoord.m_Bottom)/m_pTexture->GetHeight();
float Left = ((float)m_rectTextCoord.m_Left)/m_pTexture->GetWidth();
float Right = ((float)m_rectTextCoord.m_Right)/m_pTexture->GetWidth();
// Draw the textured rectangle.
glBegin(GL_QUADS);
glTexCoord2f(Left,Top); glVertex3i(iXOffset,iYOffset,0);
glTexCoord2f(Left,Bottom); glVertex3i(iXOffset,iYOffset+m_rectTextCoord.GetHeight(),0);
glTexCoord2f(Right,Bottom); glVertex3i(iXOffset+m_rectTextCoord.GetWidth(),iYOffset+m_rectTextCoord.GetHeight(),0);
glTexCoord2f(Right,Top); glVertex3i(iXOffset+m_rectTextCoord.GetWidth(),iYOffset,0);
glEnd();
}
}
我们首先对纹理进行绑定(让其在OpenGL中是活动的),然后图片在纹理中的坐标.这些值在0到1之间,0表示纹理的左上方,1表示纹理的右下方.然后如第一篇文章所示绘制一个矩形,所不同的在于指明各个点之前,我们调用了glTexCoord2f函数,它指明了当前绑定的OpenGL纹理的纹理点坐标.通过这样做,OpenGL就能够使用活动纹理来显示出贴纹理后的矩形.
现在来看看构造函数和析构函数.这里有两个受保护的构造函数.
构造函数
CImage::CImage(const string& strFileName) : m_pTexture(NULL), m_rectTextCoord()
{
// This line will throw an exception if the texture is not found.
m_pTexture = CTextureManager::GetInstance()->GetTexture(strFileName);
m_pTexture->AddReference();
// Set the texture coordinate to the full texture
m_rectTextCoord.m_Top = m_rectTextCoord.m_Left = 0;
m_rectTextCoord.m_Bottom = m_pTexture->GetHeight();
m_rectTextCoord.m_Right = m_pTexture->GetWidth();
}
CImage::CImage(const string& strFileName, const TRectanglei& textCoord)
: m_pTexture(NULL), m_rectTextCoord(textCoord)
{
// This line will throw an exception if the texture is not found.
m_pTexture = CTextureManager::GetInstance()->GetTexture(strFileName);
m_pTexture->AddReference();
}
析构函数仅仅简单地释放纹理,而这将会减少纹理的引用计数(如前所示):
CImage::~CImage()
{
if (m_pTexture)
m_pTexture->ReleaseReference();
}
这里作者将此类的构造函数设置为受保护,这是为了强迫用户使用包装了CImage类的智能指针.出于内存泄露的缘故,这点是值得考虑的.关于智能指针的内容就不多介绍了…在本文中作者使用了自己开发的一个智能指针,但使用boost::shared_ptr则更好.
最后,CImage类提供了两个静态方法,从而允许我们创建此类的实例.方法实现很简单:创建一个新实例,传递给一个智能指针,并返回此指针.
创建CImage实例
TImagePtr CImage::CreateImage(const string& strFileName)
{
TImagePtr imgPtr(new CImage(strFileName));
return imgPtr;
}
TImagePtr CImage::CreateImage(const string& strFileName, const TRectanglei& textCoord)
{
TImagePtr imgPtr(new CImage(strFileName,textCoord));
return imgPtr;
}
显示动画
2D游戏动画背后的思想很简单:就像卡通一样,将动作分解为离散的图片.最直观的方法就是使用一个循环,在循环中每显示一幅图片后就休眠一段时间.也许你已经猜到了,这根本不可行.有几个问题:首先,因为你从来不交换缓冲区(这在CMainWindow::Draw函数中做的),根本不会显示任何东西.第二,如果你那样做了,你程序的其他部分就无法执行,也就意味着你只能每次显示一个动画.正确的方法是:让每幅”动画”记住自己的状态(例如,当前显示的是那副图片),并且请求它们去绘制它们的当前图片.当一个新的帧需要被绘制时,每幅”动画”就会被要求跳转到动画的下一副图片去.
下面我们来看看CImageList类.此类是std::list的一个简单包装类,它用来保存图片,并且提供了一些帮助函数来播放图片.
CImageList类
#include "Image.h"
#include <list>
// Wraps a list of images which is used to play animations.
class CImageList
{
public:
// Default constructor: construct an empty list.
CImageList();
// Copy constructor: copies the content of the
// list passed in argument.
CImageList(const CImageList& other);
// Default destructor.
~CImageList();
// Assignement operator: empty the current content
// and copies the content of the list passed in argument.
CImageList& operator= (const CImageList& other);
// Empty the content of the list
void Clear();
// Append a new image to the list
void AppendImage(TImagePtr pImage);
// Return the number of images in this list
unsigned GetImagesCount() const;
// Make the first image active
void GoToFirstImage();
// Make the next image active. If the last image
// was active, we go back to the first image. In
// that case, the function returns true.
bool GoToNextImage();
// Get the current image
TImagePtr GetCurrentImage() const;
private:
// Typedef for a std::list containing TImagePtr objects
typedef std::list<TImagePtr> TImageList;
// The list of images
TImageList m_lstImages;
// Iterator pointing to the current image
TImageList::iterator m_iterCurrentImg;
};
它的实现很简单:在需要的时候增加图片到std::list<TImagePtr>中,并且维护一个迭代器,它指明了当前活动的图片.以GoToNextImage为例:
bool CImageList::GoToNextImage()
{
if (m_iterCurrentImg != m_lstImages.end() )
m_iterCurrentImg++;
else
return false;
if (m_iterCurrentImg == m_lstImages.end() )
{
m_iterCurrentImg = m_lstImages.begin();
return true;
}
return false;
}
下面我们来看看CAnimatedSprite类,此类允许你将多个动画组合到一起.举个例子说明下:假设你写一个游戏,在游戏里玩家扮演一位骑士.骑士当然就有多种动作啦:行走,攻击,静止站立…等等.一般来说,你需要为这些动作在每个方向都提供动画.这个类就可以用来表示你的骑士:你可以加载多个动画,并按照命令不断播放它们.
CAnimatedSprite类
#include "Image.h"
#include "ImageList.h"
#include <string>
#include <map>
#include <list>
// This class represent an animated sprite: it is able to play
// different animations that were previously loaded.
class CAnimatedSprite
{
public:
// Default constructor and destructor.
CAnimatedSprite();
~CAnimatedSprite();
// Adds a new animation for the sprite. The strAnimName
// is a string that identifies the animation and should
// be unique for this sprite.
void AddAnimation(const std::string& strAnimName,
const CImageList& lstAnimation);
// Plays a previously loaded animation. The strAnimName
// is the name that was passed when calling AddAnimation.
void PlayAnimation(const std::string& strAnimName);
// Draw the current frame of the animation at the sprite
// current position.
void DrawSprite();
// Go to the next animation frame.
void NextFrame();
// Set the position of the sprite
void SetPosition(int XPos, int YPos)
{
m_iXPos = XPos;
m_iYPos = YPos;
}
// Move the sprite from its current position
void OffsetPosition(int XOffset, int YOffset)
{
m_iXPos += XOffset;
m_iYPos += YOffset;
}
private:
typedef std::map<std::string, CImageList> TAnimMap;
typedef TAnimMap::iterator TAnimMapIter;
// Map containing all the animations that can be
// played.
TAnimMap m_mapAnimations;
// Iterator to the current animation being played
TAnimMapIter m_iterCurrentAnim;
// Position of the sprite
int m_iXPos;
int m_iYPos;
};
此类的实现如下:它包含了一个map,其中存储了可以用于sprite的所有动画,键为一个动画名称,值为一个CImageList对象,这个对象包含了动画.AddAnimation和PlayAnimation函数用于在map中增加或查询动画:
Code
void CAnimatedSprite::AddAnimation(const string &strAnimName, const CImageList& lstAnimation)
{
m_mapAnimations[strAnimName] = lstAnimation;
}
void CAnimatedSprite::PlayAnimation(const string &strAnimName)
{
m_iterCurrentAnim = m_mapAnimations.find(strAnimName);
if (m_iterCurrentAnim == m_mapAnimations.end())
{
string strError = "Unable to play: " + strAnimName;
strError += ". Animation not found.";
throw CException(strError);
}
}
当想播放一个不存在的动画时,则会抛出一个异常. 成员变量m_iterCurrentAnim是一个指向当前动画的迭代器.它在DrawSprite和NextFrame函数中用来访问当前动画:
void CAnimatedSprite::DrawSprite()
{
if (m_iterCurrentAnim == m_mapAnimations.end())
return;
m_iterCurrentAnim->second.GetCurrentImage()
->BlitImage(m_iXPos,m_iYPos);
}
void CAnimatedSprite::NextFrame()
{
if (m_iterCurrentAnim == m_mapAnimations.end())
return;
m_iterCurrentAnim->second.GoToNextImage();
}
示例代码
现在是用一个具体的示例来展示我们将如何使用上面这些类的时候了.示例很简单,将有一个动画角色(一个骑士),它能通过方向键控制行走,它在一副简单的地图上行进.现在还有碰撞检测,这就意味着它能穿过树木,另一个没有实现的是sprites被绘制的顺序:骑士总是绘制在场景的上面,无论他在哪里,这当然在有些情况下是不对的(比如说他隐藏在树后面).
所有代码都实现在CMainWindow类中,我们首先在类中加入一些成员变量:
// The image for the grass.
TImagePtr m_pGrassImg;
// Images for the trees
TImagePtr m_pTreesImg[16];
// The animated sprite of the knight
CAnimatedSprite* m_pKnightSprite;
// Which keys are currently pressed
bool m_KeysDown[4];
// The last direction of the knight
std::string m_strLastDir;
我们首先定义了一些TImagePtr,它们用来表示将要绘制的图片(草地和树木).然后定义了CAnimatedSprite,它用来绘制骑士.最后定义了一个bool数组,用来存储方向键的当前状态,此外还有一个字符串用来表明骑士的当前方向.这些成员变量在主窗口类的构造函数中进行初始化:
初始化
// Load the grass image and set the color key.
m_pGrassImg = CImage::CreateImage("GrassIso.bmp");
m_pGrassImg->GetTexture()->SetColorKey(0,128,128);
// Load the tree images
for (int i=0; i<16; i++)
{
TRectanglei imgRect(i/4*128,(i/4+1)*128,(i%4)*128,(i%4+1)*128);
m_pTreesImg[i] = CImage::CreateImage("Trees.bmp",imgRect);
}
CTextureManager::GetInstance()->GetTexture("Trees.bmp")
->SetColorKey(191, 123, 199);
// Load all the 'walk' animations for the knight.
m_pKnightSprite = new CAnimatedSprite;
CAnimFileLoader fileLoader1("KnightWalk.bmp", 8, 96, 96);
CTextureManager::GetInstance()->GetTexture("KnightWalk.bmp")
->SetColorKey(111, 79, 51);
m_pKnightSprite->AddAnimation("WalkE",
fileLoader1.GetAnimation(0,7));
m_pKnightSprite->AddAnimation("WalkSE",
fileLoader1.GetAnimation(8,15));
m_pKnightSprite->AddAnimation("WalkS",
fileLoader1.GetAnimation(16,23));
m_pKnightSprite->AddAnimation("WalkSW",
fileLoader1.GetAnimation(24,31));
m_pKnightSprite->AddAnimation("WalkW",
fileLoader1.GetAnimation(32,39));
m_pKnightSprite->AddAnimation("WalkNW",
fileLoader1.GetAnimation(40,47));
m_pKnightSprite->AddAnimation("WalkN",
fileLoader1.GetAnimation(48,55));
m_pKnightSprite->AddAnimation("WalkNE",
fileLoader1.GetAnimation(56,63));
// Load all the 'pause' animations for the knight.
CAnimFileLoader fileLoader2("KnightPause.bmp", 8, 96, 96);
CTextureManager::GetInstance()->GetTexture("KnightPause.bmp")
->SetColorKey(111, 79, 51);
m_pKnightSprite->AddAnimation("PauseE",
fileLoader2.GetAnimation(0,7));
m_pKnightSprite->AddAnimation("PauseSE",
fileLoader2.GetAnimation(8,15));
m_pKnightSprite->AddAnimation("PauseS",
fileLoader2.GetAnimation(16,23));
m_pKnightSprite->AddAnimation("PauseSW",
fileLoader2.GetAnimation(24,31));
m_pKnightSprite->AddAnimation("PauseW",
fileLoader2.GetAnimation(32,39));
m_pKnightSprite->AddAnimation("PauseNW",
fileLoader2.GetAnimation(40,47));
m_pKnightSprite->AddAnimation("PauseN",
fileLoader2.GetAnimation(48,55));
m_pKnightSprite->AddAnimation("PauseNE",
fileLoader2.GetAnimation(56,63));
m_pKnightSprite->PlayAnimation("PauseE");
for (int i=0; i<4; i++)
m_KeysDown[i] = false;
// Set the initial direction to the east.
m_strLastDir = "E";
m_pKnightSprite->SetPosition(350,250);
这里我们使用了一个新的类:CAnimFileLoader,它是一个用来从文件中加载一个图片列表的帮助类.它的构造函数以文件名称,每行图片数目,图片宽度和高度作为参数,过后你可以通过指定在文件中图片的起始索引位置和结束索引位置来查询图片列表(它会返回一个CImageList对象).再回过头来看上面的代码,首先我们加载草地图片,并对其进行抠色,然后为骑士加载所有的”行走”动画.每个动画的名字取决于其方向, 比如,向东行走,动画名称就是WalkE.最后指明默认的动画是”PauseE”动画.
现在来看看我们是如何处理键盘事件的:
事件处理
void CMainWindow::ProcessEvent(UINT Message, WPARAM wParam, LPARAM lParam)
{
switch (Message)
{
// Quit when we close the main window
case WM_CLOSE :
PostQuitMessage(0);
break;
case WM_SIZE:
OnSize(LOWORD(lParam),HIWORD(lParam));
break;
case WM_KEYDOWN :
switch (wParam)
{
case VK_UP:
m_KeysDown[0] = true;
break;
case VK_DOWN:
m_KeysDown[1] = true;
break;
case VK_LEFT:
m_KeysDown[2] = true;
break;
case VK_RIGHT:
m_KeysDown[3] = true;
break;
}
UpdateAnimation();
break;
case WM_KEYUP :
switch (wParam)
{
case VK_UP:
m_KeysDown[0] = false;
break;
case VK_DOWN:
m_KeysDown[1] = false;
break;
case VK_LEFT:
m_KeysDown[2] = false;
break;
case VK_RIGHT:
m_KeysDown[3] = false;
break;
}
UpdateAnimation();
break;
}
}
当方向键按下时,我们只是简单地在bool数组中设置或重设标志位,以此来表明对应键的状态,然后调用UpdateAnimation函数:
UpdateAnimation
void CMainWindow::UpdateAnimation()
{
// First check if at least one key is pressed
bool keyPressed = false;
for (int i=0; i<4; i++)
{
if (m_KeysDown[i])
{
keyPressed = true;
break;
}
}
string strAnim;
if (!keyPressed)
strAnim = "Pause" + m_strLastDir;
if (keyPressed)
{
string vertDir;
string horizDir;
if (m_KeysDown[0])
vertDir = "N";
else if (m_KeysDown[1])
vertDir = "S";
if (m_KeysDown[2])
horizDir = "W";
else if (m_KeysDown[3])
horizDir = "E";
m_strLastDir = vertDir + horizDir;
strAnim = "Walk" + m_strLastDir;
}
m_pKnightSprite->PlayAnimation(strAnim);
}
我们首先检查是否至少有一个键被按下.若没有,则指定播放的动画应该是”Pause”+最后一次骑士方向的名字.若至少有一个键被按下,我们检查哪些被按下了,并构建出上一个方向字符串.我们再看看Draw函数:
绘制代码
void CMainWindow::Draw()
{
// Clear the buffer
glClear(GL_COLOR_BUFFER_BIT);
// Draw the grass
int xPos=0, yPos=0;
for (int i=0; i<8; i++)
{
for (int j=0; j<6; j++)
{
xPos = i * 256/2 - 128;
if (i%2)
yPos = (j * 128) - 128/2;
else
yPos = (j * 128);
m_pGrassImg->BlitImage(xPos, yPos);
}
}
// Draw some trees
m_pTreesImg[0]->BlitImage(15,25);
m_pTreesImg[1]->BlitImage(695,55);
m_pTreesImg[2]->BlitImage(15,25);
m_pTreesImg[3]->BlitImage(300,400);
m_pTreesImg[4]->BlitImage(125,75);
m_pTreesImg[5]->BlitImage(350,250);
m_pTreesImg[6]->BlitImage(400,350);
m_pTreesImg[7]->BlitImage(350,105);
m_pTreesImg[8]->BlitImage(530,76);
m_pTreesImg[9]->BlitImage(125,450);
m_pTreesImg[10]->BlitImage(425,390);
m_pTreesImg[11]->BlitImage(25,125);
m_pTreesImg[12]->BlitImage(550,365);
m_pTreesImg[13]->BlitImage(680,250);
m_pTreesImg[14]->BlitImage(245,325);
m_pTreesImg[15]->BlitImage(300,245);
// Draw the knight
m_pKnightSprite->DrawSprite();
// Move to the next frame of the animation
m_pKnightSprite->NextFrame();
// Swap the buffers
SwapBuffers(m_hDeviceContext);
}
移动骑士精灵是在Update函数中完成的:
更新骑士坐标
void CMainWindow::Update(DWORD dwCurrentTime)
{
int xOffset = 0;
int yOffset = 0;
if (m_KeysDown[0])
yOffset -= 5;
if (m_KeysDown[1])
yOffset += 5;
if (m_KeysDown[2])
xOffset -= 5;
if (m_KeysDown[3])
xOffset += 5;
m_pKnightSprite->OffsetPosition(xOffset, yOffset);
}
若方向键按下,我们就在其方向上移动一定的偏移量.其实由于时间也传递进来了,我们也可以根据流逝的时间值来计算偏移量.
小结
到此为止,这个系列的第二部分就结束了.在本文中,我们学习了如何加载图片文件并将其绘制到屏幕上,以及如何绘制动画.下一篇文章,同时也是这个系列的最后一篇文章中,我们将看到如何在屏幕上输出文本,如何管理游戏的不同状态,并最终实现一个具体的实例:一个类似俄罗斯方块的游戏.敬请期待….
References
[1] Singleton article: a good introduction to the singleton pattern.
[2] Shared pointers: an extensive article about shared pointers.
[3] Boost shared_ptr: the boost library about shared_ptr.
[4] Reiner's tileset: free resources from which the images of the example were taken from.
[5] DevIL: DevIL library.
[6] FreeImage: FreeImage library.