【Cocos Creator】アタッチするだけ!TimedButton (時限スイッチ)の実装方法【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】TimedButton(時限スイッチ)の実装:アタッチするだけで「押してから◯秒だけONになるボタン」を実現する汎用スクリプト

このガイドでは、ボタンを押すと一定時間だけ「ON」状態になり、その後自動で「OFF」に戻る TimedButton(時限スイッチ)コンポーネント を実装します。
UI ボタンやゲーム内スイッチなどにアタッチするだけで、「押してから数秒間だけ有効」 という挙動を簡単に再利用できるようにするのが目的です。

本コンポーネントは他のカスタムスクリプトに一切依存せず、この1ファイルだけで完結 します。必要な設定はすべてインスペクタから行えるように設計します。


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

1. 機能要件の整理

  • ノードにアタッチすると、そのノードが「時限ボタン」として振る舞う。
  • ボタンの押下は以下の2通りをサポートする:
    • UI.Button コンポーネント付きノード:UI ボタンとしてクリックされたときに発火。
    • 任意ノード:マウス/タッチでノードをクリック(タップ)したときに発火。
  • 押されると「ON」状態になり、指定秒数が経過すると自動で「OFF」状態に戻る。
  • ON/OFF の状態は以下で可視化・連携できる:
    • インスペクタから参照できる isOn(現在状態) プロパティ(読み取り専用扱い)。
    • ON/OFF に変化したときに、インスペクタで設定した コールバック(EventHandler 配列) を呼び出す。
    • 任意で、ON/OFF でノードの色やスケールを変える簡易なビジュアルフィードバック機能。
  • 連打された場合:
    • ON 中にもう一度押されたら、残り時間をリセットして再スタート(常に「最後に押されてから◯秒後にOFF」)。
  • 外部の GameManager 等には依存せず、このコンポーネント単体で完結 する。

2. 外部依存をなくすためのアプローチ

  • クリック検出は以下を自前で処理:
    • UI.Button があれば、その clickEvents に自動で自分を登録。
    • なければ、Node.EventType.TOUCH_END / MOUSE_UP を自分で購読。
  • 外部状態管理は行わず、内部に isOn, remainingTime を持つ。
  • 他スクリプトとの連携は EventHandler 経由のみ(インスペクタで設定)。

3. インスペクタで設定可能なプロパティ設計

以下のプロパティを用意します。

  • duration(number)
    • ON 状態を維持する秒数。
    • 正の数で指定(例: 3.0)。0 以下の場合は 0.1 秒にクランプ。
  • autoRegisterButton(boolean)
    • 同じノードに Button コンポーネントがある場合、自動的に clickEvents に自分を登録するかどうか。
    • true の場合:Button クリックで TimedButton が発火。
    • false の場合:Button の clickEvents は開発者側で自由に設定。TimedButton は自前のタッチイベントでのみ発火。
  • consumePointerEvent(boolean)
    • 自前のタッチ/マウスイベントを使用する場合に、イベントを stopPropagation() するかどうか。
    • UI ツリーで他のハンドラにイベントを渡したくない場合に ON。
  • resetTimerOnPress(boolean)
    • ON 中に再度押されたとき、残り時間をリセットして延長するかどうか。
    • true:毎回「押してから duration 秒後に OFF」。
    • false:最初に ON になった時刻から duration 秒後に OFF(途中の押下は無視)。
  • startOn(boolean)
    • シーン開始時に ON 状態からスタートするかどうか。
    • true:ゲーム開始時に既に ON。duration だけ経過すると OFF。
    • false:ゲーム開始時は OFF。
  • enableVisualFeedback(boolean)
    • ON/OFF に応じてノードの見た目(色/スケール)を自動で変えるかどうか。
  • onColor(Color)
    • ON のときの色。対象は Sprite または UIRenderer(Label 等)。
    • enableVisualFeedback が true のときのみ有効。
  • offColor(Color)
    • OFF のときの色。
  • onScale(Vec3)
    • ON のときのスケール。
  • offScale(Vec3)
    • OFF のときのスケール。
  • onTurnOn(EventHandler[])
    • ON になった瞬間に呼ばれるイベント。
    • インスペクタで他ノードのメソッドを登録可能。
  • onTurnOff(EventHandler[])
    • OFF になった瞬間に呼ばれるイベント。
  • isOn(readonly)(boolean)
    • 現在 ON かどうかを確認するための読み取り専用フラグ。
    • インスペクタに表示してデバッグに利用。

TypeScriptコードの実装


import { _decorator, Component, Node, EventTouch, EventMouse, Button, EventHandler, Color, Sprite, UIRenderer, Vec3, director } from 'cc';
const { ccclass, property, executeInEditMode, menu } = _decorator;

/**
 * TimedButton
 * 押されると ON になり、指定秒数経過後に自動で OFF に戻る汎用コンポーネント。
 * - UI.Button があればクリックで発火(autoRegisterButton=true 時)。
 * - なければノード自体のタッチ/マウスクリックで発火。
 */
@ccclass('TimedButton')
@menu('Custom/TimedButton')
export class TimedButton extends Component {

    @property({
        tooltip: 'ON 状態を維持する秒数。\n0 以下の場合は 0.1 秒として扱います。'
    })
    public duration: number = 3.0;

    @property({
        tooltip: '同じノードに Button コンポーネントがある場合、\n自動的に clickEvents に自分を登録します。'
    })
    public autoRegisterButton: boolean = true;

    @property({
        tooltip: '自前のタッチ/マウスイベントを使用する場合、\nイベントを stopPropagation() して他のハンドラに渡さないようにします。'
    })
    public consumePointerEvent: boolean = false;

    @property({
        tooltip: 'ON 中に再度押されたとき、残り時間をリセットして延長するかどうか。'
    })
    public resetTimerOnPress: boolean = true;

    @property({
        tooltip: 'シーン開始時に ON 状態からスタートするかどうか。'
    })
    public startOn: boolean = false;

    @property({
        tooltip: 'ON/OFF に応じて色やスケールを自動で変更するかどうか。'
    })
    public enableVisualFeedback: boolean = true;

    @property({
        tooltip: 'ON のときの色(Sprite または UIRenderer 対象)。\nVisual Feedback 有効時のみ使用されます。'
    })
    public onColor: Color = new Color(0, 255, 0, 255);

    @property({
        tooltip: 'OFF のときの色。'
    })
    public offColor: Color = new Color(255, 255, 255, 255);

    @property({
        tooltip: 'ON のときのスケール。'
    })
    public onScale: Vec3 = new Vec3(1.05, 1.05, 1.0);

    @property({
        tooltip: 'OFF のときのスケール。'
    })
    public offScale: Vec3 = new Vec3(1.0, 1.0, 1.0);

    @property({
        type: [EventHandler],
        tooltip: 'ON になった瞬間に呼び出すイベント。\n他ノードのメソッドを登録して連携できます。'
    })
    public onTurnOn: EventHandler[] = [];

    @property({
        type: [EventHandler],
        tooltip: 'OFF になった瞬間に呼び出すイベント。'
    })
    public onTurnOff: EventHandler[] = [];

    @property({
        tooltip: '現在 ON 状態かどうか(読み取り専用)。',
        readonly: true
    })
    public isOn: boolean = false;

    // 内部状態
    private _remainingTime: number = 0;
    private _button: Button | null = null;
    private _renderer: UIRenderer | null = null;
    private _initialized: boolean = false;

    onLoad() {
        // duration の防御的クランプ
        if (this.duration <= 0) {
            console.warn('[TimedButton] duration が 0 以下のため、0.1 秒に補正します。');
            this.duration = 0.1;
        }

        // Button コンポーネント取得(あれば)
        this._button = this.getComponent(Button);
        if (!this._button) {
            // Button がなくても問題なく動作する想定なので、ここでは警告のみ出さない。
        }

        // 見た目制御用に Sprite or UIRenderer を取得(あれば)
        const sprite = this.getComponent(Sprite);
        if (sprite) {
            this._renderer = sprite;
        } else {
            const uiRenderer = this.getComponent(UIRenderer);
            if (uiRenderer) {
                this._renderer = uiRenderer;
            }
        }

        // 入力イベント登録
        this._registerInputEvents();

        // 初期状態の決定
        if (this.startOn) {
            this._turnOnInternal(true);
            this._remainingTime = this.duration;
        } else {
            this._turnOffInternal(true);
            this._remainingTime = 0;
        }

        this._initialized = true;
    }

    start() {
        // start 時点で Button が存在し、autoRegisterButton = true の場合、clickEvents に自分を登録
        if (this._button && this.autoRegisterButton) {
            this._registerToButtonClickEvents();
        }
    }

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

        this._remainingTime -= deltaTime;
        if (this._remainingTime <= 0) {
            this._remainingTime = 0;
            this._turnOffInternal(false);
        }
    }

    onEnable() {
        // ノードが再度有効になった時にイベントを再登録
        if (this._initialized) {
            this._registerInputEvents();
        }
    }

    onDisable() {
        this._unregisterInputEvents();
    }

    onDestroy() {
        this._unregisterInputEvents();
    }

    /**
     * 外部から明示的に押されたことにするための公開メソッド。
     * Button の clickEvents からもこのメソッドを呼び出します。
     */
    public press(): void {
        this._handlePressed();
    }

    // --- 内部実装 ---

    private _handlePressed(): void {
        if (!this.isOn) {
            // OFF → ON
            this._turnOnInternal(false);
            this._remainingTime = this.duration;
        } else {
            // すでに ON の場合
            if (this.resetTimerOnPress) {
                this._remainingTime = this.duration;
            }
            // resetTimerOnPress = false なら何もしない
        }
    }

    private _turnOnInternal(isInitial: boolean): void {
        if (this.isOn) {
            return;
        }
        this.isOn = true;
        this._applyVisualState();

        if (!isInitial) {
            // 初期状態で ON にしたときはイベントを飛ばさない
            this._emitEvents(this.onTurnOn);
        }
    }

    private _turnOffInternal(isInitial: boolean): void {
        if (!this.isOn && !isInitial) {
            return;
        }
        this.isOn = false;
        this._applyVisualState();

        if (!isInitial) {
            this._emitEvents(this.onTurnOff);
        }
    }

    private _applyVisualState(): void {
        if (!this.enableVisualFeedback) {
            return;
        }

        // 色変更
        if (this._renderer) {
            this._renderer.color = this.isOn ? this.onColor : this.offColor;
        }

        // スケール変更
        const targetScale = this.isOn ? this.onScale : this.offScale;
        this.node.setScale(targetScale);
    }

    private _emitEvents(handlers: EventHandler[]): void {
        if (!handlers || handlers.length === 0) {
            return;
        }
        EventHandler.emitEvents(handlers, this);
    }

    private _registerInputEvents(): void {
        // 自前のタッチ/マウスイベント
        this._unregisterInputEvents(); // 二重登録防止

        // Button があって autoRegisterButton = true の場合は、Button 経由のクリックを優先し、
        // ここではタッチ/マウスイベントも併用可とする(必要に応じて調整)。
        this.node.on(Node.EventType.TOUCH_END, this._onTouchEnd, this);
        this.node.on(Node.EventType.MOUSE_UP, this._onMouseUp, this);
    }

    private _unregisterInputEvents(): void {
        this.node.off(Node.EventType.TOUCH_END, this._onTouchEnd, this);
        this.node.off(Node.EventType.MOUSE_UP, this._onMouseUp, this);
    }

    private _onTouchEnd(event: EventTouch): void {
        if (!this.enabled || !this.node.activeInHierarchy) {
            return;
        }
        this._handlePressed();
        if (this.consumePointerEvent) {
            event.propagationStopped = true;
        }
    }

    private _onMouseUp(event: EventMouse): void {
        if (!this.enabled || !this.node.activeInHierarchy) {
            return;
        }
        this._handlePressed();
        if (this.consumePointerEvent) {
            event.propagationStopped = true;
        }
    }

    private _registerToButtonClickEvents(): void {
        if (!this._button) {
            return;
        }

        // すでに同じ EventHandler が登録されていないか簡易チェック
        const handlers = this._button.clickEvents;
        const selfUuid = this.node.uuid;

        const alreadyRegistered = handlers.some(h => {
            return h.target && h.target.uuid === selfUuid && h.component === 'TimedButton' && h.handler === 'press';
        });

        if (!alreadyRegistered) {
            const eh = new EventHandler();
            eh.target = this.node;
            eh.component = 'TimedButton';
            eh.handler = 'press';
            handlers.push(eh);
        }
    }
}

主要な処理の解説

  • onLoad
    • duration の下限チェック(防御的実装)。
    • ButtonSprite / UIRenderer を取得し、存在すれば保持。
    • 入力イベント(タッチ/マウス)の登録。
    • startOn に応じて初期状態を ON / OFF に設定し、残り時間をセット。
  • start
    • 同じノードに Button があり、autoRegisterButton が true の場合、clickEventspress() を呼ぶ EventHandler を自動追加。
  • update
    • ON 状態のときだけ _remainingTime を減算し、0 以下になったら OFF に戻す。
  • press()
    • 外部から「押された」ことを通知する公開メソッド。
    • Button の clickEvents や他スクリプトからも呼び出せる。
  • _handlePressed()
    • OFF → ON の切り替えとタイマー開始、ON 中の再押下時のタイマーリセット処理を行う。
  • _turnOnInternal / _turnOffInternal
    • isOn の更新、ビジュアル反映、EventHandler 呼び出しをまとめて行う。
    • isInitial フラグで「初期状態での ON/OFF 変更時にはイベントを飛ばさない」制御をしている。
  • _applyVisualState
    • enableVisualFeedback が true のとき、ON/OFF に応じて色とスケールを変更。
  • _registerInputEvents / _unregisterInputEvents
    • ノードへのタッチ/マウスイベント登録と解除を一元管理。
  • _registerToButtonClickEvents
    • Button.clickEvents に TimedButton.press を自動登録(重複登録防止付き)。

使用手順と動作確認

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

  1. エディタ上部メニューまたは Assets パネルで、Assets フォルダを選択します。
  2. Assets パネルで右クリック → Create → TypeScript を選択します。
  3. 新規スクリプトに TimedButton.ts という名前を付けます。
  4. 作成した TimedButton.ts をダブルクリックし、エディタ(VSCode 等)で開きます。
  5. 中身をすべて削除し、本記事の TimedButton コードを丸ごと貼り付けて保存します。

2. テスト用ノード(UI ボタン)の作成

まずは UI.Button として使う例です。

  1. Hierarchy パネルで右クリック → Create → UI → Canvas(まだなければ)を作成します。
  2. Canvas を選択した状態で右クリック → Create → UI → Button を選択し、ボタンノードを作成します。
    • デフォルトで Button, Sprite, Label などがアタッチされたノードができます。
  3. 作成された Button ノードを選択し、Inspector を確認します。

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

  1. Button ノードを選択したまま、Inspector 下部の Add Component ボタンをクリックします。
  2. Custom カテゴリ → TimedButton を選択します。
    • Custom が見当たらない場合は、検索窓に「TimedButton」と入力して検索してください。

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

Button ノードの Inspector に TimedButton コンポーネントが表示されていることを確認し、以下のように設定してみます。

  • Duration: 2.0
    • ボタンを押すと 2 秒間 ON になります。
  • Auto Register Button: true
    • Button コンポーネントのクリックで TimedButton が発火します。
  • Consume Pointer Event: false
    • 他の UI ハンドラにもイベントを流したい場合は false のまま。
  • Reset Timer On Press: true
    • ON 中に連打すると、そのたびに 2 秒にリセットされます。
  • Start On: false
    • ゲーム開始時は OFF にします。
  • Enable Visual Feedback: true
  • On Color: 緑系 (0,255,0)
  • Off Color: 白 (255,255,255)
  • On Scale: (1.1, 1.1, 1)
  • Off Scale: (1.0, 1.0, 1)

これで、ボタンを押すと少し大きくなって色が変わり、2 秒後に元に戻る挙動が確認できます。

5. ON/OFF 時に他のオブジェクトを制御する

TimedButton の EventHandler を使って、他ノードの動作を制御する例です。

  1. Hierarchy で右クリック → Create → 3D Object → Cube(または任意のノード)を作成し、名前を TargetNode に変更します。
  2. TargetNode に任意のスクリプト(例:TargetController.ts)を作成し、「有効/無効を切り替えるメソッド」を用意します。
    • 例:
      
      import { _decorator, Component, Node } from 'cc';
      const { ccclass } = _decorator;
      
      @ccclass('TargetController')
      export class TargetController extends Component {
          public setActiveFromButton(timedButton: any) {
              // 引数 timedButton は TimedButton インスタンス
              this.node.active = timedButton.isOn;
          }
      }
              
  3. TargetNode に TargetController をアタッチします。
  4. 再度 Button ノードを選択し、TimedButton コンポーネントの On Turn OnOn Turn Off を設定します:
    1. On Turn On の右側の「+」ボタンを押します。
    2. 追加された EventHandler の TargetTargetNode をドラッグ&ドロップします。
    3. ComponentTargetController を選択します。
    4. HandlersetActiveFromButton を選択します。
    5. On Turn Off にも同様に setActiveFromButton を登録します。

これで、TimedButton が ON/OFF になるたびに TargetController.setActiveFromButton が呼ばれ、TargetNode.activeisOn に同期します。

6. UI.Button なしで「当たり判定つきスイッチ」として使う

ゲーム内のオブジェクトをクリックしてスイッチにしたい場合の手順です。

  1. Hierarchy で右クリック → Create → 3D Object → Cube(または 2D ノード)を作成します。
  2. Cube に TimedButton をアタッチします(Button コンポーネントは不要)。
  3. Inspector で以下のように設定します:
    • Auto Register Button: false(Button がないので意味はありませんが、念のため)。
    • その他はお好みで。
  4. ゲーム実行中に Cube をクリック(またはタップ)すると、TimedButton が反応して ON→OFF が切り替わります。

この場合、物理コライダー は必須ではありません(Cocos の UI/ノードイベントはノードの矩形領域で処理されます)。ただし、Canvas UI でない 3D オブジェクトをクリック判定に使う場合は、カメラやイベントシステムの設定に注意してください。

7. 動作確認のポイント

  • ゲームを再生し、ボタンをクリックします。
  • Inspector で TimedButton の isOn が true に変わることを確認します。
  • 設定した Duration 秒後に、isOn が false に戻ることを確認します。
  • Reset Timer On Press = true の場合:
    • ON 中に何度か連打してみて、OFF に戻るタイミングが「最後に押した時刻から Duration 秒後」になっているか確認します。
  • Enable Visual Feedback = true の場合:
    • ON の間だけ色とスケールが変化し、OFF に戻ると元に戻るか確認します。

まとめ

この TimedButton コンポーネントは、

  • UI.Button にアタッチして「押してから◯秒だけ効果があるボタン」
  • ゲーム内オブジェクトにアタッチして「クリックすると一定時間だけ有効になるスイッチ」

といった用途を、他のスクリプトに一切依存せず、この 1 ファイルだけで実現できるように設計しました。

インスペクタで durationresetTimerOnPress、ビジュアルフィードバック、EventHandler などを調整することで、

  • 一時的なバフボタン
  • 時間制限付きギミックスイッチ
  • デバッグ用の一時トグル

など、さまざまなシーンで再利用できます。

プロジェクト内の任意のノードにアタッチして、「押したら◯秒だけ ON」という挙動を簡単に付与できるため、ボタンロジックを都度書き直す必要がなくなり、ゲーム開発の効率化 に大きく貢献します。
このコンポーネントをベースに、例えば「点滅エフェクト」「サウンド再生」「クールダウン 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をコピーしました!