7

私はゲームを書いていて、そのさまざまな状態 (Game Maker のアナロジーはフレームだろうと思います) をきれいなオブジェクト指向の方法でモデル化したいと考えています。以前は、次の方法でそれを行っていました。

class Game
{
  enum AppStates
  {
    APP_STARTING,
    APP_TITLE,
    APP_NEWGAME,
    APP_NEWLEVEL,
    APP_PLAYING,
    APP_PAUSED,
    APP_ENDED
  };

  typedef AppState(Game::*StateFn)();
  typedef std::vector<StateFn> StateFnArray;

  void Run()
  {
    // StateFn's to be registered here

    AppState lastState(APP_STARTING);
    while(lastState != APP_ENDED)
    {
      lastState = GetCycle_(lastState);
    }
    // cleanup
  }

protected:
  // define StateFn's here

  AppState GetCycle_(AppState a)
  {
    // pick StateFn based on passed variable, call it and return its result.
  }

  StateFnArray states_;
};

これは、小規模なプロジェクトではほとんど管理できませんでした。状態が使用していたすべての変数は Game クラスにダンプされましたが、オブジェクト指向性を最大限に維持し、複数の状態で共有される変数のみを公開したいと考えています。また、終了したばかりの状態で行うのではなく、新しい状態に切り替えるときに新しい状態を初期化できるようにしたいと考えています (複数の結果が生じる可能性があるため、APP_PLAYING は APP_PAUSED、APP_GAMEOVER、APP_NEWLEVEL などに切り替えることができます)。

私はこのようなことを考えました(注意!ファジースタッフ!):

struct AppState
{
  enum { LAST_STATE = -1; }
  typedef int StateID;
  typedef std::vector<AppState*> StateArray;

  static bool Add(AppState *state, StateID desiredID);
  // return false if desiredID is an id already assigned to

  static void Execute(StateID state)
  {
    while(id != LAST_STATE)
    {
      // bounds check etc.
      states_[id]->Execute();
    }
  }

  AppState() {};
  virtual ~AppState() {};

  virtual StateID Execute() =0; // return the ID for the next state to be executed

protected:
  static StageArray stages_;
};

ここでの問題は、クラスとインスタンスのレベルがごちゃ混ぜになっていることです (静的と仮想)。状態は AppState から継承する必要がありますが、私が想像するに、それらのほとんどはすべて静的メンバーを持つクラスになるか、少なくとも 1 つのクラス (TitleState、LevelIntroState、PlayingState) から複数のインスタンスは必要ありません。 、GameOverState、EndSequenceState、EditorState... - 一時停止は、意味のある状態で処理されるのではなく、状態ではなくなります)。

どうすればそれをエレガントかつ効率的に行うことができますか?

4

4 に答える 4

10

次の記事では、ゲームの状態を管理する簡単で優れた方法を紹介しています。

http://gamedevgeek.com/tutorials/managing-game-states-in-c/

基本的に、ゲーム ステートのスタックを維持し、トップ ステートのみを実行します。多くの州でインスタンスが 1 つしかないことは間違いありませんが、これは実際には問題ではありません。ただし、実際には、あなたが話している状態の多くは、複数のインスタンスを持つ可能性があります。例えば:

push TitleState
push MenuState
push LevelIntroState
change_to PlayingState
change_to GameOverState
pop (back to MenuState)

... の新しいインスタンスでやり直すことができますLevelIntroState

于 2009-03-18T08:04:47.293 に答える
3

近いうちに作成するゲームで、ある種のファクトリ パターンとステート パターンを組み合わせて使用​​しています。

コードは少し面倒かもしれませんが、きれいにしようと思います。

これは、メニューやゲームなど、すべての状態を派生させるクラスです。

class GameState {
public:
    virtual ~GameState() { }

    virtual void Logic() = 0;
    virtual void Render() = 0;
};

このクラスは、さまざまな状態を処理するためのインターフェイスになります。動的に追加し、動的に id することができます。

class State {
public:
    State();
    virtual ~State();

    void Init();
    void Shutdown();
    void SetNext( std::string next_state );
    void Exit();

    bool Logic();
    void Render();
protected:
    bool Change();

    std::string state_id;
    std::string next_state;

    GameState *current_state;
    std::vector<std::string> state_ids;

    StateFactory *state_factory;

    bool is_init;
};

ファンクターを使用して、さまざまな GameState 派生物の作成を処理しています。

class BasicStateFunctor {
public:
    virtual GameState *operator ()() = 0;
};

template<class T>
class StateFunctor : public BasicStateFunctor {
public:
    StateFunctor() { }
    GameState *operator ()() {
        return new T;
    }
    typedef T type;
};

最後に、さまざまな状態を保存および管理するファクトリです。

class StateFactory {
public:
    StateFactory();
    virtual ~StateFactory();

    bool CheckState( std::string id );
    GameState *GetState( std::string id );
    template<class T> void AddState( std::string id );
private:
    typedef std::map<std::string, BasicStateFunctor*>::iterator StateIt;
    std::map<std::string, BasicStateFunctor*> state_map;
};

定義ファイル: ここでは多くのことを省略しましたが、うまくいけばアイデアが得られるでしょう。

bool StateFactory::CheckState( std::string id )
{
    StateIt it = state_map.find( id );
    if( it != state_map.end() )
        return true;
    else
        return false;
}

GameState *StateFactory::GetState( std::string id )
{
    StateIt it = state_map.find( id );
    if( it != state_map.end() )
    {
        return (*(*it).second)();
    } else {
        //handle error here
}

template<class T> void StateFactory::AddState( std::string id )
{
    StateFunctor<T> *f = new StateFunctor<T>();
    state_map.insert( state_map.end(), std::make_pair( id, f ) );
}

void State::Init()
{
    state_factory = new StateFactory();
    state_factory->AddState<Game>( "game" );
    current_state = state_factory->GetState( "game" );
    is_init = true;
}

void State::SetNext( std::string new_state )
{
    //if the user doesn't want to exit
    if( next_state != "exit" ) {
        next_state = new_state;
    }
}

bool State::Change()
{
    //if the state needs to be changed
    if( next_state != "" && next_state != "exit" ) 
    {

        //if we're not about to exit( destructor will call delete on current_state ),
        //call destructor if it's a valid new state
        if( next_state != "exit" && state_factory->CheckState( next_state ) ) 
        {
            delete current_state;

            current_state = state_factory->GetState( next_state );

        } 
        else if( next_state == "exit" ) 
        {
                return true;
        }

        state_id = next_state;

        //set NULL so state doesn't have to be changed
        next_state = "";
    }
    return false;
}

bool State::Logic()
{
    current_state->Logic();
    return Change();
}

使用方法は次のとおりです。さまざまな状態を初期化して追加します。Init() で実行しています。

State.Init();

//remember, here's the Init() code:
state_factory = new StateFactory();
state_factory->AddState<Game>( "game" );
current_state = state_factory->GetState( "game" );
is_init = true;

フレーム機能について

State.Logic(); //Here I'm returning true when I want to quit

そしてレンダリング機能について

State.Render();

これは完璧ではないかもしれませんが、私にとってはうまくいきます。設計をさらに進めるには、State に Singleton を追加し、StateFactory を State 内の隠しクラスとして作成します。

于 2009-03-18T22:04:05.983 に答える
2

これが私の解決策です:

  • すべての状態は小さなゲームのようなものなので、スタック上で一連のゲームを管理します。
  • イベントは、誰かが停止するまでスタックをバブルアップします (そのため、「ゲーム」はそれ以上表示されなくなります)。これにより、メニュー内でプラス/マイナスを使用してマップをズームできます。OTOH、最初に開いたメニューがそれを飲み込むので、Escはバブリングを早期に停止します。
  • スタック上の各「ゲーム」には同じメソッドがあります: handleUserEvent()、keyDown()、keyUp()、mousePressed()、mouseReleased()、mouseMotion()、update() (レンダリング前の内部計算)、draw() render)、prepare() (draw() でターゲット サーフェスにスタンプされたばかりのテクスチャに何かをキャッシュすることでレンダリングを最適化します)

レンダリングには、優先度のあるレイヤーを使用しています。したがって、各ゲームは透明なキャンバスにレンダリングされ、レイヤー レンダラーはそれらを正しい順序でレンダリングします。このようにして、各ゲームは、他のプレイヤーが何をしているか気にすることなく、独自のレイヤーを更新できます。

于 2009-03-18T09:23:26.140 に答える
1

GameStatesのリストでGameStateManagerを使用します。ここで、リスト内の各Itemは、IGameStateを実装し、.render()と.HandleInput()の2つのメソッドを持つ「GameStateオブジェクト」です。

このGameStateManagerはシングルトンとして実装されているため、を呼び出すことで、どの状態も別の状態にジャンプできます。

 GameStateManager.gi().setState("main menu")

そしてメインループはこんな感じ

while(isRunning)
{
   GameStateManager.gi().getCurrentState().handleKeyboard(keysobject);
   GameStateManager.gi().getCurrentState().handleMouse(mouseobject);

   GameStateManager.gi().getCurrentState().render(screenobject);

}

このようにして状態を作成するには、IGameStateを実装する新しいクラスを作成し、それをGameStateManagerに追加するだけです。

(注:これは、メインゲーム内でミニゲームを作成するための非常に便利な方法です)

于 2009-03-18T21:49:40.090 に答える