S-5) 敵さん最適探索
procedure TForm1.EnemyThinking(Sender: TObject);
var i,j,hirank,rankwait1,rankwait2,rankwait3,per,nomove:integer;
begin
// S-5) のはじめにやらなければいけないこと -------------------------------
// 座標スライド:現在のmx,my から、ex[1]-9,ey[1]-7に向けてスライドする
// 本当はドット単位でスライドさせるべき
if slide=1 then begin
mx:=Trunc(tmx/10*(10-dmt)+(ex[enum]-10)/10*(dmt));
my:=Trunc(tmy/10*(10-dmt)+(ey[enum]-8)/10*(dmt));
dmt:=dmt+1;
StatusPrint('Scroll Wait..');
MapPrint(Sender);
if dmt>=11 then slide:=0;
exit;
end;
// S-5) 敵側最適探索 -----------------------------------------------------
// 移動可能範囲検索→思考
mx:=ex[enum]-10; my:=ey[enum]-8; // マップ位置の切り替え
// 敵の移動可能地点配列のクリア
for i:=1 to 400 do begin
ecango[i].x:=-1; ecango[i].y:=-1;
ecango[i].rank:=0; // 得点付け(あとで重要になる)
end;
ebanpei:=0; nomove:=0; hirank:=600;
EnemyRangeCheck(Sender);
// mvrecの書き込まれた地点は選択肢候補
for i:=0 to 255 do for j:=0 to 255 do begin
if mvrec[i,j]<>'' then begin
ebanpei:=ebanpei+1;
ecango[ebanpei].x:=i; ecango[ebanpei].y:=j;
end;
end;
// プレイヤーとの単純距離差を測る
per:=abs(px-ex[enum])+abs(py-ey[enum]);
// ***** ここが、敵さんごとの思考ルーチン *****
if (enum=2) and (per>10) then nomove:=1;
if (enum=3) then begin
if (per>3) and (mf1=0) then nomove:=1;
if (per<=3) then mf1:=1;
end;
if (enum=5) and (per>10) then nomove:=1;
if nomove=1 then begin
egostr:='0'; goflag:=1; golast:=1;
end else begin
// 移動全地点における距離差からランクを計算、高い所を目標値とする
if enum<=3 then begin
// 敵A〜Cはこのルーチンで使える
hirank:=600;
for i:=1 to ebanpei do begin
rankwait1:=abs(ecango[i].x-ex[enum])+abs(ecango[i].y-ey[enum]); // 自分との距離
rankwait2:=abs(ecango[i].x-px)+abs(ecango[i].y-py); // プレイヤーとの距離
ecango[i].rank:=rankwait2*6+rankwait1; // ランク付け
if hirank>ecango[i].rank then hirank:=ecango[i].rank; // ハイランク更新
end;
end;
if enum=4 then begin
// 敵D(mf2,mf3からの単純距離差6以下である位置でしか動かない)
hirank:=600;
for i:=1 to ebanpei do begin
rankwait1:=abs(ecango[i].x-mf2)+abs(ecango[i].y-mf3);
rankwait2:=abs(ecango[i].x-px)+abs(ecango[i].y-py);
rankwait3:=abs(ecango[i].x-ex[enum])+abs(ecango[i].y-ey[enum]); // 自分との距離
if rankwait1<=6 then ecango[i].rank:=rankwait2*6+rankwait3
else ecango[i].rank:=600; // ランク付け
if hirank>ecango[i].rank then hirank:=ecango[i].rank;
end;
end;
if enum=5 then begin
// 敵E(単純3になる場所を確保しようとするルーチンはここ)
hirank:=600;
for i:=1 to ebanpei do begin
rankwait2:=abs(ecango[i].x-px)+abs(ecango[i].y-py);
rankwait3:=abs(ecango[i].x-ex[enum])+abs(ecango[i].y-ey[enum]); // 自分との距離
if rankwait2=3 then ecango[i].rank:=1+rankwait3
else ecango[i].rank:=rankwait2*6+rankwait3;
if hirank>ecango[i].rank then hirank:=ecango[i].rank;
end;
end;
egostr:='0'; goflag:=1; golast:=1; // 番兵的処置
for i:=1 to ebanpei do begin
if ecango[i].rank=hirank then begin
egostr:=mvrec[ecango[i].x,ecango[i].y]; // 目的地決定
golast:=Length(mvrec[ecango[i].x,ecango[i].y]);
goflag:=1;
break;
end;
end;
end;
StatusPrint('Enemy-'+InttoStr(enum)+' Thinking..');
MapPrint(Sender);
stat:=6;
end;
|
前のページまでは、セッション3のサンプルアプリでも既に実装して、なおかつ汚いながらもソースですでに書かれていた事ばかりでした。今回のサンプルアプリの主役はこの S-5) と S-6) です。さすがにちょっと長めですね。分かりやすいように全体を4つに区切ってみましたので、それに従って説明していきますね。
まず、最初のパラグラフは、S-5) に入れていますが、敵の探索とは関係はありません。マップのスポットを、現在の位置から、行動を起こす敵さんの場所までズリズリと移動させる、という処理を行っています。次のパラグラフでもまた座標を設定しているとおり、この部分は無くても支障は出ません。しかし、スポットがずりずり移動せずに、「パッ」と切り替わったらどうでしょう?A→B→C→D→Eと切り替わっていくわけですが……正直眼が痛くなります。よってこういう配慮が必要なんですね。
具体的なロジックは、移動前移動後の座標を10分の1ずつスライドさせるという手法を取っています。もっとも、アプリを実行してみれば分かりますが、マップチップ単位で移動させているので、ちょっとガタガタが激しいです。これは意図的で、次回のサンプルアプリではドット単位のスクロールに変えておこうと思います。
2番目のパラグラフから、いよいよ思考ルーチンに入っていきます。最初になにやら配列ecango[i]の値を初期化しています。その後すぐに、mvrecに何かしら文字が書かれていた場所(=それは移動可能であることを示します)を調べ、その座標情報をいま初期化したばかりのecango[i]配列にリスト的に保管しています。ebanpei という変数にはその総数が保存されるわけですね。
そしてこの時点で、敵さんの移動可能範囲を探索するEnemyRangeCheck() を呼んでいます。先程のプレイヤー版のRangeCheck() と殆んど処理は同じだったりしますが、一応ペーストしておきますね。
procedure TForm1.EnemyRangeCheck(Sender: TObject);
var i,j:integer;
begin
// フィルターを全解除
for i:=0 to 255 do for j:=0 to 255 do begin
GW1.BGF[1,i,j]:=1;
mvrec[i,j]:='';
end;
// モンスターの位置から、移動距離 emv 分への再帰探索をはじめる
// 実はこのemvは過ち。本当は10くらいで固定すべき
Arch(ex[enum],ey[enum]-1,emv,1,'1'); Arch(ex[enum]+1,ey[enum],emv,2,'2');
Arch(ex[enum],ey[enum]+1,emv,3,'3'); Arch(ex[enum]-1,ey[enum],emv,4,'4');
// モンスターは、同じ位置のチェックを許される
mvrec[ex[enum],ey[enum]]:='0';
GW1.BGf[1,ex[enum],ey[enum]]:=0;
end;
|
単にpx,py,pmvをex,ey,emvに置き換えただけなので、ロジックの説明は不要だと思います。ただ、ちょっと注意コメントが入っていますね。プレイヤーの移動歩数値pmvと同じようにemvを当てはめるのは実は誤りなのです。なぜなら、セッション4でも書きましたが、探索歩数が短くなると、正しい動きをしてくれなくなるからなのです。よって、あくまで探索は10歩くらい(多い方が良い)で行い、後で移動できる部分移動できない部分を分離すれば良いだけなのです(BGfのフィルタをオフにしておき、それを使って判定させればいいわけです)。
ただ、ロジックがまたややこしくなるので、今回は探索歩数6、移動可能歩数6と同じに設定しておきました。実際にSLGを作ろうと思っている人は、ここに注意してくださいね。
S-5)の第2パラグラフに戻りましょう。ここの最後にて、敵さんの現在の位置と、プレイヤーの位置との単純距離差を測り、変数に保管しています。これらは次のパラグラフ以下の推論情報となっていくわけです。特に、移動可能座標を ecango[i] という配列にリストで保存する部分が重要です。これをしなくても、直接mvrecから取り出す事は可能ですが、そのままだと二次元配列であるために面倒な手間が必要だったんですね。
お待ちかね第3パラグラフが、敵A〜敵Eの最適探索の心臓部となります。今回は敵それぞれにバラバラでしたが、実際には「移動パターン」が先にあって、敵を用意する場合、どのパターンにするかという割り当てをすることになるのでしょう。
最初に、「敵が動かない条件」だけ調べています。enumは敵の番号A〜Eと対応していた事を思い出しましょう。
「現在の対象が敵さんBで、主人公との単純距離差が10より大きい場合」、
「現在の対象がCで、単純距離差が3以上かつ一度も3以下に接近されていなかった場合」、
「現在の対象がEで、主人公との単純距離差が10より大きい場合」
それぞれ、こういう事を言っているわけです。nomoveフラグが立った場合は、1歩も動かないように設定をして、以下のパラグラフを無視します。なお、敵さんDはやや特殊だったので、ここでの規定はしませんでした。
ロジックの設計や見た目の問題からいけば、正直こういう狭め方はあまりよろしくありません。他人が見て分かりにくいですからね(^^;)。望ましいのは、パターンごとに完全にロジックを区切る方法でしょう。但しロジックのソースは同じものが出てきて、長くなります。今回はあまり長いソースは嫌だったので、圧縮してしまったわけですね(^^;
この後は、敵A・B・Cのカテゴリ、敵Dのカテゴリ、敵Eのカテゴリと分けて個々に最適探索ロジックを通しています。今回のアプリの一番の核は間違いなくここでしょう。「こういう風に書けば敵の移動ロジックが書けるんだ〜」と頭に入れておいてくださいね。
敵A・B・C共通ロジック。移動可能な地点すべてに対して、「自分との単純距離(式1)」および「プレイヤーとの単純距離(式2)」を算出します。その結果、「どの地点に行くのが最も望ましいのか」という重みを、「式1+式2×6」でスコア付けしています。この値は小さくなるほど望ましい場所ということで、ハイスコア(低い方を更新するので、ロースコアと言うべき?)を更新していくというものです。このように、全地点において得点を付けることで、移動優先順位というのを選んでいるわけです。ちなみに、スコアの高い地点=「プレイヤーからもっとも遠い地点」=逃げるに適した場所、ということになります。
敵Dのロジック。「ピボット地からの距離(式1)」、「プレイヤーとの距離(式2)」、「自分との距離(式3)」において、式1が6以下となる条件(つまり、ピボットの中の範囲を差す)を満たしたときに、ランクを式3+式2×6で計算(点数そのものはさっきと同じですね)しています。
敵Eのロジック。「プレイヤーとの距離(式2)」、「自分との距離(式3)」において、式2=3である場所だけ点数を「低く見積もり」、そうでない場所は普通どおりとします。
こうして、最後の第4パラグラフへ。ハイスコアとして記録されているスコアを持っている、最初の移動可能場所を取り出し、そこを目標値と決定します。こうして最適移動アルゴリズムを終了し、S-6) の最短移動へと続くわけです。
……じっくり構えてみれば、「なるほどなー」という感じだと思います。ここのロジックの立て方で、いろんなバリエーションを持たせられるわけです。
|