【Godot】Godotの_draw入門:索敵範囲と向かう先を実行中の画面に描いて、敵AIを調べる

作成: 2026-09-23最終更新: 2026-10-10

敵AIの気づく距離と向かう先を_draw()で実行中の画面に描き、当たり判定の表示と重ねて、判定とのずれを見つけます。

敵AIの気づく距離や、いま向かっている先を確かめたいときは、実行中の画面に円や線で描くと一目で分かります。Godot では、ノードの _draw() に数行書くだけで、円・線・文字を画面に描けます。

print で距離を出しても、180、150 と数字が流れるだけで、気づく範囲がどこまで届いているのかは見えません。描いておけば、範囲や向きのずれがその場で見つかります。

この記事では、_draw() で敵AIの気づく距離・向かう先・状態を画面に描き、当たり判定の表示と重ねてずれを見つける方法を解説します。

敵のまわりに大きい輪郭の円と小さい半透明の円、頭上に札。大きい円の縁にプレイヤーが立ち、敵から旗へ線が伸びる

この記事でわかること

  • _draw() が呼ばれるときと、queue_redraw() の使い方
  • 描くときの座標と、相手の位置を to_local() で渡す方法
  • Debug メニューの表示と、自分で描く表示の使い分け
  • 描画用の子ノードを分ける理由
  • 実践:見張りの敵に円・線・状態名を描き、気づかない原因を見つけて直す

動作確認環境: Godot 4.7

Sponsored

_draw() と queue_redraw()

_draw() は、_process() のように毎フレーム呼ばれる関数ではありません。呼ばれるのは、ノードが初めて画面に出るとき、非表示から表示に戻ったとき、queue_redraw() で描き直しを頼んだときなどです。描いた絵は、次に描き直すまでそのまま使われます。

描いた円・線・札はノードと一緒に動くが、線の向きは描いたときのまま相手を外れ、queue_redrawで描き直すと相手に届く

描いた円・線・文字は、ノードに貼り付いたまま一緒に動きます。変わらないのは、描いたときの値で決まる部分です。プレイヤーへ向けた線の向きと長さ、状態の文字の中身がそうです。こうした値が変わるなら、_process() で毎フレーム queue_redraw() を呼びます。同じノードで同じフレームに何度呼んでも、描き直しは1回にまとめられます。

func _process(_delta: float) -> void:
	queue_redraw()   # 値が変わるので、毎フレーム描き直しを頼む

func _draw() -> void:
	draw_circle(Vector2.ZERO, 200.0, Color.SKY_BLUE, false, 2.0)   # 輪郭だけの円

描く関数は _draw() の中で呼びます。_process() で直接 draw_circle() を呼ぶと、円は描かれず、「Drawing is only allowed inside this node's _draw()...」というエラーが出ます。Godot 3 の記事にある update() は、4 では queue_redraw() に名前が変わりました。

他のエンジンでいうと: 状態を図形で確かめる用途では、Unity の Gizmos(OnDrawGizmos)や UE の Draw Debug にあたります。ただし Godot の _draw() はふつうの2D描画なので、製品版で消したい表示は自分で消します。

描くときの座標(to_local())

_draw() の座標は、描いているノードを原点にした位置(ローカル座標)です。Vector2.ZERO はそのノード自身の位置を指します。敵のまわりに円を描くなら、中心は Vector2.ZERO です。

つまずくのは、相手の位置へ線を引くときです。プレイヤーの global_position(ワールド全体の原点から測った位置)を、そのまま終点に渡したとします。線は、描いているノードの位置のぶんだけずれた場所を指します(ノードを回転や拡大していれば、そのぶんもずれます)。終点は to_local() で、描いているノードから見た位置に直してから渡します。

draw_line(Vector2.ZERO, to_local(player.global_position), Color.SKY_BLUE, 2.0)
global_positionのまま渡した線はずれた場所を指し、to_local()で直した線は相手に届く

Debug メニューの表示(Visible Collision Shapes)

エディタ上部の Debug メニューには、エンジンが自分で描いてくれる表示が並んでいます。敵AIを調べるときにまず使うのは Visible Collision Shapes です。押し返さずに重なった相手を知らせる Area2D の範囲も含めて、CollisionShape2D の形を半透明で描きます。ほかに、道すじの Visible Paths、ナビゲーションの Visible Navigation と Visible Avoidance もあります。

チェックは、ゲームを実行するときに読まれます。実行中に切り替えたときは、止めて実行し直します。

エンジンが描くのは、当たり判定のような「エンジンが知っている形」です。敵の気づく距離の変数や、いま向かっている先、状態の名前は、エンジンは知りません。そこを自分で描きます。

半透明の当たりの円はエンジンが描き、輪郭の円・向かう先の線・状態の札は自分で描く

2つを重ねると、描いた値と実際の判定が同じかどうかを、画面で見比べられます。自分で描いた円と、エンジンが描く当たりの円が重ならないなら、変数と判定のどちらかがずれています。

Sponsored

描画用の子ノード(DebugDraw)

描く処理は敵のスクリプトに書かず、DebugDraw という描画用の子ノードに分けます。

理由の1つは重なり順です。親が _draw() で描いた絵には、子の Sprite2D が上から重なります。敵本体に円を描くと、重なった部分がスプライトに隠れます。Scene ドックで DebugDraw を Sprite2D より下に並べると、あとから描かれるぶん、スプライトの上に重なります。

もう1つは、出し入れのしやすさです。描く処理が1つのノードにまとまっていれば、visible を切り替えるだけで消せます。queue_redraw() も、_draw() を持っている DebugDraw 自身で呼びます。敵の側で呼んでも、DebugDraw は描き直されません。書き出すときの保存画面には、開発用の機能を含めるかどうかの Export With Debug があります。これを外して書き出したゲームでは、開発用の版かどうかを返す OS.is_debug_build() が false になります。そこで _ready() で、ノードごと消しておきます。エディタから実行しているあいだは true なので、円はそのまま出ます。

EnemyにSprite2D・Sight・DebugDrawを並べると、DebugDrawがいちばん上に描かれ、F3で出し入れできる

実践:見張りの敵に、気づく距離・向かう先・状態を描く

ステルスゲームの見張り、見下ろしアクションの巡回する敵、タワーディフェンスの砲台——どれも「どこまで気づくか」「いまどこへ向かうか」「いまどの状態か」の3つで動いています。巡回して、気づいたら追う敵を作り、その3つを画面に描きます。描いた円と当たりの円を重ねて、敵が気づかない原因を見つけます。

敵のまわりに大きい円と半透明の当たりの円、頭上にPATROL。プレイヤーは大きい円の内側なのに、線は旗を向く

① 置くもの

新しい2Dのプロジェクトで始めます。画面の大きさは既定の 1152×648 のままです。

  • main.tscn: ルートは Node2D(名前 Main)。Project Settings の Application → Run → Main Scene に指定する。子に Player と Enemy を置く
  • Player: CharacterBody2D(位置 (760, 184)。敵の真上、少し離れたところ)。Groups ドックで player グループに入れる。グループに入れておくと、敵は is_in_group("player") でプレイヤーかどうかを見分けられる。子に Sprite2D(icon.svg、Scale (0.25, 0.25))と CollisionShape2D(CircleShape2D、Radius 16)
  • Enemy: Node2D(位置 (760, 324))。子に Sprite2D(icon.svg、Scale (0.5, 0.5))、Sight(Area2D。子に CollisionShape2D(CircleShape2D、Radius 100))、DebugDraw(Node2D)の順に並べる
main.tscnはMainの下にplayerグループのPlayerと、Sprite2D・半径100のSight・DebugDrawを持つEnemy

見た目の大きさは Sprite2D の Scale だけで変え、ほかのノードは (1, 1) のままにします。実行する前に、Debug → Visible Collision Shapes をオンにしておきます。

② 組み立て

プレイヤーは、矢印キーで動くだけです。

# player.gd(Player に付ける)
extends CharacterBody2D

func _physics_process(_delta: float) -> void:
	velocity = Input.get_vector("ui_left", "ui_right", "ui_up", "ui_down") * 200.0
	move_and_slide()

敵は、左右の2点を行き来し、Sight にプレイヤーが入ったら追います。Sight の Area2D は、体(CharacterBody2D など)が重なると body_entered を出します。気づく距離は detect_radius の200のつもりです。

# enemy.gd(Enemy に付ける)
class_name Enemy
extends Node2D

enum State { PATROL, CHASE }

@export var detect_radius := 200.0   # 気づく距離(のつもり)
@export var speed := 80.0

var state := State.PATROL
var target := Vector2.ZERO           # いま向かっている場所(グローバル座標)
var player: Node2D = null
var patrol_points: Array[Vector2] = []
var patrol_index := 0

@onready var sight: Area2D = $Sight

func _ready() -> void:
	patrol_points = [global_position + Vector2(-120, 0), global_position + Vector2(120, 0)]
	target = patrol_points[patrol_index]
	sight.body_entered.connect(_on_sight_body_entered)
	sight.body_exited.connect(_on_sight_body_exited)

func _physics_process(delta: float) -> void:
	if state == State.CHASE:
		target = player.global_position
	else:
		target = patrol_points[patrol_index]
		if global_position.distance_to(target) < 4.0:
			patrol_index = (patrol_index + 1) % patrol_points.size()
	global_position = global_position.move_toward(target, speed * delta)   # target へ、最大 speed * delta だけ近づく

func _on_sight_body_entered(body: Node2D) -> void:
	if body.is_in_group("player"):
		player = body
		state = State.CHASE

func _on_sight_body_exited(body: Node2D) -> void:
	if body == player:
		player = null
		state = State.PATROL

DebugDraw は、敵の変数を読んで描くだけです。enemy.gd の1行目の class_name Enemy で、Enemy を型として書けるようにしてあります。出し入れに使う F3 は、この記事の実践で自分で決めたキーです。

# debug_draw.gd(DebugDraw に付ける)
extends Node2D

@onready var enemy: Enemy = get_parent()

func _ready() -> void:
	if not OS.is_debug_build():
		queue_free()                   # 書き出した製品版では消す

func _process(_delta: float) -> void:
	queue_redraw()                     # 向かう先や状態の変化を、表示に反映する

func _unhandled_input(event: InputEvent) -> void:
	if event is InputEventKey and event.pressed and not event.echo and event.keycode == KEY_F3:
		visible = not visible          # F3 で出し入れする

func _draw() -> void:
	var color := Color.ORANGE if enemy.state == Enemy.State.CHASE else Color.SKY_BLUE
	draw_circle(Vector2.ZERO, enemy.detect_radius, color, false, 2.0)   # 気づく距離(変数の値)
	draw_line(Vector2.ZERO, to_local(enemy.target), color, 2.0)        # いま向かっている先
	draw_string(ThemeDB.fallback_font, Vector2(-40, -48), Enemy.State.keys()[enemy.state])   # 状態の名前の一覧から、いまの名前を出す

③ 動かして確かめる

直す前は当たりが描いた円より小さくPATROLのまま。直したあとは円がそろい、オレンジの線がプレイヤーへ伸びる

Main を実行します。見るのは3つです。

まず、敵が左右の巡回点へ向かって行き来します。敵のまわりには自分で描いた円(半径200)と、それより小さい半透明の円(Sight の当たり、半径100)が出ます。プレイヤーのまわりにも、当たりの小さな円が出ます。頭上は「PATROL」で、線はいま向かっている巡回点を指しています。半透明の円が見えないなら、Visible Collision Shapes をオンにしてから実行し直します。

次に、プレイヤーの位置を見ます。プレイヤーは最初から大きい円の中にいるのに、敵は巡回を続けます。敵が左右のどこにいても、中心どうしの距離は140〜184です。プレイヤーの当たりまで入れても、小さい円には届きません。敵が気づく距離は、描いた200ではなく、当たりの100でした。

ゲームの画面をクリックして↓キーで近づき、プレイヤーの当たりが小さい円に重なると、「CHASE」に変わります。円と線がオレンジになり、線がプレイヤーへ伸びます。横へ逃げ続けると、プレイヤーのほうが速いので振り切れて、「PATROL」に戻ります。

仕上げに、enemy.gd の _ready() の先頭へ1行足して、実行し直します。

	($Sight/CollisionShape2D.shape as CircleShape2D).radius = detect_radius   # 当たりの円を、描いている円に合わせる

描いた円と当たりの円の中心と半径がそろい、実行した直後に「CHASE」になります。敵を何体も置くなら、この1行の前に形を複製しておきます(理由は「うまくいかないときは」の2行目)。プレイヤーにも半径16の当たりがあるので、気づく境目は中心どうしの距離で200+16=216です。最初の距離は140なので、もう範囲の中です。

ポイントは2つです。

  • 描くのは、判定が読んでいる値: 描いた円は detect_radius を読み、判定は Sight の円を読んでいました。別々の値を見ていると、描いた円と実際の判定が食い違います。直した1行で、起動したときに当たりの円が detect_radius に合わせられ、描く値と判定の値が同じになりました
  • エンジンの表示と重ねる: 自分で描いた円だけを見ていると、ずれに気づけません。Visible Collision Shapes の当たりと重ねたので、2つの円の差が見えました。見える角度や壁の判定を足すなら、視界判定入門 が続きです
Sponsored

うまくいかないときは

症状原因直し方
円が塗りつぶされて、重なったキャラクターが隠れるdraw_circle() の既定は塗りつぶし4つ目の引数を false、5つ目に線の太さ(2.0 など)を渡す
敵を複製して1体の半径を変えたら、全員の反応する距離が変わった複製した敵は、同じ CircleShape2D を共有している(形のデータは、ノードを複製しても同じ1つを指したまま)半径を書き換える前に、shape を shape.duplicate() した物へ差し替える
draw_string() でエラーになる、文字の位置がずれる最初の引数にフォントが要る。位置は文字の左端のベースライン(文字の下側の線)フォントは ThemeDB.fallback_font(テーマが用意している予備のフォント)を渡す。頭上に出すなら、Y を負の値にする
F3 で消しても、半透明の円は残る半透明の円は Visible Collision Shapes が描いているDebug メニューでオフにして、実行し直す

おまけ:先に知っておくと良いこと

  • 編集中にも円を見たい: スクリプトの先頭に @tool(エディタの中でもそのスクリプトを動かす印)を付けると、エディタの中でも _draw() を動かせます。この記事の DebugDraw は敵のスクリプトの値を読むので、@tool を1行足すだけでは動きません。組み方は エディタ拡張入門 にあります
  • 境界で状態がちらつくとき: 気づく円と見失う円を分け、見失う円を少し大きくします。2つの円を描けば、あいだの幅が見えます
  • ナビゲーションの経路を見たいとき: NavigationAgent2D は debug_enabled をオンにすると、計算した経路を線で描きます。使い方は NavigationAgent2D入門 にあります

まとめ

  • _draw() は頼んだときだけ: 描いた絵は残る。線の先や文字の値が変わるなら queue_redraw()
  • 座標は描いているノードから見た位置: 相手の位置は to_local() で渡す
  • エンジンが知っている形は Debug メニュー: 変数・向かう先・状態は自分で描く
  • 描画用のノードは子に分ける: スプライトの上に描け、出し入れが1か所で済む
  • 描くのは判定が読んでいる値: エンジンの表示と重ねて、ずれを見る

まずは気づく距離の円から描いてみてください。あなたの敵AIで、画面に出して確かめたい値はどれですか。

次に読む学習パスの次の記事
基礎

GDScriptの静的型付け徹底解説:パフォーマンス向上とバグ防止の秘訣

この記事を読む
Godot このセクションのノート104