Let me start by saying that until very recently procedural was the paradigm of choice for about 100% of my programming activity, and I was a complete stranger to C++ and OOP concepts. Since a few weeks ago, I have been studying C++ and today I decided to take some random procedural code and translate it to object oriented design as an exercise. The code in question was an implementation of the classical game Tetris for Windows console.
#include <iostream>
using namespace std;
#include <Windows.h>
#include <thread>
#include <vector>
#define XPADDING 34
#define YPADDING 5
// Screen buffer class
//==============================================================
class Screen
{
public:
Screen(int, int);
const int screenWidth;
const int screenHeight;
wchar_t *screen;
HANDLE hConsole;
DWORD dwBytesWritten;
};
Screen::Screen(int screenWidth, int screenHeight)
: screenWidth(screenWidth), screenHeight(screenHeight)
{
screen = new wchar_t[screenWidth * screenHeight];
for (int i = 0; i < screenWidth * screenHeight; i++) screen[i] = L' ';
hConsole = CreateConsoleScreenBuffer(GENERIC_READ | GENERIC_WRITE, 0, NULL, CONSOLE_TEXTMODE_BUFFER, NULL);
SetConsoleActiveScreenBuffer(hConsole);
dwBytesWritten = 0;
}
// Tetromino Class
//==============================================================
class Tetromino
{
public:
Tetromino(wstring, int, int, int);
int y;
int x;
int rotation;
wstring layout;
int rotate(int, int);
};
Tetromino::Tetromino(wstring layout, int startingX, int startingY, int startingRotation)
: layout(layout), y(startingY), x(startingX), rotation(startingRotation)
{}
int Tetromino::rotate(int x, int y)
{
/*
* Rotates piece layout
* string based on given angle
* 'rotation'
*/
switch (rotation % 4) {
case 0: return y * 4 + x; // 0 degress
case 1: return 12 + y - (x * 4); // 90 degress
case 2: return 15 - (y * 4) - x; // 180 degress
case 3: return 3 - y + (x * 4); // 270 degress
}
return 0;
}
// Playing Field Class
//==============================================================
class PlayingField
{
public:
PlayingField(int, int);
const int fieldWidth;
const int fieldHeight;
unsigned char *pField;
bool doesPieceFit(Tetromino*, int, int, int);
};
PlayingField::PlayingField(int fieldWidth, int fieldHeight)
: fieldWidth(fieldWidth), fieldHeight(fieldHeight), pField(nullptr)
{
// Creating play field buffer
pField = new unsigned char[fieldHeight * fieldWidth];
for (int x = 0; x < fieldWidth; x++)
for (int y = 0; y < fieldHeight; y++)
// 0 characters are spaces and 9 are borders
pField[y * fieldWidth + x] = (x == 0 || x == fieldWidth - 1 || y == fieldHeight - 1) ? 9 : 0;
}
bool PlayingField::doesPieceFit(Tetromino *tetromino, int rotation, int x, int y)
{
for (int px = 0; px < 4; px++)
for (int py = 0; py < 4; py++) {
int pi = tetromino->rotate(px, py);
int fi = (y + py) * fieldWidth + (x + px);
if (x + px >= 0 && x + px < fieldWidth)
if (y + py >= 0 && y + py < fieldHeight)
// if cell value != 0, it's occupied
if (tetromino->layout[pi] == L'X' && pField[fi] != 0)
return false;
}
return true;
}
// Game class
//==============================================================
class Tetris
{
public:
Tetris(Screen*, PlayingField*, int);
bool gameOver;
int score;
void draw();
void checkLines();
void computeNextState();
void lockPieceOnField();
void processInput();
void synchronizeMovement();
private:
int lines;
int speed;
int nextPiece;
int pieceCount;
int currentPiece;
int speedCounter;
bool key[4];
bool forceDown;
bool rotateHold;
Screen *screenBuffer;
Tetromino *tetromino[7];
PlayingField *playingField;
vector<int> fullLines;
};
Tetris::Tetris(Screen *screenBuffer, PlayingField *playingField, int speed)
: speed(speed), screenBuffer(screenBuffer), playingField(playingField)
{
// Set game initial state
score = 0;
lines = 0;
pieceCount = 0;
speedCounter = 0;
gameOver = false;
forceDown = false;
nextPiece = rand() % 7;
currentPiece = rand() % 7;
// Generate pieces
int startingPieceX = playingField->fieldWidth / 2;
tetromino[0] = new Tetromino(L"..X...X...X...X.", startingPieceX, 0, 0);
tetromino[1] = new Tetromino(L"..X..XX...X.....", startingPieceX, 0, 0);
tetromino[2] = new Tetromino(L".....XX..XX.....", startingPieceX, 0, 0);
tetromino[3] = new Tetromino(L"..X..XX..X......", startingPieceX, 0, 0);
tetromino[4] = new Tetromino(L".X...XX...X.....", startingPieceX, 0, 0);
tetromino[5] = new Tetromino(L".X...X...XX.....", startingPieceX, 0, 0);
tetromino[6] = new Tetromino(L"..X...X..XX.....", startingPieceX, 0, 0);
rotateHold = true;
}
void Tetris::synchronizeMovement()
{
// Timing game ticks
this_thread::sleep_for(50ms);
speedCounter++;
forceDown = (speed == speedCounter);
}
void Tetris::processInput()
{
// x27 = right arrow key
// x25 = left arrow key
// x28 = down arrow key
for (int k = 0; k < 4; k++)
key[k] = (0x8000 & GetAsyncKeyState((unsigned char) ("\x27\x25\x28Z"[k]))) != 0;
// Handling input
Tetromino *currentTetromino = tetromino[currentPiece];
currentTetromino->x += (key[0] && playingField->doesPieceFit(currentTetromino, currentTetromino->rotation, currentTetromino->x + 1, currentTetromino->y)) ? 1 : 0;
currentTetromino->x -= (key[1] && playingField->doesPieceFit(currentTetromino, currentTetromino->rotation, currentTetromino->x - 1, currentTetromino->y)) ? 1 : 0;
currentTetromino->y += (key[2] && playingField->doesPieceFit(currentTetromino, currentTetromino->rotation, currentTetromino->x, currentTetromino->y + 1)) ? 1 : 0;
if (key[3]) {
currentTetromino->rotation += (rotateHold && playingField->doesPieceFit(currentTetromino, currentTetromino->rotation + 1, currentTetromino->x, currentTetromino->y)) ? 1 : 0;
rotateHold = false;
} else {
rotateHold = true;
}
}
void Tetris::computeNextState()
{
if (forceDown) {
Tetromino *currentTetromino = tetromino[currentPiece];
if (playingField->doesPieceFit(currentTetromino, currentTetromino->rotation, currentTetromino->x, currentTetromino->y + 1)) {
currentTetromino->y++;
} else {
lockPieceOnField();
// Set up new piece
currentPiece = nextPiece;
nextPiece = rand() % 7;
tetromino[currentPiece]->rotation = 0;
tetromino[currentPiece]->y = 0;
tetromino[currentPiece]->x = playingField->fieldWidth / 2;
// Increse game speed every 10 tics
pieceCount++;
if (pieceCount % 10 == 0)
if (speed >= 10) speed--;
checkLines();
score += 25;
if (!fullLines.empty()) score += (1 << fullLines.size()) * 100;
// Game over if it doesn't fit
gameOver = !playingField->doesPieceFit(tetromino[currentPiece], tetromino[currentPiece]->rotation, tetromino[currentPiece]->x, tetromino[currentPiece]->y);
}
speedCounter = 0;
}
}
void Tetris::lockPieceOnField()
{
Tetromino *currentTetromino = tetromino[currentPiece];
for (int px = 0; px < 4; px++)
for (int py = 0; py < 4; py++)
if (currentTetromino->layout[currentTetromino->rotate(px, py)] == L'X')
// nCurrentPiece + 1 because 0 means empty spots in the playing field
playingField->pField[(currentTetromino->y + py) * playingField->fieldWidth + (currentTetromino->x + px)] = currentPiece + 1;
}
void Tetris::checkLines()
{
Tetromino *currentTetromino = tetromino[currentPiece];
for (int py = 0; py < 4; py++) {
if (currentTetromino->y + py < playingField->fieldHeight - 1) {
bool bLine = true;
for (int px = 1; px < playingField->fieldWidth; px++)
// if any cell is empty, line isn't complete
bLine &= (playingField->pField[(currentTetromino->y + py) * playingField->fieldWidth + px]) != 0;
if (bLine) {
// draw '=' symbols
for (int px = 1; px < playingField->fieldWidth - 1; px++)
playingField->pField[(currentTetromino->y + py) * playingField->fieldWidth + px] = 8;
fullLines.push_back(currentTetromino->y + py);
lines++;
}
}
}
}
void Tetris::draw()
{
// Draw playing field
for (int x = 0; x < playingField->fieldWidth; x++)
for (int y = 0; y < playingField->fieldHeight; y++)
//mapping playing field (' ', 1,..., 9) to Screen characters (' ', A,...,#)
screenBuffer->screen[(y + YPADDING) * screenBuffer->screenWidth + (x + XPADDING)] = L" ABCDEFG=#"[playingField->pField[y * playingField->fieldWidth + x]];
// Draw pieces
for (int px = 0; px < 4; px++)
for (int py = 0; py < 4; py++) {
if (tetromino[currentPiece]->layout[tetromino[currentPiece]->rotate(px, py)] == L'X')
// Drawing current piece ( n + ASCII code of character 'A') 0 -> A, 1 - > B, ...
screenBuffer->screen[(tetromino[currentPiece]->y + py + YPADDING) * screenBuffer->screenWidth + (tetromino[currentPiece]->x + px + XPADDING)] = currentPiece + 65;
if (tetromino[nextPiece]->layout[tetromino[nextPiece]->rotate(px, py)] == L'X')
// Drawing next piece ( n + ASCII code of character 'A') 0 -> A, 1 - > B, ...
screenBuffer->screen[(YPADDING + 3 + py) * screenBuffer->screenWidth + (XPADDING / 2 + px + 3)] = nextPiece + 65;
else
screenBuffer->screen[(YPADDING + 3 + py) * screenBuffer->screenWidth + (XPADDING / 2 + px + 3)] = ' ';
}
swprintf_s(&screenBuffer->screen[YPADDING * screenBuffer->screenWidth + XPADDING / 4], 16, L"SCORE: %8d", score);
swprintf_s(&screenBuffer->screen[(YPADDING + 1) * screenBuffer->screenWidth + XPADDING / 4], 16, L"LINES: %8d", lines);
swprintf_s(&screenBuffer->screen[(YPADDING + 4) * screenBuffer->screenWidth + XPADDING / 4], 13, L"NEXT PIECE: ");
if (!fullLines.empty()) {
WriteConsoleOutputCharacter(screenBuffer->hConsole, screenBuffer->screen, screenBuffer->screenWidth * screenBuffer->screenHeight, {0,0}, &screenBuffer->dwBytesWritten);
this_thread::sleep_for(400ms);
for (auto &v : fullLines)
for (int px = 1; px < playingField->fieldWidth - 1; px++) {
for (int py = v; py > 0; py--)
// clear line, moving lines above one unit down
playingField->pField[py * playingField->fieldWidth + px] = playingField->pField[(py - 1) * playingField->fieldWidth + px];
playingField->pField[px] = 0;
}
fullLines.clear();
}
// Display Frame
WriteConsoleOutputCharacter(screenBuffer->hConsole, screenBuffer->screen, screenBuffer->screenWidth * screenBuffer->screenHeight, {0,0}, &screenBuffer->dwBytesWritten);
}
int main(void){
Screen *screenBuffer = new Screen(80, 30);
PlayingField *playingField = new PlayingField(12, 18);
Tetris *tetrisGame = new Tetris(screenBuffer, playingField, 20);
// Main game loop
while (!tetrisGame->gameOver) {
// Timing
tetrisGame->synchronizeMovement();
// Input
tetrisGame->processInput();
// Logic
tetrisGame->computeNextState();
//Render Output
tetrisGame->draw();
}
CloseHandle(screenBuffer->hConsole);
cout << "Game Over! Score:" << tetrisGame->score << endl;
system("pause");
return 0;
}
Some doubts I had while coding:
Overall code logistics. What would be the best (advised) way of interrelating my class objects? Should I pass references around as member variables (the way I did with my
Tetris
class, it has pointers toscreenBuffer
andplayingField
objects) and make most of the game functionality internal to my objects or make them as independent of one another as possible, bringing all together in my program'smain
function by accessing each object when needed (essentially pulling some of the programs functionality out of my objects)?I'm using the
this
keyword a lot. It sure clutters the code a little bit. I'll go ahead and not use it at all. I wonder if this is ok.Most of these classes don't have anything private. Should I use structures instead?
I should probably split this code into multiple files, one for each class definition.
1 Answer 1
Overall code logistics. What would be the best (advised) way of interrelating my class objects? Should I pass references around as member variables (the way I did with my Tetris class, it has pointers to
screenBuffer
andplayingField
objects) and make most of the game functionality internal to my objects or make them as independent of one another as possible, bringing all together in my program's main function by accessing each object when needed (essentially pulling some of the programs functionality out of my objects)?
I don't feel like an authoritative source enough to answer this one specifically, but if you want my opinion, I'm going to say make them separate. That way, because the objects don't overlap, you have more control at the upper level to do with them what you want. If you want to change how these objects interact, you can change the upper level without messing with the internal representation. If you need a new way for them to interact, you can just make a new method, and then you can keep the old way too, if you want, much easier.
I'm using the this keyword a lot. It sure clutters the code a little bit. I'll go ahead and not use it at all. I wonder if this is ok.
Yeah, that should be okay. The this
keyword is generally to avoid confusion for data member names. So if you had, for example:
class foo {
private:
int x;
public:
void bar() {
int x = 0;
x = 5; // the function version of x
this->x = 5; // the data member "x"
}
};
If you don't have any data members the same name as other variables in your function, you should be good. this
is unnecessary in that case.
Most of these classes don't have anything private. Should I use structures instead?
Private data members don't usually have anything to do with structures, so I'm not sure I understand the question correctly. However, I will say that structures are not a replacement for private data members. I think you misunderstand how the OOP model works.
Generally, private data members are reserved for data that no one else needs to see or access. Only the class will access these members via its own methods. If for some reason you need to access or manipulate these members, you need to make a public interface for them, that is, make special methods tasked with adjusting those private data members.
If I'm understanding you right by "Should I use structures instead?", meaning should you copy your data members to a special structure to pass around, the answer is no. Just pass the object itself around, and let other functions use the public interface you defined for it instead.
I should probably split this code into multiple files, one for each class definition.
This isn't necessary, per say, but yes, it would probably be good to do eventually.
-
\$\begingroup\$ What I mean by the third question is: Since the difference between a class and a struct in C++ are that structs have default public members and bases and classes have default private members and bases, if I don't have anything private in my class, wouldn't it be more natural to use a struct which is public by default? But now that I read it again, I used quite a shady choice of words for that 3rd question, sorry about that. The aspects you pointed make a lot of sense, specially the idea of not overlapping, it would also make my code easier to understand now that I think about it. \$\endgroup\$sage– sage2019年05月31日 03:27:49 +00:00Commented May 31, 2019 at 3:27
-
2\$\begingroup\$ Oh. I got you now. I don't know if I can answer that. I prefer classes still. in case I want to change it later, but I would imagine that's just a style choice. \$\endgroup\$Chipster– Chipster2019年06月01日 20:15:31 +00:00Commented Jun 1, 2019 at 20:15
-
\$\begingroup\$ Final product Console Tetris. After pulling all code responsible for interrelating objects out of my classes, I was left with four simple classes with no overlap and seven or so helper methods responsible for bringing the objects together into the main execution flow of the program. I bundled all these helper methods into a single utility class (this class has internal references for each relevant object), so to make them available under the same namespace, it seemed to be the most organised thing to do (feel free to correct me here). \$\endgroup\$sage– sage2019年06月02日 21:39:00 +00:00Commented Jun 2, 2019 at 21:39
-
\$\begingroup\$ First of all, really cool. You did some really good cleaning up and organization. Second of all, if you're looking for more feed back after taking suggestions, you can ask another question with the revised code if you like. It will make things easier so no one ends up reviewing two different revisions at once. There will be less confusion that way. \$\endgroup\$Chipster– Chipster2019年06月02日 22:29:01 +00:00Commented Jun 2, 2019 at 22:29
-
3\$\begingroup\$ @NorthernSage stackoverflow.com/a/54596/5416291 \$\endgroup\$Summer– Summer2019年06月03日 14:24:13 +00:00Commented Jun 3, 2019 at 14:24
_UNICODE
. Of course this makes your application implementation defined. \$\endgroup\$new
. You should not need anynew
in the code. Simple values likeScreen screenBuffer(80, 30);
andstd::vector
for some other places would improve this code a lot in terms of fixing the memory leaks, making the syntax nicer and the program faster. \$\endgroup\$