【Cocos Creator 3.8】DayNightTint の実装:アタッチするだけで「昼夜サイクルに応じた自動明暗(色合い)変化」を実現する汎用スクリプト
この記事では、ノードにアタッチするだけで「昼・夕方・夜」などの時間帯に応じて自動的に色を暗く/明るくしてくれる汎用コンポーネント DayNightTint を TypeScript で実装します。
グローバルな「ゲーム内時間」を参照するのではなく、コンポーネント自身が持つ時間パラメータを Inspector から調整できるようにすることで、他のスクリプトに一切依存せず、どのシーン・どのプロジェクトにもそのまま持ち込んで使える設計にします。
コンポーネントの設計方針
1. 要件の整理
- ゲーム内の「時間」を 0.0〜1.0 の正規化値(0 = 深夜, 0.5 = 正午, 1 = 深夜に戻る)として扱う。
- この時間値に応じて、ノードの色(
UIRenderer.color/Spriteなど)を暗く・明るく補正(Modulate)する。 - 他の「GameManager」や「TimeManager」などのスクリプトに依存しない。
- Inspector から「時間の進み方」や「昼の明るさ」「夜の暗さ」「色の変化カーブ」を調整可能にする。
- UI(Sprite, Label, RichText など)だけでなく、3Dオブジェクト(
MeshRenderer)にも適用できるように実装する。
「グローバル時間変数を監視」という要件は、外部依存を禁止という条件と衝突しやすいので、以下のように解釈して設計します。
- 各
DayNightTintが「ローカルな時間値」を持つ。 - 必要であれば「自動で時間を進める」機能を ON/OFF できる。
- 別のスクリプトから「時間値」をセットしたい場合は、
publicメソッドを通じて任意に外部から更新できる(ただし、DayNightTint 側は外部スクリプトを一切知らない)。
これにより、単体で完結しつつ、必要なら外部からも制御可能な汎用コンポーネントになります。
2. インスペクタで設定可能なプロパティ
以下のようなプロパティを設計します。
- autoUpdateTime: boolean
– 時間を自動で進めるかどうか。
– ON:update()内でdeltaTimeを使ってtimeOfDayを加算し、0〜1でループする。
– OFF: 時間は外部からsetTimeOfDay()で更新する想定。 - timeOfDay: number (0.0〜1.0)
– 現在の時間(正規化)。
– 0.0 = 深夜、0.25 = 朝、0.5 = 正午、0.75 = 夕方、1.0 = 深夜(0に戻る)。
– autoUpdateTime = false の場合は、この値を直接 Inspector で動かして見た目を確認できる。 - dayLengthSeconds: number
– 0.0 〜 1.0 までの一周にかかる秒数。
– 例: 60 にすると、現実時間 60秒で 1日が経過する。 - dayColor: Color
– 正午(timeOfDay = 0.5)付近で適用したい色。
– 通常はColor.WHITE(= 無補正)を指定する。 - nightColor: Color
– 深夜(timeOfDay = 0.0 / 1.0)付近で適用したい色。
– 例:Color(80, 80, 120)のように少し青みがかった暗い色にすると「夜っぽさ」が出る。 - duskColor: Color
– 夕方(timeOfDay ≒ 0.75)付近で適用したい色。
– 例:Color(255, 180, 150)のようなオレンジ寄りの色。 - dawnColor: Color
– 朝焼け(timeOfDay ≒ 0.25)付近で適用したい色。
– 例:Color(220, 200, 255)など、少し紫がかった明るい色。 - brightnessCurve: Enum
– 時間に対する明るさの変化カーブ。
–Linear: 単純に線形補間。
–SmoothStep: 緩やかに変化するカーブ。
–Sharp: 朝夕の変化を少し急峻にする。 - applyToChildren: boolean
– 子ノードの Renderer にも同じ色補正を適用するかどうか。
– ON: 親ノード以下の全てのUIRenderer/MeshRendererに対して色を適用。
– OFF: このコンポーネントが付いているノードの Renderer のみを対象。 - targetGroupTag: string
– 任意の文字列タグ。空の場合は無視。
– 例えば「Background」「Character」などのタグを付けておき、
将来的に外部から「特定タグだけ時間を変える」などの制御をしやすくする。 - debugLog: boolean
– ON にすると、起動時や必要なコンポーネントが見つからない場合にコンソールへ詳細ログを出す。
また、標準コンポーネントへの依存として、以下を防御的に扱います。
- UI 系:
Sprite,Label,RichText,UIRenderer(共通基底) - 3D 系:
MeshRenderer
これらが存在しない場合でもスクリプト自体はエラーで止まらず、警告ログを出して処理をスキップするようにします。
TypeScriptコードの実装
import { _decorator, Component, Node, Color, UIRenderer, MeshRenderer, math } from 'cc';
const { ccclass, property } = _decorator;
/**
* 昼夜サイクルに応じてノード(および子ノード)の色を自動的に変化させるコンポーネント。
* 他のスクリプトには依存せず、このコンポーネント単体で完結する。
*/
enum BrightnessCurveType {
LINEAR = 0,
SMOOTH_STEP = 1,
SHARP = 2,
}
@ccclass('DayNightTint')
export class DayNightTint extends Component {
@property({
tooltip: '時間を自動で進行させるかどうか。\nON: update() 内で timeOfDay が 0〜1 の範囲でループします。\nOFF: timeOfDay は Inspector から手動で変更するか、外部から setTimeOfDay() で更新してください。'
})
public autoUpdateTime: boolean = true;
@property({
tooltip: '現在の時間(0.0〜1.0)。\n0 = 深夜, 0.25 = 朝, 0.5 = 正午, 0.75 = 夕方, 1 = 深夜(=0)。\nautoUpdateTime = false のときは、この値を直接編集して見た目を確認できます。',
slide: true,
min: 0,
max: 1,
})
public timeOfDay: number = 0.25; // デフォルト: 朝
@property({
tooltip: '1日(0→1)にかかる現実時間(秒)。\n例: 60 にすると、60秒で1日が経過します。',
min: 0.1,
})
public dayLengthSeconds: number = 60;
@property({
tooltip: '正午(timeOfDay ≒ 0.5)のときの色。\n通常は Color.WHITE(= 無補正)を指定します。'
})
public dayColor: Color = new Color(255, 255, 255, 255);
@property({
tooltip: '深夜(timeOfDay ≒ 0,1)のときの色。\n暗めかつ少し青みがかった色にすると夜らしく見えます。'
})
public nightColor: Color = new Color(80, 80, 120, 255);
@property({
tooltip: '夕方(timeOfDay ≒ 0.75)のときの色。\nオレンジ寄りの色を指定すると夕焼け感が出ます。'
})
public duskColor: Color = new Color(255, 180, 150, 255);
@property({
tooltip: '朝焼け(timeOfDay ≒ 0.25)のときの色。\n少し紫〜ピンク寄りの明るい色が合います。'
})
public dawnColor: Color = new Color(220, 200, 255, 255);
@property({
tooltip: '時間に対する明るさ変化のカーブ種類。\nLINEAR: 単純な線形補間。\nSMOOTH_STEP: 緩やかに切り替わるカーブ。\nSHARP: 朝夕の変化がやや急峻になります。',
type: BrightnessCurveType
})
public brightnessCurve: BrightnessCurveType = BrightnessCurveType.SMOOTH_STEP;
@property({
tooltip: '子ノードの Renderer(Sprite, Label, MeshRenderer など)にも色補正を適用するかどうか。'
})
public applyToChildren: boolean = true;
@property({
tooltip: '任意のタグ文字列。このコンポーネント単体では特別な意味は持ちませんが、\n外部スクリプトから特定タグを持つ DayNightTint を検索して制御するなどの用途に使えます。'
})
public targetGroupTag: string = '';
@property({
tooltip: 'ON にすると、起動時や警告などのログをコンソールに詳細表示します。'
})
public debugLog: boolean = false;
// キャッシュ用配列
private _uiRenderers: UIRenderer[] = [];
private _meshRenderers: MeshRenderer[] = [];
onLoad() {
// Renderer をキャッシュしておく(パフォーマンス向上)
this._cacheRenderers();
if (this._uiRenderers.length === 0 && this._meshRenderers.length === 0) {
console.warn('[DayNightTint] 対象ノードおよび子ノードに UIRenderer / MeshRenderer が見つかりません。色補正は行われません。', this.node);
} else if (this.debugLog) {
console.log(`[DayNightTint] UIRenderer: ${this._uiRenderers.length}, MeshRenderer: ${this._meshRenderers.length} を検出しました。`, this.node);
}
// 初期状態の色を一度適用
this._applyTint();
}
start() {
if (this.debugLog) {
console.log('[DayNightTint] start() 呼び出し。初期 timeOfDay =', this.timeOfDay.toFixed(3), 'ノード =', this.node.name);
}
}
update(deltaTime: number) {
if (this.autoUpdateTime) {
const dayLen = Math.max(this.dayLengthSeconds, 0.001);
const speed = 1.0 / dayLen; // 1秒あたりどれだけ進むか
this.timeOfDay += speed * deltaTime;
// 0〜1 の範囲でループ
if (this.timeOfDay > 1.0) {
this.timeOfDay -= 1.0;
} else if (this.timeOfDay < 0.0) {
this.timeOfDay += 1.0;
}
}
this._applyTint();
}
/**
* 外部から時間をセットしたい場合の API。
* 0〜1 の範囲にクランプされます。
*/
public setTimeOfDay(value: number) {
this.timeOfDay = math.clamp01(value);
if (this.debugLog) {
console.log('[DayNightTint] setTimeOfDay() により時間が更新されました:', this.timeOfDay.toFixed(3));
}
this._applyTint();
}
/**
* 現在の timeOfDay に対応する色を計算して適用する。
*/
private _applyTint() {
const t = math.clamp01(this.timeOfDay);
// 0〜1 を 4区間に分割して、夜→朝→昼→夕→夜 を補間する
const color = this._evaluateColor(t);
// 実際に Renderer へ適用
for (const r of this._uiRenderers) {
r.color = color;
}
for (const mr of this._meshRenderers) {
// MeshRenderer は color プロパティを持たないので、sharedMaterial の色を変えるなどの方法が必要。
// ここでは簡易的に、mainColor プロパティ(存在する場合)を変更してみる。
const mat = mr.getSharedMaterial(0);
if (mat && (mat as any).setProperty) {
// 多くの標準シェーダでは "mainColor" か "albedo" が色プロパティとして使われます。
// プロジェクトのシェーダに合わせて適宜変更してください。
try {
(mat as any).setProperty('mainColor', color);
} catch (e) {
// 失敗してもゲームが止まらないように握りつぶす
if (this.debugLog) {
console.warn('[DayNightTint] MeshRenderer のマテリアルに mainColor プロパティが見つかりませんでした。', mr.node.name);
}
}
}
}
}
/**
* timeOfDay に応じて 4点 (night - dawn - day - dusk - night) を補間して Color を決定する。
*/
private _evaluateColor(t: number): Color {
// 区間の境界
const dawnStart = 0.0;
const dawnEnd = 0.25;
const dayEnd = 0.5;
const duskEnd = 0.75;
const nightEnd = 1.0;
let fromColor: Color;
let toColor: Color;
let localT: number;
if (t <= dawnEnd) {
// 夜(0) → 朝(0.25)
fromColor = this.nightColor;
toColor = this.dawnColor;
localT = this._remap(t, dawnStart, dawnEnd);
} else if (t <= dayEnd) {
// 朝(0.25) → 昼(0.5)
fromColor = this.dawnColor;
toColor = this.dayColor;
localT = this._remap(t, dawnEnd, dayEnd);
} else if (t <= duskEnd) {
// 昼(0.5) → 夕(0.75)
fromColor = this.dayColor;
toColor = this.duskColor;
localT = this._remap(t, dayEnd, duskEnd);
} else {
// 夕(0.75) → 夜(1.0)
fromColor = this.duskColor;
toColor = this.nightColor;
localT = this._remap(t, duskEnd, nightEnd);
}
// カーブ適用
localT = this._applyCurve(localT);
// Color.lerp は out 引数が必要なので、一時オブジェクトを使う
const out = new Color();
Color.lerp(out, fromColor, toColor, localT);
return out;
}
/**
* [min, max] を [0,1] に正規化
*/
private _remap(value: number, min: number, max: number): number {
if (max - min <= 0.00001) {
return 0;
}
return math.clamp01((value - min) / (max - min));
}
/**
* 設定されたカーブタイプに応じて t を変形する。
*/
private _applyCurve(t: number): number {
t = math.clamp01(t);
switch (this.brightnessCurve) {
case BrightnessCurveType.LINEAR:
return t;
case BrightnessCurveType.SMOOTH_STEP:
// Smoothstep: 3t^2 - 2t^3
return t * t * (3 - 2 * t);
case BrightnessCurveType.SHARP:
// 少し急峻な S 字カーブ
// ここでは簡易的に SmoothStep を2回適用
let s = t * t * (3 - 2 * t);
return s * s * (3 - 2 * s);
default:
return t;
}
}
/**
* このノードおよび子ノードから UIRenderer / MeshRenderer を収集してキャッシュする。
*/
private _cacheRenderers() {
this._uiRenderers.length = 0;
this._meshRenderers.length = 0;
if (this.applyToChildren) {
this.node.getComponentsInChildren(UIRenderer, this._uiRenderers);
this.node.getComponentsInChildren(MeshRenderer, this._meshRenderers);
} else {
const ui = this.node.getComponent(UIRenderer);
if (ui) {
this._uiRenderers.push(ui);
}
const mr = this.node.getComponent(MeshRenderer);
if (mr) {
this._meshRenderers.push(mr);
}
}
if (this.debugLog) {
console.log('[DayNightTint] _cacheRenderers():', {
applyToChildren: this.applyToChildren,
uiCount: this._uiRenderers.length,
meshCount: this._meshRenderers.length,
}, this.node);
}
}
}
コードのポイント解説
- onLoad()
– 対象ノード(および子ノード)からUIRenderer/MeshRendererを収集してキャッシュ。
– 見つからない場合はconsole.warnで警告を出しつつ、ゲーム自体は動作し続ける防御的実装。
– 初期状態のtimeOfDayに応じた色を一度適用。 - update(deltaTime)
–autoUpdateTimeが ON の場合のみ、dayLengthSecondsに基づいてtimeOfDayを進める。
–timeOfDayは 0〜1 の範囲でループさせる。
– 毎フレームthis._applyTint()を呼び出して色を更新。 - setTimeOfDay()
– 外部スクリプトからtimeOfDayを明示的に設定したい場合に使える public メソッド。
– 値を 0〜1 にクランプし、即座に色を更新。 - _evaluateColor()
–timeOfDayを 4 区間(夜→朝→昼→夕→夜)に分割し、
nightColor, dawnColor, dayColor, duskColorの間を補間して最終的な色を算出。
– 区間内の補間係数localTに対して_applyCurve()でカーブ補正を行い、自然な明暗変化を実現。 - _cacheRenderers()
–applyToChildrenに応じて、対象ノードのみ/子孫ノードすべてから Renderer を収集。
– 収集はonLoad()で 1 回だけ行い、update()では毎フレーム走査しないため軽量。 - MeshRenderer への適用
– 汎用的な対応として、マテリアルのmainColorプロパティに色をセットする例を記述。
– プロジェクトのシェーダ設定によってはプロパティ名が異なるため、必要に応じて “albedo” などに変更してください。
使用手順と動作確認
1. スクリプトファイルの作成
- エディタの Assets パネルで、スクリプトを置きたいフォルダ(例:
assets/scripts)を右クリックします。 - Create → TypeScript を選択し、ファイル名を
DayNightTint.tsとして作成します。 - 作成された
DayNightTint.tsをダブルクリックしてエディタで開き、
既存のテンプレートコードをすべて削除して、前述の TypeScript コードを貼り付けて保存します。
2. テスト用ノードの作成(2D UI の例)
- Hierarchy パネルで右クリック → Create → UI → Canvas を選択し、Canvas を作成します(既にある場合は流用してOK)。
- Canvas を選択した状態で、右クリック → Create → UI → Sprite を選択し、テスト用の Sprite ノードを作成します。
ノード名は分かりやすくBackgroundSpriteなどにしておくと良いです。 - Inspector で Sprite の SpriteFrame に、任意の画像(背景用のテクスチャなど)を設定します。
3. DayNightTint コンポーネントのアタッチ
- Hierarchy で
BackgroundSpriteノードを選択します。 - Inspector 下部の Add Component ボタンをクリックします。
- メニューから Custom → DayNightTint を選択してアタッチします。
4. Inspector プロパティの設定例
Inspector 上で、DayNightTint の各プロパティを次のように設定してみてください。
- autoUpdateTime: チェック ON(自動進行させる)
- timeOfDay: 0.25(朝) ※autoUpdateTime ON の場合は起動後に自動で変化します。
- dayLengthSeconds: 30(30秒で 1日が一周)
- dayColor: (255, 255, 255, 255)
- nightColor: (60, 60, 120, 255)
- duskColor: (255, 170, 130, 255)
- dawnColor: (220, 210, 255, 255)
- brightnessCurve: SMOOTH_STEP
- applyToChildren: チェック ON(Canvas 以下に背景などを複数置く場合に便利)
- targetGroupTag: “Background” など任意
- debugLog: 初回は ON にしてログを確認しても良いです
5. プレビューで動作確認
- エディタ右上の Play ボタン(ブラウザ or シミュレータ)をクリックしてゲームを実行します。
- 数十秒間観察すると、背景 Sprite の色が「夜 → 朝 → 昼 → 夕 → 夜」とゆっくり変化していくのが分かります。
- 変化が遅すぎる/速すぎる場合は、dayLengthSeconds を調整してください。
例: 10秒で一周させたい場合はdayLengthSeconds = 10にします。
6. 時間を手動で動かして確認する(autoUpdateTime OFF)
色の変化を細かく確認したい場合は、以下のようにします。
- Inspector で autoUpdateTime のチェックを OFF にします。
- ゲームを再生(Play)します。
- ゲーム実行中に、Inspector の timeOfDay スライダー を 0.0〜1.0 の間で動かしてみてください。
それに合わせて即座に色が変化するはずです。
7. MeshRenderer(3D オブジェクト)での使用例
3D シーンで、建物や地形の色を昼夜で変えたい場合も同様に使えます。
- Hierarchy で 3D オブジェクト(例:
Groundノード)を作成し、MeshRenderer を付与します(3D オブジェクトのプリセットには最初から付いていることが多い)。 - そのノードに対して Add Component → Custom → DayNightTint を追加します。
- Inspector で applyToChildren を ON にしておくと、子オブジェクトの MeshRenderer にも色が適用されます。
- 使用しているマテリアルのシェーダに
mainColorという Color プロパティが存在することを確認してください。
存在しない場合は、DayNightTint.tsのsetProperty('mainColor', color)の部分を、
プロジェクトのシェーダで使用しているプロパティ名(例:'albedo')に変更してください。
まとめ
DayNightTint コンポーネントは、
- 外部の GameManager などに一切依存せず、単体で「昼夜サイクルに応じた色補正」を実現できる。
- Inspector から 時間の進行速度・色・変化カーブ を調整できるため、デザイナーがノーコードで世界の雰囲気を作り込める。
- UI / 2D / 3D いずれにも適用可能で、背景・キャラクター・UI などを一括で昼夜対応させられる。
- タグ文字列(
targetGroupTag)を使えば、将来的に「背景だけ昼夜変化」「キャラは一定の明るさ」などの制御も容易。
このように、アタッチするだけで完結する汎用コンポーネントとして実装しておくと、
- プロトタイプ段階で素早く「時間帯の雰囲気」を試せる。
- 別プロジェクトにも
DayNightTint.tsをそのままコピーして使い回せる。 - 将来的に「本格的な時間管理システム」を導入した際も、
setTimeOfDay()を呼ぶだけで連携できる。
という利点があり、ゲーム開発の効率を大きく高めることができます。
まずはシンプルなシーンに導入して色の変化を体感し、自分のゲームに合った色・スピード・カーブを探ってみてください。




