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) の子に RayCast2D と Line2D を置く構成を想定しています。
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 シーンを作る
- 新規シーンを作成し、ルートに Node2D を置いて
LaserBeamとリネーム。 - その子に RayCast2D と Line2D を追加。
- 上記の
LaserBeam.gdをルートのLaserBeam (Node2D)にアタッチ。 RayCast2Dの Target Position はスクリプトが上書きするので、初期値はなんでもOK。
シーン構成はこんな感じになります。
LaserBeam (Node2D) ├── RayCast2D └── Line2D
このシーンを LaserBeam.tscn として保存しておきましょう。
手順②:プレイヤーに LaserBeam をアタッチする
例として、横スクロールアクションのプレイヤーにレーザーを持たせる構成です。
Player (CharacterBody2D) ├── Sprite2D ├── CollisionShape2D └── LaserBeam (Node2D) ← コンポーネントとしてアタッチ
Playerシーンを開き、子ノードとして LaserBeam.tscn をインスタンス化。LaserBeamノードの位置を「銃口のあたり」に移動。- インスペクタで
direction = (1, 0)(右向き)などを設定。 - プレイヤーのスクリプト側で、マウスクリック時に
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 開発スタイルが作れますね。
