Godot 4で暗いダンジョンや夜のステージを作るとき、PointLight2D を直接プレイヤーにペタッと貼り付けて、スクリプトもプレイヤーに全部書いてしまう……みたいな構成になりがちですよね。
最初はそれでも動きますが、

  • プレイヤーと敵で「ほぼ同じランタン処理」をコピペしてしまう
  • 燃料制限や点滅演出を追加したら、あちこちのスクリプトを修正する羽目になる
  • シーンツリーが「深い継承+ネストしまくり」でカオスになる

こういう「継承&肥大化したプレイヤースクリプト問題」を避けるために、ランタンはランタンとして1つのコンポーネントに切り出してしまいましょう。
今回は、PointLight2D を内包しつつ、燃料制限付きのランタンを実装する LanternLight コンポーネントを作っていきます。

【Godot 4】暗闇をスマートに制御!「LanternLight」コンポーネント

この LanternLight コンポーネントは、ざっくり言うと:

  • 任意のノード(プレイヤー、敵、動く足場など)にアタッチできる
  • 内部で PointLight2D を自動生成して制御する
  • 燃料量・消費速度・自動点灯/消灯・フェードアウト演出をエクスポート変数で調整できる

つまり、「光る」「燃料が減る」「切れたら暗くなる」というロジックを全部コンポーネント側に閉じ込めて、プレイヤー側はシンプルなままに保てる構成です。


フルコード:LanternLight.gd


extends Node2D
class_name LanternLight
## ランタン用コンポーネント
## 任意の親ノード(プレイヤー、敵、ギミックなど)にアタッチして使う。
##
## 内部で PointLight2D を自動生成し、
## 燃料制限やフェードアウト演出を管理する。

@export_group("基本設定")
@export var enabled: bool = true:
	set(value):
		enabled = value
		_update_light_enabled()
@export var light_color: Color = Color(1, 0.95, 0.8, 1.0) ## 暖色系の色
@export var texture: Texture2D ## 任意のライト用テクスチャ(無くてもOK)
@export var energy: float = 1.2 ## ライトの明るさ
@export var range: float = 320.0 ## ライトの半径(PointLight2D.energy + scale で表現)

@export_group("燃料設定")
@export var use_fuel: bool = true ## false にすると無限ランタンになる
@export var max_fuel: float = 60.0 ## 最大燃料(秒換算で使うと分かりやすい)
@export var fuel: float = 60.0 ## 現在燃料。シーンごとに初期値を変えてもOK
@export var fuel_consumption_per_second: float = 1.0 ## 1秒あたりの燃料消費量
@export var auto_turn_off_when_empty: bool = true ## 燃料0で自動消灯するか

@export_group("演出設定")
@export var fade_when_empty: bool = true ## 燃料切れ時にフェードアウトするか
@export var fade_duration: float = 1.0 ## フェードアウトにかける時間(秒)
@export var flicker_enabled: bool = true ## チラつき演出を有効にするか
@export var flicker_strength: float = 0.15 ## エネルギー変動の強さ
@export var flicker_speed: float = 8.0 ## チラつきの速さ

@export_group("デバッグ")
@export var debug_draw_fuel: bool = false ## デバッグ用に画面上に残燃料を表示するか

## 内部用変数
var _light: PointLight2D
var _base_energy: float
var _fade_timer: float = 0.0
var _is_fading_out: bool = false

func _ready() -> void:
	# ライトノードを自動生成して、このコンポーネントの子にする
	_light = PointLight2D.new()
	add_child(_light)

	# 初期設定を反映
	_light.color = light_color
	_light.texture = texture
	_light.energy = energy
	_base_energy = energy
	_light.enabled = enabled

	# range は scale で表現(テクスチャのサイズに依存するため、相対的な扱い)
	var scale_factor := range / 256.0
	_light.scale = Vector2.ONE * scale_factor

	# fuel が 0 以下なら最初から消灯させる
	if use_fuel and fuel <= 0.0:
		_turn_off_immediately()

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

	# 燃料システム
	if use_fuel and fuel > 0.0:
		_consume_fuel(delta)
	elif use_fuel and fuel <= 0.0 and auto_turn_off_when_empty:
		_start_fade_out()

	# フェードアウト処理
	if _is_fading_out:
		_process_fade_out(delta)

	# チラつき演出
	if flicker_enabled and not _is_fading_out and _light.enabled:
		_apply_flicker(delta)

	# デバッグ描画
	if debug_draw_fuel:
		queue_redraw()

func _draw() -> void:
	if debug_draw_fuel:
		var text := use_fuel \
			? "Fuel: %.1f / %.1f" % [fuel, max_fuel] \
			: "Fuel: ∞"
		draw_string(
			get_theme_default_font(),
			Vector2(0, -8),
			text,
			HORIZONTAL_ALIGNMENT_LEFT,
			-1.0,
			14.0,
			Color.WHITE
		)

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

## ランタンを強制的に ON にする
func turn_on() -> void:
	enabled = true
	_is_fading_out = false
	_fade_timer = 0.0
	_light.energy = _base_energy
	_update_light_enabled()

## ランタンを強制的に OFF にする
func turn_off() -> void:
	enabled = false
	_is_fading_out = false
	_fade_timer = 0.0
	_update_light_enabled()

## 燃料を追加する(回復アイテム用など)
func add_fuel(amount: float) -> void:
	if not use_fuel:
		return
	fuel = clamp(fuel + amount, 0.0, max_fuel)
	# 燃料が復活したので、必要ならフェード状態を解除
	if fuel > 0.0 and enabled:
		_is_fading_out = false
		_light.energy = _base_energy

## 燃料をリセットする(チェックポイントやリスタート用)
func reset_fuel() -> void:
	if not use_fuel:
		return
	fuel = max_fuel
	_is_fading_out = false
	_fade_timer = 0.0
	_light.energy = _base_energy
	enabled = true
	_update_light_enabled()

# === 内部処理 ===

func _consume_fuel(delta: float) -> void:
	fuel -= fuel_consumption_per_second * delta
	if fuel < 0.0:
		fuel = 0.0

func _start_fade_out() -> void:
	if not fade_when_empty:
		_turn_off_immediately()
		return
	if _is_fading_out:
		return
	_is_fading_out = true
	_fade_timer = 0.0

func _process_fade_out(delta: float) -> void:
	if fade_duration <= 0.0:
		_turn_off_immediately()
		return

	_fade_timer += delta
	var t := clamp(_fade_timer / fade_duration, 0.0, 1.0)
	# 緩やかなカーブ(イージング)で暗くする
	var eased := 1.0 - pow(t, 2.0)
	_light.energy = _base_energy * eased

	if t >= 1.0:
		_turn_off_immediately()

func _turn_off_immediately() -> void:
	_is_fading_out = false
	_fade_timer = 0.0
	enabled = false
	_update_light_enabled()
	_light.energy = 0.0

func _apply_flicker(delta: float) -> void:
	# シンプルなノイズベースのチラつき
	var time := Time.get_ticks_msec() / 1000.0
	var noise := sin(time * flicker_speed) * 0.5 + 0.5 # 0..1
	var variation := (noise - 0.5) * 2.0 * flicker_strength
	_light.energy = _base_energy * (1.0 + variation)

func _update_light_enabled() -> void:
	if not is_instance_valid(_light):
		return
	_light.enabled = enabled and (not use_fuel or fuel > 0.0)

使い方の手順

コンポーネントなので、「どのノードにも同じ手順で付けられる」のがポイントです。ここでは代表的な3パターンを例にします。

手順① スクリプトを用意する

  1. 上記コードを LanternLight.gd という名前で保存します(例: res://components/LanternLight.gd)。
  2. Godotエディタを再読み込みすると、ノード追加ダイアログの「スクリプト」タブから LanternLight が選べるようになります。

手順② プレイヤーにランタンを付ける例

典型的な2Dアクションのプレイヤー構成を想定します。

Player (CharacterBody2D)
 ├── Sprite2D
 ├── CollisionShape2D
 └── LanternLight (Node2D)  ← このコンポーネントを追加
  1. Player シーンを開く。
  2. Player の子として Node2D を追加し、名前を LanternLight に変更。
  3. そのノードに LanternLight.gd をアタッチ(もしくはクラス名から直接追加)。
  4. インスペクタで以下を調整:
    • enabled: true
    • use_fuel: true(燃料制限を使いたい場合)
    • max_fuel: 120.0(例:120秒ぶん)
    • fuel: 120.0
    • fuel_consumption_per_second: 0.5(= 1秒で0.5消費 → 4分持つ)
    • texture: ライト用のグラデーションテクスチャがあれば設定

プレイヤーの移動スクリプト側は、ランタンの存在を知らなくても動きます。
「プレイヤーがチェックポイントを通過したら燃料を全回復したい」などのときだけ、明示的に触りましょう。


# Player.gd の一部例
func restore_lantern_fuel() -> void:
	var lantern := $LanternLight as LanternLight
	if lantern:
		lantern.reset_fuel()

手順③ 敵に持たせるたいまつとして使う例

Enemy (CharacterBody2D)
 ├── Sprite2D
 ├── CollisionShape2D
 └── LanternLight (Node2D)
  • 敵のシーンに同じ LanternLight を子として追加。
  • 敵はプレイヤーより弱い光にしたい場合:
    • energy: 0.8
    • range: 220.0
    • max_fuel: 30.0(すぐ消えるたいまつ)

こうすると、プレイヤーと敵で全く同じコンポーネントを共有しつつ、パラメータだけで「雰囲気の違う光源」を作れます。

手順④ 動く足場にランタンを吊るす例

MovingPlatform (Node2D)
 ├── Sprite2D
 ├── CollisionShape2D
 └── LanternLight (Node2D)
  • use_fuel: false(動く足場は無限に光っていてほしい場合)
  • enabled: true

この場合、燃料システムは完全にスキップされるので、「燃料制限なしモード」として同じコンポーネントを再利用できます。


メリットと応用

この LanternLight コンポーネントを使う最大のメリットは、「光」と「燃料」という概念を1つの独立した部品に閉じ込められることです。

  • プレイヤーや敵のスクリプトから「燃料管理ロジック」が消えるので、責務がスッキリする
  • ライトを持つノードを増やしても、「子に LanternLight を足すだけ」で済む
  • シーン階層は浅いまま、「光る/光らない」を後付けで合成できる
  • 燃料制限の有無を use_fuel 1つで切り替えられるので、レベルデザインの試行錯誤がしやすい

たとえば「序盤ステージは無限ランタンで安心させて、中盤から燃料制限をONにする」といった調整も、パラメータを変えるだけで実現できます。

改造案:燃料が少なくなったら自動で点滅を強くする

終盤のドキドキ感を演出したいなら、「燃料が少なくなったらチラつきが激しくなる」ようにしても面白いですね。
以下のような関数を LanternLight に追加して、_process() から呼び出すだけでOKです。


## 燃料残量に応じてフリッカー強度を変える(残り少ないほど激しく)
func _update_flicker_by_fuel() -> void:
	if not use_fuel or max_fuel <= 0.0:
		return

	var ratio := fuel / max_fuel # 0.0 ~ 1.0
	# 残り 30% を切ったあたりから徐々に強く
	if ratio < 0.3:
		var t := clamp((0.3 - ratio) / 0.3, 0.0, 1.0) # 0.0 ~ 1.0
		flicker_strength = lerp(0.15, 0.5, t)
	else:
		flicker_strength = 0.15

_process(delta) の最後あたりで _update_flicker_by_fuel() を呼べば、
燃料が残りわずかになると、ランタンが心許なくチラつき始める演出が簡単に追加できます。

このように、コンポーネントとして切り出しておくと、「光の演出を変えたい」と思ったときに、1ファイルだけいじればプロジェクト全体に反映できるのが強みですね。
継承で巨大なプレイヤークラスを育てるより、こういう小さな部品をどんどん合成していくスタイルで、Godot 4ライフを快適にしていきましょう。