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

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




 みなさんこんにちは、鷹月ぐみなです。1ヵ月半のご無沙汰になりましたが、いかがお過ごしでしたでしょうか。私はいつものように業務をしながら帰宅後に創作活動といった感じでしたが、少しばかりハードだったように感じます。社員代表でスピーチしてみたり、仕事と全然関係ない所で日本音響学会に論文を発表してみたり。創作ではサークルで小さなACTゲームもリリースしました。こんな感じですから、講座の続きを書くことは実は容易ならざるものがあるのですが(^^;)、あまり間を開けすぎちゃいけないね、という事で強行しました。SLG編もあと3回、内容もやや高度になってきてますが気張っていきましょう。



 それでは始めましょうか。前回とは密接に関わっているので、「あれ、前回何やったっけ?」という方は見直して復習しておいてくださいね。
    今回のアプリケーションの範囲
    S-1) 初期設定
    S-2) [Player's Turn] ユニット選択(但し1キャラのみ)
    S-3) [Player's Turn] 移動先座標選択
    S-4) [Player's Turn] プレイヤー最短移動
    S-5) [Enemy's Turn] 最適探索
    S-6) [Enemy's Turn] 敵さん側最短移動、そして S-2)に戻る
 そう、戦術SLGルーチンの基本系を抑えようということで、まずはその中間アプローチとして、上に示したような流れを持つサンプルアプリを作りましょう、っていう事でしたね。それで、今回ははじめに現物を示す約束をしていましたね。という事で、お見せしましょう。

SLG移動サンプルアプリケーション2
ダウンロードする(180K程度)
∇ ソースファイルだけ見たい人はこちらから

 動かせる環境にある方は、さっそくDLして動かしてみてください。内容的にはセッション3のサンプルに敵の移動が付加されただけで、センセーショナルな違いというのはないのですが、それなりに相手がプレイヤー目指してちょこちょこ動いてくる所なんか、「おおっ」と思ってしまうかもしれません。ソースは完全にセッション6で決めた流れに沿って実装しています。
 えっと、敵の動き方はこんな感じに定義しましたね。ちゃんとその通りに動いているかどうか確認してみてください。

    敵さんA:猪突猛進型(ひたすらプレイヤーを追いかけてくる)
    敵さんB:通常型(PCとの単純距離差が10以内なら近づいてくる)
    敵さんC:守備型(PCとの単純距離差が3以内になった所で近づき、以後は距離に関係なく常にプレイヤーを追いかけるA型になる)
    敵さんD:ピボット型(ある地点から単純距離半径6以上の地点へは何があっても進めない。その範囲の中でプレイヤーに近づこうとする)
    敵さんE:アーチャー(単純距離差が10以内で近づくのだが、主人公からの単純距離差が3になる地点を確保しようとする)
 ある程度試したなと思ったところで、次へ進む事にしましょう。今回はソースを追いかけながら説明していくので、それを理解するためにも、アプリの動作は頭に入れておいた方が吉です。



 以下、Delphiソースの解説です。HSPなど他の言語を常用している方には、「なんだかよく分からんムー」とか拒絶感を持ってしまうかも知れませんが、見た目の体裁が違うだけで、アルゴリズムは大体似たようなものになるはずです。コメント解説もそれなりに多く入れましたので、ゆっくり見ていけば、これがどんな働きをしているのか、分かる事と思います。



S-1) 初期処理


procedure TForm1.GW1_3_MainInit(Sender: TObject);
begin
  // S-1) 初期処理  ----------------------------------------------------------
  // パラメータの初期化などはこちらで
  stat:=2; // メインルーチンはこれで管理します
  mx:=10; my:=10; // mx,myはマップの左上位置(主人公は+9,+7)
  cx:=2; cy:=2; // cx,cy は、画面内カーソル表示位置
  cur:=0; // キャラアニメーションパターン
  tog:=0; // トグルボタン
  mf1:=0; // 敵さんCのためのフラグ

  // 主人公キャラと敵キャラの座標類準備
  px:=20; py:=20; pmv:=7; // px,pyは主人公位置、pmvは主人公の移動数
  ex[1]:=25; ey[1]:=16;
  ex[2]:=30; ey[2]:=24;
  ex[3]:=20; ey[3]:=25;
  ex[4]:=36; ey[4]:=21; mf2:=ex[4]; mf3:=ey[4];
  ex[5]:=30; ey[5]:=20;
  emv:=6;


  // マップデータの読みこみ(低速BG0番へのロード)
  GW1.BG_ChipDataRead(0,'mapdata.dat');
end;

 S-1) というのは流れ一覧で示したラベルですね。ここでやっている処理はほとんど初期変数の代入です。マップやカーソル、主人公に敵さんの表示位置を入れているくらいですか。このプロシージャの最後にマップを読み込み、そこで初期処理は終わります。細かい変数の意味は……そうですね、一応しておきましょう。

stat:ゲームの流れを制御するフラグです。S-1) 〜 S-6) といった単位だと思ってください。この後解説するメインループ処理でどのように制御しているのか分かるはずです。
mx,my:マップは広いので、スクロールさせる必要があるわけですが、その左上隅の位置です。画面はここを基準として、右に20マス、下に15マス分表示されます(今回のウィンドウサイズは640×480です)。
cx,cy:遊び手が操作するカーソルの座標です。画面左上をそれぞれ0とした相対座標で管理され、マップ全体の絶対座標に変換するなら(mx+cy、my+cy)とします。
cur:何もボタンを押さない間も主人公やカーソルはちょこちょこアニメーションします。そのパターンを制御するための変数です。
tog:シューティングやアクションを作る人にはおなじみの「トグル」を制御するフラグです。たとえばスーマリでAボタンを押すとジャンプします。押しつづけていると、そのまま地上に落下しても再ジャンプはしません。ボタンをちょっと離し、もう一度押すとジャンプします。この自動再ジャンプの抑制に必要なのです。
mf1〜3:敵さん用に特別に変数が必要になった時、mfXXという名前を使う事にしました。mf1は、敵さんCの稼動スイッチフラグです。単純距離差3以内になったらオンにしておく事で、あとは敵Aと同じ動きをするように制御するのです。mf2およびmf3は、ピボットの中心座標です。
px,py,pmv:プレイヤーの初期x座標およびy座標、それと移動歩数の設定です。
ex[i],ey[i],emv:敵さんの座標です。今回はiは1〜5で、それぞれ敵A、B……Eと対応させることにしました(セッション6では11〜15とやってましたが、紛らわしいかな?と思ったので)。なおemvは敵の移動歩数です。ちゃんとしたシステムだたったら、敵ごとに設定するべきでしょう。



メインループ

procedure TForm1.GW1_4_MainJob(Sender: TObject);
begin
  // S-2) 〜 S-6) を、ステータスによって分岐
  if stat=2 then SelectUnit(Sender)
  else if stat=3 then DecPosition(Sender)
  else if stat=4 then PlayerAutoMove(Sender)
  else if stat=5 then EnemyThinking(Sender)
  else if stat=6 then EnemyAutoMove(Sender)
  else PlayerRestore(Sender);

  // 今回、全状態においてマップチップが見えるのでここで共通処理
  GW1.FloaterBGput(Epath+'bgchip2',0,mx*32,my*32,1);

end;

 S-1) 初期設定が終わったら、早速プログラムはメインループに入ります。このシステムはフレームで制御していまして、fps値(1秒間にループを周る数)は20です。その数だけこのプロシージャ MainJob()を呼び出すわけですが……やっている事は非常に単純、現在のstat値を調べ、対応する1つのサブルーチンを呼んでいるだけです。このstatは1〜6において、S-1) 〜 S-6) と対応しています。サブルーチンの中のどこかでstat値が書き換えられた場合、次のループから違う処理が発生するわけですね。
 なお、それらのサブルーチンを呼んだ後、FloaterBGPut(); というTGWの関数によって、背景マップを表示しています。これはグラフィックシステムの関数なので、具体的にどのようにビットマップを操作しているのとは聴かれても困ります(^^;)。



S-2) ユニット選択

procedure TForm1.SelectUnit(Sender: TObject);
begin
  // S-2) ユニット選択(今回は1キャラしかないけど……)----------------------

  // スペースキーを押すと、移動範囲計算処理、それからS-3)へ
  if (tog=0) and (GW1.boolButtonLeft) then begin
    if (mx+cx=px) and (my+cy=py) then begin
      SndPlaySound(PChar(Epath+'chi.wav'),SND_ASYNC);
      RangeCheck(Sender);
      stat:=3; tog:=1;
    end;
  end;

  // トグルのリセット
  if (tog=1) and not (GW1.boolBUttonLeft) then tog:=0;

  FourStick(Sender); // カーソル移動
  StatusPrint('Players Turn:');
  MapPrint(Sender);

end;

 コメントにも書いていますが、今回はプレイヤーは1キャラしかいないのにも関わらずキャラ選択させられます。このフェイズでの主な処理は、スペースキーを押すとちょっとした関数を呼んでS-3) に移動する事、それからキーを離していた場合はトグルをリセットする事、それから共通処理であるFourstick(カーソル移動処理)→StatusPrint(状態表示)→MapPrint(キャラやカーソルを画面に表示する)、これで全てです。
 スペースキーを押した場合の処理は詳しく見ておく必要があります。まず、「トグルがない状態で、ボタンを押したとき」、「現在のカーソルにプレイヤーキャラがセットされていれば」というのが条件部です。両方まとめてandで括っても同じです。ともかくこの条件を満たした場合、効果音をAPIで鳴らし、RangeCheck() を呼び、あとは流れ変数statを3にしてトグルをセットしているわけです。
 このRangeCheck() は、プレイヤーの移動範囲を検索するあのルーチンです。セッション3でやりましたね。今回は次のようなコードになっています。

procedure TForm1.RangeCheck(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;

  // 主人公の位置から、移動距離 pmv 分への再帰探索をはじめる
  Arch(px,py-1,pmv,1,'1'); Arch(px+1,py,pmv,2,'2');
  Arch(px,py+1,pmv,3,'3'); Arch(px-1,py,pmv,4,'4');
  // 同じ位置のチェックをここで許可
  mvrec[px,py]:='0';
  GW1.BGf[1,px,py]:=0;
end;

procedure TForm1.Arch(ax,ay,mv,di:integer; mway:String);
var i,num,down:integer;
begin

  // すでに地点情報が書きこまれている場所は無視する事
  if (mvrec[ax,ay]<>'') and (Length(mway) > Length(mvrec[ax,ay])) then exit;

  // その座標にプレイヤーもしくはお仲間がでんと立っているならダメよん
  if (px=ax) and (py=ay) then exit;
  for i:=1 to 5 do begin
    if (ax=ex[i]) and (ay=ey[i]) then exit;
  end;


  // まず、その場所の地形情報を調べる。downは、地形を移動するために必要な距離数
  down:=0; num:=GW1.BGf[0,ax,ay];

  // 仮決定部
  if (num>=1) and (num<=15) then down:=1;
  if num>=16 then down:=12;
  // 本決定部
  if num=0 then down:=12;
  if (num=3) or (num=12) or (num=15) then down:=2;
  if (num=2) then down:=3;
  if (num=21) then down:=6;

  if mv-down<0 then exit; //移動できないなら調べてもムダですね

  // すくなくとも移動はできるようなんで、マークして次へ
  GW1.BGF[1,ax,ay]:=0;
  mvrec[ax,ay]:=mway;

  if di<>3 then Arch(ax,ay-1,mv-down,1,mway+'1');
  if di<>4 then Arch(ax+1,ay,mv-down,2,mway+'2');
  if di<>1 then Arch(ax,ay+1,mv-down,3,mway+'3');
  if di<>2 then Arch(ax-1,ay,mv-down,4,mway+'4');
end;

 ソースだけベタ貼りしてみましたが、セッション3の関数と比べると、微妙にコードが増えているだけで、あとはまるきし同じです。あ、移動歩数値……セッション6で決めたポリシーによると、3の床作らないつもりだったけど、直してなかったです。ごめんね〜。
 増えたところだけ紹介しましょう。RangeCheck() の末尾、ここで自分の座標を移動可にして、移動のための歩行方法を「0」と置き換えてます。前のテストアプリのままだと、同じ位置には移動できなかったんですよ〜。
 それと、Arch()の方では、「探索地点にプレイヤーか敵キャラのユニットがある場合、移動不能とする」という処理が約束通り付け加わりました。
 一番気をつけておくべきは、水色太字にした部分です。セッション3では「単に移動可能である事をマークするため」の配列として紹介していました。その時に、「1という文字ではなくて、今までに歩いた経路を記憶させておく事によって、後でそれをトレースさせる事ができる」と説明したと思います。その具体的な実装がされているわけですね(前のサンプルアプリで既に実装していましたが……)。これは、S-4) の最短移動の時に役に立ちます。
 それくらいですか。ではS-2) に戻って、共通関数 FourStick()、StatusPrint()、MapPrint() のソースも一応ペーストだけしておきます。

procedure TForm1.FourStick(Sender: TObject);
begin
  // カーソル移動判定部
  if (GW1.boolKeyLeft) then begin
    SndPlaySound(PChar(Epath+'ki.wav'),SND_ASYNC);
    if cx<1 then dec(mx) else dec(cx);
  end else
  if (GW1.boolKeyRight) then begin
    SndPlaySound(PChar(Epath+'ki.wav'),SND_ASYNC);
    if cx>18 then inc(mx) else inc(cx);
  end;
  if (GW1.boolKeyUp) then begin
    SndPlaySound(PChar(Epath+'ki.wav'),SND_ASYNC);
    if cy<1 then dec(my) else dec(cy);
  end else
  if (GW1.boolKeyDown) then begin
    SndPlaySound(PChar(Epath+'ki.wav'),SND_ASYNC);
    if cy>13 then inc(my) else inc(cy);
  end;
end;

procedure TForm1.StatusPrint(pstr: String);
begin
  GW1.FontSprtZ(7,7,24,0,0,0,0,128,pstr);
  GW1.FontSprtZ(8,6,24,0,0,0,0,128,pstr);
  GW1.FontSprtZ(6,5,24,0,0,0,0,128,pstr);
  GW1.FontSprtZ(6,6,24,255,255,128,0,128,pstr);
end;

procedure TForm1.MapPrint(Sender: TObject);
var i:integer;
begin
  // マップ表示部(プレイヤーキャラ/敵キャラ/カーソル)
  // キャラクター表示
  zx:=px-mx; zy:=py-my;
  GW1.putZ(Epath+'girl',1+(cur div 5),zx*32+16,zy*32-24,100+zy);
  GW1.putZ(Epath+'girl',3+(cur div 5),zx*32+16,zy*32+8,100+zy);

  // 敵キャラ表示(Zレイアは偉大なり)
  for i:=1 to 5 do begin
    zx:=ex[i]-mx; zy:=ey[i]-my;
    GW1.putZ(Epath+'eneh',1,zx*32+16,zy*32-24,100+zy);
    GW1.putZ(Epath+'enet',i,zx*32+16,zy*32+8,100+zy);
  end;

  // カーソル表示
  if stat<=4 then GW1.put(Epath+'Cur',(cur div 5)+1,cx*32+16,cy*32+16);
  cur:=cur+1; if cur>=10 then cur:=0;
end;

 これは正直、見たまんまです。注目すべきは、MapPrint() の中にある「100+zy」という記述。これがウワサのZレイアです。y座標が高いキャラにZレイアを高く設定しておけば、

 と、このように上図のプレイヤーキャラの部分でしっかり重なってくれるわけです。しかし囲まれてますね(^^;)。しっかり敵Eは主人公から3マスの位置にいるでしょ。(Dは自分のピボットから出られないらしい……)



S-3) 移動先座標選択

procedure TForm1.DecPosition(Sender: TObject);
begin
  // S-3) 移動先座標選択 ------------------------------------------------------

  // リターンキーを押すと、先の画面S-2)に戻ります
  if (GW1.boolButtonRight) then begin
    SndPlaySound(PChar(Epath+'chi.wav'),SND_ASYNC);
    stat:=2;
  end;

  FourStick(Sender); // カーソル移動

  // スペースキーで移動開始します(大変かも……)
  if (tog=0) and (GW1.boolButtonLeft) and (GW1.BGf[1,mx+cx,my+cy]=0) then begin
      // アニメーション距離のセット
      golast:=Length(mvrec[mx+cx,my+cy]);
      goflag:=1;
      stat:=4;
  end;

  // トグルのリセット
  if (tog=1) and not (GW1.boolBUttonLeft) then tog:=0;

  StatusPrint('Kiny''s Postion?');
  MapPrint(Sender);

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

end;

 探索手続きが終わった後はこのS-3) の処理を行います。なんとなくS-2) と同じようなコードになっていますね。違うのは、移動範囲を教えてくれるフィルタを表示する命令が付け加わっているくらいでしょうか。もっともフィルタの正体は1枚のマップであり、具体的なフィルタの配置は先のArch() で行っているので、ここでは単に表示しているだけです。
 次のS-4) に行くための条件は、「トグルがオフになっていて(実は無くても大丈夫)、スペースキーを押して、なおかつ選択地点が移動可能と診断されている」となっています。それを満たした場合、最短移動のためのアニメーションフラグをセットしている、といった所です。



S-4) プレイヤー最短移動

procedure TForm1.PlayerAutoMove(Sender: TObject);
var sr:String;
begin
  // S-4) プレイヤー最短移動 --------------------------------------------------

  // 移動アニメルーチン
    sr:=mvrec[mx+cx,my+cy];
    if copy(sr,goflag,1)='1' then dec(py);
    if copy(sr,goflag,1)='2' then inc(px);
    if copy(sr,goflag,1)='3' then inc(py);
    if copy(sr,goflag,1)='4' then dec(px);
    inc(goflag);
    SndPlaySound(PChar(Epath+'kan.wav'),SND_ASYNC);
    if goflag>golast then begin
      goflag:=0; tog:=1;
      tmx:=mx; tmy:=my;
      enum:=1; // 敵1番さんからどうぞ
      stat:=5; slide:=1; dmt:=0;
    end;

  StatusPrint('Kiny''s Moving..');
  MapPrint(Sender);

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

end;

 S-4) は、mvrec[移動したい場所]に入っている経路情報(Arch() の時に書き込みましたね)を取り出し、その通りに主人公を動かすという処理を行っています。ここのロジックは、Arch() の部分と合わせて見ないと分かりにくいかもしれません。例えば「23443」と格納されていたら、プレイヤーは右下左左下と動くわけです。  そして動ききってしまった場合、次はS-5) で、いよいよ敵さんの処理に入ります。enum:=1 という部分で、敵1番を次の行動キャラクターと指定しています。



 ページが長くなったので分割しています→ ∇ 続き(6−2)へ



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

Written by. gumina(鷹月 ぐみな)