タワーディフェンスやRTS、あるいは障害物を避けて動くNPCを実装するときに、最終的に必要になるのがパスファインディングです。Godot 4にはこの用途にぴったりの AStarGrid2D クラスがあります。AStar2D よりも数倍速く、グリッドベースのゲームなら使わない手はありません。
この記事では、AStarGrid2D の最小構成からTileMapLayerと連動した障害物検知、対角移動の制御、コスト調整までを、すべて動くコードで段階的に説明します。Godot 4.4 で確認しています。
なぜ AStarGrid2D なのか
AStar2D は任意のノード(Point)をIDで登録するグラフ型のクラスです。AStarGrid2D はその名の通りグリッド専用で、内部的に2次元配列を使うため、点の追加・削除や近傍探索が単純な配列アクセスで済みます。
公式ベンチマークで言えば、同じマップサイズなら AStarGrid2D のほうが約3倍速いという報告もあります。グリッドゲームを作るなら、AStar2D をわざわざ選ぶ理由はほぼありません。
| クラス | 向いているケース | 速度 |
|---|---|---|
AStar2D |
不規則なグラフ(スカイランダー的なノードリンク) | 遅い |
AStarGrid2D |
タイルマップ・グリッド全般 | 速い |
NavigationAgent2D |
自由形状の障害物・物理連動 | 中 |
最小構成:5×5の空グリッドで経路を出す
まずは何も障害物のない5×5のグリッドで、(0,0) から (4,4) までの経路を求めるコードです。
extends Node2D
@onready var astar: AStarGrid2D = AStarGrid2D.new()
func _ready() -> void:
astar.region = Rect2i(0, 0, 5, 5)
astar.cell_size = Vector2(64, 64)
astar.diagonal_mode = AStarGrid2D.DIAGONAL_MODE_NEVER
astar.update()
var path: PackedVector2Array = astar.get_point_path(Vector2i(0, 0), Vector2i(4, 4))
for p in path:
print(p)
region がグリッドの範囲、cell_size が1セルあたりのワールド座標サイズです。get_point_path はワールド座標で経路を返してくれるので、そのままノードの position に渡せます。
ポイントは update() を呼ばないとグリッド構造が初期化されない こと。新しいプロパティを変更したら忘れずに update() を呼んでください。
TileMapLayer から障害物を読み取る
実用的なケースでは、TileMapLayerに置いた壁タイルを通れないようにしたいはずです。Godot 4.3 で導入された TileMapLayer のカスタムデータレイヤーを使うと、これがきれいに書けます。
タイルセットのカスタムデータレイヤーに walkable (Bool) を追加しておく前提です。
extends Node2D
@onready var tile_layer: TileMapLayer = $TileMapLayer
@onready var astar: AStarGrid2D = AStarGrid2D.new()
func _ready() -> void:
var rect := tile_layer.get_used_rect()
astar.region = rect
astar.cell_size = Vector2(tile_layer.tile_set.tile_size)
astar.diagonal_mode = AStarGrid2D.DIAGONAL_MODE_AT_LEAST_ONE_WALKABLE
astar.update()
for x in range(rect.position.x, rect.end.x):
for y in range(rect.position.y, rect.end.y):
var coord := Vector2i(x, y)
var data: TileData = tile_layer.get_cell_tile_data(coord)
if data == null or not data.get_custom_data("walkable"):
astar.set_point_solid(coord, true)
set_point_solid(coord, true) でそのセルを「絶対に通れない」マスにします。後から壁を壊したいゲームなら、同じメソッドを false で呼び直すだけで通行可能に戻せます。
対角移動の制御
diagonal_mode には4つのオプションがあり、地味に挙動が違います。
| 値 | 挙動 |
|---|---|
DIAGONAL_MODE_ALWAYS |
常に対角移動できる |
DIAGONAL_MODE_NEVER |
4方向のみ |
DIAGONAL_MODE_AT_LEAST_ONE_WALKABLE |
隣接2マスのうち1つでも通れれば対角OK |
DIAGONAL_MODE_ONLY_IF_NO_OBSTACLES |
隣接2マス両方が通れる場合のみ対角OK |
タワーディフェンスのように「壁の角をすり抜けるのを防ぎたい」場合は ONLY_IF_NO_OBSTACLES がベストです。逆にRPGなどで滑らかに動かしたい場合は AT_LEAST_ONE_WALKABLE が無難です。
実例:
S . . #
. . . #
. . . .
# . . G
# を壁として、AT_LEAST_ONE_WALKABLE だと S → 右下 → 右下 → 下 で角を斜めに抜けられます。ONLY_IF_NO_OBSTACLES だと一度下に降りる必要があり、距離が伸びます。
コスト調整:泥地・道・水
すべてのマスのコストが同じだとつまらないので、地形ごとに重みをつけたいケースは多いです。たとえば:
- 道(道路):コスト 1.0
- 草原:コスト 1.5
- 泥地:コスト 3.0
これは set_point_weight_scale(coord, weight) で設定できます。
for coord in mud_tiles:
astar.set_point_weight_scale(coord, 3.0)
for coord in road_tiles:
astar.set_point_weight_scale(coord, 0.8)
weight_scale はそのセルを通過するコストの倍率です。1.0未満を設定するとそのセルは「お得」になり、AIは積極的にそこを通る経路を選びます。RTSで道路に沿って歩兵が動くのはこの仕組みです。
ハマりポイント:region と座標系
AStarGrid2D の region は Rect2i で、position は左上のセル、size はセル数です。グリッドの始点が (0, 0) ではなく負の座標から始まる場合(TileMapLayer ではよくある)、region.position を負の値で初期化する必要があります。
# TileMapLayer の使用範囲が (-5, -3) から (10, 8) の場合
astar.region = tile_layer.get_used_rect() # そのまま渡せる
set_point_solid などで指定する座標も、region の座標系に従います。Vector2i(0, 0) が必ずしもグリッドの左上ではないことに注意してください。
パフォーマンスのヒント
数百マス規模なら update() を毎フレーム呼んでも問題ないことが多いですが、数万マス規模になると初期化コストが無視できなくなります。実用的なテクニック:
-
update()の呼び直しは静的な変更時のみ:壁が増減したときだけ呼ぶ。経路探索のたびに呼ばない。 -
動的な障害物は
set_point_solidで局所更新:update()を呼び直すよりはるかに速い。 - 長い経路は分割する:100マス以上の経路を毎フレーム再計算するくらいなら、目標地点まで20マスごとに区切って探索するほうが安い。
-
方向だけ知りたいなら
get_id_path一択:get_point_pathはワールド座標を返す分、get_id_pathより少し重い。
まとめ
- グリッドベースのゲームなら
AStarGrid2D一択。 - TileMapLayer のカスタムデータレイヤーで
walkableを持たせると、障害物処理が数行で書ける。 - 対角移動は
DIAGONAL_MODE_ONLY_IF_NO_OBSTACLESがタワーディフェンス向け、AT_LEAST_ONE_WALKABLEがRPG向け。 - コスト調整は
set_point_weight_scaleで。1.0 未満は「お得」、1.0 超は「重い」。 - 数万マスのパフォーマンスは、
update()の頻度と経路の分割でほぼコントロールできる。
AStarGrid2D 単体でできることはこれくらいですが、これだけで戦略ゲームのコアロジックの大半は組めてしまいます。次のステップとしては、複数AIが同じ目標に殺到したときの分散(flow field やローカル回避)あたりを調べると面白いはずです。