6
\$\begingroup\$

This is my second attempt at creating a game. I think I have gotten the basic ideas of state machines and game loops down, but I'm sure there are still a lot of things I can learn.

It is a basic Tic-Tac-Toe game. There is a menu where you can choose to play vs AI or another player. If the player is vs AI, there is a 50% chance the AI will have the first turn. Players take turn placing marks trying to get 3 in a row.

Once the game has ended, the board screen fades slightly so you can still see the board and a play again menu pops up.

Main.cpp

#include <iostream>
#include <vector>
#include <random>

#include <SDL.h>
#include <SDL_image.h>
#include <SDL_ttf.h>

#include "SDLInit.h"
#include "Texture.h"

constexpr int SCREEN_WIDTH = 640;
constexpr int SCREEN_HEIGHT = 480;

enum class GameState
{
    MAIN_MENU,
    GAME_LOOP,
    PLAY_AGAIN,
    EXIT,
};

//used to determine where to draw strikethough after win
enum class Line
{
    NONE,
    ROW_ONE,
    ROW_TWO,
    ROW_THREE,
    COL_ONE,
    COL_TWO,
    COL_THREE,
    DIAG_ONE,
    DIAG_TWO,
};

struct Game
{
    //2d vector to represent board
    Game()
    {
        board.resize(3, std::vector<int>(3));
    }

    void resetBoard();
    bool hasWon();
    bool isDraw();
    void placeMark(int row, int col);
    void placeMarkAI();

    std::vector<std::vector <int> > board;

    bool playerOneTurn_ = true;
    bool gameWasWon_ = false;
    bool gameWasDraw_ = false;
    bool vsAI_ = false;
    Line winningLine_ = Line::NONE;

};

struct AssetManager
{
    //unable to initialize a vector of textures in class
    void loadButtonTextures();
    void setBlendMode(SDL_BlendMode blendmode);
    void drawStrikeThrough(Game& game);

    std::unique_ptr<SDL_Window, sdl_deleter> window_{ SDL_CreateWindow("TicTacToe", SDL_WINDOWPOS_UNDEFINED, SDL_WINDOWPOS_UNDEFINED, SCREEN_WIDTH, SCREEN_HEIGHT, SDL_WINDOW_SHOWN) };
    std::unique_ptr<SDL_Renderer, sdl_deleter> renderer_{ SDL_CreateRenderer(window_.get(), -1, SDL_RENDERER_PRESENTVSYNC) };
    std::unique_ptr<TTF_Font, sdl_deleter> font_{ TTF_OpenFont("resources/opensans.ttf", 32) };

    SDL_Color fontColor_ = { 0xff, 0xff, 0xff };
    int fontWrapWidth_ = SCREEN_WIDTH;

    LTexture menuTexture_{ from_surface, renderer_.get(), "resources/menu.png" };
    LTexture onePlayerButton_{ from_surface, renderer_.get(), "resources/oneplayer.png" };
    LTexture twoPlayerButton_{ from_surface, renderer_.get(), "resources/twoplayer.png" };
    LTexture boardTexture_{ from_surface, renderer_.get(), "resources/board.png" };
    LTexture yesButton_{ from_surface, renderer_.get(), "resources/yes.png" };
    LTexture noButton_{ from_surface, renderer_.get(), "resources/no.png" };
    LTexture strikeThrough_{ from_surface, renderer_.get(), "resources/strikethrough.png" };
    LTexture gridButton_[3][3];

    LTexture playerOneWinsText_{ from_text, font_.get(), renderer_.get(), "Player One Wins!", fontColor_, fontWrapWidth_ };
    LTexture playerTwoWinsText_{ from_text, font_.get(), renderer_.get(), "Player Two Wins!", fontColor_, fontWrapWidth_ };
    LTexture gameWasDrawText_{ from_text, font_.get(), renderer_.get(), "It's a Draw!", fontColor_, fontWrapWidth_ };
    LTexture playAgainText_{ from_text, font_.get(), renderer_.get(), "Play Again?", fontColor_, fontWrapWidth_ };

    //'blank', 'x', or 'o'
    SDL_Rect spriteClips[3] = { { 0,0, 100, 100 },{ 100, 0, 100, 100 },{ 200, 0, 100, 100 } };
};


std::mt19937& random_engine()
{
    static std::mt19937 mersenne(std::random_device{}());
    return mersenne;
}

int getRandomNumber(int x, int y)
{
    std::uniform_int_distribution<> dist{ x,y };
    return dist(random_engine());
}

bool button_is_pressed(SDL_Event const& event, SDL_Rect const& button_rect)
{
    if (event.type == SDL_MOUSEBUTTONDOWN)
    {
        auto const& mouse_button_event = event.button;

        auto const mouse_position = SDL_Point{ mouse_button_event.x, mouse_button_event.y };

        return (mouse_button_event.button == SDL_BUTTON_LEFT) && SDL_PointInRect(&mouse_position, &button_rect);
    }

    return false;
}

void Game::resetBoard()
{
    //clear board
    for (int row = 0; row < 3; row++)
    {
        for (int col = 0; col < 3; col++)
        {
            board[row][col] = 0;
        }
    }

    //reset all members
    playerOneTurn_ = true;
    gameWasWon_ = false;
    gameWasDraw_ = false;
    vsAI_ = false;
    winningLine_ = Line::NONE;
}

bool Game::hasWon()
{
    //check rows
    for (int row = 0; row < 3; row++)
    {
        if (board[row][0] != 0 && board[row][0] == board[row][1] && board[row][0] == board[row][2])
        {
            switch (row)
            {
            case 0:
                winningLine_ = Line::ROW_ONE;
                return true;
            case 1:
                winningLine_ = Line::ROW_TWO;
                return true;
            case 2:
                winningLine_ = Line::ROW_THREE;
                return true;
            }
        }
    }
    //check cols
    for (int col = 0; col < 3; col++)
    {
        if (board[0][col] != 0 && board[0][col] == board[1][col] && board[0][col] == board[2][col])
        {
            switch (col)
            {
            case 0:
                winningLine_ = Line::COL_ONE;
                return true;
            case 1:
                winningLine_ = Line::COL_TWO;
                return true;
            case 2:
                winningLine_ = Line::COL_THREE;
                return true;
            }
        }
    }
    //check diagonal
    if (board[0][0] != 0 && board[0][0] == board[1][1] && board[0][0] == board[2][2])
    {
        winningLine_ = Line::DIAG_ONE;
        return true;
    }
    //check other diagonal
    if (board[2][0] != 0 && board[2][0] == board[1][1] && board[2][0] == board[0][2])
    {
        winningLine_ = Line::DIAG_TWO;
        return true;
    }

    return false;
}

bool Game::isDraw()
{
    for (int row = 0; row < 3; row++)
    {
        for (int col = 0; col < 3; col++)
        {
            //if there is an empty space
            if (board[row][col] == 0)
            {
                return false;
            }
        }
    }

    return true;
}

void Game::placeMark(int row, int col)
{
    //if square is empty
    if (board[row][col] == 0)
    {
        //alternate between x's and o's
        if (playerOneTurn_)
        {
            board[row][col] = 1;
            playerOneTurn_ = false;
        }
        else
        {
            board[row][col] = 2;
            playerOneTurn_ = true;
        }
    }
}

void Game::placeMarkAI()
{
    //AI checks for two lines with two 'x's and a blank space

    //rows
    for (int row = 0; row < 3; row++)
    {
        if (board[row][0] == 1 && board[row][1] == 1 && board[row][2] == 0)
        {
            placeMark(row, 2);
            return;
        }

        if (board[row][0] == 1 && board[row][2] == 1 && board[row][1] == 0)
        {
            placeMark(row, 1);
            return;
        }

        if (board[row][2] == 1 && board[row][1] == 1 && board[row][0] == 0)
        {
            placeMark(row, 0);
            return;
        }
    }

    //columns
    for (int col = 0; col < 3; col++)
    {
        if (board[0][col] == 1 && board[1][col] == 1 && board[2][col] == 0)
        {
            placeMark(2, col);
            return;
        }

        if (board[0][col] == 1 && board[2][col] == 1 && board[1][col] == 0)
        {
            placeMark(1, col);
            return;
        }

        if (board[2][col] == 1 && board[1][col] == 1 && board[0][col] == 0)
        {
            placeMark(0, col);
            return;
        }
    }

    //diagonals
    if (board[0][0] == 1 && board[1][1] == 1 && board[2][2] == 0)
    {
        placeMark(2, 2);
        return;
    }
    if (board[0][0] == 1 && board[2][2] == 1 && board[1][1] == 0)
    {
        placeMark(1, 1);
        return;
    }
    if (board[2][2] == 1 && board[1][1] == 1 && board[0][0] == 0)
    {
        placeMark(0, 0);
        return;
    }
    if (board[0][2] == 1 && board[1][1] == 1 && board[2][0] == 0)
    {
        placeMark(2, 0);
        return;
    }
    if (board[2][0] == 1 && board[1][1] == 1 && board[0][2] == 0)
    {
        placeMark(0, 2);
        return;
    }
    if (board[2][0] == 1 && board[0][2] == 1 && board[1][1] == 0)
    {
        placeMark(1, 1);
        return;
    }

    //else mark random empty spot
    int row = getRandomNumber(0, 2);
    int col = getRandomNumber(0, 2);

    while (board[row][col] != 0)
    {
        row = getRandomNumber(0, 2);
        col = getRandomNumber(0, 2);
    }

    board[row][col] = 2;
}

void AssetManager::drawStrikeThrough(Game& game)
{
    switch (game.winningLine_)
    {
    case Line::ROW_ONE:
        strikeThrough_.render(renderer_.get(), SCREEN_WIDTH / 2 - strikeThrough_.mWidth / 2 , SCREEN_HEIGHT / 2 - strikeThrough_.mHeight / 2 - 120);
        break;
    case Line::ROW_TWO:
        strikeThrough_.render(renderer_.get(), SCREEN_WIDTH / 2 - strikeThrough_.mWidth / 2, SCREEN_HEIGHT / 2 - strikeThrough_.mHeight / 2);
        break;
    case Line::ROW_THREE:
        strikeThrough_.render(renderer_.get(), SCREEN_WIDTH / 2 - strikeThrough_.mWidth / 2, SCREEN_HEIGHT / 2 - strikeThrough_.mHeight / 2 + 120);
        break;
    case Line::COL_ONE:
        strikeThrough_.render(renderer_.get(), SCREEN_WIDTH / 2 - strikeThrough_.mWidth / 2 - 120, SCREEN_HEIGHT / 2 - strikeThrough_.mHeight / 2, nullptr, 90);
        break;
    case Line::COL_TWO:
        strikeThrough_.render(renderer_.get(), SCREEN_WIDTH / 2 - strikeThrough_.mWidth / 2, SCREEN_HEIGHT / 2 - strikeThrough_.mHeight / 2, nullptr, 90);
        break;
    case Line::COL_THREE:
        strikeThrough_.render(renderer_.get(), SCREEN_WIDTH / 2 - strikeThrough_.mWidth / 2 + 120, SCREEN_HEIGHT / 2 - strikeThrough_.mHeight / 2, nullptr, 90);
        break;
    case Line::DIAG_ONE:
        strikeThrough_.render(renderer_.get(), SCREEN_WIDTH / 2 - strikeThrough_.mWidth / 2, SCREEN_HEIGHT / 2 - strikeThrough_.mHeight / 2, nullptr, 45);
        break;
    case Line::DIAG_TWO:
        strikeThrough_.render(renderer_.get(), SCREEN_WIDTH / 2 - strikeThrough_.mWidth / 2, SCREEN_HEIGHT / 2 - strikeThrough_.mHeight / 2, nullptr, 135);
        break;
    }
}

bool playAgain_render(Game& game, AssetManager& assets)
{
    if (game.gameWasWon_)
    {
        //if player's one turn, it means player two was last to make a move and vice versa
        if (!game.playerOneTurn_)
        {
            assets.playerOneWinsText_.render(assets.renderer_.get(), SCREEN_WIDTH/2 - assets.playerOneWinsText_.mWidth/2, 25);
        }
        else
        {
            assets.playerTwoWinsText_.render(assets.renderer_.get(), SCREEN_WIDTH / 2 - assets.playerTwoWinsText_.mWidth / 2, 25);
        }
    }
    else
    {
        assets.gameWasDrawText_.render(assets.renderer_.get(), SCREEN_WIDTH / 2 - assets.gameWasDrawText_.mWidth / 2, 25);
    }

    assets.playAgainText_.render(assets.renderer_.get(), SCREEN_WIDTH / 2 - assets.playAgainText_.mWidth / 2, SCREEN_HEIGHT - 100);
    assets.yesButton_.render(assets.renderer_.get(), SCREEN_WIDTH / 2 - assets.yesButton_.mWidth, SCREEN_HEIGHT - assets.yesButton_.mHeight - 10);
    assets.noButton_.render(assets.renderer_.get(), SCREEN_WIDTH / 2, SCREEN_HEIGHT - assets.yesButton_.mHeight - 10);

    SDL_RenderPresent(assets.renderer_.get());

    return true;
}
bool playAgain_update()
{
    return true;
}
bool playAgain_input(GameState& gameState, Game& game, AssetManager& assets)
{
    SDL_Event e;

    while (SDL_PollEvent(&e) != 0)
    {
        if (e.type == SDL_QUIT)
        {
            gameState = GameState::EXIT;
            return false;
        }
        if (button_is_pressed(e, assets.yesButton_.mButton))
        {
            gameState = GameState::MAIN_MENU;
            game.resetBoard();
            return false;
        }
        if (button_is_pressed(e, assets.noButton_.mButton))
        {
            gameState = GameState::EXIT;
            return false;
        }
    }

    return true;
}

bool gameLoop_render(Game& game, AssetManager& assets)
{
    SDL_SetRenderDrawColor(assets.renderer_.get(), 0x21, 0xb3, 0xfa, 0xFF);
    SDL_RenderClear(assets.renderer_.get());
    assets.boardTexture_.render(assets.renderer_.get(), SCREEN_WIDTH / 2 - assets.boardTexture_.mWidth / 2, SCREEN_HEIGHT / 2 - assets.boardTexture_.mHeight / 2);

    for (int row = 0; row < 3; row++)
    {
        for (int col = 0; col < 3; col++)
        {
            assets.gridButton_[row][col].render(assets.renderer_.get(), (SCREEN_WIDTH / 2 - assets.boardTexture_.mWidth / 2) + col * 120, (SCREEN_HEIGHT / 2 - assets.boardTexture_.mHeight / 2) + row * 120, &assets.spriteClips[game.board[row][col]]);
        }
    }

    if (game.winningLine_ != Line::NONE)
    {
        assets.drawStrikeThrough(game);
    }

    SDL_RenderPresent(assets.renderer_.get());

    return true;
}

bool gameLoop_update(GameState& gameState, Game& game, AssetManager& assets)
{
    if (game.hasWon())
    {
        //without this, game will skip to play again screen without showing the last move
        gameLoop_render(game, assets);

        game.gameWasWon_ = true;
        gameState = GameState::PLAY_AGAIN;
        return false;
    }

    if (game.isDraw())
    {
        //without this, game will skip to play again screen without showing the last move
        gameLoop_render(game, assets);

        game.gameWasDraw_ = true;
        gameState = GameState::PLAY_AGAIN;
        return false;
    }

    if (game.vsAI_ && game.playerOneTurn_ == false)
    {
        game.placeMarkAI();
        game.playerOneTurn_ = true;
    }

    return true;
}

bool gameLoop_input(GameState& gameState, Game& game, AssetManager& assets)
{
    SDL_Event e;

    while (SDL_PollEvent(&e) != 0)
    {
        if (e.type == SDL_QUIT)
        {
            gameState = GameState::EXIT;
            return false;
        }
        for (int row = 0; row < 3; row++)
        {
            for (int col = 0; col < 3; col++)
            {
                if (button_is_pressed(e, assets.gridButton_[row][col].mButton))
                {
                    game.placeMark(row, col);
                    return true;
                }
            }
        }
    }

    return true;
}

bool mainMenu_render(AssetManager& assets)
{
    SDL_RenderClear(assets.renderer_.get());
    assets.menuTexture_.render(assets.renderer_.get(), 0, 0);
    assets.onePlayerButton_.render(assets.renderer_.get(), SCREEN_WIDTH / 2 - assets.onePlayerButton_.mWidth / 2, SCREEN_HEIGHT/2);
    assets.twoPlayerButton_.render(assets.renderer_.get(), SCREEN_WIDTH / 2 - assets.twoPlayerButton_.mWidth / 2, SCREEN_HEIGHT/2 + assets.onePlayerButton_.mHeight + 25);
    SDL_RenderPresent(assets.renderer_.get());
    return true;
}
bool mainMenu_update()
{
    return true;
}
bool mainMenu_input(GameState& gameState, Game& game, AssetManager& assets)
{
    SDL_Event e;

    while (SDL_PollEvent(&e) != 0)
    {
        if (e.type == SDL_QUIT)
        {
            gameState = GameState::EXIT;
            return false;
        }
        if (button_is_pressed(e, assets.onePlayerButton_.mButton))
        {
            gameState = GameState::GAME_LOOP;
            game.vsAI_ = true;

            //randomly choose who goes first
            int whoGoesFirst = getRandomNumber(0, 1);
            if (whoGoesFirst == 1)
            {
                game.playerOneTurn_ = false;
            }

            return false;
        }
        if (button_is_pressed(e, assets.twoPlayerButton_.mButton))
        {
            gameState = GameState::GAME_LOOP;
            return false;
        }
    }

    return true;
}

void AssetManager::loadButtonTextures()
{
    //unable to initialize vector directly in class
    for (int row = 0; row < 3; row++)
    {
        for (int col = 0; col < 3; col++)
        {
            gridButton_[row][col].loadfromsurface(renderer_.get(), "resources/xospritesheet.png");
        }
    }
}
//used to allow fade affect after game over
void AssetManager::setBlendMode(SDL_BlendMode blendmode)
{
    SDL_SetRenderDrawBlendMode(renderer_.get(), blendmode);
}

int main(int argc, char* args[])
{
    //initialize sdl systems
    sdl sdlinit;
    sdl_img img;
    sdl_ttf ttf;

    GameState gameState = GameState::MAIN_MENU;
    Game game;
    AssetManager assets;
    assets.loadButtonTextures();

    //used to fade game grid after game over
    assets.setBlendMode(SDL_BLENDMODE_BLEND);

    while (gameState != GameState::EXIT)
    {
        switch (gameState)
        {
            case GameState::MAIN_MENU:
            {
                while (mainMenu_input(gameState, game, assets) &&
                    mainMenu_update() &&
                    mainMenu_render(assets))
                {}
                break;
            }
            case GameState::GAME_LOOP:
            {
                while (gameLoop_input(gameState, game, assets) &&
                    gameLoop_update(gameState, game, assets) &&
                    gameLoop_render( game, assets))
                {}
                break;
            }
            case GameState::PLAY_AGAIN:
            {
                //fade screen slightly
                SDL_Rect fade = { 0,0, SCREEN_WIDTH, SCREEN_HEIGHT };
                SDL_SetRenderDrawColor(assets.renderer_.get(), 0, 0, 0, 0x2b);
                SDL_RenderFillRect(assets.renderer_.get(), &fade);
                SDL_RenderPresent(assets.renderer_.get());

                while (playAgain_input(gameState, game, assets) &&
                    playAgain_update() &&
                    playAgain_render(game, assets))
                {}
                break;
            }
        }
    }

    return 0;
}

SDLInit.h

#pragma once

class sdl
{
public:
    sdl();

    sdl(sdl&& other) noexcept;


    ~sdl();

    auto operator=(sdl&& other) noexcept->sdl&;

    // Non-copyable
    sdl(sdl const&) = delete;
    auto operator=(sdl const&)->sdl& = delete;

    friend void _swap(sdl& a, sdl& b) noexcept;

private:

    bool _own = false;
};

class sdl_img
{
public:
    sdl_img();

    sdl_img(sdl_img&& other) noexcept;

    ~sdl_img();

    auto operator=(sdl_img&& other) noexcept->sdl_img&;

    // Non-copyable
    sdl_img(sdl_img const&) = delete;
    auto operator=(sdl_img const&)->sdl_img& = delete;

    friend void _swap(sdl_img& a, sdl_img& b) noexcept;

private:

    bool _own = false;
};

class sdl_ttf
{
public:
    sdl_ttf();

    sdl_ttf(sdl_ttf&& other) noexcept;


    ~sdl_ttf();

    auto operator=(sdl_ttf&& other) noexcept->sdl_ttf&;

    // Non-copyable
    sdl_ttf(sdl_ttf const&) = delete;
    auto operator=(sdl_ttf const&)->sdl_ttf& = delete;

    friend void _swap(sdl_ttf& a, sdl_ttf& b) noexcept;

private:

    bool _own = false;
};

struct sdl_deleter
{
    void operator()(SDL_Window* p) noexcept;
    void operator()(SDL_Renderer* p) noexcept;
    void operator()(TTF_Font* p) noexcept;
};

SDLInit.cpp

#include <iostream>

#include <SDL.h>
#include <SDL_image.h>
#include <SDL_ttf.h>

#include "SDLInit.h"

//Initialize SDL
sdl::sdl()
{
    auto const result = SDL_Init(SDL_INIT_VIDEO);
    if (result != 0)
        std::cout << "SDL could not initialize";
}

sdl::sdl(sdl&& other) noexcept
{
    _swap(*this, other);
}

sdl::~sdl()
{
    if (_own)
        SDL_Quit();
}

auto sdl::operator=(sdl&& other) noexcept -> sdl&
{
    _swap(*this, other);
    return *this;
}

void _swap(sdl& a, sdl& b) noexcept
{
    using std::swap;
    swap(a._own, b._own);
}

//Initialize SDL Image
sdl_img::sdl_img()
{
    auto const result = IMG_Init(IMG_INIT_PNG);
    if (result == 0)
        std::cout << "SDL Image could not initialize";
}

sdl_img::sdl_img(sdl_img&& other) noexcept
{
    _swap(*this, other);
}

sdl_img::~sdl_img()
{
    if (_own)
        IMG_Quit();
}

auto sdl_img::operator=(sdl_img&& other) noexcept -> sdl_img&
{
    _swap(*this, other);
    return *this;
}

void _swap(sdl_img& a, sdl_img& b) noexcept
{
    using std::swap;
    swap(a._own, b._own);
}

//Initialize SDL True Type Fonts
sdl_ttf::sdl_ttf()
{
    auto const result = TTF_Init();
    if (result != 0)
        std::cout << "SDL TTF could not initialize";
}

sdl_ttf::sdl_ttf(sdl_ttf&& other) noexcept
{
    _swap(*this, other);
}

sdl_ttf::~sdl_ttf()
{
    if (_own)
        TTF_Quit();
}

auto sdl_ttf::operator=(sdl_ttf&& other) noexcept -> sdl_ttf&
{
    _swap(*this, other);
    return *this;
}

void _swap(sdl_ttf& a, sdl_ttf& b) noexcept
{
    using std::swap;
    swap(a._own, b._own);
}

//used with std::unique_ptr
void sdl_deleter::operator()(SDL_Window* p) noexcept
{
    if (p)
        SDL_DestroyWindow(p);
}

void sdl_deleter::operator()(SDL_Renderer* p) noexcept
{
    if (p)
        SDL_DestroyRenderer(p);
}

void sdl_deleter::operator()(TTF_Font* p) noexcept
{
    if (p)
        TTF_CloseFont(p);
}

Texture.h

#pragma once
#include <SDL.h>

//used to tag how the texture was rendered
enum TextureType { from_surface, from_text };

class LTexture
{
public:

    LTexture();
    LTexture(TextureType type, SDL_Renderer* renderer, std::string const& path);
    LTexture(TextureType type, TTF_Font* font, SDL_Renderer* renderer, std::string const& text, SDL_Color color, int width);

    LTexture(LTexture&& other) noexcept
    {
        swap(*this, other);
    }

    ~LTexture();

    void render(SDL_Renderer* renderer, int x, int y, SDL_Rect* clip = nullptr, double angle = 0.0, SDL_Point* center = nullptr, SDL_RendererFlip flip = SDL_FLIP_NONE);
    void loadfromsurface(SDL_Renderer* renderer, std::string const& path);
    void loadfromtext(TTF_Font* font, SDL_Renderer* renderer, std::string const& text, SDL_Color color, int width);

    LTexture& operator=(LTexture&& other) noexcept
    {
        swap(*this, other);
        return *this;
    }
    friend void swap(LTexture& a, LTexture& b) noexcept;

    SDL_Texture* mTexture = nullptr;
    int mWidth = 0;
    int mHeight = 0;
    SDL_Rect mButton = {};

    //had to make default in order to use in another class
    LTexture(LTexture const&) = default;
    //LTexture(LTexture const&) = delete;
    auto operator=(LTexture const&) = delete;
};

Texture.cpp

#include <iostream>

#include <SDL.h>
#include <SDL_image.h>
#include <SDL_ttf.h>

#include "Texture.h"


LTexture::LTexture()
{
}

LTexture::LTexture(TextureType type, SDL_Renderer* renderer, std::string const& path)
{
    SDL_Surface *surface = IMG_Load(path.c_str());
    if (!surface)
        std::cout << "Failed to create surface";

    mTexture = SDL_CreateTextureFromSurface(renderer, surface);
    if (!mTexture)
        std::cout << "Failed to create texture";
    mWidth = surface->w;
    mHeight = surface->h;

    SDL_FreeSurface(surface);
}

LTexture::LTexture(TextureType type, TTF_Font* font, SDL_Renderer* renderer, std::string const& text, SDL_Color color, int width)
{
    SDL_Surface* textSurface = TTF_RenderText_Blended_Wrapped(font, text.c_str(), color, width);
    if (!textSurface)
        std::cout << "Failed to create surface";

    mTexture = SDL_CreateTextureFromSurface(renderer, textSurface);
    if (!mTexture)
        std::cout << "Failed to created texture";

    mWidth = textSurface->w;
    mHeight = textSurface->h;

    SDL_FreeSurface(textSurface);
}

void LTexture::render(SDL_Renderer* renderer, int x, int y, SDL_Rect* clip, double angle, SDL_Point* center, SDL_RendererFlip flip)
{
    SDL_Rect destRect = { x, y, mWidth, mHeight };
    if (clip != nullptr)
    {
        destRect.w = clip->w;
        destRect.h = clip->h;
    }
    SDL_RenderCopyEx(renderer, mTexture, clip, &destRect, angle, center, flip);
    //create a rectangle that coincides with texture to check for button presses
    mButton = { x, y, destRect.w, destRect.h };
}

void LTexture::loadfromsurface(SDL_Renderer* renderer, std::string const& path)
{
    SDL_Surface *surface = IMG_Load(path.c_str());
    if (!surface)
        std::cout << "Failed to create surface";

    mTexture = SDL_CreateTextureFromSurface(renderer, surface);
    if (!mTexture)
        std::cout << "Failed to create texture";
    mWidth = surface->w;
    mHeight = surface->h;

    SDL_FreeSurface(surface);
}

void LTexture::loadfromtext(TTF_Font* font, SDL_Renderer* renderer, std::string const& text, SDL_Color color, int width)
{
    SDL_Surface* textSurface = TTF_RenderText_Blended_Wrapped(font, text.c_str(), color, width);
    if (!textSurface)
        std::cout << "Failed to create surface";

    mTexture = SDL_CreateTextureFromSurface(renderer, textSurface);
    if (!mTexture)
        std::cout << "Failed to created texture";

    mWidth = textSurface->w;
    mHeight = textSurface->h;

    SDL_FreeSurface(textSurface);
}

void swap(LTexture& a, LTexture& b) noexcept
{
    using std::swap;
    swap(a.mTexture, b.mTexture);
    swap(a.mWidth, b.mWidth);
    swap(a.mHeight, b.mHeight);
    swap(a.mButton, b.mButton);
}

LTexture::~LTexture()
{
    if (mTexture)
        SDL_DestroyTexture(mTexture);
}

The two main issues I had making the game were:

  • I've tried making my texture wrapping using this guide which calls for the deleting the copy functions. However, I wanted to make a type of AssetManager class that I could load all of the textures into for easy use. But because the copy function for the LTexture class was deleted I would just get an error about referencing a deleted function. This resulted in me using the default copy function.

    So my question is basically, is there another way to use a resource wrapper within a different class without having to use the default copy function?

  • While making the playAgain game state I had an issue where once the last move was made (either a winning move, or a move that took the last remaining open slot) the last mark wouldn't render and the game would just move to the playAgain state. I tried moving the the order of the functions around hoping it would change anything but it didn't. I ended up having to just call the gameLoop_render function within the gameLoop_update to allow for the final mark to be shown. I realize I could have just completely redrawn the board from within the playAgain_render function, but I was trying to stay away from duplicating all of that code.

\$\endgroup\$
4
\$\begingroup\$

But because the copy function for the LTexture class was deleted I would just get an error about referencing a deleted function. This resulted in me using the default copy function.

Right now your resource wrapper class is wrong. It works, but if you would use the default copy constructor in your class, it would crash. String example

In this example you can see clearly that the "hello" string is not getting copied, instead both of the strings pointing to the same memory location. This is what happens in your example. Your SDL_Textures are going to point to the same texture, and when the LTexture destructor is going to be called, the first is going to delete the object, but the second one is going to delete an invalid memory location

Solution 1: Copy the resource

String example2

This solution is not really convinient if you want to be efficent (Imagine copying a big triangle mesh approx 1-2 gigabytes)

Solution 2: Don't allow copying.

This is what the article suggests. Te only problem is that you need to delete the copy constructor in the AssetManager class too. The compiler very clever, and if you delete a nested type's copy ctor, the owner cannot be copied.

While making the playAgain game state I had an issue where once the last move was made (either a winning move, or a move that took the last remaining open slot) the last mark wouldn't render and the game would just move to the playAgain state.

This happens because in your main loop you call the methods in the following order:

while (gameLoop_input(gameState, game, assets) &&
       gameLoop_update(gameState, game, assets) &&
       gameLoop_render( game, assets))

The && operator has a short circuit property, which means if the one of the results are false, the whole expression is going to be false, so its not going to call the other methods. In your case: The update method returns false, and then the whole expression is false, so the render method is not going to be called

One tip about your game loop: Instead of repeating the same thing with different function names, you can use inheritance and virtual functions

I was trying to stay away from duplicating all of that code.

Always avoid code duplication, it always leads to bugs

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.