0
\$\begingroup\$

I'm creating a game engine which utilizes a state machine following this GameDevGeek tutorial. My concern is the use of circular dependency which I've heard is bad. The game engine has the game state but the game state needs access to the game engine for a state to be able to transition to a new state. Is this a proper way of doing it? Namely my implementation, includes, and forward declarations. On another note, why does syntax highlighting randomly turn off after two files have #included each other? Here's my code:

// Game.h
#ifndef GAME_H
#define GAME_H

#include "GameState.h"
#include "SFML\Graphics.hpp"

class GameState;

class Game
{
public:
    Game();
    ~Game();

    void setState(GameState* state);
    void handleEvents();
    void update();
    void render();

private:
    sf::RenderWindow window_;
    GameState* state_;
};

#endif

// Game.cpp    
#include "Game.h"

Game::Game()
    : window_(sf::VideoMode(500, 500), "Test Game", sf::Style::Default) ,
{
    window_.setKeyRepeatEnabled(false);
    state_ = nullptr;
}

Game::~Game()
{
    if (state_ != nullptr)
    {
        state_->destroy();
        delete state_;
    }
}    

void Game::setState(GameState* state)
{
    if (state_ != nullptr)
    {
        state_->destroy();
        delete state_;
    }
    state_ = state;
    if (state_ != nullptr)
    {
        state_->init();
    }
}

// more code...

// GameState.h

#ifndef GAME_STATE_H
#define GAME_STATE_H

#include "Game.h"
#include "SFML\Graphics.hpp"

class Game;

class GameState
{
public:
    virtual void init() = 0;
    virtual void update(Game* game) = 0;
    virtual void render(sf::RenderWindow &window) = 0;
    virtual void destroy() = 0;

    void changeState(Game* game, GameState* state);
};

#endif GAME_STATE_H

// GameState.cpp
#include "GameState.h"

void GameState::changeState(Game* game, GameState* state)
{
    game->setState(state);
}
\$\endgroup\$
  • \$\begingroup\$ Use the observer pattern. The GameState class signals a StateChangeEvent which is handled by the Game class (or any class for that matter). Thus, there's no circular dependency, you still get the benefits described in the article, the GameState class doesn't pick up unnecessary dependencies and the GameState class doesn't care who handles its event. \$\endgroup\$ – Dunk Sep 14 '16 at 22:01
1
\$\begingroup\$

Your issue can be solved by moving some includes about. The inclusion of file containing the declaration of another class is only needed when specific details are required (which functions are publicly available, how much space should I reserve for a field of that type, etc.).

Since your GameState header only ever uses pointers (references might be better suited here so others don't mistake it for an array and whatnot) to a Game object, you can get away with a forward declaration there. This is because the size of a pointer and reference is independent of the size of the data they point to; the compiler only needs to know about the existance of the class/struct at this moment.

Down in the GameState.cpp source file you are invoking functions of Game, so you need the signature there. Simply add #include "Game.h" heere and you should be good to go.

\$\endgroup\$
1
\$\begingroup\$

I agree, it does seem a little gross for each state to hold a reference to the Game just so the state can request a state change. But, in a way it makes sense: the Game class is acting as the state manager -- it keeps track of the current state and it knows how to change states.

I can think of a couple ways to break the explicit dependency between these two classes, but I think this one is the least complex and still achieves good decoupling:

Make and use an interface that defines the role of a state changer

Basically, this won't change your current code that much, except instead of the GameState referring to an object of type Game, it would refer to an interface/abstract class type that declares a function for changing state. So something like this:

  • Create an abstract class/interface that declares the changeState(GameState * state) function; let's call it IStateChanger
  • Have the Game class extend/implement IStateChanger (Game already has setState -- so after extending IStateChanger, you would just have to rename setState to changeState)
  • Change GameState::changeState's first argument from Game * to IStateChanger *.

Any GameState::changeState calls that used to pass an instance of Game will still work, since Game would be a sub-class of IStateChanger; so why is this any better if we're still passing the Game instance around?

  • With your current code, GameState::changeState has the ability to call other functions in Game that have nothing to do with changing state (e.g. render) and could potentially mess up the program flow. By using IStateChanger, the only function that is available to GameState::changeState is exactly the one you intended to be used by passing this object.

    If you're the only one working on this project, then you know not to call any function other than Game::setState; but if someone else contributes to your project, they might not know, and they might accidentally use what's available in ways that you didn't originally intend. This is one of the main reasons "good design" is important: good design makes intention clear and makes it hard to screw things up.

  • In the future, you may decide that Game should not handle state management. Maybe you decide that you want to make a class that specifically handles this, say, StateManager. Then all you would have to do is make StateManager extend/implement IStateChanger and then you can just swap out Game for StateManager wherever Game is being assigned to an IStateChanger declaration. This is another major benefit of "good design": easy to make changes and swap out one implementation with another.

Hope this helps.

\$\endgroup\$
0
\$\begingroup\$

Add static members and methods to the class GameState to handle the responsibilities that you've forced into the main Game class.

Each instance of GameState will then have access to one, shared state environment like they do now, and Game doesn't have to carry around the baggage.

Bonus is that if you later change the state implementation, an example would be to add hierarchical states, then all the changes are internal to the state classes and not spread all over outside files.

\$\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.