Home > Game Development Walkthroughs > Tetris Revisited: Adding 2D Animations and Other Graphical Tweaks

Tetris Revisited: Adding 2D Animations and Other Graphical Tweaks

October 12, 2012 Leave a comment Go to comments

It’s game coding time with SimpleTetris again and this time we’re going to look at how to add a variety of different animations and useful graphical tweaks which can equally be applied to any game or demo.

We will create a simple animation class in C++ which takes care of tracking the position and timing of basic animations, and learn how to apply this to make changes in an object’s position, colour and alpha (transparency). We will examine how to use this class to animate several objects at different points in the path of a single animation, how to affect two paremeters of an object (eg. position and transparency) with a single animation function, and how to partly randomize the intermediate steps (interpolation) of an animation.

Although we are again using Simple2D here (and as such this article serves a tutorial on how to use it, and how SimpleTetris is evolving), the algorithms and principles can be mostly copy/pasted and are easily adaptable to other graphics libraries. The animation class works independently of Simple2D and does not depend on it.

Download: Source code (.cpp) | Source code (.h) | Executable

Introduction to 2D Animations

Animation functions and parameters

The core principle of any 2D or 3D animation is that a sequence of changes occur to one or more objects over a certain number of frames, at a specific rate (or according to a specific function – the animation does not have to be linear). This change can be the movement of a point or sprite, a fade (by altering the level of alpha – or transparency – in an object), the cycling of colours (by varying the amount of red, green and blue in a colour according to some function) and so on. We are only looking at predictable, fixed animations in this article, which is to say, not for example the movement of the player or AI-guided enemies. Here we are talking about mere visual or audio effects which are incidental to the game itself. So we have two main factors at play:

  • The bound variable(s), which is the aspect of the game object, visual effect or sound that is going to change over time – for example the bound variable could be an x or y co-ordinate (or both) of a sprite, the sprite frame number (to animate sprites which have several frames, eg. an animated GIF), a palette/colour code for an object or visual effect, the rotation angle or scale factor of an object, the volume of the currently playing game music (for fades), or any other variable you can think of.
  • The animation function, which specifies an equation describing how the bound variable(s) will change over time. This can be as simple as a linear equation – “move 100 pixels per second” – a sine or cosine function for fluid and smooth acceleration and deceleration of an object or to make wavy lines for example – or something as complex as a bezier curve, polynomial function or even a random number generator for static, distortion and certain particle effects.

Don’t worry if you’re a bit confused right now; examples of how this all hangs together follow below!

Animation types

Certain types of animations require different types of changes to your code and I will try to present examples of different types here. I’m going to introduce some random made-up terminology to clarify these types:

Animations that run constantly in the background (that is to say, they run continuously, not that they are actually in the background of the scene) – let’s be wildly creative and call them background animations – can be easily shoehorned into your game. All you have to do is add code to your per-frame game world update function to move the animations forward one frame, looping or cycling when you reach the end of the animation.

Triggered animations happen as a result of something that happens in the game – perhaps triggered by the user, perhaps not. The implementation is similar to background animations except that you must first check if the animation is active (currently running or not), maintain a flag for this, and turn it off when the animation ends rather than looping it.

Some types of animations will actually alter the flow of execution of the game, and these are by far the most complex to implement. For example, in Tetris, when a line is cleared, you may want to fade the line out. For reasons of consistency, the rest of the game update engine should be paused while you do this, because you are now creating a state where essentially one row is empty but the other blocks haven’t fallen down yet – an essentially illegal game world state in Tetris. This situation will continue for a number of frames, and only once the blocks have been re-positioned correctly should the next piece fall. If you don’t do this, and the bucket is nearly full, the player can lose the game unfairly as a new piece comes tumbling down before the previous line clear animation has finished. Depending on the code implementation, the collision detection may also be off by one row in the bucket. These kinds of animations – which I’ll call intrinsic animations – require changes to the game logic and are much harder to just throw in later as an afterthought.

Figure 1. Variable-width characters being animated in a sine wave (the text moves up and down according to the sine wave function). The position of the first character is calculated directly from the result of the animation function (sine). The remaining characters are calculated by applying an offset to the animation position relative to their X co-ordinate.

Timing your animations correctly

A big mistake that many beginning programmers make is tying the speed of an animation per-frame, such that the animation function advances by ‘one unit’ each frame. Unfortunately, the frame rate of games is not reliable, and if there is a drop in the frame rate, your animation will slow down and things will end up in the wrong places. Conversely, if your game is running on a much faster PC with a higher frame rate, the animations may run too fast. The solution to this is to tie the speed of your animations to how much time has elapsed between frames, and define a value which specifies how much movement or variation in the animation function should occur per interval of time (for example, per second). For example, let’s say you want to move an object 100 pixels per second. Between each frame, you calculate how much time has elapsed and advance the object’s position accordingly. If 1/10th of a second has elapsed (meaning the game has dropped to 10fps) you will advance the object by 10 pixels. If only 1/20th of a second has elapsed (20fps), you advance the object by 5 pixels. And if 1/100th of a second has elapsed (100fps), you advance the object by 1 pixel. In this way, the animation will run at the same speed regardless of how many frames are actually rendered during its running time. The formula is simple:

objectPosition = objectPosition + movementPerSecond / SecondsElapsedSinceLastFrame

or more generally:

boundVariable = boundVariable + animationFunctionDelta(desiredAdvancePerSecond / SecondsElapsedSinceLastFrame)

In the examples below, you will learn simple practical ways to do this calculation using timer functions, and apply it to different kinds of animation.

A simple re-usable animation class

Tracking the position and timing of animations is cumbersome so it would be nice to hide all this away in a class that does it for us. Simple2D includes a trivial animation class with which you can provide in the constructor an animation function and a number of other parameters, and the object spits out at any given moment the current result of the animation function based on the time interval you specified (the length of time it should run).

To keep things standardized, we suppose that at the start of the animation, our position is 0.0 and at the end, our position is 1.0. The animation function should always return a value between -1.0 and +1.0. We also provide some basic built-in functions which illustrate the concept:

// Built-in animation algorithms
double Animations::Linear(double intervalPc)
{
	return intervalPc;
}

double Animations::Sin(double intervalPc)
{
	return std::sin(2 * M_PI * intervalPc);
}

double Animations::Cos(double intervalPc)
{
	return std::cos(2 * M_PI * intervalPc);
}
...

These functions are provided a value from 0.0 to 1.0 indicating how far into the animation we are, and return a value from -1.0 to +1.0 indicating the actual result of the animation function.

The basic constructor looks like this:

typedef boost::function<double (double)> Simple2DAnimFunc;

// Cycle types
// Repeat:		 run from 0-1 and wrap around to 0 again
// Clamp:		 run from 0-1 and hold at 1
// Reverse:		 run from 0-1 then 1-0 and repeat
enum CycleType { Repeat, Clamp, Reverse };

Animation(Simple2DAnimFunc func, int interval, double amp = 1.0, double b = 0, CycleType ct = Repeat, bool startPaused = false);

The 1st parameter is the animation function, the 2nd is how many milliseconds the animation should run for, the 3rd is the ‘amplitude’ which is a multiplication factor applied to the result of the animation function, the 4th is the ‘base’ which is added to the result of the animation function (after multiplication) – useful when you want an offset other than zero – the 5th is the cycle type which specifies how you want the animation to loop or repeat (see code extract above), and the 6th specifies whether to start the animation timer immediately or not.

You retrieve the current position in the animation by calling GetAnimPos() and the current result of the animation function by calling GetAnimOffset().

An example should help clear this up. Let us say you have a sprite, and you want to move its X co-ordinate from 100 to 400 over a period of 3 seconds. You could create an animation like this:

Animation anim(Animations::Linear, 3000, 300, 100, Clamp);

Note that the 300 in the 3rd parameter specifies the distance the rectangle should move in total. Calling anim.GetAnimOffset() will over the course of 3 seconds from the moment the constructor was called return values from 100-400 in a linearly increasing fashion. What happens is that the 0.0-1.0 result of Animations::Linear() is scaled by 300, and added to 100, giving the desired range of X values.

Let’s take a look at how this works internally. The following code can of course be used in your own programs:

class Animation
{
...
private:
	double amplitude;
	int intervalMs;
	int startTime;
	double base;
	CycleType cycleType;
	Simple2DAnimFunc animFunc;
...
);

...
Animation::Animation(Simple2DAnimFunc func, int interval, double amp, double b, CycleType ct, bool startPaused)
	: animFunc(func), amplitude(amp), intervalMs(interval), base(b), cycleType(ct), paused(startPaused)
{
	Reset();
}
...

void Animation::Reset()
{
	startTime = GetTickCount();
}
...

double Animation::GetAnimOffset()
{
	return animFunc(GetAnimPos()) * amplitude + base;
}
...

double Animation::GetAnimPos()
{
	double animPc = static_cast<double>(GetTickCount() - startTime) / intervalMs;

	if (cycleType == Repeat)
		animPc -= static_cast<int>(animPc);

	else if (cycleType == Clamp)
		animPc = min(animPc, 1.0f);

	else if (cycleType == Reverse)
	{
		bool reverseSection = (static_cast<int>(animPc) % 2 == 1);
		animPc -= static_cast<int>(animPc);
		if (reverseSection)
			animPc = 1 - animPc;
	}

	return animPc;
}

This is a simplified version of the full code in Simple2D as it offers other features and methods. The main work is done in the first line of GetAnimPos(), which calculates the time elapsed (in milliseconds) since the animation began, then divides it by the desired animation run-time to obtain the percentage (in decimal, ie. 0.0 – 1.0) specifying how far through the animation we are. The remaining lines simply implement the logic for the cycle type.

The result of this function is then passed to the specified animation function in GetAnimOffset(), multiplied by the desired amplitude and added to the base value, to get the final result of the animation function.

In your object update code, you will simply have a line like this to update the X co-ordinate of your object:

objectX = anim.GetAnimOffset();

Updating several objects from a single animation

Sometimes you will want to modify the position of several objects along the curve of the animation function, particularly in pathing or wave formations, and for this we will need access to future (or past) results of the animation function relative to the current time. For this reason, we add a parameter to GetAnimOffset() which allows us to return some arbitrary future or past value of the animation function:

double Animation::GetAnimOffset(double offset)
{
	return animFunc(GetAnimPos(offset)) * amplitude + base;
}
...
double Animation::GetAnimPos(double offset)
{
	double animPc = static_cast<double>(GetTickCount() - startTime) / intervalMs;

	animPc += offset;
...

These are the only changes needed.

For example, let’s suppose we have some text that we want to animate in a sine wave (see Figure 1). We create an animation which gives us the position of the first character:

// Make the text wobble in a sine wave with 50 pixel amplitude,
// a 2000ms (2 second) cycle/period and a base offset when calculating the current
// position of 180 (equivalent to adding 180 pixels to the animation position)

textAnim = Animation(Animations::Sin, 2000, 50, 180);

We then cycle over each letter, calculating for each one the relative offset into the animation function we need the value from (each character has a different pixel width, hence the need for the calculations):

// Get the total pixel width of the text
int width = TextWidth(layout);

// Set the left-most pixel where text shall be plotted - this causes it to be centerised in the window
int baseX = (ResolutionX - width) / 2;

// Left-most pixel offset from baseX where we are currently drawing text (first character = 0 offset)
int posX = 0;

// Draw each character in the text one by one
for (int i = 0; i < length; i++)
{
    // Percentage we are into text (X)
    float textPc = static_cast<float>(posX) / width;

    // Amount to shift letter by
    // Gets the current animation position + textPc % (to make a smooth sine curve)
    int shiftY = static_cast<int>(textAnim.GetAnimOffset(textPc));

    // Draw the letter
    Text(baseX + posX, shiftY, StringFactory(static_cast<char>(text[i])), Colour::Yellow);

    // Update where to draw the next letter by adding the width of the current character to our x co-ordinate
    posX += TextWidth(layout, i);
}

With all of this in mind, let’s now turn to practical uses and examples from SimpleTetris.

Real-world animations

Tracer dots (background animation)

Goal: Make a series of dots move repeatedly around the edge of the bucket at a linear speed, with the speed increasing every time the player levels up (it is best to try the game to understand this better).

First, we make a linear animation with no time length (this will be decided later as it depends on the player’s level), and an amplitude equivalent to the number of pixels length the bucket geometry would be if it was stretched out into a single line. This handy GetLength() function is provided by Direct2D.

// Bucket tracer animation (in application initialization)
tracerAnimation = Animation(Animations::Linear, 0, bucketGeometry.GetLength());

When a new game starts, set the initial speed (here we specify that we want the dots to make one full traversal of the bucket geometry every 32 seconds) and reset the animation to start from the beginning:

// Reset tracer animation
tracerAnimation.SetInterval(32000);
tracerAnimation.Reset();

In the bucket rendering code, we render 3 tracer dots after the main bucket has been drawn:

// Draw tracers
D2D1_POINT_2F point, tangent;

renderer->SetBrush(Colour::CornflowerBlue);

for (double i = 0; i < 0.05; i += 0.02) { 	bucketGeometry.GetGeometry().GetOriginalGeometry()->ComputePointAtLength(static_cast<FLOAT>(tracerAnimation.GetAnimOffset(i)), NULL, &point, &tangent);
	renderer->Screen->FillEllipse(D2D1::Ellipse(point, 2, 2), renderer->CurrentBrush->GetBrush());
}

The for loop runs three times (with 0.00, 0.02 and 0.04 as values for i), using these values as an offset into the animation function relative to the current position in the animation. The code is a little awkward but Direct2D’s ComputePointAtLength() returns in screen co-ordinates the pixel on the geometry outline at a length along the geometry specified by the first parameter – which here, we set to the result of the animation function (offset by i). In this way, the dots travel along at a fixed distance from each other around the bucket.

Over in the bucket update code, we adjust the tracer dot speed when the player levels up:

// New level?
if (linesCleared / SimpleTetris::LinesPerLevel < (linesCleared + newFills) / SimpleTetris::LinesPerLevel)
{
	level++;

	// Make tracers go faster!
	tracerAnimation.SetInterval(33500 - level * 1500);
}

We merely subtract 1.5 seconds from the total traversal time per level (giving the appearance of more frantic movement as the player levels up more and more) using SetInterval() to adjust the animation’s interval parameter.

Dynamic bucket gradient (background animation)

Goal: We want the interior of the bucket to slowly undulate with a vertical graduated fill from blue to black (or a darker blue) that moves gradually up and down the bucket interior over a period of time, to give the appearance of a smooth palette animation. Additionally, we want the overall colour to become more red as the bucket becomes more full, indicating danger of the game ending.

In this case we want the animation to go forwards then backwards then forwards again etc. so that movement of the colour goes from top to bottom, then back to top again. We create a 10-second animation with this code:

bucketColourAnimation = Animation(Animations::Linear, 10000, 1, 0, Animation::Reverse);

Now in order to create the graduated fill (see Figure 3), we are going to get a bit tricky. We will set two colours – one for the top pixel row of the bucket, and one for the bottom pixel row. Direct2D’s graduated brush will interpolate the colour values of all the rows in between for us to create a smooth gradient. The animation function is returning a value from 0.0 – 1.0 here, which we will use as the amount of saturation of colour in the bottom row of the bucket. The top row will be the opposite, so that when the bottom of the bucket is fully saturated, the top is black, and vice versa. This can be achieved by calculating the result of the animation function at 1.0 minus the current position we are along, ie. the inverse. We can use GetAnimOffsetReversed() to do this, which is defined as follows:

double Animation::GetAnimOffsetReversed(double offset)
{
	return animFunc(GetAnimPosReversed(offset)) * amplitude + base;
}
...
double Animation::GetAnimPosReversed(double offset)
{
	return 1 - GetAnimPos(offset);
}

In addition, we want the bucket colour to be blue when empty, red when full, and somewhere in between for the other cases. The first task is to calculate how full the bucket is, which we do here by working out the top row in use by a block and converting it to a percentage:

// Get highest used row for colour animation
int topUsedRow = -1;

for (int y = 0; y < BucketSizeY && topUsedRow == -1; y++)
	for (int x = 0; x < BucketSizeX && topUsedRow == -1; x++)
		if (board[y][x] != -1)
			topUsedRow = y;

if (topUsedRow == -1) topUsedRow = BucketSizeY;

float usedPc = static_cast<float>(BucketSizeY - topUsedRow) / BucketSizeY;

(board[y][x] contains -1 if the square is empty, and another value if occupied)

We now use usedPc to decide how much blue and red to saturate the gradient colour with (this will be the same at both ends of the bucket because it is multiplied by the overall saturation level returned by the animation function for each end). In the code below, two D2D1::ColorFs are used to define the top and bottom row colours respectively; the three parameters are the levels of red, green (always zero here) and blue saturation respectively, from 0.0 (none) to 1.0 (full saturation):

// The gradient changes from blue to red depending on how much of the bucket has been used up
TemporaryGradient bg(new GradientObject(renderer,
	D2D1::ColorF(static_cast<FLOAT>(bucketColourAnimation.GetAnimOffsetReversed()) * usedPc, 0,
	static_cast<FLOAT>(bucketColourAnimation.GetAnimOffsetReversed()) * (1 - usedPc)),

	D2D1::ColorF(static_cast<FLOAT>(bucketColourAnimation.GetAnimOffset()) * usedPc, 0,
	static_cast<FLOAT>(bucketColourAnimation.GetAnimOffset()) * (1 - usedPc)),

	Vertical, D2D1_EXTEND_MODE_CLAMP));

The red saturation is increased (multiplied by usedPc) the more full the bucket is, whereas the blue saturation is decreased (multiplied by 1 - usedPc) in the same circumstances. As the bucket empties and usedPc decreases, the blue saturation outweighs the red saturation and vice versa.

Transient display of earned points (triggered animation)

Goal: It is very common in games to quickly flash up the number of points earned when you destroy an enemy or achieve some other trivial task, next to the location on the screen where the points were earned – eg, over the enemy’s head or near a line clear in Tetris. We can achieve this with a simple class which fires off these throwaway animations as needed.

There is a slight gotcha here to be aware of: there may be more than one of these animations taking place at once, especially in a fast-moving game where you are mowing down waves of enemies in quick succession. Therefore we need to keep a list of each activate point display animation, render them all, and throw away any that have expired as and when needed. This is the main challenge with this type of animation where there can be more than one active at a time, and where it differs from the previous examples.

First, let’s make storage for a list of active animations:

// Score animations (upon line clearance)
vector<ScoreAnimation> scoreAnimations;

We’ll make a wrapper class which stores the text string of the points earned to display, its on-screen co-ordinates and the progression of the animation:

// ==========================================================================
// Drifting score animation helper
// ==========================================================================

class ScoreAnimation
{
	// Pointer to renderer
	Simple2D *renderer;

	// Text details
	std::string text;
	TextFormat textFormat;

	// Animation position
	int x;
	int y;

	// Animation progress
	Animation a;

public:
	// Constructor
	ScoreAnimation(Simple2D *r, TetrisShape &s, int p);

	// Draw score text
	void Draw();

	// Returns true if the animation has finished
	bool Done() { return a.GetAnimPos() == 1.0f; }
};

In the game update code, when one or more lines have been filled, we first calculate how many points have been earned and then add a new ScoreAnimation to the vector of active animations, passing to its constructor a reference to the shape that caused points to be earned; we need this to calculate the starting position of the points text, as we are going to make it initially appear above the shape that caused the points to be scored then drift upwards and fade out, so we need the shape’s co-ordinates.

if (newFills > 0)
{
...
	// Setup background score animations
	renderer->AddScoreAnimation(s, renderer->CalculatePoints(newFills));
}

...
// Calculates the number of points to add right now for clearing X lines
int SimpleTetris::CalculatePoints(int numLines)
{
	int pointsPerLine[] = { 10, 100, 500, 1000 };
	return pointsPerLine[numLines - 1] * level;
}

// Add drifting score animation
void SimpleTetris::AddScoreAnimation(TetrisShape &s, int points)
{
	scoreAnimations.push_back(ScoreAnimation(this, s, points));
}

The ScoreAnimation constructor performs a number of calculations to set up the animation:

// Score animations
ScoreAnimation::ScoreAnimation(Simple2D *r, TetrisShape &s, int points)
{
	renderer = r;

	// Top-center of piece that filled the line(s)
	float w = static_cast<float>(s.GetWidth()) / 2 + s.FirstUsedColumn();
	x = static_cast<int>(TetrisBucket::LeftSide + s.PosX * SimpleTetris::BlockSize + w * SimpleTetris::BlockSize);
	y = TetrisBucket::TopSide + s.PosY * SimpleTetris::BlockSize + s.FirstUsedRow() * SimpleTetris::BlockSize;

	// Create animation
	a = Animation(Animations::Linear, 1500, 1.0f, 0, Animation::Clamp);

	// Create text data
	text = "+" + StringFactory(points);
	textFormat = renderer->MakeTextFormat(L"Verdana", 12.f);

	// Calculate width and adjust x accordingly
	TextLayout textLayout = renderer->MakeTextLayout(text, textFormat, -1, -1, false);
	int width = renderer->TextWidth(textLayout);
	int height = renderer->TextHeight(textLayout);
	SafeRelease(&textLayout);

	x -= width / 2;
	y -= height;
}

We calculate the top-center pixel of the shape which caused points to be earned using some application-specific jiggery-pokery, then make the text which will look like “+50”, “+1000” etc. This uses a non-monospaced font so we must then calculate the width and height of the text and adjust x and y so that the top-left position of the text plotting co-ordinates will cause the points text to appear directly over the center of the shape.

Finally, we create a linear 1.5-second animation with an amplitude of 1.0 and no base offset. The animation will therefore last for 1.5 seconds.

Rendering the animation is pretty trivial:

void ScoreAnimation::Draw()
{
	renderer->SetBrush(Colour::White);
	renderer->CurrentBrush->SetOpacity(static_cast<FLOAT>(a.GetAnimOffsetReversed()));
	renderer->Text(x, y - static_cast<int>(a.GetAnimOffset() * 50), text, DefaultBrush, textFormat);
	renderer->CurrentBrush->SetOpacity(1.0f);
}

Note here that the single animation function is used in two ways – first in reverse to calculate the amount of transparency (from 1.0 – 0.0, in other words from fully opaque to invisible), then multiplied by 50 to gradually make the text drift upwards from its initial position. This is the reason that we just leave the amplitude of the animation at 1.0. Once again, I recommend you play the game to see this effect in action as it is not easy to describe in words.

Finally, we just hook it up to the main rendering loop:

// Draw score animations
for (auto i = scoreAnimations.begin(); i != scoreAnimations.end(); i++)
	i->Draw();

for (auto i = scoreAnimations.begin(); i != scoreAnimations.end(); i++)
	if (i->Done())
	{
		scoreAnimations.erase(i);
		break;
	}

Note that we only kill off a maximum of one animation per rendering loop even if more than one has finished, because the vector iterator becomes invalid once you call erase(). This is basically a fudge for laziness, but as it happens, all of the animations last an identical amount of time so a deque or list may be a better choice of storage container, with a list having the particular advantage that erasing does not invalidate the iterator.

“Rolling” current score animation (triggered animation)

Goal: Another common feature of games is not to simply update the displayed score when points are scored, but to animate it by displaying a number of intermediate values (scores) between the previous score and new score to give the satisfying effect of “racking up points”. For example if your score is 100 and then becomes 200, we might want to display 110, 120, 130 … 180, 190 in successive animation frames before displaying 200. I will call this a rolling score animation.

This kind of animation poses a few small challenges: first, the displayed score might not be the player’s actual current score, so we will supplant the normal score rendering code with a new score rendering function that can handle this.

Second, more points may be earned while the score is still animating, so we need to be able to queue up additional points in a list so that they don’t get lost or the animation corrupted.

Third, we cannot use a fixed linear increment to interpolate between the scores when points are earned, because earning 1,000,000 points would then take 1,000 times more frames to animate than if we scored 1,000 points, say. Instead, we decide on a number of steps to use between the scores (for example, 20), and interpolate a total of that many steps.

Finally, we need to update the displayed rolling score every X frames (say, 2 or 3 or 4), so we need to track time just as in the other animations.

As with the drifting score animation above, we’ll make a class to wrap up the details of the animation. Here is the definition:

// ==========================================================================
// Rolling score animation helper
// ==========================================================================

class ScoreDisplay
{
private:
	// Pointer to renderer
	Simple2D *renderer;

	// Text formats
	TextFormat textFormat;
	TextFormat rollFormat;

	// Score display position
	int x;
	int y;

	// List of scores to roll
	std::deque<int> scores;

	// Last roll time
	int lastRollTime;

	// Latest score
	int score;

public:
	// First-time setup
	void Init(Simple2D *);

	// Draw score text
	void Draw();

	// Push a score onto the animation heap
	void UpdateScore(int);
};

We use a deque to store a list of scores to display, because it supports linear-time pushing and popping of values from each end, which is exactly what we need: new scores will be pushed onto the end of the deque, and the oldest score in the list will be popped from the front. When following along below, understand that the deque will not only contain newly earned scores, but also dynamically generated interpolations between previous scores and new scores which are pushed onto the deque in advance whenever the player’s score changes, such that the rendering code can just pop the first value off the deque and display it, creating the rolling effect.

The lastRollTime variable will store the value of GetTickCount() from when the value of the displayed score was last changed. We reset this to the current time every time we display a new value, and this lets us track how long has elapsed since the change so that we know when to advance onto displaying the next score.

To make the score update more noticeable, we define two text formats: one to use normally, and one to use when the score is animating. The constructor sets this up together with the co-ordinates of where to plot the score:

void ScoreDisplay::Init(Simple2D *r)
{
	renderer = r;

	x = 50;
	y = 75;

	textFormat = renderer->MakeTextFormat(L"Verdana", 24.0f);
	rollFormat = renderer->MakeTextFormat(L"Verdana", 36.0f);
}

The main game code tracks the player’s actual score so we don’t worry about managing it in this class. When their score changes, UpdateScore() is called with the new total score:

void ScoreDisplay::UpdateScore(int newScore)
{
	// New game?
	if (newScore == 0)
	{
		scores.clear();
		scores.push_back(0);
		score = 0;
		lastRollTime = 0;
	}

	// New line clear
	else
	{
		if (scores.size() == 0)
			lastRollTime = 0;

		int initialScore = (scores.size() > 0? scores.back() : score) + 1;

		// Don't roll small score increases
		if (newScore - initialScore < 50)
			scores.push_back(newScore);

		// Otherwise animate
		else
		{
			// Determine average step based on distance between scores
			int avgStep;
			if (newScore - initialScore <= 500) avgStep = (newScore - initialScore) / 10;
			else if (newScore - initialScore <= 5000) avgStep = (newScore - initialScore) / 20;
			else avgStep = (newScore - initialScore) / 40;

			for (int s = initialScore; s < newScore; s += avgStep)
			{
				int push = s + Simple2D::Random(1, avgStep - 1);
				if (push < newScore)
					scores.push_back(push);
			}
			scores.push_back(newScore);
		}
	}
}

If a new game has just started, we simply reset everything, emptying the deque, setting the displayed score to zero and so on.

Otherwise, we first look at the deque, and if it is empty this means that we are displaying the current (now previous) latest score already, so we alter lastRollTime to make the animation of the new score we were just supplied with in newScore commence immediately.

Next we calculate in initialScore the lowest score which is greater than the current (now previous) latest score. If the deque is empty, this will be in score, otherwise it will be the final (most recently added) entry in the deque.

If the increase in score is small (here, less than 50 points), we don’t bother to animate it at all, and just add it as a single entry to the end of the deque (the drawing function, as we shall see below, does not animate the score if the deque only contains one item). The reason we do this is because we only want the animation to play out when the player has scored a significant number of points. This can be tweaked as you prefer.

The final piece of code contains some slightly weird mathematics:

First, we choose how many steps of animation (how many different interpolated scores) we want based on the number of points just earned. In this example, we use 10 steps for 500 points or less, 20 steps for 5000 points or less, otherwise 40 steps. avgStep will end up containing the number of points to increment the old score by such that after the desired number of steps, it will equal the new (desired, final) score. For example, if the player scored 150 points, this will execute in 10 steps, and avgStep will be 15.

If we now just made a for loop to interpolate the scores linearly, we might push values like this into the deque, supposing the previous score was 2000 and the new score 2150:

2001, 2016, 2031, 2046, 2061, 2076, 2091, 2106, 2121, 2136, 2151

Immediately we can see a problem as 2151 is higher than the target score, so we introduce a condition to only push the score onto the deque if it is less than the target score.

To create a proper random rolling effect, we need to use not a linear interpolation, but random numbers somewhere between the old and target scores, yet still with each number in the sequence higher than the previous one. To do this, we can simply add a random number to each value that is less than the step size (avgStep):

int push = s + Simple2D::Random(1, avgStep - 1);

Now if we go from 2000 to 2100, we might get values like:

2005, 2013, 2027, 2033, 2041, 2052, 2069, 2078, 2085, 2094

After the for loop, the actual target score is pushed onto the end of the deque to make the animation finishes by displaying the correct value.

The rendering code looks like this:

void ScoreDisplay::Draw()
{
	TextFormat format = textFormat;
	Colour colour = Colour::CornflowerBlue;

	if (scores.size() > 0)
	{
		if (GetTickCount() - lastRollTime >= 40)
		{
			score = scores.front();
			scores.pop_front();
			lastRollTime = GetTickCount();
		}

		// Don't roll the final resting score
		if (scores.size() > 0)
		{
			format = rollFormat;
			colour = Colour::Orange;
		}
	}

	std::stringstream s;
	s << std::setw(6) << std::setfill('0') << score; 	renderer->Text(x, y, s.str(), colour, format);
}

First of all, if the deque is empty, then there is no rolling animation going on at the moment so we just display the score as normal.

If the deque contains items, we first check if a specific time (in this case 40ms) has elapsed since we last popped a value off the front and updated the displayed number. If not, no changes are made, otherwise we get the next value to display from the front of the deque, remove it, store it in score and reset the timer to run for another 40ms displaying this new value.

Notice that when the final item is removed from the deque, the inner if condition becomes false and the animation stops. If we didn’t do this, adding small scores where we don’t want any animation would cause a brief flicker as the rolling animation runs for a single item – so we never animate when the deque only contained one item.

Plugging all of this into the main game code requires only trivial changes:

To the per-game start code:

// No score yet
score = 0;
scoreDisplay.UpdateScore(score);

In the main rendering code, the previous score display code is replaced with:

scoreDisplay.Draw();

When points are scored we call UpdateScore():

// returns true if points have been scored
if (bucket.UpdateBucket(score, level, linesCleared))
{
...
	scoreDisplay.UpdateScore(score);
}

And the rolling score animation is complete!

Animating the removal of cleared lines (intrinsic animation)

Goal: When the player clears a line in Tetris, we want to animate the line clearance with some kind of fill or fade before the line disappears and the pieces move down.

Although this sounds simple, it is actually really awkward because it requires changes to the game logic. Here is the problem:

  • We cannot have a situation where there are one or more empty lines on a Tetris grid because this is an illegal game state.
  • We cannot move the pieces down in memory and fake their position on-screen because this will break the collision detection.
  • We cannot allow a new piece to fall while the animation is taking place because it may collide with another shape at the top of the bucket and cause the game to end unfairly.

The solution is to essentially pause the flow of the game (without actually entering the player-triggered pause state) until the animation completes.

First, in the existing game flow, the main rendering code is always rendering the current shape, and there always is a current shape. As soon as one shape comes to rest in the bucket, the next one is immediately generated and becomes the new current shape. We must change the code so that a new shape is not generated until any line clear animation has finished, but as a result of this, the current shape will remain as the one that just landed until after the animation, so we must prevent it from being rendered, updated or allowing the user to control it. To do this, we add a flag to TetrisShape to indicate whether the shape is active or not, and set it to false when a collision occurs. Here is a summary of the changes:

// Set a shape layout and its default settings
void TetrisShape::SetShape(TetrisPieceType &piece)
{
	// Copy shape information (layout, colour etc.)
	Piece = piece;

	// Set initial position and rotation
	MoveToTop();
	Active = false;
}

// Bring a piece into play
void TetrisShape::Activate()
{
	LastMoveTime = GetTickCount();
	Active = true;
}

// Move the piece down one place if enough time has elapsed
bool TetrisShape::Update(int level)
{
	if (!Active)
		return false;
...
}

// Draw a tetris piece
void TetrisShape::Draw(bool ignoreBucketHeight, bool silhouette)
{
	if (!Active)
		return;
...
}

// Draw a tetris piece's silhouette (at the lowest possible position it could fall)
void TetrisShape::DrawSilhouette()
{
	if (!Active)
		return;

	int oldPosY = PosY;

	MoveToBottom();

	// Making the shape hit the bottom of the bucket will cause it to be turned off
	// so turn it back on here
	Active = true;

	Draw(false, true);

	PosY = oldPosY;
}

...
// Return true if the piece cannot move any further, triggering the launch of a new piece
bool TetrisShape::MoveDown()
{
	bool collision = isAtBottom();

	if (!collision)
	{
	...
	}

	Active = !collision;

	return collision;
}

And in the user input management code:

if (currentShape.Active)
{
	if (key == gameKeys[TetrisControls::Left])
		currentShape.MoveLeft();

	if (key == gameKeys[TetrisControls::Right])
		currentShape.MoveRight();
...
}

Note that in the silhouette rendering, we find the silhouette position by arbitrarily moving the shape as far down as it will go. This will always trigger a collision that will de-activate the piece, so we have to turn it back on as a fudge (the piece is then returned to its proper position after rendering the silhouette).

This deals with effectively shutting a piece off once it collides with the bucket or another shape in it. We now add two new flags to TetrisBucket called animating and wantNewPiece. When a shape lands and is de-activated, we check for line clears. If there are none, we set wantNewPiece to true and the game continues as normal. Otherwise we set wantNewPiece to false and animating to true to trigger the line clear animation – creating a state where the generation of new shapes is temporarily suspended. wantNewPiece is returned by UpdateBucket() which is called each frame by the main game world update code, and set to false if it was previously true to prevent the rapid creation of many shapes. The main game flow now looks like this:

In the world update code:

// Update the current shape position only if the game is in progress
if (gameState == Playing)
{
	if (currentShape.Update(level))
		placePiece();

	// Update bucket (if during line clear animation) and check if we need to bring in a new piece
	if (bucket.UpdateBucket(score, level, linesCleared))
	{
		newPiece();
		scoreDisplay.UpdateScore(score);
	}
}

In the user input code:

if (key == gameKeys[TetrisControls::Down])
	if (currentShape.MoveDown())
		placePiece();

if (key == gameKeys[TetrisControls::FastDrop])
{
	currentShape.MoveToBottom();
	placePiece();
}

Other than checking if the piece is active, TetrisShape::Update() remains unchanged from the original code, and returns true if the piece has just landed, causing this code to be executed:

// Place the current piece into its final place in the bucket
void SimpleTetris::placePiece()
{
	bucket.Add(currentShape);
}

...

TetrisBucket::Add() previously placed the piece in the board, counted the number of filled lines (and which ones), re-ordered the bucket to remove them and updated the player’s score and level. The logic now changes: we still place the piece and detect filled lines, but now we store a list of filled lines in an array to be used in animation. If any lines have been filled, we trigger the animation and set animating to true, otherwise we set wantNewPiece to true if no lines have been filled and the game proceeds normally, with a new piece being immediately generated and activated (this happens on the next frame when UpdateBucket() returns true, causing newPiece() to execute, which remains unchanged from before).

class TetrisBucket
{
...
	// List of filled lines we are currently animating
	int filledLines[4];

	// Animation helper for cleared lines
	Animation fillAnimation;
	bool animating;

	// True if we want to request a new piece to be brought into play
	bool wantNewPiece;
...
};

...

// Initial set up of the bucket
void TetrisBucket::Init(SimpleTetris *r)
{
	// Set up animations
	fillAnimation = Animation(Animations::Linear, 250, 1, 0, Animation::Clamp);
...
}

...

// Add a shape to the bucket
void TetrisBucket::Add(TetrisShape &s)
{
	// We already know the shape is in a valid position so there is no chance of out-of-bounds indexing here
	// Therefore we can just copy it straight into the bucket array

	for (int y = 0; y < 4; y++)
		for (int x = 0; x < 4; x++)
			board[s.PosY + y][s.PosX + x] = (s.Piece.Shape[s.CurrentRotation][y][x]? s.Piece.Index : board[s.PosY + y][s.PosX + x]);

	// Check for filled lines (rows)
	int newFills = 0;

	for (int y = 0, fills = 0; y < BucketSizeY; y++, fills = 0)
	{
		for (int x = 0; x < BucketSizeX; x++)
			if (board[y][x] != -1)
				fills++;

		// Remember filled line (we have to animate it before we can update the grid)
		if (fills == BucketSizeX)
			filledLines[newFills++] = y;
	}

	// Begin clearance animation and don't ask for another piece yet
	if (newFills > 0)
	{
		fillAnimation.Reset();
		animating = true;

		// Setup background score animations
		renderer->AddScoreAnimation(s, renderer->CalculatePoints(newFills));
	}

	// No lines filled - get another piece immediately
	else
		wantNewPiece = true;
}

The dirty work now goes in UpdateBucket which essentially does nothing if there is no animation in progress, but when a line clearance animation ends, it takes the responsibility of updating the bucket to remove empty rows, the player’s level and score and turns off the animation, setting wantNewPiece to true and animating to false.

bool TetrisBucket::UpdateBucket(int &score, int &level, int &linesCleared)
{
	// End of animation sequence
	if (animating)
	{
		if (fillAnimation.GetAnimOffset() == 1.0f)
		{
			int newFills = 0;

			for (int i = 0; i < 4; i++)
				if (filledLines[i] != -1)
				{
					int y = filledLines[i];

					// Line is cleared, move the grid down
					// the line y will now become empty, and all y-i must become y+1-i
					for (int ny = y; ny >= 1; ny--)
						for (int nx = 0; nx < BucketSizeX; nx++)
							board[ny][nx] = board[ny - 1][nx];

					// Clear the top row
					for (int nx = 0; nx < BucketSizeX; nx++)
						board[0][nx] = -1;

					filledLines[i] = -1;
					newFills++;
				}

			// New level?
			if (linesCleared / SimpleTetris::LinesPerLevel < (linesCleared + newFills) / SimpleTetris::LinesPerLevel)
			{
				level++;

				// Make tracers go faster!
				tracerAnimation.SetInterval(33500 - level * 1500);
			}

			// Update score
			linesCleared += newFills;
			score += renderer->CalculatePoints(newFills);

			// Request new piece
			animating = false;
			wantNewPiece = true;
		}
	}

	if (!wantNewPiece)
		return false;

	wantNewPiece = false;
	return true;
}

In this way, the bucket does indeed contain empty rows while the animation is in progress, but no new shapes are generated and the player has no control over any shape until the animation ends. The player’s score and level are also not updated until the animation ends, although that is the designer’s option.

The final task is to actually render the animation. We store a list of cleared lines in filledLines[] earlier because we need to know which rows are being cleared; there can be more than one row, and they do not necessarily have to be consecutive. We put this array into use in the rendering code inside TetrisBucket::Draw():

// Clearance animations
if (animating)
{
	renderer->SetBrush(Colour::White);
	renderer->CurrentBrush->SetOpacity(static_cast<FLOAT>(fillAnimation.GetAnimOffset()));

	for (int i = 0; i < 4; i++)
		if (filledLines[i] != -1)
			renderer->FillRectangleWH(LeftSide, TopSide + filledLines[i] * SimpleTetris::BlockSize, BucketSizeX * SimpleTetris::BlockSize, SimpleTetris::BlockSize);
}

What we are doing here is plotting a rectangle over the top of each filled line (up to four – the maximum amount it’s possible to clear in the placement of a single piece), starting with an alpha level of 0 (completely transparent) and ending 250ms later with a level of 1 (fully opaque). It then abruptly disappears, taking the empty row with it.

When each new game starts, we’ll want to make sure all of this stuff is reset, so we call this function once per game:

// Empty the bucket
void TetrisBucket::Reset()
{
	for (int y = 0; y < BucketSizeY; y++)
		for (int x = 0; x < BucketSizeX; x++)
			board[y][x] = -1;

	for (int i = 0; i < 4; i++)
		filledLines[i] = -1;

	// Not currently animating
	animating = false;

	// No new piece needed
	wantNewPiece = false;

	// Reset tracer animation
	tracerAnimation.SetInterval(32000);
	tracerAnimation.Reset();
}

Figure 2. Before the application of graphical tweaks

Non-animation changes

There are a few simple graphical changes we can implement to make our game a bit more professional and user-friendly, so let’s finish by taking a look at these.

Graduated fills

In the previous version of Tetris, the blocks were rendered filled with a flat colour. Changing them to use a graduated fill gives them a nice fake 3D appearance, and the change is extremely simple. We just modify the piece colours when the application starts:

Before:

// Set colours for each piece type
Pieces[0].ShapeColour = Colour::Blue;
Pieces[1].ShapeColour = Colour::LightBlue;
Pieces[2].ShapeColour = Colour::LightSeaGreen;
Pieces[3].ShapeColour = Colour::Yellow;
Pieces[4].ShapeColour = Colour::Red;
Pieces[5].ShapeColour = Colour::Azure;
Pieces[6].ShapeColour = Colour::Violet;

After:

Pieces[0].ShapeColour = MakeBrush(Colour::Blue, Colour::Snow);
Pieces[1].ShapeColour = MakeBrush(Colour::Cyan, Colour::Snow);
Pieces[2].ShapeColour = MakeBrush(Colour::Green, Colour::Snow);
Pieces[3].ShapeColour = MakeBrush(Colour::Yellow, Colour::Snow);
Pieces[4].ShapeColour = MakeBrush(Colour::Red, Colour::Snow);
Pieces[5].ShapeColour = MakeBrush(Colour::Magenta, Colour::Snow);
Pieces[6].ShapeColour = MakeBrush(Colour::Chocolate, Colour::Snow);

The two-parameter version of Simple2D’s MakeBrush() creates an even, vertical graduated fill between the two specified colours (if you want a horizontal, diagonal or custom fill, the 3rd parameter can be used to specify this). No other code changes are needed, we are simply changing the Direct2D brushes used to render the blocks.

Using curved geometry instead of rectangles

Previously we rendered the bucket as three rectangles to form the sides and base, like this:

// Left side of bucket
renderer->FillRectangleWH(LeftSide - BucketWidth, TopSide, BucketWidth, BucketSizeY * SimpleTetris::BlockSize + BucketWidth);

// Right side of bucket
renderer->FillRectangleWH(LeftSide + BucketSizeX * SimpleTetris::BlockSize,
			TopSide, BucketWidth,
			BucketSizeY * SimpleTetris::BlockSize + BucketWidth);

// Bottom of bucket
renderer->FillRectangleWH(LeftSide,
			TopSide + BucketSizeY * SimpleTetris::BlockSize,
			BucketSizeX * SimpleTetris::BlockSize,
			BucketWidth);

We dispense with this code, and in the application startup create a geometric path to draw the bucket instead. This allows us to make it any arbitrary shape we want. In this case, we just add curves to the bucket corners to make it look more professional:

// Build the bucket geometry
GeometryData g = renderer->StartCreatePath(LeftSide - BucketWidth, TopSide);

#pragma warning ( disable : 4244 )
g->AddLine(D2D1::Point2F(LeftSide - BucketWidth, TopSide + BucketSizeY * SimpleTetris::BlockSize));
g->AddArc(D2D1::ArcSegment(D2D1::Point2F(LeftSide, TopSide + BucketSizeY * SimpleTetris::BlockSize + BucketWidth),
			D2D1::SizeF(BucketWidth, BucketWidth), 0.0f, D2D1_SWEEP_DIRECTION_COUNTER_CLOCKWISE, D2D1_ARC_SIZE_SMALL));
g->AddLine(D2D1::Point2F(LeftSide + BucketSizeX * SimpleTetris::BlockSize, TopSide + BucketSizeY * SimpleTetris::BlockSize + BucketWidth));
g->AddArc(D2D1::ArcSegment(D2D1::Point2F(LeftSide + BucketSizeX * SimpleTetris::BlockSize + BucketWidth, TopSide + BucketSizeY * SimpleTetris::BlockSize),
			D2D1::SizeF(BucketWidth, BucketWidth), 0.0f, D2D1_SWEEP_DIRECTION_COUNTER_CLOCKWISE, D2D1_ARC_SIZE_SMALL));

D2D1_POINT_2F bp[] = {
	{ LeftSide + BucketSizeX * SimpleTetris::BlockSize + BucketWidth,		TopSide											},
	{ LeftSide + BucketSizeX * SimpleTetris::BlockSize,						TopSide											},
	{ LeftSide + BucketSizeX * SimpleTetris::BlockSize,						TopSide + BucketSizeY * SimpleTetris::BlockSize },
	{ LeftSide,																TopSide + BucketSizeY * SimpleTetris::BlockSize },
	{ LeftSide,																TopSide											}
};
#pragma warning ( default : 4244 )

g->AddLines(bp, 5);

bucketGeometry = renderer->EndCreatePath();

First we create a new geometry with the starting point at the top-left of the bucket. The next four lines add the left-side, bottom left corner (which we define as an arc), bottom-side and botto right corner (another arc). The rest of the geometry is just straight lines to fill the inside of the bucket, so we just use a shortcut of placing the list of points in an array and adding them all to the geometry in one go with AddLines().

The bucket rendering code then becomes very simple:

renderer->SetBrush(Colour::CornflowerBlue);

bucketGeometry.Draw();

renderer->SetBrush(bg);

bucketGeometry.Fill();

We first draw the bucket outline, then the bucket fill, using two different brushes so that the outline can be a different colour to the interior.

Dimming the screen when the game is paused or over

The idea behind this is to make the overlay text of ‘Paused’ or ‘Game Over’ easier to read. It is achieved simply by drawing a black rectangle covering the entire screen, but with the alpha (transparency level) set to a fairly high value so that instead of just painting the screen black, the rectangle is partially transparent, causing the game scene behind it to be dimmed slightly:

if (gameState == GameOver)
{
	// Alpha blend a black rectangle at half transparency over the whole screen to dim the game view
	SetBrush(Colour::Black);
	CurrentBrush->SetOpacity(0.5f);
	FillRectangleWH(0, 0, ResolutionX, ResolutionY);

Figure 3. After the application of graphical tweaks

The End

And there you have it, we have now added a bunch of cool-looking simple but effective animations to our game to give it a more polished look. Now that wasn’t so traumatic was it? 🙂 (I jest)

Next time, we’ll look at adding background images, music, sound effects and an intro sequence to our game to round off the single-player experience.

Share your thoughts! Note: to post source code, enclose it in [code lang=...] [/code] tags. Valid values for 'lang' are cpp, csharp, xml, javascript, php etc. To post compiler errors or other text that is best read monospaced, use 'text' as the value for lang.

This site uses Akismet to reduce spam. Learn how your comment data is processed.