Godot で「状態異常っぽい色変化」を実装しようとすると、だいたいこんな流れになりますよね。
- プレイヤーシーンのスクリプトに「凍結」「炎上」などの状態フラグを追加
- その中で
modulateを直接書き換える - 別の敵キャラにも同じような処理をコピペ…
結果として、
- プレイヤーと敵、それぞれのスクリプトが状態管理と色制御でパンパンになる
- 「色の変化だけ」共通化したいのに、継承ツリーをいじる羽目になる
- アニメーションも一緒にやりたくなって、さらにカオス
Godot はノード継承が強力ですが、これに頼りすぎると「状態異常ごとにスクリプトが肥大化」しがちです。そこで登場するのが、今回のコンポーネント「ColorTint」です。
ColorTint コンポーネントを 1 ノードとして親にアタッチしておけば、
- 「凍結」「炎上」などの状態に応じて
- 親ノード(Sprite2D / MeshInstance2D / CanvasItemなど)の
modulateを - 滑らかに補間しながら切り替える
という処理を「合成(Composition)」でサクッと追加できます。プレイヤーでも敵でも、動く床でも、ColorTint をアタッチするだけで共通の色変化ロジックを再利用できるようになります。
【Godot 4】状態異常の色替えをコンポーネント化!「ColorTint」コンポーネント
フルコード(GDScript / Godot 4)
extends Node
class_name ColorTint
## 親ノードの modulate を、状態に応じて滑らかに変更するコンポーネント
##
## 想定親ノード:
## - Sprite2D / AnimatedSprite2D
## - CanvasItem を継承するノード (Control, Label, Node2D など)
##
## 使い方:
## - 色を変えたいノードの子として配置し、親の modulate を制御します。
## - コードから `set_state("frozen")` のように状態名を指定して呼び出します。
## 状態名 → 色 のマッピングをインスペクタから設定できるようにする
@export var state_colors: Dictionary = {
"normal": Color.WHITE, ## 通常状態
"frozen": Color(0.6, 0.8, 1.0), ## 凍結: 少し青っぽく
"burning": Color(1.0, 0.5, 0.2), ## 炎上: オレンジっぽく
}
## デフォルト状態名(シーン開始時にこの状態にします)
@export var default_state: String = "normal"
## 色を補間する時間(秒)
@export_range(0.0, 5.0, 0.05)
var transition_time: float = 0.25
## フレームごとに補間するかどうか
## true: _process() で毎フレーム補間
## false: Tween を使って一度だけ補間(Tween ベースの方が負荷は軽め)
@export var use_process_lerp: bool = false
## use_process_lerp=true のときの補間係数
## 0.0~1.0 の範囲で、1.0 に近いほど素早く変化
@export_range(0.01, 1.0, 0.01)
var lerp_speed: float = 0.15
## 親の modulate に対して「乗算」するかどうか
## true: parent.modulate * state_color
## false: parent.modulate を state_color で上書き
@export var multiply_with_parent: bool = false
## 状態遷移をデバッグ表示するか
@export var debug_print: bool = false
## 現在の状態名
var current_state: String
## 目標の色
var _target_color: Color
## 現在の色(親の modulate をキャッシュ)
var _current_color: Color
## Tween 方式で使う Tween ノード
var _tween: Tween
func _ready() -> void:
# 親が CanvasItem を継承しているか簡易チェック
var parent := get_parent()
if parent == null or not parent is CanvasItem:
push_warning("ColorTint: 親ノードが CanvasItem ではありません。modulate を変更できない可能性があります。")
# 親の現在の modulate を初期値として保持
if parent and parent is CanvasItem:
_current_color = parent.modulate
else:
_current_color = Color.WHITE
# デフォルト状態が state_colors に無ければ normal にフォールバック
if not state_colors.has(default_state):
if debug_print:
print("ColorTint: default_state '%s' が state_colors に存在しません。'normal' を使用します。" % default_state)
if state_colors.has("normal"):
default_state = "normal"
else:
# normal すら無い場合は現在色を normal として登録
state_colors["normal"] = _current_color
default_state = "normal"
current_state = default_state
_target_color = _get_state_color(current_state)
# 初期状態をすぐ反映
_apply_color_immediately(_target_color)
set_process(use_process_lerp)
func _process(delta: float) -> void:
if not use_process_lerp:
return
var parent := get_parent()
if not (parent and parent is CanvasItem):
return
# 現在色から目標色に向けて補間
_current_color = parent.modulate
_current_color = _current_color.lerp(_target_color, lerp_speed)
_set_parent_modulate(_current_color)
## 状態を変更するメインAPI
## 例: set_state("frozen"), set_state("burning"), set_state("normal")
func set_state(state_name: String) -> void:
if current_state == state_name:
return
if not state_colors.has(state_name):
push_warning("ColorTint: 未定義の状態 '%s' が指定されました。" % state_name)
return
if debug_print:
print("ColorTint: state change '%s' -> '%s'" % [current_state, state_name])
current_state = state_name
_target_color = _get_state_color(current_state)
if use_process_lerp:
# _process() で徐々に _target_color に近づける
return
else:
# Tween を使って補間
_start_tween_to_target_color()
## 現在の状態名を取得
func get_state() -> String:
return current_state
## 指定した状態の色を取得(存在しなければ白)
func _get_state_color(state_name: String) -> Color:
if state_colors.has(state_name):
return state_colors[state_name]
return Color.WHITE
## 親の modulate を即座に指定色にする
func _apply_color_immediately(color: Color) -> void:
var parent := get_parent()
if not (parent and parent is CanvasItem):
return
_set_parent_modulate(color)
## 親の modulate を設定(multiply_with_parent オプションに対応)
func _set_parent_modulate(color: Color) -> void:
var parent := get_parent()
if not (parent and parent is CanvasItem):
return
if multiply_with_parent:
# 親の元の色に対して乗算する
var base_color: Color = parent.modulate
# 乗算だとどんどん暗くなっていくのを防ぐため、
# base_color を一度 normal 状態にリセットしたい場合は、別途ロジックを追加してください。
parent.modulate = Color(
base_color.r * color.r,
base_color.g * color.g,
base_color.b * color.b,
base_color.a * color.a
)
else:
parent.modulate = color
## Tween 方式で色を補間する
func _start_tween_to_target_color() -> void:
var parent := get_parent()
if not (parent and parent is CanvasItem):
return
# 既存の Tween を停止・削除
if _tween and _tween.is_valid():
_tween.kill()
_tween = null
# Tween ノードを追加
_tween = create_tween()
_tween.set_trans(Tween.TRANS_SINE)
_tween.set_ease(Tween.EASE_OUT)
var from_color: Color = parent.modulate
var to_color: Color = _target_color
# Tween の途中で値を更新するために、コールバックを使う
_tween.tween_method(
func(c: Color) -> void:
_set_parent_modulate(c),
from_color,
to_color,
max(transition_time, 0.0001)
)
## 状態の色を動的に登録・変更するためのヘルパー
## 例: register_state_color("poison", Color(0.5, 1.0, 0.5))
func register_state_color(state_name: String, color: Color) -> void:
state_colors[state_name] = color
## 状態を元の normal に戻すショートカット
func reset_to_normal() -> void:
if state_colors.has("normal"):
set_state("normal")
使い方の手順
ここでは、プレイヤーが炎ダメージを受けたら赤く、氷ダメージを受けたら青く光るという例で説明します。
手順①: シーン構成に ColorTint を追加する
まずはプレイヤーシーンにコンポーネントをアタッチします。
Player (CharacterBody2D) ├── Sprite2D ├── CollisionShape2D └── ColorTint (Node)
- Player: いつものプレイヤーノード(CharacterBody2D でなくてもOK)
- Sprite2D: 見た目用
- ColorTint: 上記スクリプトをアタッチした Node(種類は Node でOK)
ポイントは、色を変えたいノード(Sprite2D など)の「親」ではなく「子」に ColorTint を置くことです。ColorTint は get_parent() の modulate を操作します。
手順②: インスペクタで状態と色を設定する
ColorTint ノードを選択すると、インスペクタに次のようなプロパティが出ます。
state_colors: 状態名と色のマッピングdefault_state: 初期状態(通常はnormal)transition_time: 色が切り替わる時間(Tween 方式)use_process_lerp: true なら毎フレーム補間、false なら Tweenlerp_speed: process 補間の速さmultiply_with_parent: 親の色に乗算するかどうか
例えばこんな感じに設定しておくと分かりやすいです。
state_colors.normal=Color(1, 1, 1)state_colors.frozen=Color(0.6, 0.8, 1.0)state_colors.burning=Color(1.0, 0.4, 0.2)
手順③: プレイヤースクリプトから状態を変更する
プレイヤー側では「状態が変わったタイミング」で set_state() を呼ぶだけです。
extends CharacterBody2D
@onready var color_tint: ColorTint = $ColorTint
func _ready() -> void:
# 念のため normal にしておく
color_tint.set_state("normal")
func apply_fire_damage() -> void:
# 炎上状態にして赤くする
color_tint.set_state("burning")
func apply_ice_damage() -> void:
# 凍結状態にして青くする
color_tint.set_state("frozen")
func clear_status() -> void:
# 状態異常解除
color_tint.reset_to_normal()
これで、プレイヤーの「状態管理ロジック」と「色変化ロジック」が完全に分離されました。プレイヤーは「今は炎上状態だよ」と ColorTint に伝えるだけ。色の補間や modulate の扱いはコンポーネント側に任せられます。
手順④: 敵やギミックにもそのまま再利用する
同じ ColorTint を敵キャラにも付けてみましょう。
Enemy (CharacterBody2D) ├── Sprite2D ├── CollisionShape2D └── ColorTint (Node)
extends CharacterBody2D
@onready var color_tint: ColorTint = $ColorTint
func _ready() -> void:
color_tint.set_state("normal")
func on_poison_start() -> void:
# インスペクタで "poison" を追加しておくか、コードで登録
color_tint.register_state_color("poison", Color(0.5, 1.0, 0.5))
color_tint.set_state("poison")
func on_poison_end() -> void:
color_tint.reset_to_normal()
「動く床」や「ダメージ床」にも同じようにアタッチできます。
FirePlatform (StaticBody2D) ├── Sprite2D └── ColorTint (Node)
プレイヤーが乗ったら set_state("burning")、離れたら reset_to_normal() という感じですね。
メリットと応用
この ColorTint コンポーネントを使うメリットはかなり分かりやすいです。
- シーン構造がシンプル
「色を変えたいノードの子に ColorTint を 1 個置くだけ」です。Sprite2D を継承した専用プレイヤークラスを作る必要もありません。 - 状態ロジックと描画ロジックの分離
プレイヤー・敵・ギミックのスクリプトから「modulate いじり」を追い出せます。状態管理はそれぞれのスクリプト、色変化は ColorTint に集約。 - 再利用性が高い
どのシーンにもコピペで持っていけますし、state_colorsを変えるだけでキャラごとの色味も簡単に調整できます。 - Godot の「深いノード階層」問題を回避
「状態ごとに別 Sprite2D を用意して切り替える」といった構成にせず、1 つの Sprite2D + 1 つの ColorTint で済みます。
応用案としては、例えば「HP に応じて徐々に赤くする」「ステルス状態のときだけ半透明にする」なども簡単に実現できます。
最後に、簡単な改造案として「一時的にフラッシュ(点滅)させる」関数を追加してみましょう。
## ダメージを受けたときなどに一瞬だけ白くフラッシュさせる
func flash_white(duration: float = 0.1, times: int = 2) -> void:
var parent := get_parent()
if not (parent and parent is CanvasItem):
return
# 既存の Tween を止める
if _tween and _tween.is_valid():
_tween.kill()
_tween = null
var original_color: Color = _get_state_color(current_state)
var flash_color: Color = Color(1, 1, 1)
_tween = create_tween()
_tween.set_trans(Tween.TRANS_SINE)
_tween.set_ease(Tween.EASE_IN_OUT)
for i in range(times):
_tween.tween_method(
func(c: Color) -> void:
_set_parent_modulate(c),
original_color,
flash_color,
duration * 0.5
)
_tween.tween_method(
func(c: Color) -> void:
_set_parent_modulate(c),
flash_color,
original_color,
duration * 0.5
)
# フラッシュが終わったら、現在の状態の色に戻す
_tween.tween_callback(func() -> void:
_apply_color_immediately(_get_state_color(current_state))
)
このように、「色に関する処理は全部 ColorTint に寄せる」というルールにしておくと、プロジェクトが大きくなっても管理しやすくなります。継承ベースでごちゃごちゃしてきたら、ぜひ一度 ColorTint みたいなコンポーネント化を試してみてください。
