ゲーム状態「スタック」?


52

ゲームの状態をゲームに実装する方法を考えていました。主なものは次のとおりです。

  • 半透明の上位の状態-一時停止メニューから背後のゲームまで見ることができる

  • OO-Iの何かは、これをより簡単に使用し、背後にある理論を理解し、組織化を維持し、さらに追加することに気付きます。



リンクリストの使用を計画しており、スタックとして扱いました。これは、半透明のために以下の状態にアクセスできることを意味します。
計画:状態スタックをIGameStatesへのポインターのリンクリストにする。一番上の状態は独自の更新コマンドと入力コマンドを処理し、その下の状態を描画するかどうかを決定するメンバーisTransparentを持っています。
それから私はできました:

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

プレーヤーの読み込みを表すために、オプションに移動してから、メインメニューに進みます。
これはいいアイデアですか、それとも...?他の何かを見るべきですか?

ありがとう。


OptionsMenuStateの背後にあるMainMenuStateを表示しますか?または、OptionsMenuStateの背後にあるゲーム画面だけですか?
スキズ

計画では、州に不透明度/ isTransparent値/フラグを設定しました。トップステートがこれに該当するかどうかを確認し、該当する場合はどのような価値があるかを確認します。次に、他の状態よりも多くの不透明度でレンダリングします。この場合、いいえ私はしません。
共産主義のダック

私はそれが遅いことを知っていますが、将来の読者には:newサンプルコードに示されている方法で使用しないでください。メモリリークまたはその他のより深刻なエラーを求めているだけです。
ファラプ

回答:


44

coderangerと同じエンジンで作業しました。私は異なる視点を持っています。:)

まず、FSMのスタックがありませんでした-状態のスタックがありました。状態のスタックは単一のFSMを作成します。FSMのスタックがどのようになるかはわかりません。おそらく複雑すぎて実用的ではありません。

私たちのグローバルステートマシンの最大の問題は、ステートのセットではなく、ステートのスタックであったことです。これは、たとえば、... / MainMenu / Loadingが... / Loading / MainMenuとは異なることを意味します。これは、ロード画面の前または後にメインメニューが表示されたかどうかによって異なります(ゲームは非同期で、ほとんどがサーバー駆動です)。

これが見苦しい2つの例として:

  • LoadingGameplay状態などにつながったため、Gameplay状態内で読み込むためのBase / LoadingとBase / Gameplay / LoadingGameplayがあり、通常の読み込み状態で多くのコードを繰り返す必要がありました(ただし、すべてではなく、 )。
  • 「キャラクター作成者の場合はゲームプレイに、ゲームプレイの場合はキャラクターセレクトに、キャラクターセレクトの場合はログインに戻る」などのいくつかの機能がありました。同じインターフェースウィンドウを異なる状態で表示したいが、戻る/進むボタンは引き続き機能します。

名前にもかかわらず、それはあまり「グローバル」ではありませんでした。ほとんどの内部ゲームシステムは、内部状態を追跡するためにそれを使用しませんでした。これは、他のシステムで状態をいじりたくないためです。他のユーザー、たとえばUIシステムは、これを使用できますが、状態を独自のローカル状態システムにコピーする場合にのみ使用できます。(UI状態のシステムには特に注意が必要です。UI状態はスタックではなく、実際にはDAGであり、その上に他の構造を強制しようとすると、使用するのがイライラするUIしか作成されません。)

ゲームフローが実際にどのように構成されているかを知らないインフラストラクチャプログラマからコードを統合するためのタスクを分離したので、パッチャーを書いている人に「Client_Patch_Updateにコードを入れて」、グラフィックを書いている人に「Client_MapTransfer_OnEnterにコードを入れて」ロードすると、問題なく特定のロジックフローを交換できます。

サイドプロジェクトでは、スタックではなく状態セットで幸運がありました。無関係なシステム用に複数のマシンを作成することを恐れず、「グローバルな状態」のtrapに陥ることを拒否しますグローバル変数を使用して物事を同期するための複雑な方法-確かに、あなたはいくつかの締め切り近くでそれを行うことになりますが、それをあなたの目標として設計しないでください。基本的に、ゲーム内の状態はスタックではなく、ゲーム内の状態はすべて関連しているわけではありません。

GSMはまた、関数ポインターと非ローカル動作が行う傾向があるため、物事のデバッグをより困難にしましたが、そのような大きな状態遷移のデバッグは、それができる前はあまり面白くありませんでした。状態スタックの代わりに状態セットは実際にはこれを助けませんが、あなたはそれに注意する必要があります。関数ポインターではなく仮想関数は、それをいくらか軽減するかもしれません。


すばらしい答え、ありがとう!私はあなたの投稿と過去の経験から多くを得ることができると思います。:D + 1 /ティック。
共産主義のカモ

階層の優れた点は、最上位にプッシュされたユーティリティ状態を構築でき、他に何が実行されているかを心配する必要がないことです。
-coderanger

私はそれがセットではなく階層の議論である方法を見ていません。むしろ、階層構造は、どこにプッシュされたかわからないため、すべての状態間通信をより複雑にします。

UIが実際にDAGであるという点はよく考えられていますが、スタックで表現できることは確かです。接続された有向非巡回グラフ(および接続されたDAGでない場合は考えられません)はツリーとして表示でき、スタックは本質的にツリーです。
エドロップル

2
スタックは、すべてのグラフのサブセットであるDAGのサブセットであるツリーのサブセットです。すべてのスタックはツリー、すべてのツリーはDAGですが、ほとんどのDAGはツリーではなく、ほとんどのツリーはスタックではありません。DAGにはトポロジの順序があり、スタックに格納できます(依存関係の解決などのトラバーサルのため)が、スタックに詰め込むと貴重な情報が失われます。この場合、前の兄弟がいる場合、画面とその親の間をナビゲートする機能。

11

これは、非常に役立つことがわかったgamestateスタックの実装例です。http://creators.xna.com/en-US/samples/gamestatemanagement

C#で記述されており、コンパイルするにはXNAフレームワークが必要ですが、コード、ドキュメント、ビデオを確認するだけでアイデアを得ることができます。

状態遷移、透過状態(モーダルメッセージボックスなど)、および読み込み状態(既存の状態のアンロードと次の状態の読み込みを管理する)をサポートできます。

私は(C#以外の)趣味のプロジェクトで同じ概念を使用しています(確かに、大規模なプロジェクトには適さないかもしれません)。


5

これは、使用するFSMのスタックに似ています。基本的には、各状態にエンター、イグジット、ティック機能を与え、それらを順番に呼び出します。ロードなどの処理にも非常に適しています。


3

「Game Programming Gems」ボリュームの1つには、ゲームの状態を対象としたステートマシンの実装が含まれていました。http://emergent.net/Global/Documents/textbook/Chapter1_GameAppFramework.pdfには、小さなゲームで使用する方法の例がありますが、Gamebryo固有のもので読みやすいものであってはなりません。


「DirectXを使用したロールプレイングゲームのプログラミング」の最初のセクションでは、状態システム(およびプロセスシステム-非常に興味深い違い)も実装しています。
リケット

これは素晴らしいドキュメントであり、例で使用している不要なオブジェクト階層を除いて、過去にどのように実装したかをほぼ正確に説明しています。
ダッシュトムバン

3

議論に少し標準化を加えるために、この種のデータ構造の古典的なCS用語はプッシュダウンオートマトンです。


状態スタックの実際の実装がプッシュダウンオートマトンとほぼ同等であることはわかりません。他の回答で述べたように、実際の実装では、常に「2つの状態をポップ」、「これらの状態をスワップ」、「このデータをスタック外の次の状態に渡す」などのコマンドになります。そして、オートマトンはオートマトン-コンピューター-データ構造ではありません。状態スタックとプッシュダウンオートマトンはどちらも、スタックをデータ構造として使用します。

1
「状態スタックの実際の実装がプッシュダウンオートマトンとほぼ同等であるかどうかはわかりません。」違いは何ですか?どちらにも、状態の有限セット、状態の履歴、および状態をプッシュおよびポップするプリミティブ操作があります。あなたが言及する他の操作はどれも基本的にそれとは異なりません。「ポップ2状態」は2回ポップするだけです。「スワップ」はポップとプッシュです。データの受け渡しはコアアイデアの範囲外ですが、「FSM」を使用するすべてのゲームは、名前が適用されなくなったように感じることなく、追加のデータにもタックします。
10

プッシュダウンオートマトンでは、遷移に影響を与えることができる唯一の状態は、一番上の状態です。中央で2つの状態を入れ替えることは許可されていません。中央の状態を見ることさえ許可されていません。「FSM」という用語のセマンティック拡張は合理的で有益であると感じています(そして、最も制限された意味では「DFA」と「NFA」という用語がまだあります)。そこにあるすべてのスタックベースのシステムに適用する場合、混乱が待っているだけです。

何らかの影響を与える可能性のある唯一の状態が最上位の状態である実装を好みますが、場合によっては、状態入力をフィルタリングし、処理を「下位」状態に渡すことができると便利です。(たとえば、コントローラーの入力処理はこのメソッドにマップされ、トップステートは関心のあるビットを取得し、場合によってはクリアしてから、スタック上の次のステートに制御を渡します。)
dash-tom-bang

1
良い点、修正済み!
16年

1

状態システムの機能を制限するだけでなく、スタックが完全に必要かどうかもわかりません。スタックを使用すると、いくつかの可能性の1つに状態を「終了」できません。「メインメニュー」から始めて「ゲームの読み込み」に移動するとします。保存したゲームを正常に読み込んだ後に「一時停止」状態に移行し、ユーザーが読み込みをキャンセルした場合は「メインメニュー」に戻ります。

終了時に追跡する状態を指定するだけです。

現在の状態より前の状態に戻りたい場合、たとえば「メインメニュー->オプション->メインメニュー」や「一時停止->オプション->一時停止」の場合、起動パラメータとして状態に渡します。戻る状態。


たぶん私は質問を誤解したのでしょうか?
スキズ

いいえ、あなたはしませんでした。ダウン投票者がしたと思います。
共産主義のダック

スタックを使用しても、明示的な状態遷移の使用が妨げられることはありません。
ダッシュトムバン

1

遷移などの別の解決策は、宛先エンジンとソースステートをステートマシンと共に提供することです。ステートマシンは、「エンジン」にリンクされる可能性があります。真実は、ほとんどのステートマシンはおそらく手元のプロジェクトに合わせて調整する必要があるでしょう。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モデルとして、これはある程度理にかなっています。「メインメニュー」、「オプションメニュー」、「ゲーム画面」、「一時停止画面」はより高いレベルですが、頭の中でそれをメインのゲームエンジンの内部に関連付けるため、これらの状態を呼び出すことをheします。多くの場合、コアゲームの内部状態との相互作用はなく、単に「一時停止」、「一時停止解除」、「負荷レベル1」、「開始レベル」、「再起動レベル」の形式のコマンドをコアエンジンに送信します。 「保存」、「復元」、「音量レベル57を設定」など。もちろん、これはゲームによって大きく異なる可能性があります。
ケビンキャスカート

0

これは、ほとんどすべてのプロジェクトで採用しているアプローチです。非常にうまく機能し、非常にシンプルだからです。

私の最新のプロジェクトSharplikeは、制御フローをこのように正確に処理します。私たちの状態はすべて、状態が変化したときに呼び出される一連のイベント関数と結びついており、同じ状態マシン内に複数の状態のスタックを持ち、それらの間で分岐できる「名前付きスタック」の概念を特徴としていますツール、および必要ではありませんが、持っていると便利です。

Skizzによって提案された「コントローラーが終了時にどの状態に従うべきか」と言うパラダイムに注意してください。構造的に健全ではなく、ダイアログボックスのようなものを作成します(標準のスタック状態パラダイムでは、状態サブクラス(新しいメンバーを含む)、呼び出し状態に戻ったときにそれを読み取る)、必要以上にはるかに困難です。


0

基本的に、この正確なシステムをいくつかのシステムで直交的に使用しました。たとえば、フロントエンドおよびゲーム内メニュー(別名「一時停止」)状態には、独自の状態スタックがありました。ゲーム内のUIもこのようなものを使用していましたが、状態の切り替えが色づくが状態間で共通の方法で更新される「グローバル」な側面(ヘルスバーやマップ/レーダーなど)がありました。

ゲーム内メニューはDAGによって「より良く」表現される場合がありますが、暗黙のステートマシン(別の画面に移動する各メニューオプションはそこへの行き方を知っており、戻るボタンを押すと常に一番上の状態になります)まったく同じ。

これらの他のシステムの一部には「トップステートの置換」機能もありましたが、通常、次のように実装されStatePop()ていましたStatePush(x);

メモリカードの処理は、実際には大量の「操作」を操作キューにプッシュしたため、同様でした(LIFOではなくFIFOのように機能的にスタックと同じことを行いました)。この種の構造の使用を開始すると(「現在1つのことがあり、終了すると自動的にポップします」)、コードのすべての領域に感染し始めます。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]();
}
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.