0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Godot 4のAStarGrid2Dでグリッドパスファインディングを実装する

0
Posted at

タワーディフェンスや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 と座標系

AStarGrid2DregionRect2i で、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() を毎フレーム呼んでも問題ないことが多いですが、数万マス規模になると初期化コストが無視できなくなります。実用的なテクニック:

  1. update() の呼び直しは静的な変更時のみ:壁が増減したときだけ呼ぶ。経路探索のたびに呼ばない。
  2. 動的な障害物は set_point_solid で局所更新update() を呼び直すよりはるかに速い。
  3. 長い経路は分割する:100マス以上の経路を毎フレーム再計算するくらいなら、目標地点まで20マスごとに区切って探索するほうが安い。
  4. 方向だけ知りたいなら 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 やローカル回避)あたりを調べると面白いはずです。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?