游戏状态“堆栈”?


52

我在考虑如何在游戏中实现游戏状态。我想要的主要内容是:

  • 半透明顶部状态-可以通过暂停菜单查看后面的游戏

  • 面向对象的东西,我发现这更易于使用和理解其背后的理论,以及保持组织规范并添加更多内容。



我打算使用链接列表,并将其视为堆栈。这意味着我可以访问下面的状态以获得半透明性。
计划:将状态堆栈作为指向IGameStates的指针的链接列表。顶部状态处理其自己的更新和输入命令,然后具有成员isTransparent来决定是否应绘制下面的状态。
然后我可以做:

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

要表示播放器的负载,请依次转到选项和主菜单。
这是个好主意,还是...?我应该看看别的东西吗?

谢谢。


您是否要在OptionsMenuState后面看到MainMenuState?还是只是OptionsMenuState后面的游戏屏幕?
Skizz 2010年

计划是各州将具有不透明度/ isTransparent值/标志。我将检查并查看顶级状态是否为true,如果是,则具有什么值。然后以与其他状态一样的透明度渲染它。在这种情况下,不,我不会。
共产主义鸭子

我知道这是很晚的事情,但是对未来的读者来说:不要new以示例代码中所示的方式使用,它只是在询问内存泄漏或其他更严重的错误。
法拉普

Answers:


44

我和coderanger在同一引擎上工作。我有不同的看法。:)

首先,我们没有一堆FSM-我们有一堆状态。状态堆栈构成一个FSM。我不知道一堆FSM是什么样子。可能太复杂了,无法执行任何实际操作。

我的全球状态机最大的问题是它是一堆州,而不是一组州。这意味着,例如... / MainMenu / Loading与... / Loading / MainMenu不同,这取决于您是在加载屏幕之前还是之后打开主菜单(游戏是异步的,并且加载主要由服务器驱动)。

举两个丑陋的例子:

  • 它导致了例如LoadingGameplay状态,因此您需要在游戏状态下进行Base / Loading和Base / Gameplay / LoadingGameplay的加载,这些操作必须在正常加载状态下重复很多代码(但不是全部,并添加更多代码) )。
  • 我们有几个功能,例如“如果在角色创建者中转到游戏;在游戏中转到角色选择;如果在角色选择中,返回登录”,因为我们希望在不同的状态下显示相同的界面窗口,但要进行“后退/前进”按钮仍然有效。

尽管有名称,但它不是很“全局”。大多数内部游戏系统不使用它来跟踪其内部状态,因为他们不希望其状态与其他系统发生冲突。其他系统(例如UI系统)可以使用它,但只能将状态复制到自己的本地状态系统中。(我要特别注意不要针对UI状态使用系统。UI状态不是堆栈,它实际上是DAG,并且试图强加其上的任何其他结构只会使令人沮丧的UI使用。)

这样做的好处是从不知道游戏流程实际结构的基础架构程序员中分离出集成代码的任务,因此您可以告诉编写修补程序的人“将代码放入Client_Patch_Update”和编写图形的人。加载“将您的代码放入Client_MapTransfer_OnEnter”,我们就可以轻松交换某些逻辑流程。

在一个附带项目中,我最好拥有一个状态而不是一个堆栈,不要害怕为不相关的系统制造多台机器,并且拒绝让自己陷入陷入“全局状态”的陷阱,这的确是只是通过全局变量同步事物的一种复杂方法-当然,您最终将在某个截止日期之前完成它,但不要以此为目标进行设计。从根本上讲,游戏中的状态不是堆栈,游戏中的状态也不是全部相关。

GSM也随着函数指针和非本地行为的发展而变得越来越困难,尽管调试那些大型状态转换在我们之前也不是很有趣。状态集而不是状态堆栈并不能真正帮助您,但是您应该意识到这一点。虚函数而不是函数指针可能会有所缓解。


好答案,谢谢!我想我可以从您的帖子和您过去的经历中学到很多。:D + 1 /勾号
共产党鸭子2010年

层次结构的好处是您可以构建实用程序状态,这些状态将被推到顶部,而不必担心正在运行什么。
coderanger 2010年

我看不出这是层次结构而不是集合的参数。而是,层次结构使所有的州际通信变得更加复杂,因为您不知道将它们推向何处。

UI实际上是DAG的观点已广为人知,但我确实不同意,因为UI肯定可以用堆栈表示。任何连接的有向无环图(我不认为它不是连接的DAG的情况)都可以显示为树,并且堆栈本质上是一棵树。
Ed Ropple

2
堆栈是树的子集,树是DAG的子集,DAG是所有图的子集。所有堆栈都是树,所有树都是DAG,但是大多数DAG不是树,并且大多数树不是堆栈。DAG确实具有拓扑顺序,这使您可以将它们存储在堆栈中(用于遍历,例如依赖性解析),但是一旦将它们塞入堆栈,就会丢失有价值的信息。在这种情况下,如果屏幕具有先前的同级,则可以在屏幕与其父屏幕之间导航。

11

这是一个游戏状态堆栈的示例实现,我发现它非常有用:http ://creators.xna.com/en-US/samples/gamestatemanagement

它是用C#编写的,需要XNA框架进行编译,但是您可以查看代码,文档和视频来了解它。

它可以支持状态转换,透明状态(例如模式消息框)和加载状态(用于管理现有状态的卸载和下一个状态的加载)。

我现在在我的(非C#)爱好项目中使用相同的概念(当然,它可能不适合大型项目),对于小型/爱好项目,我绝对可以推荐这种方法。


5

这类似于我们使用的FSM堆栈。基本上,只需为每个状态提供一个enter,exit和tick功能,然后按顺序调用它们。也非常适合处理诸如加载之类的事情。


3

其中一个“游戏编程宝石”卷中有一个状态机实现,用于游戏状态。http://emergent.net/Global/Documents/textbook/Chapter1_GameAppFramework.pdf给出了一个如何在小型游戏中使用的示例,并且不应因Gamebryo而太过易读。


“使用DirectX编程角色扮演游戏”的第一部分还实现了状态系统(和过程系统-非常有趣的区别)。
Ricket 2010年

那是一个很棒的文档,并且几乎解释了我过去的实现方式,只是缺少了示例中使用的不必要的对象层次结构。
dash-tom-bang 2010年

3

只是为了增加讨论的标准化,这种数据结构的经典CS术语是下推式自动机


我不确定状态堆栈的任何现实世界实现几乎等同于下推式自动机。正如在其他答案中提到的那样,实际的实现总是以诸如“弹出两个状态”,“交换这些状态”或“将该数据传递到堆栈外部的下一个状态”之类的命令结束。自动机是自动机-计算机-而不是数据结构。状态堆栈和下推自动机都使用堆栈作为数据结构。

1
“我不确定状态堆栈的任何实际实现几乎等同于下推式自动机。” 有什么不同?两者都有一组有限的状态,状态历史以及用于推入和弹出状态的原始操作。您提到的其他任何操作在根本上都没有区别。“弹出两个状态”仅弹出两次。“交换”是一种流行和推动。传递数据不在核心思想之内,但是每个使用“ FSM”的游戏也会添加其他数据,而不会觉得名称不再适用。
优厚

在下推式自动机中,唯一会影响过渡的状态是顶部状态。不允许在中间交换两个状态。即使看中间的州也是不允许的。我觉得“ FSM”一词的语义扩展是合理的,并且有好处(并且对于最严格的含义,我们仍然有“ DFA”和“ NFA”这两个词),但是“下推自动机”严格来说是计算机科学术语,如果我们将它应用于那里的每个基于堆栈的系统,那么只会有混乱的等待。

我更喜欢那些可能影响任何事物的唯一状态是最上面的状态的实现,尽管在某些情况下,能够过滤状态输入并将处理传递到“较低”状态很方便。(例如,控制器输入处理映射到此方法,最高状态获取其关心的位,并可能清除它们,然后将控制权传递到堆栈上的下一个状态。)
dash-tom-bang 2010年

1
好点,固定!
丰厚的

1

我不确定堆栈是否完全必要以及是否限制状态系统的功能。使用堆栈,您不能将状态“退出”到多种可能性之一。假设您从“主菜单”开始,然后转到“加载游戏”,则在成功加载已保存的游戏后,您可能要进入“暂停”状态,如果用户取消了加载,则返回“主菜单”。

我只是让状态指定退出时要遵循的状态。

对于您要返回到当前状态之前的状态的那些实例,例如“主菜单->选项->主菜单”和“暂停->选项->暂停”,只需将启动参数传递给状态状态回去。


也许我误解了这个问题?
Skizz 2010年

不,你没有。我认为选民没有。
共产党鸭子

使用堆栈并不排除使用显式状态转换。
dash-tom-bang 2010年

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

我在几款游戏中使用了非常相似的系统,发现除了几个例外,它还是一个出色的UI模型。

我们遇到的唯一问题是在某些情况下需要在推入新状态之前弹出多个状态(我们对UI进行重排以删除需求,因为这通常是不良UI的标志)并创建向导样式线性流(通过将数据传递到下一个状态可轻松解决)。

我们使用的实现实际上包装了堆栈,并处理了更新和渲染以及堆栈上的操作的逻辑。堆栈上的每个操作都会触发状态事件,以将发生的操作通知它们。

还添加了一些帮助程序功能以简化常见任务,例如“交换”(“交换”和“推入”,用于线性流)和“重置”(用于退回到主菜单,或结束流)。


作为UI模型,这是有道理的。我会毫不犹豫地称它们为状态,因为在我的脑海中,我将其与主游戏引擎的内部相关联,而“主菜单”,“选项菜单”,“游戏屏幕”和“暂停屏幕”是较高级别,并且通常只与核心游戏的内部状态没有任何交互,只需以“暂停”,“取消暂停”,“负载级别1”,“开始级别”,“重新开始级别”, “保存”,“还原”,“设置音量级别57”等。显然,这可能因游戏而异。
凯文·卡斯卡特

0

这是我几乎所有项目都采用的方法,因为它工作得非常好并且非常简单。

我最近的项目Sharplike正是以这种方式处理控制流。我们的状态都与一组事件功能连接在一起,这些状态函数在状态更改时被调用,它具有“命名堆栈”的概念,在该概念中,您可以在同一状态机中具有多个状态堆栈,并在它们之间进行分支。工具,不是必需的,但方便。

我要告诫Skizz建议的“告诉控制器结束时该状态应该遵循哪个状态”范式是警告:这在结构上不是很合理,并且它使对话框之类的东西(在标准堆栈状态范式中仅涉及创建一个新的对话框)。状态子类并添加新成员,然后在返回调用状态时将其读出来),其难度要大得多。


0

我基本上在多个系统中正交使用了这个精确系统。例如,前端和游戏内菜单(也称为“暂停”)状态具有其自己的状态堆栈。游戏中的UI也使用了类似的方法,尽管它具有状态切换可能会变色但在各州之间以通用方式更新的“全局”方面(例如健康栏和地图/雷达)。

游戏中的菜单可能由DAG表示为“更好”,但使用隐式状态机(转到另一个屏幕的每个菜单选项都知道如何去那里,并且按返回按钮始终会弹出顶部状态),效果是一模一样。

这些其他系统中的一些还具有“替换顶部状态”功能,但是通常先执行,StatePop()然后再执行StatePush(x);

存储卡的处理方法类似,因为实际上我确实将大量“操作”推入了操作队列(在功能上与堆栈做相同的事情,只是作为FIFO而不是LIFO);一旦开始使用这种结构(“现在发生了一件事情,当它完成后它就会弹出”),它将开始感染代码的每个区域。甚至AI也开始使用这种东西。当玩家发出声音但未被看见时,AI变得“笨拙”,然后切换为“疲倦”,然后当他们看到玩家时,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]();
}
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.