【Cocos Creator 3.8】LifetimeTimer の実装:アタッチするだけで「一定時間後に自動削除」できる汎用寿命スクリプト
パーティクル、弾丸、エフェクト、使い捨てのUIなど、「一定時間だけ存在して自動で消えてほしいノード」はゲーム内にたくさん登場します。この記事では、ノードにアタッチするだけで、生成から指定秒数後に自動的にノードを削除してくれる汎用コンポーネント LifetimeTimer を実装します。
外部の GameManager やシングルトンに一切依存せず、このコンポーネント単体で完結する設計になっているため、どのプロジェクトでも簡単に再利用できます。
コンポーネントの設計方針
機能要件
- このコンポーネントをアタッチしたノードを、生成から X 秒後に自動的に削除する。
- 削除対象は基本的にアタッチされたノード自身とする。
- 親を消すと想定外のノードまで巻き込むケースが多いため、安全なデフォルトとして自分自身を削除対象にする。
- どうしても親を消したいケースに対応するため、オプションで「親ノードを削除対象にする」設定を用意する。
- エディタ上のインスペクタから寿命時間や挙動を調整できるようにする。
- update で毎フレームカウントするのではなく、スケジューラ(scheduleOnce)を使って軽量に実装する。
- ゲーム一時停止(
director.pause())などでTimeScaleが変わっても、エンジン標準の時間経過に追従する(特別なカスタムタイム管理はしない)。
インスペクタで設定可能なプロパティ設計
LifetimeTimer では、以下のプロパティをインスペクタから設定できるようにします。
lifetime(number)- このコンポーネントが有効化されてから何秒後にノードを削除するかを指定。
- 単位は「秒」。
- 0 以下の場合は無効とし、エラーログを出して削除処理を行わない。
- デフォルト値:
1.0秒。
targetMode(enum)- どのノードを削除対象にするかを指定するモード。
- 候補:
- SELF: このコンポーネントがアタッチされたノード自身を削除(デフォルト)。
- PARENT: 親ノードを削除(親が存在しない場合は自分自身を削除)。
destroyChildrenOnly(boolean)trueの場合:- 削除対象ノード「そのもの」は残し、その子ノードだけを全削除する。
- パーティクルを子にぶら下げておき、親はコンテナとして残したい場合などに便利。
falseの場合:- 削除対象ノード自体を
destroy()する。
- 削除対象ノード自体を
- デフォルト値:
false。
autoStart(boolean)trueの場合:- コンポーネントが有効化されたタイミングで自動的にタイマーを開始。
- 通常はこちらを使う。
falseの場合:- 自動ではタイマーを開始せず、スクリプトから明示的に
startTimer()を呼び出す必要がある。 - 他のアニメーション終了後に寿命カウントを開始したい場合などに使用。
- 自動ではタイマーを開始せず、スクリプトから明示的に
- デフォルト値:
true。
logOnDestroy(boolean)- 寿命でノードを削除したときに、コンソールにログを出すかどうか。
- デバッグ中は
trueにしておくと、いつ削除されたかが分かりやすい。 - デフォルト値:
false。
これらのプロパティにより、「何秒後に」「どのノードを」「どの範囲で」削除するかを柔軟に指定できます。
TypeScriptコードの実装
以下が完成した LifetimeTimer.ts の全コードです。
import { _decorator, Component, Node, Enum, log, warn } from 'cc';
const { ccclass, property } = _decorator;
/**
* 寿命タイマーがどのノードを対象にするかを表す列挙型
*/
enum LifetimeTargetMode {
SELF = 0,
PARENT = 1,
}
Enum(LifetimeTargetMode);
@ccclass('LifetimeTimer')
export class LifetimeTimer extends Component {
@property({
tooltip: 'コンポーネント有効化(または startTimer 呼び出し)から何秒後に削除するかを指定します。0 以下の場合は無効になります。',
min: 0,
})
public lifetime: number = 1.0;
@property({
type: Enum(LifetimeTargetMode),
tooltip: '削除対象とするノードを指定します。\nSELF: このノード自身\nPARENT: 親ノード(親が無ければ自身)',
})
public targetMode: LifetimeTargetMode = LifetimeTargetMode.SELF;
@property({
tooltip: 'true の場合、削除対象ノード自体は残し、その子ノードのみ全て削除します。',
})
public destroyChildrenOnly: boolean = false;
@property({
tooltip: 'true の場合、コンポーネントが有効化されたタイミングで自動的にタイマーを開始します。',
})
public autoStart: boolean = true;
@property({
tooltip: 'true の場合、寿命で削除された際にコンソールへログを出力します。',
})
public logOnDestroy: boolean = false;
/**
* 内部的にスケジュールされた削除処理が有効かどうか
*/
private _timerScheduled: boolean = false;
onEnable() {
// autoStart が有効な場合、自動的にタイマーを開始
if (this.autoStart) {
this.startTimer();
}
}
onDisable() {
// コンポーネントが無効化された場合は、保険としてスケジュールを解除
this.cancelTimer();
}
onDestroy() {
// このコンポーネント自体が破棄されるときもスケジュールを解除
this.cancelTimer();
}
/**
* 寿命タイマーを開始します。
* lifetime が 0 以下の場合は警告を出して何もしません。
*/
public startTimer(): void {
if (this._timerScheduled) {
// すでにスケジュールされている場合はいったん解除して再スケジュール
this.cancelTimer();
}
if (this.lifetime <= 0) {
warn(`[LifetimeTimer] lifetime が 0 以下のため、削除処理は行われません。ノード名: ${this.node.name}`);
return;
}
// scheduleOnce は Component が提供する標準のスケジューラ機能
this.scheduleOnce(this._onLifetimeReached, this.lifetime);
this._timerScheduled = true;
}
/**
* 進行中の寿命タイマーをキャンセルします。
* すでに削除処理が実行済みの場合は何もしません。
*/
public cancelTimer(): void {
if (!this._timerScheduled) {
return;
}
this.unschedule(this._onLifetimeReached);
this._timerScheduled = false;
}
/**
* 寿命に達したときに呼び出されるコールバック。
* 実際の削除処理をここで行います。
*/
private _onLifetimeReached(): void {
this._timerScheduled = false;
let targetNode: Node | null = null;
switch (this.targetMode) {
case LifetimeTargetMode.SELF:
targetNode = this.node;
break;
case LifetimeTargetMode.PARENT:
if (this.node.parent) {
targetNode = this.node.parent;
} else {
// 親が無い場合は自分自身を対象とする
targetNode = this.node;
warn(`[LifetimeTimer] targetMode が PARENT ですが親ノードが存在しません。自身を削除対象にします。ノード名: ${this.node.name}`);
}
break;
default:
targetNode = this.node;
warn(`[LifetimeTimer] 未知の targetMode が指定されました。SELF として扱います。ノード名: ${this.node.name}`);
break;
}
if (!targetNode || !targetNode.isValid) {
// すでにどこかで削除されている場合は何もしない
return;
}
if (this.destroyChildrenOnly) {
// 子ノードのみ削除
const children = [...targetNode.children];
for (const child of children) {
if (child && child.isValid) {
child.destroy();
}
}
if (this.logOnDestroy) {
log(`[LifetimeTimer] 子ノードのみ削除しました。対象ノード: ${targetNode.name}, lifetime: ${this.lifetime.toFixed(3)}s`);
}
} else {
// ノード自体を削除
if (this.logOnDestroy) {
log(`[LifetimeTimer] 寿命によりノードを削除します: ${targetNode.name}, lifetime: ${this.lifetime.toFixed(3)}s`);
}
targetNode.destroy();
}
}
}
コードのポイント解説
- 列挙型
LifetimeTargetModeSELF/PARENTを Enum 化し、インスペクタ上でプルダウン選択できるようにしています。Enum(LifetimeTargetMode);を呼ぶことで、Cocos Creator のインスペクタに列挙値が表示されます。
- プロパティ定義
- すべての挙動は
@propertyでインスペクタから調整可能です。 lifetimeにはmin: 0を設定し、エディタ上で負の値を入力しにくくしています(コード側でも 0 以下を無効としてチェック)。targetModeにはtype: Enum(LifetimeTargetMode)を指定し、インスペクタでの選択を可能にしています。
- すべての挙動は
- ライフサイクルメソッド
onEnable()autoStartがtrueの場合にstartTimer()を呼び、アタッチして有効化されただけで自動で動作するようにしています。
onDisable()/onDestroy()- どちらも
cancelTimer()を呼び、不要になったスケジュールを確実に解除します。 - これにより、コンポーネントが無効化された後に誤って削除処理が走ることを防ぎます。
- どちらも
- タイマー制御
startTimer()- すでにタイマーが動いている場合は一度
cancelTimer()してから再スケジュール。 lifetime <= 0の場合はwarnを出して処理を中止し、意図しない即時削除を防止しています。this.scheduleOnce(this._onLifetimeReached, this.lifetime);で、指定秒後に一度だけコールバックを呼び出します。
- すでにタイマーが動いている場合は一度
cancelTimer()unschedule(this._onLifetimeReached)を呼んで、設定済みのスケジュールを解除します。
- 削除処理
_onLifetimeReached()targetModeに応じて削除対象ノード(targetNode)を決定。destroyChildrenOnlyがtrueの場合:targetNode.childrenをコピーしてループし、各子ノードに対してdestroy()を呼びます。
destroyChildrenOnlyがfalseの場合:targetNode.destroy()を呼び、ノードそのものを破棄します。
logOnDestroyがtrueのときのみlog()を出力し、本番ビルドではオフにしやすい設計にしています。
使用手順と動作確認
1. スクリプトファイルの作成
- エディタの Assets パネルで、任意のフォルダ(例:
assets/scripts)を右クリックします。 - Create → TypeScript を選択します。
- 新規作成されたスクリプトの名前を
LifetimeTimer.tsに変更します。 - 作成した
LifetimeTimer.tsをダブルクリックしてエディタ(VS Code など)で開き、
既存のテンプレートコードをすべて削除して、前述の TypeScript コードを丸ごと貼り付けて保存します。
2. テスト用ノードの作成
ここでは例として、画面に表示されるスプライトを一定時間後に自動削除するケースで説明します。
- Hierarchy パネルで右クリック → Create → 2D Object → Sprite を選択し、テスト用スプライトノードを作成します。
- 作成されたノードに任意の名前(例:
TestBullet)を付けます。 - Inspector で Sprite コンポーネントの SpriteFrame に適当な画像を設定し、画面に表示されるようにします。
3. LifetimeTimer コンポーネントのアタッチ
- Hierarchy で先ほど作成した
TestBulletノードを選択します。 - Inspector の一番下にある Add Component ボタンをクリックします。
- Custom → LifetimeTimer を選択します。
- もしリストに出てこない場合は、スクリプトの保存漏れやコンパイルエラーがないか確認し、エディタを再読み込みしてみてください。
4. プロパティの設定例
Inspector で LifetimeTimer の各プロパティを設定します。
- パターンA: 弾丸を2秒後に消す(一般的な使い方)
lifetime: 2.0targetMode: SELFdestroyChildrenOnly: OFF(チェックを外す)autoStart: ON(チェックを入れる)logOnDestroy: 任意(デバッグ中は ON にするとログが確認しやすい)
この設定では、シーン開始またはノード生成から 2 秒後に、
TestBulletノード自体が自動的に破棄されます。 - パターンB: 親コンテナごと 5 秒後に消す
- Hierarchy で、空のノード
BulletContainerを作成し、その子としてTestBulletを配置します。 LifetimeTimerを TestBullet ではなく BulletContainer にアタッチします。- 設定:
lifetime: 5.0targetMode: SELF(コンテナ自体を削除)destroyChildrenOnly: OFF
この場合、コンテナとその中身がまとめて 5 秒後に削除されます。
- Hierarchy で、空のノード
- パターンC: 親ノードの子だけを3秒後に消す(親は残す)
- 例:
EffectRootノードの下に複数のパーティクルノードがぶら下がっている構成。 LifetimeTimerを EffectRoot にアタッチし、以下のように設定します:lifetime: 3.0targetMode: SELF(対象は EffectRoot)destroyChildrenOnly: ON
この設定では、3 秒後に EffectRoot の子ノード(パーティクルなど)だけがすべて削除され、EffectRoot 自体は残るため、再利用がしやすくなります。
- 例:
5. 実行して動作確認
- エディタ右上の ▶︎ Play(シミュレータ) または Preview in Browser でシーンを再生します。
- 設定した
lifetime秒後に、対象ノード(または子ノード)が削除されることを確認します。 logOnDestroyを ON にしている場合は、Console パネルに[LifetimeTimer] 寿命によりノードを削除します: TestBullet, lifetime: 2.000s
のようなログが出力されることを確認します。
6. 実運用での使い方のコツ
- プレハブ(Prefab)に仕込む
- 弾丸やエフェクトの Prefab にあらかじめ
LifetimeTimerをアタッチしておくと、生成しただけで勝手に消えてくれるので管理が非常に楽になります。
- 弾丸やエフェクトの Prefab にあらかじめ
- スクリプトから手動で開始する場合
autoStartを OFF にしておき、必要なタイミングで:const lifetime = node.getComponent(LifetimeTimer); lifetime?.startTimer();のように呼び出します。
- アニメーション終了後に寿命カウントを開始したいときなどに有効です。
まとめ
この記事では、Cocos Creator 3.8 / TypeScript で、ノードにアタッチするだけで一定時間後に自動削除してくれる汎用コンポーネント LifetimeTimer を実装しました。
- 外部スクリプトへの依存ゼロで、どのプロジェクトにも簡単に持ち込める。
lifetime、targetMode、destroyChildrenOnly、autoStartなどをインスペクタから調整でき、弾丸・パーティクル・一時的な UI・エフェクトなど幅広い用途に対応できる。scheduleOnceを使ったシンプルな実装で、update の毎フレーム処理が不要なためパフォーマンスにも優しい。
こうした「寿命管理」をコンポーネントとして切り出しておくことで、生成したオブジェクトの後始末を毎回手で書く必要がなくなり、ゲームロジックをすっきり保つことができます。
今後は、同じ設計方針で「フェードアウトしてから削除するコンポーネント」「移動しながら寿命を迎えるコンポーネント」などを追加していけば、アタッチするだけでリッチな表現を量産できる再利用性の高いコンポーネント群を構築していけるはずです。




