戦術SLGプログラミング(CardWirthで)
今回のコラムは、制限された環境で戦術SLGプログラミングを勉強し実装してみた備忘録のようなものである。 したがって、CardWirthシナリオ制作にとって有益な情報は全くない。この点をご容赦いただきたい。
本コラムは、現在制作(中断)中のCardWirth追加シナリオ『六角平野の戦い』についてのコラムである。ゲーム部分は最後まで完成しており、バランス調整もほぼ完了しているので、CardWirthをすでにインストールしている方はぜひとも遊んでみてほしい。
かなり前の制作状況になるが、ニコニコ動画にて動画も公開している。
敵はなかなかそれっぽい動きをしてくれていると、個人的には思う。
シミュレーションゲーム『六角平野の戦い』の仕様
CardWirthというゲームエンジンで戦術シミュレーションゲームをプログラミングしたという話を今からしたい。
ゲームエンジンとしてCardWirthは、大量のデータを扱うにあたって貧弱と言わざるを得ない。 盤面を小さく、しかし面白いものにするために『六角平野の戦い』では仕様を以下のように策定した。
- 盤面サイズは10×8、HEX式である。
- ユニット生産/雇用の概念なし。
- ユニットの移動において、障害物・悪地形・ZOCの概念あり。
- ダメージ算出において、ユニットの相性・地形効果あり。
- 2~3マスの遠距離攻撃の概念あり、回避率の概念はなし。
- 指揮官ユニットの存在。
ユニットは槍兵→騎兵→弓兵→槍兵の三すくみをベースに設定してある。 ほかにも多数のユニットはいるが基本的には以下の通り。
- 槍兵:移動力2。近接。騎兵に強く、弓兵に弱い。ZOCを持ち、頑強。
- 騎兵:移動力3。近接。弓兵・砲兵に強く、槍兵に弱い。高い攻撃力を誇るが、ZOCを持たない。
- 弓兵:移動力2。遠距離(射程2マス)。槍兵に強く、騎兵に弱い。やや脆弱だが、ZOCを持つ。
- 砲兵:移動力1。遠距離(射程3マス)。高い攻撃力を誇るが、ZOCを持たない、極めて脆弱。
- 指揮官:移動力1。近接。倒されると負け。脆弱。
- 暗殺者:移動力2。近接。指揮官を一撃で倒せる。ZOCを無効にできる。
小規模の盤面に対してやや不釣り合いに、弓兵が射程2マス、砲兵が射程3マスと 設定しているのが工夫の一つで、近接ユニットと遠距離ユニットによる同時攻撃を仕掛けて 敵を攻めつぶすというのが、このシミュレーションゲームの基本的な力学である。
盤面が小さくても面白いシミュレーションゲームにするために、 攻撃偏重でやや大味な作戦級のウォーゲームを採用し、 個々のマス目の複雑度を上げるために地形効果・ZOC・ユニットの相性を採用した。 ちなみにHEX制を採用したのにもちゃんと理由があって、各カードの座標をずらしてクリックしやすくする、移動をより直感的にしてストレスを軽減するという意味がある。
結果として、戦略のゲームというよりは、パズルゲームに近いものとなった。 ひとくちに「CardWirthでSLGを作る」というが、作るべき仕様は割と真剣に取捨選択している。
思考ルーチン
移動範囲の計算や、思考ルーチンの実装について、CardWirthでどう実装したかについての工夫は数多くあるし、どこかでまとめてはおきたいが、ここでは触れない。 基本的には既存の高速化テクニックを用いるか、ループを全部展開するなど、ごり押しによる実装を行っている。
ここではそれよりも、簡単な計算でユニットをそれっぽく動かすために思考ルーチンをどう設計したかについて重点的に述べる。
思考ルーチンに何をさせるか
戦術シミュレーションゲームにおいて思考ルーチンはゲームコンテンツである。 プレイヤーの相手となるためには、プレイヤーの行動に対してきちんとレスポンスを返す、一貫性のある戦略(らしきもの)を持たせることが重要である。 そういうわけで、思考ルーチンには以下の要求を課すことになる。
- 明確な攻撃目標を持ち、集中攻撃を試みる。
- 障害物・悪地形・ZOCを迂回できる。
- 指揮官が存在する場合、その防衛を試みる。
上記の要求をすべて満たすのは工夫がいる。 明確な集中攻撃を実現するためには、集中攻撃目標を設定することと、それに向かうという挙動をプログラムしなければならない。 その上で、障害物・悪地形・ZOCを迂回するためには、マス目の価値を算出する必要がある。 特に、最後の「指揮官の防衛」を実装はなかなか厄介である。 例えば、複数のプレイヤー軍ユニットが迂回しつつ敵の指揮官に迫るというような状況では、 敵軍ユニットはプレイヤー軍ユニットを阻む位置に陣取る必要がある。
そしてその上で、遠距離ユニットならば敵から一定距離をとるような、そのユニットに固有の挙動が欲しいし、ステージごとに敵の作戦も柔軟に変えたい。
思考ルーチンのおおまかな仕様
少ない計算量で上記の要求を満たすため、 移動に関する思考では、 敵軍の状況や地形などから算出する「盤面の重みテーブル」と、 個々のユニットの移動に関する評価値算出を分離している。
これはゲームAI -基礎編- 『知識表現と影響マップ』 で紹介された考え方をもとにしている。
攻撃に関する思考は実に単純に組んでいて、攻撃範囲内の敵対ユニットから
の優先度で選んで攻撃している。回避率の概念が無いので、これでほぼ最適な攻撃ができる。
全体の仕様を簡単にまとめると以下の通り。
- 盤面全体の重みテーブルを計算 (ターン開始時に1回だけ実行)
- 行動済みでないユニットについて以下を行う (ユニットの行動順は固定):
- 移動可能な各マスについて、重みテーブルから評価値を算出 (ユニットごとに変える)
- 評価値が最も高かったマスへ移動
- 攻撃範囲内の敵対ユニットの優先度を算出 (いなければスキップ)
- 優先度が最も高かったユニットを攻撃
- 行動可能なユニットがいなくなれば終了
上記の仕様では、 計算量を節約するために意図的な穴をあけてある。 何かといえば、ユニットの行動順を固定していること、 重みテーブル計算をターンの開始時にしか行わないため、 味方ユニットの移動や攻撃による状況変化を無視している点である。
盤面の重みテーブル(影響マップ)
(ターン開始時に)盤面の各マスに数値による重みづけを行う。 直感的には、敵味方の影響度や距離の概念を重みとして各マスに設定し、 各ユニットの行動時に、重みテーブルから各マスの評価値を算出する。
算出方法の異なる3つのテーブルを使用している。
- (1) 味方ユニット(特に指揮官)を中心とする歩行距離テーブル。
- (2) 敵ユニットを中心とする歩行距離テーブル。
- (3) 敵ユニットを中心とする射程距離テーブル。
重みテーブルの算出に当たって、基本的には、悪地形・ZOC・敵ユニットの有無を参照する。
テーブル計算アルゴリズム
ここでは最も重要な「(2) 味方ユニット(特に指揮官)を中心とする歩行距離テーブル」を見てみよう。
まず、悪地形・ZOC・敵ユニットを「弱い障害物」とみなし、その移動コストを設定する。 ここでは例えば、以下のように設定したとして話を進める。
- デフォルト: 1
- 敵ユニット:+2
- ZOC:+1
- 悪地形:+4
- 障害物:+∞
これらのパラメータをもとに、各マスの移動コストを決めることができる。 これは実際の移動コストでなく、敵ユニットを考慮した、主観的な移動コストである。 いくつか例を見てみよう。
- ZOCの範囲外で、何もない草原のマスは、コスト1。
- ZOCを持った敵に隣接する、敵がいる草原のマスは、コスト4。
- ZOCの持った敵に隣接する水辺(悪地形)のマスは、コスト6。
以下のようなマップを一つ考えてみよう。 この概略図では、○が自軍ユニット、□が敵ユニット。黒色が障害物(通行不可)、 水色が水辺(悪地形)、網掛けがZOCである。

このマップについて、移動コストを計算するとこのような感じになる。

それから敵ユニットの重みを設定し、テーブル計算を行う。

例えば図のように、敵ユニットの重みを14と設定し、その敵がいるマスの重みを14と定める。 このように設定した重みを周囲に伝播させることで、重みづけテーブルを作る (頂点をの高さを決めて、それを均すともとれる)。

まずは14とセットした周囲のマス目の重みを計算する。 移動コストの分だけ重みを減らし、そのマスの重みを確定する。 このような操作を重みが大きいマスから順番に行い、重みづけされていないマスが無くなれば終了。
プログラミングになれた人向けに言うと、 経路長に限界をもうけたダイクストラ法を、経路長と頂点数に関する2重ループを用いて書き直し、 CardWirthというゲームエンジンに最適化している。

手続きをくりかえし、計算を終えたテーブル。 このテーブルを使うと、主観的な「最適ルート」を求めることができる。 移動距離だけで考えると、水辺を通ってもかかる時間は同じだが、 水辺を迂回したほうが良いという考慮をそこに織り込んでいる。

同様にして、敵が複数の場合でも同様の処理でテーブルの作成が可能で、 別の例を考えてみよう。

ここで、☆が指揮官ユニット、□が防衛ユニットとする。 この図の状況で、司令官ユニットの重みが20、防衛ユニットが14であるとする。 先ほどと同様に、テーブルを計算してみよう。

各マスの移動コストを算出(移動コスト1は省略)。

ユニットがいるマスの重みを設定。 この重みは状況に応じて可変である。

テーブル全体の重みづけを行う:ユニットがいるマスの重みを均す。

こうして得られたテーブルによって決まる「最適ルート」は、 守備ユニットを迂回して指揮官ユニットに迫る形になっている。
パラメータやユニットのいるマスの重みを変更することで、 この「最適ルート」についても、 守備ユニットを無理やり通過する形にできたり、 守備兵を優先して撃破するなど柔軟に変更することができる。
細かい部分は異なるが、テーブル3つの重みづけは、いずれも上にあげた方法で行われる。
「指揮官の防衛」を実現するために、実際の思考ルーチンでは、最初に「(1) 味方ユニット(特に指揮官)を中心とする歩行距離テーブル」を算出し、「(2) 敵ユニットを中心とする歩行距離テーブル」を計算する際に、指揮官に近い敵ユニットの重みを大きくしている。
ユニットごとの評価値算出
場合によっては「最適ルート」が2つ以上あったりするし、 遠距離ユニットが近接ユニットに距離をとる、という挙動もさせたい。 すでに述べたように、盤面の重みテーブルとユニットごとの評価値算出の処理を分離し、 各ユニットの個性や詳細な判断は後者で行うことにする。
各ユニットの移動において、各マスにおける以下のような情報を取得して、 「マス目の評価値」を計算し、評価値が最も高いマスへ移動する。
- (0) 思考中のユニット自身の情報
- (1) 味方ユニット(特に指揮官)を中心とする歩行距離テーブル。数値が大きいほど指揮官に近い。
- (2) 敵ユニットを中心とする歩行距離テーブル。数値が大きいほど集中攻撃すべき敵に近い。
- (3) 敵ユニットを中心とする射程距離テーブル。中心が4。
- (4) 細かい地形情報/ZOCの有無
「(0) 思考中のユニット自身の情報」を参照したうえで、(1)~(4)の地形情報の計算方式を変更している。具体的にどのような判断が織り込まれているかを例示する。
- 指揮官以外のユニットは、評価値計算において、(2) の値(の何倍か)を加えている。
- 指揮官ユニットは、評価値計算において (2) の値(の何倍か)を減じている。
- 弓兵ユニットは、(3)の値が2に近いほど大きなプラスがかかる(隣接に大きなマイナス)。
- 砲兵ユニットは、(3)の値が1に近いほど大きなプラスがかかる(隣接に大きなマイナス)。
- (1)に応じてプラス補正がかかる。味方への追随を示す。槍兵・弓兵ではこの補正が大きい。
- (4)の状況に応じて適当にプラス・マイナスの補正をくわえる。
こうすることで、特別な場合分けをすることなしに、ごく自然な形で 指揮官は敵から離れようとするし、 槍兵・弓兵は固まって行動しやすくなり、 遠距離ユニットは射程ギリギリの距離に陣取るようになる。 そしてどのユニットも有利な地形を優先する。
さらに、パラメータを調整することで柔軟に変化をつけることも可能。