Home
Code & Snippets
Tutorials
GUI Docs
Projects
Drawings & Sketches
Links
Bio & Resume

Kindly hosted by


Support This Project


Tetris Tutorial: Part #5

Hello there and welcome to the fifth tutorial on how to make a decent Tetris game!

In this tutorial we will be going into some intricates of the block animations.
Warning: this is a long tutorial, so take your time reading it by parts or over again.

OK, let’s start.

Let’s take a look at a class CAnimation. It’s a template class where the passed template attribute is used as an array for custom frame data. For example, if we want each frame to refer to textureID in Textures database, we declare animation as:

CAnimation<int> m_Animation;

Why as an ‘int’? Simply because textureID is an integer (or it can be an unsigned integer).
OK, moving on, we take a look into details of CAnimation class:

template <class T> class CAnimation
{
  private:
  // These variables store:
  unsigned int m_iCurrentFrame,      // - Current frame index
  m_iFrameCount;	                   // - Total number of frames
  
  unsigned int m_iLoopCount;         // - Number of loops this animation have to play
  unsigned int	m_iCurrentLoop;	   // - A current loop index
  int	m_iAnimationState;	   // - Animation state:
  // - 0 if animation hasn’t started
  // - 1 if it is running
  // - 2 if it’s finished
  double m_fLastTime;
  int *m_pfFrameTimes;               // - These are frame times – e.g how long each frame will be running (in msecs).
  
  CTimer m_Timer;                    // - A timer member variable – to acquire time.
  T **m_pFrames;	                   // - Custom frames array
  // …
};


The only interesting thing in this class would be the Animate() function, because other procedures deals only with resetting and get/setting member variables:

virtual int Animate()
{
  // Make sure the animation starts after class initialization
  if(m_fLastTime == 0)
    m_fLastTime = m_Timer.getAbsoluteTime();

  // If no time for frames, then do nothing – 
  // we cannot animate without knowing the time for each frame!
  if(m_pfFrameTimes == NULL)
    return -1;
  
  double time_passed = m_Timer.getAbsoluteTime() - m_fLastTime;
  
  if(m_iCurrentFrame != (m_iFrameCount - 1))
  {
    // If time has passed that corresponds to given time frame value…
    if(m_pfFrameTimes[m_iCurrentFrame] <= (int)time_passed)
    {
      // Proceed to next frame
      m_iCurrentFrame++;
      // Update last time..
      m_fLastTime = m_Timer.getAbsoluteTime();
    }
  }
  else	// We have reached last frame, check the loops
  {
    if(m_iLoopCount > 0) If there is anything to loop (>0), do it:
    {
      if(m_iCurrentLoop != (m_iLoopCount - 1))
      {
        // If loop count not equal to loop count, update it and reset the animation.
        m_iCurrentLoop++;
        m_iCurrentFrame = 0;
      }
      // Animation has finished! (all loops are gone thru)
      else
      {
        m_iAnimationState = 2;
        return 2;
      }
    }
    else  // Animation has finished! (all frames are gone thru)
    {
      m_iAnimationState = 2;
      return 2;
    }
  }
  
  // Nope, we’re still running…
  m_iAnimationState = 1;
  return 1;
};


The overall design is pretty simple: run the animation that many frames (and that many loops) until it has finished. Not so simple in code, but yet more to come… ?

OK, let’s add member variables for block animations,

Class CTetrisBlock
{
  private:
  // A List of animations
  CAnimation<tAnimationFrame> *m_pAnimations;
  
  // Current active animation
  int m_iCurrentAnim;
  
  // Number of animations
  int m_iAnimCount;
  
  ...
};

and their get/set functions accordingly. The tAnimationFrame is a structure consisting of:

struct tAnimationFrame
{
  unsigned int m_iTextureUID; // Texture UID in Texture database
  sQUAD m_FrameRect; // Rectangle reference so that our textures for the block 
                     // animation frames can be retrieved from one file.
};

I will not go into details of CTextureManager and CMaterialManager, because those are simply wrappers around dynamic arrays they control, that also acts as databases – e.g retrieval by uid, filename or index (or more). I will say only one thing: I’ve checked them and so far they’ve proven themselves as a good measure against duplicates of materials/textures.

You can take a look at tMaterial/tTexture structures, but they are very basic (except loading functions). You can load materials from file they are stored in, or add copies of materials/textures that are already in database but with modified attributes. Note that textures can have an alpha channel which you can specify by using ‘Mask’ RGB attribute of <Texture> tag:

< Texture Filename=”Data/block.bmp” Mask=”0,0,0”/> - The simplest mask/texture tag

Right now we cannot use sub-rectangles to map parts of same texture to different frames, so we are going to use separate files as frames.
OK, let’s add some textures/materials we’re going to use with our game:

<Texture Filename="Data/Frame0.bmp" RefID="1"/>
<Texture Filename="Data/Frame1.bmp" RefID="2"/>
…
…
Up to texture 10…

<Material TexRefID="1" RefID="1"/>
<Material TexRefID="2" RefID="2"/>


Up to material 10 (we could use 1 material and 1 texture if we could refer to sub-rectangle of the single texture file!)

<Texture Filename="Data/Block.bmp" RefID="11"/>
<Material TexRefID="11" RefID="11"/>

OK, so far so good? Let’s now take a look at XML file containing our game data. You probably remember the size of it last tutorial, so you will be surprised now:

<Game BlockSize="20" BlockSpacing="1" Window="0,420,300,0">
  <Figure Name="SimpleBlock" Type="Template">
    <Block PosX="0" PosY="0">
      <Animation Filename="block_draw_anim1.xml"/>
      <Animation Filename="block_draw_anim2.xml"/>
      <Animation Filename="block_disappear_animation.xml"/>
    </Block>
  </Figure>
  ...
  ...
</Game>

See, each block now has a list of animations! Please note that I didn’t put whole file here because it is too big, but the basic thing here is that EACH block will have those 3 animations! Each block in each figure! It’s not so hard to add those lines in, so let’s do it..

(Adding those lines takes time, so take your time :) )

OK, now onto the actual animations and their loading. We have 3 animations:
Block' default animation, when there’s NO animation (it sounds weird, but it’s easier to do texturing this way than adding a new variable for a texture):

<Animation>
  <Frame Rect="0,64,64,0" TextureUID="11" Time="60000"/>
< /Animation>

Block’s default animation where it is going to.. um.. shine or flash, or whatever you can draw in animation package – you will have to expand it according to your new files for each frame (or sub-rectangles for one file) – same as above:

<Animation>
  <Frame Rect="0,64,64,0" TextureUID="11" Time="50"/>
</Animation>

And finally our block’ disappearing animation – when a line is full we play this animation once (or twice, or how many times you want):

<Animation>
<Frame Rect="0,64,64,0" TextureUID="1" Time="50"/>
<Frame Rect="0,64,64,0" TextureUID="2" Time="50"/>
<Frame Rect="0,64,64,0" TextureUID="3" Time="50"/>
<Frame Rect="0,64,64,0" TextureUID="4" Time="50"/>
<Frame Rect="0,64,64,0" TextureUID="5" Time="50"/>
<Frame Rect="0,64,64,0" TextureUID="6" Time="50"/>
<Frame Rect="0,64,64,0" TextureUID="7" Time="50"/>
<Frame Rect="0,64,64,0" TextureUID="8" Time="50"/>
<Frame Rect="0,64,64,0" TextureUID="9" Time="50"/>
<Frame Rect="0,64,64,0" TextureUID="10" Time="50"/>
</Animation>

So, we’re done with our animations representations in XML file, let’s take a look at how we are going to load and use this data.
Since a user can declare ‘class T’ in class CAnimation as whatever he wants to, he’s going to have a separate functions for loading different types of data, as in here:

CAnimation<MyCustomData> m_Animations[5];

To load this one, he must declare and implement the following function:
int LoadAnimation(CAnimation<MyCustomData> *animation, TiXmlNode *this_node, char *filename);

So, to load our animation of type:
CAnimation<tAnimationFrame> *m_pAnimations;
We have to:

1. Count number of animations
  a. For each animation do:
    i. Count number of frames
    ii.	Load frames
2. Load animations

So, let’s take a look at how we’re going to do part 1: CTetrisBlock::LoadFromFile()

TiXmlNode *child_node = NULL;
child_node = main_node->FirstChild();

while(child_node !=NULL)
{
  if(stricmp(child_node->Value(), "animation") == 0) m_iAnimCount++; // Count me as animation!
   child_node = main_node->IterateChildren(child_node);
}

if(m_iAnimCount > 0) // Allocate array for animations
  m_pAnimations = new CAnimation<tAnimationFrame>[m_iAnimCount];

int count = 0;
child_node = main_node->FirstChild();

while(child_node !=NULL)
{
  if(stricmp(child_node->Value(), "animation") == 0) // This is an animation data XML node
  {
    if(LoadAnimation(&m_pAnimations[count], child_node, NULL))
    count++; // OK, this one is loaded, do next one
  }
  child_node = main_node->IterateChildren(child_node); // Go next XML node
}
SetAnimation(0); // Set our default drawing animation
OK, we counted and called functions to load each animation, let’s see what does LoadAnimation() function holds:
bool LoadAnimation(CAnimation<tAnimationFrame> *animation, TiXmlNode *this_node, char *filename)
{
  if(animation == NULL) // Make sure the user passed valid pointer to animation struct
  return false;
  
  TiXmlNode *main_node = NULL;
  
  TiXmlDocument doc(filename);
  if(filename !=NULL)
  {
    bool loadOkay = doc.LoadFile();
    
    if ( !loadOkay )
      return false;
    
    main_node = doc.FirstChild();
  }
  else
    main_node = this_node;
  
  TiXmlElement *element = NULL;
  const char *value = NULL;
  
  element = main_node->ToElement();
  // Does this XML node has external reference? - very useful thing to do, 
  // since we don't have to re-write each data all over again each time we have to, 
  // simply specify filename it's stored it, and that's it!
  value = element->Attribute("Filename");
  if(value !=NULL) // If it does, use it and load data from specified file
    return LoadAnimation(animation, NULL, (char *)value);
  
  value = element->Attribute("Loops"); // Load number of loops for this animation
  if(value !=NULL)
  {
    animation->SetLoopCount(atoi(value));
    value = NULL;
  }
  
  // Count the number of frames
  int num_frames = 0;
  
  TiXmlNode *child_node = main_node->FirstChild();
  while(child_node !=NULL)
  {
    if(stricmp(child_node->Value(), "frame") == 0)
      num_frames++;
    
    child_node = main_node->IterateChildren(child_node);
  }
  
  animation->Create(num_frames); // Re-create animation with given number of frames
  
  // Let’s load each frame now..
  int count = 0;
  child_node = main_node->FirstChild();
  
  while(child_node !=NULL)
  {
    if(stricmp(child_node->Value(), "frame") == 0)
    {
      element = child_node->ToElement();
      tAnimationFrame *frame = animation->GetFrame(count);

      value = element->Attribute("Rect");
      if(value !=NULL)
      {
        // Read sub-rectangle data
        sscanf(value, "%d,%d,%d,%d", &frame->m_FrameRect.m_iLeft, &frame->m_FrameRect.m_iRight, &frame->m_FrameRect.m_iTop, &frame->m_FrameRect.m_iBottom);
        value = NULL;
      }
      
      value = element->Attribute("TextureUID");
      if(value !=NULL)
      {
        // Read texture reference (or material later)
        if(CTextureManager::GetSingleton().GetTexture(-1, atoi(value), -1))
          frame->m_iTextureUID = atoi(value);
        
        value = NULL;
      }
      
      value = element->Attribute("Time");
      if(value !=NULL)
      {
        // Read time for this frame
        animation->SetFrameTime(count, atoi(value));
        value = NULL;
      }
    }
    
    child_node = main_node->IterateChildren(child_node); Progress to next XML node
    count++;
  }
  
  return true; // Return ‘true’ since all has been loaded successfully!
}

Also, to actually draw our quad(block) with texture/material, we have to modify our drawing function and add a pointer to current material (see TetrisBlock.h class):

void CTetrisBlock::Draw()
{
  sQUAD quad;
  
  // Calculate quad vertices based on our block’ X/Y position
  quad.m_iLeft = m_iPosX * (gBlockSize + gBlockSpacing) + gBlockSpacing;
  quad.m_iRight = quad.m_iLeft + gBlockSize - gBlockSpacing;
  quad.m_iTop = m_iPosY * (gBlockSize + gBlockSpacing) - gBlockSpacing;
  quad.m_iBottom = quad.m_iTop - gBlockSize + gBlockSpacing;

  if(m_iCurrentAnim == -1) // Make sure there is some animation running, and if not, draw a plain quad.
  {
    glBegin(GL_QUADS);
    glVertex3f(quad.m_iLeft, quad.m_iBottom, 0);
    glVertex3f(quad.m_iRight, quad.m_iBottom, 0);
    glVertex3f(quad.m_iRight, quad.m_iTop, 0);
    glVertex3f(quad.m_iLeft, quad.m_iTop, 0); 
    glEnd();
    
    return; // Do not go down because there’s no need to – exit the function
  }
  
  // Aquire current animation frame...
  tAnimationFrame *frame = m_pAnimations[m_iCurrentAnim].GetFrame(m_pAnimations[m_iCurrentAnim].GetCurrentFrame());
  int mat_uid = frame->m_iTextureUID;
  // And material...
  m_pMaterial = CMaterialManager::GetSingleton().GetMaterial(-1, mat_uid);
  
  if(m_pMaterial !=NULL) // Make sure we got a valid material reference
  {
    float left = 0, right = 0, top = 0, bottom = 0;
    if(m_pMaterial->m_pTexPtr !=NULL)
      {
      // Calculate frame texture mapping coordinates
      left = (float)frame->m_FrameRect.m_iLeft / (float)m_pMaterial->m_pTexPtr->m_iWidth;
      right = (float)frame->m_FrameRect.m_iRight / (float)m_pMaterial->m_pTexPtr->m_iWidth;
      top = (float)frame->m_FrameRect.m_iTop / (float)m_pMaterial->m_pTexPtr->m_iHeight;
      bottom = (float)frame->m_FrameRect.m_iBottom / (float)m_pMaterial->m_pTexPtr->m_iHeight;
      
      // Apply material (glBindTexture & others)
      ApplyMaterial(m_pMaterial, TEX_COLOR);
      
      // Does this texture has a mask?
      if(m_pMaterial->m_Alpha.r !=-1 && m_pMaterial->m_Alpha.g !=-1 && m_pMaterial->m_Alpha.b !=-1)
      {
        glAlphaFunc(GL_GREATER,0.0f);
        glEnable(GL_ALPHA_TEST);
        glBegin(GL_QUADS);
        glTexCoord2f(left, top); glVertex3f(quad.m_iLeft, quad.m_iBottom, 0);
        glTexCoord2f(right, top); glVertex3f(quad.m_iRight, quad.m_iBottom, 0);
        glTexCoord2f(right, bottom); glVertex3f(quad.m_iRight, quad.m_iTop, 0);
        glTexCoord2f(left, bottom); glVertex3f(quad.m_iLeft, quad.m_iTop, 0);
        glEnd(); 
        glDisable(GL_ALPHA_TEST);
      }
      else
      {
        // No alpha mask, draw textured quad
        glBegin(GL_QUADS);
        glTexCoord2f(left, top); glVertex3f(quad.m_iLeft, quad.m_iBottom, 0);
        glTexCoord2f(right, top); glVertex3f(quad.m_iRight, quad.m_iBottom, 0);
        glTexCoord2f(right, bottom); glVertex3f(quad.m_iRight, quad.m_iTop, 0);
        glTexCoord2f(left, bottom); glVertex3f(quad.m_iLeft, quad.m_iTop, 0);
        glEnd(); 
      }
    }
  }
  else
  {
    // No valid material detected, draw plain quad - what a pity...
    glBegin(GL_QUADS);
    glVertex3f(quad.m_iLeft, quad.m_iBottom, 0);
    glVertex3f(quad.m_iRight, quad.m_iBottom, 0);
    glVertex3f(quad.m_iRight, quad.m_iTop, 0);
    glVertex3f(quad.m_iLeft, quad.m_iTop, 0); 
    glEnd();
  }
}

Well, well, well… So far so good? Now let’s take a look at CTetrisBlock::Animate() function that will handle block’ animations. It is very simple:

bool CTetrisBlock::Animate()
{
  if(m_iCurrentAnim >=0 && m_iCurrentAnim < m_iAnimCount)
  {
    // 0 is ‘not started’, 1 is for ‘running’ and 2 is for ‘finished’.
    int ret = m_pAnimations[m_iCurrentAnim].Animate();
    
    if(ret == 0 || ret == 1)
      return false;	// Animation either not started or still running, return ‘false’
    else if(ret == 2)
      return true;	// Animation has finished, return ‘true’
  }
  
  return true; // No animations has been done, return ‘true’ – e.g. finish it already ?
}

Well. Now… What do we need now? Oh yeah, the AnimateLine() function of CTetrisGame. I’ve added more candy to it, so that our blocks will animate in order take a look for yourself).Well, here we go:

bool CTetrisGame::AnimateLine(unsigned int line_index)
{
  bool all_done = true;
  for (int i=0; i<m_iActivationBlock; i++)
  {
    m_ppTetrisMatrix[line_index][i]->SetAnimation(2); // Our eliminating line animation is third (-1 = 2)
    if(!m_ppTetrisMatrix[line_index][i]->Animate()) // If ANY of the blocks hasn’t finished the animation, 
                                                    // continue running (to Update() function)
      all_done = false;
  }
  
  if(m_iActivationBlock != gNumBlocksX)
    m_iActivationBlock++;
  
  if(all_done)
    m_iActivationBlock = 1;
  
  return all_done;
}

OK, what do we got left? Nothing! And that what you should be proud of – now you have a full tetris game (without scores or background) with animations and XML data files!

In Tutorial 6 we will go into bonuses and their animations/actions. See ya later!