3
\$\begingroup\$

I just finished making a flappy bird type game and would like some feedback on my game loop and state machine. The game loop was based on deWiTTER's article, and the state machine was based off of an answer by Indi on my first game. The code was condensed and re-arranged in order to just have a minimum viable project to show the basic idea. If needed, this is the githhub for the full game including the SDL and LTexture files.

One main question: I've read both deWiTTER's and Gaffer's articles on timesteps. I think I understand the gist of both, deWiTTER's a little more so. Is one solution better than the other? Or does it just depend on the game?

Also, is there an ideal amount of updates per second for physics? deWiTTER's argues that 25 is more than enough, but Gaffer's example uses 100 updates per second.

Beside that, any feedback on either the game loop or my state machine will be really helpful.

main.cpp

#include <stack>

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

#include "SDLInit.h"
#include "Texture.h"
#include "StateMachine.h"
#include "Game.h"

int main(int argc, char* args[])
{
    sdl sdlinit;
    AssetManager assets;
    Game game;

    //used for retro-ish look
    SDL_RenderSetLogicalSize(assets.renderer_.get(), VIRTUAL_WIDTH, VIRTUAL_HEIGHT);

    std::stack<std::unique_ptr<state>> stateStack;
    stateStack.push(std::unique_ptr<state>(new TitleState{&assets, &game}));

    int nextGameTick = SDL_GetTicks();
    int loops;

    while (!stateStack.empty())
    {
        loops = 0;
        state::update_result_t result;
        stateStack.top()->input();

        //update only 25fps but render as fast as possible
        while (SDL_GetTicks() > nextGameTick && loops < MAX_FRAMESKIP)
        {
            game.nowTime_ = SDL_GetTicks() / 1000.f;
            game.deltaTime_ = game.nowTime_ - game.thenTime_;
            game.thenTime_ = game.nowTime_;

            result = stateStack.top()->update();

            nextGameTick += SKIP_TICKS;
            loops++;
        }

        //used for smoother movement
        assets.interpolation_ = float(SDL_GetTicks() + SKIP_TICKS - nextGameTick)
            / float(SKIP_TICKS);

        if (result.type == state::update_result_type::continue_state)
        {
            stateStack.top()->render();
        }
        else if (result.type == state::update_result_type::replace_state)
        {
            stateStack.top().swap(result.next);
        }
        else if (result.type == state::update_result_type::push_state)
        {
            stateStack.push(std::move(result.next));
        }
        else if (result.type == state::update_result_type::end_state)
        {
            stateStack.pop();
        }
        else if (result.type == state::update_result_type::quit_state)
        {
            while (!stateStack.empty())
                stateStack.pop();
        }
    }

    return 0;
}

Globals.h

#pragma once

const int TICKS_PER_SECOND = 25;
const int SKIP_TICKS = 1000 / TICKS_PER_SECOND;
const int MAX_FRAMESKIP = 5;

constexpr int SCREEN_WIDTH = 1024;
constexpr int SCREEN_HEIGHT = 576;

constexpr int VIRTUAL_WIDTH = 512;
constexpr int VIRTUAL_HEIGHT = 288;

constexpr int BIRD_HEIGHT = 24;
constexpr int BIRD_WIDTH = 38;

constexpr int PIPE_WIDTH = 70;
constexpr int PIPE_HEIGHT = 288;
constexpr float PIPE_SPEED = 4.0f;

constexpr int BACKGROUND_LOOPING_POINT = 576;
constexpr float BACKGROUND_SCROLL_SPEED = 2.0f;

StateMachine.h

#pragma once
#include "Game.h"

#include <algorithm>
#include <vector>

class state
{
public:
    enum class update_result_type
    {
        continue_state,
        end_state,
        replace_state,
        push_state,
        quit_state,
    };

    struct update_result_t
    {
        update_result_type type = update_result_type::continue_state;
        std::unique_ptr<state> next = nullptr;
    };

    state() = default;
    virtual ~state() = default;

    virtual void input() = 0;
    virtual update_result_t update() = 0;
    virtual void render() = 0;

    state(state const&) = delete;
    auto operator=(state const&) = delete;

};

class PlayState : public state
{
public:
    PlayState(AssetManager* assets, Game* game);

    void input() override;
    update_result_t update() override;
    void render() override;

private:
    enum class UserChoice
    {
        none,
        pause,
        start,
        quit,
    };

    UserChoice choice_ = UserChoice::none;
    AssetManager* assets_ = nullptr;
    Game* game_ = nullptr;
    std::vector<PipePair> pipePair_;
    float timer_ = 0;
    int lastYPos_ = -190;
    int score_ = 0;
};


class CountdownState : public state
{
public:
    CountdownState(AssetManager* assets, Game* game);

    void input() override;
    update_result_t update() override;
    void render() override;

private:
    enum class UserChoice
    {
        none,
        start,
        quit,
    };

    UserChoice choice_ = UserChoice::none;
    AssetManager* assets_ = nullptr;
    Game* game_ = nullptr;
    float timer_ = 0.f;
    float countdownTime_ = 1.f;
    int count_ = 3;

};

class TitleState : public state
{
public:
    TitleState(AssetManager* assets, Game* game);

    void input() override;
    update_result_t update() override;
    void render() override;

private:
    enum class UserChoice
    {
        none,
        start,
        quit,
    };

    UserChoice choice_ = UserChoice::none;
    AssetManager* assets_ = nullptr;
    Game* game_ = nullptr;

};

StateMachine.cpp

#include "StateMachine.h"
#include "Globals.h"
#include <iostream>

PlayState::PlayState(AssetManager* assets, Game* game) : assets_(assets), game_(game)
{}

void PlayState::input()
{
    SDL_Event e;

    while (SDL_PollEvent(&e) != 0)
    {
        if (e.type == SDL_QUIT)
        {
            choice_ = UserChoice::quit;
        }
        else if (e.type == SDL_KEYDOWN)
        {
            if (e.key.keysym.sym == SDLK_RETURN)
            {
                choice_ = UserChoice::pause;
            }
        }
    }
}

state::update_result_t PlayState::update()
{
    assets_->updateBackground();

    static int pipeSpawnTime = 0;
    timer_ += game_->deltaTime_;

    if (timer_ >= pipeSpawnTime)
    {
        timer_ -= pipeSpawnTime;
        pipeSpawnTime = getRandomNumber(1, 2);
        pipePair_.push_back(PipePair{ lastYPos_ });
        lastYPos_ = getRandomNumber(-220, -160);
    }

    for (int i = 0; i < pipePair_.size(); i++)
    {
        pipePair_[i].update(assets_);
    }
    for (int i = 0; i < pipePair_.size(); i++)
    {
        if (pipePair_[i].remove_)
        {
            pipePair_.erase(pipePair_.begin());
        }
    }

    if (choice_ == UserChoice::quit)
        return { state::update_result_type::quit_state };
    else if (choice_ == UserChoice::pause)
    {
        assets_->scrolling_ = false;
        return { state::update_result_type::replace_state, std::unique_ptr<state>{new TitleState{assets_, game_ }} };
    }


    return {};

}

void PlayState::render()
{
    SDL_RenderClear(assets_->renderer_.get());
    assets_->renderBackground();
    for (int i = 0; i < pipePair_.size(); i++)
    {
        pipePair_[i].render(assets_);
    }

    SDL_RenderPresent(assets_->renderer_.get());
}

CountdownState::CountdownState(AssetManager* assets, Game* game) : assets_(assets), game_(game)
{}

void CountdownState::input()
{
    SDL_Event e;

    while (SDL_PollEvent(&e) != 0)
    {
        if (e.type == SDL_QUIT)
        {
            choice_ = UserChoice::quit;
        }
    }
}

state::update_result_t CountdownState::update()
{

    timer_ += game_->deltaTime_;

    if (timer_ > countdownTime_)
    {
        timer_ -= countdownTime_;
        count_--;
    }

    if (count_ == 0)
    {
        assets_->scrolling_ = true;
        return { state::update_result_type::replace_state, std::unique_ptr<state>{new PlayState{assets_, game_}} };
    }

    if ( choice_ == UserChoice::quit)
        return { state::update_result_type::quit_state };

}

void CountdownState::render()
{
    SDL_RenderClear(assets_->renderer_.get());
    assets_->renderBackground();
    LTexture timerText = { from_text, assets_->hugeFont_.get(), assets_->renderer_.get(), std::to_string(count_), assets_->whiteFont_, assets_->fontWrapWidth_ };
    timerText.render(assets_->renderer_.get(), VIRTUAL_WIDTH / 2 - timerText.width_ / 2, 120);
    SDL_RenderPresent(assets_->renderer_.get());
}

TitleState::TitleState(AssetManager* assets, Game* game) : assets_(assets), game_(game)
{}

void TitleState::input()
{
    SDL_Event e;

    while (SDL_PollEvent(&e) != 0)
    {
        if (e.type == SDL_QUIT)
        {
            choice_ = UserChoice::quit;
        }
        else if (e.type == SDL_KEYDOWN)
        { 
            if (e.key.keysym.sym == SDLK_RETURN)
            {
                choice_ = UserChoice::start;
            }
        }
    }
}

state::update_result_t TitleState::update()
{
    switch (choice_)
    {
    case UserChoice::quit:
        return { state::update_result_type::quit_state };
    case UserChoice::start:
        return { state::update_result_type::replace_state, std::unique_ptr<state>{new CountdownState{assets_, game_}} };
    default:
        return {};
    }
}

void TitleState::render()
{
    SDL_RenderClear(assets_->renderer_.get());
    assets_->renderBackground();
    SDL_RenderPresent(assets_->renderer_.get());
}

Game.h

#pragma once

#include <vector>
#include <random>

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

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


std::mt19937& random_engine();
int getRandomNumber(int x, int y);

struct Game
{
    float thenTime_ = 0.0f;
    float nowTime_ = 0.0f;
    float deltaTime_ = 0.0f;
};

struct AssetManager
{
    AssetManager();
    void updateBackground();
    void renderBackground();
    void renderAward(int score);

    std::unique_ptr<SDL_Window, sdl_deleter> window_{ SDL_CreateWindow("Flippy", 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_ACCELERATED | SDL_RENDERER_PRESENTVSYNC) };
    std::unique_ptr<TTF_Font, sdl_deleter> smallFont_{ TTF_OpenFont("resources/font.ttf", 8) };
    std::unique_ptr<TTF_Font, sdl_deleter> mediumFont_{ TTF_OpenFont("resources/font.ttf", 14) };
    std::unique_ptr<TTF_Font, sdl_deleter> flappyFont_{ TTF_OpenFont("resources/flappy.ttf", 28) };
    std::unique_ptr<TTF_Font, sdl_deleter> hugeFont_{ TTF_OpenFont("resources/font.ttf", 56) };

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

    LTexture backgroundTexture_ = { from_surface, renderer_.get(), "resources/background.png" };
    LTexture groundTexture_ = { from_surface, renderer_.get(), "resources/ground.png" };
    LTexture flippyTexture_ = { from_surface, renderer_.get(), "resources/flippy.png" };

    LTexture flippyWhaleText_ = { from_text, flappyFont_.get(), renderer_.get(), "Flippy Whale", whiteFont_, fontWrapWidth_ };
    LTexture pressEnterText = { from_text, smallFont_.get(), renderer_.get(), "Press Enter", whiteFont_, fontWrapWidth_ };
    LTexture youLostText_ = { from_text, mediumFont_.get(), renderer_.get(), "Oof! You Lost!", whiteFont_, fontWrapWidth_ };
    LTexture restartText_ = { from_text, mediumFont_.get(), renderer_.get(), "Press Enter to Play Again!", whiteFont_, fontWrapWidth_ };

    bool scrolling_ = false;
    float backgroundScroll_ = 0;
    float groundScroll_ = 0;
    float interpolation_ = 0.0f;
    std::vector<LTexture> corals;

};

struct Pipe
{
    enum class Orientation
    {
        TOP,
        BOTTOM,
    };

    Pipe(Orientation side, int yPos) : orientation_(side), yPos_(yPos)
    {}

    void render(AssetManager* assets, int color);

    float xPos_ = VIRTUAL_WIDTH + 64;
    int yPos_ = 0;
    int width_ = PIPE_WIDTH;
    int height_ = PIPE_HEIGHT;
    Orientation orientation_ = Orientation::TOP;
};

struct PipePair
{
    PipePair(int yPos) : yPos_(yPos)
    {}

    void update(AssetManager* assets);
    void render(AssetManager* assets);

    int gapHeight_ = getRandomNumber(70, 90);
    bool scored_ = false;
    float xPos_ = VIRTUAL_WIDTH + 32;
    int yPos_ = 0;
    bool remove_ = false;
    int color_ = getRandomNumber(0, 2);
    Pipe pipes_[2] = { Pipe(Pipe::Orientation::TOP, yPos_), Pipe(Pipe::Orientation::BOTTOM, yPos_ + PIPE_HEIGHT + gapHeight_) };

};

Game.cpp

#include "Game.h"

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

AssetManager::AssetManager()
{
    corals.push_back(LTexture{ from_surface, renderer_.get(), "resources/purplecoral.png" });
    corals.push_back(LTexture{ from_surface, renderer_.get(), "resources/pinkcoral.png" });
    corals.push_back(LTexture{ from_surface, renderer_.get(), "resources/yellowcoral.png" });
}

void AssetManager::updateBackground()
{
    if (scrolling_ == true)
    {
        backgroundScroll_ += BACKGROUND_SCROLL_SPEED;
        if (backgroundScroll_ > BACKGROUND_LOOPING_POINT)
        {
            backgroundScroll_ = 0;
        }
    }
}

void AssetManager::renderBackground()
{
    if (scrolling_ == true)
        backgroundTexture_.render(renderer_.get(), std::max(static_cast<int>(-backgroundScroll_ - (BACKGROUND_SCROLL_SPEED * interpolation_)), -BACKGROUND_LOOPING_POINT), 0);
    else
        backgroundTexture_.render(renderer_.get(), -backgroundScroll_, 0);
}

void PipePair::update(AssetManager* assets)
{
    if (xPos_ > -PIPE_WIDTH * 2)
    {
        xPos_ -= PIPE_SPEED;

        for (int i = 0; i < 2; i++)
        {
            pipes_[i].xPos_ -= PIPE_SPEED;
        }
    }
    else
    {
        remove_ = true;
    }
}

void PipePair::render(AssetManager* assets)
{
    for (int i = 0; i < 2; i++)
    {
        pipes_[i].render(assets, color_);
    }
}

void Pipe::render(AssetManager* assets, int color)
{
    if (orientation_ == Pipe::Orientation::TOP)
    {
        assets->corals[color].render(assets->renderer_.get(), xPos_ - (PIPE_SPEED * assets->interpolation_), yPos_, nullptr, 0.0, nullptr, SDL_FLIP_VERTICAL);
    }
    else
    {
        assets->corals[color].render(assets->renderer_.get(), xPos_ - (PIPE_SPEED * assets->interpolation_), yPos_);
    }
}
\$\endgroup\$
  • \$\begingroup\$ "Also, is there an ideal amount of updates per second for physics? deWiTTER's argues that 25 is more than enough, but Gaffer's example uses 100 updates per second." I think that's a matter of specification. You start with a number, see how smooth it runs, whether it could/should be improved and act accordingly. Modern computers won't have any trouble with 100 updates a second, but a budget device bought 10 years ago might be a completely different story the moment you run something heavy. It's all about what you consider to be enough and how much resources you want to waste on it. \$\endgroup\$ – Mast Nov 12 '18 at 7:24

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.