Состояние игры «Стек»?

52

Я думал о том, как внедрить игровые состояния в мою игру. Главные вещи, которые я хочу для этого:

  • Полупрозрачные верхние состояния - возможность видеть через меню паузы игру позади

  • Что-то в этом роде. Мне легче использовать и понять теорию, а также поддерживать организацию и добавлять к ней больше.



Я планировал использовать связанный список и рассматривать его как стек. Это означает, что я мог получить доступ к состоянию ниже для полупрозрачности.
План: наличие в стеке состояний связанного списка указателей на IGameStates. Верхнее состояние обрабатывает свои собственные команды обновления и ввода, а затем имеет член isTransparent, чтобы решить, должно ли быть нарисовано нижнее состояние.
Тогда я мог бы сделать:

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

Чтобы отобразить загрузку плеера, перейдите в раздел параметров, а затем в главное меню.
Это хорошая идея или ...? Должен ли я посмотреть на что-то еще?

Благодарю.

Коммунистическая утка
источник
Вы хотите увидеть MainMenuState за OptionsMenuState? Или просто экран игры за OptionsMenuState?
Skizz
План состоял в том, чтобы государства имели значение / флаг непрозрачности / isTransparent. Я бы проверил и посмотрел, имеет ли верхнее состояние это правда, и если да, то какое значение он имеет. Затем визуализируйте его с такой большой непрозрачностью по сравнению с другим состоянием. В этом случае нет, я бы не стал.
Коммунистическая утка
Я знаю, что уже поздно, но для будущих читателей: не используйте newспособ, показанный в примере кода, он просто запрашивает утечки памяти или другие, более серьезные ошибки.
Pharap

Ответы:

44

Я работал на том же движке, что и Coderanger. У меня другая точка зрения. :)

Во-первых, у нас не было стека автоматов - у нас был стек состояний. Стек состояний составляет один FSM. Я не знаю, как будет выглядеть стек автоматов. Вероятно, слишком сложно, чтобы сделать что-нибудь практическое с.

Моя самая большая проблема с нашим Global State Machine состояла в том, что это был набор состояний, а не набор состояний. Это означает, например, ... / MainMenu / Загрузка отличалась от ... / Загрузка / MainMenu, в зависимости от того, открылось ли главное меню до или после экрана загрузки (игра асинхронная и загрузка в основном выполняется сервером ).

В качестве двух примеров вещей это сделало безобразным:

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

Несмотря на название, оно было не очень «глобальным». Большинство внутренних игровых систем не использовали его для отслеживания своих внутренних состояний, потому что они не хотели, чтобы их состояния были испорчены другими системами. Другие, например система пользовательского интерфейса, могут использовать ее, но только для копирования состояния в свои собственные локальные системы состояний. (Я бы особенно предостерегал против системы для состояний пользовательского интерфейса. Состояние пользовательского интерфейса - это не стек, а действительно DAG, и попытка навязать ему любую другую структуру приведет только к тому, что пользовательские интерфейсы будут разочаровывать.)

Для этого было полезно изолировать задачи для интеграции кода от инфраструктурных программистов, которые не знали, как на самом деле структурирован игровой процесс, чтобы вы могли сказать парню, пишущему патчер, «поместить свой код в Client_Patch_Update», а парню, пишущему графику. загрузка «поместите ваш код в Client_MapTransfer_OnEnter», и мы могли бы без особых проблем поменять местами определенные логические потоки.

В стороннем проекте мне больше повезло с набором состояний, а не со стеком , не боясь создавать несколько машин для несвязанных систем и не позволяя себе попасть в ловушку «глобального состояния», которое действительно просто сложный способ синхронизировать вещи через глобальные переменные - конечно, вы в конечном итоге сделаете это в какой-то крайний срок, но не замышляйте это как свою цель . По сути, состояние в игре не является стеком, и состояния в игре не все связаны между собой.

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


источник
Отличный ответ, спасибо! Я думаю, что могу многое почерпнуть из твоего поста и твоего прошлого опыта. : D + 1 / Тик.
Коммунистическая утка
Приятной особенностью иерархии является то, что вы можете создавать служебные состояния, которые просто выдвигаются наверх, и вам не нужно беспокоиться о том, что еще выполняется.
кодеренджер
Я не понимаю, как это аргумент для иерархии, а не наборов. Скорее иерархия делает все межгосударственные коммуникации более сложными, потому что вы не знаете, куда их подтолкнули.
Суть в том, что пользовательские интерфейсы на самом деле являются группами обеспечения доступности баз данных, хорошо принята, но я не согласен с тем, что она, безусловно, может быть представлена ​​в стеке. Любой связанный ориентированный ациклический граф (и я не могу представить себе случай, когда это не было бы связанной группой DAG) может быть отображен в виде дерева, а стек по сути является деревом.
Эд Роппл
2
Стеки - это подмножество деревьев, которые являются подмножеством групп доступности баз данных, которые являются подмножеством всех графов. Все стеки - это деревья, все деревья - это DAG, но большинство DAG не являются деревьями, и большинство деревьев не являются стеками. DAG имеют топологический порядок, который позволяет вам хранить их в стеке (для обхода, например, для разрешения зависимостей), но как только вы втиснули их в стек, вы потеряли ценную информацию. В этом случае возможность перемещаться между экраном и его родителем, если у него есть предыдущий брат.
11

Вот пример реализации стека игровых состояний, который я считаю очень полезным: http://creators.xna.com/en-US/samples/gamestatemanagement

Он написан на C #, и для его компиляции вам понадобится среда XNA, однако вы можете просто проверить код, документацию и видео, чтобы получить идею.

Он может поддерживать переходы состояний, прозрачные состояния (такие как модальные окна сообщений) и состояния загрузки (которые управляют выгрузкой существующих состояний и загрузкой следующего состояния).

Я использую те же концепции в моих (не C #) проектах хобби сейчас (да, это может не подходить для больших проектов), и для небольших / хобби проектов, я определенно могу рекомендовать подход.

Янис Кирстейнс
источник
5

Это похоже на то, что мы используем, стека автоматов. По сути, просто дайте каждому состоянию функции входа, выхода и отметки и вызывайте их по порядку. Работает очень хорошо для обработки вещей, таких как загрузка тоже.

coderanger
источник
3

В одном из томов «Gem для программирования игр» была реализована машина состояний, предназначенная для состояний игры; http://emergent.net/Global/Documents/textbook/Chapter1_GameAppFramework.pdf содержит пример того, как использовать его для небольшой игры, и не должен быть слишком специфичным для Gamebryo, чтобы быть читаемым.

Том Хадсон
источник
В первом разделе «Программирование ролевых игр с помощью DirectX» также реализована система состояний (и система процессов - очень интересное различие).
Рикет
Это отличный документ, и он объясняет его почти точно, как я реализовал его в прошлом, если не считать ненужной иерархии объектов, которую они используют в примерах.
dash-tom-bang
3

Просто чтобы добавить немного стандартизации к обсуждению, классический термин CS для такого рода структур данных является автоматом .

необычайно щедрый
источник
Я не уверен, что какая-либо реальная реализация стеков состояний почти эквивалентна автомату. Как упоминалось в других ответах, практические реализации неизменно заканчиваются такими командами, как «выдвинуть два состояния», «поменять местами эти состояния» или «передать эти данные в следующее состояние вне стека». А автомат - это автомат, компьютер, а не структура данных. Как стеки состояний, так и автоматы pushdown используют стек в качестве структуры данных.
1
«Я не уверен, что какая-либо реальная реализация стеков состояний почти эквивалентна автомату сжатия». Какая разница? Оба имеют конечный набор состояний, историю состояний и примитивные операции для push и pop состояний. Ни одна из других операций, о которых вы упомянули, не отличается в финансовом отношении от этого. «Поп два государства» просто появляется дважды. «своп» - это поп и толчок. Передача данных выходит за рамки основной идеи, но каждая игра, использующая «FSM», также использует дополнительные данные, не чувствуя, что название больше не применимо.
Великолепно
В пуш-автомате единственное состояние, которое может повлиять на ваш переход, - это состояние сверху. Смена двух состояний в середине не допускается; даже смотреть на состояния в середине не разрешается. Я чувствую, что семантическое расширение термина «FSM» является разумным и имеет преимущества (и у нас все еще есть термины «DFA» и «NFA» для наиболее ограниченного значения), но «автомат нажатия» является строго термином компьютерной науки и Существует только путаница, ожидающая, если мы применим ее к каждой системе на основе стека.
Я предпочитаю те реализации, где единственным состоянием, которое может повлиять на что-либо, является состояние сверху, хотя в некоторых случаях удобно иметь возможность фильтровать входные данные состояния и передавать обработку в «нижнее» состояние. (Например, обработка ввода контроллера соответствует этому методу, верхнее состояние принимает биты, о которых оно заботится, и, возможно, очищает их, а затем передает управление следующему состоянию в стеке.)
dash-tom-bang
1
Хороший вопрос, исправлено!
Великолепно
1

Я не уверен, что стек абсолютно необходим, так как он ограничивает функциональность государственной системы. Используя стек, вы не можете «выйти» из состояния с одной из нескольких возможностей. Скажем, вы начинаете в «Главном меню», затем идете в «Загрузить игру», вы можете перейти в состояние «Пауза» после успешной загрузки сохраненной игры и вернуться в «Главное меню», если пользователь отменяет загрузку.

Я бы просто попросил штат указать состояние, которому он должен следовать при выходе.

Для тех случаев, когда вы хотите вернуться в состояние, предшествующее текущему состоянию, например «Главное меню-> Параметры-> Главное меню» и «Пауза-> Параметры-> Пауза», просто передайте в качестве параметра запуска состояние состояние, чтобы вернуться.

Skizz
источник
Может я неправильно понял вопрос?
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, для линейных потоков) и Reset (для возврата в главное меню или завершения потока).

Джейсон Козак
источник
В качестве модели пользовательского интерфейса это имеет некоторый смысл. Я не хотел бы называть их состояниями, поскольку в своей голове я связывал бы это с внутренностями основного игрового движка, в то время как «Главное меню», «Меню настроек», «Экран игры» и «Экран паузы» являются более высоким уровнем, и часто не имеют только взаимодействия с внутренним состоянием основной игры, и просто отправляют команды на основной движок в форме «Пауза», «Отключить», «Уровень загрузки 1», «Уровень запуска», «Уровень перезапуска», «Сохранить» и «Восстановить», «установить уровень громкости 57» и т. Д. Очевидно, что это может значительно варьироваться в зависимости от игры.
Кевин Кэткарт
0

Именно такой подход я применяю почти ко всем моим проектам, потому что он работает невероятно хорошо и чрезвычайно прост.

Мой самый последний проект, Sharplike , обрабатывает поток управления именно таким образом. Все наши состояния связаны с набором функций событий, которые вызываются при изменении состояний, и он имеет концепцию «именованного стека», в которой вы можете иметь несколько стеков состояний внутри одного и того же конечного автомата и ветвления между ними - концептуальный инструмент, и не обязательно, но удобно иметь.

Я бы предостерег от парадигмы «сообщить контроллеру, какое состояние должно следовать за этим, когда оно заканчивается», предложенной Skizz: она не структурно надёжна и делает такие вещи, как диалоговые окна (что в стандартной парадигме стекового состояния просто подразумевает создание новой создайте подкласс с новыми членами, а затем прочитайте его, когда вы вернетесь в состояние вызова), намного сложнее, чем должно быть.

Эд Роппл
источник
0

Я использовал в основном эту точную систему в нескольких системах ортогонально; например, состояния внешнего интерфейса и внутриигрового меню (он же «пауза») имели свои собственные стеки состояний. В игровом интерфейсе также использовалось что-то вроде этого, хотя у него были «глобальные» аспекты (например, индикатор здоровья и карта / радар), которые переключение состояний могло бы окрашивать, но которые обновлялись в разных штатах.

Внутриигровое меню может быть «лучше» представлено DAG, но с неявным конечным автоматом (каждый пункт меню, который идет на другой экран, знает, как туда попасть, и нажатие кнопки «Назад» всегда выдвигает верхнее состояние), эффект был точно так же.

Некоторые из этих других систем также имели функциональность «заменить верхнее состояние», но обычно это выполнялось следующим StatePop()образом StatePush(x);.

Работа с картой памяти была схожей, так как я фактически поместил тонну «операций» в очередь операций (которая функционально выполняла те же функции, что и стек, точно так же, как FIFO, а не LIFO); как только вы начинаете использовать такую ​​структуру («теперь что-то происходит, и когда это сделано, оно само всплывает»), оно начинает заражать каждую область кода. Даже ИИ начал использовать что-то вроде этого; ИИ был «невежественным», затем переключался на «осторожный», когда игрок издавал шумы, но не был виден, а затем, наконец, повышался до «активного», когда они видели игрока (и в отличие от меньших игр того времени, вы не могли скрыть в картонной коробке и заставь врага забыть о тебе! Не то чтобы я горький ...).

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]();
}
штрих-кот-бэнг
источник