3
\$\begingroup\$

So after getting a very thorough review and feedback from indi in my previous attempt, SDL/C++ High-Low Guessing Game, I decided to go through the game and fix as many mistakes as I could. But going through the feedback also just opened up even more questions.

Things I was unable to do

  • Get rid of all global variables. I was able to remove or move most of the global variables, but I had an issue with the SDL_Window*, SDL_Renderer, and TTF_Font. It was suggested that I used a std::unique_ptr, so I made the window a std::unique_ptr, but I was unable to use that window as a parameter for creating the renderer because there was no suitable conversion from a std::unique_ptr to a SDL_Window*. Thus I just kept them in global until I can figure out how to make it work.

  • Use the struct "tags" for my LTexture class. It was suggested to use struct from_surface_tag {} from_surface; and struct from_text_tag {} from_text; to indicate which version of the constructor I was using. The compiler just kept saying that from_text or from_surface were undefined. I realize that there mainly for the reader of the code, so I just made an enum to use as the tags.

  • Used the state machine that was suggested. I make the engine that was suggested, but I failed miserably. Instead I created a very basic state machine that hopefully works and gets rid of the playAgain -> mainMenu -> playAgain loop. Hopefully I'll be able to understand how to other one once I get more experience.

  • Lastly, I did not make not make the LTexture class it's own file, mainly because I struggled getting the SDL one's to work.

Here is my code:

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;
};

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);
}

Main.cpp

#include <iostream>
#include <random>
#include <string>

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

#include "SDLInit.h"

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

SDL_Window* gWindow = nullptr;
SDL_Renderer* gRenderer = nullptr;
TTF_Font* gFont = nullptr;

enum class GameStates
{
    MAIN_MENU,
    MAIN_LOOP,
    PLAY_AGAIN,
    EXIT,
};

struct Game
{
    int numberofGuesses = 0;
    const int min = 1;
    int max = 0;
    int randomNumber = 0;
    bool game_was_won = false;
};

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());
}

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

class LTexture
{
public:
    LTexture(TextureType type, SDL_Renderer* renderer, std::string const& path);
    LTexture(TextureType type, 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);

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

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

    LTexture(LTexture const&) = delete;
    LTexture& operator=(LTexture const&) = delete;

    friend void swap(LTexture& a, LTexture& b) noexcept;
};

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(gRenderer, surface);
    if (!mTexture)
        std::cout << "Failed to create texture";
    mWidth = surface->w;
    mHeight = surface->h;
}

LTexture::LTexture(TextureType type, SDL_Renderer* renderer, std::string const& text, SDL_Color color, int width)
{
    SDL_Surface* textSurface = TTF_RenderText_Blended_Wrapped(gFont, 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 destRect = { x, y, mWidth, mHeight };
    SDL_RenderCopy(renderer, mTexture, nullptr, &destRect);
    //create a rectangle that coincides with texture to check for button presses
    mButton = { x, y, mWidth, mHeight };
}

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);
}

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;
}

// compare random number to guess player provided
int compare(int randomNumber, int guess)
{
    if (randomNumber < guess)
    {
        return -1;
    }
    if (randomNumber > guess)
    {
        return 1;
    }

    return 0;
}

bool playAgain_input(GameStates& gameState, LTexture const& yesButton, LTexture const& noButton)
{
    SDL_Event e;

    while (SDL_PollEvent(&e) != 0)
    {
        if (e.type == SDL_QUIT || button_is_pressed(e, noButton.mButton))
        {
            gameState = GameStates::EXIT;
            return false;
        }

        if (button_is_pressed(e, yesButton.mButton))
        {
            gameState = GameStates::MAIN_MENU;
            return false;
        }
    }

    return true;
}

bool playAgain_update()
{
    return true;
}

bool playAgain_render(LTexture& highLowTexture, LTexture& winlose, LTexture& playAgain, LTexture& yesButton, LTexture& noButton)
{
    SDL_SetRenderDrawColor(gRenderer, 0xFF, 0xFF, 0xFF, 0xFF);
    SDL_RenderClear(gRenderer);

    highLowTexture.render(gRenderer, 0, 0);
    winlose.render(gRenderer, 335, 70);
    playAgain.render(gRenderer, 325, 300);
    yesButton.render(gRenderer, 300, 350);
    noButton.render(gRenderer, 300 + yesButton.mWidth + 10, 350);

    SDL_RenderPresent(gRenderer);

    return true;
}
void playAgain(GameStates& gameState, Game game)
{
    SDL_Color const textColor = { 0,0,0 };
    auto const textWidth = 250;

    auto highLowTexture = LTexture(from_surface, gRenderer, "Resources/HiLo.png");
    auto playAgain = LTexture(from_text, gRenderer, "Play Again?", textColor, textWidth);
    auto yesButton = LTexture{ from_surface, gRenderer, "Resources/HiLoYes.png" };
    auto noButton = LTexture{ from_surface, gRenderer, "Resources/HiLoNo.png" };

    std::string dialogue;
    //change dialogue based on if player won or not
    if (game.game_was_won)
    {
        dialogue = "YOU WON!!! The correct answer was " + std::to_string(game.randomNumber) + ".";
    }
    else
    {
        dialogue = "You lose. The correct answer was " + std::to_string(game.randomNumber) + ".";
    }

    auto winlose = LTexture{ from_text, gRenderer, dialogue, textColor, textWidth };

    while (playAgain_input(gameState, yesButton, noButton) && playAgain_update() && playAgain_render(highLowTexture, winlose, playAgain, yesButton, noButton))
    {}
}

bool gameLoop_input(GameStates& gameState, std::string& guessInput, int& guess, int& guessCount)
{
    SDL_Event e;

    SDL_StartTextInput();

    while (SDL_PollEvent(&e) != 0)
    {
        if (e.type == SDL_QUIT)
        {
            gameState = GameStates::EXIT;
            return false;
        }
        if (e.type == SDL_TEXTINPUT)
        {
            //if input is a numeric value, add to string.
            if (e.text.text[0] == '0' || e.text.text[0] == '1' || e.text.text[0] == '2' || e.text.text[0] == '3' || e.text.text[0] == '4' ||
                e.text.text[0] == '5' || e.text.text[0] == '6' || e.text.text[0] == '7' || e.text.text[0] == '8' || e.text.text[0] == '9')
            {
                guessInput += e.text.text;
            }
        }
        if (e.type == SDL_KEYDOWN)
        {
            if (e.key.keysym.sym == SDLK_RETURN || e.key.keysym.sym == SDLK_KP_ENTER)
            {
                //if input is not empty
                if (guessInput != " ")
                {
                    //convert guess to int
                    guess = stoi(guessInput);
                    //reset string
                    guessInput = " ";
                    ++guessCount;
                }
            }
            else if (e.key.keysym.sym == SDLK_BACKSPACE && guessInput.length() > 0)
            {
                guessInput.pop_back();
            }
        }
    }

    SDL_StopTextInput();
    return true;
}

bool gameLoop_update(GameStates& gameState, std::string& dialogue, int guess, int guessCount, Game& game)
{
    if (guessCount == 0)
    {
        dialogue = "I'm thinking of a number between " + std::to_string(game.min) + " and " + std::to_string(game.max) + ". You have " + std::to_string(game.numberofGuesses) + " guesses.";
    }

    // if player ran out of moves
    else if (guessCount == game.numberofGuesses)
    {
        game.game_was_won = false;
        gameState = GameStates::PLAY_AGAIN;
        return false;
    }
    else
    {
        int cmp = compare(game.randomNumber, guess);

        //change dialogue based on comparison
        if (cmp > 0)
        {
            dialogue = "Your guess was too low.";
        }
        else if (cmp < 0)
        {
            dialogue = "Your guess was too high.";
        }
        else
        {
            game.game_was_won = true;
            gameState = GameStates::PLAY_AGAIN;
            return false;
        }
    }

    return true;
}

bool gameLoop_render(LTexture& highLowTexture, LTexture& guessPrompt, std::string guessInput, std::string dialogue, SDL_Color textColor, int textWidth, int guessCount, Game game)
{
    SDL_SetRenderDrawColor(gRenderer, 0xFF, 0xFF, 0xFF, 0xFF);
    SDL_RenderClear(gRenderer);

    highLowTexture.render(gRenderer, 0, 0);
    guessPrompt.render(gRenderer, 350, 250);

    auto bubbleText = LTexture(from_text, gRenderer, dialogue, textColor, textWidth);
    bubbleText.render(gRenderer, 335, 70);

    auto guessCounter = LTexture(from_text, gRenderer, "Guesses remaining: " + std::to_string(game.numberofGuesses - guessCount), textColor, textWidth);
    guessCounter.render(gRenderer, 350, 200);

    auto inputTexture = LTexture(from_text, gRenderer, guessInput, textColor, textWidth);
    if (guessInput == "")
    {
        inputTexture = LTexture(from_text, gRenderer, " ", textColor, textWidth);
    }
    inputTexture.render(gRenderer, 350 + guessPrompt.mWidth, textWidth);

    SDL_RenderPresent(gRenderer);

    return true;
}

void gameLoop(GameStates& gameState, Game& game)
{
    std::string guessInput = " ";
    std::string dialogue = " ";

    SDL_Color const textColor = { 0,0,0 };
    auto const textWidth = 250;

    auto highLowTexture = LTexture(from_surface, gRenderer, "Resources/HiLo.png");
    auto guessPrompt = LTexture(from_text, gRenderer, "Enter a number:", textColor, textWidth);

    int guessCount = 0;
    int guess = 0;
    while (gameLoop_input(gameState, guessInput, guess, guessCount) &&
        gameLoop_update(gameState, dialogue, guess, guessCount, game) &&
        gameLoop_render(highLowTexture, guessPrompt, guessInput, dialogue, textColor, textWidth, guessCount, game))
    {}
}

bool mainMenu_input(GameStates& gameState, Game& game, LTexture& tenButton, LTexture& hundredButton, LTexture& thousandButton)
{
    SDL_Event e;

    while (SDL_PollEvent(&e) != 0)
    {
        if (e.type == SDL_QUIT)
        {
            gameState = GameStates::EXIT;
            return false;
        }
        //change game settings based on button
        if (button_is_pressed(e, tenButton.mButton))
        {
            game.numberofGuesses = 5;
            game.max = 10;
            game.randomNumber = getRandomNumber(game.min, game.max);
            gameState = GameStates::MAIN_LOOP;
            //used to make sure game works properly
            std::cout << game.randomNumber;
            return false;
        }
        else if (button_is_pressed(e, hundredButton.mButton))
        {
            game.numberofGuesses = 7;
            game.max = 100;
            game.randomNumber = getRandomNumber(game.min, game.max);
            gameState = GameStates::MAIN_LOOP;
            std::cout << game.randomNumber;
            return false;
        }
        else if (button_is_pressed(e, thousandButton.mButton))
        {
            game.numberofGuesses = 9;
            game.max = 1000;
            game.randomNumber = getRandomNumber(game.min, game.max);
            gameState = GameStates::MAIN_LOOP;
            std::cout << game.randomNumber;
            return false;
        }
    }
    return true;
}

bool mainMenu_update()
{
    return true;
}

bool mainMenu_render(LTexture& menuTexture, LTexture& tenButton, LTexture& hundredButton, LTexture& thousandButton)
{
    SDL_SetRenderDrawColor(gRenderer, 0xFF, 0xFF, 0xFF, 0xFF);
    SDL_RenderClear(gRenderer);

    menuTexture.render(gRenderer, 0, 0);
    tenButton.render(gRenderer, (SCREEN_WIDTH / 2) - (tenButton.mWidth / 2), 175);
    hundredButton.render(gRenderer, (SCREEN_WIDTH / 2) - (tenButton.mWidth / 2), 225);
    thousandButton.render(gRenderer, (SCREEN_WIDTH / 2) - (tenButton.mWidth / 2), 275);

    SDL_RenderPresent(gRenderer);

    return true;
}

void mainMenu(GameStates& gameState, Game& game)
{
    auto menuTexture = LTexture(from_surface, gRenderer, "Resources/HiLoMenu.png");
    auto tenButton = LTexture(from_surface, gRenderer, "Resources/HiLo10.png");
    auto hundredButton = LTexture(from_surface, gRenderer, "Resources/HiLo100.png");
    auto thousandButton = LTexture(from_surface, gRenderer, "Resources/HiLo1000.png");

    while (mainMenu_input(gameState, game, tenButton, hundredButton, thousandButton) &&
        mainMenu_update() &&
        mainMenu_render(menuTexture, tenButton, hundredButton, thousandButton))
    {}
}

void init()
{
    auto sdlinit = sdl{};
    auto img = sdl_img{};
    auto ttf = sdl_ttf{};

    gWindow = SDL_CreateWindow("HiLo", SDL_WINDOWPOS_UNDEFINED, SDL_WINDOWPOS_UNDEFINED, SCREEN_WIDTH, SCREEN_HEIGHT, SDL_WINDOW_SHOWN);
    if (gWindow == nullptr)
    {
        std::cout << "Failed to create window";
    }
    gRenderer = SDL_CreateRenderer(gWindow, -1, SDL_RENDERER_ACCELERATED | SDL_RENDERER_PRESENTVSYNC);
    if (gRenderer == nullptr)
    {
        std::cout << "Failed to create renderer";
    }
    gFont = TTF_OpenFont("Resources/opensans.ttf", 20);
    if (gFont == nullptr)
    {
        std::cout << "Failed to open font";
    }
}

void close()
{
    if (gFont)
    {
        TTF_CloseFont(gFont);
    }
    if (gWindow)
    {
        SDL_DestroyWindow(gWindow);
    }
    if (gRenderer)
    {
        SDL_DestroyRenderer(gRenderer);
    }
}

int main(int argc, char* args[])
{
    init();

    GameStates gameState = GameStates::MAIN_MENU;
    Game game;

    while (gameState != GameStates::EXIT)
    {
        switch (gameState)
        {
        case GameStates::MAIN_MENU:
            mainMenu(gameState, game);
            break;
        case GameStates::MAIN_LOOP:
            gameLoop(gameState, game);
            break;
        case GameStates::PLAY_AGAIN:
            playAgain(gameState, game);
            break;
        }
    }

    close();

    return 0;
}

I know I still have a lot to learn, but any additional feedback would be greatly appreciated.

I also have a few more questions:

  • I was previously told that my code looks very "C-ish". If it still is, what makes it "C-ish"? I probably just don't know enough about the difference between the two to really tell.

  • What is the difference between doing something like sdl sdlinit; and auto sdlinit = sdl{}? Additionally, how do you determine when to use auto and when to specify?

Thanks again for your time.

\$\endgroup\$
  • \$\begingroup\$ You should flag a moderator and request your accounts be merged \$\endgroup\$ – brug Aug 1 '18 at 4:02
  • \$\begingroup\$ Regarding auto usage, have a look at this quite good answer which hopefully makes things a bit clearer. \$\endgroup\$ – yuri Aug 1 '18 at 6:17
  • \$\begingroup\$ The correct way of using smart ptr for Window & Renderer is : std::unique_ptr<SDL_Window, void(*)(SDL_Window*)> window(SDL_CreateWindow(...), SDL_DestroyWindow); std::unique_ptr<SDL_Renderer, void(*)(SDL_Renderer*)> renderer(SDL_CreateRenderer(window.get(), ...), SDL_DestroyRenderer); Then you use window.get() which returns SDL_Window*, and renderer.get() which returns SDL_Renderer*. \$\endgroup\$ – Julien Vernay Aug 1 '18 at 21:10

Your Answer

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

Browse other questions tagged or ask your own question.