Falling Blocks Part 2: cSquare and cBlock
cSquare.h
We need a class to represent individual squares for a few reasons. For one, our blocks are composed of squares. Also, any part of our block can collide with something else so it's nice to be able to check each square for collisions separately. Most importantly though, when our focus block hits the bottom of the game area, it becomes a part of the square pile. Parts of this pile can be cleared and the squares above the cleared squares will move down. This completely destroys the shape of our blocks. For this reason, we can't just store everything as a block.
Add a file called "cSquare.h" to your project and then add the following code:
#pragma once
#include "Enums.h"
#include "Defines.h"
class cSquare
{
private:
public:
};
Note that we will be using values from both "Enums.h" and "Defines.h" in this class.
Now we need to determine the data that this class needs to store. As previously discussed, we will be storing the center of our square so we need variables to track its x and y locations. Each type of block in our game will be a different color, so we need to know what type of block our square belongs to. We'll also need a pointer to our bitmap surface so we can draw our square. Add the following to the private section of cSquare:
// Location of the center of the square
int m_CenterX;
int m_CenterY;
// Type of block. Needed to locate the correct square in our bitmap
BlockType m_BlockType;
// A pointer to our bitmap surface from "Main.cpp"
SDL_Surface* m_Bitmap;
The constructor for this class will be very simple. We'll just have it initialize our data members. We will also need to add a default constructor to our class because our compiler will expect it. If we declare pointers to cSquare objects and the compiler can't find a default constructor, it will issue error messages. Add the following to the public section of cSquare:
// Default constructor, your compiler will probably require this
cSquare()
{
}
// Main constructor takes location and type of block, and a pointer to our bitmap surface.
cSquare(int x, int y, SDL_Surface* bitmap, BlockType type) : m_CenterX(x), m_CenterY(y),
m_Bitmap(bitmap), m_BlockType(type)
{
}
The only functionality we'll really need for our squares is the ability to draw and move them. Let's start with the Draw() method.
We'll be calling SDL_BlitSurface() which requires a pointer to our window surface so we should have our function take a pointer to our window surface so we can pass it in. The function will actually be quite simple. It will determine which type of block our square belongs to, find the appropriate colored square in our bitmap surface, and then draw the square at its current location. Add the following to the public section of cSquare:
// Draw() takes a pointer to the surface to draw to (our window)
void Draw(SDL_Surface* window)
{
SDL_Rect source;
// switch statement to determine the location of the square within our bitmap
switch (m_BlockType)
{
case SQUARE_BLOCK:
{
SDL_Rect temp = { RED_SQUARE_X, RED_SQUARE_Y, SQUARE_MEDIAN * 2,
SQUARE_MEDIAN * 2 };
source = temp;
} break;
case T_BLOCK:
{
SDL_Rect temp = { PURPLE_SQUARE_X, PURPLE_SQUARE_Y, SQUARE_MEDIAN * 2,
SQUARE_MEDIAN * 2 };
source = temp;
} break;
case L_BLOCK:
{
SDL_Rect temp = { GREY_SQUARE_X, GREY_SQUARE_Y, SQUARE_MEDIAN * 2,
SQUARE_MEDIAN * 2 };
source = temp;
} break;
case BACKWARDS_L_BLOCK:
{
SDL_Rect temp = { BLUE_SQUARE_X, BLUE_SQUARE_Y, SQUARE_MEDIAN * 2,
SQUARE_MEDIAN * 2 };
source = temp;
} break;
case STRAIGHT_BLOCK:
{
SDL_Rect temp = { GREEN_SQUARE_X, GREEN_SQUARE_Y, SQUARE_MEDIAN * 2,
SQUARE_MEDIAN * 2 };
source = temp;
} break;
case S_BLOCK:
{
SDL_Rect temp = { BLACK_SQUARE_X, BLACK_SQUARE_Y, SQUARE_MEDIAN * 2,
SQUARE_MEDIAN * 2 };
source = temp;
} break;
case BACKWARDS_S_BLOCK:
{
SDL_Rect temp = { YELLOW_SQUARE_X, YELLOW_SQUARE_Y, SQUARE_MEDIAN * 2,
SQUARE_MEDIAN * 2 };
source = temp;
} break;
}
// Draw at square's current location. Remember that m_X and m_Y store the
// center of the square.
SDL_Rect destination = { m_CenterX - SQUARE_MEDIAN, m_CenterY - SQUARE_MEDIAN,
SQUARE_MEDIAN * 2, SQUARE_MEDIAN * 2 };
SDL_BlitSurface(m_Bitmap, &source, window, &destination);
}
Move() is also very simple. We just determine which direction our square is moving and change the appropriate data member. Add the following to the public section of cSquare:
// Remember, SQUARE_MEDIAN represents the distance from the square's center to
// its sides. SQUARE_MEDIAN*2 gives us the width and height of our squares.
void Move(Direction dir)
{
switch (dir)
{
case LEFT:
{
m_CenterX -= SQUARE_MEDIAN * 2;
} break;
case RIGHT:
{
m_CenterX += SQUARE_MEDIAN * 2;
} break;
case DOWN:
{
m_CenterY += SQUARE_MEDIAN*2;
} break;
}
}
To finish, let's add some accessor and mutator methods for our location variables:
// Accessors
int GetCenterX() { return m_CenterX; }
int GetCenterY() { return m_CenterY; }
// Mutators
void SetCenterX(int x) { m_CenterX = x; }
void SetCenterY(int y) { m_CenterY = y; }
cBlock.h
Our cBlock class will store its center, type, and the four squares that it's built from. Note that we'll consider a block's center to be the point about which we will rotate its squares. Our constructor will simply initialize the block's data members and call a function that sets up the squares. We'll get to this function in a moment. For now, add the following to "cBlock.h":
#pragma once
#include "cSquare.h"
class cBlock
{
private:
// Location of the center of the block
int m_CenterX;
int m_CenterY;
// Type of block
BlockType m_Type;
// Array of pointers to the squares that make up the block
cSquare* m_Squares[4];
public:
// The constructor just sets the block location and calls SetupSquares
cBlock(int x, int y, SDL_Surface* bitmap, BlockType type) : m_CenterX(x), m_CenterY(y),
m_Type(type)
{
// Set our square pointers to null
for (int i=0; i<4; i++)
{
m_Squares[i] = NULL;
}
SetupSquares(x, y, bitmap);
}
};
SetupSquares() initializes the locations of the block's squares with respect to its center. It takes the center point of the block so we can move it to somewhere else on the screen and reset its shape. It also takes a pointer to our bitmap, which we'll pass to cSquare's constructor.
If you find the following code hard to follow, don't worry about it. I'm sure you could manage setting up the shapes of the blocks on your own. It's usually a bit trickier figuring out what someone else is doing. Anyways, add the following to the public section of cBlock:
// Setup our block according to its location and type. Note that the squares
// are defined according to their distance from the block's center. This
// function takes a surface that gets passed to cSquare's constructor.
void SetupSquares(int x, int y, SDL_Surface* bitmap)
{
// This function takes the center location of the block. We set our data
// members to these values to make sure our squares don't get defined
// around a new center without our block's center values changing too.
m_CenterX = x;
m_CenterY = y;
// Make sure that any current squares are deleted
for (int i=0; i<4; i++)
{
if (m_Squares[i])
delete m_Squares[i];
}
switch (m_Type)
{
case SQUARE_BLOCK:
{
// [0][2]
// [1][3]
m_Squares[0] = new cSquare(x - SQUARE_MEDIAN, y - SQUARE_MEDIAN,
bitmap, m_Type);
m_Squares[1] = new cSquare(x - SQUARE_MEDIAN, y + SQUARE_MEDIAN,
bitmap, m_Type);
m_Squares[2] = new cSquare(x + SQUARE_MEDIAN, y - SQUARE_MEDIAN,
bitmap, m_Type);
m_Squares[3] = new cSquare(x + SQUARE_MEDIAN, y + SQUARE_MEDIAN,
bitmap, m_Type);
} break;
case T_BLOCK:
{
// [0]
// [2][1][3]
m_Squares[0] = new cSquare(x + SQUARE_MEDIAN, y - SQUARE_MEDIAN,
bitmap, m_Type);
m_Squares[1] = new cSquare(x + SQUARE_MEDIAN, y + SQUARE_MEDIAN,
bitmap, m_Type);
m_Squares[2] = new cSquare(x - SQUARE_MEDIAN, y + SQUARE_MEDIAN,
bitmap, m_Type);
m_Squares[3] = new cSquare(x + (SQUARE_MEDIAN * 3), y + SQUARE_MEDIAN,
bitmap, m_Type);
} break;
case L_BLOCK:
{
// [0]
// [1]
// [2][3]
m_Squares[0] = new cSquare(x - SQUARE_MEDIAN, y - SQUARE_MEDIAN,
bitmap, m_Type);
m_Squares[1] = new cSquare(x - SQUARE_MEDIAN, y + SQUARE_MEDIAN,
bitmap, m_Type);
m_Squares[2] = new cSquare(x - SQUARE_MEDIAN, y + (SQUARE_MEDIAN * 3),
bitmap, m_Type);
m_Squares[3] = new cSquare(x + SQUARE_MEDIAN, y + (SQUARE_MEDIAN * 3),
bitmap, m_Type);
} break;
case BACKWARDS_L_BLOCK:
{
// [0]
// [1]
// [3][2]
m_Squares[0] = new cSquare(x + SQUARE_MEDIAN, y - SQUARE_MEDIAN,
bitmap, m_Type);
m_Squares[1] = new cSquare(x + SQUARE_MEDIAN, y + SQUARE_MEDIAN,
bitmap, m_Type);
m_Squares[2] = new cSquare(x + SQUARE_MEDIAN, y + (SQUARE_MEDIAN * 3),
bitmap, m_Type);
m_Squares[3] = new cSquare(x - SQUARE_MEDIAN, y + (SQUARE_MEDIAN * 3),
bitmap, m_Type);
} break;
case STRAIGHT_BLOCK:
{
// [0]
// [1]
// [2]
// [3]
m_Squares[0] = new cSquare(x + SQUARE_MEDIAN, y - (SQUARE_MEDIAN * 3),
bitmap, m_Type);
m_Squares[1] = new cSquare(x + SQUARE_MEDIAN, y - SQUARE_MEDIAN,
bitmap, m_Type);
m_Squares[2] = new cSquare(x + SQUARE_MEDIAN, y + SQUARE_MEDIAN,
bitmap, m_Type);
m_Squares[3] = new cSquare(x + SQUARE_MEDIAN, y + (SQUARE_MEDIAN * 3),
bitmap, m_Type);
} break;
case S_BLOCK:
{
// [1][0]
// [3][2]
m_Squares[0] = new cSquare(x + (SQUARE_MEDIAN * 3), y - SQUARE_MEDIAN,
bitmap, m_Type);
m_Squares[1] = new cSquare(x + SQUARE_MEDIAN, y - SQUARE_MEDIAN,
bitmap, m_Type);
m_Squares[2] = new cSquare(x + SQUARE_MEDIAN, y + SQUARE_MEDIAN,
bitmap, m_Type);
m_Squares[3] = new cSquare(x - SQUARE_MEDIAN, y + SQUARE_MEDIAN,
bitmap, m_Type);
} break;
case BACKWARDS_S_BLOCK:
{
// [0][1]
// [2][3]
m_Squares[0] = new cSquare(x - SQUARE_MEDIAN, y - SQUARE_MEDIAN,
bitmap, m_Type);
m_Squares[1] = new cSquare(x + SQUARE_MEDIAN, y - SQUARE_MEDIAN,
bitmap, m_Type);
m_Squares[2] = new cSquare(x + SQUARE_MEDIAN, y + SQUARE_MEDIAN,
bitmap, m_Type);
m_Squares[3] = new cSquare(x + (SQUARE_MEDIAN * 3), y + SQUARE_MEDIAN,
bitmap, m_Type);
} break;
}
}
cBlock's Draw() method simply calls the Draw() methods of its squares. Its Move() method just changes its center variables and then calls its squares' Move() methods. Add the following to the public section of cBlock:
// Draw() simply iterates through the squares and calls their Draw() functions.
void Draw(SDL_Surface* Window)
{
for (int i=0; i<4; i++)
{
m_Squares[i]->Draw(Window);
}
}
// Move() simply changes the block's center and calls the squares' move functions.
void Move(Direction dir)
{
switch (dir)
{
case LEFT:
{
m_CenterX -= SQUARE_MEDIAN * 2;
} break;
case RIGHT:
{
m_CenterX += SQUARE_MEDIAN * 2;
} break;
case DOWN:
{
m_CenterY += SQUARE_MEDIAN*2;
} break;
}
for (int i=0; i<4; i++)
{
m_Squares[i]->Move(dir);
}
}
cBlock's Rotate() function might be a little hard to explain without a bit of linear algebra, but I'll do my best. To rotate a point around the origin (0,0) by a given angle, we use the following formula:
x = x*cos(angle) - y*sin(angle);
y = x*sin(angle) + y*cos(angle);
We can simplify this formula however, because we'll always be rotating the block by 90 degrees. cos(90) is zero, and sin(90) is one. Our formula simplifies to:
x = -y;
y = x;
Quite a bit more manageable if you ask me. Don't get too excited though, if we plug this formula into our code right now, our blocks will wind up rotating around the top left of our window. This is because that is where the origin is; (0,0) refers to the top left of a window.
What we want is for our block to rotate around its center. We know where our center is because we store it in m_CenterX and m_CenterY, but how do we make our squares rotate around that center?
First, we subtract the center of the square we want to rotate by the center of our block. This places the center of our block at (0,0). Because the center of our block is now at the origin, our rotation will work properly. After we rotate, we just add the center of our block to the center of our square, which puts it right back to where it was.
If you need to visualize this, image a block in the middle of the screen. If you could watch the entire rotation, you would see the block move to the top left of the window, rotate around the origin (it would rotate around itself in this case, because its center is at the origin), then move back to the middle of the screen.
Add the following to the public section of cBlock:
void Rotate()
{
// We need two sets of temporary variables so we don't incorrectly
// alter one of them.If we set x to -y, then set y to x, we'd actually
// be setting y to -y because x is now -y
int x1, y1, x2, y2;
for (int i=0; i<4; i++)
{
// Get the center of the current square
x1 = m_Squares[i]->GetCenterX();
y1 = m_Squares[i]->GetCenterY();
// Move the square so it's positioned at the origin
x1 -= m_CenterX;
y1 -= m_CenterY;
// Do the actual rotation
x2 = - y1;
y2 = x1;
// Move the square back to its proper location
x2 += m_CenterX;
y2 += m_CenterY;
// Set the square's location to our temporary variables
m_Squares[i]->SetCenterX(x2);
m_Squares[i]->SetCenterY(y2);
}
}
Before we can rotate a block, we need to check to see if the block will collide with anything. We handle collision detection outside of cBlock, so we need an accesor method that returns the positions of our block's squares after rotation. Note that we wont bother returning the actual squares, just an array containing their locations. This function is very similar to Rotate(). Add the following to the public section of cBlock:
// This function gets the locations of the squares after
// a rotation and returns an array of those values.
int* GetRotatedSquares()
{
int* temp_array = new int[8];
int x1, y1, x2, y2;
for (int i=0; i<4; i++)
{
x1 = m_Squares[i]->GetCenterX();
y1 = m_Squares[i]->GetCenterY();
x1 -= m_CenterX;
y1 -= m_CenterY;
x2 = - y1;
y2 = x1;
x2 += m_CenterX;
y2 += m_CenterY;
// Instead of setting the squares, we just store the values
temp_array[i*2] = x2;
temp_array[i*2+1] = y2;
}
return temp_array;
}
To finish, we just need an accesor method that returns an array of pointers to our squares. Note that the notation ** is required because we are returning a pointer to an array of pointers. Add the following to the public section of cBlock:
// This returns a pointer to an array of pointers to the squares of the block.
cSquare** GetSquares()
{
return m_Squares;
}
Falling Blocks Part 3: Includes, Global Data, and Prototypes