ゲームプログラミング/RPG

出典: フリー教科書『ウィキブックス(Wikibooks)』
ナビゲーションに移動 検索に移動

RPG[編集]

ここでいう「RPG」とは、特に断りのないかぎり、ターン制RPG(Turn-based RPG)のこととする。(アクションRPGやシミュレーションRPG、アクティブバトルなどを語る場合には、その際にそのことを注記する。)

いわゆるドラクエ シリーズやや昔のFF(ファイナルファンタジー)1~3あたりのようなシステムのターン戦闘システムのゲームを念頭に、このページでは「RPG」と読んでいる。

現代では『RPGツクール』やウディタなどのパソコン用のゲーム製作ツールで、手軽にパソコンでターン制RPGをつくることもできる。


なお、JRPGとは英語では慣用的に、ドラクエのようなターン制RPG(英: Turn-based RPG)のことを英語(和製英語でなく実際の国際英語)のゲームのこと、またはせいぜいスーパーファミコン程度(またはそれに毛の生えたレベル)のような雰囲気の2Dグラフィックなどを駆使したRPG、またはドラクエや昔のファイナルファンタジーなどの典型的なRPGと類似したシステム、あるいは日本製ゲームのようにストーリー重視やキャラクター重視のゲームのことを「JRPG」や「J-RPG」と言っている。

あまり「JRPG」に明確な定義は無い。たとえば欧米人・外国人の作ったインディーズ系のターン制RPGの自己紹介文を見ても、アメコミみたいな筋肉ムキムキの主人公や女性ヒロインのデザインがアメコミ風(美少女でなく)美女なビジュアルのゲームでも「JRPG」や「J-RPG」という表現が見られる場合もある[1]一方で、アクションRPGでも何故かJRPGという場合もあったりと、あまりJRPGとは意味が統一されていない。


いっぽう、もし「TRPG」というと、いわゆるテーブルトークRPG(和製英語。英語ではテーブルトップRPGという)などと混同される可能性もあるだろう。日本製RPGにターン制RPGの多いことなどもあってか、ターン制RPGのシステムのことを英語では(TRPGではなく)JRPGという場合もある。

このページでいうRPGとは当然、けっしてテーブルトークRPGではなくて、コンピューターゲ-ムのRPG、古い言い方ならビデオゲームのRPGのことである。


設計の方針[編集]

初期段階ではデータの個数は少なめに[編集]

細かな設計方法を述べる前に、まず、設計時の大まかな注意事項を述べる。

ゲーム好きの子供などがRPGの企画を考えるとき、よく、自由帳などのノートに、いきなり、大量の武器や防具の「攻撃力」や「防御力」などのデータを、ノートに書き起こし足りする場合がある。

例えば

1. 竹やり: 攻撃力=1, 値段 = 50G,
2. 鉄のナイフ: 攻撃力=3, 値段 = 100G,
3. 鉄の剣: 攻撃力=10, 値段 = 700G,
(中略)
130. ミスリルのよろい: 防御力 = 40, 値段= 13000G,
131. ミスリルのたて: 防御力 = 70, 値段= 10000G,
(後略)


・・・・みたいに、何十個や何百個もの武器や防具のデータを、ノートに書き起こしたくなる人もいるだろう。


しかし、プログラミングでは、けっして、プログラミングの最初に、いきなり大量のデータを搭載するようなプログラミングは、してはならない。

なぜなら、もし、武器や防具のプログラムを修正するとき、いままでプログラムした武器データ・防具データすべてを修正する場合がありうるからである。

つまり、もし、先に何百個もの武器や防具のデータをソースコードに入力してしまうと、修正時に、その何百個ものデータを書き直す必要が生じかねない。

なので、書き直しの個数を減らすために、RPGのプログラムを書く場合には、武器や防具などのデータの個数は、せいぜい2つか3つといった個数で、武器や防具などそれぞれのデータを作成しておくのがコツである。

RPGだけにかぎらず、アクションゲームなど他のジャンルのゲームでも同様に、設計の初期段階で使用するデータは、少なめにしておくのがコツである。

(ゲームだけにかぎらず、ほぼ全てのプログラミングにおいて、設計の初期段階では、データを少なくしておくべきである。)


もし、既にソースコードに大量の武器防具データを入力してしまった人は、それらの入力してしまったデータを、ソースコードとは別のテキストファイルまたは表計算ファイル( マイクロソフトExcel(エクセル) など)に移動しておくのが便利である。そのテキストファイル(または表計算ファイル)に「武器の設定資料」や「防具の設定資料」などと名づけて保管しておくと、のちのち、集団作業をする場合や、ゲームユーザーに設定を公開する場合などに便利である。


特に、武器や防具などのデータは、表計算ソフトで

┏━━━━━┳━━━━┳━━━━━┳
┃       ┃       ┃       ┃          
┣━━━━━╋━━━━╋━━━━━╋
┃     ┃     ┃      ┃      
┣━━━━━╋━━━━╋━━━━━╋
┃       ┃     ┃          ┃
┣━━━━━╋━━━━╋━━━━━╋━
┃        ┃     ┃       ┃            
┣━━━━━╋━━━━╋━━━━━╋

のような空白の表に、

┏━━━━━┳━━━━┳━━━━━┳
┃名称   ┃攻撃力 ┃ 値段  ┃          
┣━━━━━╋━━━━╋━━━━━╋
┃竹やり  ┃  1  ┃  50  ┃      
┣━━━━━╋━━━━╋━━━━━╋
┃鉄のナイフ┃  3  ┃  100  ┃
┣━━━━━╋━━━━╋━━━━━╋━
┃鉄の剣  ┃  10 ┃  700  ┃            
┣━━━━━╋━━━━╋━━━━━╋

のように入力しておくと、後々で編集しやすく、また、必要に応じてテキストファイルなどに変換できるので便利である。


なお、表計算ソフトの入手には、有料ソフトのマイクロソフトExcelのほか、無料のフリーソフトのw:LibreOffice(オフィスソフト名)のCalc(これがLibreOfficeの表計算ソフト)などがある。


参考: CSVファイル[編集]

(なお、プログラムで読み込むための大量の数値データを入出力するとき、テキストファイル(.txt)を使うよりも、一般にCSVファイル(w:Comma-Separated Values)というのを使う場合が多い。)ゲームにかぎらず、IT企業などでも、そのような大量のデータを処理するときに、CSVファイルを使う。

たいていの表計算ソフトには、CSVファイルへの出力の機能もついている。

上記の武器の表の場合、CSVファイルとして出力すると、

名称,攻撃力,値段
竹やり,1,50
鉄のナイフ,3,100
鉄の剣,10,700

のように出力される。

C言語には、テキストファイルを読み取る関数もあるので、それらとCSVファイル(もしくはtxtファイル)を組み合わせることで、上記の武器・防具の設定ファイルを、ほぼそのままでゲーム用データを読み込み処理に応用できる。

いきなり構造体や関数を使わない[編集]

  • 方針

C言語では、何度も利用する処理をまとめるのに「関数」という機能があります。

また、同じパターンのデータを複製するために「構造体」という機能があります。さらにC++では、これらと似たような機能をもつ「クラス」というものもあります。

RPGのプログラミングでも、最終的には関数や構造体や(C++なら)クラスなどを使うことになるでしょう。しかし、あまり最初から構造体や関数を使わないほうが、便利です。

なぜかというと、もし、いきなり関数や構造体を使おうにも、アプリの実行状態を見てない段階なので、具体的にどんな処理をまとめれば良いのかなどのイメージがわかないので、プログラミングが頓挫しがちになるでしょう。

もし、アプリの実際の動作をまだ見てない状態で、せっかく関数などのプログラミングをしても、たいてい、その関数などの仕組みは、見当はずれのプログラムになりがちです。


それでも、もし最終的にいきなり関数や構造体などを使ってプログラムを実行できばいいのですが、そもそも、たいていの場合の初心者は、はじめて書くプログラムでは、なんなかのプログラムミスがありコンパイルエラーになり、実行自体を開始できないでしょう。


なので初心者は、ともかく通常の変数ばかりのような初歩的な機能ばかりを使ってでもいいので、まずは動作を実行できるプログラミングをするほうが、手っ取り早いです。


最終的には構造体や関数などをつかって処理をまとめる目標は必要ですが、しかし、あるアプリの開発の初期段階のプログラムでは、あえて「関数」や「構造体」などの機能を利用しないのがコツです。


  • 判断基準

初心者のアプリ開発初期では、ちょっとくらい繰り返し処理があっても(たとえば「ある3行の命令を、4回繰り返す」くらいなら)、関数なんて使わずに、1行づつ処理を書いてしまいましょう。初心者には、せいぜいFOR文やIF文を使う程度でも、十分でしょう。

関数を使うほうが便利になる場合とは、もっと行数が多い場合や(関数内の処理が何十行以上もある場合)、もしくは繰り返し回数が多い場合です(何十回以上も繰り返す場合など)。


なぜ、関数や構造体を開発初期では使わないほうがよいかというと、たいていのプログラム言語では、関数などの定義の宣言場所と、その関数を呼び出す場所とが、文法上、けっこう離れた位置になります。

このため、関数や構造体が多いと、かえってプログラム全体の見通しが悪くなりますし、修正などのさいに、スクロールバーをスクロールする手間が生じます。

そして、実際のアプリは、学校などで習う十行ていどで終わるプログラムとは違い、実務のプログラムでは何千行も行数がある場合もあるので、スクロールの手間は、けっこう面倒です。


学校などで習う関数の命令文は、初心者でも理解しやすいように関数内の処理が数行で終わる例がほとんどですが、しかし実際のアプリ開発では、3行×3回ていどなら関数を使わないで、1行づつ書いたほうが分かりやすくなるしょう。


このように、学校(大学や専門学校)などで習うプログラミングの授業とは、違います。大学などでは、どんどんと「構造体」やら「関数」やら「クラス」やらの高度な機能を習いますが、しかし、ゲームプログラミングでは、そういうのは後回しです。


「毎回、この処理を1行ずつ書くのがメンドウだな~。関数にしたいな~~(もしくは「構造体にしたいな」)。」と思ったら、そこで初めて、以降に作成する処理を、自作の関数や構造体に置き換えればいいのです。

なお、このやり方で開発を進めていくと、関数を使わずに当初は自作関数を使わずに命令を1行ずつ書いていた処理と、自作関数を使って書いた処理とが、混在することになります。

しかし、混在を気にする必要はありません。

バグがない限り、直さないほうが安全です。

どうしても混在が今後の作業の懸念として気になるなら、コードをいじるのではなくて、仕様書を更新したりするとか、そっちのほうに頭を使いましょう。


また、次回作を作るときに、次回作のコードでは、自作関数で置き換えるなどの整理をしましょう。


なお、自作関数や構造体などを使って、1行ずつベタに書いていたコードを置き換えたりして整理したりすることをIT用語で「リファクタリング」と言います。

リファクタリングは次回作の製作で行いましょう。前作・今作は、リファクタリングせずに、そのままにしておくのが安全です。RPGやシミュレーションゲームなどの長時間プレイに及ぶゲームの場合、もしリファクタリングしてしまうと、テストプレイに時間が多く取られてしまいます。


  • コメントの習慣をつけよう

初心者に必要なノウハウは、コメントをプログラム中に書く習慣をつける事です。

// 武器の強さ
    int tanken_tuyosa = 10; // 短剣の強さ
    int tyouken_tuyosa = 25; // 長剣の強さ

などのように、コメントを書けば、いちいち構造体やクラスにしなくても、第三者が見ても用途がわかります。いちいち、武器の構造体やら武器クラスとかを考える手間が減ります。

初心者には、「関数」や「構造体」よりも、コメントの習慣のほうが大切です。


学校では、採点の手間が掛かるのでコメントをあまり練習しないでしょうが、しかし実務ではコメントの習慣は必要でしょう。

たとえ自分1人で開発しているアプリであっても、日数が経つと過去のプログラム時に思いついたアイデアを忘れることも多いので、未来の自分が思い出せるようにコメントで情報を残しておきましょう。


異なる関数どうしでのパラメータ共有[編集]

おおまかな内容

グローバル変数やポインタなどを使って、別々の関数どうしでパラメータを共有できることを紹介。

普通のC言語の知識があれば、それで足りる知識の紹介なので、すでにC言語をある程度 知っている人は読まなくてもいい。

モードの管理手法について[編集]

基礎[編集]

ターン制RPGの場合、モードの切り替え方法は、 たとえば、モード番号 mode_number みたいな変数を用意して、

マップ移動モード: 1000番
会話モード: 2000番
メニューモード: 3000番
戦闘モード: 4000番

のように、番号で管理したほうがラクでしょう。

いちいち、マップ移動フラグ map_flag (// map_flag = 1ならマップ表示)や会話中フラグ talk_flag (//1なら会話ウィンドウ表示)みたいな変数を用意しないほうがラクでしょう。

その理由は、

たとえば戦闘モードへの突入のためのモードの切り替え時に、いちいちmap_flagをオフ(0にセット)にして、talk_flagをオフにして、menu_flagをオフにして、・・・などのプログラム作業を、モード番号管理の方式ならば1回の作業で省略できます。

また、もし、管理するフラグの個数が増えると、フラグの切り替えのし忘れなどのバグも発生しやすくなります。

このように、モード番号方式にすると、自然と、各モードへの遷移時に他モードを排他することができ、モードの混在を防げるので、ゲームシステムをモジュール化できます。

よって、モード番号の方式で管理するのが無難でしょう。

これはつまり、何かのオン/オフのフラグは、なるべく特別なイベント(例: 中ボスを倒した。伝説の武器を手に入れた。)などの例外的な処理の管理のみで活用するのが無難ということです。


なお、1番、2番、3番、・・・などのように1つおきではなく、1000番、2000番、のように間を置くのは、途中で追加するモードを挿入しやすくするためです。

たとえば、マップ移動でも、通常の歩行での移動を1000番として、ダッシュ移動を1100番、船による海上の移動を1200番のように、モードを追加したくなるかもしれません。なので、ある程度の間を、事前に基本モード間に空けとくと便利です。


特に、メニューモードや戦闘モードなど、コマンドをいろいろとプレイヤーが選ぶことになるので、それぞれのコマンドに対応したモードを多く追加することになるでしょう。

メニューモードの内部にコマンド(たとえば「装備」コマンド)に対応した中モードがあって、さらにコマンド(武器を装備? それとも防具を装備?)があって、選んだコマンドに対応した小モードに遷移する・・・みたいな3段階のモードの存在する可能性もあるので、基本のモード番号は3桁以上(つまり100番以上、できれば1000番ほど)ないと、不便でしょう。

このため、最低でも

マップ移動モード: 100番
会話モード: 200番
メニューモード: 300番
戦闘モード: 400番

くらいの3桁以上で管理するべきです。

変数を媒介としてモード番号の数値の定義[編集]

モード番号は、開発中に番号の数値の仕様を変更したくなる可能性があるので(例えば、オープニング画面のモード番号を追加したくなったとか)、後から番号を変更しやすいように、変数を媒介して定義するとラクです。

まず、単純な方法で、

// モード番号の定義
int mode_map = 100; // マップ画面のモード番号
int mode_talk = 200; // 会話画面のモード番号
int mode_menu = 300 // メニュー画面のモード番号
int mode_battle = 400 // 戦闘画面のモード番号

みたいに定義する方法があります。

こうすれば、もしモード番号の数値を変更するときには、対応する宣言文 int mode_〇〇 = △△の1個の文だけを変更するだけで済みます。

なお、C言語には定数値の宣言の就職子 const がありますので、

// モード番号の定義
const int mode_map = 100; // マップ画面のモード番号
const int mode_talk = 200; // 会話画面のモード番号
const int mode_menu = 300 // メニュー画面のモード番号
const int mode_battle = 400 // 戦闘画面のモード番号

と書くと、よりいっそう、意味が明確になります。


ほかにも、defineマクロを宣言する方法もあります。

// モード番号の定義
#define MODE_MAP 100 // マップ画面のモード番号
#define MODE_TALK 200 // 会話画面のモード番号
#define MODE_MENU 300 // メニュー画面のモード番号
#define MODE_BATTLE 400 // 戦闘画面のモード番号

マクロ名が大文字になってることに、特に深い理由は無く、単なるC言語業界での慣習です。


列挙型 enum[編集]

なお、モード番号を使う代わりに、C言語の列挙型 enum の機能を使うことでも、上記のような排他的な制御は出来ます。そもそもC言語の列挙型の正体そのものが、上述のモード番号のように、整数(int型)の値を条件分岐用データに割り当てる命令です。

しかし、C++と無印C言語でenumの文法が違っていたり、市販の入門書ではenumについて触れてないことも多く、列挙型はあまり初心者向けではありません。


モード番号方式の限界[編集]

ただし、上記の一連のノウハウは、あくまでターン制RPGの場合だけでしょう。

もしアクションRPGの場合なら、マップ移動中でも戦闘したり会話したりする可能性もあるので、モード番号方式ではなくフラグ方式にせざるを得ないでしょう。

このことは、ターン制RPGとアクションRPGとでは、コードを共有する事が、なかなか難しい事にも、つながるでしょう。


2Dマップ[編集]

方針だけ述べる。具体的なコードは、上手い人のコードを参考にしよう(コードがけっこう長くなり、紹介がメンドウくさい)。

2Dマップがあると、俄然、RPGっぽく見えるようになって、ヤル気が出るので、さあ作ろう。

マップのデータは、ふつう、2次元配列で書く。

まず、グローバル領域で、たとえば

static int maptable[10][10] = {
	{ 0,0,0,0,0,0,0,0,0,0 }, //0 y
	{ 0,0,0,0,0,0,0,0,0,0 }, //1
	{ 0,0,0,0,0,0,0,0,0,0 }, //2
	{ 0,0,0,0,0,0,0,0,0,0 }, //3
	{ 0,0,0,0,0,0,0,0,0,0 }, //4
	{ 0,0,0,0,0,0,0,0,0,0 }, //5
	{ 0,0,0,0,0,0,0,0,0,0 }  //6
};

のように、とりあえず配列を確保しよう。


ここで重要なのは、 確保した配列のタテとヨコは、ヨコの並びをx方向として、タテの並びをy方向としたとき、

maptable[y][x]

のように確保されていることに注意する。


さて、各配列に「0」を入れても、はたして0番が何を意味するか、まだ何も決めていない。

ゲームにもよるが、とりあえず、何もマップチップを上書きしない領域だと定義しよう。

背景色と同じ色のベタ塗りのマップチップを作っておけば、それで上書きすれば、あたかも何も上書きしてないように見える。

いちいちif文などで、何も上書きしないように場合わけをするのもメンドウであるので、どっちの方式にするか、決めておこう。


さて、マップ用の配列は、作成したいマップで最低限必要なマス目よりも、やや大きめの領域を確保しておく必要がある。

たとえば、5×5のマップを作りたいなら、配列ではもっと大目に、たとえば 8×7 とか確保しておく必要がある。


もし、なんらかのバグで確保されていない領域を呼び出してしまうと、プログラムがエラーで異常停止してしまう。


たとえば、一番左端をもしx=0とした場合、もしこの場所に主人公がいると、さらにプレイヤーが「左端に行こう」と考えて、左ボタンを押したときに、エラーで異常停止してしまう。

こういう停止はメンドウくさいので、だったら念のため、最初から、マップを移動可能な場所よりも何マスか大目に確保しておくと安全である。


さて、まだ配列を確保しただけなので、「0」が何かとか、「1」が何かとか、まったく定義していない。

とりあえず、

0番は、進入不可能の暗闇。
1番は、床とか、草原とか平地とか、とにかく歩ける場所

としよう。 だと思ってればイイ。


たとえば、もし配列 maptable の宣言が、

static int maptable[10][10] = {
	{ 0,0,0,0,0,0,0,0,0,0 }, //0 y
	{ 0,0,1,1,1,1,1,1,0,0 }, //1
	{ 0,0,1,1,1,1,1,1,0,0 }, //2
	{ 0,0,1,1,1,1,1,1,0,0 }, //3
	{ 0,0,1,1,1,1,1,1,0,0 }, //4
	{ 0,0,0,0,0,0,0,0,0,0 }, //5
	{ 0,0,0,0,0,0,0,0,0,0 }  //6
};

のような宣言だったら、このマップでは真ん中のほうに4マス×5マスの移動可能な地帯がある。 その周囲は移動不可能になっている。

ともかく、このように、プログラマーは、まず、何番のマップチップが何を現すかということを、脳内で設計しておく必要がある。


配列の範囲外の読み込みエラー防止のためも兼ねて、0番は、すべてのゲームキャラが進入禁止としておくのは安全だろう。


あとはもう、win32 API などの機能を使って、マップチップの画像を配列にもとづいて各マスごとに表示すればいい。


前提として、まずマップチップ画像の読み込みを行う必要がある。あるいは、画像をあらかじめ実行ファイルに組み込んでおく必要がある。

画像の実行ファイルへの組み込みは、Visual C++ にそういう機能があるので、それを使えばいい。

画像を読み込みたいなら、ゲーム起動時にでも世も込んでおけばいい。



イメージ的にコードの雰囲気(ふんいき)を書くと、たとえば case WM_PAINT: の節に、下記のように for文などを使って画像ハンドらに画像を代入するコードを裏画面に書いていき、最後にまとめて本画面に描画することになる。


(イメージ)
(※ あくまでイメージです。このままでは、動きません。)
       int iTemp;

	for (x_map = 0; x_map <= 9; ++x_map)
	{
		for (y_map = 0; y_map <= 6; ++y_map)
		{			
			iTemp = maptable[y_map][x_map]; // 配列の文字が長いので、いったんiに置き換え
			hbmp = hbmp_mapchip_list[iTemp].hbmp_mapchip;

			SelectObject(hMdc, hbmp);
			BitBlt(hbackDC, 225 + x_map * 32, 140 + y_map * 32, 32, 32, hMdc, 0, 0, SRCCOPY);

			// DeleteDC(hMdc); // これを入れると、マップが表示されない。
		}
	}


	// 裏画面から本画面に転送
	BitBlt(hdc, 0, 0, 700, 500, hbackDC, 0, 0, SRCCOPY);

	hbmp = NULL; // 初期化。


	// 中間ハンドルからhbmpを解除
	SelectObject(hMdc, NULL);
	SelectObject(hbackDC, NULL);

	// 不要になった中間物を削除	
	DeleteDC(hbackDC);
	DeleteDC(hMdc);

	DeleteObject(hbmp);
}



SelectObject や BitBlt は、Win32 API の専用の関数なので、分からなければ読者で調べてください。


なお、マップチップ画像そのものの作成は、単にWindowsアクセサリ『ペイント』などの画像作成アプリでビットマップ画像を用意すればいい。

ツクールやウディタなどだと、いくつかのマップチップをまとめた「タイルセット」などを取り扱うが、実はプログラミング的には、わざわざタイルセットを作らなくても、マップチップ1個ずつを読みとりすることも可能である(単にwin32APIのビットマップ画像の読みとりの機能を使えばいい)。

(win32 API の初期設定のままではビットマップしか表示できないハズ。PNGなどを使いたいなら、GDI+の設定を行うこと。GDI+については説明を省略。)。


なお、マップチップのサイズの規格は、よくある規格は16ピクセルの倍数で、「px」をピクセル単位の意味として16px×16pxまたは32px×32pxまたは64px×64pxのマップチップ規格がよくある(ツクールやウディタのマップチップのサイズ規格も、この系統)。

とりあえず、私たちにとって作るのがラクなのは、小さいほうが作成がラクなので、とりあえず16×16のマップチップを作ろう。


プログラム技術としてはマップ表示は単なる二次元配列なので難しいことは無い。しかし、その他の準備が忙しく、たとえばマップチップ用のビットマップを用意したりとか、あるいはキーボード操作のプログラムを作ったりとか、そういう準備が難しい。


これだけだと、まだマップを表示しただけなので、さらに主人公キャラのキャラチップとか、主人公がx=何マス目、y=何マス目にいるかの変数とかも宣言する必要がある。

戦闘中のアルゴリズム[編集]

素早さ順行動のアルゴリズム[編集]

ターン制RPGでは、戦闘中、敵と味方が入り乱れて、素早さ順に行動するシステムが、一般的です。

このアルゴリズムが、けっこう難しいです。

ドラクエ1のように、敵と味方が1対1の戦いなら、けっこう簡単ですが、ドラクエ2以降や『ファイナルファンタジー』シリーズのように、味方が複数人、敵も複数匹の場合、それらの行動準の決定アルゴリズムは、けっこう難しいです。

さて、まずアナタは、ドラクエ1のように敵と主人公が1対1のバトルのプログラムを、あなたの自作ゲームのソースコードに書いてください。そして、その1対1のバトルのプログラムを実際に自作ゲームに組み込んで、とりあえずプレイ可能な状態まで持っていってください。

それが出来たら、次に、その1対1のプログラムを拡張して 多数 対 多数 のプログラムに改造していきましょう。

では、そのような多数対多数での素早さ順行動アルゴリズムのための考えかたを述べます。


まず、敵と味方の両方とも、それぞれのキャラ(または敵モンスター)の素早さの値を、登場の順番ごとに(まだ並び替えはしない)1つの配列に入れます。

けっして「敵どうしだけ」、「味方どうしだけ」でなく、敵と味方の両方を比較する必要があります。


たとえば、味方3人、敵2匹なら、配列の要素の5個を使います。

あらかじめ、要素数20や30など、十分に要素数の大きい配列を1つ用意しておきましょう。

そして、この、たとえば要素数20の配列の先端5個ぶんの要素に、素早さの値を入れるわけです。

たとえば

for(id=0;   i <  partyNinzu ;id=id+1 ) // まず味方パーティのメンバー順に味方人数分だけ素早さを配列に代入
{
sankaSubayasa[id] = mikata_Subayasa[id]; // 味方の人数分、味方の素早さを代入
}

for(id=0 ;  id <  tekiNinzu; id=id+1 )// つづけて敵パーティの人数分を代入
{
sankaSubayasa[partyNinzu + id] = teki_Subayasa[id]; // つづけて、敵の人数分、敵の素早さを配列に代入

}

のようになります。今後の説明のため、とりあえずこの配列の名前を「素早さ配列」とでも名づけておきます。

※ なお、この作業のために、あらかじめ、キャラのステータスを配列または構造体配列などにしておく必要があります。少なくとも、各キャラの素早さは配列化しておかなければ、並び替えが困難(ほぼ不可能)になります。なので、事前に、構造体配列などの配列にしておきましょう。wikibooksのC言語の教科書でも構造体配列について解説してあります。
※ 上のコード例では、説明の簡単化のため、構造体配列を使わないで説明しています。実際のゲーム開発では、もしかしたら構造体配列で書いたほうがラクかもしれません。

ともかく、このように、for文などを使って、まとめて入れましょう。


さて、並び替えたい対象物は、けっして「素早さ」そのものではないです。なので、上で作った配列(「素早さ配列」)そのものは、並び替えません。

行動するキャラクター・敵の順番を並び替えたいわけです。


なので、そのためには、キャラクターまたは敵のIDが入った配列が新たに必要です。なので、そのキャラクターIDまたは敵IDの入った配列の呼び名をとりあえず「行動順配列」と言いましょう。


たとえば、この行動順配列の内容がたとえば「3, 1, 5, 4, 2」だったら、

まず ID=3番 の奴が最初に行動し、
その次に ID=1番 の奴が行動し、
その次に ID=5番 の奴が行動し、
その次に ID=4番 の奴が行動し、
さいごに ID=2番 の奴が行動する.

というような仕組みになります。


要するに私たちは最終的な目標として、「行動順配列」を作れればいいのです。そして、そのための手段として、「素早さ配列」を使います。

そして、「行動順配列」を作るために、「素早さ配列」の各要素の数値を「ソート」技術で比較します。ソートの技術は、一般的な情報科学の本に書いてあるので、それを参照してください。


一般に、2つの変数の数値を入れ替えるアルゴリズムがあります。コレを使うとラクですし、そもそもソートのアルゴリズムはコレを応用します。

まず、X=3, Y=5 を入れ替えたい場合、

int X=3;
int Y=5;

int irekaeA = X;
int irekaeB = Y;

そして、すばやさ比較をして、「素早さ配列どうしの入れ替えの入れ替えと一緒に、行動順配列どうしもする」のがテクニックです。(※ 一般のソートの教科書では、ひとつの配列しか入れ替えないので、なかなかコレに思い至らない。)

イメージ的にプログラムを書くと、

if (subayasaHairetu[2] >= subayasaHairetu[5]) {
 // (ifカッコの中の)前の数が大きい場合には入れ替えしない。
}
if (subayasaHairetu[2] < subayasaHairetu[5]) {
  // 素早さ配列の入れ替え
    int irekae_Subayasa_A ;
    int irekae_Subayasa_B ;

    irekae_Subayasa_A = subayasaHairetu[2];
    irekae_Subayasa_B = subayasaHairetu[5];

    subayasaHairetu[2] = irekae_Subayasa_B;
    subayasaHairetu[5] = irekae_Subayasa_A;

  // 行動順配列の入れ替え
    int irekae_Junjo_A ;
    int irekae_Junjo_B ;

    irekae_Junjo_A = junjoHairetu[2] ;
    irekae_Junjo_B = junjoHairetu[5] ;

    junjoHairetu[2] = irekae_Junjo_B;
    junjoHairetu[5] = irekae_Junjo_A;
}

みたいな感じの作業を、すべてのキャラ同士の組み合わせで、for文を使って網羅的に行えばよいのです。

上記コード例の場合、そのときの行動順配列で2番目と5番目のキャラを素早さ比較しており、もし素早さが高ければ、そのキャラを先にもってきています。

ただし、配列の「2」番と「5」番なので、日常の数え方では3番と6番のことです(配列は0から数え始めるので)。


さて、for文の走査は単に、リーグ戦方式で、単に片っ端から走査していけばいいだけです。

2番目と5番目だけでなく、(日常の数え方では)

まず

1番目と2番目、
1番目と3番目、
1番目と4番目、

・・・

2番目と3番目,
2番目と4番目
2番目と5番目

・・・

3番目と4番目,
3番目と5番目

・・・

みたいな組み合わせをfor文で片端から試せばいいだけです。


たとえば、もし戦闘参加キャラが敵と味方あわせて5人なら、走査する組み合わせは合計で 5+4+3+2+1 = 15回 の程度です。


このfor文の走査の順序によって、ソートのアルゴリズムがおおむね決まります。

ソートのアルゴリズムは『バブルソート』でも『選択ソート』でも何でもいいので、とにかく「素早さ配列どうしの入れ替えの入れ替えと一緒に、行動順配列どうしもする」のがテクニック。


なぜ、これで上手くいくかというと、

行動順配列は、入れ替えをする前の内容は、単なる順番 0,1,2,3 でした。つまり、行動順配列の初期値の内容は、キャラクターおよびモンスターの通し番号でした。

そして、素早さといっしょに行動順配列を入れ替えをしたのだから、行動順配列は最終的に、もとの通し番号が対応する素早さの順序どおりになっているハズだからです。


理由

なぜ、これで素早さ配列および行動順配列が求まるかというと、

まず、

1番目と2番目、
1番目と3番目、
1番目と4番目、
1番目と5番目、

の比較では、もっとも早いキャラを決定します。

この比較のさい、単に比較をするだけでなく、さらに素早いほうのキャラを1番に毎回の比較ごとに移動しています。

なので、2~5番の全員との比較を終えた時点になれば、配列の0番には(日常の数え方で)キャラ番号の1~5番目のキャラのうち最も素早いキャラのデータが代入されることになります。

そして次に2番目に早いキャラを決定する必要があるので、残りのキャラどうしで、

2番目と3番目、
2番目と4番目、
2番目と5番目、

の比較および配列1番への移動をする必要があるのです。そして、2番目に早いキャラが決まり、配列1番にそのキャラのデータが代入されています。

同様に3番目に早いキャラ、4番目に早いキャラ、・・・を決定していきます、

戦闘中の各ターンの進行の自動化[編集]

戦闘で各ターンのコマンド入力が終わったら、今度は、自動的にそのターンの戦闘が始まらなければいけません。

そして、1~2秒くらいおきに、たとえば

「戦士の攻撃!ゴブリンに5のダメージを与えた!」 (0~2秒のあいだ表示)
「ゴブリンの攻撃!魔法使いは2のダメージを受けた!」(2~4秒のあいだ表示)
「魔法使いの攻撃!ゴブリンは4のダメージを受けた!」(4~6秒のあいだ表示)

みたいに秒の経過によってメッセージが自動進行する必要があります。

※ 説明の単純化のため、全員、通常攻撃で肉弾攻撃だとします。

このような自動進行の処理のため、タイマーを呼び出す必要があります。

タイマーは、オペレーティングシステムが管理しています。

オペレーテイングシステムの種類によって、タイマーの呼び出し方は異なります。

Windowsの場合、Windows API にある SetTimer 関数でタイマーを呼び出せます。

SetTimer 関数で指定した時間が経過するごとに、case WM_TIMER ラベルが呼び出され、ラベル内のプログラムを実行します。

この case WM_TIMER ラベルをWindows API の、WM_PAINT などのある位置の前後に作る必要があります。


さて、case WM_TIMER ラベルが書けたら、


今度はそこに、さきほどの素早さの処理で作成した行動順配列の順番に基づき、戦闘に参加している各キャラが行動する必要があります。

コードは、書きのようなイメージになります。(イメージですので、下記のままでは動きません。)

case WM_TIMER:

if (mode_scene == MODE_BATTLE_NOW ) {			
			TimeCount++; // バトル時以外はカウントしない. 戦闘開始期はゼロになっている

				if (monster_alive == 1 && TimeCount >= 2 && timerFlag == 0)  // 自動進行の開始時に timerFlagをゼロにセットしておく
				{
					timerFlag = 1;
					ID = 0; // 戦闘参加キャラの通し番号. TimerFlag-1でも代用できるが、意味を考えて別の変数を用意した

					// 行動者が味方側の場合
					if (junjoHairetu[0] < partyNinzu) {
						
						heroside_attack(hWnd);

						InvalidateRect(hWnd, NULL, FALSE);
						UpdateWindow(hWnd);
					}

					// 行動者が敵側の場合					
					if (junjoHairetu[0] >= partyNinzu) {
						
						if (encount_mons_alive == 1) {
							enemy_attack(hWnd);
						}
						InvalidateRect(hWnd, NULL, FALSE);
						UpdateWindow(hWnd);
					}
				}

				if (monster_alive == 1 &&  TimeCount >= 4 && timerFlag == 1) 
				{
					timerFlag = 2;
					ID = 1;

					// 行動者が味方側の場合
					if (junjoHairetu[1] < partyNinzu) {

						heroside_attack(hWnd);

						InvalidateRect(hWnd, NULL, FALSE);
						UpdateWindow(hWnd);
					}

					// 行動者が敵側の場合					
					if (junjoHairetu[1] >= partyNinzu) {

						if (encount_mons_alive == 1) {
							enemy_attack(hWnd);
						}

						InvalidateRect(hWnd, NULL, FALSE);
						UpdateWindow(hWnd);
					}
				}
	


				if (monster_alive == 1 && TimeCount >= 6 && timerFlag == 2) 
				{					
					timerFlag = 3;
					ID = 2;

					// 行動者が味方側の場合
					if (junjoHairetu[2] < partyNinzu) {

						heroside_attack(hWnd);

						InvalidateRect(hWnd, NULL, FALSE);
						UpdateWindow(hWnd);
					}

					// 行動者が敵側の場合					
					if (junjoHairetu[2] >= partyNinzu) {

						if (encount_mons_alive == 1) {
							enemy_attack(hWnd);
						}

						InvalidateRect(hWnd, NULL, FALSE);
						UpdateWindow(hWnd);
					}

	                        }
				if ( TimeCount >= 9) {
					mode_scene = MODE_BATTLE_COMMAND; // コマンド画面に復帰するためのモード設定
					TimeCount = 0;
					timerFlag = 0;
					ID = 0;
				}
				InvalidateRect(hWnd, NULL, FALSE);
		} 	
		break;


説明の単純化のため、For文を使わないでコードを書いています。また、味方の死亡を無視しています。

敵の死亡だけ考慮してあるのは、戦闘を有限ターンで終了させるためです。本当は上記コードにさらに敵の死亡判定のためのコードが加わったり、敵全滅時の自動進行のコードが加わりますが、説明の単純化のために除去してあります。

そのほか、クリティカルヒットやミスなどの判定も無視しています。敵のAIも無視して、たとえば、敵の(味方への)攻撃対象は、あらかじめパーティ最前列の戦士だけを攻撃するように決めてあるとします。


さて上記コード例で「行動者が味方側の場合」と「行動者が敵側の場合」とでif文や関数を別々に分けている理由は、

参照先のデータベース(または構造体)が、それぞれ異なっているからです。

味方キャラのデータベースを参照するか、それともモンスターのデータベースを参照するかの違いがあるからです。

このように、文章だけなら一見すると同じ動作であっても、参照データベース(あるいは参照先の構造体など)が異なっていれば、実質的に別々の命令文になります。


上記コードの重要ポイントは、たとえば TimerFlag という変数を用意して、指定時間の経過ごとに case を呼び出した時に、このTimerFlag フラグと同じ番号(行動順配列での通し番号)のキャラだけが動くように設計している事です。通し番号の異なる別番号のキャラのIf文ブロックはマスク(遮蔽)されており、別キャラは行動しないようになっています。

そして、このTimerFlagが、行動順配列の各キャラの行動ごとに、TimerFlagの値が順番どおりに1アップしていくようにしておけば、よいのです(説明が難しい。要するに、こういう感じでRPG戦闘シーンでの自動処理が実装できる。実際に Visual C++ でやればワカル)。

戦闘中のモンスター隊列[編集]

さて、戦闘中のモンスター隊列の構成を定義するための配列(または同等の内容の数列)も、必要です。

まず、画面に敵を、表示したい順に表示するためも必要です。

また、上述の「素早さ順」での行動処理のためにも、前提として、敵パーティの構成を表すための配列が必要になります。


説明の単純化のため、敵は横一列に、左から右に、並んでいるとしましょう(ドラクエ2~3みたいな方式だとしましょう)。


たとえば敵が左から順に

ホブゴブリンが1匹、毒々スライムが1匹、ゾンビ兵士が1匹

出現したとしましょう。(説明の単純化のため、モンスターは各種類ごとに1体だけとする。)


そして、ホブゴブリンのモンスター用データベース中でのIDが13番だとして、 毒々スライムのIDが8番、 ゾンビ兵士のIDが25番、 だとしましょう。

すると、この敵パーティを表すための配列として、

{13,8,25,-99}

のような配列が必要です。(もしくは、配列に格納できるような数値の列「13,8,25,-99」)

末尾の-99は、これはその配列の読み終わりとして使用するための数字です。この数字は、別に「-10」でも「-99」でもいいですが、モンスターのidとけっして重ならないようにする必要があります。

通常、なんらかのデータベースのidの値は0以上の正の整数ですので、マイナスの整数を「終了」の意味で使用しておけば、安全でしょう。


このように、敵側にも、配列が必要になります。

しかも、この敵側の配列は、そのゲーム中で出現する敵パーティの全パターンを用意する必要があります。


たとい1体だけしか出現しない敵でも(たとえばボス敵や強敵)、その1体分の敵パーティの配列が必要です。

たとえば、その1体だけで出現する、あるボス敵「暗黒大王」のモンスター用データベース中でのIDが205番だとしたら、

{205,-99}

のような、敵1体だけの配列を用意する必要があります。


余談: マイナスを含む並び替え[編集]

なお、もしマイナスの数を含む数をふくめて並び替えをしたいのでしたら、 上記の「-99」など終了処理を負数で区別する方法は、そのままでは使えないです。

たとえば、方向でもし東向きをプラス、西向きをマイナスとした場合、終了コードのつもりの「-99」は、誤解で「西に99マス」などと誤解される恐れがあります。

たとえば、

-3 , 5 ,2, -6, -2 , 0 , 1

の並び替えをしたい場合を考えましょう。


解決策としては、いろいろあります。

解決策1[編集]

一番ラクな方法は、要素数を新たな変数として追加し、それと組み合わせて配列を使うことでしょう。


たとえば、

-3 , 5 ,2, -6, -2 , 0 , 1

は7個の数が並んでいるので、

youso = 7;

みたいに新たに変数を用意します。

そして、たとえば最初の「-3」から順番意読み取りの際に、数えおわった個数を+1していき、+7になったら読み取りを終了することです。

この方法は、拡張性が高いのでオススメですが、ただし、要素を書く前に個数を宣言する必要があります。

実際の配列データは

-3 , 5 ,2, -6, -2 , 0 , 1, 0 ,0 ,0 , 0 ,0 ,0 , (以下略)

のようになっているので、けっして数え終わって欲しいところでキリよく要素が終わるわけではないので、なので、要素数をあらかじめ宣言する必要があるのです。

さもないと、8番目の「0」が、読み取りたい数値としてゼロなのか、それとも、単にデータなしのためゼロなのか、不明になります。


終了コードなどとして負数を使用するのは、マイナスを含む並び替えでは、あきらめましょう。それが安全でしょう。


解決策2[編集]

最初の解決策1(要素数を新たな変数として追加)がもっとも安全かつ簡単ですが、比較のため、別の解決策も紹介しておきます。


次の解決策2は、数学的にはエレガントですが、しかしプログラミング的には、煩雑になり、また、バグ時などのエラーの波及をしやすい欠点があります。


さて、並び替えのもうひとつの解決策として、

一例は、符号と絶対値を分離して、その組み合わせとして処理することです。


たとえば、

-3 , 5 ,2, -6, -2 , 0 , 1

の並び替えをしたい場合、


プレイヤーからは表面的には「-6」は1つの数に見えまずが、これをあえて、「フラグhugouが状態2(マイナスに相当)である」「絶対値 zetai が6である」というように、2つの変数hugou と zetaiに分けます


符号がプラスなら、hugou = 1 にでもしておきましょう。また、ゼロは便宜上、hugou = 1 にしておきましょう。


すると、上記の数の並びは、(hugou, zetai)のベクトルで考えると、

(2,3) , (1,5) ,(1,2), (2,6), (2,2) , (1,0) , (1,1)

という組み合わせに変換するので、マイナスが使われない形になります。


なので、終了処理に、「-99」などの負数を割り当てても、並び替え対象の数値と重なる心配がなくなります。: (2, 6,) , (2, 3) , (2, 2) , (1,0) , (1,1) ,(1,2), (1,5)

(2,3) , (1,5) ,(1,2), (2,6), (2,2) , (1,0) , (1,1),(-99,0)

などのように、終了コード「(-99,0)」を末尾に追加しても、なんの混同の心配もなくなります。


さて、いったん上記のように、数を、符号と絶対値の組み合わせに分解して、同じ符号どうしで並びかえたなら、

あとは、符号が同じものどうしで、並び替えをするだけですみます。


たとえば、

(2,3) , (1,5) ,(1,2), (2,6), (2,2) , (1,0) , (1,1)


の並び替えをしたい場合、ベクトルの第一引数に注目し、※ (第一引数、第二引数)

まず、

(2,3) , (2,6), (2,2) のグループ
(1,5) ,(1,2), (1,0) , (1,1)のグループ

に分けます。


そして、絶対値(例では第二引数)だけに注目すれば、

マイナス数の絶対値は 3,6,2 → 2,3,6 と並び替え → 逆順にして6,3,2 → マイナス符号をつけて -6,-3,-2
プラス数の絶対値は 5,2,0,1, → 0,1,2,5 と並び替え

となるので、簡単に並び替えできます。


あとは、これを合成し、

-6, -3 , -2 , 0 , 1 ,2, 5

と、並び替えできます。


なお、説明の都合上でベクトルを蒸気の解説で使ったが、しかしC言語にベクトルの機能は無いので、もし上記の機能を実装するなら配列や構造体配列を使って、ベクトルと似たような処理を実装することになる。

たとえば配列で実装するなら、符号用の配列 hugou[id] と、絶対値用の配列 zetai[id] のようなものを用意したりすることになるだろう。

もし1番目の最初の数が「-3」なら、符号フラグは2、絶対値は3だからベクトル表現では

(2,3)

であるが、これは配列で表現するなら、たとえば

hugou[0]=2; zetai[0]=3;

のように記述できる(C言語では配列の番号は0から数える)。


欠点

この方法は、一見すると数学的にエレガントに見えるかもしれませんが、しかし、もしバグやタイプミスなどによって数え間違えると、数え落としや重複があると、以降の符号が1個ずつズレてしまうなどの大きな影響があります。

たとえば、説明のため上記では

-3 , 5 ,2, -6, -2 , 0 , 1

の分解を

(2,3) , (1,5) ,(1,2), (2,6), (2,2) , (1,0) , (1,1)

とマトメて書きましたが、 実際には

hugou 2,1,1,2,2,1,1
zatai 3,5,2,6,2,0,1

のように別々の変数に分かれて保管されるのです。

もし、たとえば hugou の最初に、バグなどで「1」が加わると、

hugou 1,2,1,1,2,2,1,1
zatai 3,5,2,6,2,0,1,0

のようになってしまい、すべての符号が1個分、ズレてしまい、

3, -5, 2, 6 ,-2 , 0, 1 ,0

となってしまいます。

正しい元々の数字の並びの

-3 , 5 ,2, -6, -2 , 0 , 1

と比べると、符号がいくつも間違っており(1個目と2個目と4個目が違う)、解決策2ではエラーの波及が大きいことが分かります。

  1. ^ たとえば欧米製のフリーのRPG 『Valyria Tear』(2020年2月13日に閲覧して確認) では同ゲームを「J-RPG」と謳っているが、どこにも日本風の萌え美少女アニメのようなデザインの女性ヒロインなんて出てこない。