|
Kindly hosted by
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!
|