Вступ
Цього тижня я зміг реалізувати рендеринг спрайтів! Це чудово, тому що тепер все, що я роблю, виглядає більше як справжня гра. Це також не надто цікаво з технічної точки зору. Та ось що дійсно цікаво — причина, чому я потрібен був рендеринг спрайтів: Машини Станів!
Стан
В попередніх статтях я розповідав про впровадження кастомних подій та префабів, і тепер переваги цих функцій починають справжньо проявлятися із впровадженням станів. Кожен стан представлений як окремий ігровий об'єкт зі своїм набором компонентів та подій. Замість стандартних подій оновлення, стани мають кастомні події оновлення, а також події, які викликаються під час входу та виходу з цього стану. Щоб побачити, як це працює, ось приклад для стрибка:
Якщо гравець стрибає прямо вгору, програється параметрична анімація на основі швидкості, але якщо гравець стрибає, йдучи, спочатку виконується одноразова анімація перевороту, а потім переходить в стрибок.
{
"Inherits": "State_Base",
"Name": "State_Jump",
"Components": {
"State": {
"Sprite": "resources/sprites/player_jump_5.png"
},
"State_Jump": {
"JumpSpeedFast": 4.0,
"JumpSpeedSlow": 1.0
}
},
"Events": {
"State_Update": [
"...",
"State_Jump_UpdateAnimation"
],
"State_CheckNext": [
"...",
"State_CheckNext_Landing"
]
}
},
{
"Inherits": "State_Base",
"Name": "State_Jump_Flip",
"Components": {
"State": {
"Sprite": "resources/sprites/player_jump_flip_4.png"
},
"State_Animation": {
"FramesPerSecond": 12,
"Loop": false
},
"State_NextState": {
"Duration": 20,
"NextState": "State_Jump"
}
},
"Events": {
"State_Update": [
"...",
"State_AnimationComponent_Update"
],
"State_CheckNext": [
"...",
"State_CheckNext_Landing",
"State_NextStateComponent_CheckNext"
]
}
}
Як видно зі станів, обидва стани мають деяку спільну поведінку, але також відрізняються анімаційною поведінкою. Звичайний стрибок має компонент даних з параметрами для анімації стрибка, в той час як переворот має базовий компонент анімації, тому кожен стан містить лише ті дані, які необхідні для його поведінки.
Стан перевороту також має поведінку, яка автоматично переводить його в стан стрибка, базуючись на даних в компоненті наступного стану. Щоб зрозуміти, як це працює, давайте подивимось на машину станів.
Машина Станів
Машина станів є компонентом та набором поведінок, які управляють оновленням поточного стану і зміною активного стану. Базова структура компонента є таблицею об'єктів станів і ідентифікаторів, які вказують на поточний та попередній стан.
typedef struct _StateMachineComponent
{
GHashTable *states;
json_object *state_array;
guint default_state;
guint current_state;
guint prev_state;
} StateMachineComponent;
"StateMachine": {
"States": [
"State_Idle",
"State_Walk",
"State_Jump",
"State_Jump_Flip"
],
"DefaultState": "State_Idle"
},
Як можна очікувати, кожен кадр машина станів викликає подію оновлення для поточного стану.
Але справжня магія відбувається в поведінці перемикання станів.
_EVENT void StateMachineComponent_PostUpdate(GameObject *this, UpdateParams *params)
{
COMPONENT_REQUIRED(StateMachineComponent, stateMachine, c_state_machine_comp_id);
object_id state_id = (object_id)g_hash_table_lookup(stateMachine->states, (gpointer)stateMachine->current_state);
GameObject *state = Scene_GetObject(params->scene, state_id);
StateCheckNextParams args = {
.scene = params->scene,
.owner = this,
.handled = FALSE,
.next_state_id = 0,
};
GameObject_CallEvent(state, c_event_state_check_next_id, &args);
if (args.next_state_id)
{
SetNextState(this, params->scene, stateMachine, args.next_state_id);
}
}
У функції post update машина станів отримує поточний стан та викликає подію StateCheckNext. Це перша подія в движку, яка використовує структуру params для _виведення значень. Машина станів передає посилання на поточну сцену, а також на об'єкт, який володіє станами, але значення handled та nextstateid встановлюються в події CheckNext.
void State_NextStateComponent_CheckNext(GameObject *this, StateCheckNextParams *params)
{
if (params->handled) return;
COMPONENT_REQUIRED(StateComponent, state, c_state_comp_id);
COMPONENT_REQUIRED(State_NextStateComponent, stateNext, c_state_next_state_comp_id);
if (state->current_frame >= stateNext->duration)
{
params->handled = TRUE;
params->next_state_id = stateNext->next_state_id;
}
}
Оскільки стани є ігровими об'єктами, вони можуть визначати кілька функцій поведінки для події CheckNext. Змінна handled використовується для того, щоб вказати, що попередня поведінка вже вибрала наступний стан. Хоча поведінки і можуть ігнорувати цей прапорець, це дозволяє програмісту використовувати порядок функцій CheckNext для визначення того, яка поведінка має пріоритет, коли кілька умов виконуються одночасно.
Висновок
Система станів має великий потенціал для ігор, орієнтованих на персонажів, таких як бойові ігри. Вона також робить цей базовий проєкт набагато більш відшліфованим, оскільки тепер значно легше додавати дуже специфічні умовні стани, наприклад, перехід стрибка в переворот, не додаючи багато зайвої роботи.
Ви також могли помітити деякі відображення для налагодження на екрані. Я почав роботу над системою колізій, але вона ще не готова до демонстрації, тож чекайте оновлень про це в наступній статті.
Перекладено з: Game Engine Architecture in C — Part 3: State Machines