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

- 基本ルーチンの実装へ
Article Written: 2000/2/11




 こんにちは、鷹月ぐみなです。最近は色々と忙しくなってしまって、私自身がSLGをあまりプレイしなくなってしまいました(^^;)。代わりというか、作り手側(たかつきCOMPANY)として、ファンタジーSLGを製作する企画が一つあるので、ひょっとしたら秋頃にでも出すかも知れません。
 さてさて、「戦術型SLG」の基礎講座は前回で終わりです。今回以降、実際のゲームのルーチンの雛型的なものを組み立てていきます。理解しやすいように気を使って書いていくつもりですが、それでも分からなくなったら前に戻るなり、鷹月に聞くなりしてください。せっかくここまで読んできたわけですから、最後まで理解した方が得ですしね。



 では始めましょう。今まではSLGを構成するものを要素に分解し、紹介してきました。情報処理用語ではトップダウンと言います。ここからはその逆のプロセスを辿ります。即ちゲームを作るいろんな要素を結合し、SLGシステムを作っていくのです。これはボトムアップと呼ばれます……と、先日情報処理技術者になったので、それらしく言葉を添えてみるのでした(笑)。
 SLGシステムと言っても、そのままゲームで使われている所までは作りません。手ほどきレベルで留めるって明言していますしね。基本的な流れが一通り実装できれば良いかな、と思います。その基本的な流れと言うやつは次のようになっています。
    R-1) 初期処理
    R-2) イベント発生フェイズ
    R-3) [Player's Turn] ユニット選択
    R-4) [Player's Turn] 移動先座標選択
    R-5) [Player's Turn] 最短移動
    R-6) [Player's Turn] 攻撃相手選択
    R-7) [Player's Turn] 攻撃処理
    R-8) サブルーチン:生存判定
    R-9) [Enemy's Turn] 最適探索
    R-10) [Enemy's Turn] 敵さん側最短移動
    R-11) [Enemy's Turn] 攻撃処理
    R-12) サブルーチン:生存判定
    R-13) ターン終了処理、R-2) に戻る
 この分け方はかなりプログラムレベルで切り分けています。ソースファイルを見たときに、ほとんどこの流れで分割されていることに納得していただけることでしょう。
 なおここでは、タクティクスオウガにあるような「攻撃後移動」「WTによる移動順決定」「反撃処理」は入れていません。攻撃をしたら移動はもうできず、行動順は常にプレイヤーたち→敵さんたちが交互に行われ、攻撃した方は反撃を受けません。そういう仕様だと思ってください。
 だいたい単語を読めばどんな流れになっているのか分かるとは思いますが、一応補足も兼ねて紹介しておきますね。

R-1) 初期処理
マップの初期化やロード、音楽を鳴らす云々で、プログラムを書いていると絶対に必要になる部分です。また、ステージスタート時の会話もここに含む場合があります。それゆえに初期「設定」ではなく、初期「処理」としています。

R-2) イベント発生フェイズ
ここから1ターンが始まります。特定ターンにおける会話イベントなどはここで発生します。なお、1ターンというのは、全キャラクターが行動を1回終了するまでの時間のことです(SLG以外では通例サイクルと呼ばれます)。フェイズあるいはフェーズは、1ターンの中の特定の流れを示す言葉です。「移動フェイズ」「戦闘フェイズ」などと使われます。

R-3) ユニット選択
どのプレイヤーキャラを操作するかを選択する部分です。前回のサンプルアプリにもちゃんとありましたね。実際のSLGではキャラに合わせてボタンを押すと、行動の内容を選択するメニューが出たりしますが、今回はそれらはやりません。即移動にしちゃいましょう。

R-4) 移動先座標選択
サンプルアプリで実装済み。移動可能地点の表示と、光らなかった場所はクリックしても動けないよ、という処理までここに入ります。

R-5) 最短移動
選んだ地点に行くための経路はすでに計算されているので、あとはその通りに動いていくだけです。とことこ。

R-6) 攻撃相手選択
移動終了した地点の隣接もしくは攻撃可能地点に敵さんが居た場合、攻撃に移れるわけですが、対象が複数居た場合はどれを狙うか選択することができます。その処理です。

R-7) 攻撃処理
読んで字のごとく。アニメーションが本来入るわけですが、鷹月はむろん実装するつもりはないです。それはそうと、ダメージの与え方などは各人のデザイン能力が問われます。この講座では、鷹月ぐみなが95年に発表したTRPGシステム「サイラジア・ワールドRPG」のダメージ判定処理をそのまま使います。改めて考えなくて良いから楽〜。

R-8) 生存判定
ダメージを受けてLPが0になったら生存判定です。サブルーチンとわざわざ書いているのは、R-12) で全く同じ処理をするからです。こう書いてあれ、と思った方はいるかな?
確かにR-8) はダメージを受けたのは敵側だろうから敵側の生存判定、R-12) はプレイヤー側の生存判定というのが基本的なんですが……そこはそれ、範囲魔法で自分も巻き添えをくったりとか、反撃されてやられたとか、将来の拡張目的というやつです。

R-9) 最適探索
前回のセッションで解説した、敵さんそれぞれの移動目標地の探索処理です。

R-10) 敵さん最短移動
移動のルーチンは、R-5) とまったく同じです。動くキャラクターが違うだけ。

R-11) 敵側攻撃処理
もちろん敵側にも、攻撃相手選択のルーチンはあるんですが、それらは攻撃処理の中に統合できます(別に切り分けても構まなかったのですが……)。ユーザにウェイトを入れるか入れないかという差で選んでみました。

R-12) 生存判定
R-13) ターン終了処理、R-2) に戻る
実装はしないでしょうが、ターン終了時まで有効な魔法だとか、ライフの回復とか、ゲームの場合はいろんな要素が出てきます。それらのステータスを調整する処理がここにきます。

 以上です。生存判定から全滅時の処理などに分岐したりしますが、それらは流れからは余剰だと思いますので今は無視します。ともかく、ここに示した基本と言うべき流れのSLGシステムを実装する事が、このSLG講座の最終目標としましょう。ようやく完全なビジョンを示しました。そう、講義を終わった頃には、この程度のSLGが作れる知識をGETしているというわけです。



 早速設計……といきたいのですが、前回のサンプルアプリで実装した部分が R-3)、R-4)、R-5) だけだった事を考えると、いきなり増やしすぎですね。物事には順序があるというわけで、最終段階との間に一つ、中間点を設ける事にしましょう。それは次のような流れを持ちます。
    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システムを作るために、考えるべきことがいくつかあります。まず今回ソースに追加されるべき敵さんの、行動パターンを決めておきましょう。あまり少ないのも何ですし、かといって多くても無意味なので、5体くらいがいいですね。それで、5体とも違った行動パターンを持たせることにします。
    敵さんA:猪突猛進型(ひたすらプレイヤーを追いかけてくる)
    敵さんB:通常型(PCとの単純距離差が10以内なら近づいてくる)
    敵さんC:守備型(PCとの単純距離差が3以内になった所で近づき、以後は距離に関係なく常にプレイヤーを追いかけるA型になる)
    敵さんD:ピボット型(ある地点から単純距離半径6以上の地点へは何があっても進めない。その範囲の中でプレイヤーに近づこうとする)
    敵さんE:アーチャー(単純距離差が10以内で近づくのだが、主人公からの単純距離差が3になる地点を確保しようとする)
 こんな所で良いでしょう。
 では次、プレイヤーと敵さんの移動歩数値を決めておきます。プレイヤーは7敵は6とします。それに従って、地形の移動ウェイトを設定します。床を1、大理石の床を2、石の置物6、壁を12としておきます。それ以外のオブジェクトも強引にこの4種類のどれかに割り付けます。
 次は音楽……まぁ、これは特にストーリーも何もないので、前回使わせてもらったみくしぎさんの「十字架の誓い」を再使用させてもらうことにします。
 グラフィック系は……プレイヤーキャラは泣く大臣さんのFGPを使います。敵さんに関しては、タイプA〜Eが一目見て分かる、というために、いま適当にこしらえたものを使うことにしましょう。かきかき。
    ... 何これ?(爆)
 ……ま、まぁ分かりやすいという事で、容姿について触れるのはご勘弁を。え、えーと次は製作言語。前回のサンプルアプリはDelphi+TGWだったので、今回もこれを使うことにします。
 とまぁ、こんな風に現時点で決められそうな設定を確定させておきます。もちろんコーディングしている最中になって気が付く事もいくらでもありますが、後になって困るよりは今のうちにいくつか解決させておいた方がいいですよね。そういった細かい問題を二つほど挙げてみましょう。



□ マップへの相乗り問題

 あるマスへ移動可か不可かは、セッション3で紹介した探索ルーチンを使うわけですが、この判断はあくまでマップ地形からしか判断していません。キャラの存在を考慮していないわけで、このままだとキャラ同士が同じマスに重なってしまう可能性が出てきます。もちろん原則的にあってはならない事象なので、解決させなくてはいけません。
 方法としては、移動可能地点探索のさい、キャラのいるマップチップの移動ウェイト値を50あたりにしておく事が考えられます。こうすると壁同様、絶対に止まれないからです(※1)。それはそれとして、「キャラがそのマスの上にいる」というのはプログラム的にはどう判定させると良いでしょうか。二つの方法が考えられます。

A) 各々のマスに対して、その上にあるオブジェクト情報を付与する
 単純な二次元配列で解決する方法です。変数では objp[x,y] のように表します。この中に入る値として、「0:何もいない」「1:プレイヤー1が居る」「2:プレイヤー2が居る」……「11:敵さん1がいる」「12:敵さん2がいる」……という事にします。探索の際に対象のマスに対して、何かキャラが乗っていることがすぐ分かるだけではなく、どんなキャラが乗っているかまで一瞬で分かり、便利ではあります。しかし、マップの広さがたとえば256×256マスだった場合、65536個の配列を必要とし、相当のメモリを消費する事になります。やむを得ない場合は良いとは思うのですが、たかだか今回のシステムではプレイヤー1キャラ、敵さん5キャラの6つのオブジェクトしかないのにこれはちょっと贅沢すぎます。

B) 全キャラの居場所を調べる
 Aで丁度キャラクター毎に番号が一意に付いているのでそれは採用しましょう。プレイヤー1はキャラクター番号1番、敵さん1はキャラクター番号11番という感じになります。さて、プレイヤーも敵さんもキャラクターと考えるなら、iを番号として、x[i]、y[i] という配列でその座標を一まとめにして格納することができます(x[i]: キャラクターiのx座標、y[i]: キャラクターiのy座標)
 それともう一つ、sta[i] という配列を用意します。これは、キャラクターiのステータスを示し、たとえば「d」が入っていたら死亡していることを示します。これだけ定義ができれば、ソースコードも書くのに苦労は要りません。探索ルーチンSearch() が座標ax,ayについて、そのマスに誰かキャラクターが居るか居ないかの判定は次のようになります。(Delphiソースの場合)


begin // プレイヤー側の探索ルーチン
  flg:=0 // フラグを初期化する
  for i:=11 to 15 do begin // 敵さんそれぞれに対して、
    if sta[i]='d' then continue; // 死んでるならマップに居ないだろうから無視
    if (ax=x[i) and (ay=y[i]) then flg:=1; // 該当すればフラグを立てる
  end;
  if flg=1 then 「この場所には誰かがいるので移動できないと判定」
  else 「この場所には少なくとも誰もいないと判定」
end;

 もちろんプレイヤー自身はこの判定に加える必要はありません。多少、処理するのに時間がかかりますが、敵5匹程度ですから、気にするほどの処理落ちは出ないと思います。
 まあ今回は後者の方法を採択することにしましょう。
 それはそうと、さきほど「※1」と注意マークを付けておきました。「キャラが居るマスを壁同様に扱う」、これは正しいでしょうか、それとも間違っているでしょうか?
 これはどちらでも致命的にはなりませんが、SLGのシステムが微妙に変わってきてしまいます。壁同様にしてウェイトを高くしておき、「ここ以上は進めない」としてしまうと、ゲーム中において、相手キャラは当然としても、味方キャラを越えて先に行く事ができなくなります。逆に、ウェイトは元のマスと同じだけど、単にその場所へは止まれないとした場合、そのオブジェクトを通り抜けることができます。この折衷案を取り、「自分のキャラだったら単に止まれない、相手のキャラだったらウェイトをかけておく」とすると、味方は通り抜けられ、敵は通り抜けられないというシステム(タクティクスオウガはそうですね)ができあがります。今回は単純な1番目(味方だろうと敵さんだろうとそこで進めなくなる)を採用する事にします。



□ 縦32ピクセル以上のキャラの表示順問題

 今回使おうと思っているマップチップは32×32ピクセルですが、キャラクターは32×48という、1マスを出るサイズの大きさです。この場合、ちょうど上下に並んだときに、キャラ同士が重ね合わさる事になります。
← ちょうどこうなってくれると良いんですが……
← 時にはこうなってしまう可能性もあるということです

 後者のような状態になってはいけません。「常に手前にいるキャラクターが手前に表示されていなくてはならない」わけです。この解決方法も二つ考えられます。

A) グラフィックシステムがZレイアをサポートしている場合
 Z-Layerとは、オブジェクトの表示順を設定するための「グラフィックプレーンの高さ」の概念の事です。これをグラフィックシステムが最初からサポートしている場合、キャラクタに高さ情報を指定することにより、自動的に重ね合わせの順番を判定し処理してくれます。Zレイア値は高いほど手前になります。(階層と思って、あなたはそれを上から眺めていると思ってください)これがあると実装が楽です。
 具体的には、各キャラクターのZレイア値を動的に、そのキャラクターのy座標に等しい値にするだけです。表示は誰を先にしようが後にしようが同じ結果になります。今回鷹月が使おうと思っているシステムは、TGWがZレイアをサポートしているので、こちらを採択します。うーん、楽。

B) レイアのないグラフィックシステムの場合
 たとえば標準のままのHSPはレイアをサポートしていません。重ね合わせるというのは、単に上塗りしていくだけの事になります。この場合、表示(作画)する命令の順番が重要になってきます。まかり間違っても、Aで恐らく使うことになるでしょう、

  for i:=11 to 15 do
    キャラクター表示(x[i],y[i],パーツ番号i);
 こんなソースではダメということです。たとえばy座標が、{敵1、敵2、敵3、敵4、敵5}={7,8,6,9,5} だった場合、作画順序は敵5(y=5)→敵3(y=6)→敵1(y=7)→敵2(y=8)→敵4(y=9)と上塗りしなくてはいけません。これを実現するためには……基礎からプログラムを勉強された方はたぶん知っているでしょう、ソートのルーチンを使う必要があります。ソートには色々な種類がありますが、どうせ敵さんが5匹しかいない少ないものなので、もっとも単純と言われる選択交換法(Selection Sort)を使うことにしましょう。順番順番に隣と比べていき、相手のほうが値が小さかったらお互いを入れ替えるというものです。


// --------- For Delphi.

// ローカル変数の設定
var temp,temp2:integer;
    a,b:array[11..15] of integer;
begin
  // a[i]はキャラ番号、b[i]はそれぞれのy座標を入れる
  for i:=11 to 15 do
  begin
    a[i]:=i;
    b[i]:=y[i];
  end;

  // セレクション・ソート(昇順)
  for i:=11 to 14 do
    for j:=i+1 to 15 do
      if b[i]>b[j] then 
      begin
        // a[i]とa[j]を入れ替える。テンポラリが要るのがミソ
        temp:=a[j]; temp2:=b[j];
        a[j]:=a[i]; b[j]:=b[i];
        a[i]:=temp; b[i]:=temp2;
      end;

  // こうして、a[i]には表示順が入っているわけですね 
  for i:=11 to 15 do
    キャラクター表示(x[a[i]],y[a[i]],パーツ番号a[i]);           
end;

 ……実は頭で推測して書いているだけで実際に試してないので、間違ってたらごめん(爆)。その場合、こっそり後で直しておきます(ぉ



 まだ懸念事項はあるにはあるんですが、これは実装経験の問題もあるから、伏せておきましょう。基本的にはこれで考えるべきことは終わりました。後は、今までのアルゴリズム・ルーチンを全て統合していくだけです。次のセッション7では、はじめに中間地点版サンプルアプリを見てもらい、その解説をしていく事になるでしょう。そしてセッション8では攻撃やダメージの処理について考え、セッション9が最終目的版のサンプルアプリの紹介および解説です。終わりは見えてきましたね!



 動く現物は今回もありませんでしたが、ゲーム製作において非常に重要な「設計」をやりました。頭にイメージだけは湧いても、これをしっかりと整理できなくては実装できません。今回の分類部分に関しては、紙に書き付けて覚えるくらいの事をした方がいいかもしれません。実際鷹月も、この後プログラムするにあたって、これらを書きつけたものを見ながら行うわけですし。
 次回はそんなわけで最初に現物が出てきます。もちろんこれを書いている時点で何も作ってないし、鷹月はそれなりに忙しい身です。長くは待たせないようにはしたいと思いますが、すぐにはできません。ご了承くださいね。
 また、記事の感想お待ちしています。講義というのは不特定多数にばら撒いているものではなくて、これを多少なりとも期待してくれる「特定の」人のために書いているわけですから、その特定の人が誰であるか、その存在くらいは私は把握する権利があるのだと考えます。まあ、SLG講座が一通り終わった時に、それを全部読んでくれて「ためになった」と思った時にでも構いませんので。
(毎回書いてね、ってわけじゃないんです。1度くらいはね、って意味です)

- 鷹月 ぐみな



  Session7:ちょっと違うRPG・準備編 (2000/2/12)



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

Written by. gumina(鷹月 ぐみな)