「シーン管理」タグのついた投稿

今回は、結構前のメモで作った状態管理用のクラスを、どんなゲームでも使いまわし出来るように変更します。

前回作ったクラスは、GameStatesというenumを使用して各シーンを管理していましたが、その場合他のゲームで使う場合は新しくenumを書き直す必要があったり、他の型(例えばstring型)を使って管理しようとした場合変更するのが面倒です。なので今回はC#のジェネリックを使用し、使いまわし出来るようにします。ジェネリックは簡単に言うとContent.Load<ジェネリック>(path)

で「ジェネリック」と書いてある部分です。これにより、使用時に使う型を決められるので、柔軟性が出来ます。

まず、GameStateManagerクラスを変更します。ジェネリックを使用したクラスは、クラス名の宣言後に<T>みたいにジェネリックを使用していることを表さなくてはいけません(<>の中身はTである必要はありません。好きに変更できますが、その場合下にあるコードのTの部分を全て新しい名前に変更してください)。

class GameStateManager<T>
{
T m_currentState;
        ...

そして、クラス内で、GameStatesのenumを使用していた箇所を全て`T`に置き換えてください。

次に、GameStateクラスをジェネリックを使用するように変更します。クラスの宣言部だけでなく、GameStateManagerを使用している部分もGameStateManagerから、GameStateManager<T>に変更してください。

abstract class GameState<T>
{
GameStateManager<T> manager; // このクラスを保存しているクラス
        ...

public GameState(Game game, GraphicsDeviceManager graphics, GameStateManager<T> owner)
{
this.game = game;
this.graphics = graphics;
this.content = game.Content;
this.spriteBatch = new SpriteBatch(graphics.GraphicsDevice);
this.manager = owner;
}

public GameStateManager<T> Manager
{
get { return manager; }
}

        // ...その他のコード

次に、またGameStateManagerクラスを変更します。今度は、GameStateクラスを使用している部分を、GameState<T>に変更します。このクラスは、全体的に変更部分が多いので、クラスの殆どを乗せたコードを下に張っときます。

class GameStateManager<T>
{
Dictionary<T, GameState<T>> m_states;
T m_currentState;

public GameStateManager()
{
m_states = new Dictionary<T, GameState<T>>();
}

public T CurrentState
{
get { return m_currentState; }
set { m_currentState = value; }
}

public void AddState(GameState<T> toAdd, T type)
{
m_states.Add(type, toAdd);
}

public bool RemoveState(T toRemove)
{
return m_states.Remove(toRemove);
}

public void ChangeState(T newState)
{
m_currentState = newState;
}

public void Update(GameTime gameTime)
{
GameState<T> state;

//現在のstateがある場合、保存されているGameStateへの参照を得る
if (m_states.TryGetValue(m_currentState, out state))
{
// 現在のシーンを更新
state.Update(gameTime);
}
}

次に、MenuStateクラスを変更します。変更部分は、GameStateクラスを継承するときに、どの型を使ってシーンを切り替えるかを表すのと、GameStateManagerにも同じ様に型を表します。今回は、string型を使用してシーンの管理をします。

class MenuState : GameState<string>
{
public MenuState(Game game, GraphicsDeviceManager graphics, GameStateManager<string> owner)
: base(game, graphics, owner)
{
// ... 初期化処理
}

public override void Update(GameTime gameTime)
{
// ..メニュー画面の更新処理

// メニューで選択された場合
if (Input.IsPressed(Keys.Enter))
{
                    // 別のシーンへ移動
Manager.ChangeState("Play");
}
}

後は、Game1クラス内で、実際に各シーンとマネージャを使用する箇所を変更するだけです。

public class Game1 : Microsoft.Xna.Framework.Game
{
GraphicsDeviceManager graphics;
SpriteBatch spriteBatch;
GameStateManager<string> manager;

...

protected override void LoadContent()
{
spriteBatch = new SpriteBatch(GraphicsDevice);

// 新しいシーンを足す
manager = new GameStateManager<string>();
manager.AddState(new MenuState(this, graphics, manager), "Menu");
}

以上です。少し説明が飛びがちで解り難かったかも知れませんが、ジェネリックを使用したほうが、ライブラリ化する時とかに再利用性が高まるのでオススメです。ですが、解りやすいコードをとことん目指すのであれば、string型や、int型を使用するだけでも十分です。

 

今回は、ゲームの状態管理をクラス化する方法をメモります。

今回は設計として、3種類のクラスを作ります:

–       全ての状態(これからはシーンと書きます)の基礎となるクラス

–       基礎クラスを継承した各シーンのクラス(例:メニューシーンクラス, ローディングシーン)

–       全てのシーンクラスをを管理するクラス

最初に各シーンの基礎となるクラスを作ります。まず、各シーンに必要となるであろうメンバーを宣言し、コンストラクタで受け取ります。

abstract class GameState
{
GameStateManager manager; // このクラスを保存しているクラス
ContentManager content;
GraphicsDeviceManager graphics;
Game game;
SpriteBatch spriteBatch;

public GameState(Game game, GraphicsDeviceManager graphics, GameStateManager owner)
{
this.game = game;
this.graphics = graphics;
this.content = game.Content;
this.spriteBatch = new SpriteBatch(graphics.GraphicsDevice);
this.manager = owner;
}
 

各シーンごとにこれらのメンバーを保存することによって、Game1に近い形で使えるようになります。

次に、各メンバーのアクセサを作ります。Getだけにしているのは、継承するクラスや、外部から変更できなくするためです。

public Game Game
{
get { return game; }
}

public GameStateManager Manager
{
get { return manager; }
}

public GraphicsDevice GraphicsDevice
{
get { return graphics.GraphicsDevice; }
}

public GraphicsDeviceManager GraphicsManager
{
get { return graphics; }
}

public ContentManager Content
{
get { return content; }
}

public SpriteBatch SpriteBatch
{
get { return spriteBatch; }
}
 

最後に、更新と描画の基礎となるメソッドを作ります。このクラスは、上記でabstractクラスとして宣言されており、abstractメソッドを宣言できます。abstractと記されているメソッドは継承したクラスが必ずオーバーライドしなくてはいけません(していない場合は、エラーになります)。これにより、各シーンは更新と、描画のメソッドを持つことを保障されます。

// 継承するクラスはこのメソッドをオーバーライドしなければいけない
public abstract void Update(GameTime gameTime);
public abstract void Draw(GameTime gameTime);
 

ちなみにabstractクラスは、インターフェイス同様インスタンスを作ることが出来ません(継承したクラスのインスタンスは作れます)。インターフェイスとの違いは、メソッドを宣言だけでなくコンストラクタやアクセサを含めたメソッドを実装を出来る事です。なので、状況によっては、GameStateクラス内に何かメソッド足すことも出来ます。

基礎クラスが出来た後は、各シーンを管理するクラスを作ります。まず、前回同様各シーンを表すenumを宣言します。

enum GameStates
{
Menu,
Playing,
Paused,
Shop,
Transition,
}
 

次に、管理クラスの宣言部分を書きます。今回、各シーンを保存するのにDictionaryクラスを使用します(Dictionaryクラスについてはこちらをご参照ください)。List<>とかのクラスでなくDictionaryを使う理由は、同じ値を持つkey(今回はGameStatesのenum)を二つ保存できないからと、enumに限らず様々なクラスを中身を得る時に使えるからです。例を挙げると、Dictionary<string, GameState>と宣言すると、”Menu”などの文字列で管理できます(中身を参照する場合はm_states[GameStates.Menu] の代わりに、m_states[”Menu”]となります)。

class GameStateManager
{
Dictionary<GameStates, GameState> m_states;
GameStates m_currentState;

public GameStateManager()
{
m_states = new Dictionary<GameStates, GameState>();
m_currentState = GameStates.Menu;
}
 

m_currentStateは現在のシーンを表し、そのシーンだけ更新と描画をします。次に、シーンを足す、消す、切り替えるをする為のメソッドを作ります。

public void AddState(GameState toAdd, GameStates type)
{
m_states.Add(type, toAdd);
}

public bool RemoveState(GameStates toRemove)
{
return m_states.Remove(toRemove);
}

public void ChangeState(GameStates newState)
{
m_currentState = newState;
}
 

後は更新と描画処理です。それぞれのメソッドは、現在設定されているシーンがあるかを確かめ、もしDictionary内に有ればそのシーンを処理します。

public void Update(GameTime gameTime)
{
GameState state;

//現在のstateがある場合、保存されているGameStateへの参照を得る
if (m_states.TryGetValue(m_currentState, out state))
{
// 現在のシーンを更新
state.Update(gameTime);
}
}
 

描画処理も同じ様に実装できます。

これで基礎となる部分は実装できたので後は実際に各シーンのクラスを作るのと、Game1内でそれらを動かすだけです。各シーンのクラスは上でも書いたように、GameStateクラスを継承します。メニューのシーンクラスの実装例は以下です。

class MenuState : GameState
{
Menu menu;
SpriteFont font;

public MenuState(Game game, GraphicsDeviceManager graphics, GameStateManager owner)
: base(game, graphics, owner)
{
// ... 初期化処理
}

public override void Update(GameTime gameTime)
{
// ..メニュー画面の更新処理
}

public override void Draw(GameTime gameTime)
{
// ..メニュー画面の描画処理
}
}
 

後は、Game1内でGameStateManagerを使うだけです。

GameStateManager manager;
protected override void LoadContent()
{
spriteBatch = new SpriteBatch(GraphicsDevice);

// 新しいシーンを足す
manager = new GameStateManager();
manager.AddState(new MenuState(this, graphics, manager), GameStates.Menu);
}
protected override void Update(GameTime gameTime)
{
manager.Update(gameTime);
base.Update(gameTime);
}

protected override void Draw(GameTime gameTime)
{
GraphicsDevice.Clear(Color.CornflowerBlue);
manager.Draw(gameTime);
base.Draw(gameTime);
}
 

以上です。これはあくまで実装例なので、色々と自分の使いやすいようにいじってみてください(特にシーンの切り替え部分は個人差が出ますので、使いやすいようにしてください)。

次回は、今回作ったクラスを色々なゲームに使いまわし出来る様に変更する事を書く予定ですが、それ以外にも実装方法自体を変えることも出来るのでそのへんも使いやすいように変更してください(例を挙げると、AppHubのサンプルはDictionaryではなく、stackを使用して管理しています)。

今回はゲーム内の状態変化の実装の仕方についてメモります。

状態変化とは具体的な例を挙げれば:

 

オープニング → メニュー → ゲーム→ その他色々

 

と言う風にシーンを移動することです。

これに関しては実装方法が幾つかあり、1回で全部を書くのは時間がかかるので、今回はenumとswitch文を使用しての実装方法を書きます。なお、このメモには前に書いたメニュークラスと、同じ記事で説明した入力用のメソッドを使っています。今回乗せる全てのコードは、Game1クラス内に書かれています。

 

まず、状態(シーン)のタイプを表すenumを宣言します。

enum GameStates
{
Menu,
InGame,
Pause,
};

// 現在の状態を表す変数。最初はメニュー画面
GameStates state = GameStates.Menu;

次に、Update()メソッド内にGameStatesのenumを使用してのswitch文を書きます。これで各シーンごとに何を更新するかを決めています(例:stateの値がGameStates.Pauseの場合は、ポーズの部分だけを処理する)。Switch文が長くなると読みにくくなるので、各シーンの処理は個別のメソッドに分けています。

protected override void Update(GameTime gameTime)
{
float delta = (float)gameTime.ElapsedGameTime.TotalSeconds;

// 現在の状態によって呼ぶupdateメソッドを
// 変更する
switch (state)
{
case GameStates.Menu:
UpdateMenu(delta);
break;
case GameStates.InGame:
UpdateInGame(delta);
break;
case GameStates.Pause:
UpdatePause(delta);
break;
}

base.Update(gameTime);
}
 

各メソッドの処理は必要なものに合わせてください。メニュー画面は、前に作ったものをUpdateMenu()メソッドに移すことも出来ます。

private void UpdateMenu(float delta)
{
menu.Update(delta);
if (Input.IsPressed(Keys.Down))
{
// 下へ移動
menu.SelectNext();
}

if (Input.IsPressed(Keys.Up))
{
// 上へ移動
menu.SelectPrevious();
}

// メニューで選択された場合
if (Input.IsPressed(Keys.Enter))
{
int option = menu.SelectedNumber;

if (option == 0)
{
state = GameStates.InGame;
}
else if (option == 1)
{
// ...Optionが選択されたときの処理
}
else if (option == 2)
{
this.Exit();
}
}
}

private void UpdateInGame(float delta)
{
if (Input.IsPressed(Keys.Space))
{
// ポーズ画面へ移動
state = GameStates.Pause;
}
else if (Input.IsPressed(Keys.Enter))
{
// メニュー画面へ移動
state = GameStates.Menu;
}
}

private void UpdatePause(float delta)
{
if (Input.IsPressed(Keys.Space))
{
// ゲーム画面へ戻る
state = GameStates.InGame;
}
}
 

各メソッド内では、入力によってstate変数を変えることで違うシーンに移動しています。

後は、描画処理です。描画もUpdate()メソッドと同じ様に書くことで実装できます。

protected override void Draw(GameTime gameTime)

GraphicsDevice.Clear(Color.CornflowerBlue);

spriteBatch.Begin();

switch (state)
{
case GameStates.Menu:
DrawMenu();
break;
case GameStates.InGame:
DrawInGame();
break;
case GameStates.Pause:
DrawPause();
break;
}

spriteBatch.End();
base.Draw(gameTime);
}
 

Draw()メソッドもUpdate()メソッド同様シーンごとにメソッドを分けています。ちなみに、このサンプルには2種類のSpriteFontを使用しています。

private void DrawMenu()
{
menu.Draw(spriteBatch, font);
spriteBatch.DrawString(largeFont, "Menu Screen", new Vector2(300, 20), Color.Silver);
}

private void DrawInGame()
{
spriteBatch.DrawString(largeFont, "In Game", new Vector2(300, 20), Color.Silver);
spriteBatch.DrawString(font, "Press Space to Pause", new Vector2(300, 300), Color.Silver);
spriteBatch.DrawString(font, "Press Enter to return to Menu", new Vector2(300, 340), Color.Silver);
}

private void DrawPause()
{
spriteBatch.DrawString(font, "Pause Screen", new Vector2(300, 20), Color.Silver);
spriteBatch.DrawString(font, "Press Space to resume", new Vector2(300, 300), Color.Silver);
}
 

以上です。実装後はこんな感じになります:

 

この方法はゲームのシーン以降だけでなく、様々なところで使えます。ですが、ゲームのシーン用となると処理が多くなり、結果的にGame1が巨大化したり、作業分担が面倒になるので、個人的にはオブジェクト指向を使った状態変化を実装することをオススメします。それに関しては、多分次回メモります。