ゲームプログラミング/RPG/戦闘
戦闘中のアルゴリズム
編集素早さ順行動のアルゴリズム
編集ターン制RPGでは、戦闘中、敵と味方が入り乱れて、素早さ順に行動するシステムが、一般的です。
このアルゴリズムが、けっこう難しいです。
ドラクエ1のように、敵と味方が1対1の戦いなら簡単ですが、ドラクエ2以降や『ファイナルファンタジー』シリーズのように、味方が複数人、敵も複数匹の場合、それらの行動準の決定アルゴリズムは、けっこう難しいです。
さて、まずアナタのすべき事として、ドラクエ1のように敵と主人公が1対1のバトルのプログラムを、あなたの自作ゲームのソースコードに書いてください。それが今後の作業の出発点です。そして、その1対1のバトルのプログラムを実際に自作ゲームに組み込んで、とりあえずプレイ可能な状態まで持っていってください。(本セクションの主題ではないので詳細を省略しますが、この時点ですでに、色々な作業があります。コマンド入力後の戦闘自動化のためのタイマー処理など、実装するために調べる点は多くあるでしょう。その他、戦闘勝利や戦闘敗北などの実装もあります。)
1対1の戦闘の実装が出来たら、次に、その1対1のプログラムを拡張して 多数 対 多数 のプログラムに改造していきましょう。
では、そのような多数対多数での素早さ順行動アルゴリズムのための考えかたを述べます。
あるwiki編集者は、次のようなコードで、素早さ順の並び換えが可能だと主張しています。
#include <stdbool.h>
#include <stdint.h>
#include <stdlib.h>
typedef struct {
const char * name;
uint8_t speed;
bool is_enemy;
} character;
int compare_by_speed(const void* ca, const void* cb) {
return *((*character) cb->speed) - *((*character) ca->speed);
}
void sort_by_speed(character characters[], size_t len) {
qsort(characters, len, sizeof(character), compare_by_speed);
}
character[] get_characters() {
return {
{.name = "勇者", .speed = 20, .is_enemy = false},
{.name = "僧侶", .speed = 25, .is_enemy = false},
{.name = "戦士", .speed = 15, .is_enemy = false},
{.name = "魔法使い", .speed = 17, .is_enemy = false},
{.name = "ゴブリン", .speed = 21, .is_enemy = true},
{.name = "スライム", .speed = 5, .is_enemy = true}
};
}
void process() {
character charas = get_characters();
sort_by_speed(charas, 6);
for (size_t i = 0; i < len; i++) {
// 素早さが高いほうが先に来ているため、ここでいろいろやる
}
}
上記コードを書いた編集者が解説を加えないので、別のあるwiki編集者が解説を補います。
まず、上記コードにはmain関数が無いので、これだけでは動作しません。
また、変数 len が未定義なのでエラーです。
試しにLinuxの Fedora 35 のg++で上記コードを実行したら、下記のようなエラーメッセージが出ました(2022年1月24日にg++ (GCC) 11.2.1 で実行)。
sort.c: 関数 ‘int compare_by_speed(const void*, const void*)’ 内: sort.c:12:23: エラー: expected primary-expression before ‘)’ token 12 | return *((*character) cb->speed) - *((*character) ca->speed); | ^ sort.c:12:24: エラー: expected ‘)’ before ‘cb’ 12 | return *((*character) cb->speed) - *((*character) ca->speed); | ~ ^~~ | ) sort.c: 大域スコープ: sort.c:19:10: エラー: structured binding declaration cannot have type ‘character’ 19 | character[] get_characters() { | ^~ sort.c:19:10: 備考: type must be cv-qualified ‘auto’ or reference to cv-qualified ‘auto’ sort.c:19:10: エラー: empty structured binding declaration sort.c:19:13: エラー: expected initializer before ‘get_characters’ 19 | character[] get_characters() { | ^~~~~~~~~~~~~~ sort.c: 関数 ‘void process()’ 内: sort.c:31:22: エラー: ‘get_characters’ was not declared in this scope; did you mean ‘character’? 31 | character charas = get_characters(); | ^~~~~~~~~~~~~~ | character sort.c:32:17: エラー: cannot convert ‘character’ to ‘character*’ 32 | sort_by_speed(charas, 6); | ^~~~~~ | | | character sort.c:15:30: 備考: initializing argument 1 of ‘void sort_by_speed(character*, size_t)’ 15 | void sort_by_speed(character characters[], size_t len) { | ~~~~~~~~~~^~~~~~~~~~~~ sort.c:33:26: エラー: ‘len’ was not declared in this scope; did you mean ‘mblen’? 33 | for (size_t i = 0; i < len; i++) { | ^~~ | mblen
uint8_t という型は、ヘッダ stdint.h をインクルードすることで使用可能になる仕様です。下記にあるuint8_tのコード例は、gccでなら実行できます。
ただし、uint8○○ などの類の型は windowsでのサポート状況に疑問があります。実際、Windows7 の Visual Studio 2019 で、gccならビルドできる下記の uintのプログラムをテストしてみたら、エラーになりビルド失敗しました(2022年1月26日に実験)。
コード例
#include <stdio.h>
#include <stdint.h>
int main(void) {
int a = 3; // 比較用
uint8_t b = 7;
printf("この数は %d\n", b);
}
実行結果
この数は 7
マイクロソフトの公式ドキュメントにuint8○○ などの類の型の紹介があるので、もしかしたら windows でも新しめの一部のバージョンでなら使用可能かもしれません[1]。
なお、Windowsにはこれとは別に __int8 というのも存在します[2]。
上記コードでは、構造体によってモンスターと味方パーティとが同一の構造体の配列に収まっていますが、実際のゲームではそのような事例は稀(まれ)でしょう。実際は、味方パーティのデータベースは戦闘以外でもアクセスすることから事前に存在しているので、その既存のデータベースをもとに、戦闘用の素早さ順の処理のための配列を作ることになるでしょう。
関数を使っていることもありコードは短いですが、しかし関数を使っているので、関数の「オーバーヘッド」というものにより処理速度は若干ですが低下しています。従って、そのような目的には適さないでしょう。
ポインタを使った処理は、おそらく高速化の目的だろうと思います。しかし関数のオーバーヘッドによる速度低下と効果が打ち消しあってしまいます。
以上、上記コードの解説は終わり。
さて、別のあるwiki編集者は、次のようなことを主張しています。
日本のIT用語で、並び換えをすることを「ソート」と言います。特に自動的に多くのものを何らかの基準に基づいて並び換えする場合、「ソート」という言葉を使います。
文献『ゲームクリエイターの仕事 イマドキのゲーム制作現場を大解剖』によると、ゲーム業界でもソートという用語が使われており、この文献では「マージソート」や「クイックソート」と言う用語が紹介されています[3]。本来、「バブルソート」とは何種類もあるfor文によるソートのうちの一種です。クイックソートなど、バブルソート以外のソートがあります。
このようにソートは何種類もあるので、それらをひとまとめに「バブルソート」というのは厳密ではないですが、for文などを使って走査的にグループ内の要素全部を並び換えすることを慣習的に「バブルソート」と言いあらわすこともあります。「バブルソート」と言った場合、あるグループのものを並び換えする場合、主にfor文とif文などを使って、そのグループ内の要素全部を比較する方法です。
上記例のコードの場合なら、戦闘に参加している敵味方のメンバー全員を、素早さを基準に大小判定して、並び変えています。
このため、最低限必要なのは、
- for文、
- 大小の判定、
- 判定結果の保管、
です。
また、判定結果を保管するために配列が必要になります。
ソートの話
編集下記コードは、ゲームのではなく、高等学校の『情報』教科の教員研修用資料[4](※web公開されている)にあるJavaScriptでのソートのプログラム例ですが、おおむね他のどのプログラム言語でも、バブルソートの構造は、下記JavaScriptのコードのように二重のfor文でif文を囲ったものになります。
- ※ 高校側では、ソートの概念は紹介するが、ソートの実習までは習わないのが現状(2023年まではそう)。
- ※ 上記の高校教育の現状のため、このページで、ソートの一般例を解説する。
<script>
function selectionsort(a){
for(i=0;i<a.length;i++){
for(j=i+1;j<a.length;j++){
if(a[j]<a[i]){
temp = a[i];
a[i] = a[j];
a[j] = temp;
}
}
}
}
a = [7,22,11,34,17,52,26,13,40,20,10,5,16,8,4,2,1];
document.write(" ソート前 ",a,"<br>");
selectionsort(a);
document.write(" ソート後 ",a,"<br>");
</script>
- (※ これはゲーム用ではないし、そもそも引用元から一部を抜粋したものなので、これだけでは動きません。イメージを把握する目的で上記コードをお読みください。そもそもC言語ではなくJavaScriptのコードです。)
上記単独では動きません。下記コードの中に、上記コードを埋め込んでから、コードを拡張子,html で保存し(たとえば test1.html みたいなファイル名)、webブラウザでそもファイルを開いて見てください。それ以外の方法だと、必ずしも正常動作をしません。
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<title>Hello World!</title>
<script>
// ここを上記コードのscript内のコードに置き換える
</script>
</head>
</html>
- 実行結果
ソート前 7,22,11,34,17,52,26,13,40,20,10,5,16,8,4,2,1 ソート後 1,2,4,5,7,8,10,11,13,16,17,20,22,26,34,40,52
文部科学省の上記のコードでも、for文、if文、配列、を使っているのが分かるかと思います。
また、ソートをするだけなら、ポインタなどは使う必要がありません。文部科学省のコードでも、ポインタ的な機能は使っていません。もっともそもそもJavaScriptなのでC言語のポインタは存在しないのですが。
上記コードの意味を解説します。また説明の簡単化のためにコードの変数名を書き換えます。
- ※ もしかしたら、変数名とかミスなどで間違っているかもしれないので、読者は上記コードから確認してください。
<script>
function selectionsort(a){
for(kijyun =0; kijyun < a.length; kijyun++){ // kijyun = 基準の位置;
for(taisyou = kijyun +1; taisyou <a.length; taisyou++){ // taisyou = 比較対象の位置;
if(a[taisyou ] < a[kijyun]){ // もし置換の条件を満たしていたら・・・(ブロック内を実行)
temp = a[kijyun]; // あとで使うので、temp に一時保管してるだけ
a[kijyun] = a[taisyou ]; // まず、基準位置を比較位置にある数に置き換え
a[taisyou ] = temp; // 置換前に基準位置にあった数で、比較位置を上書きする
}
}
}
}
a = [7,22,11,34,17,52,26,13,40,20,10,5,16,8,4,2,1];
document.write(" ソート前 kubetu ",a,"<br>"); // ブラウザ確認時に文科省コードとの区別のため「kubetu」を追加
selectionsort(a);
document.write(" ソート後 kubetu ",a,"<br>"); // ブラウザ確認時に文科省コードとの区別のため「kubetu」を追加
</script>
一応、webブラウザで動作を確認してあり、上記の文科省コードと同じ結果になります。
上記コード以外のアルゴリズムでもソートは可能でありますが、おそらく上記コードのような例(文科省サイトが紹介している例)が、いちばん初心者には分かりやすい例だと思います。
最初の kijyun は、0から始まります。なぜなら、ソート対象の配列の第0項から読み取るので( 7,22,11,・・・の「7」のある最初の第0項のことです。 )、よって0から始まります。
さて、途中のソート中、第0項にある数値は、「7」から、別の変数に何度か変わります。7 → 5 → 4 →2 →1 のように、変わっていきます。なので、基準になっているのは、けっして「7」という特定の数ではなく、第0項という位置こそが基準になっており、その0項にある数をそれ以降の数と走査して比べて行っています。
0項め自身と比べても意味がないので、for(taisyou = kijyun +1; taisyou <a.length; taisyou++){
では、基準位置の次の項から走査を開始するように+1した対象からスタートします。
念のため、文部科学省の公開している他言語版(の研修資料でも確認してみましたが、同様の構造です。たとえばSwift版(iOS版)なら 『高等学校情報科「情報Ⅰ」教員研修用教材 swift版(第3章のみ) - 20200716-mxt_jogai01-000001169_004.pdf』(※ 2022年2月6日に確認. )。
他の言語でも、ソートは可能ですし、文科省サイトではVBA版とかドリトル版とか公開されています[5]。しかしさすがに昨今の一般のゲームはドリトルやVBでは制作しないと思われますので、省略します。
ソートのアルゴリズムは他にもありますが、しかし、どの方式でもソートはデバッグ(バグ修正まで含む)がとても難しいので、とりあえず上記の文科省コードのアルゴリズムを真似るのが良いと思います(デバッグ済みなので)。もし自分でアルゴリズムを考えようとすると、デバッグ(バグを直すところまで)に1週間とかの日数が掛かりかねないので、自作アルゴリズムは後回しにしましょう。ゲームが完成してから、そういう他のアルゴリズムの練習は、暇つぶしにやりましょう。
ゲームの話に戻る
編集さて、ゲームの話に戻ります。配列に保管するのは数値なので、その数値が何を意味するかは、事前に決めておかねばなりません。たとえば保管内容「0」なら主人公のキャラ、保管内容「1」なら主人公以外の最初の仲間のキャラ、のようにです。
たとえば、アリス、ボブ、クレアの3人が登場するゲームなら、0ならアリス(主人公)、1ならボブ、2ならクレア、という風に、キャラクターの対応するID番号を決めておきます。
また、戦闘ソートは、けっして「敵どうしだけ」、「味方どうしだけ」の比較でなく、敵と味方の両方を比較する必要があります。なぜなら戦闘に参加してるのは味方だけでなく敵も参加者だから、です。
たとえば、味方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の入った配列の呼び名をとりあえず「行動順配列」と言いましょう。ID番号を使わずにポインタでも処理だけなら可能かもしれませんが、説明が難しくなるのに本セクションの主題とは無関係なのと、商業ゲーム制作でも少なくとも仕様書などで敵モンスターには必ずID番号が登録されているのは有名な話なので、事前にID番号が仕様であるならそれを流用したほうが効率的でしょう。このID番号と仕様書の関係のように、ゲームの他の部分との擦り合せをしながらコードを書いていく必要があります。けっして「ほらこのコードで並び換えできる」とVisual C++ でのゲーム化確認すらしなくてコマンドプロンプトのコードを書いて終わりなのではありません。
ともかく、この行動順配列の内容がたとえば「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文を使って網羅的に行えばよいのです。
以上のように、ゲームプログラミングとは、段階的にアルゴリズムを組み立てていきます。実際に上記のような段階を一つ一つずつ、ゲームの実機上(たとえばVisual C++上プログラムやDXライブラリで開発したアプリなどで)で、確認していきます。
一般のプログラミングはどうか知りませんが、少なくともゲーム制作において、段階的にアルゴリズムをひとつひとつゲームとして妥当な動作かどうかを確認していかないことは、仕様変更などに全く対応できず、実用的な価値のない独り善がりなコードです。そういう独り善がりな人は大学などにスッこんでてください。
さて、上記コード例の場合、そのときの行動順配列で2番目と5番目のキャラを素早さ比較しており、もし素早さが高ければ、そのキャラを先にもってきています。
ただし、配列の「2」番と「5」番なので、日常の数え方では3番と6番のことです(配列は0から数え始めるので)。さて、for文の走査は単に、w:リーグ戦方式で、単に片っ端から走査していけばいいだけです。この方法をw:バブルソートといいます。
- より実際的な場合
実際には、戦闘に参加している自陣営パーティのキャラが、必ずしもID順に並んでいるとは限りませんので、そのために復号する処理が必要です。たとえば、ドラクエ3やウィザードリィ・シリーズでは、登録した順番と、パーティのキャラクターの隊列の前後の順番とは違います。
つまり、たとえば、並び替え処理用で味方「1」番とされたキャラが、実際に登録IDが「13」番のキャラだったりするような場合もあるわけです。 並び替え処理用の味方2番のキャラは、たとえば登録IDが7番かもしれません。
なので、
- 1→13,
- 2 → 7
のような、並び替え番号から登録ID番号への、復号の処理がさらに必要です。
その復号処理の際、並び替え配列には敵と味方が混在していますので、味方の番号だけを復号処理する必要があるので、そのための配列も追加で必要になりす。
そのため、復号処理中に扱っている対象が敵か味方かを記録しておくフラグ記憶用の配列が必要になります(下記コードでは PorEflag 配列。Party or Enemy のフラグ(flag)という意味)。あらかじめ、その配列もグローバル変数などとして用意しておく必要があります。
よって、上記の「// 行動順配列の入れ替え」などの一連のプログラムの後に、さらに、たとえば次のような敵味方の判別用フラグのための配列作成が必要になります。
for (int temp = 0; temp <= partyNinzu - 1 + enemyNinzu; ++temp) {
// 行動順配列で走査中の要素が味方キャラの場合
if (junjoHairetu[temp] <= partyNinzu - 1) {
PorEflag[temp] = 1; // 味方ならフラグ1にセット
}
// 行動順配列で走査中の要素が敵の場合
if (junjoHairetu[temp] > partyNinzu - 1) {
PorEflag[temp] = 2; // 敵ならフラグ2にセット
}
}
なお、復号は、並び替え処理側の関数でなくとも、その関数(並び替え処理関数)の呼び出し側関数(「並び替え処理関数」とは別の関数)でも可能です。呼び出し側の関数の都合によって復号のコードは違うので、復号のコードの具体例の詳細については省略します。
この復号も、けっこう難しいプログラムになり、初心者にはハードルが高いですが、頑張りましょう。
一般にRPGにおいて、戦闘に参加するのはパーティとして引き連れているキャラクターですので、 つまり、ソート結果をもとに適切なパーティ加入中キャラを呼び出し、さらにそのパーティ加入中キャラのIDをもとに内部データベ-スの登録キャラのIDにアクセスするという手続きが必要になり、これまた難問です。いやまあ、配列で書けば、せいぜい
登録データベース配列[パーティ配列[ソート結果配列[temp] ] ]
のような入れ子になった配列のコード1つで済むのですが、しかし実際にこれをプログラミングしようとすると、デバッグ中に入れ子になった各パラメータを追跡する手間があるので、けっこう大変です。よって「酒場」・「冒険者ギルド」方式への対応は、初心者には敷居が高いです。
このセクションの説明の主題であるソートやバブルソートとは異なりますが、ゲームでは戦闘中にスキルなどで素早さなどのパラメータを一時的に上昇させたりなどの、スキルや魔法などを使うキャラクターや敵などもいます。 たとえばファミコン版ドラクエ3の時点(1980年代後半)でもう、パーティ全員の素早さを上昇させる呪文「ピオリム」および敵全員の素早さを下げる呪文「ボミオス」があります。
ゲーム開発では、そういった仕様のことも考えて、コードを書かなければいけません。
このため、けっしてデータベースどうしの数値を直接に比較するのではなく、 そうではなくて、まず戦闘用の新しい配列にその戦闘中だけ有効な一時的パラメータを書き込み、 そしてその一時的パラメータとしての一時的素早さの数値どうしを比較するなどの演算を行うという必要があります。
また、この一時パラメータ化の作業の手段として、味方の素早さと敵の素早さをひとつの配列にまとめれば、 参照先のデータベースの異なるものを同一の配列変数で扱えるようになるので一石二鳥です。
配列にまとめる前は参照先データベースが異なっていたのですから、ここではまったく構造体にする必要性はありません。通常の配列で十分です。
特に「素早さ」だけ、バブルソートのために走査的に全員を比較する必要があるなど、使い方が特殊です。
ゲームでは通常「攻撃力」や「防御力」をバブルソートすることはありません。 少なくともスーファミ時代のドラクエやファイナルファンタジー、現代のツクールやウディタのサンプルゲームはそうです。
このように、ゲーム開発にはゲーム特有の事情があります(戦闘中だけ有効なスキルがある等)。 そういった事も考えて、第三者にも使いやすくて分かりやすいコードを書かなければならないでしょう。
また、素早さ変更スキルによって1ターン中にすら順番が変わりうるのです。 だからまず、追加するべきフラグとして、結局はターン中に1体ずつ、そのキャラが行動済みかどうかを判定するためのフラグも必要になります。
そして、素早さが戦闘中に変更されたら、行動順を再計算する必要にせまられます。 そして再計算後は、行動済みフラグがオンになっている行動済みキャラに順番が回ってきても行動スキップする、というようなコードも必要になります。
ゲームプログラミングは、こういった仕様の変更や追加を見越して、いろいろと対応しなければなりません。 だから、段階をひとつずつ確認していくコードを書いていく必要があるでしょう。
だから、たとえデータベース同士の素早さを直接に敵と味方とも比較するアルゴリズムが思いついても、 それは価値が乏しいのです。
また、戦闘中の一時的なパラメータを素早さ以外の攻撃力などもまとめて構造体に出来たとしても、 実用的な価値があるか、疑わしいです。
ゲームの開発はけっして競技プログラミングではないので、ゲームとしての仕様とのすり合わせを出来ていないアルゴリズムには、価値が無いのです。また、こういうゲーム独特の順序への介入があるので、情報工学的な教科書にあるソートのコードを丸写しするような態度は無駄ですし、邪魔ですので、捨て去るべき理論です。
なお、上記のように戦闘中の一時パラメータ化のそのほかの副産物としては、戦闘中に誤って通常時の素早さパラメータなどを書き換えてしまうミスを、未然防止できます。
戦闘中の各ターンの進行の自動化
編集戦闘で各ターンのコマンド入力が終わったら、今度は、自動的にそのターンの戦闘が始まらなければいけません。
そして、1~2秒くらいおきに、たとえば
- 「戦士の攻撃!ゴブリンに5のダメージを与えた!」 (0~2秒のあいだ表示)
- 「ゴブリンの攻撃!魔法使いは2のダメージを受けた!」(2~4秒のあいだ表示)
- 「魔法使いの攻撃!ゴブリンは4のダメージを受けた!」(4~6秒のあいだ表示)
みたいに秒の経過によってメッセージが自動進行する必要があります。
- ※ 説明の単純化のため、全員、通常攻撃で肉弾攻撃だとします。
このような自動進行の処理のため、WindowsAPIならタイマーを呼び出す必要があります。タイマーは、オペレーティングシステムが管理しています。(DXライブラリでは方法が異なるが、とりあえず説明の都合で、WindowsAPIの場合を先に説明する。)
オペレーテイングシステムの種類によって、タイマーの呼び出し方は異なります。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++ でやればワカル)。
カウンター攻撃に対するカウンター攻撃の問題
編集実装はだいぶ先になるでしょうが、たとえばドラクエの「マホカンタ」や、ファイナルファンタジーの「リフレク」のように、攻撃をカウンターするスキルがある場合、
どのゲームでも、一度カウンターされた攻撃を同一ターンの同一攻撃にて再度カウンターすることは、プログラミング的な事情により不可能です。
背理法で考えると分かります。
つまり、仮に、カウンター攻撃に対しての、さらなるカウンター攻撃が可能だと仮定しましょう。
そして、敵パーティ・味方パ-ティの双方がともにカウンタースキルを発動していたとして、誰かの攻撃にカウンターに対してカウンターが必ず発動するとしましょう。
説明の簡単のため、味方から敵に攻撃開始したとしましょう。
すると、まずは
- 味方は敵は攻撃しようとするが、しかし反射する仕様なので、攻撃を食らうことになるのは味方自身 (段階A)
となります。
しかし、決してここで終わりではなく、背理法の仮定により、
- 味方自身に跳ね返ってきた攻撃が、さらに跳ね返り、敵へと向かう、 (段階B)
ことになります。
当然、さらに背理法の仮定により、
- 敵へ跳ね返ってきた攻撃が、さらに味方に跳ね返る、 (段階C)
となり、攻撃がこのあと味方に向かうので、段階Bがこのあと起きます。
よって、段階Bと段階Cとを永遠に繰り返すことになってしまいます。いわゆる「無限ループ」です。
このため、つまり無限ループ防止のため、普通のRPGではカウンター攻撃は絶対に1回しか反射しません。
- 等比数列の例え
もし数学に例えると、高校数学における、公比(-1)の等比数列が絶対に収束しない問題と同じです。
だから、どうしても何度でも反射する仕様にて、カウンターの攻撃対象を収束させたいなら、公比の絶対値を1未満にすれば原理的には可能です。
しかし、ゲームでそこまでする必要があるかどうか。
脚注
編集- ^ 『基本データ型』2022/01/04 ※自動翻訳 2022年1月24日に確認
- ^ 『データ型の範囲』2021/12/01、※自動翻訳 2022年1月24日に確認
- ^ 蛭田健司『ゲームクリエイターの仕事 イマドキのゲーム制作現場を大解剖』、翔泳社、2016年4月14日 初版 第1刷 発行、P90
- ^ 『高等学校情報科「情報Ⅰ」教員研修用教材 JavaScript 版(第3 章のみ) - 20200716-mxt_jogai01-000001169_001.pdf』
- ^ 『第3章 他プログラミング言語版:文部科学省』 2022年2月6日に確認.