Welcome, Guest. Please login or register.

Login with username, password and session length

 
Advanced search

1411490 Posts in 69371 Topics- by 58428 Members - Latest Member: shelton786

April 24, 2024, 11:00:03 PM

Need hosting? Check out Digital Ocean
(more details in this thread)
TIGSource ForumsCommunityTownhallForum IssuesArchived subforums (read only)TutorialsGame State Management
Pages: [1]
Print
Author Topic: Game State Management  (Read 2837 times)
J-Snake
Level 10
*****


A fool with a tool is still a fool.


View Profile WWW
« on: July 01, 2013, 07:37:00 PM »

Game state management example using XNA 4:
https://www.dropbox.com/s/bz6t9yydm6q97nt/MyGameStateManagementFrame.rar

This 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:


Code:
    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:

Code:
    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());
            }
        }

      

    }
« Last Edit: August 10, 2013, 02:26:28 PM by J-Snake » Logged

Independent game developer with an elaborate focus on interesting gameplay, rewarding depth of play and technical quality.<br /><br />Trap Them: http://store.steampowered.com/app/375930
J-Snake
Level 10
*****


A fool with a tool is still a fool.


View Profile WWW
« Reply #1 on: July 01, 2013, 07:40:17 PM »

The screenManager class:

Code:
    public class CscreenManager   //updates states and is responsible for screen transitions
    {
        Game1         gameLink;
        SpriteBatch   spriteBatch;
        List<Cscreen> screenList;
                      
        Cscreen       newScreen;
        bool          changingScreen;
        bool          dyingAll;
        int           screensToGoBack;

        Texture2D     texBlackPixel;
                    
                                          ////////////////////////////////////////// BEGIN: INTERNAL HELP FUNCTIONS
        int lastIndex()
        {
            return screenList.Count - 1;
        }

        bool checkAllDead()
        {
            bool allDead = true;

            for (int i = 0; i < screenList.Count; i++)
            {
                if (screenList[i].getState().getState() != Cstate.EnumState.dead)
                {
                    allDead = false;
                    break;
                }
            }

            return allDead;
        }

        void setDyingAll()
        {
            for (int i = 0; i < screenList.Count; i++)
                screenList[i].getState().setDying();
        }

      
                                     ///////////////////////////////////////////////// END: INTERNAL HELP FUNCTIONS



                                ///////////////////////////////////////////////// BEGIN: INTERNAL TOP LEVEL FUNCTIONS
        void updateStates()
        {
            if (screenList.Count == 0) return;


            if (changingScreen)  //in this branch a new screen will be added and/or screen/s will be removed, only the screen on top of the stack can be active
            {
                if (dyingAll)                            //the last branch in the load() was called:
                {                                        //all previous screens are removed after transitioning off before the new screen is activating
                    if (checkAllDead())
                    {
                        newScreen.initialize();
                        newScreen.getState().setActivating();
                        screenList.Clear();
                        screenList.Add(newScreen);
                        newScreen = null;
                        changingScreen = false;
                        dyingAll = false;

                        GC.Collect();   //appropriate time to call the garbage collector to preserve a seamless flow and minimize GC-calls during screen-updates
                    }                  //(only relevant for managed languages)  
                }
                else if (screenList[lastIndex()].getState().getState() == Cstate.EnumState.dead)    //"returnToPreviousScreen()" was called:
                {                                                                                   //removes the "screensToGoBack"-amount of screens on top of the stack
                    for (int i = 0; i < screensToGoBack; i++)
                        screenList.RemoveAt(lastIndex());

                    screenList[lastIndex()].getState().setActivating();
                    changingScreen = false;
                    screensToGoBack = 1;

                    GC.Collect();

                }
                else if (screenList[lastIndex()].getState().getState() == Cstate.EnumState.hidden) //the middle branch in the load() was called:
                {                                                                                     //the new screen is added on top of the current screen
                    newScreen.initialize();                                                           //none of the previous screens are removed, they are "hiding"
                    newScreen.getState().setActivating();                              
                    screenList.Add(newScreen);                                                          
                    newScreen = null;
                    changingScreen = false;

                    GC.Collect();
                }

            }

          for (int i = 0; i < screenList.Count; i++) //screen states are always updating
                screenList[i].getState().update();
        }

        void updateScreens()
        {
            for (int i = 0; i < screenList.Count; i++)
                screenList[i].update();
        }

                                ///////////////////////////////////////////////// END: INTERNAL TOP LEVEL FUNCTIONS
  




                                              /////////////////////////////////////////////////////// BEGIN: PUBLIC INTERFACE


        public CscreenManager(Game1 game)
        {
            gameLink = game;
            spriteBatch = new SpriteBatch(game.GraphicsDevice);
            screenList = new List<Cscreen>();

            newScreen = null;
            changingScreen = false;
            dyingAll = false;
            screensToGoBack = 1;

            texBlackPixel= game.Content.Load<Texture2D>(@"sprites/blackPixel");
        }

        public Texture2D getTexBlackPixel()
        {
            return texBlackPixel;
        }

        public SpriteBatch getSpriteBatch()
        {
            return spriteBatch;
        }

        public Game1 getGameLink()
        {
            return gameLink;
        }

        public Cscreen getPreviousScreen(int goBack = 1)
        {
            if (goBack < 1 || goBack >= screenList.Count) return null;

            return screenList[lastIndex() - goBack];
        }



        public void update()
        {
            updateStates();               //screen and state updates are separated to keep the updates perfectly in sync, no state has a "frame-advantage"
            updateScreens();            
        }

        public void load(Cscreen screen, bool hide = false)            
        {

            if ((screen == null) ||
                (screenList.Count > 0 && screenList[lastIndex()].getState().getState() != Cstate.EnumState.active)) return;

            newScreen = screen;
            changingScreen = true;


            if (screenList.Count == 0)        // ensures the first screen is added
            {
                newScreen.initialize();
                newScreen.getState().setActivating();
                screenList.Add(newScreen);
                newScreen = null;
                changingScreen = false;     // there is no screen to change since it is the first screen
            }
            else if (hide)                  
            {
                screenList[lastIndex()].getState().setHiding();
            }
            else
            {
                dyingAll = true;
                setDyingAll();
            }

        }

        public void returnToPreviousScreen(int goBack = 1)
        {
            if (goBack > 0) screensToGoBack = goBack;

            if (screenList.Count > screensToGoBack && screenList[lastIndex()].getState().getState() == Cstate.EnumState.active)
            {
                screenList[lastIndex()].getState().setDying();
                changingScreen = true;
            }
        }

        public void draw(Matrix screenResizeMatrix) // you can also pass the matrix as a reference but it won't affect performance on this level
        {                                              
            for (int i = 0; i < screenList.Count; i++)  
              screenList[i].draw(screenResizeMatrix);
        }


 }
« Last Edit: August 10, 2013, 02:10:13 PM by J-Snake » Logged

Independent game developer with an elaborate focus on interesting gameplay, rewarding depth of play and technical quality.<br /><br />Trap Them: http://store.steampowered.com/app/375930
J-Snake
Level 10
*****


A fool with a tool is still a fool.


View Profile WWW
« Reply #2 on: July 01, 2013, 07:44:50 PM »

The following example shows a gameplay screen which can be paused and resumed:


 
Code:
    class CgameplayScreen: Cscreen
    {
        Vector2   bombPosition;
        Vector2   velocity;
        Texture2D texBomb;

        bool      reactToInput;

        public CgameplayScreen(CscreenManager screenManager) : base(screenManager){}

        override public void initialize()
        {
            state.setHidePosition(0.2f);
            reactToInput = true;

            bombPosition = new Vector2(100.0f, 200.0f);
            velocity     = new Vector2(2.0f, 0.0f);

            texBomb = textures.load(@"sprites/bomb");
        }

        void updateBombPos()
        {
            bombPosition += velocity;

            if (bombPosition.X > 500.0f)
            {
                bombPosition.X = 500.0f;
                velocity = -velocity;
            }
            if (bombPosition.X < 100.0f)
            {
                bombPosition.X = 100.0f;
                velocity = -velocity;
            }
        }

        override protected void updateActivating()   //add the comment out commands below to keep it updating in the back
        {                                            //play with some settings to shape some understanding how you can influence the happening
            reactToInput = true;       // the settings you can change for every screen by default are: backBufferFading, updateWhenHidden, hidePosition, fadeInSpeed, fadeOutSpeed
                                                                                                   
           // updateBombPos();       
        }

        override protected void updateActive()
        {
            KeyboardState keyBoardState = Keyboard.GetState();


            if (reactToInput)
            {
                if (keyBoardState.IsKeyDown(Keys.Enter))
                {
                    // updateWhenHidden = true;
                    // reactToInput = false;
                    // state.setHidePosition(0.8f);

                    screenManager.load(new CpauseScreen(screenManager), true);   //by changing to "false" you won't be able to go back since the screen is gone, check it out
                    return;                                                      //also the transition to the pause screen will appear very instant all of a sudden, 
                }                                                                //look at the pause screen settings and explain how that impression comes!
             }                                                                 


            updateBombPos();
           
        }

        override protected void updateDying()
        {
            //if you wish to add own transitioning off effect set "backBufferFading" to false, set "state.setFadeOutSpeed()" to desired length and update your own transition effect
        }

        override protected void draw1()
        {
            SpriteBatch spriteBatch = screenManager.getSpriteBatch();

            spriteBatch.Draw(texBomb,bombPosition, Color.White);
        }

    }




Code:
   class CpauseScreen : Cscreen
    {
        Vector2   bombPosition;
        Vector2   velocity;
        Texture2D texPause;

        public CpauseScreen(CscreenManager screenManager) : base(screenManager){}

        override public void initialize()
        {
            backBufferFading = false;
            state = new Cstate(1.0f, 1.0f);

            bombPosition = new Vector2(100.0f, 200.0f);
            velocity = new Vector2(2.0f, 0.0f);

            texPause = textures.load(@"sprites/pause");
        }


        override protected void updateActivating()
        {
           
        }

        override protected void updateActive()
        {
            KeyboardState keyBoardState = Keyboard.GetState();

            if (keyBoardState.IsKeyDown(Keys.Back))
            {
                screenManager.returnToPreviousScreen();
            }
        }

        override protected void updateDying()
        {
           
        }

        override protected void draw1()
        {
            SpriteBatch spriteBatch = screenManager.getSpriteBatch();

            spriteBatch.Draw(texPause, new Vector2(350.0f,200.0f), Color.White);
        }

    }
« Last Edit: July 05, 2013, 03:45:00 PM by J-Snake » Logged

Independent game developer with an elaborate focus on interesting gameplay, rewarding depth of play and technical quality.<br /><br />Trap Them: http://store.steampowered.com/app/375930
darkhog
Level 7
**


Dragon Agent


View Profile
« Reply #3 on: July 02, 2013, 12:36:55 AM »

I've came up with another state design (though kinda similar) that I'm using in Super Heli Land (code's pascal as I feel most convenient with it).

Each state descends from base TState class:

Code:
TState = class
    public
      constructor Create;virtual;
      destructor Destroy;override;
      procedure Update;virtual;
      procedure BeforeDraw;virtual;
      procedure Draw;virtual;
      procedure Main;virtual;

    private

  end;

Now, there's one global variable called CurrentState:

Code:
var CurrentState:TState

Which contain of course current state. When changing state, I use following assignment:

Code:
CurrentState:=MainMenuState;
Rightmost part contains object of one of descendant classes to TState, in this case TMainMenuState.

If I want to change state, I just set CurrentState to another object of different state.

Main loop look like this:

Code:
      repeat
        CurrentState.BeforeDraw;
        CurrentState.Draw;
        CurrentState.Main;
      until quit;
and I have separate CurrentState.Update called in a timer to actually update things like object positions, etc.

Let's get back to TState, shall we?
Code:
TState = class
    public
      constructor Create;virtual;
      destructor Destroy;override;
      procedure Update;virtual;
      procedure BeforeDraw;virtual;
      procedure Draw;virtual;
      procedure Main;virtual;

    private

  end;
In constructor Create, state is supposed to load all its resources such as graphics, sounds, etc. to its private members. In destroy it is supposed to free all of them.

While designing this, I was heavily inspired by Unity's MonoBehaviour class.
Logged


Be a computer virus!


I cannot C well, so I stick with simpler languages.

There are no impossible things, there is only lack of skill.
J-Snake
Level 10
*****


A fool with a tool is still a fool.


View Profile WWW
« Reply #4 on: July 02, 2013, 11:33:25 AM »

I need just one state object type. What is the benefit of the added complexity?
Logged

Independent game developer with an elaborate focus on interesting gameplay, rewarding depth of play and technical quality.<br /><br />Trap Them: http://store.steampowered.com/app/375930
darkhog
Level 7
**


Dragon Agent


View Profile
« Reply #5 on: July 02, 2013, 02:24:10 PM »

Well, for one you keep every state's code separate without doing one big switch..case. This in turn leads to more manageable code and when something wrong with e.g. Options state, you go to TOptionsState class without going through said big switch...case and risking breaking e.g. level or main menu stuff.

When you need to change state (like going from main menu to options), you just update value CurrentState points to (it's in fact pointer to state that is currently in use).

As I said - it's just for maintenance sake and you can add/remove states pretty easily - just add or remove descendant class of TState.
Logged


Be a computer virus!


I cannot C well, so I stick with simpler languages.

There are no impossible things, there is only lack of skill.
J-Snake
Level 10
*****


A fool with a tool is still a fool.


View Profile WWW
« Reply #6 on: July 02, 2013, 04:05:24 PM »

I don't see enough abstraction and functionality from this example. Feel free to open up a new tutorial and post your whole frame if you feel confident. Some variety won't hurt as there aren't too many contributions to it. In this tutorial a screen and its state are cleanly separated and encapsulated. They are not melted as one thing.
Logged

Independent game developer with an elaborate focus on interesting gameplay, rewarding depth of play and technical quality.<br /><br />Trap Them: http://store.steampowered.com/app/375930
darkhog
Level 7
**


Dragon Agent


View Profile
« Reply #7 on: July 03, 2013, 01:43:30 AM »

Maybe I will, after finishing Super Heli Land (I plan to release source code anyway - IMO all indies, unless their game is intended for sale should do that, it's selfish to keep you code if you're not gonna sell your game).
Logged


Be a computer virus!


I cannot C well, so I stick with simpler languages.

There are no impossible things, there is only lack of skill.
Pages: [1]
Print
Jump to:  

Theme orange-lt created by panic