ゲームプログラミング/モード管理
モードの管理手法について
編集基礎
編集ターン制RPGの場合、マップ移動モードとか戦闘モードとか、ああいうのの切り替えをどう行えばいいのでしょうか?
モードの切り替え方法は、いくつかあります。たとえば、モード番号 mode_number みたいな変数を用意して、
マップ移動モード: 1000番 会話モード: 2000番 メニューモード: 3000番 戦闘モード: 4000番
のように、番号で管理する方法があります。
特に断らず「モード」という言葉を本ページでは使いましたが、果たしてゲーム業界でどう呼んでいるのか知りません。とりあえず、書籍『ゲームプランとデザインの教科書』が、架空のスマホゲーム企画書で「モード」という言葉を使っています。「移動先指定モード」とか[1]。
もしかしたら会社によっては「戦闘パート」切り替えとか「戦闘シーン」とか、そういう別の表現かもしれません。セガのゲーム関係者の人の書いた書籍に「アクションシーン」という用語があるので、少なくともセガ周囲では「シーン」という表現が使われるようです[2]。
ゲーム以外だと「モード」という言葉は、Windowsの「全画面モード」とか、マンガ「企業戦士YAMAZAKI」の「モード変換」とか、アニメ「ビーストウォーズ」の「ビーストモード」とかで「モード」という言葉が1995年より前くらいに生まれた中高年アニメファンやマンガファンに馴染みがあるでしょうか。
本ページでは以降、本文上記のような切り替えを「モード」切り替えなどと呼ぶことにします。
情報工学では、「ステートパターン」という考え方が、これに近いです。ステート state とは「状態」という意味の英語です。情報工学の「デザインパターン論」という学問分野に、「ステートパターン」という用語があります。YouTube 動画 『第4回 ステートパターン初級〜中級編【UnityC# で学ぶデザインパターン】』,2020/12/08 , 2022年10月30日に確認 によれば、ステートパターンはゲーム開発でよく使う考え方とのことです[3]。書籍では、洋書ですが "game programming patterns" に書いてあるらしいです(wiki著者が買ってない)。なお、リンク先動画はアクションゲームを解説していると、リンク先動画で言っています。
ほか、CEDEC2012での講演でも、コンシューマーゲーム開発経験のあるゲーム専門学校講師が、講演スライド中でstateパターンおよびstrategyパターンにも言及しています[4]
本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番ほど)ないと、不便でしょう。
このため、最低でも
マップ移動モード: 100番 会話モード: 200番 メニューモード: 300番 戦闘モード: 400番
くらいの3桁以上で管理するべきです。
変数を媒介としてモード番号の数値の定義
編集モード番号は、開発中に番号の数値の仕様を変更したくなる可能性があるので(例えば、オープニング画面のモード番号を追加したくなったとか)、後から番号を変更しやすいように、変数を媒介して定義するとラクです。
まず、単純な方法で、
// モード番号の定義
int mode_map1 = 100; // マップ画面のモード番号
int mode_talk1 = 200; // 会話画面のモード番号
int mode_menu1 = 300 // メニュー画面のモード番号
int mode_battle1 = 400 // 戦闘画面のモード番号
みたいに定義する方法があります。
こうすれば、もしモード番号の数値を変更するときには、対応する宣言文 int mode_〇〇 = △△
の1個の文だけを変更するだけで済みます。
なお、C言語には定数値の宣言の修飾子 const がありますので、
// モード番号の定義
const int mode_map1 = 100; // マップ画面のモード番号
const int mode_talk1 = 200; // 会話画面のモード番号
const int mode_menu1 = 300 // メニュー画面のモード番号
const int mode_battle1 = 400 // 戦闘画面のモード番号
と書くと、よりいっそう、意味が明確になります。
ほかにも、defineマクロを宣言する方法もあります。
// モード番号の定義
#define MODE_MAP1 100 // マップ画面のモード番号
#define MODE_TALK1 200 // 会話画面のモード番号
#define MODE_MENU1 300 // メニュー画面のモード番号
#define MODE_BATTLE1 400 // 戦闘画面のモード番号
マクロ名が大文字になってることに、特に深い理由は無く、単なるC言語業界での慣習です。なお、defineマクロは、コンパイル前に単語単位でテキストを置き換える命令です。このため、
マクロ単語に数字を「1」と後につけている理由は、のちのち、それぞれのモードのサブメニュー画面が追加されていくから、それとの区別のためです。
あらかじめ「1」とか「main」とかをつけておかないと、あとでサブ画面との区別のために、変数名をコードエディタの置換機能で一括変換する際に面倒なことになります。
たとえば
MODE_MAP MODE_MAP_sub
で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言語の場合
編集モード番号を使う代わりに、C言語の列挙型を使うことでも、上記のような排他的な制御は出来ます。
下記は、まだ列挙型を使ってないコードです。
#include "stdio.h"
#include "string.h"
enum mode { battle, map, menu };
int main(void) {
enum mode modevar = map;
if (modevar == battle) {
printf("いまバトル画面 \n");
}
if (modevar == map) {
printf("いまマップ画面 \n");
}
if (modevar == menu) {
printf("いまメニュー画面 \n");
}
}
- 実行結果
いまマップ画面
次に、下記に列挙型を使ったコードを示します。
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みると下記のように順番の番号になりました。
#include <stdio.h>
enum gameMode {
a,
b,
c,
d,
e
};
enum gameMode nowMode;
int main(void) {
printf("%d\n",a);
printf("%d\n",b);
printf("%d\n",c);
printf("%d\n",d);
printf("%d\n",e);
}
実験結果
0 1 2 3 4
たしかに、0番から順番に番号が振られていることが分かります。
では、enumがひとつのブロックではなく、複数個のブロックで下記のように宣言されている場合、どうなるのでしょうか。 結果は、それぞれのenumブロックごとに0番から開始です。
#include <stdio.h>
enum gameMode {
a,
b,
c,
};
enum gameMode2 {
// c,
d,
e
};
enum gameMode nowMode;
enum gameMode2 nowMode2;
int main(void) {
printf("%d\n",a);
printf("%d\n",b);
printf("%d\n",c);
printf("%d\n",d);
printf("%d\n end\n",e); // 前実験との区別のためend追加
}
実験結果
0 1 2 0 1 end
enumが複数個あると、上記のようにそれぞれのブロックごとに0番から開始になって同じパターンで番号がつけられるので、たとえばaとdがともに0のように、別々の名前のenum要素でも同じ値が重複します。
なお、上記コード例で変数Cをコメントアウトしているのは、Cを2つめのenumで宣言すると、1つめのenumでもCが定義されているので重複定義になるからです。このように重複定義を検出する機能はありますが、しかし番号まで保持するわけではないです。
なお、下記コードのような、別々のenumブロックの中で同じ変数名を使うのは、標準C言語の素朴なenumでは不可能です。
#include <stdio.h>
// エラーになります
enum gameMode {
a,
b,
c,
};
enum gameMode2 {
c, // c が重複しておりエラー
d,
};
enum gameMode nowMode;
enum gameMode2 nowMode2;
int main(void) {
printf("%d\n",a);
}
このように、enumの要素(「列挙子」という)は、グローバル変数のように扱われますので、同じ名前のものは作れません。たとえ別々のenumブロックで宣言されていようが、同じ名前のものは作れません。
そのほか、「enumの中で別のenumを宣言する」のようなenumの入れ子(ネスト)は不可能です。
enumは関数の引数にすることもできます。
#include <stdio.h>
enum gameMode {
battle,
map,
menu,
item,
};
void func1 (enum gameMode mode){
printf("%d\n",mode);
}
int main(void) {
printf("enum 実験\n");
printf("%d\n",battle);
printf("%d\n",map);
printf("%d\n",menu);
printf("%d\n",item);
printf("\n",item);
printf("enum関数 実験\n");
func1(battle);
func1(map);
func1(menu);
func1(item);
}
実験結果
enum 実験 0 1 2 3 enum関数 実験 0 1 2 3
情報科学では、「状態遷移」(じょうたいせんい)という概念が、昭和の古い時代からあります。現代ではこれを知らなくても別にゲームプログラミングは可能なのですが、万が一これを知らずに同じアイデアを研究してしまうのは時間の無駄であり、わざわざ再発明をする必要もないので、一応は理論の概要を紹介をしておきます。
『オートマトン』論みたいなタイトルの情報科学書を読むと、だいたい、下記のような感じの図があります。図中のそれぞれのSが、それぞれの状態を図示しています。
下記の図の例が分かりやすいでしょうか。
S0からS4には、直接は行けません。S1やS2を経由しないと、S4には行けません。それぞれのSは、プログラムのそれぞれの状態を表しています。
たとえばゲームでRPGなら普通、マップ画面の状態から、いきなり戦闘後の経験値入手画面の状態には行けません。マップ画面から、モンスターに遭遇の状態を経由して、モンスターを倒すなどして(あるいはモンスターが逃げるなど)、モンスターがいなくなったら、ようやく経験値の入手の画面の状態になる、・・・といったように、大方のRPGでもそうです。
上記の図のように、そのソフトの状態遷移の経路のありかたを図示したものを、状態遷移図(じょうたいせんいず)と言います。
現代では、なんとなくフローチャートでも代用できそうですが、より抽象的かつ古典的な概念として「状態遷移図」という理論がありました。
書籍を探すなら(ゲーム制作では不要ですが)、『オートマトン』論のほか、『チューリングマシン』(または『チューリング機械』)などの題名の情報科学者にも、似たような意味の似たような図がある場合が多いです。
というか、オートマトン論の教科書の中に章のひとつとしてチューリングマシンの話題もあったり、あるいはチューリングマシンが題名の教科書の中に章のひとつとしてオートマトン論の紹介があったりします。
ただ、こういったオートマトンやチューリングマシンの教科書を読んでも、デザインパターンの話題は無いのが普通です。当然、stateパターンなんて用語も理論も、オートマトン・チューリングマシンの教科書のどこにも無いのが普通です。
どちらかというと『計算量の理論』とか、そういうのをチューリングマシンの教科書は説明したがっていたりします。
ゲーム制作としては、べつにオートマトン論・チューリング機械にある数式はべつに覚えなくていいです(そんな数式を覚えたところでゲームは作れません)。
しかし用語「状態遷移」と、図示のアイデアだけ、借用すれば十分でしょう。そんな用語を知らない1980年代の若者だって、BASICとかの初心者むけ言語でアマチュア用ですがゲームを作れました。
- 古典よりも最近の入門書のほうが大事
学問というのは、長期的にはたしかに古典に敬意を払うのも必要なのですが、しかし実用性を言えば、理系の学問の場合、学習者は、とりあえずは新しめの理論から先に学ぶほうが実用的です。
そもそも工学とか社会科学における新しい理論というのは往々にして、古典的な理論がもはや現代の社会や産業の進歩といった現実に合わなくなったので、だから現実をもとに新しい理論を作る必要にさまられて、それで新しい理論ができたりもします。
そういう事情があるので、中学高校の情報科学の教育も、けっして歴史的な順序通りには、教えていません。もし歴史的な順序どおりで古典から教えてしまうと、学生からすれば、古典知識を現代流にアレンジするために二度手間になって非効率だからです。
日本の中学・高校の情報科学の教育カリキュラムでも、基本的に優先して教えている理論は、じつは、新しめの理論のうち、子供にも分かりやすくて、しかも使用頻度の高い理論・技術から先に、中学校・高校などで教えるようになっています。
中学高校の情報科学のカリキュラムを考えている文科省や大学の偉い先生たちが、わざわざ、今の現実に追いついている新しめの理論を、子供にも分かりやすく、かみくだいて、教科書を作ってくれたりしているのです。
だから、わざわざオートマトン理論などを経由せずに、直接的に、現代的な理論である stateパターン あるいはenumによる状態遷移の概念を学んでしまいましょう。
たとえるなら、伝言ゲームでは途中の仲介者が増えるほど伝言ミスが起きやすくなるように、途中の理論(古典や抽象論など)が増えるほど、勘違いなどのミスが発生しやすくなります。
数学者チューリングさんの研究していた第二次世界大戦後の時代はまだ真空管コンピュータの時代だったので、当時はC言語なんて無かったので(というか半導体コンピュータそのものが無い)、彼はstateパターンを知らなくても仕方ありません。
ですが21世紀に生きる現代の我々は、C言語というプログラミング言語や、stateパターンという理論を知ってるので、伝言ゲームごっこなんて非効率な勉強法はせず、さっさと直接的に enum やstateパターンなどの概念を学びましょう。
古典や抽象論は、決して知識の土台や骨組みにするべきではなく、そうではなくて補強材にするべきです。知識の土台や骨組みは、なるべく現代的なノウハウにするべきです。少なくとも、現実の問題に対応しなければいけない、プログラマー的な職業では、そうあるべきでしょう。
中学校の社会科の授業を思い出してください。歴史よりも先に地理を習うのが中学社会では普通です(なお、規制緩和により、地理と歴史を並行して習っても良くなっている)。
ゲームだけでなく、マンガ・アニメ・イラストなどでも同様、練習の順序は、古典からではなく、抽象論からでもなく、さっさと最近の新しめの作品の模写や、新しめの具体的なノウハウ集のような実用的な理論を、学ぶほうが効率的です。
アニメーター向けのイラスト教本を見ても、けっして、古典美術の話題なんてしていませんし、抽象芸術の解説もしていません。そんな古典美術などの話題をしても、アニメーター志望者にとっては伝言ゲーム的になって非効率だからです。
なお、書籍『ゲームをテストする バグのないゲームを支える知識と手法』および書籍『ゼロからはじめるゲームテスト: 壁抜けしたら無限ガチャで最強モードな件? 』に状態遷移図および状態遷移表のことが書いてあります[5][6]。
ゲームテスターの業界では使う知識なのかもしれません。
アニメ評論家の岡田斗司夫の90年代後半の対談集『マジメな話』で、誰かとの対談で言われてるんですが、専門家以外の人や新人にとっての研究では、あまり古典を研究しすぎないほうが良いことを進めています。
たとえば、文学研究・シナリオ研究なら、「源氏物語よりも手塚治虫の絶版漫画を集めてそれを研究したほうが、研究成果も出やすい」みたいなことを岡田らは言っています。
岡田は指摘してないのですが、学問の世界の常識として、古典はすでに多くの研究者によって研究済みなので、これから新規の研究成果を出すのは、至難の業(ワザ)だと、よく言われています。なので、なるべく新しめの時代やジャンルを研究すると、研究成果も出やすいです。(なお、当時やその少し前の岡田は、東大講師です。)
たとえば日本でインターネットが出てきたときに、社会研究をするなら、インターネット社会の研究をすると論文やレポートなどを書きやすい的なアレです。
受験勉強などでは古典や古典的ジャンルのほうが研究や教材が普及しているなどの理由からか出題されやすいし、逆に時事は出題されづらいですが、しかし人生も創作も研究も、決して古典ではないし受験ではありません。
あと、研究のむずかしさと、研究の一般社会への価値は、比例しません。ビジネス常識であり学問研究(受験勉強ではなく研究の常識)でも常識なのですが、難しいだけのことに社会では価値ありません。創作でたとえるなら、絵を描くさい、利き腕でないほうで描くのは難しいですが、しかしそれで普段よりもヘタな絵を描いても、一般社会では価値ありませんし、アニメ業界でもゲーム業界でもそんな絵をどこの会社も求めていないのが現実です。
理系や数学でもそうで、情報工学とかにおける情報数学・離散数学(りさんすうがく)などもそうで昭和後半の昔は差別されており、古典的なジャンルの数学者の中には新興ジャンルの情報数学・離散数学などに文句で「単なるパズルじゃん」みたいに不満を言う人もいましたが、しかし結果は21世紀のように情報工学の隆盛を通じて情報数学・離散数学も隆盛です(数学者の秋山仁(専攻は離散数学)の伝記(90年代ごろの本)などを読むと、過去のそういう差別的な時代背景も書かれている)。一般人むけのパズルみたいに簡単な努力でも研究成果が出るなら、べつにそれでも構わないのが現実です。
投資家が求めている人とは、利益・成果およびそれを生み出してくれる人であり、決して、単に難しいことのできるだけの変わり者ではありません。難しい課題にチャレンジして褒められるのは小中学生のような義務教育を受ける子供であり、教師や文科省などから推奨された課題にチャレンジした場合の話です。義務教育の課題では、難しい課題はそのぶん成果も得やすい課題が選ばれていますが、しかし現実社会では必ずしも難しさと成果は比例しないので、決して難しさと成果・価値とを混同してはいけません。もし混同してしまうと、人生の進路などを大幅に見誤ってしまい、貴重な時間を大幅に無駄にしてしまいます(年単位で無駄にしてしまいかねません)。
なお、手塚治虫のマンガ研究の例はあくまで90年代における例ですので、今さら2020年を過ぎた現代に手塚を研究しても、もはや研究成果を出すのは至難のワザかもしれません。
さて、ジブリの宮崎監督は、少年時代は手塚治虫などに憧れて漫画家を目指していたとも言われていますが、しかしマンガよりも新しいアニメ業界に可能性を感じて結果的にはアニメ業界に就職しました。なお、ナウシカの映画を作る際に原作漫画が必要だということで、マンガ版ナウシカの作者として、宮崎監督は漫画家にもなれました。
ほか、ガンダムの富野監督も、彼やその同期が映像業界に就職した時代では実写映画が花形の時代で[7]、アニメはまだ評価の低かった時代でした。アニメどころかテレビCMですら評価の低い時代であり、さらにアニメは評価が低くて最低水準の評価に近い状態だったほどです。
ともかく、受験勉強の弊害なのか何か知らないですが、世間の人は、古い業界でそのため難しい業界を志望したがりますが、しかし、それはこれからの時代の価値ではありません。
さて、富野監督が CEDEC 2009 の公演でも言っていますが、
- 「いわゆる博識/百科全書派的な抱え込み方では,クリエイターにはなれない。それでは次のステップを踏めないのだ。知恵を持ちすぎると,その知恵の維持やメンテナンスに縛られ,「ネクスト」を生めないのである。」[8] です。
教科書的な知恵を管理する仕事は、大学や評論家などの別の職業の人に任せましょう。なぜなら、経済のしくみは分業です。なんでもかんでも一人で研究しようとするのは、ヤメましょう。
なんでもかんでも一人で研究するのは難しいですが、しかし難しいだけのことに社会では何の価値はありません。「経済は分業」という言葉を使う人はさらっと、難かしさと価値の区別もしています。
決して「古典の研究者が低能」だとかそういう話ではなく、それほどまでに「古典の研究は難しいので、深入りしないほうが安全」という話です。
専門の古典研究者は、そういう危険なことを一般人に代わって研究してくれる、役立つ人材である、という話です。
だから、分業しましょう。
enumとセーブデータ互換性との対立
編集実用上、enumは、セーブデータで保管される変数には利用できません。
なぜなら、コード改修の際に、製作者側のコードだけで幾ら enum要素の順序を入れ替えても、ユーザー側・プレイヤー側の手元にあるセーブデータ側の数値は昔のままなので、つまりenumの順序を書き換えることによってコードとセーブデータとの間での不整合が起きてしまうので、セーブデータの互換性が無くなってしまうからです。
この事はあらためて考えてみると、どうやら「ソースコードの互換性 と セーブデータの互換性 は、対立しあう」と言えそうです。
一般にどのソフトウェアでも、正式リリースする前ならセーブデータの互換性が崩れても大丈夫でしょうが、しかしリリース後は少なくともサポート期間中なら、セーブデータの互換性を守らないといけません。
ゲームの場合、セーブシステムの無いジャンルなら何の問題もありませんが、しかし21世紀の今どき、そんなジャンルはほぼ皆無です。
一般的なRPGの場合、現在のモードがマップモードか戦闘モードかなどは普通、セーブしません。なので、これらのモード管理をenumで行っても、とくに問題は無いでしょう。
裏を返すと、セーブデータには「薬草」やら「銅の剣」など所持品の所有状況が保存されるので、そういった所持品IDはenum化できないか、仮に無理やりenum化してもセーブデータの互換性のために順番をプログラマーが覚え続けざるを得ないので、わざわざenum化する利点がなくなってしまいます。
このため現代のプログラミングでも実はセーブデータ互換性の必要性のため、enumを用いずにID番号によるプログラミングしていく手法の必要性は残り続けると言えるでしょう。
セーブデータは何もゲーム産業に限らず、一般的なソウトウェアにもセーブのシステムは存在しますので、上述の事をあれこれ考えてみると、「構造化プログラミング」によってID番号的な管理方式をなるべく追放してきた手法そのものに、限界があると考えざるを得ません。セーブシステムの存在するかぎり、どの業界でもプログラマーは最終的には、どこかでID番号を管理しつづける事になるでしょう。
C+11のenumクラス
編集C+11から、enumクラスと新しい仕様が追加されています。
もしこのenumクラスを使ってモード管理する場合、下記のようなコードになります。
#include "stdio.h"
#include "string.h"
enum class mode { battle, map, menu };
int main(void) {
enum mode modevar = mode::battle;
if (modevar == mode::battle) {
printf("いまバトル画面だよ \n"); // 区別のため「だよ」追加
}
if (modevar == mode::map) {
printf("いまマップ画面だよ \n");
}
if (modevar == mode::menu) {
printf("いまメニュー画面だよ\n");
}
}
- 実行結果
いまバトル画面だよ
enum名をクラス名として扱うように変わるので、mode::battle
のようにクラス名::要素名
のようにスコープ解決演算子を使わないとアクセスできなくなるので、名前の衝突の可能性が減り、安全ではあります。
しかし、安全のため、もはや素朴には整数に変換されないし、そもそもenumクラスには整数の代入ができないので、たとえばそのままでは配列のインデックスには使えません。
このため、ゲームの場合なら、たとえば装備画面の「0番スロット」や「1番スロット」など特定のスロットの配列にひもづけたい場合、そのままではenumクラスは配列にできないので、少し手間が増えます。
もっとも、(int)
で型キャストで整数型に変えることで使えるようになります。
つまり、enumクラスは整数型ではないのです。
enum class の型キャストの仕方は下記のとおり。
#include "stdio.h"
#include "string.h"
enum class mode : int {
battle,
map,
menu
};
int main(void) {
enum mode modevar = mode::menu;
if (modevar == mode::battle) {
printf("いまバトル画面です \n");
}
if (modevar == mode::map) {
printf("いまマップ画面です \n");
}
if (modevar == mode::menu) {
printf("いまメニュー画面です\n"); // 区別のため「です」追加
}
printf("%d\n",mode::menu); // printf だけ例外的にキャスト不要
int a[3]={555, 23, 1192};
printf("%d\n",a[(int)mode::menu]);
}
- 実行結果
いまメニュー画面です 2 1192
enum クラスの宣言時に型の指定ができるようなりましたが、整数型しか使えません。int型の他にもshort型などの整数型がありますが、今時のゲーム制作なら普通にint型にしておけば十分でしょう。
この型宣言で int 型を宣言しても、そのままでは配列のインデックスに使えません。上記コードのように、配列に使うには (int)
と型キャストが必要になります。
なお、 enum struct とも書けます。現状では、enum class と enum struct は機能が同じです。
#include "stdio.h"
#include "string.h"
enum struct mode {
battle,
map,
menu
};
int main(void) {
enum mode modevar = mode::menu;
if (modevar == mode::battle) {
printf("いまバトル画面です \n");
}
if (modevar == mode::map) {
printf("いまマップ画面です \n");
}
if (modevar == mode::menu) {
printf("いまメニュー画面です\n"); // 区別のため「です」追加
}
}
- 実行結果
いまメニュー画面です
そのほか、名前空間にクラス名が入りますので、要素名が同じでクラス名の異なる enum クラス宣言が下記のようにあっても、エラーにならずに定義できます。(一方、標準Cの素朴なenumだと、別々のenumブロックでも同名のenum要素(列挙子)がひとつでも存在すればエラーになる。)
#include "stdio.h"
#include "string.h"
enum class mode1 { battle, map, menu };
enum class mode2 { battle, map, menu };
int main(void) {
enum mode1 modevar = mode1::battle;
if (modevar == mode1::battle) {
printf("いまバトル画面だよ \n"); // 区別のため「だよ」追加
}
if (modevar == mode1::map) {
printf("いまマップ画面だよ \n");
}
if (modevar == mode1::menu) {
printf("いまメニュー画面だよ\n");
}
}
- 実行結果
いまバトル画面だよ
なお、列挙型名(「mode1」とかのほう)の重複は、当然ながらエラーになります。列挙子が異なっていても、列挙型が重複していればエラーになります。
その他
編集上述の
- enumの方式、
- define マクロの方式
- const int の方式
の方式はどれも、C言語およびC++を念頭にした説明です。
pythonやjavascriptなどC系以外の言語には、そもそもdefineマクロが存在しなかったり存在していてもCとは挙動が違っていたり、 あるいはpythonにenumという機能自体はあるものの挙動がC言語と異なっていたり、などの差異があります。
javascript にconstは存在しますが、pythonにはconstは存在しません。変数宣言時の型は、pythonもjavascriptも存在しません。
モード番号方式の限界
編集ただし、上記の一連のノウハウは、あくまでターン制RPGの場合だけでしょう。
もしアクションRPGの場合なら、マップ移動中でも戦闘したり会話したりする可能性もあるので、モード番号方式ではなくフラグ方式にせざるを得ないでしょう。
このことは、ターン制RPGとアクションRPGとでは、コードを共有する事が、なかなか難しい事にも、つながるでしょう。
モード管理の発展例: 並列的なサブモードの羅列の場合
編集モード管理のプログラムを書いていると、ゲームのランタイム制作が進んでくると、モード番号を配列の要素番号(角カッコ[ ]
の中の変数)として扱いたい場合がある。
たとえば、RPGの装備システムの実装で、武器装備モード、頭防具装備モード、胴防具装備モード、足防具装備モード、・・・とか似たようなモードが並列的にいくつも羅列的に存在している仕様を実装しなければならぬ場合がある。
このように並列的に類似モードが存在する場合に、それぞれ別々にコードを書くのは面倒なので、なんとかして統一したいと感じるだろうが、しかし参照的のデータベースが異なる(参照先が武器だったり頭防具だったりモードごとに異なる)ので、 単純には統一できなくて困ることがある。
ここでもし、対策として例えば
- カーソル位置変数=1で決定ボタンを押せば武器装備モードに移行、カーソル位置変数=2で決定ボタンなら頭防具装備モードに移行・・・
、のような感じのアイデアを考えたとしよう。
これを
- 装備種類変数 1番 = 武器モード
- 装備種類変数 2番 = 頭防具モード
のような仕様の感じにすれば、カーソル位置変数とモード変数が同じになるので、のちのちの作業で武器装備アルゴリズムと防具装備アルゴリズムを別々に書く手間が省けるのでは・・・という魂胆である。(また、別の部分のプログラムで、装備品(「鉄の剣」やら「皮の帽子」など)の定義はすでにしてあるとして、その装備品の定義でよく多重配列や構造体の配列が使われていたりする。なので、もし上記のようにモードを整数で定義すれば、配列の要素番号と連動させやすくなるという目論見が、背景としてあるわけだ。)
しかし enum で定義した場合、このような整数と単純にenum変数をイコールしたりのは不可能だし、あるいはenum変数を配列に代入したりするのも不可能である。なぜならenum型はそもそも整数型ではないので、enumのままでは、そのような処理はできない。
なので、対策として、
- 対策1: if文を装備関係の各モードごとに書いてenum変数を読み取って別途に用意したint型変数(装備関係モード管理用)に適した変数を代入するというゴリ押しの方式、
- 対策2: あるいは、最初から装備品のモード管理をint型でスタイリッシュに行う方式、
などの対策がある。
技巧的なのは、対策2の最初からモード管理を int 型で行う方式であるのが、正直言って、かなりの難しさである。
なぜなら、仕様にもよるがRPGの装備システムの実装はなかなか難しく、「構造体の配列」または更にその「多重配列」などの初心者潰しの技巧をいくつも用いることになりやすいので、このため、いきなりモード管理をint型で行って装備システムと有機的かつスタイリッシュに結合しようとすると、プログラミングの序盤から「構造体の多重配列」などの技巧的なテクニックを多用することになるので、初心者だと極めて挫折しやすいだろう。
なので、挫折防止のため開発方針としては、enumが使える開発環境なら、開発序盤はまず対策1のゴリ押しの方法で、とりあえず enum で定義するのがラクである。
enumが使えない場合は仕方なく int 型で定義するわけだが、その場合も決していきなりデータベース変数(たとえば上記の例なら装備品データベースの変数)との連携などスタイリッシュなことは一切考えず、単にモードを整数で表すことだけを考えるようにするのが、開発方針としてはラクだろう。
- ^ 川上大典 ほか著『ゲームプランとデザインの教科書』、秀和システム、2018年11月1日 第1版 第1刷、P.255
- ^ 『nyny著『ローポリスーパーテクニック』、ソフトバンククリエイティブ、2010年2月16日 初版 第5刷、P28
- ^ 『第4回 ステートパターン初級〜中級編【UnityC# で学ぶデザインパターン】』,2020/12/08 , 2022年10月30日に確認
- ^ 『オブジェクト指向できていますか?』 Aug. 27, 2012 、大圖衛玄、スライド 35 of 129,
- ^ 花房 輝鑑 著『ゲームをテストする バグのないゲームを支える知識と手法 』、翔泳社、
- ^ 『ゼロからはじめるゲームテスト』制作委員会 著、『ゼロからはじめるゲームテスト: 壁抜けしたら無限ガチャで最強モードな件? 』、オーム社、
- ^ CEDEC 2009「ガンダムの父」富野 由悠季氏が若いクリエイター達を挑発する基調講演,「慣れたら死ぬぞ」の中身とは?
- ^ CEDEC 2009「ガンダムの父」富野 由悠季氏が若いクリエイター達を挑発する基調講演,「慣れたら死ぬぞ」の中身とは?