Game state management example using XNA 4:https://www.dropbox.com/s/bz6t9yydm6q97nt/MyGameStateManagementFrame.rarThis tutorial aims to show interested people a relatively simple implementation of a powerfull and unbreakable screen management system. It assumes you are running the game in a
fixed timestep. Otherwise adapt transition-speeds according to your game loop.
What is game state management? In games you are certainly familiar with an option-screen, a pause-screen, a gameplay-screen and so on. You can think about games taking place inside a set of screens/states, but only in few of them at a time. So game state management refers to managing the screen transitions and the presentation of the screens.
I am introducing the following design idea:Think about a "state-object" attached to a screen. This state-object provides all the necessary information to handle the screen. Now it only needs a screen manager who is in control of those state-objects. It manages a "stack" of screens. Based on the state of a screen the manager can decide what happens to the screen. If a change has to occure there are only 3 scenarious we usually think of:
1. the manager needs to delete all of the existing screens and then load the new actual screen
2. the manager needs to "hide" the current screen and load a new one on top of it (you can think
of it as a pop up but it is not limited to this contemplation)
3. the manager needs to to delete the current screen and transition to the previous one
(popping from stack)
In all of these 3 scenarios the new screen ends up being on top of the stack. As you will figure out it is the only one being in an "active" state.
You will also see that the actual meat is mainly to understand the load(),returnToPreviousScreen() and the updateStates() function in CsreenManager.
Required programming language skills:If you are familiar with basic polymorphism in C++,C# or Java-like languages it should already suffice to follow. The example is using XNA, it is a framework written in C#. However I try not to use its feature set so that it looks closer to C++. I want to provide a good abstraction but keep it as simple as possible. If you find the example is offering too much functionality or lacking in it feel free to modify it to your needs, I think it is in a good format to be extended or reduced.
Some advice for good practice:You will notice that in order to keep simplicity a new screen is created in updateActive() in the currently active screen and directly passed to the load-function of the screenManager. It means the deactivation of the actual screen is about to start. To avoid a load spike during this process it is good practice to do as much initialization as possible in intialize() and not in constructor(). That way the active screen will transition off seamlessly when the new screen is created. It is because initialize() will be called by the screenManager after the active screen has transitioned off.
Sometimes a certain set of screens might be in need to share the same object/s which shall update independent from screen-transitions.
Example:It can happen in your game that you are traversing several map-screens but all of them are meant to play the same soundtrack and not to restart it whenever a new map-transition occurs. Now it can be vey convoluted to integrate the soundtrack into every screen and to keep track of when it should restart and when it is not. A much nicer solution is to create a "mapMusicPlayer-object" outside of the screens in the game-class itself. Now you can just create the mapMusicPlayer once entering a map-screen and simply delete it when leaving the map-screens. During a map-transition you only check if the mapMusicPlayer is existing (!=null) to know whether you need to create a new one or not. The screens can reference the game-class via screenManager.getGameLink().
In the following the documented code of the 3 classes and an example are presented:The state class: public class Cstate // represents the state of the screen it is attached to
{
public enum EnumState {dead, dying, hiding, hidden, activating, active}; //dead,hidden and active are fixed states
//dying,hiding and activating represent the transition to the fixed states
float fadeInSpeed;
float fadeOutSpeed;
float hidePosition;
float transitionPosition;
EnumState state; // the state is always in one of the 6 enum states
//////////////////////////////////////////////////// BEGIN: INTERNAL HELP FUNCTIONS
void defaultInit()
{
fadeInSpeed = 0.032f;
fadeOutSpeed = 0.064f;
hidePosition = 0.0f;
transitionPosition = 0.0f;
state = EnumState.dead;
}
/////////////////////////////////////////////////////////END: INTERNAL HELP FUNCTIONS
/////////////////////////////////////////////////////////////// BEGIN: PUBLIC INTERFACE
//////////////////////////////////////////////////////// BEGIN: SETTERS & GETTERS
public void setActivating()
{
if (state != EnumState.active)
state = EnumState.activating;
}
public void setDying()
{
if (state != EnumState.dead)
state = EnumState.dying;
}
public void setHiding()
{
if (state != EnumState.hidden)
state = EnumState.hiding;
}
public void setFadeInSpeed(float inSpeed)
{
fadeInSpeed = inSpeed;
if (fadeInSpeed < 0.001f)
fadeInSpeed = 0.001f;
}
public void setFadeOutSpeed(float outSpeed)
{
fadeOutSpeed = outSpeed;
if (fadeOutSpeed < 0.001f)
fadeOutSpeed = 0.001f;
}
public void setHidePosition(float hidePos)
{
hidePosition = hidePos;
if (hidePosition < 0.0f) hidePosition = 0.0f;
else
if (hidePosition > 1.0f) hidePosition = 1.0f;
}
public EnumState getState()
{
return state;
}
public float getTransitionPos()
{
return transitionPosition;
}
//////////////////////////////////////////////////////// END: SETTERS & GETTERS
public Cstate(float hidePos = 0.0f)
{
defaultInit();
setHidePosition(hidePos);
}
public Cstate(float inSpeed,float outSpeed, float hidePos = 0.0f)
{
defaultInit();
setFadeInSpeed(inSpeed);
setFadeOutSpeed(outSpeed);
setHidePosition(hidePos);
}
public void update()
{
if(state == EnumState.activating) // transition to active when activating is set
{
transitionPosition += fadeInSpeed;
if (transitionPosition >= 1.0f)
{
transitionPosition = 1.0f;
state = EnumState.active;
}
}
else if (state == EnumState.hiding) // transition to hidden when hiding is set
{
transitionPosition -= fadeOutSpeed;
if (transitionPosition <= hidePosition)
{
transitionPosition = hidePosition;
state = EnumState.hidden;
}
}
else if (state == EnumState.dying) // transition to dead when dying is set
{
transitionPosition -= fadeOutSpeed;
if(transitionPosition <= 0.0f)
{
transitionPosition = 0.0f;
state = EnumState.dead;
}
}
}
}
The screen class: public class Ctexture2DContainer //eases up texture loading and handles the disposal of unmanaged textures
{
ContentManager content;
~ Ctexture2DContainer()
{
content.Unload(); //unloads unmanaged texture content before deleting itself
}
public Ctexture2DContainer(Game1 game)
{
content = new ContentManager(game.Services, "Content");
}
public Texture2D load(string path)
{
Texture2D texture = content.Load<Texture2D>(path);
return texture;
}
}
public class CsoundContainer // analog container for sounds
{
ContentManager content;
~ CsoundContainer()
{
content.Unload();
}
public CsoundContainer(Game1 game)
{
content = new ContentManager(game.Services, "Content");
}
public SoundEffect load(string path)
{
SoundEffect sound = content.Load<SoundEffect>(path);
return sound;
}
}
public abstract class Cscreen // rule of thumb: the actual game takes places inside a set of screens, but only in few of them at a time
{
protected CscreenManager screenManager;
protected Ctexture2DContainer textures;
protected CsoundContainer sounds;
//matrices corresponding to their draw layers, meant to control the camera if necessary
protected Matrix draw1Matrix, draw2Matrix, draw3Matrix, draw4Matrix, draw5Matrix, draw6Matrix;
protected bool draw1Enabled, draw2Enabled, draw3Enabled, draw4Enabled, draw5Enabled, draw6Enabled; // you can enable/disable the display of draw layers
protected Cstate state; //the screen manager manages screens by controlling their state and taking actions according to it
protected bool backBufferFading;
protected bool updateWhenHidden;
Texture2D texBlackPixel;
//////////////////////////////////////////////////////////// BEGIN: INTERNAL HELP FUNCTIONS
void defaultInit(CscreenManager manager)
{
screenManager = manager;
state = new Cstate();
backBufferFading = true;
updateWhenHidden = false;
draw1Matrix = draw2Matrix = draw3Matrix = draw4Matrix = draw5Matrix = draw6Matrix = Matrix.Identity;
draw1Enabled = true; //note: the first draw layer is enabled by default, don't forget to enable the corresponding layer if you are overloading more draw-functions
draw2Enabled = draw3Enabled = draw4Enabled = draw5Enabled = draw6Enabled = false;
textures = new Ctexture2DContainer(manager.getGameLink());
sounds = new CsoundContainer(manager.getGameLink());
texBlackPixel = screenManager.getTexBlackPixel();
}
// just for blending a black picture over the screen
void fadeBackBufferToBlack(float transitionPos) // used for the default transition effect
{
SpriteBatch spriteBatch = screenManager.getSpriteBatch();
Viewport viewport = screenManager.getGameLink().GraphicsDevice.Viewport;
spriteBatch.Begin();
spriteBatch.Draw(texBlackPixel, new Rectangle(0, 0, viewport.Width, viewport.Height), Color.Black * transitionPos);
spriteBatch.End();
}
//////////////////////////////////////////////////////////////// END: INTERNAL HELP FUNCTIONS
/////////////////////////////////////////////////////////// BEGIN: PROTECTED OVERRIDES
virtual protected void updateActivating() { }
virtual protected void updateActive() { }
virtual protected void updateDying() { }
virtual protected void draw1() { } // feel free to add more or remove draw layers according to your specific needs
virtual protected void draw2() { }
virtual protected void draw3() { }
virtual protected void draw4() { }
virtual protected void draw5() { }
virtual protected void draw6() { }
//////////////////////////////////////////////////////////////// END:PROTECTED OVERRIDES
/////////////////////////////////////////////////////////////////// BEGIN: CONSTRUCTOR & PUBLIC INTERFACE
virtual public void initialize() { } // should only be accessible for the screenManager, but we leave it public here
// if you work in C++ you can add the manager as a friend and keep it protected
// initialize() is meant to be called by the screenManager once after the active screen has transitioned off
// so it is good practice to do as much as possible initialization in intialize() and not in the constructor
// that way the active screen will transition off seamlessly when the new screen is created.
protected Cscreen(CscreenManager screenManager,bool fading=true)
{
defaultInit(screenManager);
backBufferFading = fading;
}
protected Cscreen(CscreenManager screenManager, Cstate newState, bool fading = true)
{
defaultInit(screenManager);
if(newState != null)
state = newState;
backBufferFading = fading;
}
public Cstate getState()
{
return state;
}
public CscreenManager getScreenManager()
{
return screenManager;
}
public void update()
{
if (state.getState() == Cstate.EnumState.activating)
{
updateActivating();
}
else if (state.getState() == Cstate.EnumState.active ||
(updateWhenHidden && (state.getState() == Cstate.EnumState.hiding || state.getState() == Cstate.EnumState.hidden)))
{ // if necessary set "updateWhenHidden" to update a hidden screen,
updateActive(); // think for example of Crysis; while you are in weapon-customization screen the gameplay screen in the back doesn't freeze
}
else if (state.getState() == Cstate.EnumState.dying)
{
updateDying(); // set "backBufferFading" to false and "state.setFadeOutSpeed()" to desired length and update your own transition effect in updateDying() if necessary
}
}
public void draw(Matrix screenResizeMatrix) //"screenResizeMatrix" is meant to resize the screen-output to fit different resolutions
{
if (state.getState() != Cstate.EnumState.dead)
{
SpriteBatch spriteBatch = screenManager.getSpriteBatch();
if (draw1Enabled)
{
spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.NonPremultiplied, SamplerState.LinearClamp,
DepthStencilState.Default, RasterizerState.CullNone, null, draw1Matrix * screenResizeMatrix);
draw1(); //note: matrix multiplication is not commutative,
//know the correct order of execution when you are porting this to a different platform
spriteBatch.End();
}
if (draw2Enabled)
{
spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.NonPremultiplied, SamplerState.LinearClamp,
DepthStencilState.Default, RasterizerState.CullNone, null, draw2Matrix * screenResizeMatrix);
draw2();
spriteBatch.End();
}
if (draw3Enabled)
{
spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.NonPremultiplied, SamplerState.LinearClamp,
DepthStencilState.Default, RasterizerState.CullNone, null, draw3Matrix * screenResizeMatrix);
draw3();
spriteBatch.End();
}
if (draw4Enabled)
{
spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.NonPremultiplied, SamplerState.LinearClamp,
DepthStencilState.Default, RasterizerState.CullNone, null, draw4Matrix * screenResizeMatrix);
draw4();
spriteBatch.End();
}
if (draw5Enabled)
{
spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.NonPremultiplied, SamplerState.LinearClamp,
DepthStencilState.Default, RasterizerState.CullNone, null, draw5Matrix * screenResizeMatrix);
draw5();
spriteBatch.End();
}
if (draw6Enabled)
{
spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.NonPremultiplied, SamplerState.LinearClamp,
DepthStencilState.Default, RasterizerState.CullNone, null, draw6Matrix * screenResizeMatrix);
draw6();
spriteBatch.End();
}
if (state.getState() != Cstate.EnumState.active && backBufferFading) //default transition effect
fadeBackBufferToBlack(1 - state.getTransitionPos());
}
}
}