И теперь плавно переходим от архитектуры к алгоритмам. Начнем с State Machine. С конечных автоматов. Мы не будем рассматривать полноценную теорию автоматов, все их виды и правила работы с ними. Тут нам потребуется следующие идеи:
Абстрактный автомат - некая абстракция, модель устройства, схожая с математической функцией - что-то подается на вход, как-то перерабатывается и получается результат.
Конечный автомат - это абстрактный автомат, который находится в одном конкретном состоянии и может менять его в зависимости от каких-то входящих данных.
Для понимания желательно вспомнить графы

Давайте возьмем в качестве примера передвижение человечка по карте. Как мы можем описать ситуацию, когда человечек ничего не делает? Состояние покоя. Когда человек прыгает и находится в воздухе? Состояние прыжка, он прыгнул и до сих пор не приземлился. Не все действия доступны в состоянии прыжка. Воду пить не хотелось бы.
Начальное состояние: человечек ничего не делает. Если нажать Space, он подпрыгнет. Если нажать F, то он начнет пить воду. Но если он уже находится в прыжке, то нажатие F не должно приводить к выпиванию воды (с точки зрения геймдизайна такое возможно, но не всегда нужно). Более того, во время прыжка мы не хотим открывать двери.
Представляете как эти требования могут выглядеть в виде кода? Примерно так:
is_jumping = False
is_drinking = False
#...
while running:
for event in pygame.event.get():
#...
if event.type == pygame.KEYDOWN:
if event.key == pygame.K_SPACE and not is_jumping and not is_drinking:
is_jumping = True # Character starts jumping
elif event.key == pygame.K_f and not is_jumping and not is_drinking:
is_drinking = True # Character starts drinking water
# ...
# Draw character
if is_jumping:
pygame.draw.rect(screen, BLUE, (player_pos[0], player_pos[1] - 20, player_size, player_size))
is_jumping = False
elif is_drinking:
pygame.draw.ellipse(screen, RED, (player_pos[0], player_pos[1], player_size, player_size))
is_drinking = False
else:
pygame.draw.rect(screen, BLUE, (player_pos[0], player_pos[1], player_size, player_size))
public class Game1 : Game
{
Vector2 playerPos;
int playerSize = 50;
bool isJumping = false;
bool isDrinking = false;
// ...
protected override void Update(GameTime gameTime)
{
var keyState = Keyboard.GetState();
if (keyState.IsKeyDown(Keys.Space) && !isJumping && !isDrinking)
{
isJumping = true;
}
else if (keyState.IsKeyDown(Keys.F) && !isJumping && !isDrinking)
{
isDrinking = true;
}
base.Update(gameTime);
}
protected override void Draw(GameTime gameTime)
{
GraphicsDevice.Clear(Color.White);
spriteBatch.Begin();
if (isJumping)
{
spriteBatch.Draw(GetRectangleTexture(), new Rectangle((int)playerPos.X, (int)(playerPos.Y - 20), playerSize, playerSize), Color.Blue);
isJumping = false;
}
else if (isDrinking)
{
spriteBatch.Draw(GetEllipseTexture(), new Rectangle((int)playerPos.X, (int)playerPos.Y, playerSize, playerSize), Color.Red);
isDrinking = false;
}
else
{
spriteBatch.Draw(GetRectangleTexture(), new Rectangle((int)playerPos.X, (int)playerPos.Y, playerSize, playerSize), Color.Blue);
}
spriteBatch.End();
base.Draw(gameTime);
}
}
А теперь добавьте дополнительные условия, особенно в духе "если в прыжке, но только начали, то можно еще отменить прыжок, а если оторвались от земли, то уже не можем отменить". Ну, получается примерно так:
is_jumping = False
is_drinking = False
is_attacking = False
is_opening_door = False
jump_timer = 0
can_cancel_jump = False
while running:
for event in pygame.event.get():
if event.type == pygame.KEYDOWN:
if event.key == pygame.K_SPACE:
if not is_jumping and not is_drinking and not is_attacking and not is_opening_door:
is_jumping = True
can_cancel_jump = True
jump_timer = 0
elif event.key == pygame.K_f:
if not is_jumping and not is_drinking and not is_attacking:
is_drinking = True
elif event.key == pygame.K_e:
if not is_jumping and not is_drinking and not is_attacking:
is_opening_door = True
elif event.key == pygame.K_LSHIFT:
if not is_jumping and not is_drinking and not is_opening_door:
is_attacking = True
elif event.key == pygame.K_ESCAPE:
if is_jumping and can_cancel_jump:
is_jumping = False
can_cancel_jump = False
jump_timer = 0
if is_jumping:
jump_timer += 1
if jump_timer > 5:
can_cancel_jump = False # too late to cancel
if jump_timer > 20:
is_jumping = False
jump_timer = 0
bool isJumping = false, isDrinking = false;
bool isAttacking = false, isOpeningDoor = false;
int jumpTimer = 0;
bool canCancelJump = false;
// in Update:
var kb = Keyboard.GetState();
if (kb.IsKeyDown(Keys.Space)
&& !isJumping && !isDrinking && !isAttacking && !isOpeningDoor)
{
isJumping = true;
canCancelJump = true;
jumpTimer = 0;
}
else if (kb.IsKeyDown(Keys.F) && !isJumping && !isDrinking && !isAttacking)
{
isDrinking = true;
}
else if (kb.IsKeyDown(Keys.E) && !isJumping && !isDrinking && !isAttacking)
{
isOpeningDoor = true;
}
else if (kb.IsKeyDown(Keys.LeftShift)
&& !isJumping && !isDrinking && !isOpeningDoor)
{
isAttacking = true;
}
else if (kb.IsKeyDown(Keys.Escape) && isJumping && canCancelJump)
{
isJumping = false;
canCancelJump = false;
jumpTimer = 0;
}
if (isJumping)
{
jumpTimer++;
if (jumpTimer > 5) canCancelJump = false;
if (jumpTimer > 20) { isJumping = false; jumpTimer = 0; }
}
Четыре действия - уже шесть флагов и каждый if проверяет по четыре условия. Добавьте приседание, бег, лазание по лестнице - и каждое новое действие добавляет флаг и проверку во все остальные if-ы. Это флаговый ад.
А в виде автомата это может выглядеть так:

И причем здесь игра? При том, что наш автомат всегда находится в одном конкретном состоянии и попасть он может в определенный набор других состояний с помощью четко определенных действий, которые мы заранее определяем. Поэтому все, что нам нужно - получить состояние и передать действие. Если из нашего состояния нет стрелочки с указанным действием, значит, нам ничего не надо делать. И код может выглядеть так:
from enum import Enum, auto
class StateType(Enum):
IDLE = auto()
JUMPING = auto()
DRINKING = auto()
class State:
state_type = None # override in subclasses
def handle_input(self, event):
pass
def update(self):
pass
def draw(self, screen):
pass
class IdleState(State):
state_type = StateType.IDLE
def handle_input(self, event):
if event.type == pygame.KEYDOWN:
if event.key == pygame.K_SPACE:
return JumpingState()
elif event.key == pygame.K_f:
return DrinkingState()
return self
def draw(self, screen):
pygame.draw.rect(screen, BLUE, (player_pos[0], player_pos[1], player_size, player_size))
class JumpingState(State):
state_type = StateType.JUMPING
def __init__(self):
self.jump_height = 20
def update(self):
# ...
player_pos[1] -= self.jump_height
def draw(self, screen):
pygame.draw.rect(screen, BLUE, (player_pos[0], player_pos[1], player_size, player_size))
def handle_input(self, event):
# After one frame return to idle
return IdleState()
class DrinkingState(State):
state_type = StateType.DRINKING
def draw(self, screen):
pygame.draw.ellipse(screen, RED, (player_pos[0], player_pos[1], player_size, player_size)) #or it can be inside Player
def handle_input(self, event):
return IdleState()
current_state = IdleState()
running = True
while running:
for event in pygame.event.get():
# ...
current_state = current_state.handle_input(event)
current_state.update()
current_state.draw(screen)
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Input;
public class Game1 : Game
{
GraphicsDeviceManager graphics;
SpriteBatch spriteBatch;
Vector2 playerPos;
int playerSize = 50;
State currentState;
public Game1()
{
graphics = new GraphicsDeviceManager(this);
Content.RootDirectory = "Content";
IsMouseVisible = true;
graphics.PreferredBackBufferWidth = 640;
graphics.PreferredBackBufferHeight = 480;
playerPos = new Vector2(graphics.PreferredBackBufferWidth / 2,
graphics.PreferredBackBufferHeight / 2);
currentState = new IdleState(this);
}
protected override void LoadContent()
{
spriteBatch = new SpriteBatch(GraphicsDevice);
}
protected override void Update(GameTime gameTime)
{
var state = Keyboard.GetState();
currentState = currentState.HandleInput(state);
currentState.Update();
base.Update(gameTime);
}
protected override void Draw(GameTime gameTime)
{
GraphicsDevice.Clear(Color.White);
spriteBatch.Begin();
currentState.Draw(spriteBatch);
spriteBatch.End();
base.Draw(gameTime);
}
}
public abstract class State
{
protected Game1 game;
protected static Texture2D rectTex;
protected static Texture2D ellipseTex;
public State(Game1 game)
{
this.game = game;
}
public abstract State HandleInput(KeyboardState state);
public virtual void Update() { }
public abstract void Draw(SpriteBatch spriteBatch);
protected Texture2D GetRectangleTexture()
{
if (rectTex != null) return rectTex;
rectTex = new Texture2D(game.GraphicsDevice, game.playerSize, game.playerSize);
var data = new Color[game.playerSize * game.playerSize];
for (int i = 0; i < data.Length; i++) data[i] = Color.White;
rectTex.SetData(data);
return rectTex;
}
protected Texture2D GetEllipseTexture()
{
if (ellipseTex != null)
return ellipseTex;
int w = game.playerSize, h = game.playerSize;
ellipseTex = new Texture2D(game.GraphicsDevice, w, h);
var data = new Color[w * h];
float rx = w / 2f, ry = h / 2f;
for (int y = 0; y < h; y++)
for (int x = 0; x < w; x++)
{
float dx = (x - rx) / rx, dy = (y - ry) / ry;
data[y * w + x] = dx * dx + dy * dy <= 1f ? Color.White : Color.Transparent;
}
ellipseTex.SetData(data);
return ellipseTex;
}
}
public class IdleState : State
{
public IdleState(Game1 game) : base(game) { }
public override State HandleInput(KeyboardState state)
{
if (state.IsKeyDown(Keys.Space))
return new JumpingState(game);
if (state.IsKeyDown(Keys.F))
return new DrinkingState(game);
return this;
}
public override void Draw(SpriteBatch spriteBatch)
{
spriteBatch.Draw(GetRectangleTexture(), game.playerPos, Color.Blue);
}
}
public class JumpingState : State
{
int jumpHeight = 20;
public JumpingState(Game1 game) : base(game)
{
game.playerPos.Y -= jumpHeight; // Simulate jump
}
public override State HandleInput(KeyboardState state)
{
return new IdleState(game);
}
public override void Draw(SpriteBatch spriteBatch)
{
spriteBatch.Draw(GetRectangleTexture(), game.playerPos, Color.Blue);
}
}
public class DrinkingState : State
{
public DrinkingState(Game1 game) : base(game) { }
public override State HandleInput(KeyboardState state)
{
return new IdleState(game);
}
public override void Draw(SpriteBatch spriteBatch)
{
spriteBatch.Draw(GetEllipseTexture(), game.playerPos, Color.Red);
}
}
Подобные автоматы можно строить не только для человечка, но и для других сущностей.
Что такое сцена? Грубо говоря, набор каких-то объектов, правил взаимодействия с ними. Можно считать сцену в игре/движке той же сценой, что и в театре. Любой спектакль - набор сцен. При этом антракт мы тоже можем назвать сценой. Но она особая - мы можем выйти из зала, позвонить по телефону, при этом актеры не играют свои роли, а ведут себя как обычные люди.
Какие сцены могут быть у нас в игре?
Обратите внимание, что сцена главного меню не содержит главного героя, следовательно, мы не можем им управлять, что логично, т.к. игра еще не началась. И тут нам тоже могут пригодиться конечные автоматы, где состояниями являются сцены. В данном примере переход от главного меню возможен по действию “Нажатие на кнопку “Начать игру”.
Естественно, подобный автомат напрямую не пересекается с автоматом игрока.
Однако, возможны ситуации, когда автоматы являются вложенными. Например, можно сделать автомат для отдельной сцены "Главное меню" (состояние игры), где мы определяем переходы между различными подсценами.
Мы можем гораздо проще визуализировать все наши переплетения условий, состояний. Так нам будет проще и дебажить, и разрабатывать, и сохранять состояние игры и всех сущностей.
Но у FSM есть и минусы. Когда состояний становится много, количество переходов растёт лавинообразно - автомат превращается в паутину. Если нужны параллельные состояния (бежать и стрелять), одного автомата не хватит - придётся комбинировать несколько или переходить к деревьям поведений.
| Что | Где подробнее |
|---|---|
| Анимации - тоже автоматы состояний | Анимации |
| ИИ врагов на основе FSM (Patrol/Chase/Attack) | ИИ врагов |
| Деревья поведений - когда FSM не хватает | Деревья поведений |
| Переходы через события вместо флагов | Событийная модель |
| Пошаговый цикл как FSM | Пошаговые игры |
| Флаговый ад - зачем вообще нужны автоматы | Типичные ошибки |
| FSM с вероятностными переходами | Цепи Маркова |
| Обзор архитектурных подходов | Архитектура |