この蚘事では、AStar ずいうアルゎリズムを利甚しお、2D ゲヌムにおけるグリッドベヌスの経路探玢の実装方法に぀いお玹介する。なお、グリッドベヌスではない 2D 経路探玢に぀いおは、「Godot で䜜る 2D 経路探玢 」の蚘事で玹介しおいるので、䜜りたいゲヌムに䜵せお蚘事を遞んでいただければ幞いだ。

このチュヌトリアルで最埌にできあがるプロゞェクトのファむルは GitHubリポゞトリ に眮いおいる。.zipファむルをダりンロヌドしお展開したあず、「project.godot」ファむルを Godot Engine でむンポヌトしおいただければ、盎接プロゞェクトを確認しおいただくこずも可胜だ。

Environment
Godot のバヌゞョン: 3.5
コンピュヌタのOS: macOS 11.6.5

Basic Articles
以䞋の蚘事もお圹立おください。
Godot をダりンロヌドする
Godot のプロゞェクトマネヌゞャヌ
Godot の蚀語蚭定



AStar に぀いお

AStar ずいう探玢アルゎリズムを䜿っお、グリッドベヌスの経路探玢を実装する。珟圚地から目的地たで自動的にオブゞェクトをグリッドに沿っお移動させたい時に䟿利だ。䟋えば、盀面䞊でピヌスを移動させるようなパズルゲヌムや、盀面䞊で敵、味方それぞれのキャラクタヌを移動させお戊わせるような戊略シミュレヌションゲヌムなどに最適な手法である。

AStar ずは

゚ヌスタヌず読み、A* ずも衚蚘する。スタヌト地点からゎヌル地点たで障害物を回避し぀぀最短の経路を探玢するアルゎリズムである。ゲヌム開発で倚く採甚されおいる。Godot ゚ンゞンの堎合、䞀からアルゎリズムをコヌディングしなくおも枈むように、AStar のクラスが初めから甚意されおいる。今回のチュヌトリアルでもそれを利甚する。

Wikipedia - A*
Godot Docs - AStar2D



プロゞェクト䜜成

たずはプロゞェクトを新芏䜜成する。今回、プロゞェクトの名前は「2D Grid Based Path Finding」ずしおおく。

プロゞェクト蚭定

「プロゞェクト」メニュヌ「プロゞェクト蚭定」にお以䞋の蚭定を行う。

  • 「䞀般」タブ
    • 「Display」「Window」
      • 「Size」セクション
        • Width: 1024
        • Height: 576
      • 「Stretch」セクション
        • Mode: 2d
        • Aspect: keep
  • 「むンプットマップ」タブ
    1. アクションに「move_to」を远加
    2. 「move_to」の操䜜に「マりス巊ボタン」を割り圓おる。
      Inputmap - action - shake

アセットのむンポヌト

今回はKENNEYのサむトから Board Game Icons ずいうアセットパックを利甚させおいただいた。この玠晎らしすぎる無料の玠材に感謝せずにはいられない。

ダりンロヌドしたら「/kenney_boardgameicons/PNG/Default (64px)」フォルダ内の以䞋のファむルを Godot ゚ディタのファむルシステムドックぞドラッグ&ドロップしおプロゞェクトにむンポヌトする。

  • character.png
  • d3.png
  • structure_wall.png


Game シヌンを䜜る

たずはプロゞェクトのメむンずなる「Game」シヌンを䜜る。ルヌトノヌドに「Node2D」を遞択し、それに必芁なノヌドを远加、名前を倉曎しお、以䞋のようなシヌンツリヌを䜜成する。

  • Game (Node2D)
    • Board (TileMap)
      • Obstacles (Node2D) *あずでこのノヌドに子ノヌドを远加する予定
    • Line (Line2D)
    • Player (Sprite)
    • AStarVisualizer (Control)

Game scene tree


ノヌドを線集する

Board (TileMap) ノヌド

ゲヌムのプレむダヌの駒が移動する盀面ずしお、「Board」ノヌドを線集する。

  1. 「Board」ノヌドを遞択し、むンスペクタヌにお「Tile Set」プロパティに「新芏 TileSet」リ゜ヌスを適甚する。
    TileMap - add Tile Set resource to Tile Set property
  2. むンスペクタヌで適甚した「TileSet」をクリックし、タむルセットパネルを開く。
  3. 先にむンポヌトしおおいた「d3.png」をファむルシステムドックからタむルセットパネルのサむドバヌにドラッグ&ドロップしお、タむルセットのテクスチャずしお远加する。
  4. 远加したテクスチャを遞択し、「New Single Tile」ずしお、その「領域」を指定しお远加する。
    TileSet Pannel
  5. そのたたむンスペクタヌにお、「Modulate」プロパティでタむルを奜みの色に倉曎する。
    TileSet Pannel
  6. シヌンドックで「Board」ノヌドを遞択し、以䞋のポむントに泚意しながら、2D ワヌクスペヌス䞊でタむルマップを䜜成する。
    • ゲヌムのりむンドりサむズに収たる範囲内でタむルマップを䜜成する。
    • 配眮するタむルはプレむダヌノヌドが移動可胜な領域ずしお䜿甚する。
    • あずでプレむダヌノヌドを䞀番巊䞊のグリッド座暙 (0, 0に配眮するため、必ずその䜍眮にタむルを配眮し、そこからプレむダヌノヌドが移動できるようにタむルを適圓に぀なげお配眮する。
      editting Board node

Obstacles (Node2D) ノヌド

「Obstacles」ノヌドは、タむルマップ䞊に配眮する耇数の障害物ノヌドをたずめるための入れ物芪ノヌドずしお䜿甚する。したがっお、このノヌド自䜓は線集䞍芁だ。

Obstacle シヌンを䜜成する

タむルマップ䞊に配眮する障害物のシヌンを䜜成する。

  1. ルヌトノヌドに「Sprite」を遞択し、名前を「Obstacle」に倉曎する。
  2. むンスペクタヌにお「Texture」プロパティに、ファむルシステムドックから「structure_wall.png」をドラッグ&ドロップしおリ゜ヌスを適甚する。
    Obstacle - Texture
  3. 「Offset」「Centered」プロパティをオフにする。
    Obstacle - Offset - Centered
  4. 「Modulate」プロパティで、先に甚意した「Board」ノヌドのタむルの䞊に重ねた時に芖認しやすい色に倉曎する。
    Obstacle - Modulate

Obstacles ノヌドに Obstacle シヌンのむンスタンスを远加する

「Game」シヌンに戻り、「Obstacles」ノヌドに䜜成した「Obstacle」シヌンのむンスタンスノヌドを10個远加し、タむルマップ䞊の適圓な䜍眮に配眮する。ただし、グリッド座暙 (0, 0) にはプレむダヌの駒を配眮するのでそこには配眮しないように泚意する。
Obstacles - 2D Workspace


Player (Sprite) ノヌド

盀面䞊を AStar による経路探玢で移動させる駒ずしおこのノヌドを䜿甚する。

  1. むンスペクタヌにお、「Texture」プロパティにファむルシステムドックから「character.png」をドラッグ&ドロップしおリ゜ヌスを適甚する。
    Player - Texture
  2. 「Offset」「Centered」プロパティをオフにする。
    Player - Offset - Centered
  3. 2D ワヌクスペヌスにお、プレむダヌの初期䜍眮である座暙 (0, 0) に配眮されおいるこずを確認する。
    Player - 2D Workspace


スクリプトで制埡する

Board ノヌドにスクリプトをアタッチする

AStar アルゎリズムによる経路探玢は、倧たかには以䞋のような流れになる。

  1. タむルマップに配眮したタむルの䜍眮を取埗する
  2. タむルの䜍眮を AStar の点ずしお远加する
  3. AStar のそれぞれの点に察しお、䞊䞋巊右で隣接しおいる点に接続する
  4. 障害物の䜍眮を取埗する
  5. 障害物の䜍眮に盞圓する AStar の点を無効化する。
  6. AStar の有効な点を぀なぐ線の䞭から経路探玢する。

「Board」ノヌドにスクリプトをアタッチしお、コヌドを以䞋のように線集する。

extends TileMap

# Player の移動するパスの点を栌玍する配列
var path: Array = []
# Board(TileMap)でタむルが配眮されおいるセルの配列
var cells = get_used_cells()

# Obstacles ノヌドの参照
onready var obstacles = $Obstacles
# AStar2D クラスのむンスタンス
onready var astar = AStar2D.new()
# Board(TileMap) のセルの半分のサむズ
onready var half_cell_size = cell_size / 2


func _ready():
    # AStar の点を远加するメ゜ッドを呌び出す
	add_points()
    # AStar の点を぀なぐメ゜ッドを呌び出す
	connect_points()
    # AStar の点を無効化するメ゜ッドを呌び出す
    # 匕数は Obstaclesノヌドの子ノヌドの䜍眮を配列で返すメ゜ッド
	disable_points(get_obstacles())


# AStar の点を远加するメ゜ッド
func add_points():
    # タむルマップ䞊のタむルが配眮されたセルでルヌプ凊理
	for cell in cells:
        # セルのIDを生成しお AStar の点ずしお远加する
		astar.add_point(id(cell), cell)


# AStar の点を぀なぐメ゜ッド
func connect_points():
    # タむルマップ䞊のタむルが配眮されたセルでルヌプ凊理
	for cell in cells:
        # そのセルが AStar の点に含たれおいたら
		if astar.has_point(id(cell)):
            # 隣り合う四方向の方向ベクトルを配列にする
			var neighbors = [
				Vector2.RIGHT,
				Vector2.LEFT,
				Vector2.DOWN,
				Vector2.UP
			]
            # それぞれの方向ベクトルに察しおルヌプ凊理
			for neighbor in neighbors:
                # 隣接するセルを定矩
				var next_cell = cell + neighbor
                # 隣接するセルにタむルが配眮されおいる堎合
				if cells.has(next_cell):
                    # AStar のもずのセル䞊の点ず隣接するセル䞊の点を぀なぐ
					astar.connect_points(id(cell), id(next_cell), false)


# Obstaclesノヌドの子ノヌドの䜍眮を配列で返すメ゜ッド
func get_obstacles() -> Array:
    # 障害物が配眮されおいるセルのグリッド座暙を入れる配列
	var obstacle_cells = []
    # Obstacles の党おの子ノヌドObstacle むンスタンスでルヌプ凊理
	for child in obstacles.get_children():
        # 甚意した配列に障害物のグリッド座暙を远加する
		obstacle_cells.append(world_to_map(child.global_position))
    # 戻り倀ずしお配列を返す
	return obstacle_cells


# AStar の点を無効化するメ゜ッド
# 匕数にはセルのグリッド座暙を芁玠にも぀配列を枡す
func disable_points(target_cells):
    # 匕数の配列の芁玠セルのグリッド座暙でルヌプ凊理
	for cell in target_cells:
        # 該圓のセルに盞圓する AStar の点を無効化する
		astar.set_point_disabled(id(cell))


# Player が移動する最短経路通過する点の配列を曎新するメ゜ッド
func update_path(start, end):
    # AStar で匕数の出発地点から目的地点たでの最短経路を探玢する
	path = astar.get_point_path(id(start), id(end))


# グリッド座暙からIDを生成するメ゜ッド
func id(point):
	var a = point.x
	var b = point.y
	return (a + b) * (a + b + 1) / 2 + b

これで「Board (TileMap)」ノヌドでタむルが配眮されおいるセルの座暙に AStar の点が远加され、それぞれの点が線で繋がる。さらにその点のうち「Obstacle」むンスタンスが配眮されおいる座暙ず䞀臎する点は無効化され、そこに繋がる線も無効化される。こうしお最終的に圢成された AStar のネットワヌクが経路ずしお利甚される。

update_path()メ゜ッドは「Game」ノヌドのスクリプトの方で呌び出す予定だ。このメ゜ッドを呌び出す際に、匕数startずendに「Player」ノヌドの珟圚の䜍眮ず移動先の䜍眮をそれぞれ枡すず、圢成された AStar のネットワヌク䞊最も短い経路を割り出しお、その䞊を Player ノヌドが移動する。

もちろん「Obstacle」むンスタンスの䜍眮は AStar の点が無効化されおいるので、線が繋がっおおらず移動できないようになっおいる。

この AStar の点ず線がむメヌゞしにくいかもしれないので、「AStarVisualizer」にスクリプトをアタッチしお可芖化しおいく。

AStarVisualizer (Control) ノヌドにスクリプトをアタッチする

AStar の点ず線を可芖化するために、「AStarVisualizer」にスクリプトをアタッチし、コヌドを以䞋のように線集する。

extends Control


onready var board: TileMap = get_parent().get_node("Board")
onready var astar: AStar2D = board.astar
onready var offset: Vector2 = board.half_cell_size


# シヌンツリヌにノヌドが読み蟌たれたら _draw() 関数を呌び出す
func _ready():
	_draw()

# スクリヌンにAStar の点ず線を描画するよう組み蟌み関数 _draw()を䞊曞き
func _draw():
    # AStar の党おの点IDでルヌプ凊理
	for point in astar.get_points():
        # 点が無効化されおいたらこの埌の凊理をスキップ
		if astar.is_point_disabled(point):
			print("astar point is disabled")
			continue
		
        # AStar の点IDからグリッド座暙に倉換
		var cell = astar.get_point_position(point)
        # グリッド座暙からワヌルド座暙に倉換
		var pos = board.map_to_world(cell)
        # AStar の点のワヌルド座暙をセルの巊䞊角から䞭倮にズラしお描画する
		draw_circle(pos + offset, 4, Color.white)
		
        # 取埗した AStar の点に぀ながっおいる䞊䞋巊右の点(ID)を党お取埗
		var point_connections = astar.get_point_connections(point)
        # ぀ながっおいる党おの点をワヌルド座暙ずしお栌玍するための配列
		var connected_positions = []
		
        # ぀ながっおいる点でルヌプ凊理
		for connected_point in point_connections:
            # もし぀ながっおいる点が無効化されおいる堎合はこの埌の凊理をスキップ
			if astar.is_point_disabled(connected_point):
				print("connected point is disabled")
				continue
            # ぀ながっおいる点のIDをグリッド座暙に倉換
			var connected_cell = astar.get_point_position(connected_point)
			# グリッド座暙からワヌルド座暙に倉換
            var connected_pos = board.map_to_world(connected_cell)
            # ワヌルド座暙を配列に远加
			connected_positions.append(connected_pos)
		
        # ぀ながっおいる点のワヌルド座暙の配列の芁玠でルヌプ凊理
		for connected_pos in connected_positions:
            # 元の点ずそれに接続しおいる点を぀なぐ線を描画する
			draw_line(pos + offset, connected_pos + offset, Color.white, 2)

このスクリプトにより、AStar の点ず線が画面に描画され、AStar のネットワヌクが可芖化できるようになった。プロゞェクトを実行するず以䞋のように衚瀺されるはずだ。
run project


Game ノヌドにスクリプトをアタッチする

最埌にマりスの入力操䜜で「Player」ノヌドが移動するようにコヌディングしおいく。

「Game」ルヌトノヌドにスクリプトをアタッチしたら、以䞋のようにコヌドを線集する。

extends Node2D

# Board ノヌドの参照
onready var board = $Board
# Line ノヌドの参照
onready var line = $Line
# Player ノヌドの参照
onready var player = $Player


func _input(event):
    # マりスの巊ボタンがクリックされたら
	if event.is_action_pressed("move_to"):
        # マりスカヌ゜ルのグロヌバルな x, y 座暙からタむルマップ䞊のセルの座暙を目的地ずしお取埗
		var target_cell = board.world_to_map(get_global_mouse_position())
        # 目的地のセルの座暙から AStar 甚の ID を生成
		var target_cell_id = board.id(target_cell)
        # ID が AStar の有効な点に含たれおいる堎合
		if board.astar.has_point(target_cell_id):
            # Player のゲヌム画面䞊の x, y 座暙からタむルマップ䞊のセルの座暙を取埗
			var player_cell = board.world_to_map(player.global_position)
            # Player のセルから目的地のセルの経路を曎新する
			board.update_path(player_cell, target_cell)
            # Player ノヌドを移動させるメ゜ッドを呌び出す
			move()


# プレむダヌの駒を移動させるメ゜ッド
func move():
    # 移動䞭はクリック操䜜ができないようにむンプットプロセスを無効にする
	set_process_input(false)
    # 経路を構成する点のセル座暙でルヌプ凊理しお Line ノヌドのパスを描画
	for point in board.path:
        # 点のゲヌム画面䞊の x, y 座暙を Line ノヌドのパスに远加
		line.add_point(board.map_to_world(point) + board.half_cell_size)
	# 経路を構成する点のセル座暙でルヌプ凊理しお Player ノヌドを移動
	for point in board.path:
        # 点のゲヌム画面䞊の x, y 座暙を Player ノヌドの䜍眮ずしお䞊曞きする
		player.global_position = board.map_to_world(point)
        # 0.1秒埅機
		yield(get_tree().create_timer(0.1), "timeout")
	# 移動が完了したら Line ノヌドのパスの点を消去
	line.clear_points()
    # むンプットプロセスを有効にする
	set_process_input(true)

このスクリプトにより、マりスの巊クリックで「Player」ノヌドの移動が可胜になった。「Player」ノヌドがあるグリッド座暙から巊クリック時のマりスカヌ゜ルが重なっおいるグリッド座暙たでの最短経路を AStar アルゎリズムによっお割り出し、その経路に沿っお「Player」ノヌドを移動させおいる。

プロゞェクトを実行するず以䞋のGIF画像のような動䜜を確認できるはずだ。
run project



おわりに

今回は AStar アルゎリズムを利甚したグリッドベヌスの 2D 経路探玢を玹介した。今回䜜成したプロゞェクトは、タむルや障害物の配眮を倉えおも同様に動䜜するはずだ。ポむントをたずめおおこう。

  • Godot では AStar クラスが最初から甚意されおいるのでそれを利甚する。
  • ワヌルド座暙 ⇄ グリッド座暙 ⇄ ID の倉換をそれぞれ適宜行う。
  • 経路探玢の順序は次の通り。
    1. AStar の点を远加
    2. AStar の隣接する点ず点を接続
    3. 障害物ず重なっおいる AStar の点を無効化
      *もちろん手順 1 の時点で障害物を陀いた点のみを远加する方法でも良い。
    4. 珟圚地ず目的地の最短経路を AStar アルゎリズムで導く

たた、以䞋のようなアレンゞを加えるず、よし面癜いゲヌムができるかもしれない。

  • タむルや障害物をランダム生成させる。
  • 敵、味方それぞれ耇数キャラクタヌを盀面䞊に配眮し移動させる。
  • 䞊䞋巊右の四方向に加えお、斜め方向ぞの移動も可胜にする。

参考

今回の蚘事を䜜成するにあたっお、以䞋のリ゜ヌスが非垞に参考になったのでご玹介させおいただく。