【Cocos Creator】アタッチするだけ!TabSwitcher (タブ切り替え)の実装方法【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】TabSwitcher の実装:アタッチするだけで「装備/アイテム/設定」などのタブボタンに応じてパネル表示を切り替える汎用スクリプト

UI でよくある「タブ切り替え(装備・アイテム・設定…)」を、1 つのコンポーネントをアタッチするだけで実現できるようにします。
この TabSwitcher は、タブボタンと対応パネルをインスペクタで登録するだけで、クリック時に自動で表示パネルを切り替えてくれる汎用スクリプトです。
特定の GameManager やシングルトンに依存せず、どのシーン・どの UI でも再利用できます。


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

要件整理

  • 複数の「タブボタン」と、それぞれに対応する「パネル(コンテンツ)」を持つ。
  • タブボタンをクリック(またはタップ)すると、対応パネルだけを表示し、他のパネルは非表示にする。
  • 「選択中タブ」と「非選択タブ」で、見た目を簡易的に切り替えられるようにする(任意)。
  • 外部のカスタムスクリプトに依存せず、このコンポーネント単体で完結する。
  • 必要なノードやコンポーネント(Button, Sprite など)は @property でインスペクタから指定する。
  • 不足している標準コンポーネントは getComponent で取得を試み、なければエラーログを出す。

基本アーキテクチャ

  • TabSwitcher を 1 つの親ノード(例:タブ全体のルートノード)にアタッチする。
  • インスペクタで以下を設定する:
    • タブボタンと対応パネルのペア一覧
    • 初期状態で選択するタブのインデックス
    • 選択中タブに適用する色(任意)
    • 非選択タブに適用する色(任意)
  • ボタンには Button コンポーネントが付いている前提で、onLoad 時にクリックイベントを登録する。
  • クリックされたタブのインデックスをもとに、全パネルの表示/非表示とタブ見た目の更新を行う。

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

  • tabsTabConfig[]
    • 要素ごとに「タブボタンノード」と「対応パネルノード」をセットで登録する配列。
    • 例:0 番目 = 装備タブボタン & 装備パネル、1 番目 = アイテムタブボタン & アイテムパネル …
    • 各要素の構造:
      • buttonNode: Node … タブとしてクリックされるボタンのノード(Button コンポーネント必須)
      • panelNode: Node … 表示を切り替えるコンテンツパネルのノード
  • defaultTabIndex: number
    • ゲーム開始時(start)に選択状態にするタブのインデックス。
    • 0 以上 tabs.length - 1 以下の値。
    • 範囲外の場合は 0 にフォールバックする。
  • useTabColorHighlight: boolean
    • true のとき、選択中タブと非選択タブの色を自動で切り替える。
    • false のとき、色変更は行わない(見た目は自前で制御したい場合)。
  • selectedColor: Color
    • useTabColorHighlighttrue のときに使用。
    • 選択中タブの Sprite または Label に適用する色。
  • unselectedColor: Color
    • 非選択タブに適用する色。
  • logWarnings: boolean
    • true のとき、不足している Button / Sprite / Label などをコンソール警告として出力する。
    • 完成後にログを減らしたい場合は false にしてサイレントにできる。

TypeScriptコードの実装


import { _decorator, Component, Node, Button, Color, Sprite, Label, UIOpacity, warn, log } from 'cc';
const { ccclass, property } = _decorator;

/**
 * 個々のタブ設定(タブボタンと対応パネルのペア)
 */
@ccclass('TabConfig')
export class TabConfig {
    @property({
        tooltip: 'タブとしてクリックされるボタンのノード。\nButton コンポーネントが付いている必要があります。'
    })
    public buttonNode: Node | null = null;

    @property({
        tooltip: 'このタブが選択されたときに表示するパネル(コンテンツ)のルートノード。'
    })
    public panelNode: Node | null = null;
}

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

    @property({
        type: [TabConfig],
        tooltip: 'タブボタンと対応パネルのペアを配列で登録します。\n' +
                 '例: 0=装備タブ & 装備パネル, 1=アイテムタブ & アイテムパネル, ...'
    })
    public tabs: TabConfig[] = [];

    @property({
        tooltip: '起動時に選択状態にするタブのインデックス。\n' +
                 '範囲外の場合は 0 番が選択されます。'
    })
    public defaultTabIndex: number = 0;

    @property({
        tooltip: 'タブ選択状態で色を自動変更するかどうか。\n' +
                 'true の場合、タブの Sprite または Label の色を切り替えます。'
    })
    public useTabColorHighlight: boolean = true;

    @property({
        tooltip: '選択中タブの色(useTabColorHighlight が true のときに使用)。'
    })
    public selectedColor: Color = new Color(255, 255, 255, 255);

    @property({
        tooltip: '非選択タブの色(useTabColorHighlight が true のときに使用)。'
    })
    public unselectedColor: Color = new Color(180, 180, 180, 255);

    @property({
        tooltip: 'true のとき、不足している Button / Sprite / Label などを警告ログとして出力します。'
    })
    public logWarnings: boolean = true;

    /** 現在選択されているタブのインデックス。-1 の場合は未選択。 */
    private _currentIndex: number = -1;

    onLoad() {
        // タブ配列が空の場合は警告
        if (this.tabs.length === 0) {
            if (this.logWarnings) {
                warn('[TabSwitcher] tabs が設定されていません。インスペクタでタブを追加してください。', this.node);
            }
            return;
        }

        // 各タブボタンにクリックイベントを登録
        for (let i = 0; i < this.tabs.length; i++) {
            const config = this.tabs[i];
            if (!config) {
                continue;
            }

            const btnNode = config.buttonNode;
            if (!btnNode) {
                if (this.logWarnings) {
                    warn(`[TabSwitcher] tabs[${i}].buttonNode が設定されていません。`, this.node);
                }
                continue;
            }

            const buttonComp = btnNode.getComponent(Button);
            if (!buttonComp) {
                if (this.logWarnings) {
                    warn(`[TabSwitcher] ノード "${btnNode.name}" に Button コンポーネントがありません。タブとして動作しません。`, btnNode);
                }
                continue;
            }

            // 既存のクリックイベントに追加する形で登録
            buttonComp.node.on(Button.EventType.CLICK, () => {
                this.selectTab(i);
            }, this);
        }
    }

    start() {
        if (this.tabs.length === 0) {
            return;
        }

        // defaultTabIndex の範囲をクランプ
        let index = this.defaultTabIndex;
        if (index < 0 || index >= this.tabs.length) {
            if (this.logWarnings) {
                warn(`[TabSwitcher] defaultTabIndex (${index}) が範囲外です。0 にフォールバックします。`, this.node);
            }
            index = 0;
        }

        // 起動時にデフォルトタブを選択
        this.selectTab(index, true);
    }

    /**
     * 指定インデックスのタブを選択状態にし、対応パネルを表示する。
     * @param index 選択するタブのインデックス
     * @param force 同じインデックスでも強制的に更新する場合 true
     */
    public selectTab(index: number, force: boolean = false): void {
        if (this.tabs.length === 0) {
            return;
        }

        if (index < 0 || index >= this.tabs.length) {
            if (this.logWarnings) {
                warn(`[TabSwitcher] selectTab: index (${index}) が範囲外です。`, this.node);
            }
            return;
        }

        if (!force && index === this._currentIndex) {
            // すでに選択中のタブ
            return;
        }

        this._currentIndex = index;

        // 各タブとパネルの状態を更新
        for (let i = 0; i < this.tabs.length; i++) {
            const config = this.tabs[i];
            if (!config) {
                continue;
            }

            // パネル表示/非表示
            const panelNode = config.panelNode;
            if (panelNode) {
                const isActive = (i === index);
                panelNode.active = isActive;

                // UIOpacity があればフェード制御などにも応用可能(ここでは単純に 255 / 0)
                const uiOpacity = panelNode.getComponent(UIOpacity);
                if (uiOpacity) {
                    uiOpacity.opacity = isActive ? 255 : 0;
                }
            } else if (this.logWarnings) {
                warn(`[TabSwitcher] tabs[${i}].panelNode が設定されていません。`, this.node);
            }

            // タブの見た目更新(色変更)
            this.updateTabAppearance(i, i === index);
        }

        if (this.logWarnings) {
            log(`[TabSwitcher] タブ ${index} を選択しました。`, this.node);
        }
    }

    /**
     * タブの見た目(色など)を更新する。
     * @param tabIndex 対象タブのインデックス
     * @param selected 選択状態かどうか
     */
    private updateTabAppearance(tabIndex: number, selected: boolean): void {
        if (!this.useTabColorHighlight) {
            return;
        }

        const config = this.tabs[tabIndex];
        if (!config || !config.buttonNode) {
            return;
        }

        const node = config.buttonNode;
        const colorToApply = selected ? this.selectedColor : this.unselectedColor;

        // Sprite があれば Sprite に色を適用
        const sprite = node.getComponent(Sprite);
        if (sprite) {
            sprite.color = colorToApply;
        } else {
            // Label があれば Label に色を適用
            const label = node.getComponent(Label);
            if (label) {
                label.color = colorToApply;
            } else if (this.logWarnings) {
                // どちらもない場合は一度だけ注意喚起(タブ全般の見た目を自前で制御している場合は無視してよい)
                warn(`[TabSwitcher] タブノード "${node.name}" に Sprite / Label がありません。色変更は行われません。`, node);
            }
        }
    }

    /**
     * 現在選択されているタブのインデックスを返します。未選択の場合は -1。
     */
    public get currentIndex(): number {
        return this._currentIndex;
    }
}

コードのポイント解説

  • TabConfig クラス
    • 各タブの「ボタンノード」と「パネルノード」を 1 セットとして管理するための単純なデータクラス。
    • @property を付けることで、インスペクタで配列として編集可能になります。
  • tabs: TabConfig[]
    • タブのペアをまとめて管理する配列。インスペクタで「Size」を増やして、要素ごとにボタンとパネルを指定します。
  • onLoad()
    • 各タブの buttonNode から Button コンポーネントを取得し、クリックイベントを登録しています。
    • Button がない、buttonNode が未設定などの場合は warn で警告(logWarnings が true のとき)。
  • start()
    • defaultTabIndex を範囲チェックし、問題なければ selectTab() で初期タブを選択します。
  • selectTab(index, force)
    • タブ切り替えの中核ロジック。
    • 全タブをループし、選択中インデックスかどうかで:
      • 対応パネルの active を ON/OFF
      • UIOpacity があれば 255/0 をセット(任意の演出拡張のベース)
      • updateTabAppearance() でボタン側の色を更新
  • updateTabAppearance()
    • useTabColorHighlight が true の場合のみ実行。
    • タブボタンノードに Sprite があればその色を、なければ Label の色を変更。
    • どちらもない場合は警告を出し、色変更はスキップします。
  • 完全な独立性
    • 他のカスタムスクリプト(GameManager など)には一切依存していません。
    • 必要な情報はすべて @property 経由でインスペクタから設定する設計になっています。

使用手順と動作確認

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

  1. Assets パネルで右クリックします。
  2. Create > TypeScript を選択します。
  3. ファイル名を TabSwitcher.ts にします。
  4. 作成された TabSwitcher.ts をダブルクリックして開き、上記のコード全体を貼り付けて保存します。

2. テスト用 UI ノードの用意

ここでは例として、「装備」「アイテム」「設定」の 3 タブを作る手順を示します。

  1. Hierarchy パネルで右クリック → Create > Canvas(まだ Canvas がない場合)。
  2. Canvas の下に、右クリック → Create > UI > Node で空ノードを作成し、名前を TabsRoot などに変更します。
    • この TabsRootTabSwitcher コンポーネントをアタッチします。
  3. TabsRoot の子として、タブボタンを 3 つ作成します:
    1. TabsRoot を右クリック → Create > UI > Button でボタンを作成し、名前を Tab_Equip に変更。
    2. 同様に Tab_ItemTab_Settings を作成。
    3. 各ボタンの子に Label がある場合は、テキストを「装備」「アイテム」「設定」などに変更します。
  4. Canvas の下(または TabsRoot の下)に、パネル用ノードを 3 つ作成します:
    1. Canvas を右クリック → Create > UI > Node でノードを作成し、名前を Panel_Equip に変更。
    2. 同様に Panel_ItemPanel_Settings を作成。
    3. それぞれのパネルの中に、適当な Label や Sprite を置いて内容を区別できるようにしておきます。
      • 例:Panel_Equip 内の Label に「装備パネル」、Panel_Item に「アイテムパネル」など。

3. TabSwitcher コンポーネントをアタッチ

  1. Hierarchy で TabsRoot ノードを選択します。
  2. Inspector パネルの下部で Add ComponentCustomTabSwitcher を選択して追加します。

4. インスペクタでプロパティを設定

  1. tabs 配列の設定
    1. Tabs プロパティの「Size」を 3 に設定します(装備/アイテム/設定の 3 タブ)。
    2. 要素 0(Element 0):
      • Button NodeTab_Equip をドラッグ&ドロップ。
      • Panel NodePanel_Equip をドラッグ&ドロップ。
    3. 要素 1(Element 1):
      • Button NodeTab_Item を設定。
      • Panel NodePanel_Item を設定。
    4. 要素 2(Element 2):
      • Button NodeTab_Settings を設定。
      • Panel NodePanel_Settings を設定。
  2. defaultTabIndex の設定
    • Default Tab Index0 に設定すると、ゲーム開始時に「装備」タブが選択されます。
    • 「アイテム」から始めたい場合は 1、「設定」からなら 2 にします。
  3. 色ハイライトの設定(任意)
    • Use Tab Color Highlighttrue にします。
    • Selected Color(255, 255, 255, 255)(白)、
    • Unselected Color(160, 160, 160, 255) など、少し暗い色にすると分かりやすいです。
    • ボタンノードに SpriteLabel が付いていることを確認してください。
      • なければ、色変更はスキップされ、警告ログが出ます(logWarnings が true のとき)。
  4. ログ出力の設定
    • 開発中は Log Warningstrue にしておくと、不足設定があればコンソールに警告が出ます。
    • 完成後ログを抑えたい場合は false にしても構いません。

5. 再生して動作確認

  1. エディタ右上の「▶」ボタンを押してゲームを再生します。
  2. 初期状態で defaultTabIndex で指定したタブのパネルのみが表示されていることを確認します。
    • 例:defaultTabIndex = 0 なら Panel_Equip だけが active。
  3. ゲーム画面上で「アイテム」「設定」などのタブボタンをクリックしてみます。
    • クリックしたタブに対応するパネルだけが表示され、他のパネルは非表示になること。
    • Use Tab Color Highlight = true の場合、選択中タブの色が Selected Color に変わり、他は Unselected Color になっていること。
  4. Console パネルで、設定ミスがあれば [TabSwitcher] から始まる警告ログが出ていないか確認します。

まとめ

TabSwitcher コンポーネントを使うことで、

  • 「装備」「アイテム」「設定」などの典型的なタブ UI を、ボタンとパネルをインスペクタで登録するだけで実装できるようになりました。
  • 外部の GameManager やシングルトンに依存しないため、どのシーン・どのプロジェクトにも簡単に持ち込めます。
  • 色ハイライトやログ出力もオプションとして用意しているので、UI デザインや開発フェーズに合わせて柔軟に調整可能です。

応用として:

  • 各パネルにさらにスクロールビューやリストを組み合わせれば、装備一覧・アイテム一覧・設定画面などがすぐに構築できます。
  • UIOpacity を利用してフェード演出を追加したり、タブ切り替え時にサウンドを鳴らす処理を selectTab() 内に拡張することも容易です。

このように、1 つの独立した汎用コンポーネントとして設計しておくことで、UI タブ切り替えというよくある要素を何度でも再利用でき、ゲーム開発の効率が大きく向上します。
まずはシンプルな 2〜3 タブ構成で試し、慣れてきたら複数画面や別シーンでも再利用してみてください。

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をコピーしました!