I've been working on a small game as a hobby, and I've been having trouble finding a good way to control the state of the game that could scale easily. I couldn't find a solution that I felt was correct, so I wrote my own.
I created an abstract base class State that has an abstract function update that takes and returns a pointer to the State base class. All children of State's update function should check if conditions are met to change the State if so it should delete the current state, and return another child of State. If it shouldn't transition states it update its data members and return the current state for the next pass in the loop.
I'm not a very experienced C++ programmer, and I feel I'm in over my head. I came to this solution by myself, and even though it work correctly and appears like it will scale fine in the future I'm worried if I went about this wrong or if there are safer or more efficient ways to deal with this problem. Looking back at my code I'm surprised I can delete a pointer of a class inside one of its own functions. Also the update method by design is completely unsafe which could be trouble.
I cut the state machine out of my project, and made a simple model of how it could be used, with two example states.
main.cpp
#include "State.h"
#include "State1.h"
int main() {
State* state = new State1();
//Main loop, how state updates itself.
while (state->isRunning()) {
state = state->update(state);
}
return 0;
}
State.h
class State {
public:
State();
virtual ~State();
//Very unsafe method to update data in the main loop and change state.
//Up to programmer to not mess things up.
virtual State* update(State* currentState) = 0;
//getter for running
bool isRunning() const;
protected:
static bool running;
};
State has a static variable running to keep track of when it should quit the main loop, like when the user closes the window. I felt a static variable was fine here since there should only ever be one state at a time.
State.cpp
#include "State.h"
bool State::running = true;
State::State() {
}
bool State::isRunning() const{
return running;
}
State::~State() {
}
State1.h
#include "State.h"
class State1 : public State {
public:
State1();
~State1();
State* update(State* currentState);
private:
int dataMember;
int* dataMemberThatNeedsToBeDeleted;
};
State1.cpp
Here State2 requires an int to be constructed.
One advantage I found with this model is that states can easily share data between each other when transitioning. For instance if I was building a fighting game and it was currently on a character select state it could share the characters the players chose with the fighting state and that state could construct the characters to fight in that scene, and when one player wins it can share data like k/d ratio and winner with the score state.
States also are responsible to delete all memory allocated.
#include "State1.h"
#include "State2.h"
#include <iostream>
State1::State1() : dataMember(50), dataMemberThatNeedsToBeDeleted(new int(30)) {
std::cout << "creating state1!" << std::endl;
}
State *State1::update(State *currentState) {
std::cout << "State1's first data member is: " << dataMember << std::endl;
std::cout << "State1's other data member is: " << *dataMemberThatNeedsToBeDeleted << std::endl;
std::cout << std::endl;
delete currentState;
return new State2(dataMember);
//In the actual code it would use if/switch statements to control the flow
//example if this state was talking to an inputManager:
// if (key.pressed = esc) {
// running = false;
// return currentState;
// } else if (key.pressed = i) {
// delete currentState;
// return new InventoryState(playerCharacter);
// } else
// return currentState;
}
State1::~State1() {
delete dataMemberThatNeedsToBeDeleted;
}
State2.h
#include "State.h"
class State2 : public State {
public:
State2(int _dataMember);
~State2();
State* update(State* currentState);
private:
int dataMember;
};
State2.cpp
#include "State2.h"
#include "iostream"
State2::State2(int _dataMember) : dataMember(_dataMember) {
std::cout << "creating state2!" << std::endl;
}
State *State2::update(State *currentState) {
std::cout << "State2's data member is " << dataMember << std::endl;
std::cout << std::endl;
running = false;
return currentState;
}
State2::~State2() {}
And I get the correct output of:
creating state1!
State1's first data member is: 50
State1's other data member is: 30
creating state2!
State2's data member is 50
running
is never instantiated andisRunning
is never defined. AlsoIOStream
is not the same as the standard include fileiostream
. Please fix the errors so that the question is on topic. \$\endgroup\$<iostream>
, not<IOStream>
. \$\endgroup\$