【Cocos Creator】アタッチするだけ!LeverSwitch (レバー)の実装方法【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】LeverSwitch の実装:アタッチするだけで「クリック/タップでON/OFFが切り替わるレバー」を実現する汎用スクリプト

このガイドでは、任意のノードにアタッチするだけで「レバーを左右に倒して ON/OFF を切り替える」挙動を実現できる汎用コンポーネント LeverSwitch を実装します。
マウスクリックやタップでインタラクトでき、ON/OFF 状態に応じて回転角度や色・ラベル表示を変えられるので、UI のスイッチやゲーム内のレバーギミックにそのまま使えます。


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

要件整理

  • ノードにアタッチすると「レバー」として振る舞う。
  • クリック / タップ(Pointer)でインタラクトすると、ON ⇔ OFF をトグル切り替え。
  • ON/OFF に応じてノードの回転角度を変える(例:左に倒れて OFF、右に倒れて ON)。
  • ON/OFF に応じて色やラベル文字を変えられる(任意)。
  • ON/OFF 変更時に任意のノードに対して EventHandler を発火できる。
  • 他のカスタムスクリプトへの依存は一切なし。全てインスペクタ設定で完結。
  • 防御的実装:必要な標準コンポーネント(UITransform など)は存在チェックし、不足時はログで警告。

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

LeverSwitch が持つ主なプロパティと役割は以下の通りです。

  • initialOn (boolean)
    • 初期状態が ON かどうか。
    • true: シーン開始時に ON 状態、false: OFF 状態で開始。
  • interactable (boolean)
    • プレイヤー操作で切り替え可能かどうか。
    • false にすると、スクリプトからの制御専用レバーとして使える。
  • offAngle (number)
    • OFF 状態のときの Z 回転角度(度数法)。
    • 例: -30 で左に 30 度倒れた見た目。
  • onAngle (number)
    • ON 状態のときの Z 回転角度(度数法)。
    • 例: 30 で右に 30 度倒れた見た目。
  • useSmoothAnimation (boolean)
    • ON/OFF 切り替え時に角度をスムーズに補間するか、瞬時に切り替えるか。
  • animationDuration (number)
    • スムーズアニメーションを行うときの所要時間(秒)。
    • 例: 0.15 〜 0.3 くらいがレバーらしい。
  • clickSound (AudioClip | null)
    • ON/OFF を切り替えたときに再生する効果音。
    • 未設定の場合は音は鳴らない。
  • targetGraphicNode (Node | null)
    • ON/OFF に応じて色を変えたい Node(Sprite や Label など)。
    • 未設定なら色変更は行わない。
  • offColor (Color)
    • OFF 状態のときの色。
  • onColor (Color)
    • ON 状態のときの色。
  • labelNode (Node | null)
    • ON/OFF 状態を文字で表示したい Label ノード。
    • 未設定ならテキスト変更は行わない。
  • offText (string)
    • OFF 状態のときに Label に表示する文字列。
    • 例: “OFF”、”0” など。
  • onText (string)
    • ON 状態のときに Label に表示する文字列。
    • 例: “ON”、”1” など。
  • playClickSoundOnEveryToggle (boolean)
    • ON → OFF / OFF → ON のどちらでも毎回クリック音を鳴らすかどうか。
  • onTurnedOn (EventHandler[])
  • onTurnedOff (EventHandler[])
  • onToggled (EventHandler[])

これらは Cocos の EventHandler を使い、インスペクタから任意のノード・任意のコンポーネントのメソッドを呼び出せるようにします。
これにより、他のスクリプトに依存せず「レバーが ON になったらドアを開ける」「OFF になったらライトを消す」といった連携が可能です。


TypeScriptコードの実装


import { _decorator, Component, Node, EventTouch, EventMouse, input, Input, Vec3, Color, Sprite, Label, AudioSource, AudioClip, UITransform, EventHandler, tween } from 'cc';
const { ccclass, property } = _decorator;

/**
 * LeverSwitch
 * 任意のノードを「クリック/タップでON/OFFが切り替わるレバー」にする汎用コンポーネント
 */
@ccclass('LeverSwitch')
export class LeverSwitch extends Component {

    @property({
        tooltip: 'シーン開始時の初期状態がONかどうか。\nON: onAngle側に倒れた状態で開始 / OFF: offAngle側で開始'
    })
    public initialOn: boolean = false;

    @property({
        tooltip: 'プレイヤーのクリック/タップでレバーを操作できるかどうか。\nfalseにするとスクリプトからのみ制御されます。'
    })
    public interactable: boolean = true;

    @property({
        tooltip: 'OFF状態のときのZ回転角度(度数法)。\n例: -30 で左に30度倒れた見た目になります。'
    })
    public offAngle: number = -30;

    @property({
        tooltip: 'ON状態のときのZ回転角度(度数法)。\n例: 30 で右に30度倒れた見た目になります。'
    })
    public onAngle: number = 30;

    @property({
        tooltip: 'ON/OFF切り替え時に角度をスムーズに補間するかどうか。\nfalseの場合は瞬時に角度が切り替わります。'
    })
    public useSmoothAnimation: boolean = true;

    @property({
        tooltip: 'スムーズアニメーションの所要時間(秒)。\nuseSmoothAnimationがtrueのときのみ有効です。'
    })
    public animationDuration: number = 0.15;

    @property({
        type: AudioClip,
        tooltip: 'ON/OFF切り替え時に再生するクリック音。\n未設定の場合は音は鳴りません。'
    })
    public clickSound: AudioClip | null = null;

    @property({
        tooltip: 'ON/OFF切り替えのたびに毎回クリック音を再生するかどうか。'
    })
    public playClickSoundOnEveryToggle: boolean = true;

    @property({
        type: Node,
        tooltip: 'ON/OFFに応じて色を変えたいノード(SpriteやLabelなど)。\n未設定の場合は色の変更は行いません。'
    })
    public targetGraphicNode: Node | null = null;

    @property({
        tooltip: 'OFF状態のときに適用する色。\ntargetGraphicNodeにSpriteまたはLabelがある場合のみ有効です。'
    })
    public offColor: Color = new Color(150, 150, 150, 255);

    @property({
        tooltip: 'ON状態のときに適用する色。\ntargetGraphicNodeにSpriteまたはLabelがある場合のみ有効です。'
    })
    public onColor: Color = new Color(0, 255, 0, 255);

    @property({
        type: Node,
        tooltip: 'ON/OFF状態を文字で表示したいLabelノード。\n未設定の場合はテキストの変更は行いません。'
    })
    public labelNode: Node | null = null;

    @property({
        tooltip: 'OFF状態のときに表示するテキスト。'
    })
    public offText: string = 'OFF';

    @property({
        tooltip: 'ON状態のときに表示するテキスト。'
    })
    public onText: string = 'ON';

    @property({
        type: [EventHandler],
        tooltip: 'レバーがONになったときに呼び出されるイベントハンドラ一覧。'
    })
    public onTurnedOn: EventHandler[] = [];

    @property({
        type: [EventHandler],
        tooltip: 'レバーがOFFになったときに呼び出されるイベントハンドラ一覧。'
    })
    public onTurnedOff: EventHandler[] = [];

    @property({
        type: [EventHandler],
        tooltip: 'レバーの状態がトグル(ON⇔OFF)されたときに毎回呼び出されるイベントハンドラ一覧。'
    })
    public onToggled: EventHandler[] = [];

    // 現在のON/OFF状態
    private _isOn: boolean = false;

    // アニメーション中かどうか
    private _isAnimating: boolean = false;

    // AudioSource(存在すれば使用)
    private _audioSource: AudioSource | null = null;

    // クリック判定に使うUITransform
    private _uiTransform: UITransform | null = null;

    onLoad() {
        // AudioSourceを取得(あれば使用)
        this._audioSource = this.getComponent(AudioSource);
        if (!this._audioSource && this.clickSound) {
            console.warn('[LeverSwitch] AudioSourceがアタッチされていませんが、clickSoundが設定されています。効果音を鳴らしたい場合は同じノードにAudioSourceコンポーネントを追加してください。');
        }

        // UITransformを取得して警告(クリック判定に推奨)
        this._uiTransform = this.getComponent(UITransform);
        if (!this._uiTransform) {
            console.warn('[LeverSwitch] UITransformコンポーネントが見つかりません。UI要素としてクリック領域を持たせたい場合は、このノードにUITransformを追加してください。');
        }

        // 初期状態を設定
        this._isOn = this.initialOn;
        this.applyVisualState(false);
    }

    start() {
        // ポインタイベントを登録(Node自体のイベントを使用)
        this.node.on(Node.EventType.TOUCH_END, this.onPointerUp, this);
        this.node.on(Node.EventType.MOUSE_UP, this.onPointerUp, this);
    }

    onDestroy() {
        // イベント解除
        this.node.off(Node.EventType.TOUCH_END, this.onPointerUp, this);
        this.node.off(Node.EventType.MOUSE_UP, this.onPointerUp, this);
    }

    /**
     * 現在の状態がONかどうかを返す
     */
    public get isOn(): boolean {
        return this._isOn;
    }

    /**
     * 外部から状態を設定するためのメソッド(アニメーションの有無を指定可能)
     */
    public setState(isOn: boolean, animate: boolean = true) {
        if (this._isOn === isOn) {
            return;
        }
        this._isOn = isOn;
        this.applyVisualState(animate);
        this.invokeEvents();
    }

    /**
     * 外部からトグルさせるためのメソッド
     */
    public toggle(animate: boolean = true) {
        this.setState(!this._isOn, animate);
    }

    /**
     * ポインタが離されたとき(クリック/タップ)に呼ばれる
     */
    private onPointerUp(event: EventTouch | EventMouse) {
        if (!this.interactable) {
            return;
        }

        // すでにアニメーション中なら無視(連打防止)
        if (this._isAnimating) {
            return;
        }

        // クリック位置がノードの領域内かを簡易チェック(UITransformがある場合)
        if (this._uiTransform && event instanceof EventTouch) {
            const location = event.getUILocation();
            const localPos = this._uiTransform.convertToNodeSpaceAR(new Vec3(location.x, location.y, 0));
            const rect = this._uiTransform.contentSize;
            const halfW = rect.width / 2;
            const halfH = rect.height / 2;
            if (localPos.x < -halfW || localPos.x > halfW || localPos.y < -halfH || localPos.y > halfH) {
                // 領域外なら無視
                return;
            }
        }

        // 状態をトグル
        this.toggle(true);
    }

    /**
     * 現在の_isOn状態に応じて見た目を適用する
     */
    private applyVisualState(animate: boolean) {
        const targetAngle = this._isOn ? this.onAngle : this.offAngle;

        // 回転の適用
        if (this.useSmoothAnimation && animate && this.animationDuration > 0) {
            this._isAnimating = true;
            const currentEuler = this.node.eulerAngles;
            const targetEuler = new Vec3(currentEuler.x, currentEuler.y, targetAngle);

            tween(this.node)
                .to(this.animationDuration, { eulerAngles: targetEuler })
                .call(() => {
                    this._isAnimating = false;
                })
                .start();
        } else {
            const currentEuler = this.node.eulerAngles;
            this.node.setRotationFromEuler(currentEuler.x, currentEuler.y, targetAngle);
        }

        // 色の適用
        if (this.targetGraphicNode) {
            const sprite = this.targetGraphicNode.getComponent(Sprite);
            const label = this.targetGraphicNode.getComponent(Label);

            if (!sprite && !label) {
                console.warn('[LeverSwitch] targetGraphicNodeにSpriteまたはLabelコンポーネントが見つかりません。色変更は行われません。');
            }

            const colorToApply = this._isOn ? this.onColor : this.offColor;

            if (sprite) {
                sprite.color = colorToApply;
            }
            if (label) {
                label.color = colorToApply;
            }
        }

        // テキストの適用
        if (this.labelNode) {
            const label = this.labelNode.getComponent(Label);
            if (!label) {
                console.warn('[LeverSwitch] labelNodeにLabelコンポーネントが見つかりません。テキスト変更は行われません。');
            } else {
                label.string = this._isOn ? this.onText : this.offText;
            }
        }

        // 効果音の再生
        if (this.clickSound && this.playClickSoundOnEveryToggle) {
            this.playClick();
        }
    }

    /**
     * 効果音を再生する
     */
    private playClick() {
        if (!this.clickSound) {
            return;
        }

        if (this._audioSource) {
            this._audioSource.playOneShot(this.clickSound, 1.0);
        } else {
            console.warn('[LeverSwitch] clickSoundは設定されていますが、このノードにAudioSourceがありません。効果音を鳴らすにはAudioSourceを追加してください。');
        }
    }

    /**
     * 状態変更時にイベントを呼び出す
     */
    private invokeEvents() {
        // ON/OFF共通
        if (this.onToggled.length > 0) {
            EventHandler.emitEvents(this.onToggled, this);
        }

        if (this._isOn) {
            if (this.onTurnedOn.length > 0) {
                EventHandler.emitEvents(this.onTurnedOn, this);
            }
        } else {
            if (this.onTurnedOff.length > 0) {
                EventHandler.emitEvents(this.onTurnedOff, this);
            }
        }
    }
}

コードのポイント解説

  • onLoad
    • AudioSource と UITransform を getComponent で取得し、なければ警告ログ。
    • initialOn に基づいて内部状態 _isOn を決め、applyVisualState(false) で初期見た目を設定。
  • start
    • Node.EventType.TOUCH_ENDNode.EventType.MOUSE_UP を登録し、クリック/タップで onPointerUp が呼ばれるようにする。
  • onPointerUp
    • interactable が false なら無視。
    • アニメーション中(_isAnimating)は連打による多重トグルを防止。
    • UITransform がある場合は、タップ位置がノード矩形内かを簡易チェック。
    • 条件を満たせば toggle(true) で状態を反転。
  • applyVisualState
    • ON/OFF に応じて offAngle/onAngle からターゲット角度を決定。
    • useSmoothAnimationanimationDuration に応じて、tween でスムーズ回転 or 即時回転。
    • targetGraphicNode に Sprite / Label があれば、offColor/onColor を適用。
    • labelNode に Label があれば、offText/onText を表示。
    • clickSound が設定されていれば、AudioSource から playOneShot で再生。
  • invokeEvents
    • 状態変更ごとに onToggled を実行。
    • ON のとき onTurnedOn、OFF のとき onTurnedOffEventHandler.emitEvents で呼び出し。
    • 引数には this(LeverSwitch インスタンス)を渡すので、受け取り側で isOn 参照も可能。
  • 公開API
    • get isOn(): boolean で現在の状態を参照。
    • setState(isOn: boolean, animate = true) で外部から明示的に ON/OFF を切り替え。
    • toggle(animate = true) で外部からトグル制御。

使用手順と動作確認

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

  1. エディタ下部の Assets パネルで、スクリプトを置きたいフォルダ(例: assets/scripts)を選択します。
  2. そのフォルダを右クリック → CreateTypeScript を選択します。
  3. 新規スクリプトに LeverSwitch.ts という名前を付けます。
  4. ダブルクリックしてエディタ(VS Code など)で開き、本文をすべて削除して上記の TypeScript コードを貼り付け、保存します。

2. テスト用レバーノードの作成

まずはシンプルに「レバー画像をクリックすると左右に倒れる」挙動だけ確認します。

  1. Hierarchy パネルで、Canvas 配下にテスト用ノードを作成します。
    • 右クリック → CreateUISprite
    • 作成された Sprite ノードの名前を TestLever などに変更します。
  2. InspectorTestLever を選択し、Sprite の SpriteFrame にレバーっぽい画像を設定します(なければ四角でも可)。
  3. 同じく Inspector の Add Component ボタンをクリック → CustomLeverSwitch を選択してアタッチします。

3. LeverSwitch の基本設定

TestLever ノードを選択した状態で、Inspector の LeverSwitch コンポーネントを次のように設定してみましょう。

  • Initial On: false(初期は OFF)
  • Interactable: true
  • Off Angle: -30
  • On Angle: 30
  • Use Smooth Animation: true
  • Animation Duration: 0.2
  • Click Sound: (まだ未設定でOK)
  • Play Click Sound On Every Toggle: true
  • Target Graphic Node: (後で設定)
  • Label Node: (後で設定)

この状態でプレビューすると、レバー画像をクリック/タップするたびに、左(OFF)と右(ON)にスムーズに倒れるはずです。

4. 色とラベル表示を連動させる

次に、ON/OFF に応じて色と文字を変えてみます。

  1. 色を変える対象ノードの設定
    1. TestLever ノード自体の色を変えたい場合は、そのまま Target Graphic NodeTestLever をドラッグ&ドロップします。
    2. 別のノードの色を変えたい場合は、Canvas 配下などに任意の Sprite / Label ノードを作成し、そのノードを Target Graphic Node に設定してください。
    3. Off Color: (150,150,150,255) のままでOK(グレー)
    4. On Color: (0,255,0,255) にして緑色に。
  2. ラベル表示の設定
    1. Hierarchy で Canvas を右クリック → Create → UI → Label を選択し、LeverLabel という名前にします。
    2. LeverLabelTestLever の子にして、レバーの近くに配置します。
    3. LeverLabel を選択し、Inspector で Label コンポーネントに任意のフォントサイズや位置を設定します。
    4. TestLever を選択し、LeverSwitch の Label NodeLeverLabel をドラッグ&ドロップします。
    5. Off Text: “OFF”
    6. On Text: “ON”

再度プレビューすると、レバーをクリックするたびにレバーの色がグレー/緑に変わり、ラベルが “OFF”/”ON” と切り替わるのが確認できます。

5. 効果音を付ける

  1. Assets パネルで効果音の AudioClip(例: click.wav)をインポートします。
  2. TestLever ノードを選択し、Inspector の Add ComponentAudioAudioSource を追加します。
  3. AudioSource の Clip は空のままで構いません(playOneShot を使うため)。
  4. LeverSwitch の Click Sound に、インポートした AudioClip をドラッグ&ドロップします。
  5. Play Click Sound On Every Toggle が true になっていることを確認します。

プレビューすると、レバーを ON/OFF するたびにクリック音が鳴ります。
もし AudioSource を付け忘れると、コンソールに「AudioSourceがありません」と警告が出るので、追加してください。

6. イベントハンドラで他ノードと連携する

最後に、レバーの ON/OFF に応じて別ノードの挙動を変える例を紹介します。ここでは「レバーが ON のときにランプを表示、OFF のときに非表示にする」例です。

  1. Hierarchy で Canvas 配下に UI → Sprite を作成し、Lamp という名前にします。
  2. Lamp にランプ画像(または適当な画像)を設定し、初期状態では Enabled を ON にしておきます。
  3. Lamp に簡単なスクリプトを付けてもよいですが、既にある Active プロパティを直接 EventHandler で制御できます。
  4. TestLever を選択し、LeverSwitch コンポーネントの On Turned On の横にある + ボタンをクリックします。
  5. 追加された EventHandler の TargetLamp ノードをドラッグ&ドロップします。
  6. Component ドロップダウンから cc.Node を選択します。
  7. Handler ドロップダウンから setActive を選択します。
  8. CustomEventDatatrue と入力します(文字列として渡されますが、setActive は “true” を truthy として扱います)。
  9. 同様に、On Turned Off にも EventHandler を追加し、Target: LampComponent: cc.NodeHandler: setActiveCustomEventData: false と設定します。

これで、レバーが ON になるとランプが表示され、OFF になるとランプが非表示になります。
より複雑な処理をしたい場合は、任意のコンポーネントの任意のメソッドを Handler に指定し、そのメソッド側で (event: Event, customData: string) などを受け取るように実装すれば対応できます。


まとめ

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

  • ノードにアタッチするだけで「クリック/タップで ON/OFF が切り替わるレバー」として機能する。
  • 角度・アニメーション・色・ラベル・効果音をすべてインスペクタから調整できる。
  • EventHandler を使って ON/OFF 時の任意の処理を外部スクリプトに通知できる。
  • 他のカスタムスクリプトに依存せず、このスクリプト単体で完結する。

UI スイッチ、ギミック用レバー、オプション画面のトグルなど、多くの場面で使い回せる汎用コンポーネントとしてプロジェクトに組み込んでおくと、
「レバーっぽい挙動を毎回実装し直す」コストを大きく削減できます。

必要に応じて、

  • ドラッグで倒した方向に応じて ON/OFF を決める
  • キーボードやゲームパッド入力でもトグルする
  • マルチステート(3 段階以上)のレバーに拡張する

といった拡張も同じ設計パターンで実装できます。
まずは今回の LeverSwitch をベースに、あなたのゲームの 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をコピーしました!