We'll display the rows cleared count on the DOM instead of in the game window itself, so you can delete the ScreenHeight constant from the file. We no longer need additional space to accommodate for the score text:
namespace Constants {
const int BoardColumns = 10;
const int BoardHeight = 720;
const int BoardRows = 20;
const int BoardWidth = 360;
const int Offset = BoardWidth / BoardColumns;
const int PieceSize = 4;
// const int ScreenHeight = BoardHeight + 50; <----- Delete this line
}
No changes need to be made to the Piece class files (piece.cpp/piece.h). However, we will need to update the Board class. Let's start with the header file (board.h). Starting with the bottom and working our way up, let's update the displayScore() function. In the <body> section of the index.html file, there's a <span> element with id="score". We're going to update this element using the emscripten_run_script command to display the current score. As a result, the displayScore() function becomes much shorter. The before and after is shown as follows.
Here is the original version of the Board class's displayScore() function:
void Board::displayScore(SDL_Renderer *renderer, TTF_Font *font) {
std::stringstream message;
message << "ROWS: " << currentScore_;
SDL_Color white = { 255, 255, 255 };
SDL_Surface *surface = TTF_RenderText_Blended(
font,
message.str().c_str(),
white);
SDL_Texture *texture = SDL_CreateTextureFromSurface(
renderer,
surface);
SDL_Rect messageRect{ 20, BoardHeight + 15, surface->w, surface->h };
SDL_FreeSurface(surface);
SDL_RenderCopy(renderer, texture, nullptr, &messageRect);
SDL_DestroyTexture(texture);
}
Here is the ported version of the displayScore() function:
void Board::displayScore(int newScore) {
std::stringstream action;
action << "document.getElementById('score').innerHTML =" << newScore;
emscripten_run_script(action.str().c_str());
}
The emscripten_run_script action simply finds the <span> element on the DOM and sets the innerHTML to the current score. We can't use the EM_ASM() function here because Emscripten doesn't recognize the document object. Since we have access to the private currentScore_ variable in the class, we're going to move the displayScore() call in the draw() function into the unite() function. This limits the amount of calls to displayScore() to ensure that the function is called only when the score has actually changed. We only need to add one line of code to accomplish this. Here's what the unite() function looks like now:
void Board::unite(const Piece &piece) {
for (int column = 0; column < PieceSize; ++column) {
for (int row = 0; row < PieceSize; ++row) {
if (piece.isBlock(column, row)) {
int columnTarget = piece.getColumn() + column;
int rowTarget = piece.getRow() + row;
cells_[columnTarget][rowTarget] = true;
}
}
}
// Continuously loops through each of the rows until no full rows are
// detected and ensures the full rows are collapsed and non-full rows
// are shifted accordingly:
while (areFullRowsPresent()) {
for (int row = BoardRows - 1; row >= 0; --row) {
if (isRowFull(row)) {
updateOffsetRow(row);
currentScore_ += 1;
for (int column = 0; column < BoardColumns; ++column) {
cells_[column][0] = false;
}
}
}
displayScore(currentScore_); // <----- Add this line
}
}
Since we're no longer using the SDL2_ttf library, we can update the draw() function signature and remove the displayScore() function call. Here's the updated draw() function:
void Board::draw(SDL_Renderer *renderer/*, TTF_Font *font */) {
// ^^^^^^^^^^^^^^ <-- Remove this argument
// displayScore(renderer, font); <----- Delete this line
SDL_SetRenderDrawColor(
renderer,
/* Light Gray: */ 140, 140, 140, 255);
for (int column = 0; column < BoardColumns; ++column) {
for (int row = 0; row < BoardRows; ++row) {
if (cells_[column][row]) {
SDL_Rect rect{
column * Offset + 1,
row * Offset + 1,
Offset - 2,
Offset - 2
};
SDL_RenderFillRect(renderer, &rect);
}
}
}
}
The displayScore() function call was removed from the first line of the function and the TTF_Font *font argument was removed as well. Let's add a call to displayScore() in the constructor to ensure that the initial value is set to 0 when the game ends and a new one begins:
Board::Board() : cells_{{ false }}, currentScore_(0) {
displayScore(0); // <----- Add this line
}
That's it for the class file. Since we changed the signatures for the displayScore() and draw() functions, and removed the dependency for SDL2_ttf, we'll need to update the header file. Remove the following lines from board.h:
#ifndef TETRIS_BOARD_H
#define TETRIS_BOARD_H
#include <SDL2/SDL.h>
// #include <SDL2/SDL2_ttf.h> <----- Delete this line
#include "constants.h"
#include "piece.h"
using namespace Constants;
class Board {
public:
Board();
void draw(SDL_Renderer *renderer /*, TTF_Font *font */);
// ^^^^^^^^^^^^^^ <-- Remove this
bool isCollision(const Piece &piece) const;
void unite(const Piece &piece);
private:
bool isRowFull(int row);
bool areFullRowsPresent();
void updateOffsetRow(int fullRow);
void displayScore(SDL_Renderer *renderer, TTF_Font *font);
// ^^^^^^^^^^^^^^ <-- Remove this
bool cells_[BoardColumns][BoardRows];
int currentScore_;
};
#endif // TETRIS_BOARD_H
We're moving right along! The final change we need to make is the also the biggest one. The existing code base has a Game class that manages the application logic and a main.cpp file that calls the Game.loop() function in the main() function. The looping mechanism is a while loop that continues to run as long as the SDL_QUIT event hasn't fired. We need to change our approach to accommodate for Emscripten.
Emscripten provides an emscripten_set_main_loop function that accepts an em_callback_func looping function, fps, and a simulate_infinite_loop flag. We can't include the Game class and pass Game.loop() as the em_callback_func argument, because the build will fail. Instead, we're going to eliminate the Game class completely and move the logic into the main.cpp file. Copy the contents of game.cpp into main.cpp (overwriting the existing contents) and delete the Game class files (game.cpp/game.h). Since we're not declaring a class for Game, remove the Game:: prefixes from the functions. The constructor and destructor are no longer valid (they're no longer part of a class), so we need to move that logic to a different location. We also need to reorder the file to ensure that our called functions come before the calling functions. The final result looks like this:
#include <emscripten/emscripten.h>
#include <SDL2/SDL.h>
#include <stdexcept>
#include "constants.h"
#include "board.h"
#include "piece.h"
using namespace std;
using namespace Constants;
static SDL_Window *window = nullptr;
static SDL_Renderer *renderer = nullptr;
static Piece currentPiece{ static_cast<Piece::Kind>(rand() % 7) };
static Board board;
static int moveTime;
void checkForCollision(const Piece &newPiece) {
if (board.isCollision(newPiece)) {
board.unite(currentPiece);
currentPiece = Piece{ static_cast<Piece::Kind>(rand() % 7) };
if (board.isCollision(currentPiece)) board = Board();
} else {
currentPiece = newPiece;
}
}
void handleKeyEvents(SDL_Event &event) {
Piece newPiece = currentPiece;
switch (event.key.keysym.sym) {
case SDLK_DOWN:
newPiece.move(0, 1);
break;
case SDLK_RIGHT:
newPiece.move(1, 0);
break;
case SDLK_LEFT:
newPiece.move(-1, 0);
break;
case SDLK_UP:
newPiece.rotate();
break;
default:
break;
}
if (!board.isCollision(newPiece)) currentPiece = newPiece;
}
void loop() {
SDL_Event event;
while (SDL_PollEvent(&event)) {
switch (event.type) {
case SDL_KEYDOWN:
handleKeyEvents(event);
break;
case SDL_QUIT:
break;
default:
break;
}
}
SDL_SetRenderDrawColor(renderer, /* Dark Gray: */ 58, 58, 58, 255);
SDL_RenderClear(renderer);
board.draw(renderer);
currentPiece.draw(renderer);
if (SDL_GetTicks() > moveTime) {
moveTime += 1000;
Piece newPiece = currentPiece;
newPiece.move(0, 1);
checkForCollision(newPiece);
}
SDL_RenderPresent(renderer);
}
int main() {
moveTime = SDL_GetTicks();
if (SDL_Init(SDL_INIT_VIDEO) != 0) {
throw std::runtime_error("SDL_Init(SDL_INIT_VIDEO)");
}
SDL_CreateWindowAndRenderer(
BoardWidth,
BoardHeight,
SDL_WINDOW_OPENGL,
&window,
&renderer);
emscripten_set_main_loop(loop, 0, 1);
SDL_DestroyRenderer(renderer);
renderer = nullptr;
SDL_DestroyWindow(window);
window = nullptr;
SDL_Quit();
return 0;
}
The handleKeyEvents() and checkForCollision() functions haven't changed; we simply moved them to the top of the file. The loop() function return type was changed from bool to void as required by emscripten_set_main_loop. Finally, the code from the constructor and destructor was moved into the main() function and any references to SDL2_ttf were removed. Instead of the while statement that called the loop() function of Game, we have the emscripten_set_main_loop(loop, 0, 1) call. We changed the #include statements at the top of the file to accommodate for Emscripten, SDL2, and our Board and Piece classes. That's it for changes — now it's time to configure the build and test out the game.