Talking about OOD, GUI hierarchies are often organized using the Composite pattern.
In order to connect button clicks with events like switching menus, colorizing GUI elements or similar, you could have a look at the Observer or Command pattern. One possibility consists of using abstract base classes, where derived classes implement the concrete action. C++11 offers an interesting alternative: std::function allows you to link arbitrary actions (even in a single line, using lambda expressions).
Concerning states, there are -- as mentioned by eXpl0it3r -- different possibilities. If two states are mutually exclusive, it will be enough to simply store the current state; otherwise you may think about an advanced architecture such as a stack. An easy way to refer to different states are enums, which can be mapped to the corresponding state instances.
I'm not going to separate the quote into different bits because I don't know where to split it, sorry.
std::function is indeed quite nice, and I've used it a few times, but it's not enough for what I want to do. Since I wanted something modular, having an enum for storing states wasn't much of a viable option, since I'd have to change it every time I add a state.
I came up with a base State class, like this one:
class State
{
public:
StateEngine &parent;
const size_t name_hash;
virtual void activate() = 0;
virtual void deactivate() = 0;
virtual void loop() = 0;
State(StateEngine &parent_):parent(parent_){}
};
The StateEngine class looks like this:
class StateEngine
{
private:
friend class State;
std::hash<std::string> hash_function;
std::vector<std::shared_ptr<State>> states;
size_t current_state_hash;
State &get_state(size_t &state_hash);
public:
void set_current_state(size_t state_hash);
State &get_current_state();
State &add_state(State &new_state);
State &operator[](size_t state_hash);
};
Example usage would be like this:
class Game : public State
{
private:
sf::RenderWindow &window;
sf::RectangleShape player;
size_t menu_hash;
public:
void activate()
{
player.setPosition({0.f,0.f});
player.setSize({30.f,30.f});
player.setFillColor(sf::Color::Red);
active = true;
}
void deactivate()
{
// We could do something like clear enemy vectors here..
active = false;
}
void loop()
{
sf::Event event;
while(window.pollEvent(event))
{
if(event.type == sf::Event::Closed)
window.close();
else if(event.type == sf::KeyEvent && event.key.code == sf::Keyboard::Escape)
parent.set_current_state(menu_hash);
}
player.setPosition({
static_cast<float>(sf::Mouse::getPosition(window).x),
static_cast<float>(sf::Mouse::getPosition(window).y)
});
window.clear();
window.draw(player);
window.display();
}
Game(StateEngine &parent_, sf::RenderWindow &window_) : State(parent_) window(window_)
{
name_hash = parent.hash_function("game");
menu_hash = parent.hash_function("menu");
}
};
class MenuScreen : public State
{
private:
sf::Text text;
sf::Font font;
sf::RenderWindow &window;
size_t game_hash;
public:
void activate()
{
font.loadFromFile("some_font.ttf");
text.setString("lol");
text.setCharacterSize(42);
text.setPosition({100.f,100.f});
active = true;
}
void deactivate()
{
active = false;
}
void loop()
{
sf::Event event;
while(window.pollEvent(event))
{
if(event.type == sf::Event::MousePressed)
if(text.getGlobalBounds().contains(sf::Mouse::getPosition(window))
parent.set_current_state(game_hash);
else if(event.type == sf::Event::Closed)
window.close();
}
}
Menu(StateEngine &parent_, sf::RenderWindow &window_) : State(parent_), window(window_)
{
game_hash = parent.hash_function("game");
name_hash = parent.hash_function("menu");
}
};
int main()
{
sf::RenderWindow window{{800,600}, "Test"};
StateEngine state_engine;
Game game_state(window);
Menu menu_state(menu);
while(window.isOpen())
{
state_engine.get_current_state().loop();
}
return 0;
}
I know the code is long, sorry. This code differs from my actual implementation, which isn't currently working. :P
I think this is a good way to make everything modular. If I get my implementation working, this could be great. :D
Any apparent flaws in the system? I'd be happy to have them pointed out to me.
I guess there are some mistakes:
class MenuScreen : public State
// ...
Menu menu_state(menu);
Game(StateEngine &parent_, sf::RenderWindow &window_)
// ...
Game game_state(window);
Menu(StateEngine &parent_, sf::RenderWindow &window_)
// ...
Menu menu_state(menu);
Missing implementation of StateEngine.
Anyways as for the code design:
- In C++ size_t is defined in the std namespace, so you should be using std::size_t instead.
- Personally I don't feel that loop() is a fitting function name, since it's too generic and doesn't really describe the functionality.
- What is the reason, that you use a std::hash and map it somehow to a std::vector? Why don't you simply go for std::unordered_map, which basically is a hash table?
- Are you sure, you want to use shared_ptr rather than unique_ptr? I.e. should your states ever be shared?
- static_cast<sf::Vector2f>() does the trick as well, if you don't need to change x or y.
- Wouldn't it be a good idea to integrate the split between update and draw at the state level?
- I highly suggest to use some kind of indicator in the variable name, to point out what's a member variable and what's a local variable. It will make your code much more readable, especially once you've way more content in there.
- How is the method of using hashes more flexible than enums? Currently you've to define them in every state...
I guess there are some mistakes:
class MenuScreen : public State
// ...
Menu menu_state(menu);
Game(StateEngine &parent_, sf::RenderWindow &window_)
// ...
Game game_state(window);
Menu(StateEngine &parent_, sf::RenderWindow &window_)
// ...
Menu menu_state(menu);
Missing implementation of StateEngine.
Yeah, the point wasn't to copy-paste my code into here, since it's not working. I literally wrote all of that in this forum post editor because I don't want to show an implementation, I want to show how my system would work. It's all theory.
As for the actual mistakes, yeah, those might be a bit confusing.
Personally I don't feel that loop() is a fitting function name, since it's too generic and doesn't really describe the functionality.
It is, because the state isn't necessarily graphical. It is only what happens in the loop. In the Game class, I could have separated it into multiple functions, which I'm already doing.
What is the reason, that you use a std::hash and map it somehow to a std::vector? Why don't you simply go for std::unordered_map, which basically is a hash table?
How is the method of using hashes more flexible than enums? Currently you've to define them in every state...
Hashes are good because they're fast. It's more flexible than an enum because I don't need all the states predefined somewhere. I could remove the Game state entirely and it would still compile and run. As for the unordered_map, I'm not sure what you're talking about, as I'm not storing hashes in a std::vector?
Are you sure, you want to use shared_ptr rather than unique_ptr? I.e. should your states ever be shared?
I highly suggest to use some kind of indicator in the variable name, to point out what's a member variable and what's a local variable. It will make your code much more readable, especially once you've way more content in there.
As I may have badly conveyed, I'm only demonstrating the design of this state system. I might use unique_ptr later, and since the whole thing was written on the fly, I didn't really care for variable naming.
static_cast<sf::Vector2f>() does the trick as well, if you don't need to change x or y.
I didn't know that worked. Thanks.