【Godot】GodotのRayCast2D入門:崖の手前で引き返し、壁越しには気づかない敵を作る

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

RayCast2Dで足元・壁・視線を調べ、崖の手前で引き返し、壁越しには気づかない巡回敵を作る方法を解説します。

横スクロールで巡回する敵には、崖の手前で引き返す、壁の手前で引き返す、壁の裏のプレイヤーには気づかない、の3つが欲しくなります。Godot では、RayCast2D で線を1本伸ばし、その線で最初に当たる物を調べて作ります。

床に立っているかを答える is_on_floor() で止めようとすると、落ちる直前まで true のままなので、敵は床の端から落ちます。距離だけで気づかせると、壁越しのプレイヤーにも寄ってきます。

この記事では、RayCast2D で足元・壁・視線を調べ、崖の手前で引き返し、壁越しには気づかない敵を作る方法を解説します。線で足りないときに使う ShapeCast2D も紹介します。

崖のふちで下へ伸ばした線と、壁で止まる視線。敵は崖の手前で引き返し、壁の裏のプレイヤーには気づかない

この記事でわかること

  • RayCast2D が答える「最初に当たった物」の読み方
  • 崖・壁・視線を、線の置き方と答えの読み方で調べ分ける方法
  • 向きを変えたときに線もついてくる置き方と、結果をその場で更新する方法
  • 自分の体やプレイヤーを壁と間違えない、マスクの分け方

動作確認環境: Godot 4.7

Sponsored

RayCast2Dの仕組み

RayCast2D は、ノードの位置から target_position まで見えない線を伸ばし、その線で最初に当たった物を1つ答えるノードです。この線を レイ(光線)と呼び、線を伸ばして調べることをレイキャストと呼びます。

RayCast2D は原点から target_position まで線を伸ばし、最初に当たった物と、当たった点と面の向きを答える。奥の物は答えない

答えは4つのメソッドで読みます。

  • is_colliding(): 何かに当たったか
  • get_collider(): 最初に当たった相手
  • get_collision_point(): 当たった点(ノードからではなく、ゲームの世界全体で測った位置)
  • get_collision_normal(): 当たった面の向き

当たった点と面の向きは、is_colliding() が true のときだけ使います。外れたフレームでも、前に当たったときの値が残っているからです。

線は物理フレーム(物理の計算を1回進める単位。既定で毎秒60回)ごとに撃ち直されるので、結果は _physics_process() の中で読みます。置いたばかりの RayCast2D は、target_position が (0, 50) で、真下へ50ピクセルの線です。

他のエンジンでいうと: 線を伸ばして最初に当たった相手を調べる、Unity の Physics2D.Raycast や Unreal Engine の Line Trace by Channel に近い機能です。

Debug メニューの Visible Collision Shapes をオンにして実行すると、線が矢印で見えます。矢印は当たった所で止まらず、いつも target_position まで描かれます。何かに当たっているあいだは赤くなります。

崖・壁・視線の調べ方

is_on_floor() が教えてくれるのは、直前の move_and_slide()(速度に合わせて体を動かし、壁や床で止める関数)で床に触れたかどうかです。崖の手前で知りたいのは「一歩先の下に、まだ床があるか」なので、聞いていることが違います。

そこで、体の少し前から真下へ線を伸ばします。同じ RayCast2D でも、置き方を変えると3つのことが調べられます。

  • 崖: つま先の少し先から真下へ。当たらなければ、その先に床が無い
  • 壁: 体の前へ短く。当たれば、すぐ前に壁がある
  • 視線: 目から相手へ。最初に当たったのが相手なら、あいだに何も無い
崖は当たらないことで、壁は当たることで、視線は最初に当たった相手で読む。同じ線でも置き方と読み方が違う

is_on_wall() でも壁は分かりますが、それは体が壁にぶつかってからです。線なら、ぶつかる手前で引き返せます。

線の向きとローカル座標

target_position は、RayCast2D から見た位置(ローカル座標)です。線はノードと一緒に動き、親を回せば線も回ります。

困るのは、敵が向きを変えるときです。Sprite2D の flip_h で絵だけ裏返しても、線は右を向いたままです。左へ歩きながら右の壁を調べ、左の崖は見逃します。

絵だけ裏返すと線は右を向いたまま。Pivot の scale.x を -1 にすると、絵と線が一緒に左を向く

Sprite2D と線は、まとめて Node2D(ここでは Pivot)の子にします。Pivot の scale.x を -1 にすると、絵と線が一緒に裏返ります。CharacterBody2D そのものは等倍のままにしておきます。体の当たり判定や移動に影響させないためです。

結果が更新されるタイミング

RayCast2D が線を撃ち直すのは、その物理フレームで親の _physics_process() が終わったあとです。物理の処理はツリーの上から順に回るので、子の線は親のあとに順番が来ます。スクリプトで target_position を書き換えて、すぐに get_collider() を読むと、返ってくるのは書き換える前の線の結果です。

親の処理が終わってから線が撃ち直されるので、書き換えた直後は前の答え。force_raycast_update() のあとなら今の答え

その場で結果が要るときは、読む前に force_raycast_update() を呼びます。いまの線で撃ち直して、結果を入れ替えてくれます。視線のように、毎フレーム相手へ向け直す線で使います。巡回用の線は、向きを変えたフレームには結果を読まないので、呼ばなくて構いません。

Sponsored

当てる相手の選び方(collision_mask)

線が当たるのは、collision_mask(どのレイヤーの相手に当たるか)にチェックしたレイヤーの相手だけです。レイヤーとマスクの考え方は Collision Layer と Mask 入門 にあります。

壁を調べる線がプレイヤーにも当たると、敵はプレイヤーを壁だと思って、その手前で引き返します。壁用の線のマスクは、地形のレイヤーだけにします。逆に視線の線には、地形とプレイヤーの両方を入れます。壁が入っていないと、線が壁を素通りしてプレイヤーに当たってしまうからです。

壁用の線にプレイヤーのレイヤーを入れると、プレイヤーの手前で引き返す。地形だけにすれば素通りして、壁の手前で引き返す

自分の体にも気をつけます。線が自動で外すのは、直属の親の当たり判定だけです(exclude_parent)。Pivot の下に置いた線から見ると直属の親は Pivot なので、敵の体は外れていません。敵の体は専用のレイヤーに置き、線のマスクに入れないでおきます。

ShapeCast2D:幅のある形で調べる

線には太さがありません。体ならまたげる小さな隙間でも、足元の線はそこへ落ちて「床が無い」と答えます。

細い線は小さな隙間に落ちて床が無いと答える。体と同じ幅の形なら、隙間の両側の床に触れて床があると答える

ShapeCast2D は、線の代わりに形を動かして調べるノードです。shape プロパティ(子の CollisionShape2D ではなく、ShapeCast2D 自身の項目)に入れた形を target_position まで動かし、最初にぶつかった位置で触れている相手を集めます。足元を体と同じ幅の長方形で調べれば、隙間の両側の床に触れて「床がある」と答えます。

そのかわり崖では、形の一部が床に触れているあいだ「床がある」と答え続けるので、線よりふちの近くまで歩きます。崖の判定には線、隙間をまたぐ判定には形、と使い分けます。相手は複数返ってくるので、get_collision_count() で数を、get_collider(i) で i 番目の相手を読みます。

Sponsored

実践:崖の手前で引き返し、壁越しには気づかない敵を作る

横スクロールの巡回兵、ステルスアクションの警備員、メトロイドヴァニアの雑魚敵——足元と壁と視線を調べる敵は、ジャンルを問わず出てきます。ここでは床の上を往復し、崖と壁の手前で引き返し、壁の裏のプレイヤーには気づかない敵を作ります。

実践の場面。敵は崖と壁のあいだを往復し、足元と前の2本の線で引き返す。壁の裏のプレイヤーへの視線は壁で止まる

① 置くもの

新しいシーンから始めます。見た目の素材は使わず、Debug メニューの Visible Collision Shapes をオンにして、当たり判定と線を見ながら進めます。

まず Project Settings の Layer Names → 2D Physics で、レイヤー1を world、2を player、3を enemy と名付けます。

  • ルートに Node2D を置いて Main にし、main.tscn として保存して Main Scene に指定する
  • StaticBody2D を2つ置き、それぞれの子に CollisionShape2D を付けて RectangleShape2D を入れる。レイヤーは world のまま
名前PositionSize
Ground(480, 416)(640, 32)
Wall(704, 320)(32, 160)
  • CharacterBody2D を置いて Player にする。Position は (760, 376)。子に CollisionShape2D を付けて RectangleShape2D (32, 48) を入れる
  • Player の Collision → Layer は player だけにする(world のチェックを外す)。スクリプトは付けず、Groups ドックで player グループに入れる。グループはノードに付ける名札で、敵のスクリプトはこの名札からプレイヤーを探す
  • CharacterBody2D を置いて Enemy にし、Position を (400, 384) にする。Layer は enemy だけ、Mask は world だけにして、子に次を置く
ノード置き場所設定
CollisionShape2DEnemy の子RectangleShape2D (32, 32)
Pivot(Node2D)Enemy の子そのまま
Sprite2DPivot の子Texture に icon.svg、Scale (0.25, 0.25)
FloorCheck(RayCast2D)Pivot の子Position (20, 0)、Target Position (0, 24)、Mask は world
WallCheck(RayCast2D)Pivot の子Target Position (24, 0)、Mask は world
Sight(RayCast2D)Enemy の子Position (0, -8)、Mask は world と player
Alert(Label)Enemy の子Text は !、Position (-4, -44)、Visible はオフ

床は x が 160〜800 で、上面が y=400 です。壁は床の右寄り(x が 688〜720)に立ち、プレイヤーは壁の右側の床に立っています。敵の体は 32×32 なので、中心が y=384 のときちょうど床に乗ります。

② 組み立て

Enemy にスクリプトを付けます。

# enemy.gd(Enemy の CharacterBody2D)
extends CharacterBody2D

const SPEED := 80.0          # 巡回の速さ(ピクセル/秒)
const GRAVITY := 980.0
const SIGHT_RANGE := 120.0   # これより遠いプレイヤーには気づかない

var direction := 1.0         # 1 で右、-1 で左

@onready var pivot: Node2D = $Pivot
@onready var floor_check: RayCast2D = $Pivot/FloorCheck
@onready var wall_check: RayCast2D = $Pivot/WallCheck
@onready var sight: RayCast2D = $Sight
@onready var alert: Label = $Alert
@onready var player: Node2D = get_tree().get_first_node_in_group("player")

func _physics_process(delta: float) -> void:
	if not is_on_floor():
		velocity.y += GRAVITY * delta

	if _can_see_player():
		alert.visible = true
		velocity.x = 0.0                          # 気づいたら立ち止まる
	else:
		alert.visible = false
		var no_floor := not floor_check.is_colliding()
		if is_on_floor() and (no_floor or wall_check.is_colliding()):
			direction = -direction                # 一歩先に床が無いか、前に壁があれば引き返す
			pivot.scale.x = direction             # 絵と2本の線をまとめて裏返す
		velocity.x = direction * SPEED

	move_and_slide()

func _can_see_player() -> bool:
	sight.target_position = sight.to_local(player.global_position)   # 線の先をプレイヤーへ向ける
	sight.force_raycast_update()                                     # 向けた線で、いま撃ち直す
	var close := global_position.distance_to(player.global_position) <= SIGHT_RANGE
	return close and sight.get_collider() == player                  # 近くて、最初に当たったのがプレイヤー

FloorCheck は、体の前の端(中心から16ピクセル)より4ピクセル先から、下へ24ピクセル伸びます。始点は床の上面より16ピクセル上なので、床の中から撃つことはなく、先は上面より8ピクセル下まで届きます。8ピクセルより深い下り段差は、崖と同じに扱って引き返します。Pivot を裏返すと、FloorCheck は体の反対側の前へ移り、WallCheck も反対を向きます。

視線だけは Pivot の外に置き、毎フレームプレイヤーへ向け直します。向きは線の先で決めるので、裏返す必要がないからです。そのかわり、背中側のプレイヤーにも気づきます。sight.to_local() は、ワールドでの位置を Sight から見た位置に直す関数です。プレイヤーのワールドでの位置(global_position)をこれに通してから target_position に入れると、線の先がプレイヤーに届きます。向け直したら、force_raycast_update() で撃ち直してから相手を比べます。

③ 動かして確かめる

壁の裏にいると、敵は x≈180 と x≈664 で引き返して気づかない。壁の左へ移すと、x≈379 で止まって ! を出す

F5 で実行し、敵の動きと ! を見ます。見るのは3つです。

敵は右へ歩き、中心が x≈664 に来たところで引き返します。WallCheck の先が壁に届いた瞬間で、体と壁のあいだにはまだ8ピクセルほどの隙間があります。左へ戻ると、中心が x≈180 で FloorCheck が床を外れ、崖のふちの手前で引き返します。落ちるなら、FloorCheck の Mask か Target Position が違っています。

壁の手前で折り返すとき、プレイヤーまでは約96ピクセルで、気づく距離の120ピクセルより近くにいます。それでも ! は出ません。目からプレイヤーへ向けた線が、先に壁に当たっているからです。Sight の矢印は壁を突き抜けてプレイヤーまで伸び、赤く光っています。赤は何かに当たった印なので、相手が誰かは ! で見ます。

仕上げに、Player の Position を (260, 376)(壁の左の床の上)に変えて実行します。敵はまず右へ歩いて壁で引き返し、戻ってきてプレイヤーまで120ピクセルの位置(中心 x≈379)で止まり、頭の上に ! を出します。実行してから7秒ほどです。止まらずに通り過ぎるなら、Player の Layer か Sight の Mask に player が入っていません。

線も気づく距離もさっきと同じで、変わったのはあいだに壁が無いことだけです。

ポイントは2つです。

  • 線の答えは、置き方で読み分ける: 崖は当たらないこと、壁は当たること、視線は最初の相手が誰かで判断します。同じ RayCast2D でも、見る答えが違います
  • 向け直した線は、その場で撃ち直す: target_position を変えたら、読む前に force_raycast_update()。巡回用の線は Pivot ごと裏返せば、次のフレームから新しい向きで答えます

この敵にプレイヤーを追いかけさせるなら、状態の切り替えは ステートマシン入門 の形がそのまま使えます。

うまくいかないときは

症状原因直し方
平らな床の上で、向きを変え続ける線の始点が床の中に埋まっている。始点を含む形には、既定では当たらない(hit_from_inside がオフ)始点を床の外へ出す。体の中心の高さから、床に届く長さで下へ伸ばす
コードで collision_mask = 3 にしたら、レイヤー3が見えないマスクの数値は、レイヤー1が1、2が2、3が4、と倍々の値の合計。3 はレイヤー1と2set_collision_mask_value(3, true) のように番号で指定する
壁の角で ! が点いたり消えたりする線が壁の角をかすめ、フレームごとに最初の相手が入れ替わる数フレーム続けて見えたときだけ、気づいた扱いにする
Area2D で作った相手に線が当たらないcollide_with_areas が既定でオフ線の Collide With → Areas をオンにする

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

  • ノードを置かずに撃つ: 弾の着弾のように、その場限りの線はコードからも撃てます。PhysicsRayQueryParameters2D.create() で問い合わせを作り、get_world_2d().direct_space_state の intersect_ray() に渡します。マスクを省くと全レイヤーに当たるので、当てたい相手のレイヤーだけを指定します。書く場所は _physics_process() の中です
  • 正面だけ見る敵にする: この記事の敵は、背中側のプレイヤーにも気づきます。正面だけにするなら、プレイヤーが direction の側にいるか、の条件を1つ足します
  • 3Dでも置き方は同じ: RayCast3D も、線の置き方と読み方は変わりません。3Dは上が +Y なので、真下へ伸ばす線は (0, -1, 0) の向きになります。視界を扇形にする作り方は ステルス視界 にあります
Sponsored

まとめ

  • 1本の線は、最初の1つだけ答える: 奥に何があっても、返ってくるのは手前の相手
  • 崖・壁・視線は置き方の違い: 崖は当たらないこと、壁は当たること、視線は相手が誰かで読む
  • 向きは Pivot ごと裏返し、向け直した線は撃ち直す: target_position はローカル座標。同じフレームで読むなら force_raycast_update()
  • マスクで相手を選ぶ: 壁用の線は地形だけ、視線は地形とプレイヤー
  • 細い線で足りなければ ShapeCast2D: 小さな隙間は、幅のある形で調べる

まずは RayCast2D を1本置き、Visible Collision Shapes をオンにして実行してください。矢印が赤くなる位置を見ながら線を置けば、崖も壁も視線も同じ書き方で調べられます。いま作っている敵に足りないのは、足元・前・視線のどの線でしょうか。

次に読む学習パスの次の記事
物理・移動

move_and_slideとmove_towardの使い分け:ノックバック処理の実装

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