【Cocos Creator 3.8】ColorTint の実装:アタッチするだけで「凍結・炎上などの状態に応じてノードの色を滑らかに変化」させる汎用スクリプト
このコンポーネントは、ノードにアタッチするだけで「凍結なら青く」「炎上なら赤く」「毒なら緑に」といった状態異常表現を、Modulate(色乗算)で滑らかに補間しながら切り替えるための汎用スクリプトです。
Sprite や Label など、UI/2Dオブジェクトの色変化演出を簡単に統一管理したいときに便利です。
コンポーネントの設計方針
まず、今回作る ColorTint コンポーネントの要件を整理します。
- ノードにアタッチするだけで動作する(他のカスタムスクリプトへの依存なし)。
- 対象ノード(および任意で子ノード)の Modulate 色を、指定した状態に応じて滑らかに変更する。
- 「状態」はインスペクタから列挙的に選べるようにする(例:Normal / Frozen / Burning / Poisoned など)。
- 状態ごとに「目標色」「補間時間(フェード時間)」をインスペクタで調整可能にする。
- Sprite, Label, UIRenderer など、色を持つ代表的コンポーネントに自動対応し、存在しない場合はログで警告する。
- 元の色(Normal 状態)を自動取得しておき、Normal に戻したときはその色にフェードバックする。
インスペクタで設定可能なプロパティ設計
ColorTint コンポーネントでは、以下のようなプロパティを用意します。
applyToChildren(boolean)- 説明:子ノードを含めて色変化を適用するかどうか。
- true:このノード以下の全ての UIRenderer(Sprite, Label など)に一括適用。
- false:このノード自身の UIRenderer のみが対象。
state(enum)- 説明:現在の状態を表す列挙値。インスペクタから直接切り替え可能。
- 例:Normal / Frozen / Burning / Poisoned / Custom1 / Custom2 など。
- 状態を変更すると、その状態に対応した目標色へ向けてフェードを開始。
frozenColor(Color)- 説明:Frozen(凍結)状態のときの目標 Modulate 色。
- 初期値:薄い青系(例:R=150,G=200,B=255,A=255)。
burningColor(Color)- 説明:Burning(炎上)状態のときの目標 Modulate 色。
- 初期値:赤〜オレンジ系(例:R=255,G=120,B=80,A=255)。
poisonedColor(Color)- 説明:Poisoned(毒)状態のときの目標 Modulate 色。
- 初期値:緑系(例:R=120,G=255,B=120,A=255)。
customColor1,customColor2(Color)- 説明:任意の追加状態用の色。UI ハイライトやバフ状態などに利用可能。
transitionDuration(number)- 説明:状態変更時に、現在の色から目標色へ補間する時間(秒)。
- 0 の場合:即座に切り替わる(フェードなし)。
- 例:0.25〜0.5 秒程度が使いやすい。
useUnscaledTime(boolean)- 説明:timeScale の影響を受けずに色変化させるかどうか。
- true:Time.timeScale を 0 にしてもフェードが進む。
- false:ゲームの時間に同期してフェードする。
logMissingRenderers(boolean)- 説明:対象ノードに UIRenderer(Sprite, Label など)が見つからない場合に、警告ログを出すかどうか。
- 防御的実装のため、デフォルトで true にしておく。
また、内部的には以下を管理します(インスペクタには表示しない)。
- 初期状態(Normal)のときの各 UIRenderer の元の色。
- 現在のフェード進行度(経過時間 / transitionDuration)。
- 現在の色と目標色のペア。
TypeScriptコードの実装
以下が、Cocos Creator 3.8.7 でそのまま使用できる ColorTint.ts の完全な実装例です。
import { _decorator, Component, Node, Color, UIRenderer, Enum, game } from 'cc';
const { ccclass, property } = _decorator;
// 状態を表す列挙型
enum TintState {
Normal = 0,
Frozen = 1,
Burning = 2,
Poisoned = 3,
Custom1 = 4,
Custom2 = 5,
}
// Cocos のインスペクタで enum として扱うための設定
Enum(TintState);
@ccclass('ColorTint')
export class ColorTint extends Component {
@property({
type: Enum(TintState),
tooltip: '現在の色状態を選択します。\nNormal: 初期色\nFrozen: 凍結(青系)\nBurning: 炎上(赤系)\nPoisoned: 毒(緑系)\nCustom1/2: 任意の色'
})
public state: TintState = TintState.Normal;
@property({
tooltip: '子ノードを含めて色変化を適用するかどうか。\nON: このノード以下の全 UIRenderer に適用\nOFF: このノード自身の UIRenderer のみ'
})
public applyToChildren: boolean = true;
@property({
tooltip: '状態変更時の色補間にかける時間(秒)。\n0 の場合は即座に切り替わります。'
})
public transitionDuration: number = 0.25;
@property({
tooltip: 'ゲームの timeScale の影響を受けずに色を変化させるかどうか。\nON: ポーズ中でもフェードが進行します。'
})
public useUnscaledTime: boolean = false;
@property({
tooltip: 'UIRenderer(Sprite/Labelなど)が見つからない場合に警告ログを出すかどうか。'
})
public logMissingRenderers: boolean = true;
@property({
tooltip: 'Frozen(凍結)状態の目標色。',
})
public frozenColor: Color = new Color(150, 200, 255, 255);
@property({
tooltip: 'Burning(炎上)状態の目標色。',
})
public burningColor: Color = new Color(255, 120, 80, 255);
@property({
tooltip: 'Poisoned(毒)状態の目標色。',
})
public poisonedColor: Color = new Color(120, 255, 120, 255);
@property({
tooltip: 'Custom1 状態の目標色。\nバフやハイライトなど任意の用途に利用できます。',
})
public customColor1: Color = new Color(255, 255, 150, 255);
@property({
tooltip: 'Custom2 状態の目標色。',
})
public customColor2: Color = new Color(180, 130, 255, 255);
// --- 内部管理用フィールド ---
// 対象となる UIRenderer コンポーネント群
private _renderers: UIRenderer[] = [];
// 各レンダラーの「Normal 状態」のときの元の色
private _originalColors: Color[] = [];
// フェード開始時の色
private _fromColors: Color[] = [];
// 目標色
private _toColor: Color = new Color(255, 255, 255, 255);
// フェード進行管理
private _elapsed: number = 0;
private _isTransitioning: boolean = false;
// 前フレームの状態(インスペクタで変更されたかを検知するため)
private _lastState: TintState = TintState.Normal;
onLoad() {
// 対象となる UIRenderer を取得
this._collectRenderers();
if (this._renderers.length === 0 && this.logMissingRenderers) {
console.warn(
'[ColorTint] 対象ノードに UIRenderer (Sprite, Label など) が見つかりません。',
'ノード名:', this.node.name
);
}
// 各レンダラーの初期色を保存(Normal 状態の基準色)
this._originalColors = this._renderers.map(r => r.color.clone());
// 初期状態に応じて色を設定
this._lastState = this.state;
this._applyStateImmediate(this.state);
}
start() {
// onLoad で初期化済みなので特別な処理は不要だが、
// 念のため状態を再適用しておく(エディタ上で変更された場合への保険)。
this._applyStateImmediate(this.state);
}
update(deltaTime: number) {
// インスペクタで state が変更されたかどうかを検知
if (this.state !== this._lastState) {
this._onStateChanged(this._lastState, this.state);
this._lastState = this.state;
}
if (!this._isTransitioning || this.transitionDuration <= 0) {
return;
}
// スケールされていない時間を使うかどうか
const dt = this.useUnscaledTime ? (game.deltaTime) : deltaTime;
this._elapsed += dt;
const t = Math.min(this._elapsed / this.transitionDuration, 1);
// 各レンダラーの色を補間
for (let i = 0; i < this._renderers.length; i++) {
const renderer = this._renderers[i];
const from = this._fromColors[i];
const to = this._toColor;
if (!renderer || !from || !to) {
continue;
}
const r = from.r + (to.r - from.r) * t;
const g = from.g + (to.g - from.g) * t;
const b = from.b + (to.b - from.b) * t;
const a = from.a + (to.a - from.a) * t;
renderer.color = new Color(r, g, b, a);
}
if (t >= 1) {
this._isTransitioning = false;
}
}
/**
* 対象となる UIRenderer を収集する。
* applyToChildren が true の場合は子孫ノードも含める。
*/
private _collectRenderers() {
this._renderers.length = 0;
if (this.applyToChildren) {
this._renderers = this.node.getComponentsInChildren(UIRenderer);
} else {
const r = this.node.getComponent(UIRenderer);
if (r) {
this._renderers.push(r);
}
}
}
/**
* 状態が変更されたときに呼ばれる。
*/
private _onStateChanged(from: TintState, to: TintState) {
// 対象レンダラーが変更されている可能性があるため再収集
this._collectRenderers();
if (this._renderers.length === 0) {
if (this.logMissingRenderers) {
console.warn(
'[ColorTint] 状態変更が行われましたが、対象ノードに UIRenderer がありません。',
'ノード名:', this.node.name
);
}
return;
}
// フェード開始色を現在の色として保存
this._fromColors = this._renderers.map(r => r.color.clone());
// 目標色を決定
this._toColor = this._getTargetColorForState(to);
// フェード管理を初期化
this._elapsed = 0;
if (this.transitionDuration <= 0) {
// 即時反映
for (let i = 0; i < this._renderers.length; i++) {
this._renderers[i].color = this._toColor.clone();
}
this._isTransitioning = false;
} else {
this._isTransitioning = true;
}
}
/**
* 現在の状態に対応する色を即時適用する(初期化用)。
*/
private _applyStateImmediate(state: TintState) {
this._collectRenderers();
if (this._renderers.length === 0) {
return;
}
const targetColor = this._getTargetColorForState(state);
for (let i = 0; i < this._renderers.length; i++) {
this._renderers[i].color = targetColor.clone();
}
this._isTransitioning = false;
this._elapsed = 0;
}
/**
* 指定した状態に対応する目標色を返す。
* Normal の場合は、各レンダラーの元の色(_originalColors)を使う。
*/
private _getTargetColorForState(state: TintState): Color {
switch (state) {
case TintState.Frozen:
return this.frozenColor;
case TintState.Burning:
return this.burningColor;
case TintState.Poisoned:
return this.poisonedColor;
case TintState.Custom1:
return this.customColor1;
case TintState.Custom2:
return this.customColor2;
case TintState.Normal:
default:
// Normal の場合は「元の色」に戻す。
// _originalColors はレンダラーごとに異なるので、
// ここでは代表として 0 番目を返し、適用時には clone して使う。
if (this._originalColors.length > 0) {
// 0番目の色を代表として返す
return this._originalColors[0];
} else {
// フォールバックとして白
return new Color(255, 255, 255, 255);
}
}
}
// --- 便利な公開メソッド(コードからも状態変更しやすくする) ---
/**
* コードから状態を変更するためのユーティリティメソッド。
* 例: this.getComponent(ColorTint)?.setStateFrozen();
*/
public setState(state: TintState) {
this.state = state;
}
public setStateNormal() { this.setState(TintState.Normal); }
public setStateFrozen() { this.setState(TintState.Frozen); }
public setStateBurning() { this.setState(TintState.Burning); }
public setStatePoisoned() { this.setState(TintState.Poisoned); }
public setStateCustom1() { this.setState(TintState.Custom1); }
public setStateCustom2() { this.setState(TintState.Custom2); }
}
コードのポイント解説
- onLoad
_collectRenderers()で対象 UIRenderer を取得。- 見つからない場合は
logMissingRenderersが true なら警告ログ。 - 取得したレンダラーの
colorを_originalColorsに保存し、Normal 状態の基準色とする。 - 現在の
stateに応じて_applyStateImmediateで初期色を設定。
- update
- 毎フレーム、
stateが前フレームの_lastStateと異なるかをチェック。 - 変更されていれば
_onStateChangedを呼び、フェードを開始。 _isTransitioningが true なら、経過時間から補間係数tを計算し、各レンダラーの色を線形補間。useUnscaledTimeが true の場合はgame.deltaTimeを使い、timeScale の影響を受けない。
- 毎フレーム、
- _onStateChanged
- 対象レンダラーを再収集(動的に子ノードが増減しても対応)。
- 現在色を
_fromColorsに保存し、_getTargetColorForStateで目標色を決定。 transitionDuration <= 0の場合は即座に目標色を適用し、フェードは行わない。
- _getTargetColorForState
- 各状態に応じて、インスペクタで設定された Color を返す。
- Normal の場合は、保存しておいた
_originalColors[0]を代表として返す。 - 複数レンダラーがある場合でも、Normal 状態では
_applyStateImmediate内で_originalColorsを使って適用するため、元の色に戻る。
- 公開メソッド
setStateXXX()シリーズで、ゲームロジックから簡単に状態変更できる。- このコンポーネント自体は他スクリプトに依存せず、あくまで「状態に応じた色変化」だけを担当。
使用手順と動作確認
ここからは、実際にエディタ上で ColorTint を使ってみる手順を説明します。
1. スクリプトファイルの作成
- Assets パネルで右クリックします。
Create → TypeScriptを選択します。- 新しく作成されたスクリプトの名前を
ColorTint.tsに変更します。 - ダブルクリックしてエディタ(VSCode 等)で開き、上記の TypeScript コードをすべて貼り付けて保存します。
2. テスト用ノードの作成
ここでは Sprite を例に説明しますが、Label など他の UIRenderer でも同様に動作します。
- Hierarchy パネルで右クリック →
Create → 2D Object → Spriteを選択し、テスト用の Sprite ノードを作成します。 - 作成した Sprite ノードに適当な画像を設定しておきます。
3. ColorTint コンポーネントのアタッチ
- Hierarchy で先ほど作成した Sprite ノードを選択します。
- Inspector の下部にある
Add Componentボタンをクリックします。 Customカテゴリの中からColorTintを選択して追加します。
4. プロパティの設定例
Inspector 上で、ColorTint の各プロパティを次のように設定してみます。
- applyToChildren:
ON(子ノードにも色変化を適用したい場合) - transitionDuration:
0.3 - useUnscaledTime:
OFF(通常は OFF で問題ありません) - frozenColor:薄い青(デフォルトのままでもOK)
- burningColor:赤〜オレンジ(デフォルトのままでもOK)
- poisonedColor:緑(デフォルトのままでもOK)
- customColor1 / customColor2:お好みで設定
- state:最初は
Normalのまま
5. エディタ上での簡易テスト
- Scene を保存します。
- 再生ボタン(▶)を押してプレビューを開始します。
- プレイ中に Inspector で
stateをFrozenに変更します。- → Sprite が元の色から薄い青に、0.3 秒かけて滑らかに変化するのを確認できます。
- 続けて
Burning,Poisoned,Custom1などに切り替えて、色変化を確認します。 Normalに戻すと、アタッチしたときの元の色へフェードバックします。
6. コードから状態を切り替えてみる(任意)
他のスクリプトから状態を切り替えたい場合も、シンプルな呼び出しで済みます。
(ColorTint は外部スクリプトに依存していないので、どのスクリプトからでも呼べます)
// 例: 同じノードにアタッチされたスクリプトから
import { _decorator, Component } from 'cc';
const { ccclass } = _decorator;
import { ColorTint } from './ColorTint';
@ccclass('SampleStatusChanger')
export class SampleStatusChanger extends Component {
start() {
const tint = this.getComponent(ColorTint);
if (tint) {
// 炎上状態にする
tint.setStateBurning();
// 3秒後に凍結状態にする、なども自由に実装可能
}
}
}
このように、ゲームロジック側は「どの状態にしたいか」だけを決めればよく、色の補間・適用はすべて ColorTint 側に任せられるため、責務が分離されて扱いやすくなります。
まとめ
今回作成した ColorTint コンポーネントは、
- ノードにアタッチするだけで、状態に応じた色変化を簡単に表現できる。
- 状態ごとの色やフェード時間をすべてインスペクタから調整可能。
- Sprite / Label / その他 UIRenderer に自動対応し、存在しない場合は警告ログで気づける。
- 他のカスタムスクリプトに一切依存せず、完全に独立した汎用コンポーネントとしてプロジェクト内のどこでも再利用できる。
応用例としては、
- 敵キャラクターの状態異常(凍結・炎上・毒など)の視覚表現。
- プレイヤーの無敵時間中のハイライト(Custom1/2 を利用)。
- UI ボタンのアクティブ / 非アクティブ状態の色切り替え。
- バフ・デバフ状態を色で分かりやすく表示するステータスパネル。
といったさまざまな用途にそのまま利用できます。
演出ごとに色変更ロジックをバラバラに書くのではなく、ColorTint 一つに集約しておくことで、調整や仕様変更にも強い設計になります。
必要に応じて、状態の種類を増やしたり、フラッシュ演出や点滅などを拡張していくのも容易です。
まずはプロジェクト内の代表的なキャラクターや UI にアタッチして、状態異常やバフ表現の共通基盤として活用してみてください。




