Godot 4で「昼と夜の雰囲気」を表現しようとすると、つい次のような実装をしがちですよね。

  • 各シーン(プレイヤー、敵、建物、木…)それぞれに「時間によって色を変える」処理を書いてしまう
  • 共通化しようとしてベースシーンを作り、そこから全部継承していく → ノード階層がどんどん重くなる
  • 「このシーンだけ夜でも明るくしたい…」と思った瞬間、継承構造が崩壊してつらい

Godot標準のやり方(シーン継承+深いノード階層)でも実現はできますが、あとから仕様が変わったときに地獄になりがちです。
そこで今回は、「昼夜の色合い」をどのノードにも後付けできるコンポーネント DayNightTint を用意して、継承ではなく合成(Composition)で解決していきましょう。

【Godot 4】シーン全体を昼夜対応!「DayNightTint」コンポーネント

DayNightTint コンポーネントは、グローバルな時間情報(例: 0.0〜1.0 で一日の進行度を表す)を監視し、
アタッチされたノード(親ノード)の modulate を自動で昼用/夜用に補間してくれるシンプルなスクリプトです。

  • プレイヤーだけ夜に少し暗くする
  • 背景はガッツリ暗くする
  • 街灯は逆に夜に明るくする

といった調整を、コンポーネントをペタッと貼るだけで実現できます。


フルコード:DayNightTint.gd


extends Node
class_name DayNightTint
## 昼夜に応じて親ノードの modulate を自動調整するコンポーネント
##
## 想定:
## - グローバルな「時間管理シングルトン(TimeManager など)」があり、
##   0.0〜1.0 の day_time (一日の進行度) を公開している。
## - 0.0 = 深夜 / 0.25 = 朝 / 0.5 = 正午 / 0.75 = 夕方 / 1.0 = 深夜(=0.0に戻る)
##
## このコンポーネントは毎フレームその値を監視し、
## 親ノードの modulate を昼用カラーと夜用カラーの間で補間します。

@export var autostart: bool = true:
    ## true の場合、自動的に _ready で有効化します。
    ## false にしておけば、スクリプトから enable()/disable() を切り替えられます。
    set(value):
        autostart = value
        if is_inside_tree():
            _enabled = autostart

@export var time_manager_path: NodePath:
    ## グローバル時間を持つノードへのパス。
    ## 例: "/root/TimeManager"
    ## 空の場合は自動で /root/TimeManager を探しにいきます。
    ## 任意クラスでもよいですが、以下のプロパティを想定しています:
    ## - var day_time: float (0.0〜1.0)
    ## - signal day_time_changed(new_value: float) (任意。あれば利用)
    set(value):
        time_manager_path = value
        _resolve_time_manager()

@export_range(0.0, 1.0, 0.01)
var min_brightness: float = 0.3:
    ## 夜の最低明るさ(0.0〜1.0)
    ## 0.0 = 完全に真っ暗, 1.0 = 昼と同じ明るさ
    set(value):
        min_brightness = clamp(value, 0.0, 1.0)

@export var day_color: Color = Color(1, 1, 1, 1):
    ## 昼の基準カラー。
    ## 通常は白(1,1,1,1)でOK。少し黄色っぽくするなどの演出も可能。

@export var night_color: Color = Color(0.3, 0.35, 0.5, 1.0):
    ## 夜の基準カラー。
    ## デフォルトはやや青みがかった暗い色。

@export_range(0.0, 1.0, 0.01)
var night_strength: float = 1.0:
    ## 夜の暗さの強さ(0.0〜1.0)。
    ## 1.0 = 完全に night_color まで暗くする
    ## 0.5 = 昼と夜の中間程度の暗さ
    set(value):
        night_strength = clamp(value, 0.0, 1.0)

@export_range(0.0, 1.0, 0.01)
var dawn_dusk_width: float = 0.15:
    ## 朝焼け・夕焼けの「グラデーション幅」。
    ## 値が大きいほど、昼→夜の変化がゆっくりになります。
    set(value):
        dawn_dusk_width = clamp(value, 0.0, 0.5)

@export var use_smooth_step: bool = true:
    ## true の場合、昼夜の補間に smoothstep を使って滑らかにします。
    ## false の場合は単純な線形補間になります。

@export var affect_alpha: bool = false:
    ## true の場合、カラーだけでなく透明度(alpha)も補間します。
    ## 例: 夜になると少し透明にするなどの表現が可能。

@export var debug_print: bool = false:
    ## デバッグ用。true にすると、たまに現在の day_time と補間値をログ出力します。

var _time_manager: Node = null
var _enabled: bool = true
var _original_modulate: Color
var _has_original: bool = false

func _ready() -> void:
    _enabled = autostart
    _resolve_time_manager()
    _cache_original_modulate()
    # 初期状態を一度更新
    _update_tint()

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

func enable() -> void:
    ## コンポーネントを有効化
    _enabled = true

func disable() -> void:
    ## コンポーネントを無効化し、元の modulate に戻します。
    _enabled = false
    _restore_original_modulate()

func _resolve_time_manager() -> void:
    ## time_manager_path から時間管理ノードを取得
    if not is_inside_tree():
        return

    var path := time_manager_path
    if path.is_empty():
        # デフォルトで /root/TimeManager を探す
        path = NodePath("/root/TimeManager")

    if has_node(path):
        _time_manager = get_node(path)
    else:
        # ルート直下を直接探すパターン(オートローダー想定)
        var root := get_tree().root
        if root.has_node(path):
            _time_manager = root.get_node(path)
        else:
            _time_manager = null

    if _time_manager == null and debug_print:
        push_warning("[DayNightTint] TimeManager が見つかりません: %s" % str(path))

func _cache_original_modulate() -> void:
    ## 親ノードの元の modulate を保存しておく
    var parent := get_parent()
    if parent == null:
        return
    if parent.has_method("get_modulate"):
        _original_modulate = parent.get_modulate()
        _has_original = true
    elif "modulate" in parent:
        _original_modulate = parent.modulate
        _has_original = true
    else:
        if debug_print:
            push_warning("[DayNightTint] 親ノードに modulate がありません: %s" % [parent])

func _restore_original_modulate() -> void:
    ## 親ノードの modulate を元に戻す
    if not _has_original:
        return
    var parent := get_parent()
    if parent == null:
        return

    if parent.has_method("set_modulate"):
        parent.set_modulate(_original_modulate)
    elif "modulate" in parent:
        parent.modulate = _original_modulate

func _get_day_time() -> float:
    ## TimeManager から day_time を取得。なければ 0.5(=正午)を返す。
    if _time_manager == null:
        return 0.5
    if "day_time" in _time_manager:
        var t = float(_time_manager.day_time)
        # 0〜1 にクランプし、ループさせる
        t = fmod(t, 1.0)
        if t < 0.0:
            t += 1.0
        return t
    return 0.5

func _compute_night_factor(day_time: float) -> float:
    ## day_time (0〜1) から「どれくらい夜か」を 0〜1 で計算する。
    ##
    ## ここでは簡易的に:
    ## - 正午(0.5)で 0
    ## - 深夜(0.0/1.0)で 1
    ## - dawn_dusk_width で滑らかに遷移
    var distance_to_midday := abs(day_time - 0.5) * 2.0  # 0(昼)〜1(深夜)
    var factor := clamp(distance_to_midday, 0.0, 1.0)

    # dawn_dusk_width で変化をなだらかにする
    if dawn_dusk_width > 0.0:
        var edge0 := 0.5 - dawn_dusk_width
        var edge1 := 1.0
        # 0(昼)〜1(夜) の範囲で smoothstep 的に持ち上げる
        var x := clamp(factor, edge0, edge1)
        factor = (x - edge0) / max(edge1 - edge0, 0.0001)
        factor = clamp(factor, 0.0, 1.0)

    if use_smooth_step:
        factor = factor * factor * (3.0 - 2.0 * factor)  # smoothstep

    # night_strength で全体の暗さを調整
    factor *= night_strength
    return clamp(factor, 0.0, 1.0)

func _update_tint() -> void:
    var parent := get_parent()
    if parent == null:
        return

    var day_time := _get_day_time()
    var night_factor := _compute_night_factor(day_time)

    # 昼カラーと夜カラーを補間
    var color := day_color.lerp(night_color, night_factor)

    # 最低明るさを保証
    var brightness := color.get_v()
    if brightness < min_brightness:
        var scale := min_brightness / max(brightness, 0.0001)
        color.r *= scale
        color.g *= scale
        color.b *= scale

    # alpha を補間しない設定なら、元 modulate の alpha を維持
    if _has_original and not affect_alpha:
        color.a = _original_modulate.a

    if parent.has_method("set_modulate"):
        parent.set_modulate(color)
    elif "modulate" in parent:
        parent.modulate = color

    if debug_print and randi() % 120 == 0: # たまにログを出す(毎フレームはうるさいので)
        print("[DayNightTint] day_time=%.2f, night_factor=%.2f, color=%s" %
            [day_time, night_factor, str(color)])

使い方の手順

ここでは、グローバル時間を管理する TimeManager シングルトンを用意している前提で進めます。
(まだない場合は、まず簡単なものを作りましょう)

① TimeManager(オートローダー)を用意する

例として、TimeManager.gdres:// 直下に作り、Project Settings → Autoload に登録します。


extends Node
class_name TimeManager

## 0.0〜1.0 で一日の進行度を表す
@export_range(0.0, 1.0, 0.001)
var day_time: float = 0.3

@export_range(1.0, 600.0, 1.0)
var day_length_seconds: float = 120.0  # 1日を何秒で回すか

func _process(delta: float) -> void:
    if day_length_seconds <= 0.0:
        return
    day_time += delta / day_length_seconds
    day_time = fmod(day_time, 1.0)

これで /root/TimeManagerday_time が生えるので、DayNightTint から参照できます。

② DayNightTint.gd を作り、コンポーネントとして保存

  • 上記の DayNightTint.gd をそのまま res://components/DayNightTint.gd などに保存
  • ファイル名とパスは自由ですが、class_name DayNightTint があるので、どこからでもアタッチできます

③ プレイヤーに昼夜コンポーネントをアタッチする例

たとえばプレイヤーのシーン構成が次のような場合:

Player (CharacterBody2D)
 ├── Sprite2D
 ├── CollisionShape2D
 └── DayNightTint (Node)
  1. Player シーンを開く
  2. Player の子として Node を追加し、名前を DayNightTint に変更
  3. その Node に DayNightTint.gd をアタッチ
  4. インスペクタで以下を設定
    • autostart: true(デフォルトのままでOK)
    • time_manager_path: 空のまま(自動で /root/TimeManager を探します)
    • day_color: (1,1,1,1)
    • night_color: (0.4,0.45,0.6,1) など、お好みの夜色
    • night_strength: 0.8 くらい(夜にしっかり暗くしたい場合)

これで、TimeManager.day_time が進むのに合わせて、Player の modulate が自動で昼夜に応じて変化します。

④ 他のシーンにもポン付けする(敵、背景、動く床など)

同じコンポーネントを、敵や背景、動く床などにもそのまま使い回せます。

敵キャラの例:

Enemy (CharacterBody2D)
 ├── Sprite2D
 ├── CollisionShape2D
 └── DayNightTint (Node)
  • 敵は夜にもっと暗くしたい → night_strength = 1.0, night_color をかなり暗めに

背景のタイルマップの例:

Level01 (Node2D)
 ├── TileMap
 ├── ParallaxBackground
 │    └── ParallaxLayer
 │         └── Sprite2D (遠景山)
 └── DayNightTint (Node)
  • Level01 の modulate を変えることで、シーン全体の色合いを昼夜でガラッと変える
  • 背景だけを暗くする場合は、ParallaxBackground の子に DayNightTint をつける、など柔軟に配置

動く床の例:

MovingPlatform (Node2D)
 ├── Sprite2D
 ├── CollisionShape2D
 ├── AnimationPlayer
 └── DayNightTint (Node)
  • 夜になると少しだけ透明にしたい → affect_alpha = true にして night_color.a を 0.7 などに設定

どのシーンも、「継承し直す」のではなく、ただ Node を1個追加してスクリプトを貼るだけで昼夜対応になります。これがコンポーネント指向の気持ちよさですね。


メリットと応用

  • シーン構造がスッキリ
    「昼夜対応版プレイヤー」「昼夜対応版敵」みたいな派生シーンを作らなくて済みます。
    すべて「元シーン + DayNightTint コンポーネント」で完結。
  • レベルデザイン時の調整が楽
    各シーンごとに night_strengthnight_color をいじるだけで、
    「このステージは夜でも明るめ」「この敵は夜にシルエットだけ見える」などを簡単に表現できます。
  • 再利用性が高い
    2D/3D問わず、modulate を持つノードならどこにでも貼れます。
    将来、別プロジェクトにコピペしてもすぐ使えるのも嬉しいポイントです。
  • テストがしやすい
    TimeManager.day_time をエディタ上でいじるだけで挙動を確認できるので、
    ゲーム全体を動かさなくてもビジュアル調整がしやすいです。

さらに、「合成」なので、他のコンポーネントと共存させるのも簡単です。

  • 点滅コンポーネント(ダメージ時に赤くする)
  • フェードイン/アウトコンポーネント

などと組み合わせて、「昼夜 + ダメージ演出 + 登場演出」みたいな複雑な振る舞いも、継承ツリーを増やさずに実現できます。

改造案:昼夜で自動的にライトを ON/OFF する

例えば、DayNightTint にちょっと手を加えて、「夜になったら子ノードのライトを ON」にすることもできます。


func _toggle_child_lights(night_factor: float) -> void:
    ## night_factor が 0.7 以上になったら、子の Light2D を有効化する例
    var is_night := night_factor >= 0.7
    for child in get_parent().get_children():
        if child is Light2D:
            child.visible = is_night

これを _update_tint() の最後で呼べば、
「夜になると街灯が自動で点く」「朝になったら消える」といった演出を、ライト側のシーンを一切いじらずに実現できます。

こんな感じで、DayNightTint をベースに自分のゲーム専用の昼夜コンポーネントへ育てていくと、
プロジェクト全体の見通しがかなり良くなります。継承ツリーに悩む前に、「まずはコンポーネントを1個足してみる」発想でいきましょう。