Search code examples
c#state-pattern

State Pattern in c#


I got into this book called "game programming patterns" and I started implementing some of its patterns in C#. Right now, I'm implementing the State Pattern which is described by the UML Class Diagram below.enter image description here

This is my main program. I use 2 different threads in order to not stop the game loop with the keyboard user input. In the hero, class the HandleInput method calls the HandleInput method of the current state stored in the field state.

class Program
{

    private static string key;
    private static Thread t;
    private static Hero hero1;

    public delegate void PressDelegate();
    public static event PressDelegate OnPress;
    static void Main(string[] args)
    {
        // Creating Hero
        hero1 = new Hero("zelda");

        // Subscribing to KeyboardInput Event
        OnPress += KeyboardInputHandler;

        // Game Loop
        t = new Thread(new ThreadStart(KeyboardInput));
        t.Start();
    }
    private static void KeyboardInput()
    {
        while (true)
        {
            key = Console.ReadLine();
            OnPress();
        }

    }
    private static void KeyboardInputHandler()
    {
        hero1.HandleInput(key);
    }
}

}

My goal is to when user press Space, the hero changes his states to JumpingState assuming that its default state is the idlestate and after 1-second changes back to idle (simulating the gravity effect). The problem is I've tried some solutions like having the timer inside the IdleState but then I'll have to do that for every state that transition to jumping state (for example a ducking state). I've also tried to do that on the hero class with an if statement :( if state is JumpingState then start the timer and when finish call SetState(IdleState) something like that) but I feel that this breaks the fact that the SetState method is only called inside the state and not on the hero.

I really appreciate your help :)


Solution

  • Usually a game is run in a game-loop. A very simple one could look like this:

    1. Read input
    2. Process input (move character, advance enemies etc)
    3. Draw the result

    This loop would typically at a more or less fixed rate. this is in contrast to most windows programs that only run when the user has provided some kind of input, or the program has some timer that has triggered.

    So the active state would receive an update call every frame, and it has then a chance to decide what to do. The state would typically check the game-state and user input, from this it can then decide what to do.

    Instead of reading the input in a separate thread you can use Console.KeyAvailable to check if a key is available, and if so, ReadKey should not block.

    The way I'm used to program state machines is that the current state is called with some input parameters, and returns the next state to use. There are a bunch of details that can be taken into consideration:

    • Should you process all input? only the last input each frame? Only if the key is pressed when the frame starts?
    • Should it be possible to switch states more than once per frame? I.e. "Jumping" may transition to "Idle", but if the user is holding the shift-key then it could immediately transition to "crouching".
    • It is common to separate "state" and "transition". This helps making the state machine more abstract. I.e. Instead of having hard coding that the hero should jump on space in each state, you can have a OnKeyTransition that takes a input key and a target state as input. You can then add this transition to all states that should support jumping.
    • There can be many different kinds of state machines in a game. One high level might be if the player is driving a car, on foot, piloting an airplane etc. A lower level state machine might handle animations. A third kind could be used for AI. All of these have different kinds of requirements, so need to be treated differently.

    This would be an example of a very simple state machine. I'm not sure that it is exactly what you are looking for, but I hope it will provide some inspiration at least.

    public class State
    {
        public virtual State Update(ConsoleKey? input, TimeSpan deltaTime) => this;
    }
    
    public class IdleState : State
    {
        public override State Update(ConsoleKey? input, TimeSpan deltaTime)
        {
            if (input == ConsoleKey.Spacebar)
            {
                return new JumpingState();
            }
            return base.Update(input, deltaTime);
        }
    }
    
    public class JumpingState : State
    {
        private TimeSpan elapsed = TimeSpan.Zero;
    
        public override State Update(ConsoleKey? input, TimeSpan deltaTime)
        {
            elapsed += deltaTime;
            if (elapsed > TimeSpan.FromSeconds(1))
            {
                return new IdleState();
            }
            return base.Update(input, deltaTime);
        }
    }
    
    public class Game
    {
        static void Main(string[] args)
        {
            var game = new Game();
            game.StartGame();
    
        }
    
        State currentState = new IdleState();
        private TimeSpan frameRate = TimeSpan.FromMilliseconds(30);
    
        public void StartGame()
        {
            Console.WriteLine("Game Started");
            while (true)
            {
    
                var input = GetLastKeypress()?.Key;
                if (input == ConsoleKey.Escape)
                {
                    Console.WriteLine("Game Over");
                    return;
                }
    
                // Update the state 
                var nextState = currentState.Update(input, frameRate);
                if (nextState != currentState)
                {
                    currentState = nextState;
                    Console.WriteLine(currentState.GetType().Name);
                }
                Thread.Sleep(frameRate);
            }
        }
    
        private ConsoleKeyInfo? GetLastKeypress()
        {
            ConsoleKeyInfo? info = null;
            while (Console.KeyAvailable)
            {
                info = Console.ReadKey();
            }
            return info;
        }
    }