C# 中的状态模式

State Pattern in c#

我开始阅读这本名为 "game programming patterns" 的书,并开始在 C# 中实现其中的一些模式。现在,我正在实施下面的 UML Class 图所描述的状态模式。

这是我的主程序。我使用 2 个不同的线程,以便不通过键盘用户输入停止游戏循环。在英雄中,classHandleInput方法调用state字段中存储的当前状态的HandleInput方法

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);
    }
}

}

我的目标是当用户按下 Space 时,英雄将他的状态更改为 JumpingState,假设其默认状态为空闲状态,并在 1 秒后变回空闲状态(模拟重力效果)。问题是我已经尝试了一些解决方案,比如在 IdleState 中设置计时器,但是我必须对每个过渡到跳跃状态(例如闪避状态)的状态都这样做。我还尝试使用 if 语句对英雄 class 执行此操作 :( 如果状态为 JumpingState 则启动计时器并在完成时调用 SetState(IdleState) 之类的东西)但我觉得这打破了事实SetState 方法仅在状态内部调用,而不是在英雄上调用。

非常感谢您的帮助:)

通常游戏是 运行 在一个游戏循环中。一个非常简单的可能如下所示:

  1. 读取输入
  2. 处理输入(移动角色、推进敌人等)
  3. 绘制结果

这个循环通常会以或多或少的固定速率进行。这与大多数 windows 程序形成对比,大多数 运行 程序仅在用户提供某种输入或程序触发了某些计时器时才 运行。

所以活动状态每帧都会收到一个更新调用,然后它有机会决定要做什么。状态通常会检查游戏状态和用户输入,然后它可以决定要做什么。

您可以使用 Console.KeyAvailable to check if a key is available, and if so, ReadKey 不应阻塞,而不是在单独的线程中读取输入。

我习惯于对状态机进行编程的方式是使用一些输入参数调用当前状态,然后 returns 调用下一个要使用的状态。有很多细节可以考虑:

  • 您应该处理所有输入吗?每帧只有最后一个输入?仅当帧开始时按下键?
  • 每帧是否可以多次切换状态? IE。 "Jumping" 可能会转换为 "Idle",但如果用户按住 shift 键,则它会立即转换为 "crouching"。
  • 将"state"和"transition"分开是很常见的。这有助于使状态机更加抽象。 IE。您可以使用一个 OnKeyTransition 将输入键和目标状态作为输入,而不是硬编码英雄应该在 space 上跳转。然后,您可以将此转换添加到应支持跳跃的所有状态。
  • 游戏中可以有多种不同的状态机。一个高级别可能是玩家正在开车、步行、驾驶飞机等。较低级别的状态机可能会处理动画。第三种可以用于人工智能。所有这些都有不同的要求,因此需要区别对待。

这将是一个非常简单的状态机示例。我不确定这是否正是您要找的,但我希望它至少能提供一些灵感。

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;
    }
}