Part2:The CImage Class & Displaying Animations

目录:

1、The CImage Class

2、Displaying Animations

1、The CImage Class

Let's now look at how this texture is used by the CImage class. As we saw earlier, the CTexture is not manipulated directly by the user. The reason is that it is mainly a wrapper around a resource file and such file can be made of several images: suppose that you want to display several kind of trees in your game, it could be convenient to have them all stored in the same file. So, the texture class in itself doesn't have any functionality to draw the image on the screen, but only to load a file. The image class is the one responsible to draw the texture (or only a part of it) on the screen. Several images can then reference the same texture but use a different portion of it.

// 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;
  // Blit a part of the image at the specified location.
  void BlitImagePart(int iXOffset, int iYOffset, const TRectanglei& part);
 // 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;
};
Before going into the details about how to instantiate this class, we will look at how it works. It has two members: the texture from which the image comes from and a rectangle specifying the portion of the texture which contains the image. I won't put the code of the CRectangle class because it is very trivial: It contains four members which are the top, bottom, left and right coordinates of the rectangle plus some support functions (like checking if it intersects with another rectangle, retrieve the width and height of the rectangle, ...). It is a template class, so you can choose the type of the rectangle coordinates ( integer,float,double, ...). TRectangleiis a typedef for a rectangle with integer coordinates. Let's see how the BlitImagefunction works, by drawing the texture at the location specified by the arguments:

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();
  }
}

We first bind the texture (make it the active one in OpenGL), then we calculate the coordinates of the image within the texture. Those values are expressed between 0 and 1, with 0 being the top/left side of the texture and 1 being the bottom/right side of the texture. We then draw a rectangle as seen in the first tutorial, except that before specifying each point, we callglTexCoord2fwhich specifies a texel (point in a texture) in the current binded OpenGL texture. By doing this, OpenGL will be able to associate texels from the texture to pixels on the screen, and display our textured rectangle using the active texture.

Let's now look at the constructors and destructor. There are two constructors (which are protected): one which accepts only a texture name and one which accepts both a texture name and a rectangle. The one with only the texture name will use the full texture as the image, and the other one will use the image contained at the specified rectangle in the file.

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();
}

The constructors retrieve the texture through the texture manager. Remember that this call can throw an exception if the texture doesn't exist. Then the reference count of the texture is increased. In case no rectangle was specified, the full texture is used as an image. The destructor simply releases the texture which decrements the reference count as seen earlier in the texture class.

As I already said, the constructors of the class are protected. The reason for that is to force the user to use a smart pointer that wraps theCImageclass. Ok, before panicking because of this strange thing, let me first say that wrapping theCImageclass into a smart pointer is not a necessity but it is very useful to make sure that all of the resources are released when not used anymore. If you don't allocate dynamicallyCImageobjects (using new), this is already done for you (through the destructor). But as soon as you are creating dynamic objects, you can always forget to delete them, which lead to unreleased resources. Furthermore, if you start exchanging those objects between different parts of your code, which part should be responsible to delete the object? All those problems are solved by wrapping the object into a smart pointer class. I won't fully discuss how it is implemented because there are already a lot of articles covering this subject (you can have a look at the references, there is a link to a good article). In brief, a smart pointer takes care of the lifetime of the object which it is maintaining: when the object is not needed anymore, it is destroyed. You can 'share' this pointer and once it is not needed anymore, the pointed object will be deleted. You can also easily access the wrapped object as if you were manipulating it directly: the smart pointer overloads the->and. operators to redirect them to the owned object. All of that sounds a bit complicated, but the usage is really easy: Instead of using the pointer to the object directly, you give it to a smart pointer which will take care of its lifetime for you (you don't have to delete the pointer anymore). The access to the object is almost transparent because you can still access the members as if you were using the pointer directly.

For this tutorial, I provided my own smart pointer class but it is preferable in general to use theboost::shared_ptr class (see references). The reason why I provided mine is simply to avoid having yet another dependency so that it is easier for you to compile the project (you don't have to download the package from boost). You can have a look at how it is implemented but I won't give a full explanation here.

Finally, the CImage class provides twostatichelper functions to be able to create instances of the class. They simply create a new instance, pass it to a smart pointer and return the smart pointer:

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;
}

TImagePtr类其他函数的实现:

void CImage::BlitImagePart(int iXOffset, 
						   int iYOffset, 
						   const TRectanglei& part)
{
	// Check if the part is completely in the image
	assert(part.m_Top>=0);
	assert(part.m_Bottom<=m_rectTextCoord.GetHeight());
	assert(part.m_Left>=0);
	assert(part.m_Right<=m_rectTextCoord.GetWidth());

	if (m_pTexture)
	{
		m_pTexture->Bind();

		TRectanglei newTextRect = part;
		newTextRect.OffsetRect(m_rectTextCoord.m_Left,m_rectTextCoord.m_Top);

		// Get the coordinates of the image in the texture, expressed
		// as a value from 0 to 1.
		float Top	 = ((float)newTextRect.m_Top)/m_pTexture->GetHeight();
		float Bottom = ((float)newTextRect.m_Bottom)/m_pTexture->GetHeight();
		float Left   = ((float)newTextRect.m_Left)/m_pTexture->GetWidth();
		float Right  = ((float)newTextRect.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+newTextRect.GetHeight(),0);
		glTexCoord2f(Right,Bottom);	glVertex3i(iXOffset+newTextRect.GetWidth(),iYOffset+newTextRect.GetHeight(),0);
		glTexCoord2f(Right,Top);	glVertex3i(iXOffset+newTextRect.GetWidth(),iYOffset,0);
		glEnd();
	}
}


2、Displaying Animations

What would be a game without animations? Probably quite boring to play, so let's look at how we can add some dynamism here by playing animations. The basic idea behind animations in 2D games is rather simple: It is the same as a cartoon, which consists of breaking up the movement into distinct images. The brute force approach would be to have a loop in which you sleep for a while before displaying the next image. As you might already have guessed, this doesn't work at all. You have several issues if you try to do that: first, nothing will be displayed at all because you never swap the buffers (which was done in theCMainWindow::Draw() function). Second, if you do that, the rest of your program is not processed at all, which also means that you would only be able to display one animation at a time. Not very convenient... The correct approach consists of letting each 'animation' remember its state (e.g. which image it is currently displaying) and asking all of them to draw their current image. When a new frame should be drawn, each animation is 'asked' to go to the next image in the animation. This way, you keep a continuous flow in your program.

Let's now take a look at the CImageListclass. This class is basically a wrapper class around astd::listwhich contains images and provides some helper functions to play the images:

// 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();

  // Assignment 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;
};
The implementation is pretty straightforward: it basically adds images to a std::list<TImagePtr> on demand and keeps an iterator which points to the currently active image. Let's for example take a look at the GoToNextImage() function:
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;
}

We first check if the iterator is valid (doesn't point at the end of the list). The iterator is invalid when the list is empty: in that case we simply return from the function, otherwise we increase the iterator. We then check if the iterator reached the end of the list (which happens when it was previously pointing to the last image). In that case we reset it to the first image and we returntrue. I won't explain the other functions because they are rather trivial, but don't hesitate to take a look at the code.

Let's now look at the CAnimatedSpriteclass which allows you to group several animations together. Let's take an example: suppose that you are writing a game in which the player plays a knight. This knight will of course have multiple different animations: walk, attack, standstill, ... In general, you will need to provide such animations for each direction your knight can have in your game. This class will then be used to represent your knight: you will be able to load several animations and replay them later on demand:

// 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;
};

The principle of the class is the following: it contains a map of all animations that can be played for the sprite, with the key being astringidentifying the animation and the value being aCImageListobject containing the animation. TheAddAnimationandPlayAnimationsimply add or retrieve an animation from the map:

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);
  }
}
When trying to play an non existing animation, an exception is thrown. The m_iterCurrentAnim variable is an iterator pointing to the current animation. It is used in the DrawSpriteand NextFramemethod to access the current animation:
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();
}
In the DrawSprite method, we retrieve the current image of the current animation and simply blit it at the specified position on the screen (remember how the CImageclass was working). In the NextFrame, we simply go to the next image in the current animation.

CImageLIst类其他函数的实现:

CImageList::CImageList() : m_lstImages()
{
}

CImageList::CImageList(const CImageList& other)
{
	TImageList::const_iterator iter = other.m_lstImages.begin();
	for (iter; iter!=other.m_lstImages.end(); iter++)
	{
		m_lstImages.push_back(*iter);
	}
	m_iterCurrentImg = m_lstImages.begin();
}

CImageList::~CImageList()
{
}

CImageList& CImageList::operator= (const CImageList& other)
{
	Clear();

	TImageList::const_iterator iter = other.m_lstImages.begin();
	for (iter; iter!=other.m_lstImages.end(); iter++)
	{
		m_lstImages.push_back(*iter);
	}
	m_iterCurrentImg = m_lstImages.begin();
	return *this;
}

void CImageList::Clear()
{
	m_lstImages.clear();
	m_iterCurrentImg = m_lstImages.end();
}

void CImageList::AppendImage(TImagePtr pImage)
{
	m_lstImages.push_back(pImage);
	m_iterCurrentImg = m_lstImages.begin();
}

unsigned CImageList::GetImagesCount() const
{
	return (unsigned)m_lstImages.size();
}

void CImageList::GoToFirstImage()
{
	m_iterCurrentImg = m_lstImages.begin();
}

TImagePtr CImageList::GetCurrentImage() const
{
	TImagePtr pToReturn = NULL;
	if (m_iterCurrentImg != m_lstImages.end() )
		pToReturn = *m_iterCurrentImg;
	return pToReturn;
}




  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值