Unityを触り始めた頃にやりがちなのが、UIの処理を全部ひとつの Update() に書いてしまうパターンです。
「装備ボタンが押されたら…」「アイテムボタンが押されたら…」「設定ボタンが押されたら…」と条件分岐だらけになり、気づけば巨大なGodクラス化してしまいます。
この状態だと、
- ボタンやパネルが1つ増えるたびにスクリプトを開いて修正
- どの処理がどのUIに対応しているのか追いにくい
- プレハブ化しづらく、シーンごとにコピペ地獄
といった問題がどんどん積み上がっていきます。
そこでこの記事では、「装備」「アイテム」「設定」などのボタンで表示パネルを切り替える処理を、ひとつの責務に絞ったコンポーネント 「TabSwitcher」 に切り出して実装してみます。
タブ切り替えというよくあるUIパターンをコンポーネント化しておけば、プレハブとしてどのシーンでも再利用できて便利ですね。
【Unity】ボタン連打でも迷子にならないタブUI!「TabSwitcher」コンポーネント
以下が、Unity UI(UGUI)のボタンでパネルを切り替えるための完全な実装例です。
Unity6 / Unity 2022 以降で動く、標準機能のみを使ったコードになっています。
using System;
using UnityEngine;
using UnityEngine.UI;
namespace Sample.UI
{
/// <summary>
/// 複数のタブボタンと対応するパネルを管理し、
/// ボタンが押されたときに表示パネルを切り替えるコンポーネント。
///
/// ・「装備」「アイテム」「設定」などの典型的なタブ切り替えを想定
/// ・タブ情報はインスペクターから配列で設定
/// ・1つだけアクティブにし、それ以外は非表示にする
///
/// 責務を「タブ切り替えの制御」に限定し、UIの中身の処理は別コンポーネントに任せます。
/// </summary>
public class TabSwitcher : MonoBehaviour
{
[Serializable]
private class TabEntry
{
[Tooltip("このタブを押したときにアクティブにするボタン")]
[SerializeField] private Button tabButton;
[Tooltip("このタブに対応するコンテンツパネル(GameObject)")]
[SerializeField] private GameObject contentPanel;
[Tooltip("最初に選択されるタブかどうか")]
[SerializeField] private bool isDefault;
// プロパティ経由で外部から読み取りだけできるようにする
public Button TabButton => tabButton;
public GameObject ContentPanel => contentPanel;
public bool IsDefault => isDefault;
}
[Header("タブ設定")]
[Tooltip("タブボタンと対応するパネルの組み合わせを登録します")]
[SerializeField] private TabEntry[] _tabs = Array.Empty<TabEntry>();
[Header("オプション")]
[Tooltip("起動時に自動でデフォルトタブを選択するか")]
[SerializeField] private bool _selectOnStart = true;
// 現在アクティブなタブのインデックス(-1 は未選択)
private int _currentIndex = -1;
private void Awake()
{
// ボタンにリスナーを登録
SetupButtonListeners();
}
private void Start()
{
if (_selectOnStart)
{
SelectDefaultTab();
}
}
/// <summary>
/// 各タブボタンにクリックイベントを登録する。
/// </summary>
private void SetupButtonListeners()
{
// 念のため、重複登録を避けるために全てのリスナーをクリアしてから登録
for (int i = 0; i < _tabs.Length; i++)
{
var tab = _tabs[i];
if (tab == null || tab.TabButton == null)
{
continue;
}
// 既存リスナーをクリア(プレハブ再利用時などの二重登録防止)
tab.TabButton.onClick.RemoveListener(() => OnTabButtonClicked(i));
// ループ変数をローカルにキャプチャして正しいインデックスを渡す
int capturedIndex = i;
tab.TabButton.onClick.AddListener(() => OnTabButtonClicked(capturedIndex));
}
}
/// <summary>
/// デフォルトタブを選択する。
/// 1つもフラグが立っていない場合は、先頭のタブを選択。
/// </summary>
private void SelectDefaultTab()
{
if (_tabs == null || _tabs.Length == 0)
{
Debug.LogWarning("[TabSwitcher] タブが1つも設定されていません。");
return;
}
int defaultIndex = -1;
// isDefault が true のものを探す
for (int i = 0; i < _tabs.Length; i++)
{
if (_tabs[i] != null && _tabs[i].IsDefault)
{
defaultIndex = i;
break;
}
}
// 見つからなければ 0 番を採用
if (defaultIndex < 0)
{
defaultIndex = 0;
}
SwitchTo(defaultIndex);
}
/// <summary>
/// ボタンがクリックされたときに呼ばれるコールバック。
/// </summary>
/// <param name="index">クリックされたタブのインデックス</param>
private void OnTabButtonClicked(int index)
{
SwitchTo(index);
}
/// <summary>
/// 指定インデックスのタブをアクティブにし、それ以外を非アクティブにする。
/// </summary>
/// <param name="index">切り替え先のタブインデックス</param>
public void SwitchTo(int index)
{
if (_tabs == null || _tabs.Length == 0)
{
return;
}
if (index < 0 || index >= _tabs.Length)
{
Debug.LogWarning($"[TabSwitcher] 不正なタブインデックスが指定されました: {index}");
return;
}
// すでに選択中なら何もしない(不要な処理を避ける)
if (_currentIndex == index)
{
return;
}
_currentIndex = index;
for (int i = 0; i < _tabs.Length; i++)
{
var tab = _tabs[i];
if (tab == null || tab.ContentPanel == null)
{
continue;
}
bool isActive = (i == index);
tab.ContentPanel.SetActive(isActive);
// ボタンの見た目を変えたい場合はここで色や選択状態を更新するとよい
// 例:
// var colors = tab.TabButton.colors;
// colors.normalColor = isActive ? Color.white : Color.gray;
// tab.TabButton.colors = colors;
}
}
/// <summary>
/// 現在アクティブなタブのインデックスを取得する。
/// UIの状態に応じて別処理をしたいときに使えます。
/// </summary>
public int GetCurrentIndex()
{
return _currentIndex;
}
}
}
使い方の手順
-
UI を用意する
- Canvas の下に、タブボタン用の GameObject(例:
TabButtons)を作成し、Button を3つ配置します。
例:Button_Equipment(装備),Button_Items(アイテム),Button_Settings(設定) - 同じ Canvas の下に、タブごとのパネルを3つ用意します。
例:Panel_Equipment,Panel_Items,Panel_Settings - 各パネルの中身(装備リスト、アイテム一覧、設定スライダーなど)は自由にレイアウトしてOKです。
- Canvas の下に、タブボタン用の GameObject(例:
-
TabSwitcher をアタッチするオブジェクトを作る
- 空の GameObject を作成し、名前を
TabSwitcherRootなどにします。 TabSwitcherコンポーネントを追加します。
- 空の GameObject を作成し、名前を
-
インスペクターでタブを設定する
TabSwitcherの_tabs配列のサイズを、タブの数(ここでは3)に設定します。- 要素ごとに以下を割り当てます:
- Element 0: 装備タブ
Tab ButtonにButton_Equipmentをドラッグ&ドロップContent PanelにPanel_Equipmentをドラッグ&ドロップIs Defaultにチェック(最初に表示したいタブ)
- Element 1: アイテムタブ
Tab ButtonにButton_ItemsContent PanelにPanel_ItemsIs Defaultはオフ
- Element 2: 設定タブ
Tab ButtonにButton_SettingsContent PanelにPanel_SettingsIs Defaultはオフ
- Element 0: 装備タブ
Select On Startにチェックを入れておくと、ゲーム開始時に自動でデフォルトタブが表示されます。
-
プレハブ化して再利用する
Canvasごと、あるいはTabSwitcherRootとその子階層をプレハブ化しておくと、別シーンでも同じタブUIをそのまま使い回せます。- 例えば「クエスト画面」「ショップ画面」などでも、タブの中身だけ差し替えて使えるのでレベルデザインがかなり楽になります。
メリットと応用
この TabSwitcher コンポーネントを使うことで、
- 「どのボタンでどのパネルを表示するか」というロジックを1か所に集約できる
- UIの中身(装備リストの更新、アイテムのソート、設定の保存など)は別コンポーネントに分割できる
- タブ構成をインスペクターから差し替えるだけで、別画面でも同じ仕組みを再利用できる
- ボタンやパネルが増えても、
_tabsに1行追加するだけで済む
といったメリットがあります。
プレハブ化しておけば、UIデザイナーやレベルデザイナーがインスペクター上でタブを増減できるので、スクリプトを頻繁に触らなくても画面構成を変えられます。
応用として、タブ切り替え時にアニメーションやサウンドを鳴らしたい場合は、SwitchTo の中を少し拡張するだけで対応できます。
例えば「タブ切り替え時にフェードインする」簡単な改造案はこんな感じです。
/// <summary>
/// タブが切り替わったときに呼び出して、パネルをふわっとフェードインさせる例。
/// (本格的なアニメーションは Animator や DOTween などを使うとよいです)
/// </summary>
private void PlaySimpleFadeIn(GameObject panel)
{
// ここではシンプルに、アクティブ化した直後に
// CanvasGroup の alpha を 0 → 1 に補間するような処理を想定しています。
var canvasGroup = panel.GetComponent<CanvasGroup>();
if (canvasGroup == null)
{
canvasGroup = panel.AddComponent<CanvasGroup>();
}
canvasGroup.alpha = 0f;
panel.SetActive(true);
// 実際の補間処理は別コンポーネントに切り出すのがおすすめですが、
// 簡易的にコルーチンで実装してもOKです。
StartCoroutine(FadeInCoroutine(canvasGroup, 0.2f));
}
このように、タブ切り替え自体の責務は TabSwitcher に任せつつ、アニメーションやサウンドなどの装飾は別メソッド・別コンポーネントに分けていくと、UIまわりのコードがどんどん整理されていきます。
