エンジニアとしての市場価値を測りませんか?PR

企業からあなたに合ったオリジナルのスカウトを受け取って、市場価値を測りましょう

33
42

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

switch文を使わないゲーム状態遷移inC++

Last updated at Posted at 2019-03-24

はじめに

今回Qiitaに書き込みするのは初めてです。C++でゲームプログラミングやってるので、自分のやり方を書き残していこうと思います。
独学プログラマ なので、間違っていたり、少しばかり極端なことも書くことになるかと思いますので、C++やゲームプログラミングをやっている諸兄を憤慨させることになるかもしれませんが、その場合はできる限り冷静にご指摘いただけると助かります。
なお、検証はVisualStudio2017のC++コンパイラ及びWindows10の実行環境です。

ここでいう状態遷移とは

ここでいう状態遷移とは、ゲームの各画面(シーン)状態(タイトル画面→キャラセレなど→ゲーム本編→ゲームオーバーなど)の流れをコントロールするものを表しています。
statemachine.PNG
そのほかにもキャラクターのニュートラル→ジャンプ→着地→しゃがみ→攻撃→ダメージなどの状態遷移も扱います。

switch文を使わないってどういう事?

おそらくプログラミングを習い始めの時は、上記に書いたような状態遷移を扱う場合は状態を表すenum変数などを用意して、何かイベントが起きるたびに変数を変更し、switch分岐で処理を切り替えていたと思います。

switch( screenstate ){
case title_scene:
  //タイトル用の処理
  break;
case characterselect_scene:
  //キャラセレ用の処理
  break;
    :
    :
  (以下略)
}

全然それで間違っていないのですが、どうしてもこのswitch文を置いている関数ブロックが長くなってしまうのがあまり好きじゃないです。これが置かれるであろう関数はUpdate関数とかになると思いますが…
凝ったことをやろうとすると、どうしてもこの部分が複雑になっていってよくないです。というわけで僕のプログラムでは状態遷移にswitch文を使用しません。

じゃあなにを使用するのか?

じゃあどうやって状態遷移をするのかというと3つほど考えます

  • Stateパターンによる切り替え
  • メンバ関数ポインタによる切り替え
  • ラムダ式による切り替え(たまにしか使いませんが…)

もしほかに良い、カッコいい状態遷移をお知りの諸兄はコメント欄等で教えていただけると幸いです。

Stateパターンで状態遷移

ひとまずは、シーンの遷移をStateパターンで作ってみることを考えます。基底クラスをSceneとします。

遷移基底クラス

class Input;
class SceneController;
///シーン管理のための基底クラス
///(純粋仮想クラス)
class Scene
{
protected:
  SceneController& _controller;
public:
  Scene(SceneController& controller);
  virtual ~Scene();

  ///シーンの更新を行う
  virtual void Update(const Input& input) = 0;
  ///シーンの描画を行う
  virtual void Draw()= 0;
};

ちなみにInputは入力情報が入ったオブジェクトクラスです。SceneControllerは遷移を制御するクラスです。こいつを基底クラスに持たせることによって、各シーンは次のシーンへ飛ぶことができるようにしています。 ついでだからSceneControllerも書きますが

遷移制御クラス

class Scene;
class Input;
///シーン管理クラス
class SceneController
{
private:
  shared_ptr<Scene> _scene;
public:
  SceneController();
  ~SceneController();
  
  //各シーンのUpdateを呼び出す
  void SceneUpdate(const Input& input);
  
  //シーンを変更する
  void ChangeScene(Scene*);

};

ChangeSceneでシーンの変更を行うようにしています。さて、こうするとクラス・ファイルの数は増えますが、それぞれの状態を独立して管理でき、さらには状態遷移について管理するのは実は状態遷移前のクラスが状態遷移後のクラスを知ってればいいという事になります。

あとはかんたん

さて、あとはご想像の通り、Sceneを継承したクラスを作っていけば良いです。
classdiagram.PNG
ここまでができていれば後は、切り替え部分だけですが簡単です。SceneControllerのメンバ関数にChangeSceneがあったと思いますが、アレの中身は

void
SceneController::ChangeScene(Scene* scene) {
  _scene.reset(scene);
}

このようになっているとします。あとは呼び出し側がChangeSceneにnewして突っ込めば終わりですよね。初期シーンがタイトルシーンだとすると

SceneController::SceneController(){
  _scene.reset(new TitleScene(*this));
}

これでデフォルトがタイトルシーンとなります。thisを渡しているのはタイトルシーンじしんが次のシーンへの切り替えをできるようにするためです。

という事でタイトルシーンが切り替えする部分は

void 
TitleScene::FadeoutUpdate(const Input& input) {
  if (--_wait == 0) {
    _controller.ChangeScene(new CharacterSelectScene(_controller));
  }
  else {
    //フェードアウト処理コード(略)
  }
}

のようにします。タイトルがフェードした後に次の処理に来るように書いています。ちなみにしれっとスマートポインタを使用しているので元のシーンは自動的に消えます。

ちょっと改良

階層型メニューとか、ポーズ画面とかのように、遷移後に「前の画面に戻りたい」という事があるかと思います。結構やってる人も多いみたいなんで書くのは憚られるんですが、敢えて書きます。
状態遷移オブジェクトをスタック型で管理するという事です。

状態遷移をスタックで管理!?

そもそもスタックを知らない人もいるかと思いますが、FILO(First In Last Out)の構造です。どうするかをポーズ画面を例にとって説明します。

まず、ここまでの話をそのまま実装しているなら
1.PNG
こういう遷移になっているでしょうが、ポーズはポーズしたところでゲームオブジェクトを消してもまずいし、終わったら速やかにポーズ状態を解除して、ゲーム中に戻ってきてほしい…なのでスタックを使って
pausein.PNG
こうして
pauseout.PNG
こうしたい。

実装

さて、どう実装しようか?という話ですが、素直にstd::stackを使います。普通のStateパターンが実装できてるなら簡単ですよ~。
まず、SceneControllerをこう書き換えます。

# include<stack>
class Scene;
class Input;
///シーン管理クラス
class SceneController
{
private:
  std::stack<std::shared_ptr<Scene>> _scene;
  
public:
  SceneController();
  ~SceneController();

  void SceneUpdate(const Input& input);
  void ChangeScene(Scene*);
  void PushScene(Scene*);
  void PopScene();
};

さっきまでただのshared_ptr<Scene>だったのが、stack<shared_ptr<Scene>>になってるのが…わかるだろう?
まずはChangeSceneの挙動は**「変えずに」**スタックに対応させていきたい。となるとChangeScene

void
SceneController::ChangeScene(Scene* scene) {
  _scene.pop();
  _scene.emplace(scene);
}

こうなる。ちなみにemplaceというのは、新しい記法で、↑のは内容的には
_scene.push(shared_ptr<Scene>(scene));
と同じ結果になる。もともとあったシーンオブジェクトはpopしてしまって、新しいのをpushしてるんで、Changeと同じ意味になります。
さて、これで元のChangeSceneはできたわけだ。ちがうのはここからだ・・・です。PushScenePopSceneですが、こいつらも簡単です。

void
SceneController::PushScene(Scene* scene) {
  _scene.emplace(scene);
}

void
SceneController::PopScene() {
  _scene.pop();
  assert(!_scene.empty());
}

・・・工事完了です。ね?簡単でしょ?
あとは、ゲームプレイ中にポーズボタン押されたタイミングで、ポーズをPushSceneすればOK

void
GamePlayingScene::Update(const Input& input) {
  if (input.IsTriggered(0, "pause")) {
    _controller.PushScene(new PauseScene(_controller));
  }
  (以下略)
}

あ、PushとPauseがスペル似てるから、混乱しそうだけど間違えないでくださいね。Popはただpopするだけなので説明はしませんが、Pauseシーンが終わる際にpop命令出せばまた元のプレイシーンに戻るわけです。
はい、Stateパターンの状態遷移に関してはこのくらいです。

メンバ関数ポインタによる状態遷移

さて、Stateパターンくらいは知ってるよって人も多いんじゃないかなと思います。が、ここで考えるかもしれません。
「でもさぁ、シーンならともかくキャラクタの状態遷移にStateパターン使うのは大げさすぎ!やっぱりパパッとswitch文書いて…終わりでいいんじゃない?」
まぁ待て待て、まだ慌てるような時間じゃない

メンバ関数ポインタとは・・・?

そこで活用してみたいのはメンバ関数ポインタです。
メンバ関数ポインタじたい基本なはずですが、なんかマイナーなのか知らない人が多いみたいです。
メンバ関数ポインタとはその名の通りクラスのメンバ関数を指し示すポインタの事です。
通常の関数ポインタとちょっと違うので、注意してください。

基本構文

メンバ関数ポインタ宣言

戻り値 (クラス名::*変数名)(パラメータ);
ちょーっとややこしいですが、けったいな宣言ですね。メンバ指定子にアスタリスクがつくというなんだこれって感じの宣言になります。
実際のコードだとこんな感じですね
void (Player::*_updater)(const Input&);
これで宣言はOKなので、あとはメンバ関数の代入と実行だけです。自クラス(この例だとPlayerクラス)内で宣言する場合でも、所有クラス名::を忘れいないようにしなければならないので、注意してください。

メンバ関数ポインタにメンバ関数を代入

ポインタ変数=&クラス::メンバ関数名;
注意点は、&が付くところと所有クラスの名前::がやっぱり必要なところです。&を忘れがちなので注意しましょう。

メンバ関数ポインタで関数を実行

(オブジェクト名->*)(パラメータ);
文法はこうです。これはオブジェクトがポインタだった場合なので、オブジェクトが非ポインタなら
(オブジェクト名.*)(パラメータ);
になります。アスタリスクが変なつき方をしているので、注意しましょう。初めての人がみたら何やってんの?って感じです。実際のコードは

void 
PauseScene::Update(const Input& input) {
  (this->*_updater)(input);
}

こんな感じですね。Update関数はとにかく_updaterを実行して、状態によって_updaterの中身が切り替わっていくというイメージですね。

ラムダ式を使って状態遷移

正直これはおまけみたいなもんです。
前回のメンバ関数ポインタの話をラムダ式にしただけですね。
ラムダ式自体はご存知だと思います。
ただしメンバ変数に利用するにはもうちょっと知識が必要です。それはラムダ式の「型」がなんなのかという知識です。

いつもだったら、ラムダ式を代入する先の変数はauto指定をしていると思うんですが、これって型はどうなってるんでしょうか?

ラムダ式は関数オブジェクト(ファンクタ)に代入されます。さて、ファンクタの代入先の型はどう作ったらいいんでしょうか?

それはfunction型を使います。まず#include<functional>します。そのうえで

std::function< 戻り値(パラメータ) > ラムダ変数;

という感じで宣言します。このラムダ変数に対して、ラムダ式を代入することができますので、内容の切り替えが可能!!というわけです。

ラムダ変数 = [ バインド ]( パラメータ ){ 処理 };

で代入できます。で、今回みたいに状態遷移に利用したい場合には予めメンバのconstラムダ変数に初期化子で代入する事になります。例えば

const std::function<void(const Input& input)> lambda_fadein;
const std::function<void(const Input& input)> lambda_fadeout;

のように宣言しておいて、コンストラクタ初期化子(初期化リスト)で

PauseScene::PauseScene(SceneController& controller):Scene(controller),
lambda_fadein ([this](const Input& input) {
  ()
  --_frame;
}),
lambda_fadeout([this](const Input& input) {
  ()
  ++_frame;
}){
  ()
}

のように、初期化リストでlambda_fadeinやlamda_fadeoutなどの値を入れてしまいます。constがついてるから、ここでやらなければ怒られるんです。
それができたら、

std::function<void(const Input& input)> _lambda;

で宣言して、

_lambda=lambda_fadein;

などやって切り替えます。
ちなみに実行は、当然のように

_lambda(input);

です。簡単ですね。

それでは、switch文を使わない状態遷移に関しては以上です。読んでいただきありがとうございました。

注意点(およびご意見)

さっそくプログラマ諸兄からご指摘がありましたので記載しておきます。

  • 「メンバ関数ポインタは空関数があると、最適化の際に違う場所を指し示すことがあるよ」
    とのこと

さて、空関数は当然のように意味がないため最適化された時に実体が省略されてるっぽい?飛び先としてのシンボルは存在するものの空関数を指し示しているはずの場所は次の関数となっており、予想外の挙動を示したとのこと。

一応、VisualStudio2017のReleaseモードでいくつかのパターンを試してみましたが、今のところ確認できていない。ご指摘いただいた兄貴曰く「コンパイラによるのかもしれない」とのこと、予想外の挙動をされても怖いので安全策としてvoid空関数ではなくboolかintあたりにして0かfalseを返すようにするか、volatile指定子をつけるかという対処になるかな。最適化についての挙動を確認できていないため、どこまで用心深くやるかは環境次第かなと。

  • メンバ関数ポインタだとデバッグ時に状態を確認できないため、enumでやったほうが分かりやすいのではないか?

とのこと。一応、VisualStudio2017の場合ならば現在の状態のメンバ関数はデバッガで確認できます。
しかしながら、これも環境によってはそんな親切な機能がない場合もありますので、開発環境次第という事ですね。
VisualStudioが神ツールであることがわかり、ありがたいご意見でした。世の中VisualStudioで開発できる環境ばかりでもないので、注意しましょう。

33
42
9

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up

@tsuchinokoman's pickup articles

tsuchinokoman

@tsuchinokoman(竜一 川野)

書籍「DirectX12の魔導書」の著者。 現在は専門学校でDxLibとかDirectX12とかC++プログラミングとか数学とか教えつつ、VKetCloudのエンジン開発部分も担当しています。 https://twitter.com/CTsuchinoko
Linked from these articles

Comments

tsuchinokoman
@tsuchinokoman(竜一 川野)
0
tsuchinokoman
@tsuchinokoman(竜一 川野)

@alt わざわざ詳細な編集リクエストありがとうございます。次回から気を付けて書いていきます。ありがとうございました。

シンタックスハイライト by alt 2019/03/25 12:04

1
@alphya(もかむ くるが)

良い、カッコいいかは分かりませんが、有限状態マシンも使えそうですね...!
有限状態マシン - boostjp

1
tsuchinokoman
@tsuchinokoman(竜一 川野)

コメントありがとうございます。boostのやつですね。
まだ使ったことなかったんですよね。今度使ってみたら記事を書いてみようと思います。

1
@avalon_reimu(Avalon Adamantaimai)

この方式には問題があります。
ChangeSceneでSceneの子クラスのポインタを渡すことでシーンの切り替えを実現していますが、
この場合、切り替え元の子Sceneで切り替え先の子Sceneのインスタンスを作成するために、
切り替え先の子Sceneクラスのヘッダーを読み込むなどしなければなりません。
結果的に切り替え元が切り替え先に依存する必要が出てきて、
切り替え先に変更があった場合は切り替え元もコンパイルし直す事となり、ビルド時間が増大します。
これがオプション画面とか特定の用途にしか使わないSceneならいいですが、
例えばEsc押したときタイトル画面に戻るみたいな機能にした時、タイトル画面に変更を加えるなんて事になったら目も当てられません。

やはり素直にenumとswitch文を使うしか無いのでは?と思います。
個人的にはunordered_mapを使うのとか良さそうだと思ったのですがどうでしょう?

0
@NyancoRitter

(遥か昔の記事にコメントを書いて良い場所なのかどうか…)

class SceneController
{
  //シーンを変更する
  void ChangeScene(Scene*);
};

ここを

class SceneController
{
  //各シーンに遷移するメソッド(数個程度ですよね,きっと)
  //(引数の ??? については各シーンへの遷移に必要なものがあるなら)
  void ChangeToTitleScene(???);  //タイトルシーンに変更する
  void ChangeToGameOverScene(???);  //ゲームオーバー〃
  ...
};

みたくすれば良いのではなかろうか? とか思いました.

  • 各シーンは互いの実装を知らなくて済む
  • 各シーンのインスタンスは毎回生成と破棄するのか否か,みたいなことを SceneController の内側で勝手にやれる.

(「んなこと言われても,シーンの種類ってのは数十個とかあるんですよね!」みたいな話だったりするならば違ってきますが)

あと,Scene みたいな基底クラスを前提にすると,
当然ながら「各シーンを実装するのに Scene という基底クラスから派生しなきゃならない」という足枷みたいなのができてしまうのが不便そう…かも? …みたいな.
「全シーンの Update() の型(引数とか戻り値)が共通」というのはそこそこ不便だったりしないのだろうか?
そのせいで,各シーンのインスタンスには別口で先に必要な物を渡しておく(例えば生成時にctorで参照を渡しておかねばならない的な)必要が生じたり…みたいなしわ寄せが割ときつかったりしないのかな?と.

↑の変更に加えて,例えば SceneController の内側を「メンバ関数ポインタによる状態遷移」みたいにしておけば
(すなわち,Scene による多態は無しで,個々のシーン型毎の固有の使い方に関しては SceneController が一手に引き受けて頑張るならば)
とりあえず各シーンの実装が Scene を継承する必要性を消せるのではないだろうか?

1
tsuchinokoman
@tsuchinokoman(竜一 川野)

@NyancoRitter さん、コメントありがとうございます。
まぁ、今回のこれに関しては、テーマが「ポリモーフィズムを使いこなそう」の一環だったりするので、無理やり感はあるかもしれません。
あと、SceneControllerが一手に引き受けるのは、ちょっと元々のコンセプトからははずれていて「そういうのやめよう」って話で、状態遷移図で言うと現在の状態は「次の状態の事だけ知っている」という状態にして管理者の負担を減らそうみたいなコンセプトだったので

0
@NyancoRitter

なんというか,「どのシーンなのかわからんけども,とにかく何らかのシーン」という型として扱いたい場面があるか? っていうのを想像してみると「無いのでは…?」みたいな.
記事内の記述で言えば

階層型メニューとか、ポーズ画面とかのように、遷移後に「前の画面に戻りたい」という事があるかと思います

という話が「シーン」に対してあるか? という.
例えば「タイトルシーンをスタックの奥底にずっと眠らせておく」という実装もあり得るのかもしれないけれども,そうする必要性を感じない(し,かえって不便そう),みたいな.

要は,(ステートパターンではない形で)簡素に?/愚直に? 実装した場合の管理側の負担がどれほどになるのか? 次第なのだろうと思います.
(これ系の話で挙げられている「シーン」の例というのが,「タイトル」とか「ゲームプレイ」とか「ゲームオーバー」みたいな,「大きい粒度の物が少数個」であるために,そこらへんの負担が非常に小さく見えてしまっているのかも)

1
@NyancoRitter

個人的に最近ちょっと「ゲームってどうやって作るの?」みたいなのに興味があって,これ系の話を見たりしているのですが,状態スタックの話にしても,そこに積むのは別に State という型じゃなくても enum の値とか std::function<XXX> とかでも良いわけで,そこらへんを「じゃあコレで」って決めるのが何だか難しいなぁ(というか,決めかねる,というか)みたいな.

2

Let's comment your feelings that are more than good

33
42

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Login to continue?

Login or Sign up with social account

Login or Sign up with your email address