GameState is a bad method nowadays ?

Started by
12 comments, last by Ravyne 8 years, 7 months ago

Hi,

I'm still using game state to have different state like menu and in-game for example.

The modern way is to use only scene and code actor to do something for this actual scene.

Is it a bad method nowadays to use game state ?

Thanks

Advertisement
It’s equally relevant as it ever has been, and largely a matter of choice.
When you set up your engine to use states, there are no downsides. If you ultimately end up wanting to use only 1 state for the whole game, then just make one GameState class and do it all that way. It has the flexibility to work in any system, so there is no reason not to do it.

I personally still choose to use game states, as they are still the best way to isolate sections of code in your game.


L. Spiro

I restore Nintendo 64 video-game OST’s into HD! https://www.youtube.com/channel/UCCtX_wedtZ5BoyQBXEhnVZw/playlists?view=1&sort=lad&flow=grid

Generally speaking it is a bad idea because you may want to be making use of multiple states at the same time. Imagine a game where you can drop down a console to read logging info and enter commands while the game is running. Perhaps it could even have a game menu opened at the same time on top of all that other stuff. A good example of this is Valve's Source engine. Team Fortress 2 can have the game running while a console, title screen, server browser and options window are all open at the same time, and that's not the end of it.

State machines, including nested state machines, are an incredibly powerful tool both inside computer science generally and inside games specifically.

While you can use scenes to implement some of those things, and tools like Unity do this, there is also nothing inherently wrong with using state machines to track and flow through the system. It is not "a bad method". It is one method of many.

State machines are the correct tool to use when you flow through states. That is what they are for, that is what they do. If that is what you need part of your game to do, then it is the right tool for the job. When you need to flow from state to state to state, using transition rules along the way, you use a state machine.

For the example of having multiple pieces open at once, that is composition rather than state. Each component is engaged in states. The console is using states to indicate its drawing mode and display status. The option windows are a hierarchy defined by state and state machines. The title screen and menus are a state machine defined by state and state machines. Most of the AI in TF2 is powered by state machines. Most game objects and spawners and other parts are run by state machines.

You don't need to use state machines for everything, and there may be other tools available for various tasks, but still embrace the fact that state machines are found practically everywhere in computer programming, including concepts like the lowly switch statement and if/else trees.

Threading is important, too.

Generally speaking it is a bad idea because you may want to be making use of multiple states at the same time. Imagine a game where you can drop down a console to read logging info and enter commands while the game is running. Perhaps it could even have a game menu opened at the same time on top of all that other stuff. A good example of this is Valve's Source engine. Team Fortress 2 can have the game running while a console, title screen, server browser and options window are all open at the same time, and that's not the end of it.

Solution: Stack of states.
This is what we use on devices such as Nintendo Wii that have the potential to force menus/notifications over your game (low battery on a controller, etc.)

Threading is important, too.

Threads are unrelated to states.


L. Spiro

I restore Nintendo 64 video-game OST’s into HD! https://www.youtube.com/channel/UCCtX_wedtZ5BoyQBXEhnVZw/playlists?view=1&sort=lad&flow=grid

Threads are unrelated to states.

Not necessarily. I imagine a main loop that iterates over multiple states could potentially hang in certain circumstances and this could be avoided by associating a thread with each particular state.

My thoughts being said, could you elaborate on what you mean by that?

My thoughts being said, could you elaborate on what you mean by that?

The state machine used for a game loop should be on a single thread.
States are not threads. The sound thread, rendering thread, and data-loading thread are separate and driven by the game thread. There should be no conflicts.


L. Spiro

I restore Nintendo 64 video-game OST’s into HD! https://www.youtube.com/channel/UCCtX_wedtZ5BoyQBXEhnVZw/playlists?view=1&sort=lad&flow=grid

Threads are unrelated to states.

Not necessarily. I imagine a main loop that iterates over multiple states could potentially hang in certain circumstances and this could be avoided by associating a thread with each particular state.

My thoughts being said, could you elaborate on what you mean by that?

You use state machines at places where you can do several things, and you need to keep track what you are doing right now. You always do one thing at a time with a state machine. A state machine is not active by itself, it's just a value in memory somewhere, representing "this is what you are doing now".

For example, an NPC can walk, sit, and attack. It can do all these things, but only one thing at a time. Depending on the state, you display a different sprite.

Another example is in the game loop, where you have one game loop to display the main menu, the high score, and the game display. You can display all these things, but always one thing at a time. A use of the latter can be key handling. Suppose I press "x". If I do that in the main menu, the program should exit. If I press "x" in the game, I should go down a level. In the key handler you get the state of the game to decide what to do.

A thread is "a point of execution", statements are being executed, decisions are being made, things are being done. You play music, display graphics, compute next action of the NPC. You use state machines to see where you left off the last time. "oh, this NPC is attacking, but nearly dead. Perhaps he should hide instead?", and you change the state of the NPC to "finding a place to hide and rest". The next time you visit the NPC routine, you move it towards safety, as its state told you that is what it should do.

In short, threads are "actors". They run, and move the game forward, playing music, computing next move of the NPC, etc. State machines are used to keep track of what you were doing, so a thread can jump in at the right spot, and decide the next move to make.

Technically, a state machine is anything with a state of its own, it remembers what it is "doing" now. Even a single variable can be seen as a state machine (it keeps track of its value right now, and it can have only one value at a time). In general however, the term "state machine" is used for more elaborate structures. A NPC state machine for example. An NPC can be sitting, standing, attacking, etc. Attached to these states ("modes") is a whole set of other behaviours. Display of graphics, movement, decision making of how to go from here, interaction with player or other NPCs, etc.

Without a coordinated idea of the state of the NPC, it's quite impossible to make it look convincing.

In the design of a game, you also use state machines. In that case, you are not worried about "what am I doing now" (since there is no "now" while designing), but more about "what can an NPC do, what can happen, and how to react".

The states of a state machine act as a structuring mechanism here, you can organize things by state, and reason about each state separately, fleshing out the details. You can also drop all details of each state, and reason about changing states, or overall behaviour through time.

This is deprecated code from my library (i.e. several years old and simply scrapped), but it might be useful to someone:

state.hpp

/* BDAPI - Brain Damage API - Version 0.2.7.0 */
#ifndef BDAPI_UTILITY_STATE_HPP
#define BDAPI_UTILITY_STATE_HPP
#include "bdapi.hpp"

/* includes */

// standard
#include <deque>

// bdapi
#include "utility/container.hpp"

// namespaces
namespace bdapi   {
namespace utility {

/* state class */

class state
{
	result (*function_initialize)() = NULL;
	result (*function_resume    )() = NULL;
	result (*function_iterate   )() = NULL;
	result (*function_suspend   )() = NULL;
	result (*function_kill      )() = NULL;

	string name;

	bl initialized = false;
	bl active      = false;

	s64 initialize_count = 0;
	s64 resume_count     = 0;
	s64 iterate_count    = 0;
	s64 suspend_count    = 0;
	s64 kill_count       = 0;

public: // get

	inline const string& get_name          () const { return name; };

	inline const bl& is_initialized        () const { return initialized; };
	inline const bl& is_active             () const { return active;      };

	inline const s64& get_initialize_count () const { return initialize_count; };
	inline const s64& get_resume_count     () const { return resume_count;     };
	inline const s64& get_iterate_count    () const { return iterate_count;    };
	inline const s64& get_suspend_count    () const { return suspend_count;    };
	inline const s64& get_kill_count       () const { return kill_count;       };

public: // initialization

	state () {};
	state (
		string name,
		result(*function_initialize)(),
		result(*function_resume)(),
		result(*function_iterate)(),
		result(*function_suspend)(),
		result(*function_kill)() );
	state ( const state& copy );
 ~state () {};

	result initialize (
		string name,
		result(*function_initialize)(),
		result(*function_resume)(),
		result(*function_iterate)(),
		result(*function_suspend)(),
		result(*function_kill)() );

public: // core

	result do_initialize ();
	result do_resume     ();
	result do_iterate    ();
	result do_suspend    ();
	result do_kill       ();

};

/* state handler class */

class state_handler
{
	typedef container< state*, std::deque > state_deque;

	state_deque states;

	state* current_state = NULL;

public: // get

	inline const state_deque& get_states () const { return states; };

	inline state* get_current_state      () const { return current_state; };

public: // initialization

	state_handler () {};
	state_handler ( state* initial_state      );
	state_handler ( const state_handler& copy );
 ~state_handler () {};

	result initialize ( state* initial_state );

public: // core

	result do_initialize ();
	result do_resume     ();
	result do_iterate    ();
	result do_suspend    ();
	result do_kill       ();

	result change_state ( state* new_state );
	result end_state    (                  );

public: // query

	string get_name (       ) const;
	string get_name ( s32 i ) const;

	s64 get_initialize_count (       ) const;
	s64 get_initialize_count ( s32 i ) const;
	s64 get_resume_count     (       ) const;
	s64 get_resume_count     ( s32 i ) const;
	s64 get_iterate_count    (       ) const;
	s64 get_iterate_count    ( s32 i ) const;
	s64 get_suspend_count    (       ) const;
	s64 get_suspend_count    ( s32 i ) const;
	s64 get_kill_count       (       ) const;
	s64 get_kill_count       ( s32 i ) const;

	s32 get_size () const;

	bl is_initialized (       ) const;
	bl is_initialized ( s32 i ) const;
	bl is_active      (       ) const;
	bl is_active      ( s32 i ) const;

	bl is_empty () const;
};

/* end */

}
}

#endif

state.cpp

/* BDAPI - Brain Damage API - Version 0.2.7.0 */
#ifndef BDAPI_UTILITY_STATE_UTILITY_CPP
#define BDAPI_UTILITY_STATE_UTILITY_CPP
#include "utility/state.hpp"

/* includes */

// namespaces
namespace bdapi   {
namespace utility {

/* state class */

// constructors
state::state( string name, result(*function_initialize)(), result(*function_resume)(),
result(*function_iterate)(), result(*function_suspend)(), result(*function_kill)() )
{
	initialize( name, function_initialize, function_resume, function_iterate, function_suspend,
		function_kill );
}
state::state( const state& copy )
{
	function_initialize = copy.function_initialize;
	function_resume     = copy.function_resume;
	function_iterate    = copy.function_iterate;
	function_suspend    = copy.function_suspend;
	function_kill       = copy.function_kill;

	name = copy.name;

	initialized = copy.initialized;
	active      = copy.active;

	initialize_count = copy.initialize_count;
	resume_count     = copy.resume_count;
	iterate_count    = copy.iterate_count;
	suspend_count    = copy.suspend_count;
	kill_count       = copy.kill_count;
}

// initializers
result state::initialize( string name, result(*function_initialize)(), result(*function_resume)(),
result(*function_iterate)(), result(*function_suspend)(), result(*function_kill)() )
{
	this->function_initialize = function_initialize;
	this->function_resume     = function_resume;
	this->function_iterate    = function_iterate;
	this->function_suspend    = function_suspend;
	this->function_kill       = function_kill;

	this->name = name;

	return 1;
}

// do initialize
result state::do_initialize()
{
	if( !initialized )
	{
		result initialize_result = (*function_initialize)();

		if( !initialize_result )
		{
			BD_WRITE_ERROR( "Failed to initialize state ~FE\"%s\" "
				"~F8[~FF%i~F8.~FF%i~F8.~FF%i~F8.~FF%i~F8.~FF%i~F8]",
				name.c_str(), initialize_count, resume_count, iterate_count, suspend_count, kill_count );

			return 0;
		}
		else
		{
			initialize_count++;

			initialized = true;

			return do_resume();
		}
	}

	BD_WRITE_ERROR( "Failed to initialize state ~FE\"%s\" "
		"~F8[~FF%i~F8.~FF%i~F8.~FF%i~F8.~FF%i~F8.~FF%i~F8] "
		"~F7as it has already been initialized",
		name.c_str(), initialize_count, resume_count, iterate_count, suspend_count, kill_count );

	return 0;
}

// do resume
result state::do_resume()
{
	if( !initialized )
	{
		BD_WRITE_ERROR( "Failed to resume state ~FE\"%s\" "
			"~F8[~FF%i~F8.~FF%i~F8.~FF%i~F8.~FF%i~F8.~FF%i~F8] "
			"~F7as it has not been initialized",
			name.c_str(), initialize_count, resume_count, iterate_count, suspend_count, kill_count );

		return 0;
	}

	if( !active )
	{
		result resume_result = (*function_resume)();

		if( !resume_result )
		{
			BD_WRITE_ERROR( "Failed to resume state ~FE\"%s\" "
				"~F8[~FF%i~F8.~FF%i~F8.~FF%i~F8.~FF%i~F8.~FF%i~F8]",
				name.c_str(), initialize_count, resume_count, iterate_count, suspend_count, kill_count );

			return 0;
		}
		else
		{
			resume_count++;

			active = true;

			return resume_result;
		}
	}

	BD_WRITE_ERROR( "Failed to resume state ~FE\"%s\" "
		"~F8[~FF%i~F8.~FF%i~F8.~FF%i~F8.~FF%i~F8.~FF%i~F8] "
		"~F7as it is active",
		name.c_str(), initialize_count, resume_count, iterate_count, suspend_count, kill_count );

	return 0;
}

// do iterate
result state::do_iterate()
{
	if( !initialized )
	{
		BD_WRITE_ERROR( "Failed to iterate state ~FE\"%s\" "
			"~F8[~FF%i~F8.~FF%i~F8.~FF%i~F8.~FF%i~F8.~FF%i~F8] "
			"~F7as it has not been initialized",
			name.c_str(), initialize_count, resume_count, iterate_count, suspend_count, kill_count );

		return 0;
	}

	if( !active )
	{
		BD_WRITE_ERROR( "Failed to iterate state ~FE\"%s\" "
			"~F8[~FF%i~F8.~FF%i~F8.~FF%i~F8.~FF%i~F8.~FF%i~F8] "
			"~F7as it inactive",
			name.c_str(), initialize_count, resume_count, iterate_count, suspend_count, kill_count );

		return 0;
	}

	result iterate_result = (*function_iterate)();

	if( !iterate_result )
	{
		BD_WRITE_ERROR( "Failed to iterate state ~FE\"%s\" "
			"~F8[~FF%i~F8.~FF%i~F8.~FF%i~F8.~FF%i~F8.~FF%i~F8]",
			name.c_str(), initialize_count, resume_count, iterate_count, suspend_count, kill_count );

		return 0;
	}
	else
	{
		iterate_count++;

		return iterate_result;
	}
}

// do suspend
result state::do_suspend()
{
	if( !initialized )
	{
		BD_WRITE_ERROR( "Failed to suspend state ~FE\"%s\" "
			"~F8[~FF%i~F8.~FF%i~F8.~FF%i~F8.~FF%i~F8.~FF%i~F8] "
			"~F7as it has not been initialized",
			name.c_str(), initialize_count, resume_count, iterate_count, suspend_count, kill_count );

		return 0;
	}

	if( !active )
	{
		BD_WRITE_ERROR( "Failed to suspend state ~FE\"%s\" "
			"~F8[~FF%i~F8.~FF%i~F8.~FF%i~F8.~FF%i~F8.~FF%i~F8] "
			"~F7as it inactive",
			name.c_str(), initialize_count, resume_count, iterate_count, suspend_count, kill_count );

		return 0;
	}

	result suspend_result = (*function_suspend)();

	if( !suspend_result )
	{
		BD_WRITE_ERROR( "Failed to suspend state ~FE\"%s\" "
			"~F8[~FF%i~F8.~FF%i~F8.~FF%i~F8.~FF%i~F8.~FF%i~F8]",
			name.c_str(), initialize_count, resume_count, iterate_count, suspend_count, kill_count );

		return 0;
	}
	else
	{
		suspend_count++;

		active = false;

		return suspend_result;
	}
}

// do kill
result state::do_kill()
{
	if( !initialized )
	{
		BD_WRITE_ERROR( "Failed to terminate state ~FE\"%s\" "
			"~F8[~FF%i~F8.~FF%i~F8.~FF%i~F8.~FF%i~F8.~FF%i~F8] "
			"~F7as it has not been initialized",
			name.c_str(), initialize_count, resume_count, iterate_count, suspend_count, kill_count );

		return 0;
	}

	if( !do_suspend() )
	{
		BD_WRITE_ERROR( "Failed to terminate state ~FE\"%s\" "
			"~F8[~FF%i~F8.~FF%i~F8.~FF%i~F8.~FF%i~F8.~FF%i~F8] "
			"~F7as it is active",
			name.c_str(), initialize_count, resume_count, iterate_count, suspend_count, kill_count );

		return 0;
	}

	result result_kill = (*function_kill)();

	if( !result_kill )
	{
		BD_WRITE_ERROR( "Failed to terminate state ~FE\"%s\" "
			"~F8[~FF%i~F8.~FF%i~F8.~FF%i~F8.~FF%i~F8.~FF%i~F8]",
			name.c_str(), initialize_count, resume_count, iterate_count, suspend_count, kill_count );

		return 0;
	}
	else
	{
		kill_count++;

		initialized = false;
		active      = false;

		return result_kill;
	}
}

/* state handler class */

// constructors
state_handler::state_handler( state* initial_state )
{
	initialize( initial_state );
}
state_handler::state_handler( const state_handler& copy )
{
	states = copy.states;

	current_state = copy.current_state;
}

// initializers
result state_handler::initialize( state* initial_state )
{
	if( !initial_state )
	{
		BD_WRITE_ERROR( "Initial state has not been allocated" );

		return 0;
	}

	states.insert( initial_state );

	current_state = states.get_back();

	return 1;
}

// do initialize
result state_handler::do_initialize()
{
	if( !current_state )
	{
		BD_WRITE_ERROR( "Initial state has not been allocated" );

		return 0;
	}

	return current_state->do_initialize();
}

// do resume
result state_handler::do_resume()
{
	if( !current_state )
	{
		BD_WRITE_ERROR( "Initial state has not been allocated" );

		return 0;
	}

	return current_state->do_resume();
}

// do iterate
result state_handler::do_iterate()
{
	if( !current_state )
	{
		BD_WRITE_ERROR( "Initial state has not been allocated" );

		return 0;
	}

	return current_state->do_iterate();
}

// do suspend
result state_handler::do_suspend()
{
	if( !current_state )
	{
		BD_WRITE_ERROR( "Initial state has not been allocated" );

		return 0;
	}

	return current_state->do_suspend();
}

// do kill
result state_handler::do_kill()
{
	if( !current_state )
	{
		BD_WRITE_ERROR( "Initial state has not been allocated" );

		return 0;
	}

	return current_state->do_kill();
}

// change state
result state_handler::change_state( state* new_state )
{
	if( !new_state )
	{
		BD_WRITE_ERROR( "New state has not been allocated" );

		return 0;
	}

	if( !is_empty() )
	{
		if( !do_suspend() )
		{
			BD_WRITE_ERROR( "Failed to suspend current state" );

			return 0;
		}

		for_i( states )
		{
			if( new_state == *i )
			{
				states.insert( new_state );

				current_state = states.get_back();

				return do_resume();
			}
		}
	}

	states.insert( new_state );

	current_state = states.get_back();

	return do_initialize();
}

// end state
result state_handler::end_state()
{
	if( is_empty() )
	{
		BD_WRITE_ERROR( "State handler is empty" );

		return 0;
	}

	if( !do_kill() )
	{
		BD_WRITE_ERROR( "Failed to terminate current state" );

		return 0;
	}

	states.pop_back();

	if( is_empty() )
	{
		current_state = NULL;

		return 1;
	}

	while( !states.get_back()->is_initialized() )
	{
		states.pop_back();

		if( is_empty() )
		{
			current_state = NULL;

			return 1;
		}
	}

	current_state = states.get_back();

	return do_resume();
}

// get name
string state_handler::get_name() const
{
	if( !current_state )
	{
		return "";
	}
	else
	{
		return current_state->get_name();
	}
}
string state_handler::get_name( s32 i ) const
{
	if( is_empty() )
	{
		return "";
	}
	else
	{
		return states[i]->get_name();
	}
}

// get initialize count
s64 state_handler::get_initialize_count() const
{
	if( !current_state )
	{
		return -1;
	}
	else
	{
		return current_state->get_initialize_count();
	}
}
s64 state_handler::get_initialize_count( s32 i ) const
{
	if( is_empty() )
	{
		return -1;
	}
	else
	{
		return states[i]->get_initialize_count();
	}
}

// get resume count
s64 state_handler::get_resume_count() const
{
	if( !current_state )
	{
		return -1;
	}
	else
	{
		return current_state->get_resume_count();
	}
}
s64 state_handler::get_resume_count( s32 i ) const
{
	if( is_empty() )
	{
		return -1;
	}
	else
	{
		return states[i]->get_resume_count();
	}
}

// get iterate count
s64 state_handler::get_iterate_count() const
{
	if( !current_state )
	{
		return -1;
	}
	else
	{
		return current_state->get_iterate_count();
	}
}
s64 state_handler::get_iterate_count( s32 i ) const
{
	if( is_empty() )
	{
		return -1;
	}
	else
	{
		return states[i]->get_iterate_count();
	}
}

// get suspend count
s64 state_handler::get_suspend_count() const
{
	if( !current_state )
	{
		return -1;
	}
	else
	{
		return current_state->get_suspend_count();
	}
}
s64 state_handler::get_suspend_count( s32 i ) const
{
	if( is_empty() )
	{
		return -1;
	}
	else
	{
		return states[i]->get_suspend_count();
	}
}

// get kill count
s64 state_handler::get_kill_count() const
{
	if( !current_state )
	{
		return -1;
	}
	else
	{
		return current_state->get_kill_count();
	}
}
s64 state_handler::get_kill_count( s32 i ) const
{
	if( is_empty() )
	{
		return -1;
	}
	else
	{
		return states[i]->get_kill_count();
	}
}

// get size
s32 state_handler::get_size() const
{
	return states.get_size();
}

// is initialized
bl state_handler::is_initialized() const
{
	if( !current_state )
	{
		return false;
	}
	else
	{
		return current_state->is_initialized();
	}
}
bl state_handler::is_initialized( s32 i ) const
{
	if( is_empty() )
	{
		return false;
	}
	else
	{
		return states[i]->is_initialized();
	}
}

// is active
bl state_handler::is_active() const
{
	if( !current_state )
	{
		return false;
	}
	else
	{
		return current_state->is_active();
	}
}
bl state_handler::is_active( s32 i ) const
{
	if( is_empty() )
	{
		return false;
	}
	else
	{
		return states[i]->is_active();
	}
}

// is empty
bl state_handler::is_empty() const
{
	return states.is_empty();
}

/* end */

}
}

#endif

I am aware that my younger coding style causes offense to some programmers, but please keep in mind that my framework no longer uses this code at all. Even though this code commits many cardinal sins against coding etiquette, it remains more or less comprehensible (I think).

This topic is closed to new replies.

Advertisement