Godot 4で「レーザーっぽい攻撃」を作ろうとすると、だいたいこんな流れになりますよね。

  • RayCast2D をプレイヤーや砲台ノードにベタ書きで実装
  • Line2D を同じシーンに置いて、RayCast2D の長さに合わせて毎フレーム更新
  • 敵へのダメージ処理もそのスクリプトに直書き

最初はそれで動くんですが、

  • 敵もレーザーを撃ちたい
  • 動く床もレーザーを出したい
  • レーザーの見た目だけ変えたい

…といった要望が増えてくると、「レーザー処理をコピペしまくったスクリプト地獄」になりがちです。さらに、RayCast2D と Line2D の設定があちこちに散らばり、どこを直せばいいのか分かりづらくなります。

そこで今回は、RayCast2D と Line2D をひとまとめにした「LaserBeam」コンポーネントを用意して、プレイヤーでも敵でも、どんなノードにもポン付けできるレーザーを目指しましょう。「継承より合成」で、LaserBeam をアタッチするだけでレーザー機能を付与できるようにします。

【Godot 4】RayCast2D+Line2Dでスマートにレーザー実装!「LaserBeam」コンポーネント

コンポーネントの概要

LaserBeam コンポーネントは、以下を担当します。

  • RayCast2D:レーザーのヒット判定(障害物までの距離を取得)
  • Line2D:RayCast2D のヒット位置までの線を描画
  • 方向・長さ・レイヤーなどを @export で柔軟に設定可能
  • ヒットした相手への処理(ダメージなど)を シグナルで通知

これを 1 つのシーン/スクリプトとして完結させ、プレイヤー・敵・ギミックに「コンポーネントとして」アタッチして使う構成にします。


フルコード:LaserBeam.gd

以下は、Node2D をベースにしたコンポーネント実装です。シーンとしては LaserBeam (Node2D) の子に RayCast2DLine2D を置く構成を想定しています。


extends Node2D
class_name LaserBeam
## LaserBeam コンポーネント
## RayCast2D を使って障害物に当たるまでの直線を Line2D で描画し、
## ヒット情報をシグナルで通知するコンポーネントです。

## === エディタで設定するパラメータ ==============================

@export_category("Laser Settings")

@export var enabled: bool = true:
	set(value):
		enabled = value
		# レーザーの有効/無効に応じて Line2D の表示を切り替える
		if is_inside_tree():
			if _line:
				_line.visible = enabled
			if _ray:
				_ray.enabled = enabled

## レーザーの最大射程(ピクセル単位)
@export var max_distance: float = 800.0

## レーザーの方向(ローカル座標系)。右向き(1,0)を基準に、親ノードの回転と合わせて使う想定
@export var direction: Vector2 = Vector2.RIGHT

## レーザーの太さ
@export var width: float = 3.0

## レーザーの色
@export var color: Color = Color(1.0, 0.1, 0.1, 1.0)

## レーザーの更新を毎フレーム行うかどうか
## true: _process() で常に RayCast2D を更新
## false: 手動で update_laser() を呼び出す方式
@export var auto_update: bool = true

@export_category("Collision Settings")

## RayCast2D のコリジョンマスク
## どのレイヤーのオブジェクトに当たるかを指定
@export_flags_2d_physics var collision_mask: int = 1

## 自分自身や親のコリジョンを無視するかどうか
@export var exclude_parent: bool = true

## 一度に複数のヒットを取りたい場合は true(Godot 4 の RayCast2D は
## 複数ヒットをサポートしているが、ここでは先頭のみ利用)
@export var hit_multiple: bool = false

@export_category("Damage / Callback")

## 1秒あたりのダメージ量など、ゲーム側の都合に合わせて使う
## コンポーネント側では値をそのままシグナルに載せるだけ
@export var damage_per_second: float = 10.0

## レーザーがヒットしている間、毎フレームシグナルを飛ばすかどうか
@export var emit_signal_every_frame: bool = true


## === シグナル ==================================================

## レーザーが何かに当たったとき
## target: ヒットしたオブジェクト(CollisionObject2D 等)
## position: ワールド座標でのヒット位置
## normal: 法線ベクトル
## damage_per_second: エディタで設定した値をそのまま渡す
signal hit(target, position: Vector2, normal: Vector2, damage_per_second: float)

## レーザーが何にも当たっていないときに通知したい場合
signal miss()


## === 内部変数 ==================================================

var _ray: RayCast2D
var _line: Line2D


func _ready() -> void:
	## 子ノードの参照を取得
	_ray = $RayCast2D
	_line = $Line2D

	## RayCast2D の基本設定
	_ray.collision_mask = collision_mask
	_ray.enabled = enabled
	_ray.hit_from_inside = true
	_ray.collide_with_areas = true
	_ray.collide_with_bodies = true

	if exclude_parent and owner and owner is CollisionObject2D:
		## 親のコリジョンを除外して、自己ヒットを防ぐ
		_ray.add_exception(owner)

	## Line2D の見た目を初期化
	_line.width = width
	_line.default_color = color
	_line.visible = enabled

	## 初期状態のレーザーを描画
	update_laser()


func _process(delta: float) -> void:
	if not enabled:
		return
	if auto_update:
		update_laser(delta)


## === パブリック API =============================================

## レーザーの状態を更新し、RayCast2D と Line2D を同期させる
## delta はダメージ計算等に使いたい場合に利用(ここでは未使用)
func update_laser(delta: float = 0.0) -> void:
	if not is_inside_tree():
		return

	## RayCast2D の先端位置を更新
	var dir := direction.normalized()
	var target_local_position := dir * max_distance
	_ray.target_position = target_local_position

	## RayCast2D を再計算
	_ray.force_raycast_update()

	## Line2D のポイント配列を更新
	_line.clear_points()
	_line.add_point(Vector2.ZERO) # 始点はコンポーネントの原点

	var hit_position: Vector2
	var hit_normal: Vector2
	var hit_object: Object = null

	if _ray.is_colliding():
		hit_position = to_local(_ray.get_collision_point())
		hit_normal = _ray.get_collision_normal()
		hit_object = _ray.get_collider()

		## ヒット位置まで線を引く
		_line.add_point(hit_position)

		## シグナル送信
		if emit_signal_every_frame:
			emit_signal("hit", hit_object, _ray.get_collision_point(), hit_normal, damage_per_second)
	else:
		## 何にも当たっていない場合は最大距離まで描画
		_line.add_point(target_local_position)
		if emit_signal_every_frame:
			emit_signal("miss")


## レーザーを一時的にオン・オフするためのヘルパー
func set_enabled(value: bool) -> void:
	enabled = value
	if _line:
		_line.visible = enabled
	if _ray:
		_ray.enabled = enabled


## レーザーの色を動的に変更したい場合のヘルパー
func set_color(new_color: Color) -> void:
	color = new_color
	if _line:
		_line.default_color = color


## レーザーの方向をワールド座標から設定するヘルパー
## 例: マウス方向を向かせたいときなど
func look_at_world_position(world_pos: Vector2) -> void:
	var global_origin := global_position
	direction = (world_pos - global_origin).normalized()

使い方の手順

手順①:LaserBeam シーンを作る

  1. 新規シーンを作成し、ルートに Node2D を置いて LaserBeam とリネーム。
  2. その子に RayCast2DLine2D を追加。
  3. 上記の LaserBeam.gd をルートの LaserBeam (Node2D) にアタッチ。
  4. RayCast2DTarget Position はスクリプトが上書きするので、初期値はなんでもOK。

シーン構成はこんな感じになります。

LaserBeam (Node2D)
 ├── RayCast2D
 └── Line2D

このシーンを LaserBeam.tscn として保存しておきましょう。

手順②:プレイヤーに LaserBeam をアタッチする

例として、横スクロールアクションのプレイヤーにレーザーを持たせる構成です。

Player (CharacterBody2D)
 ├── Sprite2D
 ├── CollisionShape2D
 └── LaserBeam (Node2D)   ← コンポーネントとしてアタッチ
  1. Player シーンを開き、子ノードとして LaserBeam.tscn をインスタンス化。
  2. LaserBeam ノードの位置を「銃口のあたり」に移動。
  3. インスペクタで direction = (1, 0)(右向き)などを設定。
  4. プレイヤーのスクリプト側で、マウスクリック時に enabled を切り替えるなどの制御を行います。

プレイヤースクリプトの例:


extends CharacterBody2D

@onready var laser: LaserBeam = $LaserBeam

func _process(delta: float) -> void:
	# 左クリックでレーザーON/OFF
	if Input.is_action_just_pressed("shoot"):
		laser.set_enabled(true)
	if Input.is_action_just_released("shoot"):
		laser.set_enabled(false)

	# マウス方向を向くレーザーにしたい場合
	var mouse_pos := get_global_mouse_position()
	laser.look_at_world_position(mouse_pos)

手順③:敵に LaserBeam を再利用する

同じコンポーネントをそのまま敵にも付けられます。

TurretEnemy (Node2D)
 ├── Sprite2D
 ├── CollisionShape2D
 └── LaserBeam (Node2D)

敵のスクリプト例:


extends Node2D

@onready var laser: LaserBeam = $LaserBeam
@export var fire_interval: float = 2.0

var _timer: float = 0.0

func _ready() -> void:
	# ヒットした相手にダメージを与える
	laser.hit.connect(_on_laser_hit)

func _process(delta: float) -> void:
	_timer += delta
	if _timer >= fire_interval:
		_timer = 0.0
		laser.set_enabled(true)
	else:
		laser.set_enabled(false)

	# 常に右方向を向く固定砲台
	laser.direction = Vector2.RIGHT


func _on_laser_hit(target, position: Vector2, normal: Vector2, dps: float) -> void:
	# ターゲットが HP を持っているならダメージを与える
	if "apply_damage" in target:
		target.apply_damage(dps * get_process_delta_time())

このように、LaserBeam コンポーネント自体は「ダメージの中身」までは知らず、シグナルで「当たったよ」というイベントだけを投げます。ダメージ処理は各ゲームオブジェクト側に委ねることで、コンポーネントの再利用性が高くなります。

手順④:動く床やギミックにもアタッチする

例えば、常に床の端から下方向にレーザーを出して、プレイヤーが触れるとアウト、というギミックも簡単です。

DeathFloor (StaticBody2D)
 ├── CollisionShape2D
 └── LaserBeam (Node2D)

スクリプト例:


extends StaticBody2D

@onready var laser: LaserBeam = $LaserBeam

func _ready() -> void:
	laser.direction = Vector2.DOWN
	laser.set_enabled(true)
	laser.hit.connect(_on_laser_hit)

func _on_laser_hit(target, position: Vector2, normal: Vector2, dps: float) -> void:
	if target.name == "Player":
		target.call_deferred("die")  # プレイヤー側に die() がある想定

メリットと応用

LaserBeam コンポーネントを導入することで、次のようなメリットがあります。

  • シーン構造がスッキリする
    レーザー関連のノード(RayCast2D / Line2D)が 1 つのシーンにまとまるので、プレイヤーや敵のシーンは「LaserBeam を 1 ノード追加するだけ」で済みます。
  • 継承地獄を回避できる
    「レーザーを撃てるプレイヤー」「レーザーを撃てる敵」といった専用クラスを作らなくても、既存のキャラクターに LaserBeam をコンポーネントとして付けるだけで機能を拡張できます。
  • 見た目と判定を一元管理
    RayCast2D と Line2D の設定を 1 箇所に閉じ込めているので、レーザーの太さ・色・射程・コリジョンマスクを変えるときも、このコンポーネントだけ触ればOKです。
  • レベルデザインが楽になる
    ステージ上にレーザー砲台やトラップをポンポン置くだけで動くので、シーンエディタで「置いて、向きと射程を変える」だけの作業にできます。

改造案:レーザーの点滅アニメーションを追加する

例えば、レーザーが「チャージ中は薄く、発射中は明るく光る」ような演出を入れたい場合、コンポーネント側に簡単なアニメーション関数を追加するのもアリです。


## LaserBeam.gd に追加できる簡易アニメーション例
var _blink_time: float = 0.0
@export var blink_speed: float = 4.0
@export var blink_intensity: float = 0.5

func animate_blink(delta: float) -> void:
	_blink_time += delta * blink_speed
	var t := (sin(_blink_time) * 0.5 + 0.5) * blink_intensity
	var base_color := color
	var animated_color := Color(
		base_color.r + t,
		base_color.g + t * 0.2,
		base_color.b,
		base_color.a
	)
	if _line:
		_line.default_color = animated_color

そして _process(delta) 内で animate_blink(delta) を呼び出せば、レーザーがビカビカ光るようになります。もちろん、これも「コンポーネント側に閉じ込めておく」ことで、すべてのレーザー持ちオブジェクトに一括で適用できます。

こんな感じで、「LaserBeam」コンポーネントをベースに、プロジェクトごとの演出やルールを少しずつ足していくと、継承に頼らない気持ちいい Godot 4 開発スタイルが作れますね。