ゲープロ講座セッション8:戦術SLGの移動アルゴリズム(6-2)

- 敵のルーチンの実装
Article Written: 2000/3/30




 はい、ここから後半です。前半部分は理解できましたか?ソースを追って頭が疲れてたり、もしくは前半部分がよく分からないというのだったら、以下を読むのはもうちょっと後にしてからの方が良いかもしれませんよ。大丈夫ですか?
 大丈夫ということなら、続いていきましょう。



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) の最短移動へと続くわけです。
 ……じっくり構えてみれば、「なるほどなー」という感じだと思います。ここのロジックの立て方で、いろんなバリエーションを持たせられるわけです。



S-6) 敵さん最短移動

procedure TForm1.EnemyAutoMove(Sender: TObject);
begin
  // S-6) 敵さん側最短移動 --------------------------------------------------
    if copy(egostr,goflag,1)='0' then begin
      goflag:=0; stat:=7;
      dmt:=0; tmx:=mx; tmy:=my;     
      MapPrint(Sender);
      GW1.FloaterBGput(Epath+'filter',1,mx*32,my*32,20);
    end;
    if copy(egostr,goflag,1)='1' then dec(ey[enum]);
    if copy(egostr,goflag,1)='2' then inc(ex[enum]);
    if copy(egostr,goflag,1)='3' then inc(ey[enum]);
    if copy(egostr,goflag,1)='4' then dec(ex[enum]);
    inc(goflag);
    SndPlaySound(PChar(Epath+'kan.wav'),SND_ASYNC);
    if goflag>golast then begin
      goflag:=0;
      dmt:=0; tmx:=mx; tmy:=my;
      stat:=7;
    end;

  StatusPrint('Enemy-'+InttoStr(enum)+' Moving..');
  MapPrint(Sender);

  // フィルタ表示
  GW1.FloaterBGput(Epath+'filter',1,mx*32,my*32,20);

  if (stat=7) then begin
    if enum<=4 then begin
      enum:=enum+1; stat:=5; tmx:=mx; tmy:=my; slide:=1; dmt:=0;
    end else begin
      enum:=1;
    end;
  end;

end;

 ここは本質的には、S-4) プレイヤー最短移動と変わるものはありません。ただし、移動地点までの距離が0であったか、もしくはnomoveと決めていた場合は、アニメーションを行わせずに終了という例外を設けています(目標値へは距離があるけど、)。もっともよく見ると分かるのですが、この処理は書かなくても大丈夫なようになっています(^^;)。ま、心づけというやつですね。(単に消し忘れていただけだったり……)
 アニメーションが終わったら、enumを加算しています。敵A〜Eの処理すべてが終わったらstat=7に、そうでないならstat=5に戻って次のキャラに処理を移すようになっています。
 このstat=7は、ソースファイルを見てください。単にスポットを敵から主人公キャラに戻しているだけですので、説明は不要と思います。

 これで、このサンプルアプリケーションの解説は終了となります。お疲れ様でした。……理解できましたか?実際のところ、一回でぱっと分かったら凄いと思います。すぐ分からないのが当然ですので、反復して理解していただけたらなと思います。



 サンプルアプリを動かして、敵がしっかりチョコチョコ追ってくるのを確認し、ソースファイルを見てロジックを理解できれば(セッション6で書いた流れの通り実装している事も確認しましたか?)、もう難しい事はないと思います。あとは、最終目標である「攻撃とダメージ処理」を足しつつ、上手くいけば複数プレイヤーキャラに対応できる処理を書く。これで、一通りのSLGルーチンができあがり、という事になりますね。これについて色々書きたいのですが、今回はずいぶん詰め込んでしまったと思うので、次回ゆっくりと考えていく事にしましょう。



 はい、今回の講義はいかがでしたか?攻撃のルーチンがまだとはいえ、だいぶ実際のSLGシステムに近くなってきたのではと思います。セッション6の要件をただ実装しただけとはいえ、やはり現物とソースが出てくると違いますかね?
 そういえば敵の移動地点をスコアで図る手法、これって完全に人工知能の一分野なんですね。最初に「日本音響学会に論文を提出」と書いていましたが、これも人工知能関連で、「話題をスコア付けしつつ推定していく」という似たようなものだったりします。ゲーム研究も学術研究も、本気になると紙一重の差なのかもしれません。
 次のセッション9は、現物はナシということで、そんなに時間はかからないと思いますが、なにせ新年度(4月)ということで、いろんなゴタゴタがあります。同人誌の原稿も描かなくちゃいけないということもあり、ある程度待っててくださいな。感想、特にこのロジックを使おうと思っている方からの反応をお待ちしています。
 しかし鷹月は、プログラマーから引退しようと思ってるのですが、(ゲームデザインに腰を据えるため)ここ最近プログラムしっぱなしですね。ふーやれやれ。

- 鷹月 ぐみな



 Session9はしばらくお待ちください〜



カレッジの入り口に戻る
鷹月ぐみな情報局2号館

Written by. gumina(鷹月 ぐみな)