Some context:
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.
My code:
#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 this 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…
Any advice or critic will be much appreciated and welcome. Don’t feel the need to hold back, harsh criticism is precisely what I’m looking for. Thanks in advance to any anyone who takes the time to look into this question.