Ігровий стан «Стек»?


52

Я думав про те, як втілити ігрові стани у свою гру. Основні речі, які я хочу за це:

  • Напівпрозорі верхні стани - це можливість бачити через меню пауз до гри позаду

  • Щось OO - мені здається, це легше використовувати та розуміти теорію, яка стоїть позаду, а також зберігати орґанізовані та додавати більше.



Я планував використовувати зв'язаний список і ставився до нього як до стеку. Це означає, що я міг отримати доступ до стану нижче для напівпрозорості.
План: Нехай стек стану стане пов'язаним списком покажчиків на IGameStates. Верхній стан обробляє власні команди оновлення та введення, а потім має член isTransparent, щоб вирішити, чи слід підводити стан під ним.
Тоді я міг би зробити:

states.push_back(new MainMenuState());
states.push_back(new OptionsMenuState());
states.pop_front();

Щоб представити завантаження плеєра, перейдіть до опцій, а потім до головного меню.
Це гарна ідея, чи ...? Чи варто дивитись на щось інше?

Дякую.


Ви хочете бачити MainMenuState за OptionsMenuState? Або просто екран гри за OptionsMenuState?
Skizz

План полягав у тому, що штати матимуть непрозорість / єпрозоре значення / прапор. Я перевірив би і чи був у верхній державі це правда, і якщо так, то яке значення воно мало. Тоді візьміть це з такою непрозорістю над іншою державою. У цьому випадку ніякого я не хотів.
Качка комуністична

Я знаю, що пізно вдень, але майбутнім читачам: не використовуйте newтак, як показано у зразковому коді, це просто прохання про витоки пам'яті чи інші, більш серйозні помилки.
Фарап

Відповіді:


44

Я працював на тому ж двигуні, що і кодерангер. Я маю різні точки зору. :)

По-перше, у нас не було стека FSM - у нас був стек штатів. Стек штатів складає єдину FSM. Я не знаю, як виглядатиме стопка FSM. Напевно, занадто складно, щоб робити щось практичне.

Моя найбільша проблема з нашою Глобальною державною машиною полягала в тому, що це була група штатів, а не набір штатів. Це означає, наприклад, ... / MainMenu / Завантаження було іншим, ніж ... / Завантаження / MainMenu, залежно від того, чи було у вас головне меню до або після завантажувального екрана (гра асинхронна і завантаження здебільшого керується сервером ).

У якості двох прикладів речей це стало потворним:

  • Це призвело, наприклад, до стану LoadingGameplay, тому для завантаження в стані Gameplay був Base / Loading, Base / Gameplay / LoadingGameplay, який повинен був повторити велику частину коду в звичайному стані завантаження (але не все, і додати ще трохи ).
  • У нас було кілька функцій на кшталт "якщо в творці персонажів перейти до геймплея; якщо в геймплейі перейти до вибору символів; якщо у виборі символів повернутися до входу", тому що ми хотіли показати ті самі вікна інтерфейсу в різних станах, але зробити Назад / Вперед кнопки все ще працюють.

Незважаючи на назву, вона була не дуже "глобальною". Більшість внутрішніх ігрових систем не використовували його для відстеження внутрішніх станів, оскільки вони не хотіли, щоб їхні стани спілкувалися з іншими системами. Інші, наприклад, система користувальницького інтерфейсу, могли використовувати її, але лише для копіювання стану у власні місцеві державні системи. (Я б особливо застерігсь від системи для штатів інтерфейсу користувача. Стан користувальницького інтерфейсу - це не стек, це дійсно DAG, і намагаючись змусити будь-яку іншу структуру на ньому збиратися зробити лише користувацькі інтерфейси, які засмучують використання.)

Це було добре - виділити завдання інтеграції коду від інфраструктурних програмістів, які не знали, як ігровий потік насправді структурований, так що ви можете сказати хлопцеві, який пише патч, "поставив свій код у Client_Patch_Update", а хлопець написав графіку завантажуючи "помістіть свій код у Client_MapTransfer_OnEnter", і ми зможемо без особливих проблем помінятися певними логічними потоками.

У побічному проекті мені пощастило з набором штатів , а не стеком , не побоюючись зробити кілька машин для споріднених систем, і відмовився дозволити собі потрапити в пастку "глобальної держави", що насправді просто складний спосіб синхронізації речей за допомогою глобальних змінних - Звичайно, ви все одно зробите це в якийсь термін, але не плануйте це як свою мету . По суті, стан у грі не є стеком, а стани в грі не всі пов'язані.

GSM, як правило, покажчики функцій та нелокальна поведінка, ускладнювали налагодження речей, хоча налагодження таких великих переходів у державі було не дуже цікавим, перш ніж у нас це було. Набори держав замість державних стеків насправді це не допомагають, але ви повинні знати про це. Віртуальні функції, а не покажчики функцій, можуть дещо полегшити це.


Чудова відповідь, дякую! Я думаю, що я можу багато чого взяти з вашої посади та вашого минулого досвіду. : D + 1 / галочка.
Качка комуніста

Приємна річ у ієрархії полягає в тому, що ви можете створювати утилітні стани, які просто натиснуті на вершину, і не потрібно турбуватися про те, що ще працює.
кодерангер

Я не бачу, як це аргумент для ієрархії, а не множин. Швидше за все, ієрархія ускладнює все міждержавне спілкування, оскільки ви не маєте уявлення, куди їх підштовхнули.

Справа в тому, що користувальницькі інтерфейси є насправді DAG, але я не погоджуюся в тому, що він, безумовно, може бути представлений у стеці. Будь-який підключений спрямований ациклічний графік (і я не можу придумати випадок, коли це не було б з'єднаним DAG) може відображатися як дерево, а стек - це по суті дерево.
Ед Роппл

2
Стеки - це підмножина дерев, яка є підмножиною DAG, які є підмножиною всіх графіків. Усі стеки - це дерева, усі дерева - DAG, але більшість DAG не є деревами, і більшість дерев не є стеками. DAG мають топологічне впорядкування, яке дозволить вам зберігати їх у стеку (для обходу, наприклад, роздільної здатності залежностей), але після того, як ви вкачаєте їх у стек, ви втратили цінну інформацію. У цьому випадку можливість переміщатися між екраном та його батьківським, якщо він має попередній брат.

11

Ось приклад реалізації стека ігор, який мені здається дуже корисним: http://creators.xna.com/en-US/samples/gamestatemanagement

Він написаний на C #, і для його складання вам потрібна рамка XNA, однак ви можете просто ознайомитися з кодом, документацією та відео, щоб отримати ідею.

Він може підтримувати переходи станів, прозорі стани (наприклад, модальні поля повідомлень) та стани завантаження (які керують вивантаженням існуючих станів та завантаженням наступного стану).

Зараз я використовую ті самі концепції у своїх хобі-проектах (не для C #) (надано, можливо, це не підходить для великих проектів), а для малих проектів / хобі я напевно можу порекомендувати цей підхід.


5

Це схоже на те, що ми використовуємо, стек FSM. По суті, просто дайте кожній державі функцію введення, виходу та галочки та викликайте їх по порядку. Працює дуже добре для обробки таких речей, як завантаження.


3

Один з томів "Ігри про грі програмування" мав в ньому реалізовану машину, призначену для ігрових станів; http://emergent.net/Global/Documents/textbook/Chapter1_GameAppFramework.pdf має приклад того, як використовувати його для невеликої гри, і не повинен бути надто специфічним для Gamebryo, щоб читати.


Перший розділ "Програмування рольових ігор з DirectX" також реалізує державну систему (а процесна система - дуже цікаве розмежування).
Ricket

Це чудовий документ і пояснює його майже точно, як я його реалізував у минулому, окрім непотрібної ієрархії об'єктів, яку вони використовують у прикладах.
Даш-Том-Банг

3

Тільки для додання трохи стандартизації до дискусії, класичний термін CS для подібних структур даних - це автоматичний віджимання .


Я не впевнений, що реальна реалізація державних стеків майже еквівалентна автомату віджимання. Як згадується в інших відповідях, практичні реалізації незмінно закінчуються такими командами, як "pop dva state", "swap цих станів" або "передача цих даних у наступний стан поза стеком". А автомат - це автомат - комп'ютер - не структура даних. І стеки штатів, і автоматизовані автоматичні виправлення використовують стек як структуру даних.

1
"Я не впевнений, що будь-яка реальна реалізація державних стеків майже еквівалентна автоматичному натисканню". Яка різниця? Обидва мають кінцевий набір станів, історію держав та примітивні операції по натисканню та попсовому стану. Жодна з інших операцій, які ви згадуєте, принципово не відрізняється від цієї. "Поп дві держави" просто вискакує двічі. "swap" - це поп і поштовх. Передача даних не входить до основної ідеї, але кожна гра, яка використовує "FSM", також враховує додаткові дані, не відчуваючи, що назва більше не застосовується.
чудовий

У автоматичному віджиманні єдиний стан, який може вплинути на ваш перехід, - це стан зверху. Заміна двох станів посередині не дозволяється; навіть дивитися на стани посередині не дозволяється. Я вважаю, що семантичне розширення терміна "FSM" є розумним і має переваги (і у нас все ще є терміни "DFA" і "NFA" для найбільш обмеженого значення), але "автоматичний автоматичний прийом" - це строго термін інформатики і очікується лише плутанина, якщо ми застосуємо її до кожної системи на основі стека.

Я віддаю перевагу тим реалізаціям, де єдиним станом, який може впливати на що завгодно, є стан, що знаходиться на вершині, хоча в деяких випадках зручно мати можливість фільтрувати вхід стану та передавати обробку в "нижчий" стан. (Наприклад, обробка вводу контролера відображає цей метод. Верхня держава бере біти, про які вона піклується, і, можливо, очищає їх, потім передає контроль у наступний стан на стеці.)
dash-tom-bang

1
Добрий момент, фіксований!
чудовий

1

Я не впевнений, що стек цілком необхідний, а також обмеження функціональності державної системи. Використовуючи стек, ви не можете "вийти" зі стану на одну з декількох можливостей. Скажімо, ви починаєте з "Головного меню", а потім переходите до "Завантажити гру", ви можете перейти до стану "Призупинити" після успішного завантаження збереженої гри та повернутися в "Головне меню", якщо користувач скасує завантаження.

Я б просто зобов’язав державу вказати стан, яке слідкувати, коли воно закінчується.

Для тих випадків, коли ви хочете повернутися до стану, що передує поточному стану, наприклад, "Головне меню-> Опції-> Головне меню" та "Призупинення-> Опції-> Призупинення", просто переведіть як параметр запуску в стан держава повернутися до.


Може, я неправильно зрозумів питання?
Skizz

Ні, ти цього не зробив. Я думаю, що це зробив нижчий виборець.
Качка комуністична

Використання стека не виключає використання явних переходів стану.
Даш-Том-Банг

1

Іншим рішенням переходів та інших подібних речей є надання стану призначення та джерела разом із машиною стану, яка могла б бути пов'язана з "двигуном", що б там не було. Правда полягає в тому, що більшість державних машин, ймовірно, потребують адаптації до проекту. Одне рішення може принести користь тій чи іншій грі, інші рішення можуть перешкоджати цьому.

class StateMachine
{
public:
    StateMachine(Engine *);
    void Push(State *);
    State *Pop();
    void Update();
    Engine *GetEngine();

private:
    std::stack<State *> _states;
    Engine *_engine;
};

Штати висуваються з поточним станом і машиною як параметри.

void StateMachine::Push(State *state)
{
    State *from = 0;
    if (!_states.empty()) from = _states.top();
    _states.push(state);
    state->Enter(this, from);
}

Держави спливають таким же чином. Чи буде ви закликати Enter()нижчу State- питання щодо впровадження.

State *StateMachine::Pop()
{
    _ASSERT(!_states.empty());
    State *state = _states.top();
    State *to = 0;
    _states.pop();
    if (!_states.empty()) to = _states.top();
    state->Exit(this, to);
    return state;
}

Під час введення, оновлення чи виходу з нього Stateотримується вся необхідна йому інформація.

void SomeGameState::Enter(StateMachine *sm, State *from)
{
    Engine *eng = sm->GetEngine();
    eng->GetKeyboard()->KeyDown.Bind(this, &SomeGameState::KeyDown);
    LoadLevelState *state = new LoadLevelState();
    state->SetLevel(eng->GetSaveGame()->GetLevelName());
    state->Load.Bind(this, &SomeGameState::OnLevelLoaded);
    sm->Push(state);
}

void SomeGameState::Update(StateMachine *sm)
{
    Engine *eng = sm->GetEngine();
    float time = eng->GetFrameTime();
    if (shouldExit)
        sm->Pop();
}

void SomeGameState::Exit(StateMachine *sm, State *from)
{
    Engine *eng = sm->GetEngine();
    eng->GetKeyboard()->KeyDown.UnsubscribeAll(this);
}

0

Я використовував дуже схожу систему в декількох іграх і виявив, що за кількома винятками вона служить відмінною моделлю інтерфейсу користувача.

Єдині проблеми, з якими ми стикалися, - це випадки, коли в певних випадках бажано повернути кілька станів перед тим, як натиснути новий стан (ми повторно передали користувальницький інтерфейс, щоб усунути вимогу, як це зазвичай було ознакою поганого інтерфейсу) та створити стиль майстра лінійні потоки (вирішуються легко шляхом передачі даних у наступний стан).

Реалізація, яку ми використовували, фактично завернула стек і обробляла логіку оновлення та рендерінгу, а також операції над стеком. Кожна операція на стеку ініціювала події в станах, щоб сповіщати їх про операцію, що відбувається.

Було додано кілька допоміжних функцій для спрощення загальних завдань, таких як Swap (Pop & Push, для лінійних потоків) та Скидання (для повернення до головного меню або припинення потоку).


Як модель UI це має певний сенс. Я б не вагався називати їх державами, оскільки в голові я б асоціював це з внутрішніми системами основного ігрового механізму, тоді як "Головне меню", "Меню опцій", "Ігровий екран" та "Екран призупинення" вищого рівня, і часто не мають взаємодії з внутрішнім станом основної гри, а просто надсилають команди до основного двигуна у формі "Призупинити", "Скасувати", "Завантажити рівень 1", "Стартовий рівень", "Перезапустити рівень", "Зберегти" та "Відновити", "Встановити рівень гучності 57" тощо. Очевидно, що це залежно від гри.
Кевін Кеткарт

0

Такий підхід я застосовую майже для всіх своїх проектів, оскільки він працює надзвичайно добре і надзвичайно простий.

Мій останній проект, Sharplike , точно обробляє контрольний потік. Всі наші держави пов'язані з набором функцій подій, які викликаються при зміні станів, і в ньому є концепція "іменований стек", в якій ви можете мати кілька стеків станів у межах однієї машини машини та гілки між ними - концептуальна інструмент, і не потрібно, але зручно мати.

Я б застережував проти "скажіть контролеру, який стан повинен слідувати за цією, коли він закінчується" парадигмою, запропонованою Skizz: вона не є структурно надійною, і вона робить такі елементи, як діалогові вікна (що в стандартній парадигмі стану стека просто передбачає створення нової підклас штату з новими членами, а потім їх зчитування, коли ви повернетесь до стану виклику) набагато складніше, ніж це має бути.


0

Я в основному використовував цю точну систему в декількох системах ортогонально; наприклад, фронтенд та ігрове меню (також "пауза") мали свої власні стеки. Користувальницький інтерфейс гри також використовував щось подібне, хоча він мав "глобальні" аспекти (наприклад, панель здоров'я та карта / радар), які перемикання стану може підфарбовувати, але які оновлюються загальноприйнятим чином у всіх штатах.

Ігрове меню може бути "краще" представлене DAG, але з неявною машиною стану (кожна опція меню, що переходить на інший екран, знає, як туди їхати, і натисканням кнопки "назад" завжди вискакує верхній стан) ефект був точно так само.

Деякі з цих інших систем також мали функцію "замінити верхній стан", але це, як правило, реалізоване, як StatePop()далі StatePush(x);.

Поводження з карткою пам'яті було подібним, оскільки я фактично штовхнув багато "операцій" у чергу операцій (що функціонально робило те саме, що і стек, так само як FIFO, а не LIFO); як тільки ви почнете використовувати таку структуру ("зараз відбувається одне, і коли це все зробиться"), він починає заражати кожну область коду. Навіть ШІ почав використовувати щось подібне; AI був "незрозумілим", потім перейшов у "насторожено", коли гравець шумів, але його не бачили, а потім нарешті підвищили до "активного", коли побачили гравця (і на відміну від менших ігор того часу, ви не могли сховатися в картонній коробці і змусити ворога забути про тебе! Не те, що я гірко ...).

GameState.h:

enum GameState
{
   k_frontend,
   k_gameplay,
   k_inGameMenu,
   k_moviePlayback,
   k_numStates
};

void GameStatePush(GameState);
void GameStatePop();
void GameStateUpdate();

GameState.cpp:

// k_maxNumStates could be bigger, but we don't need more than
// one of each state on the stack.
static const int k_maxNumStates = k_numStates;
static GameState s_states[k_maxNumStates] = { k_frontEnd };
static int s_numStates = 1;

static void (*s_startupFunctions)()[] =
   { FrontEndStart, GameplayStart, InGameMenuStart, MovieStart };
static void (*s_shutdownFunctions)()[] =
   { FrontEndStop, GameplayStop, InGameMenuStop, MovieStop };
static void (*s_updateFunctions)()[] =
   { FrontEndUpdate, GameplayUpdate, InGameMenuUpdate, MovieUpdate };

static void GameStateStart(GameState);
static void GameStateStop(GameState);

void GameStatePush(GameState gs)
{
   Assert(s_numStates < k_maxNumStates);
   GameStateStop(s_states[s_numStates - 1])
   s_states[s_numStates] = gs;
   s_numStates++;
   GameStateStart(gs);
}

void GameStatePop()
{
   Assert(s_numStates > 1);  // can't pop last state
   s_numStates--;
   GameStateStop(s_states[s_numStates]);
   GameStateStart(s_states[s_numStates - 1]);
}

void GameStateUpdate()
{
   GameState current = s_states[s_numStates - 1];
   s_updateFunctions[current]();
}

void GameStateStart(GameState gs)
{
   s_startupFunctions[gs]();
}

void GameStateStop(GameState gs)
{
   s_shutdownFunctions[gs]();
}
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.