I have made a simple sliding puzzle/game of fifteen using SDL. I originally attempted this to learn about game states, but it turned into a general exercise in code organisation and SDL use. Therefore, I would especially appreciate comments regarding organisation and clarity, but general suggestions or comments are very welcome as well.
I should note that there is a bug that occasionally causes the program to remain open (despite being closed according to XCode and Activity Monitor) until you hard reboot the computer. I'm not sure what causes this bug (please tell me if you do), but I'd advise against running the code on your computer!
Posted below are Game.cpp, Gamestate_MainGame.cpp, Gamestate_Menu.cpp, Graphics.cpp and Tile.h. The full source of this project can be found here
Game.cpp
#include <iostream>
#include <random>
#include "Game.h"
#include "Tile.h"
#include "Gamestate.h"
Game::Game() {
quit = false;
}
bool Game::init(GameState* state) {
graphics.setup();
pushState(state);
return true;
}
void Game::loop()
{
while(quit == false)
{
update();
render();
}
quitGame();
}
void Game::update()
{
while(SDL_PollEvent(&event))
{
if(states.size() > 0){
states.back()->handleEvents(event);
}
if(states.size() > 0){
states.back()->update();
}
}
}
void Game::render()
{
if(states.size() > 0)
states.back()->render();
}
void Game::setQuit() {
quit = true;
}
void Game::toggleCatMode() {
if (catMode == false)
catMode = true;
else if (catMode == true)
catMode = false;
}
void Game::pushState(GameState* state) {
states.push_back(state);
if(state->init(&graphics, this) == false)
quit = true;
}
void Game::popState() {
delete states.back();
states.pop_back();
if(states.size() == 0)
quit = true;
}
void Game::quitGame() {
while(states.size() > 0)
{
delete states.back();
states.pop_back();
}
graphics.destroy();
SDL_Quit();
}
Gamestate_MainGame.cpp
#include <iostream>
#include <random>
#include "Gamestate_MainGame.h"
bool MainGame::init(Graphics* graph, Game* g) {
game = g;
graphics = graph;
int shiftamount = 3;
// Fill vector<SDL_Rect> 'positions' with possible positions of n*n tiles & make shadow positions
loadPositions(positions, graphics->gridSize());
loadPositions(shadowPositions, graphics->gridSize(), shiftamount);
// Assign these starting positions to n*n tiles in vector<Tile> 'tiles' & make tile shadows
makeTiles(tiles, positions, Tile::type::button);
makeTiles(shadowTiles, shadowPositions, Tile::type::shadow);
// Audio loading
Mix_OpenAudio(44100, MIX_DEFAULT_FORMAT, 2, 2048);
click = Mix_LoadWAV("assets/hover.wav");
if( click == NULL ) {
std::cerr << "Failed to load beat 'click', error:" << Mix_GetError() << std::endl;
return false;
}
cat1 = Mix_LoadWAV("assets/meow1.wav");
if( cat1 == NULL ) {
std::cerr << "Failed to load beat 'meow', error:" << Mix_GetError() << std::endl;
return false;
}
cat2 = Mix_LoadWAV("assets/meow2.wav");
if( cat2 == NULL ) {
std::cerr << "Failed to load beat 'meow', error:" << Mix_GetError() << std::endl;
return false;
}
cat3 = Mix_LoadWAV("assets/meow3.wav");
if( cat3 == NULL ) {
std::cerr << "Failed to load beat 'meow', error:" << Mix_GetError() << std::endl;
return false;
}
return true;
}
void MainGame::loadPositions(std::vector<SDL_Rect>& positions, const int& gridsize, const int& shiftPx) {
int x = graphics->winPadding();
int y = graphics->winPadding();
for (int i = 0; i < gridsize; ++i) {
y = graphics->winPadding() + i*graphics->tileSize() + i*graphics->tilePadding() + shiftPx;
for (int j = 0; j < gridsize; ++j) {
x = graphics->winPadding() + j*graphics->tileSize() + j*graphics->tilePadding() + shiftPx;
positions.push_back( SDL_Rect{ x, y, graphics->tileSize(), graphics->tileSize() } );
}
}
}
void MainGame::makeTiles(std::vector<Tile>& tiles, const std::vector<SDL_Rect>& positions, const int& tiletype) {
for (int i = 0; i < positions.size(); ++i) {
tiles.push_back(Tile{positions[i], i+1, tiletype}); // start counting from 1 for positions
}
tiles[tiles.size()-1].setTileType(Tile::type::invisible); // last tile should be invisible
}
void MainGame::handleEvents(SDL_Event& event) {
if (event.type == SDL_QUIT)
game->setQuit();
if (event.type == SDL_KEYDOWN) {
if (event.key.keysym.sym == SDLK_ESCAPE)
game->setQuit();
}
if (event.type == SDL_KEYDOWN) {
if (event.key.keysym.sym == SDLK_SPACE)
game->popState();
}
if (event.type == SDL_MOUSEBUTTONDOWN) {
if (gameWin == true)
restart();
else {
int x; int y;
SDL_GetMouseState(&x, &y);
clickedTile = getClickedTile(x, y); // retreives clicked tile number or -1
}
}
}
void MainGame::restart() {
gameWin = false;
clicks = 0;
scrambleTiles(tiles, shadowTiles);
}
void MainGame::update() {
if (isSolved()) {
gameWin = true;
}
else if (clickedTile >= 0) { // if mouse clicked on a tile
if (isNeighbour(tiles[clickedTile], tiles[tiles.size()-1])) {
tiles[clickedTile].swap(tiles[tiles.size()-1]);
shadowTiles[clickedTile].swap(shadowTiles[shadowTiles.size()-1]);
clickedTile = -1; // reset to default
++clicks;
if (game->isCatMode()){
std::random_device rd;
std::mt19937 rng(rd());
std::uniform_int_distribution<int> range(0, 2);
int random = range(rd);
switch(random) {
case 0: Mix_PlayChannel(-1, cat1, 0); break;
case 1: Mix_PlayChannel(-1, cat2, 0); break;
case 2: Mix_PlayChannel(-1, cat3, 0); break;
}
}
else
Mix_PlayChannel(-1, click, 0);
}
}
}
void MainGame::scrambleTiles(std::vector<Tile>& t, std::vector<Tile>& tshadow) {
std::random_device rd;
std::mt19937 rng(rd());
std::uniform_int_distribution<int> tilerange(0, t.size()-2); // don't scramble last ('empty') tile
for (int i = t.size()-2; i >= 0; --i) {
int n = tilerange(rng);
t[i].swap(t[n]);
tshadow[i].swap(tshadow[n]);
}
}
int MainGame::getClickedTile(const int &x, const int &y) {
int tilenum = -1;
for (int i = 0; i < tiles.size(); ++i) {
if (! (x < tiles[i].position().x || x > tiles[i].position().x + graphics->tileSize() ||
y < tiles[i].position().y || y > tiles[i].position().y + graphics->tileSize() ))
tilenum = i ;
}
return tilenum;
}
bool MainGame::isNeighbour(const Tile& a, const Tile& b) {
if (a.posNumber() == b.posNumber() - 1 ||
a.posNumber() == b.posNumber() + 1 ||
a.posNumber() == b.posNumber() + graphics->gridSize() ||
a.posNumber() == b.posNumber() - graphics->gridSize())
return true;
return false;
}
bool MainGame::isSolved() {
int correctTilesN = 0;
for (int i = 0; i < tiles.size(); ++i) {
if (tiles[i].position().x == positions[i].x &&
tiles[i].position().y == positions[i].y)
correctTilesN += 1;
}
if (correctTilesN == graphics->gridSize()*graphics->gridSize())
return true;
return false;
}
void MainGame::render() {
graphics->renderClear();
graphics->drawBoard(shadowTiles);
graphics->drawBoard(tiles, true);
if (game->isCatMode()) {
graphics->renderCat(tiles, positions);
}
graphics->updateClicks(clicks);
graphics->winMessage(gameWin, clicks, "You win! Click anywhere to restart"); // make sure it's not the start of the game
graphics->menuInstruction("Press spacebar for main menu");
graphics->update();
}
MainGame::~MainGame() {
Mix_Quit();
game = NULL;
graphics = NULL;
click = NULL;
cat1 = NULL;
cat2 = NULL;
cat3 = NULL;
}
Gamestate_Menu.cpp
#include "Gamestate_Menu.h"
#include "Gamestate_MainGame.h"
#include <iostream>
bool MenuState::init(Graphics* graph, Game* g){
graphics = graph;
game = g;
int shiftAmount = 6;
// Make menu tiles
loadPositions(positions, gridSize);
makeTiles(tiles, positions, Tile::type::button);
// Make menu 'shadow' tiles
loadPositions(shadowPositions, gridSize, shiftAmount);
makeTiles(shadowTiles, shadowPositions, Tile::type::shadow);
// Load sounds
Mix_OpenAudio(44100, MIX_DEFAULT_FORMAT, 2, 2048);
rollOver = Mix_LoadWAV("assets/hover.wav");
if( rollOver == NULL ) {
std::cerr << "Failed to load beat 'rollover', error:" << Mix_GetError() << std::endl;
return false;
}
clickSound = Mix_LoadWAV("assets/press.wav");
if( clickSound == NULL ) {
std::cerr << "Failed to load beat 'clicksound', error:" << Mix_GetError() << std::endl;
return false;
}
return true;
}
void MenuState::loadPositions(std::vector<SDL_Rect>& shadowPositions, const int& gridsize, const int& shiftPx) {
int x = graphics->winPadding();
int y = graphics->winPadding();
for (int i = 0; i < gridsize; ++i) {
y = graphics->winPadding() + i*tileSize + i*tilePadding + shiftPx;
for (int j = 0; j < gridsize; ++j) {
x = graphics->winPadding() + j*tileSize + j*tilePadding + shiftPx;
shadowPositions.push_back( SDL_Rect{ x, y, tileSize, tileSize } );
}
}
}
void MenuState::makeTiles(std::vector<Tile>& tiles, const std::vector<SDL_Rect>& positions, const int& tileType) {
for (int i = 0; i < positions.size(); ++i)
tiles.push_back(Tile{positions[i], i, tileType});
}
void MenuState::handleEvents(SDL_Event& e) {
if (e.type == SDL_QUIT)
game->setQuit();
if (e.type == SDL_KEYDOWN) {
if (e.key.keysym.sym == SDLK_ESCAPE)
game->setQuit();
}
if (e.type == SDL_MOUSEMOTION) {
int x; int y;
SDL_GetMouseState(&x, &y);
mousePos = getActiveTile(x, y);
}
if (e.type == SDL_MOUSEBUTTONDOWN)
click = true;
}
int MenuState::getActiveTile(const int &x, const int &y) {
int tilenum = -1;
for (int i = tiles.size(); i >= 0; --i) {
if ( !(x < tiles[i].position().x || x > tiles[i].position().x + tiles[i].position().w ||
y < tiles[i].position().y || y > tiles[i].position().y + tiles[i].position().h) )
tilenum = i ;
}
return tilenum;
}
void MenuState::update() {
// if cursor landed on a button
if (mousePos >=0) {
setRollOver();
if (click == true) {
switch(mousePos) {
case 0: graphics->setGridSize(3);
pushButton();
break;
case 1: graphics->setGridSize(4);
pushButton();
break;
case 2: graphics->setGridSize(5);
pushButton();
break;
case 3: graphics->setGridSize(6);
pushButton();
break;
case 4: graphics->setGridSize(7);
pushButton();
break;
case 5: graphics->setGridSize(8);
pushButton();
break;
case 6: pushButton();
break;
case 7: game->pushState(new MainGame);
break;
}
click = false;
}
}
else if (prevMousePos >= 0) {// unset rollover
if (tiles[prevMousePos].tileType() != Tile::type::buttonpressed)
tiles[prevMousePos].setTileType(Tile::type::button);
}
}
void MenuState::setRollOver() {
// Play sound if cursor first landed on button
if (tiles[mousePos].tileType() != Tile::type::rollover &&
tiles[mousePos].tileType() != Tile::type::buttonpressed)
Mix_PlayChannel(-1, rollOver, 0);
// reset all tiles
for (int i = tiles.size(); i >= 0; --i) {
if (tiles[i].tileType() != Tile::type::buttonpressed)
tiles[i].setTileType(Tile::type::button);
}
// set rollover tile
if (tiles[mousePos].tileType() != Tile::type::buttonpressed)
tiles[mousePos].setTileType(Tile::type::rollover);
prevMousePos = mousePos;
}
void MenuState::pushButton() {
// Catmode button can be turned on/off
if (mousePos == 6) {
Mix_PlayChannel(-1, clickSound, 0);
if (game->isCatMode())
liftTile(mousePos);
else
pushTile(mousePos);
game->toggleCatMode();
}
// all other tiles can only be turned on
else if (prevButtonClicked != mousePos) {
Mix_PlayChannel(-1, clickSound, 0);
pushTile(mousePos);
if (prevButtonClicked >= 0) {
liftTile(prevButtonClicked);
}
prevButtonClicked = mousePos;
}
}
void MenuState::pushTile(const int& mousepos) {
tiles[mousepos].setTileShift(2, 2);
tiles[mousepos].setTileType(Tile::type::buttonpressed);
}
void MenuState::liftTile(const int& prevbuttonClicked) {
tiles[prevbuttonClicked].setTileShift(-2, -2);
tiles[prevbuttonClicked].setTileType(Tile::type::button);
}
void MenuState::render() {
graphics->renderClear();
graphics->drawBoard(shadowTiles);
graphics->drawBoard(tiles);
graphics->menuText(tiles, game->isCatMode());
graphics->update();
}
MenuState::~MenuState() {
Mix_Quit();
graphics = NULL;
game = NULL;
rollOver = NULL;
clickSound = NULL;
}
Graphics.cpp
#include "Graphics.h"
#include <iostream>
#include <SDL2_ttf/SDL_ttf.h>
#include <SDL2_image/SDL_image.h>
void Graphics::setup() {
if (SDL_Init(SDL_INIT_EVERYTHING) < 0)
std::cerr << "Error: init" << SDL_GetError() << std::endl;
else {
window = SDL_CreateWindow("Sliding Puzzle", SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED, WINDOW_WIDTH, WINDOW_HEIGHT, SDL_WINDOW_SHOWN);
if (window == nullptr)
std::cerr << "Error: Create window" << SDL_GetError() << std::endl;
else {
renderer = SDL_CreateRenderer(window, -1, SDL_RENDERER_ACCELERATED | SDL_RENDERER_PRESENTVSYNC);
if (renderer == nullptr)
std::cerr << "Error: Create renderer" << SDL_GetError() << std::endl;
else {
if( TTF_Init() == -1 )
std::cerr << "SDL_ttf could not initialise" << TTF_GetError() << std::endl;
else {
cat = IMG_LoadTexture(renderer, "assets/cat2.png");
if (cat == nullptr)
std::cerr << "Error: Load cat texture";
}
}
}
}
}
void Graphics::destroy() {
TTF_Quit();
SDL_DestroyRenderer(renderer);
SDL_DestroyWindow(window);
window = NULL;
renderer = NULL;
tilenumber = NULL;
clicks = NULL;
wintext = NULL;
menutext = NULL;
menuinstr = NULL;
cat = NULL;
}
void Graphics::setGridSize(const int& n) {
grid = n;
rawSize = (WINDOW_WIDTH - 2*WINDOW_PADDING) / grid;
TILE_PADDING = rawSize / 10;
TILE_SIZE = rawSize - TILE_PADDING;
}
void Graphics::drawBoard(const std::vector<Tile>& t, const bool& drawnumber) {
for (int i = 0; i < t.size(); ++i) {
SDL_Rect temp = t[i].position();
if (t[i].tileType() == Tile::type::invisible)
SDL_SetRenderDrawColor(renderer, 163, 118, 172, 1); // bg colour
else if (t[i].tileType() == Tile::type::rollover || t[i].tileType() == Tile::type::buttonpressed)
SDL_SetRenderDrawColor(renderer, 255, 255, 255, 1); // highlight colour
else if (t[i].tileType() == Tile::type::shadow)
SDL_SetRenderDrawColor(renderer, 100, 65, 107, 1); // shadow colour
else
SDL_SetRenderDrawColor(renderer, 237, 229, 239, 1); // default colour
SDL_RenderFillRect(renderer, &temp);
if (drawnumber){
if (t[i].tileType() != Tile::type::invisible) {
std::string num = std::to_string(i+1); // position numbers count from 1
tilenumber = renderText(num, "assets/Calibrib.ttf", fontcolour, 25);
SDL_QueryTexture(tilenumber, NULL, NULL, &temp.w, &temp.h);
renderTexture(tilenumber, temp.x+(TILE_SIZE/2 - temp.w/2), temp.y+(TILE_SIZE/2 - temp.h/2), nullptr);
}
}
}
}
void Graphics::renderCat(const std::vector<Tile>& tiles, const std::vector<SDL_Rect>& positions) {
// for all but the last (invisible) tile
for (int i = tiles.size()-2; i >= 0; --i) {
for (int j = tiles.size()-2; j >= 0; --j) {
// 'attach' tile position n to position of cat photo
SDL_Rect temp = positions[j];
renderTexture(cat, tiles[j].position().x, tiles[j].position().y, &temp);
}
}
}
void Graphics::updateClicks(const int& n) {
std::string count = std::to_string(n);
std::string message = "Clicks: " + count;
clicks = renderText(message, "assets/GreenFlame.ttf", fontcolour, 15);
int W; int H;
SDL_QueryTexture(clicks, NULL, NULL, &W, &H);
SDL_Rect clickBanner { WINDOW_WIDTH/2-W, WINDOW_HEIGHT/6-3*H, 2*W, 2*H };
SDL_Rect clickBannerShadow { WINDOW_WIDTH/2-W + 3, WINDOW_HEIGHT/6-3*H + 3, 2*W, 2*H};
SDL_SetRenderDrawColor (renderer, 100, 65, 107, 1);
SDL_RenderFillRect (renderer, &clickBannerShadow);
SDL_SetRenderDrawColor (renderer, 237, 229, 239, 1);
SDL_RenderFillRect (renderer, &clickBanner);
renderTexture(clicks, (0.77*(clickBanner.x+clickBanner.w) - W/2), (0.8*(clickBanner.y+clickBanner.h) - H/2), nullptr);
}
void Graphics::winMessage(const bool& win, const int& clicks, const std::string& message) {
if (win == true && clicks > 0){
wintext = renderText(message, "assets/Calibrib.ttf", fontcolour, 15);
int W; int H;
SDL_QueryTexture(wintext, NULL, NULL, &W, &H);
SDL_Rect winBannerShadow { WINDOW_PADDING + 3, WINDOW_PADDING+TILE_SIZE+TILE_PADDING + 3,
3*TILE_SIZE + 2*TILE_PADDING, TILE_SIZE};
SDL_Rect winBanner { WINDOW_PADDING, WINDOW_PADDING+TILE_SIZE+TILE_PADDING,
3*TILE_SIZE + 2*TILE_PADDING, TILE_SIZE};
SDL_SetRenderDrawColor (renderer, 100, 65, 107, 1);
SDL_RenderFillRect (renderer, &winBannerShadow);
SDL_SetRenderDrawColor (renderer, 237, 229, 239, 1);
SDL_RenderFillRect (renderer, &winBanner);
renderTexture(wintext, (0.5*WINDOW_WIDTH - 0.5*W), (0.5*WINDOW_HEIGHT - H), nullptr);
}
}
void Graphics::menuInstruction(const std::string& message) {
SDL_Color darkpink { 100, 65, 107, 1 };
menuinstr = renderText(message, "assets/GreenFlame.ttf", darkpink, 12);
int W; int H;
SDL_QueryTexture(menuinstr, NULL, NULL, &W, &H);
renderTexture(menuinstr, (0.5*WINDOW_WIDTH - 0.5*W), (WINDOW_HEIGHT - 0.5*WINDOW_PADDING), nullptr);
}
void Graphics::menuText(std::vector<Tile> tiles, const bool& catmode) {
int number = 3;
int W; int H;
for (int i = 0; i < 6; ++i){
std::string numStr = std::to_string(number);
std::string message = numStr + " x " + numStr;
menutext = renderText(message, "assets/Calibrib.ttf", fontcolour, 25);
SDL_QueryTexture(menutext, NULL, NULL, &W, &H);
renderTexture(menutext, (tiles[i].position().x + 0.5*tiles[i].position().w - 0.5*W),
(tiles[i].position().y + 0.5*tiles[i].position().h - 0.5*H), nullptr);
number += 1;
}
if (catmode){
SDL_Color tilecolour { 237, 229, 239, 1 };
menutext = renderText("Cat Mode", "assets/Calibrib.ttf", tilecolour, 20);
SDL_QueryTexture(menutext, NULL, NULL, &W, &H);
renderTexture(menutext, (tiles[6].position().x + 0.5*tiles[6].position().w - 0.5*W + 2),
(tiles[6].position().y + 0.5*tiles[6].position().h - 0.5*H + 2), nullptr);
}
menutext = renderText("START", "assets/Calibrib.ttf", fontcolour, 25);
SDL_QueryTexture(menutext, NULL, NULL, &W, &H);
renderTexture(menutext, (tiles[7].position().x + 0.5*tiles[7].position().w - 0.5*W),
(tiles[7].position().y + 0.5*tiles[7].position().h - 0.5*H), nullptr);
menutext = renderText("Select your grid size:", "assets/GreenFlame.ttf", fontcolour, 20);
SDL_QueryTexture(menutext, NULL, NULL, &W, &H);
renderTexture(menutext, 0.5*WINDOW_WIDTH - 0.5*W, WINDOW_PADDING-1.5*H, nullptr);
}
void Graphics::renderTexture(SDL_Texture *tex, int x, int y, SDL_Rect *clip = nullptr) {
SDL_Rect dst;
dst.x = x;
dst.y = y;
if (clip != nullptr){
dst.w = clip->w;
dst.h = clip->h;
}
else {
SDL_QueryTexture(tex, NULL, NULL, &dst.w, &dst.h);
}
SDL_RenderCopy(renderer, tex, clip, &dst);
}
SDL_Texture* Graphics::renderText(const std::string &message, const std::string &filepath, SDL_Color colour, int fontsize) {
//Open the font
TTF_Font *font = TTF_OpenFont(filepath.c_str(), fontsize);
if (font == nullptr){
std::cerr << "TTF_OpenFont" << TTF_GetError;
return nullptr;
}
//render to a surface as that's what TTF_RenderText returns
SDL_Surface *surf = TTF_RenderText_Blended(font, message.c_str(), colour);
if (surf == nullptr){
TTF_CloseFont(font);
std::cout << "TTF_RenderText" << TTF_GetError;
return nullptr;
}
//load that surface into texture
SDL_Texture *texture = SDL_CreateTextureFromSurface(renderer, surf);
if (texture == nullptr){
std::cout << "CreateTexture" << SDL_GetError();
}
//Clean up surface and font
SDL_FreeSurface(surf);
TTF_CloseFont(font);
return texture;
}
Tile.h
#ifndef Tile_h
#define Tile_h
#include <SDL2/SDL.h>
class Tile {
public:
Tile(SDL_Rect r, int c, int t) :
tilePosition{r}, currentNumber{c}, tiletype{t} {}
void swap(Tile& t);
void setTileType(int n) { tiletype = n; }
void setTileShift(const int& x, const int& y) { tilePosition.x += x; tilePosition.y += y; }
SDL_Rect position() const { return tilePosition; }
int posNumber() const { return currentNumber; }
int tileType() const { return tiletype; }
enum type { button = 0, rollover, shadow, invisible, buttonpressed};
private:
SDL_Rect tilePosition;
int currentNumber;
int tiletype;
};
#endif