ゲームプログラミング/RPG/異なる関数どうしでのパラメータ共有
おおまかな方針
編集ゲームは、関数などのいくつかのモジュールに分かれているので、どうやってそれら別々のモジュールで変数を共有するか、疑問に思うだろう。
別々の関数でパラメータを共有する方法はいくつかあるが、一番簡単な方法は、単にグローバル変数を使うことである。
- ※ 日本では、なぜか勘違い・生半可な理解で「グローバル変数はなるべく一切(いっさい)使わないで、保守性のために外部ファイルからアクセスできないようにするため、C++ なら例えば class などを使って、ソフトウェア工学のカプセル化の手法を使うほうがイイ」という迷信があります。しかしこれはマチガイ・勘違いであり、証拠として文献『低レベルプログラミング』(Igor Zhirkov 著、古川邦夫 監訳)には、堂々と、カプセル化された部品(文献には「パーツ」とある)的なプログラムを呼び出す方法は「グローバル変数を使う」か「隠されていない一群の関数を使う」と書いてあります[1]。考えてみれば当然で、そもそも外部ファイルから何も変数や関数にアクセスできなければ、そもそもプログラムを起動できません。
関数をこえての変数の共有には、グローバル変数を使う方法以外にも、ポインタを使う方法がある。
このセクションでは、上記の検証のほか、配列や構造体なども大まかに説明する。 なお、簡単な仕組みのゲームなら、配列や構造体を使わなくてもゲームは製作可能である。
なので、配列や構造体はゲーム制作には必ずしも必須の知識ではないが、しかし学習コストはそれほど高くないし、市販のどの『C言語』入門書にも詳しく書かれているので、現代なら学んでおいても特に損は無いだろう。
異なる関数どうしでのパラメータ共有
編集RPGでは、戦闘モードとかマップ上での移動モードとか買い物モードとか、作中でいろんなモードがあるが、それぞれのモードでHP(ヒットポイント)やMPなどのパラメータを共有する必要にせまられ、さらに、各モードから、パラメータを書き換え、それを他のモードにも反例させる必要が生じる。
それぞれのモードは、関数であらわす場合が多いだろう(関数を使わずにメインループに直に書いてもゲーム制作はできる。本ページではプログラミング知識の教育の都合上、今回は関数を用いたとして説明をする)。関数を使うとなると、戦闘モードやらマップモードなどといった別々の各モードは、つまり、別々の関数となる。なので、別々の関数どうしで、パラメータを共有する方法が分かれば良い。
このような機能を実装には、C言語によるプログラミングでは、「グローバル変数」を使うか、あるいは「ポインタ」を使うことになる。
- (近年ではUnityというゲームエンジンが普及しているが、Unityの日本ユーザーの用語では、モードではなく「シーン」と呼んでいる。「戦闘シーン」とか「マップ移動シーン」などのような言い回しをしている。
- だが本書では、シーンと言うのは劇中での物語の状況を表すのにも使われることや、商標権などの心配から、よりコンピュータ的な「モード」と言う言い回しを使うことにする。)
これらの機能を使わない場合での、関数の返り値を使って共有を目指す方法だと、1個ずつしか値を返せないので、プログラムが困難である。
おそらくだがシミュレーションゲームでも、同様に、複数のパラメータの受け渡しと、各モード間でのパラメータ共有が必要になるので、グローバル変数またはポインタを使うことになるだろう。
グローバル変数を使う場合
編集単純な例
編集説明の都合のため、コンソールアプリで説明しますが、GUIアプリでも、C++なら、だいたい似たような発想で、実装が可能です。(なお、C#ではグローバル変数が無いので不可能。C#の場合については別セクションで述べる。)
まずは下記のような、グローバル変数を使う方式が、パラメータの共有の基本です。
//例 グローバル変数によるRPGの各モード間でのパラメータの共有と書き換え
// Windows の Visual C++ の場合
#include <stdio.h>
#include <stdlib.h> // 「続行するには何かキーを押してください . . .」を表示するのに必要。
static int hp, mp, exp_;
static int kizugusuri_kosuu;
int battle()
{
printf("モンスターと戦闘中・・・ \n");
hp = hp - 5;
printf("負傷したぞ! 5のダメージ! HPが5減ったぞ! \n");
printf("HP = %d \n", hp);
printf("魔法で攻撃するぞ!MPが3減るぞ! \n");
mp = mp - 3;
printf("MP = %d \n", mp);
printf("モンスターを倒したぞ! 経験値を2ポイント獲得したぞ! \n");
exp_ = exp_ + 2;
return 0;
}
int heal()
{
printf("傷薬で回復だ! \n");
hp = hp + 3;
printf("HP = %d \n", hp);
printf("傷薬が1個減ったぞ! \n");
kizugusuri_kosuu = kizugusuri_kosuu - 1;
printf("傷薬は残り %d個 \n", kizugusuri_kosuu);
return 0;
}
int main(void)
{
hp = 30; mp = 25; exp_ = 0;
kizugusuri_kosuu = 10;
printf("冒険を始めたぞ! 最初のHPは%d、MPは%d。経験値は%d \n", hp, mp, exp_);
printf("モンスターに遭遇したぞ! 戦闘だ! \n");
battle();
printf("戦闘終了後のHPは%d、MPは%d。経験値は%d \n", hp, mp, exp_);
printf("回復アイテムを使用するぞ! \n");
heal();
printf("回復後のHPは%d、MPは%d。経験値は%d \n", hp, mp, exp_);
printf("回復後の傷薬の残り個数は%d個 \n", kizugusuri_kosuu);
system("pause");// 「続行するには何かキーを押してください . . .」の待機命令
}
- 実行結果
- (Visual Studio 2017 の C++ で動作することを確認ずみ。)
冒険を始めたぞ! 最初のHPは30、MPは25。経験値は0 モンスターに遭遇したぞ! 戦闘だ! モンスターと戦闘中・・・ 負傷したぞ! 5のダメージ! HPが5減ったぞ! HP = 25 魔法で攻撃するぞ!MPが3減るぞ! MP = 22 モンスターを倒したぞ! 経験値を2ポイント獲得したぞ! 戦闘終了後のHPは25、MPは22。経験値は2 回復アイテムを使用するぞ! 傷薬で回復だ! HP = 28 傷薬が1個減ったぞ! 傷薬は残り 9個 回復後のHPは28、MPは22。経験値は2 回復後の傷薬の残り個数は9個 続行するには何かキーを押してください . . .
どの関数の中でもない場所で、変数を定義すると、その変数はすべての関数で共有される。このように、すべての変数で共有された変数をグローバル変数という。
static をつけておかないと、battle()やheal()関数を終了して抜けた直後に初期化されてしまうので、static で宣言する。
なお、冒頭の関数外部でstatic宣言のある static int hp, mp, exp_;
をする代わりに、main関数の中でstaticを用いた同じ変数宣言をしても、(Visual Studio 2017 では)コンパイルエラーになる。
おそらく、C言語の仕様として、コンパイルではコードを前から順にコンパイルしていくので、battle関数などが、battle関数よりも後ろで定義されている変数hpや変数mpなどの存在を認識できないからである。
かといって、battle関数をmainの後ろにもっていくと、今度は、main関数がbattle関数などを認識できなくなり、これもまたコンパイルエラーになる。
グローバル変数のない言語(C#など)の場合
編集基本
編集近年では、さまざまな理由により、初心者が Visual C++ を勉強するのは困難になっています(フォームデザイナーの廃止など)。マイクロソフト社は、代わりに C# を普及させたいようだ。
では、ゲーム特にRPGをつくりたい私たちは、C#でどのようなコードで、上述のようなモード間のパラメータ共有の機能を実現できるのだろうか?
結論を言うと、市販の教本やらマイクロソフトの公式サイトにあるような static宣言を使っただけのプログラムでは、残念ながらメインループを1周すると初期状態に戻ってしまうので、役立ちません。つまり、教本やらマイクロソフト公式サイトが説明不十分です。
1ループ内ではstatic宣言の初期化は1回しか行われないのですが、しかしループを周回するたびに毎回static宣言の初期化が行われてしまい、役立ちません。(なので、GUIアプリ開発では、初期値の代入をstatic宣言では行ってはいけないのです。)
答えを言うと、「コンストラクタ」というものを使う必要があります。初期値の具体的な代入などの設定は、コンストラクタで宣言します。コンストラクラ外部でのstatic宣言は、単に変数名とその型を宣言してメモリ確保するだけにします。逆に言うと、コンストラクタに触れず、「ゲッター」だの「セッター」だのをどうこう言ってる人は、GUIアプリ開発においてはピント外れです。
C#にはグローバル変数はないですが、それでもいちおう、windowsのVisual C#でも、同一プロジェクト内部にある他csファイルの変数へのアクセスは可能であり、Visual C# の場合、具体的には、下記の
- pubic static 修飾子
- コンストラクタ
- ToString() などのメソッド
- デザインファイルの書き換え
を行えば、とりあえず、グローバル変数の代わりに、他のファイルにある変数にアクセスして書き換えや読み込みをできるようになる。
予備知識がけっこう多いので、果たしてVisual C# でゲーム制作すべきかどうか、考え直したほうが良い。「どうしても Visual C# で作りたい」という人のために、一応、具体策を説明しておく。
pubic static 修飾子
編集まず宣言側で、たとえばOpeningファイルで宣言するなら、
どこのメソッド内でもない場所で、
pubic static 修飾子で宣言しないとけない。つまり
public static int herHP;
のように宣言しないといけない。
そうすれば、別ファイルからでも、アクセスだけなら、
ファイル名.変数名
で可能である。(しかし、これではアクセスだけしか出来ない。書き換えを出来るようにするのが、とても難しい。後述のように、コンストラクタの作成や、デザインファイルの書き換えなど、色々と必要になる。)(そして、ネットの解説サイトの多くは書き換え方法を書いてないので、役立たない。なお、コンソールアプリでの書き換え方法としてよくみる、static 宣言すればどうにかなるとう方法では、GUIアプリでのグローバル変数的な運用の出来ない。)
なお、そのcsファイル名と同一名称のクラスがファイル内のコードにあるので、結果的に
クラス名.変数名
というアクセス方法でもある。
たとえば、ファイル名が「Opening」で、そこでゲーム中の諸パラメータを宣言していたとして、たとえば主人公のヒットポイント(変数名「heroHP」とする)を定義していたとしたら、
Opening.heroHP
でアクセスできる。
コンストラクタ
編集さて、public static int herHP;
では(どこのメソッド内でもない場所)、初期値を入れてはいけない。もしここで初期値を入れると、このファイルを読み込む際に毎回、初期化をされてしまい、ゲームにならない(ゲームで言うなら、主人公が初期地点から動けない)。
だから、これだけではコードが不十分で、さらに、初期値をアプリ起動のときにだけ、1回だけ初期設定するようにしないといけない。アプリ起動時以外では、読み込んではいけない。
どうするかというと、結論から言うと、コンストラクタで宣言すればいい。コンソールアプリ(DOSプロンプト)だったら、コンストラクタを使わなくても変数のstatic宣言ぐらいで1回だけ済む場合もあるのだが、しかしフォームデザイナによる開発では実装の都合からか、static宣言だけでは1回だけの初期設定にはならず、毎回読み込まれてしまい、つまり主人公が一歩も動けない状態になりゲームにならない。
あれこれと説明したが、要するにコンストラクタを使えば、この問題は解決できる。具体的なコードは下記のようになる(作るアプリによって変数名やファイル名などは各人ごとに微妙に違う)。
public partial class Opening : UserControl
{
public static int heroHP ;
public Opening() // これがコンストラクタ
{
InitializeComponent();
heroHP = 30;
}
フォームデザイナで上手くクリックすると、コンストラクタの雛形(ひながた)が自動的に作られるので、そのコンストラクタのブロック内に初期値を書く。(ネットの解説サイトの多くには、コンストラクタで初期化する事が書いてないので、まあサイトにヤル気が無い。フォームデザイナの場合、なぜか単に static 宣言しただけでは、ダメである。)
なのでともかく、まず、コンストラクタを作る必要がある。ともかくフォームデザイナをあれこれクリックして、ファイルの中にある、ファイル名と同一名称のクラスの中に、さらに
public ファイル名
{
InitializeComponent();
}
というのが作成されれば、コンストラクタの自動生成は成功なので、あとは初期値を書くのである。(具体的な自動生成の方法が分からなければググってください。)
前提として、フォームデザイナが使えるファイルでないと、当然ながらフォームデザイナによる自動生成および自動設定は出来ない。
つまり、エントリポイントである自動生成される Program.cs ファイルで、上記のような処理をやろうとしても、フォームデザイナを作れないのでダメである。
エントリポイントとは別に、フォームデザインのファイルを作る必要がある。
なお、面倒くさがって、上記コードを手書きでコピーペーストしてコードをcsファイルに貼り付けても、ダメである。エラーになる。
きちんと、フォームデザイナを経由して、関連する設定も行わないといけない。
なので、まず、フォームデザイナで、デザインのファイルを作る必要がある。そのデザインをダブルクリックするなどして自動生成されるcsファイルを、上手く編集していくことになる。
いちおう、これでアクセスは可能である。
もしコンストラクタを使わない方法だと、メインループを一周すると、状態が元に戻ってしまいます。
その証拠に、たとえばif文などで、薬草など初期アイテムの個数を初期値(たとえば5個とする)からひとつ減ったとき(たとえば4個とする)にだけMessageBoxで「今、薬草が4個」などと表示されるプログラムを書いていしてみると、
画面上の表示はそのまま5個なのに「今、薬草が4個」のようにボックスでは表示されたりします。
これはなぜ、そうなるのか言語化して仕組みを考えてみると、つまりメインループを一周すると、コンストラクタ以外の状態が元に戻ってしまうのが Visual C# の実際の仕組みだからです。
static宣言は、メインループが1回終わるまでしか有効ではないのが、Visual C# で動作確認された実際の仕組みです。だからかメインループが1周すると、static宣言も初期状態に戻ってしまうので、static宣言だけではゲームとしての初期設定の機能を果たしませんし、ゲームに限らず Visual C++や Direct X のグローバル変数のようには使えないのです。
もしゲームでない、よくプログラミング教本にあるような1周すれば終わるような通常のコンソールアプリならば、メインループが存在しないのが通常なので、プログラムの終わりまで到達したらそこでプログラムが終了するので、static宣言だけでも問題が表れなかったのです。
しかし、私たちの作るゲームほか、通常のゲームには、メインループが存在します。だから、C#ではstatic宣言だけでは、グローバル変数のようにはならないのです。この事を書いてないネット上の解説サイトや、市販の入門書は、残念ながら能力不足です。
また、そもそもイベントドリブン型のプログラムには、メインループが存在しますし、マイクロソフトのC# 形式のプログラムにはそもそもメインループが存在します。
つまり、C#による、フォームデザイナなどを使ってプログラムする方式は、イベントドリブン型です。ネットや書籍では特に言及されませんが、C#のフォームデザイナ方式のプログラミングはイベントドリブン型なのです。
比較のため、(Visual )C++の場合、なぜコンストラクタを意識する必要が無かったを考えてみれば、
- C++ではグローバル変数の場所がメインループの外側にあるので、ループを一周してまたループ内が実行される段階になっても、そのループ内にグローバル変数の宣言部分が無いので、問題が起きない。
- また、C++ではメインループの外側に、アプリをロードしたときだけのイベントが用意されているので、そこで初期設定を行ってもいい、
という仕組みだったので、C++では問題が起きなかったのです。
しかし、C#では、上記の2つとも使えません。なので、コンストラクタで対応するしかないのです。
ToString() などのメソッド
編集しかし、上記はアクセスするだけなので、HPなどゲーム内数値の書き換えはまだ何も行っていない。数値の書き換えをできないとゲームにならない(数値の書き換えが出来ないと、たとえば現在位置を表す変数すらも書き換えできないので、そのままでは主人公が静止したまま、一歩も動けないのでゲームにならない)。
書き換え方法は、たとえば、あるラベルを書き換える場合、HPの書き換えを例に説明するなら(現在位置だと壁などの判定が難しいので、位置では説明しない)、HPを計算する各部で、
そのラベルを管理している(デザインファイルとセットの)csファイルのほうで、
this.ラベル名.Text = Opening.heroHP.ToString();
Invalidate();
という処理が必要である。
ToString()
とは、数値オブジェクトを文字列に置き換えるメソッドである。
「(string)heroHP」とか書いてもVisual C#ではダメであり、それでは変換できないです。上述のようにVisual C#では ToString()
メソッドを使う必要があります。
ともかく、こういう感じのコードを、たとえばボタンイベントのメソッド内とか、あるいはタイマーイベントのメソッド内とかに書く。
具体的にどんなラベル名かとか、あるいはラベル以外ならプロパティが何かとかは、デザインファイルを見ればわかるので、 クリックして開いて調べてコピペすればいい。
デザインファイルの書き換え
編集さらにデザインファイル側で、
this.ラベル名.Text = Opening.heroHP.ToString();
をしておく。
とりあえず、これで、グローバル変数のように機能する。
以上。
結論
編集このように、とても面倒である。
しかもこれはまだ残念なことに、DirectX的なダブルバッファも出来てないし、処理速度もなんだか遅いし、タイマー処理も自作しないといけない。つまり、C#および .net によるプログラミングは、ゲーム制作的にはWindowsAPIプログラミングに近い。
なので、正直言って、あまりwindowsの Visual C# によるゲーム制作は薦められない。
パソコン用ゲームを作るなら、フリーソフトのDXライブラリで作るのが、一番ラクであろう。次点でDirectXだろう。
C#は、DXライブラリ よりも勉強量が多いくせに、なのにDXライブラリよりもC#は動作が遅くて低性能である。つまり、C#は、C++の上位の言語ではないです。少なくともマイクロソフト社がwindowsで提供している Visual C# は、Visual C++ の上位の言語ではないです。
マイクロソフト社がどう宣伝しているのかは知りませんが、実際にコーディングをして確認したところ、上述のようにC++あるいはDirectXで可能だった多くのことが、C#では不可能または著しく困難です。
利点は Visual C# だとメモリ使用量が少ないぐらいで数十メガバイトで動くことくらいだが(DXライブラリだと100メガ以上になるのが普通)、しかし市販のパソコンのメモリが数GBもある2020年代の現代、数十メガと100メガとの違いはあまり気にする必要は無い。
結論から言うと、携帯ゲームはともかく、パソコン用のゲームなら、採用すべきプログラム言語は、Windowsユーザーの場合、それは1つは、マイクロソフト社の提供する Direct X (ダイレクト・エックス)というソフトを使用して、言語にはVisual C++を使用した環境です。Visual C++ と Direct X を組み合わせた言語は、90年代からゲーム開発に用いられており、RPGの開発にもよく用いられている組合せなので、C++とDirectXはゲーム業界での実用性が実証されています。
もうひとつは、別企業のUnityというソフトを使うことです。
Unityが普及する前の一昔前の日本では、アマチュアのゲーム作家の多くが、Visual C++ 上でDirect X を動作させた環境で、ゲーム作成をしました。だから少なくとも、C++は実績はあります。伝聞ですが、アマチュアではなくプロのPCゲーム会社のプログラマーは現代でもDirectXを使っていると言われます。ゲーム専門学校の授業カリキュラムを調べても、ほとんどの授業時間がDirectXとC++と標準C言語です。
さて、開発言語によっては、このような、関数的な振る舞いをするオブジェクトどうしに、他オブジェクトとパラメータのやりとりをするための機能が貧弱だったりします。
たとえば、WindowsのVisual C#のGUI開発では、ボタンやテキストボックスなどのオブジェクトをまとめて、「ユーザーコントロール」という1種のグループにできます。
GUI開発初心者のゲーム開発では、これらのユーザーコントロールの機能を使って、例えばRPGの戦闘画面を作成したり、セーブモードの画面を作成したり、さらにはマップ上での会話モードを作成したり、いろいろとゲーム内の各も度のグラフィック画面を作成しようとするかもしれません。
せっかくユーザーコントロール等のコンテナで戦闘画面をつくっても、あるいは戦闘の計算のアルゴリズムを作っても、戦闘モードの計算結果をマップ移動モードなどに受け渡せなければ、戦闘した結果がなくなってしまいます。
そういう欠陥を放置する間抜け企業が世界に多いので、世間の馬鹿企業を出し抜いたUnityなどの企業が新規にゲームエンジンを開発して儲けてるわけです。
このセクションで説明した、コンストラクタとstatic変数を組み合わせてどうこうという話、まったく市販のC#入門書には書かれていないのが実情です。マイクロソフト社も、満足に説明していません。これがマヌケ。
マイクロソフトの一番の問題は、自分達でC#やら.netなどを開発していながら、実はC++を中心に売れ筋のアプリを開発しているというアレっぷりです。フリーソフトなどで、どの言語や開発環境でそのアプリが作られているかを調べられるフリーソフトがあるのですが、そういうソフトで検査すると、マイクロソフトの有料の有名アプリの多くがC++製品だという実態が明らかにされています。
アメリカのベンチャー企業は10個起業したうちの9個は失敗で、残り1個が大成功して世界的企業になって、9社の損失ぶんを埋めるらしいです。
10個のプログラム言語のうち9個は失敗だと思うのが良いと思います。マイクロソフトにも ActiveX や Silveright など、多くの失敗があります。他社も失敗しています。成功した1個がC++とDirectXです。DirectXはゲーム以外の多くのアプリにも活用されています。
そのほか、かつてC# をベースとした .net Framework からDirectXを呼び出して使えるようにする「XNA」というマイクロソフト独自技術があったが、しかし2014年にXNAは終了している。どうしてもマイクロソフトのこれらの系統の最新技術を使いたい場合、2022年現在ならUWPというマイクロソフトの開発ツールで開発することになるだろうが、UWPはwindows10以降でないと開発できない。
なお、.net Frameworkは、マイクロソフトの言語が多数あったのを(Visual Basic や Visual C# や F# など多々言語があった)のを、共通言語ランタイム(CLR)を採用することで言語依存性を極力減らした開発環境である。.net Framework およびその後継の .NET Core は、Javaのように中間コード(IL)を介する方式である。
上記のような特徴のため、.net 系は、C++ のように、直接に機械語に翻訳する方式(ネイティブコード)とは異なる。このことは、C++ と比べて .net 系は、アプリの処理速度などの性能に若干の速度低下などの影響を与える可能性がある。そこまでリスクを考慮して、それでも使う必要があれば、使えばいい。
構造体とグローバル変数
編集基本
編集パラメータの種類が増えてくると、構造体を使わないと、データ管理が困難になる。
上述のコードを構造体を使って書き換えるコードを下記に示す。
構造体をRPGの用途としてグローバル変数として認識させるためには、下記のコードのように、冒頭部分のグローバル領域部分で、 static struct chara_status yuusya_status;
のように、使用する構造体変数をstatic変数として宣言しなければならない。
もし、main関数の中で、この構造体変数を宣言しても、battle関数などが構造体変数yuusya_statusを認識できず、エラーになる。
よって、下記のようなコードになる。
//例 グローバル変数と構造体によるRPGの各モード間でのパラメータの共有と書き換え
// Windows の Visual C++ の場合
#include <stdio.h>
#include <stdlib.h> // 「続行するには何かキーを押してください . . .」を表示する
struct chara_status {
int hp, mp, exp_;
};
static int kizugusuri_kosuu;
static struct chara_status yuusya_status; // 主人公「勇者」のステータスを構造体変数として宣言
int battle()
{
printf("モンスターと戦闘中・・・ \n");
yuusya_status.hp = yuusya_status.hp - 5;
printf("負傷したぞ! 5のダメージ! HPが5減ったぞ! \n");
printf("HP = %d \n", yuusya_status.hp);
printf("魔法で攻撃するぞ!MPが3減るぞ! \n");
yuusya_status.mp = yuusya_status.mp - 3;
printf("MP = %d \n", yuusya_status.mp);
printf("モンスターを倒したぞ! 経験値を2ポイント獲得したぞ! \n");
yuusya_status.exp_ = yuusya_status.exp_ + 2;
return 0;
}
int heal()
{
printf("傷薬で回復だ! \n");
yuusya_status.hp = yuusya_status.hp + 3;
printf("HP = %d \n", yuusya_status.hp);
printf("傷薬が1個減ったぞ! \n");
kizugusuri_kosuu = kizugusuri_kosuu - 1;
printf("傷薬は残り %d個 \n", kizugusuri_kosuu);
return 0;
}
int main(void)
{
yuusya_status.hp = 30; // 具体的に勇者の各パラメータを代入
yuusya_status.mp = 25;
yuusya_status.exp_ = 0;
kizugusuri_kosuu = 10;
printf("冒険を始めたぞ! 最初のHPは%d、MPは%d。経験値は%d \n", yuusya_status.hp, yuusya_status.mp, yuusya_status.exp_);
printf("モンスターに遭遇したぞ! 戦闘だ! \n");
battle();
printf("戦闘終了後のHPは%d、MPは%d。経験値は%d \n", yuusya_status.hp, yuusya_status.mp, yuusya_status.exp_);
printf("回復アイテムを使用するぞ! \n");
heal();
printf("回復後のHPは%d、MPは%d。経験値は%d \n", yuusya_status.hp, yuusya_status.mp, yuusya_status.exp_);
printf("回復後の傷薬の残り個数は%d個 \n", kizugusuri_kosuu);
system("pause");// 「続行するには何かキーを押してください . . .」の待機命令
}
構造体配列とグローバル変数
編集- (※ 未記述)
(※『C言語』で構造体の配列が説明されているので、こちらのページでは説明の必要が無くなった。過去、C言語教科書での構造体の配列の説明が不足していた時期があった。本セクションの作られた経緯を記録するため、消さずに残す。)
なお「構造体配列」という用語は由緒正しいものであり、たとえば1990年代のC言語入門書のナツメ社 『入門ソフトウェアシリーズ(1) C言語』[2]でも「構造体配列」という用語が使われている。実は昔は普通に標準C言語の入門書でも構造体配列を教えていたのだが、なぜかC++の書籍では「構造体配列」の解説をあまりを見かけなくなった。なお、2020年以降でも、標準C言語の書籍には「構造体配列」が書いてある。
ポインタを使う場合
編集下記のような単純な例では、わざわざポインタを使う必要はありません。グローバル変数を用いたほうが早いでしょう。
また、Java はそもそも理念上の理由からポインタの機能が無く、一般的にJavaではポインタの使用を禁止しています。ほか、Visual C# ではポインタが初期設定では使えません。
ですが、説明のため本ページでは、意図的に、比較的に簡単だけど非実用的なポインタによる例を、主に c++を想定して書いています。
単純な例
編集main 関数などで、ヒットポイントやマジックパワーなどのパラメータ郡などを一括に宣言して、さらにその場所をパラメータ郡の保管場所にしておく。そして、戦闘モードとかマップ移動モードとか各モードでは、ポインタによって、main関数に保管しておいたパラメータを書き換えるという処置が必要になる。(ポインタを使わないと、直接的に、先頭モードなどの各関数の外にある(main関数など外部保管場所にある)変数を書き換えることができない。)
//例 ポインタによるRPGの各モード間でのパラメータの共有と書き換え
// Windows の Visual C++ の場合
#include <stdio.h>
#include <stdlib.h> // 「続行するには何かキーを押してください . . .」を表示するのに必要。
int battle(int *hp, int *mp, int *exp_)
{
printf("モンスターと戦闘中・・・ \n");
*hp = *hp - 5;
printf("負傷したぞ! 5のダメージ! HPが5減ったぞ! \n");
printf("HP = %d \n", *hp);
printf("魔法で攻撃するぞ!MPが3減るぞ! \n");
*mp = *mp - 3;
printf("MP = %d \n", *mp);
printf("モンスターを倒したぞ! 経験値を2ポイント獲得したぞ! \n");
*exp_ = *exp_ + 2;
return 0;
}
int heal(int *hp, int *mp, int *exp_, int *kizugusuri_kosuu)
{
printf("傷薬で回復だ! \n");
*hp = *hp + 3;
printf("HP = %d \n", *hp);
printf("傷薬が1個減ったぞ! \n");
*kizugusuri_kosuu = *kizugusuri_kosuu - 1;
printf("傷薬は残り %d個 \n", *kizugusuri_kosuu);
return 0;
}
int main(void)
{
int hp, mp, exp_;
int kizugusuri_kosuu;
hp = 30; mp = 25; exp_ = 0;
kizugusuri_kosuu = 10;
printf("冒険を始めたぞ! 最初のHPは%d、MPは%d。経験値は%d \n", hp, mp, exp_);
printf("モンスターに遭遇したぞ! 戦闘だ! \n");
battle(&hp, &mp, &exp_);
printf("戦闘終了後のHPは%d、MPは%d。経験値は%d \n", hp, mp, exp_);
printf("回復アイテムを使用するぞ! \n");
heal(&hp, &mp, &exp_, &kizugusuri_kosuu);
printf("回復後のHPは%d、MPは%d。経験値は%d \n", hp, mp, exp_);
printf("回復後の傷薬の残り個数は%d個 \n", kizugusuri_kosuu);
system("pause");// 「続行するには何かキーを押してください . . .」の待機命令
}
また、このことから、もしRPGやシミュレーションゲームなどのプログラムを開発する場合は、慣れないうちは、ポインタの機能に制限の少ないC++などが好ましい。( C# だと、ポインタの活用に制限を掛けており、コードが複雑になる。)
なお、上記のコードの実行結果は、下記のようになる。
- 実行結果
冒険を始めたぞ! 最初のHPは30、MPは25。経験値は0 モンスターに遭遇したぞ! 戦闘だ! モンスターと戦闘中・・・ 負傷したぞ! 5のダメージ! HPが5減ったぞ! HP = 25 魔法で攻撃するぞ!MPが3減るぞ! MP = 22 モンスターを倒したぞ! 経験値を2ポイント獲得したぞ! 戦闘終了後のHPは25、MPは22。経験値は2 回復アイテムを使用するぞ! 傷薬で回復だ! HP = 28 傷薬が1個減ったぞ! 傷薬は残り 9個 回復後のHPは28、MPは22。経験値は2 回復後の傷薬の残り個数は9個 続行するには何かキーを押してください . . .
構造体
編集上述のコードを構造体を使って書き換えると、下記のようになる。
//例 構造体を使って整理。
//例 ポインタによるRPGの各モード間でのパラメータの共有と書き換え
#include <stdio.h>
#include <stdlib.h> // 「続行するには何かキーを押してください . . .」を表示するのに必要。
struct chara_status {
int hp;
int mp;
int exp_;
};
int battle(struct chara_status *boukensya_status)
{
printf("モンスターと戦闘中・・・ \n");
boukensya_status->hp = boukensya_status->hp - 5;
printf("負傷したぞ! 5のダメージ! HPが5減ったぞ! \n");
printf("HP = %d \n", boukensya_status->hp);
printf("魔法で攻撃するぞ!MPが3減るぞ! \n");
boukensya_status->mp = boukensya_status->mp - 3;
printf("MP = %d \n", boukensya_status->mp);
printf("モンスターを倒したぞ! 経験値を2ポイント獲得したぞ! \n");
boukensya_status->exp_ = boukensya_status->exp_ + 2;
return 0;
}
int heal(struct chara_status *boukensya_status, int *kizugusuri_kosuu)
{
printf("傷薬で回復だ! \n");
boukensya_status->hp = boukensya_status->hp + 3;
printf("HP = %d \n", boukensya_status->hp);
printf("傷薬が1個減ったぞ! \n");
*kizugusuri_kosuu = *kizugusuri_kosuu - 1;
printf("傷薬は残り %d個 \n", *kizugusuri_kosuu);
return 0;
}
int main(void)
{
struct chara_status yuusya_status; // 主人公「勇者」のステータスを構造体変数として宣言
yuusya_status.hp = 30; // 具体的に勇者の各パラメータを代入
yuusya_status.mp = 25;
yuusya_status.exp_ = 0;
int kizugusuri_kosuu = 10; // 道具はステータスとは別に管理する
printf("冒険を始めたぞ! 最初のHPは%d、MPは%d。経験値は%d \n", &yuusya_status);
printf("モンスターに遭遇したぞ! 戦闘だ! \n");
battle(&yuusya_status);
printf("戦闘終了後のHPは%d、MPは%d。経験値は%d \n", yuusya_status.hp, yuusya_status.mp, yuusya_status.exp_);
printf("回復アイテムを使用するぞ! \n");
heal(&yuusya_status, &kizugusuri_kosuu);
printf("回復後のHPは%d、MPは%d。経験値は%d \n", yuusya_status.hp, yuusya_status.mp, yuusya_status.exp_);
printf("回復後の傷薬の残り個数は%d個 \n", kizugusuri_kosuu);
system("pause");// 「続行するには何かキーを押してください . . .」の待機命令
return 0;
}
- (※ 実行結果については、前の節で紹介した結果とほぼ同じなので、省略する。)
上述のように構造体を使うと、たとえば関数
int battle(struct chara_status *boukensya_status);
のように、各人物のHPやMPなどのデータを呼び出すときに、いちいち1つずつ引数として何個ものパラメータを記述する必要はなくなり、1つの構造体でまとめて引数にできる。
また、引数 *boukensya_status
を見ると分かるように、受け取り関数 battle() 側では、その受け取り変数の型がポインタ型であることが分かるだけであり、具体的な構造体変数の中身を受け取り側で指定することはできないし、指定しても意味が無い。
このことは、大量のパラメータを扱うRPGやシミュレーションゲームにおいて、特に重要な技術であろう。
ところで上述コードの yuusya_status->hp
のように、呼びだされた関数から構造体変数にアクセスするときは、アロー演算子 ->
を使う。
アロー演算子を使わずに、 * yuusya_status.hp
と書いても、コンパイルできずエラーになるのが通常である。
なお、上述の構造体の例では、冒険者は「勇者」1人だけだったが、もし、勇者に加えて「魔法使い」「戦士」など追加の冒険者をパーティに加えて4人パーティとかのアルゴリズムを作りたい場合は、構造体と配列を組み合わせた、いわゆる「構造体配列」のテクニックを使えばいい。
構造体配列とポインタ
編集- ※ 教科書としての説明の都合上、実用性を無視して、構造体配列とポインタの組み合わせで、どうゲーム中の関数的なプログラムをつくるかを説明しています。実際のには、ゲーム制作では、特別な理由がない限り、ポインタを使わない出の通常の構造体配列でも充分です。(なお構造体配列は、ゲーム中のデータベースの読取り結果の保管などで使う可能性があります。そういうデータベース的な内容を保管するのに構造体配列が便利だからです。)
冒険者の人数を変数とした配列を使って説明する。
説明の単純化のため、冒険者はまだ勇者1人とする。(仲間を追加したくなれば、この人数を意味する変数をイジればいいだけ。)
- 1人パーティの例
//例 構造体配列を使って整理。
//例 ポインタによるRPGの各モード間でのパラメータの共有と書き換え
#include <stdio.h>
#include <string.h>
#include <stdlib.h> // 「続行するには何かキーを押してください . . .」を表示するのに必要。
struct chara_status {
char name[20];
int hp;
int mp;
int exp_;
};
int battle(struct chara_status *poihiki)
{
printf("モンスターと戦闘中・・・ \n");
poihiki->hp = poihiki->hp - 5;
printf("%sが負傷したぞ! 5のダメージ! HPが5減ったぞ! \n", poihiki->name);
printf("HP = %d \n", poihiki->hp);
printf("魔法で攻撃するぞ!%sのMPが3減るぞ! \n", poihiki->name);
poihiki->mp = poihiki->mp - 3;
printf("MP = %d \n", poihiki->mp);
printf("モンスターを倒したぞ! 経験値を2ポイント獲得したぞ! \n");
poihiki->exp_ = poihiki->exp_ + 2;
return 0;
}
int heal(struct chara_status *poihiki, int *kizugusuri_kosuu)
{
printf("傷薬で回復だ! \n");
poihiki->hp = poihiki->hp + 3;
printf("HP = %d \n", poihiki->hp);
printf("傷薬が1個減ったぞ! \n");
*kizugusuri_kosuu = *kizugusuri_kosuu - 1;
printf("傷薬は残り %d個 \n", *kizugusuri_kosuu);
return 0;
}
int main(void)
{
struct chara_status boukensya_status[1]; // 主人公「勇者」のステータスを構造体変数として宣言
strcpy_s(boukensya_status[0].name, 10, "勇者");
boukensya_status[0].hp = 30; // 具体的に勇者の各パラメータを代入
boukensya_status[0].mp = 25;
boukensya_status[0].exp_ = 0;
int kizugusuri_kosuu = 10; // 道具はステータスとは別に管理する
printf("冒険を始めたぞ! %sの最初のHPは%d、MPは%d。経験値は%d \n", boukensya_status[0].name, boukensya_status[0].hp, boukensya_status[0].mp, boukensya_status[0].exp_);
printf("モンスターに遭遇したぞ! 戦闘だ! \n");
battle(& boukensya_status[0] );
printf("戦闘終了後の%sのHPは%d、MPは%d。経験値は%d \n", boukensya_status[0].name, boukensya_status[0].hp, boukensya_status[0].mp, boukensya_status[0].exp_);
printf("回復アイテムを使用するぞ! \n");
heal(& boukensya_status[0] , &kizugusuri_kosuu);
printf("回復後の%sのHPは%d、MPは%d。経験値は%d \n", boukensya_status[0].name, boukensya_status[0].hp, boukensya_status[0].mp, boukensya_status[0].exp_);
printf("回復後の傷薬の残り個数は%d個 \n", kizugusuri_kosuu);
system("pause");// 「続行するには何かキーを押してください . . .」の待機命令
return 0;
}
技巧的になるが、上述のコード int battle(struct chara_status *poihiki)
のように、受け取り関数の引数をポインタ形で宣言するのが、コツである。
仮に、受け取り関数の側で、たとえば int battle( struct chara_status *boukensya_status[] )
のように配列の書式で書いても、コンパイルエラーになってしまい、失敗する。
このように、受け取り関数の側では、送り側の関数から送られてきた情報が「配列」であるかどうかを判断する必要は無く、受け取ったのが「何かのアドレスである」事だけが分かればいいので、ポインタ形を宣言するのである。
なお、配列の番号は0番から数えるのがC言語の仕様なので、 boukensya_status[0]
が主人公のステータスである。
実際のRPGゲームでは、パーティメンバーは複数人である場合が多い。 コードは下記のようになるだろう。
パーティは勇者と僧侶の2名とした。
傷薬で回復するかわりに、回復魔法で回復するとする。
- 2人以上のパーティの例
//例 構造体配列を使って整理。
//例 ポインタによるRPGの各モード間でのパラメータの共有と書き換え
// 複数人パーティの場合。
#include <stdio.h>
#include <string.h>
#include <stdlib.h> // 「続行するには何かキーを押してください . . .」を表示するのに必要。
struct chara_status {
char name[20];
int hp;
int mp;
int exp_;
};
int battle(struct chara_status *poihiki1, struct chara_status *poihiki2 )
{
printf("モンスターと戦闘中・・・ \n");
poihiki1->hp = poihiki1->hp - 5;
printf("%sが負傷したぞ! 5のダメージ! HPが5減ったぞ! \n", poihiki1->name);
printf("%sのHP = %d \n", poihiki1->name, poihiki1->hp);
printf("%sが魔法で攻撃するぞ!%sのMPが3減るぞ! \n", poihiki2->name, poihiki2->name);
poihiki2->mp = poihiki2->mp - 3;
printf("%sの残りMP = %d \n", poihiki2->name, poihiki2->mp);
printf("モンスターを倒したぞ! 経験値を2ポイント獲得したぞ! \n");
poihiki1->exp_ = poihiki1->exp_ + 2;
poihiki2->exp_ = poihiki2->exp_ + 2;
return 0;
}
int heal_magic(struct chara_status *poihiki1, struct chara_status *poihiki2 , int *kizugusuri_kosuu)
{
printf("%sの回復魔法で%sを回復だ! \n", poihiki2->name, poihiki1->name);
poihiki1->hp = poihiki1->hp + 3;
printf("%sのHP = %d \n", poihiki1->name, poihiki1->hp);
printf("%sのMPが2減ったぞ! \n", poihiki2->name);
poihiki2->mp = poihiki2->mp - 2;
printf("%sのMP = %d \n", poihiki2->name, poihiki2->mp);
return 0;
}
int main(void)
{
struct chara_status boukensya_status[1]; // 主人公メンバーのステータスを構造体変数として宣言
strcpy_s(boukensya_status[0].name, 10, "勇者");
boukensya_status[0].hp = 30; // 具体的に勇者の各パラメータを代入
boukensya_status[0].mp = 25;
boukensya_status[0].exp_ = 0;
strcpy_s(boukensya_status[1].name, 10, "僧侶");
boukensya_status[1].hp = 20; // 具体的に僧侶の各パラメータを代入
boukensya_status[1].mp = 45;
boukensya_status[1].exp_ = 0;
int kizugusuri_kosuu = 10; // 道具はステータスとは別に管理する
printf("冒険を始めたぞ!\n %sの最初のHPは%d、MPは%d。経験値は%d \n", boukensya_status[0].name, boukensya_status[0].hp, boukensya_status[0].mp, boukensya_status[0].exp_);
printf("仲間の%sの最初のHPは%d、MPは%d。経験値は%d \n", boukensya_status[1].name, boukensya_status[1].hp, boukensya_status[1].mp, boukensya_status[1].exp_);
printf("モンスターに遭遇したぞ! 戦闘だ! \n");
battle(&boukensya_status[0], &boukensya_status[1] );
printf("戦闘終了後の%sのHPは%d、MPは%d。経験値は%d \n", boukensya_status[0].name, boukensya_status[0].hp, boukensya_status[0].mp, boukensya_status[0].exp_);
printf("戦闘終了後の%sのHPは%d、MPは%d。経験値は%d \n", boukensya_status[1].name, boukensya_status[1].hp, boukensya_status[1].mp, boukensya_status[1].exp_);
printf("回復魔法を使用するぞ! \n");
heal_magic(&boukensya_status[0], &boukensya_status[1], &kizugusuri_kosuu);
printf("回復後の%sのHPは%d、MPは%d。経験値は%d \n", boukensya_status[0].name, boukensya_status[0].hp, boukensya_status[0].mp, boukensya_status[0].exp_);
printf("回復後の%sのHPは%d、MPは%d。経験値は%d \n", boukensya_status[1].name, boukensya_status[1].hp, boukensya_status[1].mp, boukensya_status[1].exp_);
printf("回復後の傷薬の残り個数は%d個 \n", kizugusuri_kosuu);
system("pause");// 「続行するには何かキーを押してください . . .」の待機命令
}
- 実行結果
冒険を始めたぞ! 勇者の最初のHPは30、MPは25。経験値は0 仲間の僧侶の最初のHPは20、MPは45。経験値は0 モンスターに遭遇したぞ! 戦闘だ! モンスターと戦闘中・・・ 勇者が負傷したぞ! 5のダメージ! HPが5減ったぞ! 勇者のHP = 25 僧侶が魔法で攻撃するぞ!僧侶のMPが3減るぞ! 僧侶の残りMP = 42 モンスターを倒したぞ! 経験値を2ポイント獲得したぞ! 戦闘終了後の勇者のHPは25、MPは25。経験値は2 戦闘終了後の僧侶のHPは20、MPは42。経験値は2 回復魔法を使用するぞ! 僧侶の回復魔法で勇者を回復だ! 勇者のHP = 28 僧侶のMPが2減ったぞ! 僧侶のMP = 40 回復後の勇者のHPは28、MPは25。経験値は2 回復後の僧侶のHPは20、MPは40。経験値は2 回復後の傷薬の残り個数は10個 続行するには何かキーを押してください . . .
- 解説
配列の番号は0番から数えるのがC言語の仕様なので、
boukensya_status[0]
が勇者のステータス、boukensya_status[1]
が僧侶のステータス、
である。
このように、RPGのプログラム内部ではキャラクターを番号で管理することになる。