ゲームプログラミング/モード管理
ターン制RPGの場合、マップ移動モードとか戦闘モードとか、ああいうのの切り替えをどう行えばいいのでしょうか?
モードの切り替え方法は、いくつかあります。たとえば、モード番号 mode_number みたいな変数を用意して、
のように、番号で管理する方法があります。
情報工学では、「ステートパターン」という考え方が、これに近いです。ステート state とは「状態」という意味の英語です。情報工学の「デザインパターン論」という学問分野に、「ステートパターン」という用語があります。YouTube 動画 『第4回 ステートパターン初級〜中級編【UnityC# で学ぶデザインパターン】』,2020/12/08 , 2022年10月30日に確認 によれば、ステートパターンはゲーム開発でよく使う考え方とのことです。書籍では、洋書ですが "game programming patterns" に書いてあるらしいです(wiki著者が買ってない)。なお、リンク先動画はアクションゲームを解説していると、リンク先動画で言っています。
ほか、CEDEC2012での講演でも、コンシューマーゲーム開発経験のあるゲーム専門学校講師が、講演スライド中でstateパターンおよびstrategyパターンにも言及しています
本wiki本ページでは、C#やJavaなどの知識は前提とせず、C言語および初歩のC++の知識だけで解説します。
さて、単純なターン制RPGをつくるだけなら、あまりマップ移動フラグ map_flag (trueならマップ表示) や会話中フラグ talk_flag (trueなら会話ウィンドウ表示) みたいな変数を用意しないほうがラクでしょう。
その理由は、
たとえば戦闘モードへの突入のためのモードの切り替え時に、いちいちmap_flagをオフ(0にセット)にして、talk_flagをオフにして、menu_flagをオフにして、・・・などのプログラム作業を、モード番号管理の方式ならば1回の作業で省略できます。
また、もし、管理するフラグの個数が増えると、フラグの切り替えのし忘れなどのバグも発生しやすくなります。
このように、モード番号方式にすると、自然と、各モードへの遷移時に他モードを排他することができ、モードの混在を防げるので、ゲームシステムをモジュール化できます。
よって、モード番号のような方式で管理するのが無難でしょう。
これはつまり、何かのオン/オフのフラグは、なるべく特別なイベント(例: 中ボスを倒した。伝説の武器を手に入れた。)などの例外的な処理の管理のみで活用するのが無難ということです。
なお、1番、2番、3番、・・・などのように1つおきではなく、1000番、2000番、のように間を置くのは、途中で追加するモードを挿入しやすくするためです。
たとえば、マップ移動でも、通常の歩行での移動を1000番として、ダッシュ移動を1010番、船による海上の移動を1020番のように、モードを追加したくなるかもしれません。なので、ある程度の間を、事前に基本モード間に空けとくと便利です。
特に、メニューモードや戦闘モードなど、コマンドをいろいろとプレイヤーが選ぶことになるので、それぞれのコマンドに対応したモードを多く追加することになるでしょう。
メニューモードの内部にコマンド(たとえば「装備」コマンド)に対応した中モードがあって、さらにコマンド(武器を装備? それとも防具を装備?)があって、選んだコマンドに対応した小モードに遷移する・・・みたいな3段階のモードの存在する可能性もあるので、基本のモード番号は3桁以上(つまり100番以上、できれば1000番ほど)ないと、不便でしょう。
このため、最低でも
くらいの3桁以上で管理するべきです。
モード番号は、開発中に番号の数値の仕様を変更したくなる可能性があるので(例えば、オープニング画面のモード番号を追加したくなったとか)、後から番号を変更しやすいように、変数を媒介して定義するとラクです。
まず、単純な方法で、
みたいに定義する方法があります。
こうすれば、もしモード番号の数値を変更するときには、対応する宣言文 int mode_〇〇 = △△の1個の文だけを変更するだけで済みます。
なお、C言語には定数値の宣言の修飾子 const がありますので、
と書くと、よりいっそう、意味が明確になります。
ほかにも、defineマクロを宣言する方法もあります。
マクロ名が大文字になってることに、特に深い理由は無く、単なるC言語業界での慣習です。なお、defineマクロは、コンパイル前に単語単位でテキストを置き換える命令です。このため、
マクロ単語に数字を「1」と後につけている理由は、のちのち、それぞれのモードのサブメニュー画面が追加されていくから、それとの区別のためです。
あらかじめ「1」とか「main」とかをつけておかないと、あとでサブ画面との区別のために、変数名をコードエディタの置換機能で一括変換する際に面倒なことになります。 たとえば
でMODE_MAPだけを名前変更したくて「MODE_MAP_main」にしようとしても、 「MODE_MAP_sub」も一緒に「MODE_MAP_main_sub」になってしまう。
なので、あらかじめ、「1」でも「main」でもいいので、モード名の定義のさい、定義名称のうしろにサブ画面モードとの区別のための追加文字を付けて起きましょう。
なお、defineは別に数を認識しているわけではなく、実際に数値以外の文字などに置き換えにすることも出来るので、想定外のバグが発生しやすいという難点もある。なので、もしかしたらconst int の方式のほうが安全かもしれない。また、const と違って、defineは特に重複した定義を検出しないので、利用には注意のこと。
なぜそこまでしてdefineを使うかと言うと、define命令はコンパイル前に文字を置き換えるので、int や関数などを用いた場合よりもdefineのほうが処理が早くなる場合もあるからである。
しかし、別のセクションでフレームレートの60 FPSの説明をしているように、フレームレート以上の処理速度を目指しても無駄になる可能性が高いので、現代のようなハードウェアが十分に高速化してコンパイラも賢くなった時代にdefineを使う意義は低くなっているのかもしれない。
モード番号を使う代わりに、C言語の列挙型を使うことでも、上記のような排他的な制御は出来ます。
下記は、まだ列挙型を使ってないコードです。
次に、下記に列挙型を使ったコードを示します。
YouTube 動画 『第4回 ステートパターン初級〜中級編【UnityC# で学ぶデザインパターン】』,2020/12/08 , 2022年10月30日に確認 でも、enumを使ってステートの切り替えや条件判定などを解説しています。
enumは、コンパイル時に自動的に宣言順に0,1,2,3,4,・・・の値をenum変数につけています。 なので、いちいち、enumを使わずにfor文などを使ってモード変数に値を0,1,2,3,・・・の順につける必要はありません。forでモードに値をつけるくらいなら、enumで値をつけたほうが合理的です。
また、enumはこのように順番に番号をつけていく方式なので、なるべく一つのenum宣言の中に、すべてのモードの宣言を入れたほうが安全です。
実際、gcc(MinGW)で実験して番号付けをprintfみると下記のように順番の番号になりました。
実験結果
たしかに、0番から順番に番号が振られていることが分かります。
では、enumがひとつのブロックではなく、複数個のブロックで下記のように宣言されている場合、どうなるのでしょうか。 結果は、それぞれのenumブロックごとに0番から開始です。
実験結果
enumが複数個あると、上記のようにそれぞれのブロックごとに0番から開始になって同じパターンで番号がつけられるので、たとえばaとdがともに0のように、別々の名前のenum要素でも同じ値が重複します。
なお、上記コード例で変数Cをコメントアウトしているのは、Cを2つめのenumで宣言すると、1つめのenumでもCが定義されているので重複定義になるからです。このように重複定義を検出する機能はありますが、しかし番号まで保持するわけではないです。
なお、下記コードのような、別々のenumブロックの中で同じ変数名を使うのは、標準C言語の素朴なenumでは不可能です。
このように、enumの要素(「列挙子」という)は、グローバル変数のように扱われますので、同じ名前のものは作れません。たとえ別々のenumブロックで宣言されていようが、同じ名前のものは作れません。
そのほか、「enumの中で別のenumを宣言する」のようなenumの入れ子(ネスト)は不可能です。
enumは関数の引数にすることもできます。
実験結果
実用上、enumは、セーブデータで保管される変数には利用できません。
なぜなら、コード改修の際に、製作者側のコードだけで幾ら enum要素の順序を入れ替えても、ユーザー側・プレイヤー側の手元にあるセーブデータ側の数値は昔のままなので、つまりenumの順序を書き換えることによってコードとセーブデータとの間での不整合が起きてしまうので、セーブデータの互換性が無くなってしまうからです。
この事はあらためて考えてみると、どうやら「ソースコードの互換性 と セーブデータの互換性 は、対立しあう」と言えそうです。
一般にどのソフトウェアでも、正式リリースする前ならセーブデータの互換性が崩れても大丈夫でしょうが、しかしリリース後は少なくともサポート期間中なら、セーブデータの互換性を守らないといけません。
ゲームの場合、セーブシステムの無いジャンルなら何の問題もありませんが、しかし21世紀の今どき、そんなジャンルはほぼ皆無です。
一般的なRPGの場合、現在のモードがマップモードか戦闘モードかなどは普通、セーブしません。なので、これらのモード管理をenumで行っても、とくに問題は無いでしょう。
裏を返すと、セーブデータには「薬草」やら「銅の剣」など所持品の所有状況が保存されるので、そういった所持品IDはenum化できないか、仮に無理やりenum化してもセーブデータの互換性のために順番をプログラマーが覚え続けざるを得ないので、わざわざenum化する利点がなくなってしまいます。
このため現代のプログラミングでも実はセーブデータ互換性の必要性のため、enumを用いずにID番号によるプログラミングしていく手法の必要性は残り続けると言えるでしょう。
セーブデータは何もゲーム産業に限らず、一般的なソウトウェアにもセーブのシステムは存在しますので、上述の事をあれこれ考えてみると、「構造化プログラミング」によってID番号的な管理方式をなるべく追放してきた手法そのものに、限界があると考えざるを得ません。セーブシステムの存在するかぎり、どの業界でもプログラマーは最終的には、どこかでID番号を管理しつづける事になるでしょう。
C+11から、enumクラスと新しい仕様が追加されています。
もしこのenumクラスを使ってモード管理する場合、下記のようなコードになります。
enum名をクラス名として扱うように変わるので、mode::battleのようにクラス名::要素名のようにスコープ解決演算子を使わないとアクセスできなくなるので、名前の衝突の可能性が減り、安全ではあります。
しかし、安全のため、もはや素朴には整数に変換されないし、そもそもenumクラスには整数の代入ができないので、たとえばそのままでは配列のインデックスには使えません。
このため、ゲームの場合なら、たとえば装備画面の「0番スロット」や「1番スロット」など特定のスロットの配列にひもづけたい場合、そのままではenumクラスは配列にできないので、少し手間が増えます。
もっとも、(int)で型キャストで整数型に変えることで使えるようになります。
つまり、enumクラスは整数型ではないのです。
enum class の型キャストの仕方は下記のとおり。
enum クラスの宣言時に型の指定ができるようなりましたが、整数型しか使えません。int型の他にもshort型などの整数型がありますが、今時のゲーム制作なら普通にint型にしておけば十分でしょう。
この型宣言で int 型を宣言しても、そのままでは配列のインデックスに使えません。上記コードのように、配列に使うには (int)と型キャストが必要になります。
なお、 enum struct とも書けます。現状では、enum class と enum struct は機能が同じです。
そのほか、名前空間にクラス名が入りますので、要素名が同じでクラス名の異なる enum クラス宣言が下記のようにあっても、エラーにならずに定義できます。(一方、標準Cの素朴なenumだと、別々のenumブロックでも同名のenum要素(列挙子)がひとつでも存在すればエラーになる。)
なお、列挙型名(「mode1」とかのほう)の重複は、当然ながらエラーになります。列挙子が異なっていても、列挙型が重複していればエラーになります。
上述の
の方式はどれも、C言語およびC++を念頭にした説明です。
pythonやjavascriptなどC系以外の言語には、そもそもdefineマクロが存在しなかったり存在していてもCとは挙動が違っていたり、 あるいはpythonにenumという機能自体はあるものの挙動がC言語と異なっていたり、などの差異があります。
javascript にconstは存在しますが、pythonにはconstは存在しません。変数宣言時の型は、pythonもjavascriptも存在しません。
ただし、上記の一連のノウハウは、あくまでターン制RPGの場合だけでしょう。
もしアクションRPGの場合なら、マップ移動中でも戦闘したり会話したりする可能性もあるので、モード番号方式ではなくフラグ方式にせざるを得ないでしょう。
このことは、ターン制RPGとアクションRPGとでは、コードを共有する事が、なかなか難しい事にも、つながるでしょう。
モード管理のプログラムを書いていると、ゲームのランタイム制作が進んでくると、モード番号を配列の要素番号(角カッコ[ ] の中の変数)として扱いたい場合がある。
たとえば、RPGの装備システムの実装で、武器装備モード、頭防具装備モード、胴防具装備モード、足防具装備モード、・・・とか似たようなモードが並列的にいくつも羅列的に存在している仕様を実装しなければならぬ場合がある。
このように並列的に類似モードが存在する場合に、それぞれ別々にコードを書くのは面倒なので、なんとかして統一したいと感じるだろうが、しかし参照的のデータベースが異なる(参照先が武器だったり頭防具だったりモードごとに異なる)ので、 単純には統一できなくて困ることがある。
ここでもし、対策として例えば
、のような感じのアイデアを考えたとしよう。
これを
のような仕様の感じにすれば、カーソル位置変数とモード変数が同じになるので、のちのちの作業で武器装備アルゴリズムと防具装備アルゴリズムを別々に書く手間が省けるのでは・・・という魂胆である。(また、別の部分のプログラムで、装備品(「鉄の剣」やら「皮の帽子」など)の定義はすでにしてあるとして、その装備品の定義でよく多重配列や構造体の配列が使われていたりする。なので、もし上記のようにモードを整数で定義すれば、配列の要素番号と連動させやすくなるという目論見が、背景としてあるわけだ。)
しかし enum で定義した場合、このような整数と単純にenum変数をイコールしたりのは不可能だし、あるいはenum変数を配列に代入したりするのも不可能である。なぜならenum型はそもそも整数型ではないので、enumのままでは、そのような処理はできない。
なので、対策として、
などの対策がある。
技巧的なのは、対策2の最初からモード管理を int 型で行う方式であるのが、正直言って、かなりの難しさである。
なぜなら、仕様にもよるがRPGの装備システムの実装はなかなか難しく、「構造体の配列」または更にその「多重配列」などの初心者潰しの技巧をいくつも用いることになりやすいので、このため、いきなりモード管理をint型で行って装備システムと有機的かつスタイリッシュに結合しようとすると、プログラミングの序盤から「構造体の多重配列」などの技巧的なテクニックを多用することになるので、初心者だと極めて挫折しやすいだろう。
なので、挫折防止のため開発方針としては、enumが使える開発環境なら、開発序盤はまず対策1のゴリ押しの方法で、とりあえず enum で定義するのがラクである。
enumが使えない場合は仕方なく int 型で定義するわけだが、その場合も決していきなりデータベース変数(たとえば上記の例なら装備品データベースの変数)との連携などスタイリッシュなことは一切考えず、単にモードを整数で表すことだけを考えるようにするのが、開発方針としてはラクだろう。