The Blog

Passing Data Between Game States

Overview

I mentioned in the previous article that passing data between game states is easy enough, and a many readers wanted to know how this could be done.  So by popular demand I will provide 2 methods for how this can be done.  I will also go into more detail on how state changes are done, because the straight-forward method will result in crashes.

 

Simple but Sloppy

The easiest method for doing this is also a bit sloppy, but when you are just trying to gain experience in making games and if your game is fairly simple, this kind of shortcut isn’t really forbidden and might save you time, so I am posting it.

In the general flow of things you will be inheriting from CGame anyway.

class CMyGame : public CGame {
};

And this pointer is passed to all states. Any members you add to your CMyGame class will be accessible by all states. Generally you would want to restrict this to data that could be accessed by any number of states for any purpose, or data that persists for the lifetime of the game. The high score for the current game, etc.
However, if you are in a rush and willing to be a bit sloppy for the sake of just getting things done (a habit you should try not to carry for long), you could also add data to CMyGame that is just meant to be set by one state and used by another. That is simple enough that I don’t think I need to show an example.

 

A Better Way (A Note on Pointers)

There is a better way but it might be a little confusing if you are just starting out in the world of programming.  Firstly, and this might seem a  little random, but there is something to be said of the sizes of pointers and how to correctly cast them to integer types.  Most of the time, I see Windows® programmers casting pointers to type DWORD.  Others cast them to unsigned long.  Both are very much incorrect.  Pointers are always (or at the very least almost) the same size as the native register size of the operating system, so on 64-bit Windows® 7 you will have 64-bit pointers.  DWORD is strictly 32 bits. so that obviously won’t work.  Both long and int change sizes on a per-compiler basis, but there is no specific rule saying one will be the same size as pointers on all compilers.  View the table on 64-bit data models here.  If not for Microsoft® Visual Studio®, the most widely used compiler, long would be easily usable as an integer form of a pointer.

What is the solution?  Use a type that is specifically defined to always be the same size as a pointer.  In Microsoft® Visual Studio® that means UINT_PTR.  This is taken directly from the Microsoft® headers:

// The INT_PTR is guaranteed to be the same size as a pointer.  Its
// size with change with pointer size (32/64).  It should be used
// anywhere that a pointer is cast to an integer type. UINT_PTR is
// the unsigned variation.
//
// __int3264 is intrinsic to 64b MIDL but not to old MIDL or to C compiler.
//
#if ( 501 < __midl )

    typedef [public] __int3264 INT_PTR, *PINT_PTR;
    typedef [public] unsigned __int3264 UINT_PTR, *PUINT_PTR;

    typedef [public] __int3264 LONG_PTR, *PLONG_PTR;
    typedef [public] unsigned __int3264 ULONG_PTR, *PULONG_PTR;

#else  // midl64
// old midl and C++ compiler

#if defined(_WIN64)
    typedef __int64 INT_PTR, *PINT_PTR;
    typedef unsigned __int64 UINT_PTR, *PUINT_PTR;

    typedef __int64 LONG_PTR, *PLONG_PTR;
    typedef unsigned __int64 ULONG_PTR, *PULONG_PTR;

    #define __int3264   __int64

#else
    typedef _W64 int INT_PTR, *PINT_PTR;
    typedef _W64 unsigned int UINT_PTR, *PUINT_PTR;

    typedef _W64 long LONG_PTR, *PLONG_PTR;
    typedef _W64 unsigned long ULONG_PTR, *PULONG_PTR;

    #define __int3264   __int32

#endif
#endif // midl64

Of course, “size with change” is supposed to be “size will change”.  In L. Spiro Engine, this type is “LSUINTPTR”.  Don’t forget it as you read forward.

 

A Better Way (Implementation)

Our goal is to pass data from state A to state B.  The most logical solution is to allocate some data in state A and give that pointer to state B, and of course make sure state B de-allocates that data.  If we are dealing with pointers here, one simple solution is to pass a void * over to state B.  So why did I bother discussing UINT_PTR and LSUINTPTR?  Because it often happens that you just want to send a number over to a state.

My CGameState class need information.  It needs to know which level to load, which enemy scripts to load, etc.  Giving it a structure with all of these details in it is one way to go, but if your organization is good you can send it all the information it needs through just a single number.  Pass it number 3.  Which stage will it load?  Stage 3.  Which enemy scripts will it run?  EnemyScript3.ini.  For example.  By using type UINT_PTR (or LSUINTPTR in my case) you can safely pass pointers when a single number is not enough (with a cast), and also save yourself the trouble of constantly casting basic numbers to void *, which will end up being the most common case.

So now we need to add another member to our previous CState::Init() method.

		virtual LSVOID LSE_CALL		Init( CGame * _pgGame, LSUINTPTR _uptrUser = 0 );

Now the states you define for your games can receive any kind of data that is explicitly meant for them. Of course this means there are a few more restrictions on what states call other states, but in a real-world game that situation can’t be avoided. If your CGameState state class needs to know which level to know, that information has to be passed regardless of what state was trying to activate the CGameState state. That can’t be helped. Of course the above code indicates that if no explicit stage is specified, stage 0 will be loaded. Additionally, checks against NULL inside CGameState will prevent it from crashing if a pointer was necessary. Print a message explaining what data the state intended to receive and you are good to go without stability risks.

 

Setting a State

As I mentioned, I will go into more detail on what it takes to change from one state to another, especially because it matters here more than ever.  The above code shows how a state might receive data, but how do you send that data?

I mentioned before that when you tell the CGame (or CMyGame or whatever) class to change the state, that operation (changing the state) has to be delayed, because if it were to change the state immediately when your CGameState class calls that function, the CGameState class would have been deleted by the CGame class and your game would crash when control returned back to the CGameState class.  Obviously the CGame class can’t just immediately change the state, so it has to remember a few things in order to change the state later, when none of the CGameState functions are being called.  This minor detail should be the burden of the engine, mot the game developer, so the CGame does the following:

	/**
	 * Set the next state.  State is set on the next frame, after the time and input have been updated.
	 *
	 * \param _ui32Id ID of the state.  Can be one of the LSE_GS_* enumerated values or a user-defined per-game value.  To
	 *	generate per-game states (based off this ID), a custom state factory must be provided to this class.
	 * \param _uptrUser User-defined data to be sent to the state in its Init() function.  This has no meaning to the engine.
	 */
	LSVOID LSE_CALL CGame::SetNextState( LSUINT32 _ui32Id, LSUINTPTR _uptrUser ) {
		m_sNextState.ui32Id = _ui32Id;
		m_sNextState.uiptrUser = _uptrUser;
		m_sNextState.bSetNextState = true;
	}

The m_sNextState member is straight-forward. It simply keeps track of everything the CGame class will need to know when the time to actually change the state comes, which will be at the start of the next frame.

So examine the following code.  We are in the main menu and for whatever reason (the user selected the “Begin at Stage 3” option) we will start the game on level 3.

LSVOID LSE_CALL CMainMenuState::Tick( CGame * _pgGame ) {
	…
	// User has pressed magic button “Start on Stage 3”.
	_pgGame->SetNextState( MY_GAME_GAMESTATE, 3 );
	…
}

The above function is called which stores the state and the data to be passed to that state when it is time to change. Here is another example using allocation.

LSVOID LSE_CALL CMainMenuState::Tick( CGame * _pgGame ) {
	…
	// User has pressed magic button “Start on Stage 3”.
	MY_GAME_GAMEDATE_STRUCT * pmggsGameData = new MY_GAME_GAMEDATE_STRUCT();
	pmggsGameData->pcStageName = "Frenzy.stg";
	pmggsGameData->ui32Lives = ui32CurLives;
	pmggsGameData->ui32Continues = ui32PlayerContinues;
	_pgGame->SetNextState( MY_GAME_GAMESTATE, reinterpret_cast<LSUINTPTR>(pmggsGameData) );
	…
}

If m_sNextState.bSetNextState is true, at the start of the next frame the CGame class changes to whatever stage is supposed to be next. It passes to that state the data that is stored in m_sNextState.

If the state being set requires only a number, it can just read it as such, but if it expects to receive a pointer then it must also call delete on that data.

LSVOID LSE_CALL CGameState::Init( CGame * _pgGame, LSUINTPTR _uptrUser ) {
	MY_GAME_GAMEDATE_STRUCT * pmggsInfo = reinterpret_cast(_uptrUser);
	// Load the level specified by pmggsInfo->pcStageName, etc.
	…
	// Done with the data.  Delete it.
	delete pmggsInfo;
}

 

Conclusion

I added a new member to Init() to illustrate how to pass data from one state to another, but this is still a simplification of my engine’s actual CState::Init().  There are still more things your Init() function might find useful, which I have left out for brevity (and frankly to this day have never used, even though I anticipated their use many years ago).  Generally all you will need is a pointer to your CGame class and another _upntrUser parameter to behave as you define, and that will be enough for all of your needs.  I said before that passing data from one state to another state is trivial, but perhaps it is one of those things that only becomes obvious once it is actually shown to you.

Much of my own development as a programmer has relied on such a situation, so if this did not occur to you after reading my initial article on the subject, don’t worry.  Thinking back, it probably wouldn’t have occurred to me either, but once it did I felt ready to tackle a lot of new problems, and I hope this article has done the same for you.

 

 

L. Spiro

About L. Spiro

L. Spiro is veteran of the gaming industry and currently makes video games in Tokyo, Japan, as an R&D programmer for tri-Ace (http://research.tri-ace.com/). L. Spiro has worked on Ghost Recon 2 Online, 187 Ride or Die, Catz 5, Dogz 5, Imagine Happy Cooking, Ready Steady Cook, Leisure Suit Larry Beach Volley, HOT PIXEL, After Dark: Flying Toaster and more, for Ubisoft, Atari, Lucas Arts, Eidos Entertainment, Vivendi Universal, Konami, and more.

3 Awesome Comments So Far

Don't be a stranger, join the discussion by leaving your own comment
  1. none
    January 7, 2013 at 2:51 AM #

    How would each state trigger audio or render entities does the CGame hold a renderer and audio member or is there something else you do?

    • L. Spiro
      January 20, 2013 at 11:44 PM #

      Starting with this framework, all of that is technically up to you, but the way L. Spiro Engine works is that there is a CSceneManager class and your states can create as many of these as it wants (though it usually only needs 1). These are also built on the Tick()/Update() idea and you called these methods manually from your start (since it is the only thing that knows they exist).
      Inside the Draw() method of your state you would call the Draw() method of your scenes and they would go over all the objects in the scene, cull them, put them into render queues for sorting, and tell each object to render itself in the order specified by the render queue.
      Objects render themselves using graphics commands exposed by the graphics library. I simplified this by making all the rendering commands (such as state-setting, etc.) static functions, not instance-based. The exposed functions are just wrappers that provide a common interface between OpenGL, OpenGL ES 2.0, Direct3D 9, and Direct3D 11. For example, “static LSVOID CFnd::SetClearDepth( LSREAL _fValue );”

      Note that only state-setting functions are static in my engine. A vertex buffer, texture, render target, etc., are all still instance-based, of course. The higher-level scene manager culls and sorts the objects, but the objects themselves do the final rendering using their own vertex/index buffers etc.

      As for audio, the sound library is also a set of static functions which again act just as wrappers for the underlying API (in my case there is only OpenAL, but it can be expanded to include any other audio libraries).
      And then again you create instances of a listener, source, etc. And again these are registered with the scene manager, which handles updating them when you call Tick(). Note that the audio is actually run on a second thread which also automatically updates audio events and advances all of the audio players on a regular basis, even if you don’t also signal an update from the scene manager. The only reason the scene manager also signals an update of all the audio events is so that added events will start playing immediately. Otherwise they would have to wait for the audio thread to get to them, which takes between 50 and 200 milliseconds depending on your needs. This also means care must be taken to ensure thread safety.

      Audio events could come from anywhere. You can add them to the scene manager within the logic of your state’s Tick() function as you see fit. Of course it always helps to set up a framework to make the playing of certain sounds automatic, such as things colliding and making a bump noise. Some audio events might come from the models themselves as they make foot-step sounds during a walking animation. It is open-ended, but the main point is to add them to the scene manager so that it can manage them for you.

      L. Spiro

  2. Tony
    December 3, 2013 at 12:01 AM #

    I just felt I should thank you for these articles. Very helpful and I can’t thank you enough for spending your time posting these.

Leave a Comment

Remember to play nicely folks, nobody likes a troll.