Шаблон состояний в C#
Я прочитал книгу под названием "Шаблоны программирования игр" и начал реализовывать некоторые из ее шаблонов на C#. Прямо сейчас я реализую шаблон состояния, который описывается приведенной ниже диаграммой классов UML.
Это моя основная программа. Я использую 2 разных потока, чтобы не останавливать игровой цикл пользовательским вводом с клавиатуры. В главном классе метод HandleInput вызывает метод HandleInput текущего состояния, хранящегося в поле 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);
}
}
}
Моя цель состоит в том, чтобы, когда пользователь нажимает пробел, герой меняет свое состояние на JumpingState, предполагая, что его состояние по умолчанию - это состояние ожидания, и через 1 секунду возвращается в состояние ожидания (имитируя эффект гравитации). Проблема в том, что я пробовал некоторые решения, такие как наличие таймера внутри IdleState, но тогда мне придется делать это для каждого состояния, которое переходит в состояние прыжка (например, состояние приглушения). Я также пытался сделать это в классе героя с помощью оператора if:(если состояние JumpingState, тогда запускайте таймер, а когда завершите вызов SetState(IdleState) что-то в этом роде), но я чувствую, что это нарушает тот факт, что метод SetState вызывается только внутри государства, а не на герое.
Я очень ценю вашу помощь:)
1 ответ
Обычно игра запускается в игровом цикле. Очень простой вариант может выглядеть так:
- Прочитать ввод
- Ввод процесса (перемещение персонажа, продвижение врагов и т. Д.)
- Нарисуйте результат
Этот цикл обычно имеет более или менее фиксированную скорость. это отличается от большинства программ Windows, которые запускаются только тогда, когда пользователь ввел какой-либо ввод или в программе есть сработавший таймер.
Таким образом, активное состояние будет получать вызов обновления каждый кадр, и тогда у него будет возможность решить, что делать. Состояние обычно проверяет состояние игры и пользовательский ввод, а затем может решить, что делать.
Вместо чтения ввода в отдельном потоке вы можете использовать Console.KeyAvailable, чтобы проверить, доступен ли ключ, и если да, ReadKey не должен блокироваться.
Я привык программировать конечные автоматы так, что текущее состояние вызывается с некоторыми входными параметрами и возвращает следующее состояние для использования. Есть ряд деталей, которые можно принять во внимание:
- Следует ли обрабатывать весь ввод? только последний ввод каждого кадра? Только если клавиша нажимается при запуске кадра?
- Можно ли переключать состояния более одного раза за кадр? Т.е. "прыжок" может перейти в "холостой ход", но если пользователь удерживает клавишу Shift, он может немедленно перейти в "приседание".
- Обычно разделяют "состояние" и "переход". Это помогает сделать конечный автомат более абстрактным. Т.е. вместо жесткого кодирования того, что герой должен прыгать в пространстве в каждом состоянии, вы можете иметь OnKeyTransition, который принимает ключ ввода и целевое состояние в качестве ввода. Затем вы можете добавить этот переход ко всем состояниям, которые должны поддерживать прыжки.
- В игре может быть много разных видов конечных автоматов. Один высокий уровень может быть, если игрок управляет автомобилем, пешком, пилотирует самолет и т. Д. Конечный автомат более низкого уровня может обрабатывать анимацию. Третий вид можно использовать для ИИ. Все они имеют разные требования, поэтому к ним нужно относиться по-разному.
Это был бы пример очень простого конечного автомата. Я не уверен, что это именно то, что вы ищете, но я надеюсь, что это хоть как-то вдохновит вас.
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;
}
}