【Cocos Creator】アタッチするだけ!BuffTimer (バフ時間管理)の実装方法【TypeScript】

Godot 4ゲーム制作 実践ドリル 100本ノック

新品価格
¥1,250から
(2025/12/13 21:27時点)

Godot4& GDScriptではじめる 2Dゲーム開発レシピ

新品価格
¥590から
(2025/12/13 21:46時点)

Unity 6 C#スクリプト 100本ノック

新品価格
¥1,230から
(2025/12/29 10:28時点)

Cocos Creator100本ノックTypeScriptで書く!

新品価格
¥1,250から
(2025/12/29 10:32時点)

【Cocos Creator 3.8】BuffTimer(バフ時間管理)の実装:アタッチするだけで「バフ残り時間バー表示&自動解除」を実現する汎用スクリプト

このガイドでは、任意のノードにアタッチするだけで

  • バフの残り時間を UI バー(プログレスバー)として表示
  • 時間切れで自動的に「バフ終了」イベントを発行
  • 任意のコールバック(例:ステータスを元に戻す処理)を簡単にフック

できる汎用コンポーネント BuffTimer を実装します。

このコンポーネントは、例えば「攻撃力アップ」「移動速度アップ」などの一時的な強化状態の残り時間をプレイヤーに見せたいときに便利です。UI ノードにアタッチするだけで動き、他のカスタムスクリプトへの依存は一切ありません。


コンポーネントの設計方針

機能要件の整理

  • バフの総時間残り時間を管理する。
  • 残り時間を 0〜1 の割合に変換し、UI バー(ProgressBar または Sprite の fillRange) に反映する。
  • バフ開始時に初期化し、時間経過に応じて自動カウントダウンする。
  • 残り時間が 0 になったら
    • 自動でタイマーを停止
    • Inspector で設定した「バフ終了時コールバック」を呼び出す(任意)
    • 必要に応じて自動でノードを非表示/破棄するオプション
  • 外部依存をなくすため、他の GameManager などには一切依存しない
  • 標準コンポーネント(ProgressBar / Sprite)が無い場合は
    • 警告ログを出す
    • 安全に動作(バー更新をスキップ)

UI バーの対応パターン

BuffTimer は、以下の 2 パターンの UI バーに対応します。

  1. ProgressBar コンポーネント(推奨)
    • Node に ProgressBar をアタッチしている場合、progress プロパティを更新します。
  2. Sprite の fillRange を使ったバー
    • Node に Sprite をアタッチし、Fill モードに設定している場合、fillRange を更新します。

どちらも自動検出します。両方ついている場合は ProgressBar を優先し、それが無ければ Sprite を使います。

Inspector で設定可能なプロパティ

BuffTimer コンポーネントに用意する @property を一覧で整理します。

  • totalDuration (number)
    • バフの総時間(秒)。
    • 例:5.0 にすると 5 秒間のバフ。
    • 0 以下の場合は自動で 0.1 秒に補正してエラーを回避。
  • autoStartOnLoad (boolean)
    • コンポーネントが有効化されたときに自動でバフタイマーを開始するかどうか。
    • true:onEnablestartBuff() を自動実行。
    • false:外部スクリプトから startBuff() を呼ぶまで待機。
  • startFull (boolean)
    • バフ開始時にバーを満タンから減らしていくかどうか。
    • true:開始時に progress=1 → 0 に向かって減少。
    • false:開始時に progress=0 → 1 に向かって増加(「チャージ」風表現)。
  • hideNodeWhenFinished (boolean)
    • バフ終了時に、この Node を自動で非表示にするかどうか。
    • true:終了時に this.node.active = false;
    • false:表示はそのまま。
  • destroyNodeWhenFinished (boolean)
    • バフ終了時に、この Node を自動で破棄するかどうか。
    • true:終了時に this.node.destroy();
    • false:破棄しない。
    • 注意:破棄は非表示より強い動作なので、true の場合は非表示設定より優先されます。
  • loop (boolean)
    • バフ終了後、自動的に再度同じ時間で開始するかどうか。
    • true:ループ。終了時に startBuff() を自動呼び出し。
    • false:一度きり。
  • onBuffStartEventName (string)
    • バフ開始時に、この Node から発行するカスタムイベント名。
    • 空文字列の場合はイベントを発行しない。
    • 例:"BuffStarted"
  • onBuffEndEventName (string)
    • バフ終了時に、この Node から発行するカスタムイベント名。
    • 空文字列の場合はイベントを発行しない。
    • 例:"BuffEnded"
  • debugLog (boolean)
    • コンソールにデバッグログを出力するかどうか。
    • true:開始/終了/警告などをログ出力。
    • false:ログを抑制。

これらのプロパティにより、BuffTimer は単独で完結しつつ、色々な UI やゲームロジックに柔軟に対応できます。


TypeScriptコードの実装

以下が完成した BuffTimer コンポーネントの全コードです。


import { _decorator, Component, Node, ProgressBar, Sprite, UITransform, EventTarget, log, warn } from 'cc';
const { ccclass, property } = _decorator;

/**
 * BuffTimer
 * 任意のノードにアタッチするだけで、
 * - バフ時間をカウントダウン
 * - ProgressBar または Sprite(fillRange) による残り時間バー表示
 * - 終了時にイベント発行 / 自動非表示 / 自動破棄 / ループ
 * を行う汎用コンポーネント。
 */
@ccclass('BuffTimer')
export class BuffTimer extends Component {

    @property({
        tooltip: 'バフの総時間(秒)。0 以下の場合は自動で 0.1 秒に補正されます。'
    })
    public totalDuration: number = 5.0;

    @property({
        tooltip: 'コンポーネントが有効化されたときに自動でバフタイマーを開始するかどうか。'
    })
    public autoStartOnLoad: boolean = true;

    @property({
        tooltip: 'true の場合、バーは満タン(1)から 0 へ減少します。false の場合、0 から 1 へ増加します。'
    })
    public startFull: boolean = true;

    @property({
        tooltip: 'バフ終了時に、このノードを自動で非表示にするかどうか。destroyNodeWhenFinished が true の場合は無視されます。'
    })
    public hideNodeWhenFinished: boolean = false;

    @property({
        tooltip: 'バフ終了時に、このノードを自動で破棄するかどうか。true の場合、hideNodeWhenFinished より優先されます。'
    })
    public destroyNodeWhenFinished: boolean = false;

    @property({
        tooltip: 'バフ終了時に自動的に再度同じ時間で開始するかどうか。'
    })
    public loop: boolean = false;

    @property({
        tooltip: 'バフ開始時に、このノードから発行するカスタムイベント名。空文字列の場合は発行しません。'
    })
    public onBuffStartEventName: string = '';

    @property({
        tooltip: 'バフ終了時に、このノードから発行するカスタムイベント名。空文字列の場合は発行しません。'
    })
    public onBuffEndEventName: string = '';

    @property({
        tooltip: 'true の場合、開始・終了・警告などのデバッグログをコンソールに出力します。'
    })
    public debugLog: boolean = false;

    // 内部状態
    private _elapsed: number = 0;           // 経過時間(秒)
    private _running: boolean = false;      // タイマーが動作中かどうか
    private _progressBar: ProgressBar | null = null;
    private _sprite: Sprite | null = null;
    private _eventTarget: EventTarget = new EventTarget();

    onLoad() {
        // 必要な UI コンポーネントを取得(存在しない場合は null)
        this._progressBar = this.getComponent(ProgressBar);
        this._sprite = this.getComponent(Sprite);

        if (!this._progressBar && !this._sprite) {
            warn('[BuffTimer] ProgressBar も Sprite も見つかりません。このノードに ProgressBar または Sprite( Fill モード ) を追加してください。 node=', this.node.name);
        }

        // Sprite がある場合、Fill モードであるかを軽くチェック(強制はしない)
        if (this._sprite) {
            const uiTrans = this.node.getComponent(UITransform);
            if (!uiTrans) {
                warn('[BuffTimer] Sprite はありますが UITransform がありません。UI ノードとして使用する場合は UITransform を追加してください。 node=', this.node.name);
            }
        }

        // totalDuration の防御的補正
        if (this.totalDuration <= 0) {
            warn('[BuffTimer] totalDuration が 0 以下のため、0.1 秒に補正します。');
            this.totalDuration = 0.1;
        }

        // 初期状態としてバーを 0 か 1 に設定
        this._applyProgress(this.startFull ? 1 : 0);
    }

    onEnable() {
        if (this.autoStartOnLoad) {
            this.startBuff();
        }
    }

    update(deltaTime: number) {
        if (!this._running) {
            return;
        }

        this._elapsed += deltaTime;
        let ratio = this._elapsed / this.totalDuration;

        if (ratio > 1) {
            ratio = 1;
        }

        // startFull が true の場合は 1→0 へ、false の場合は 0→1 へ
        const displayProgress = this.startFull ? (1 - ratio) : ratio;
        this._applyProgress(displayProgress);

        if (this._elapsed >= this.totalDuration) {
            this._onBuffFinished();
        }
    }

    /**
     * バフタイマーを開始またはリスタートします。
     * 外部スクリプトからも呼び出し可能です。
     */
    public startBuff(): void {
        this._elapsed = 0;
        this._running = true;

        // 開始時のバーの状態をリセット
        this._applyProgress(this.startFull ? 1 : 0);

        if (this.debugLog) {
            log('[BuffTimer] Buff started. duration =', this.totalDuration, 'sec, node =', this.node.name);
        }

        // カスタムイベント発行(バフ開始)
        if (this.onBuffStartEventName) {
            this.node.emit(this.onBuffStartEventName, this);
            this._eventTarget.emit(this.onBuffStartEventName, this);
        }
    }

    /**
     * バフタイマーを停止します(時間はリセットされません)。
     */
    public stopBuff(): void {
        this._running = false;
        if (this.debugLog) {
            log('[BuffTimer] Buff stopped. node =', this.node.name);
        }
    }

    /**
     * バフタイマーを一時停止します(stopBuff と同義ですが、意味を明確にするために用意)。
     */
    public pauseBuff(): void {
        this.stopBuff();
    }

    /**
     * バフタイマーが現在動作中かどうかを返します。
     */
    public isRunning(): boolean {
        return this._running;
    }

    /**
     * 残り時間(秒)を返します。
     */
    public getRemainingTime(): number {
        return Math.max(0, this.totalDuration - this._elapsed);
    }

    /**
     * 残り割合(0〜1)を返します。
     * startFull に関わらず、0 = 終了、1 = フルタイム残り。
     */
    public getRemainingRatio(): number {
        const remaining = this.getRemainingTime();
        return remaining / this.totalDuration;
    }

    /**
     * カスタムイベントをリッスンするための EventTarget を取得します。
     * this.node.on(...) ではなく、BuffTimer 内部のイベントで連携したい場合に使用します。
     */
    public getEventTarget(): EventTarget {
        return this._eventTarget;
    }

    // 内部:バーの見た目を更新
    private _applyProgress(value: number): void {
        // 0〜1 にクランプ
        const v = Math.min(1, Math.max(0, value));

        if (this._progressBar) {
            this._progressBar.progress = v;
        } else if (this._sprite) {
            // Sprite の fillRange を利用する場合。Sprite 側は Fill モードに設定しておく必要があります。
            this._sprite.fillRange = v;
        }
    }

    // 内部:バフ終了時の処理
    private _onBuffFinished(): void {
        this._running = false;

        // 終了時は常に 0 にしておく(startFull に関わらず)
        this._applyProgress(this.startFull ? 0 : 1);

        if (this.debugLog) {
            log('[BuffTimer] Buff finished. node =', this.node.name);
        }

        // カスタムイベント発行(バフ終了)
        if (this.onBuffEndEventName) {
            this.node.emit(this.onBuffEndEventName, this);
            this._eventTarget.emit(this.onBuffEndEventName, this);
        }

        // 自動非表示・破棄
        if (this.destroyNodeWhenFinished) {
            this.node.destroy();
            return; // ここで処理終了
        } else if (this.hideNodeWhenFinished) {
            this.node.active = false;
        }

        // ループ設定があれば再スタート
        if (this.loop) {
            this.startBuff();
        }
    }
}

コードのポイント解説

  • onLoad()
    • ProgressBar または SpritegetComponent で取得。
    • どちらもなければ warn で警告を出し、バー更新はスキップ(防御的)。
    • totalDuration <= 0 の場合は 0.1 秒に補正。
    • 開始前のバー状態を startFull に応じて 0 or 1 に設定。
  • onEnable()
    • autoStartOnLoad が true のとき、自動で startBuff() を呼びます。
  • update(deltaTime)
    • _running が true のときだけ経過時間を加算。
    • _elapsed / totalDuration で進行割合を計算し、startFull に応じて 1→0 or 0→1 の値に変換。
    • バーの見た目更新は _applyProgress() に集約。
    • 時間が尽きたら _onBuffFinished() で終了処理。
  • startBuff()
    • 経過時間をリセットし、_running = true
    • バーの状態を初期化(満タン or 空)。
    • 必要なら開始イベント(onBuffStartEventName)を発行。
  • _onBuffFinished()
    • _running = false とし、バーを終了状態に更新。
    • 必要なら終了イベント(onBuffEndEventName)を発行。
    • destroyNodeWhenFinished / hideNodeWhenFinished / loop を順に処理。
  • イベント発行
    • this.node.emit(eventName, this) で、同じノード上の他コンポーネントから簡単に受け取れるようにしています。
    • さらに _eventTarget も用意しているので、BuffTimer に直接リスナーを登録したい場合にも対応可能です。

使用手順と動作確認

1. スクリプトファイルの作成

  1. Assets パネルで右クリック → Create → TypeScript を選択します。
  2. ファイル名を BuffTimer.ts にします。
  3. 作成された BuffTimer.ts をダブルクリックして開き、内容をすべて削除してから、上記のコードを丸ごと貼り付けて保存します。

2. テスト用 UI ノード(ProgressBar)の作成

まずは ProgressBar を使った例で動作確認します。

  1. Hierarchy パネルで右クリック → Create → UI → Canvas を選択し、Canvas を作成します(既にある場合は省略)。
  2. Canvas を選択し、右クリック → Create → UI → ProgressBar を選択して ProgressBar ノードを作成します。
    • 作成されたノード名はデフォルトで ProgressBar になっているはずです。
  3. Hierarchy で ProgressBar ノードを選択し、Inspector を確認します。
    • ProgressBar コンポーネント がアタッチされていることを確認します。

3. BuffTimer コンポーネントのアタッチ

  1. Hierarchy で先ほどの ProgressBar ノードを選択します。
  2. Inspector の下部で Add Component ボタンをクリックします。
  3. Custom カテゴリ(または Scripts カテゴリ)から BuffTimer を選択してアタッチします。

4. Inspector でプロパティを設定

ProgressBar ノードに BuffTimer がアタッチされた状態で、Inspector の BuffTimer セクションを以下のように設定します。

  • totalDuration: 5
    • 5 秒間のバフとして動作します。
  • autoStartOnLoad: true
    • シーン再生と同時にバフタイマーが自動開始されます。
  • startFull: true
    • バーが満タンから 0 へ向かって減少します(残り時間表示)。
  • hideNodeWhenFinished: false
  • destroyNodeWhenFinished: false
  • loop: false
  • onBuffStartEventName: 空のまま(今回は未使用)
  • onBuffEndEventName: 空のまま(今回は未使用)
  • debugLog: true(ログを見たい場合)

5. ゲームを再生して確認

  1. 上部ツールバーの Play ボタンを押してシーンを再生します。
  2. Game ビュー上で、ProgressBar が5 秒かけて徐々に減少して 0 になることを確認します。
  3. Console パネルを開くと、[BuffTimer] Buff started...[BuffTimer] Buff finished... のログが表示されているはずです(debugLog = true の場合)。

6. Sprite(fillRange) を使ったバーでの確認(オプション)

次に、Sprite の fillRange を使ったバーでも動作することを確認します。

  1. Hierarchy で Canvas を右クリック → Create → UI → Sprite を選択します。
  2. 新しい Sprite ノード(例:BuffBarSprite)を選択し、Inspector の Sprite コンポーネントを確認します。
  3. Sprite コンポーネントの設定を変更します:
    • TypeFilled に変更。
    • Fill Type を好みで HorizontalRadial などに設定。
    • Fill Start を 0、Fill Range を 1 に設定。
  4. 同じ Sprite ノードに BuffTimer コンポーネントを追加します(ProgressBar は不要です)。
  5. BuffTimer のプロパティを先ほどと同様に設定します(totalDuration = 3 など、短めでも可)。
  6. シーンを再生し、Sprite の Fill 表示が時間経過と共に変化することを確認します。

7. バフ終了で「ステータスを戻す」処理をフックする例

BuffTimer は他のカスタムスクリプトに依存しませんが、イベントを使って他のコンポーネントから簡単にフックできます。ここでは、同じノード上の別スクリプトから「バフ終了時に攻撃力を元に戻す」イメージの処理を行う例を示します。

例:同じノードに AttackBuffHandler.ts を追加し、以下のように書きます。


import { _decorator, Component, log } from 'cc';
const { ccclass, property } = _decorator;

@ccclass('AttackBuffHandler')
export class AttackBuffHandler extends Component {

    @property({
        tooltip: '攻撃力の通常値'
    })
    public normalAttack: number = 10;

    @property({
        tooltip: 'バフ中の攻撃力'
    })
    public buffedAttack: number = 20;

    private _currentAttack: number = 0;

    start() {
        this._currentAttack = this.normalAttack;

        // 同じノード上の BuffTimer を取得
        const buffTimer = this.getComponent('BuffTimer') as any;
        if (!buffTimer) {
            log('[AttackBuffHandler] BuffTimer が見つかりません。同じノードに BuffTimer を追加してください。');
            return;
        }

        // ノードのイベントを利用して、バフ開始・終了をフック
        this.node.on('BuffStarted', this.onBuffStarted, this);
        this.node.on('BuffEnded', this.onBuffEnded, this);
    }

    onBuffStarted() {
        this._currentAttack = this.buffedAttack;
        log('[AttackBuffHandler] Buff started. attack =', this._currentAttack);
    }

    onBuffEnded() {
        this._currentAttack = this.normalAttack;
        log('[AttackBuffHandler] Buff ended. attack =', this._currentAttack);
    }
}

このとき、BuffTimer 側では以下のように設定します。

  • onBuffStartEventName: BuffStarted
  • onBuffEndEventName: BuffEnded

こうすることで、BuffTimer は依然として完全に独立したまま、他コンポーネントが自由に「バフ開始/終了」に反応できる設計になります。


まとめ

  • BuffTimer は、任意のノードにアタッチするだけで
    • バフ時間のカウントダウン管理
    • UI バー(ProgressBar / Sprite fillRange)への残り時間表示
    • バフ開始/終了イベントの発行
    • 自動非表示・自動破棄・ループ再生

    を行う完全独立・汎用コンポーネントです。

  • Inspector から
    • バフ時間(totalDuration)
    • 開始タイミング(autoStartOnLoad)
    • 表示方向(startFull)
    • 終了時の挙動(hide / destroy / loop)
    • イベント名(onBuffStartEventName / onBuffEndEventName)

    を調整するだけで、様々なゲームに簡単に適用できます。

  • 外部の GameManager やシングルトンに依存しないため、
    • UI プレハブにそのまま組み込める
    • 別プロジェクトへの持ち出しも容易
    • テストシーンで単体検証しやすい

    といった利点があります。

この BuffTimer をベースに、例えば「デバフタイマー」「スキルクールダウン表示」「チャージバー」などにも簡単に応用できます。プロジェクトの共通 UI 部品としてプレハブ化しておくと、ゲーム開発のスピードと保守性が大きく向上するはずです。

Godot 4ゲーム制作 実践ドリル 100本ノック

新品価格
¥1,250から
(2025/12/13 21:27時点)

Godot4& GDScriptではじめる 2Dゲーム開発レシピ

新品価格
¥590から
(2025/12/13 21:46時点)

Unity 6 C#スクリプト 100本ノック

新品価格
¥1,230から
(2025/12/29 10:28時点)

Cocos Creator100本ノックTypeScriptで書く!

新品価格
¥1,250から
(2025/12/29 10:32時点)

URLをコピーしました!