Unityを触り始めた頃にありがちなのが、Update() に「入力処理」「UI処理」「シーン遷移」「サウンド再生」など、ありとあらゆる処理を書き込んでしまうパターンです。
動き始めるまでは良いのですが、機能が増えるほど 1つのスクリプトが肥大化 して、修正や再利用がつらくなっていきます。
この記事では、「ボタンが押されたらシーンを切り替える」 というよくある処理を、
小さな責務に分割したコンポーネント 「SceneChanger」 として実装していきます。
ボタンのクリック処理はボタンに任せ、シーン遷移は SceneChanger に任せる。
そんなふうにコンポーネントを分けることで、UIボタンをプレハブ化してどこでも再利用しやすい設計にしていきましょう。
【Unity】ワンアクションでシーン切り替え!「SceneChanger」コンポーネント
今回作る SceneChanger コンポーネントの役割は、とてもシンプルです。
- 親オブジェクトにある
Buttonが押されたら - 指定したシーンパスを
SceneManagerでロードする
「ボタンが押されたらどうするか?」という振る舞いだけを切り出しておくことで、
UIデザイナーやレベルデザイナーが インスペクター上でシーンのパスだけ差し替えて使い回せる ようになります。
フルコード
using UnityEngine;
using UnityEngine.SceneManagement;
using UnityEngine.UI;
namespace Sample.SceneControl
{
/// <summary>
/// 親の Button がクリックされたら、指定されたシーンをロードするコンポーネント。
///
/// - Button 自体の onClick には何も設定しなくてOK
/// - SceneChanger を Button の子オブジェクトとしてアタッチして使う
/// - シーンは Build Settings に登録されている必要がある
///
/// 責務は「シーン遷移」のみ。入力やアニメーションは他コンポーネントに任せる想定です。
/// </summary>
[DisallowMultipleComponent]
public class SceneChanger : MonoBehaviour
{
// ロードしたいシーンのパス(例: "Scenes/Title", "Scenes/Game/Main" など)
// Build Settings に登録されていることが前提です。
[SerializeField]
private string scenePath = string.Empty;
// シーンロード時のモード(Single: 既存シーンを破棄して1つだけ、Additive: 追加ロード)
[SerializeField]
private LoadSceneMode loadMode = LoadSceneMode.Single;
// 誤設定防止のため、ボタンを明示的に指定できるようにしておく
// 未指定の場合は親オブジェクトから自動取得を試みる
[SerializeField]
private Button targetButton;
// シーン名やパスの入力ミスを検出するためのフラグ
// 実行時エラーを避けるため、エディタ上でも警告を出します。
[SerializeField]
private bool validateOnStart = true;
private void Reset()
{
// コンポーネント追加時に、親から Button を自動で探してセットする
TryCacheParentButton();
}
private void Awake()
{
// Awake 時点で Button がセットされていなければ、親から取得を試みる
if (targetButton == null)
{
TryCacheParentButton();
}
// Button が見つからなかった場合は警告を出して処理を止める
if (targetButton == null)
{
Debug.LogWarning(
$"[SceneChanger] 親階層に Button コンポーネントが見つかりませんでした。" +
$" ゲームオブジェクト: {gameObject.name}",
this
);
return;
}
// ボタンのクリックイベントに、シーン切り替え処理を登録
targetButton.onClick.AddListener(OnButtonClicked);
}
private void Start()
{
if (!validateOnStart)
{
return;
}
// シーンパスが空の場合は警告
if (string.IsNullOrWhiteSpace(scenePath))
{
Debug.LogWarning(
$"[SceneChanger] scenePath が設定されていません。" +
$" インスペクターからシーンのパスを指定してください。" +
$" ゲームオブジェクト: {gameObject.name}",
this
);
return;
}
// Build Settings に登録されていない可能性をログで注意喚起
if (!IsSceneInBuildSettings(scenePath))
{
Debug.LogWarning(
$"[SceneChanger] 指定されたシーンパス \"{scenePath}\" は " +
$"Build Settings に登録されていない可能性があります。" +
$" 実行時にロードに失敗する場合は、File > Build Settings から追加してください。",
this
);
}
}
private void OnDestroy()
{
// メモリリークを避けるため、イベント登録を解除しておく
if (targetButton != null)
{
targetButton.onClick.RemoveListener(OnButtonClicked);
}
}
/// <summary>
/// Button がクリックされたときに呼ばれるコールバック。
/// 実際のシーン遷移処理をここで行う。
/// </summary>
private void OnButtonClicked()
{
if (string.IsNullOrWhiteSpace(scenePath))
{
Debug.LogWarning(
$"[SceneChanger] scenePath が設定されていないため、シーン遷移をスキップしました。" +
$" ゲームオブジェクト: {gameObject.name}",
this
);
return;
}
// 実際のシーンロード処理
// 非同期ロードにしたい場合は LoadSceneAsync に差し替えると良いです。
Debug.Log($"[SceneChanger] シーンをロードします: \"{scenePath}\" (Mode: {loadMode})", this);
try
{
SceneManager.LoadScene(scenePath, loadMode);
}
catch (System.Exception ex)
{
Debug.LogError(
$"[SceneChanger] シーンロードに失敗しました。パス: \"{scenePath}\" " +
$"例外: {ex.Message}",
this
);
}
}
/// <summary>
/// 親階層から Button コンポーネントを取得してキャッシュする。
/// </summary>
private void TryCacheParentButton()
{
// GetComponentInParent は自分自身も含めた親階層を探索します
targetButton = GetComponentInParent<Button>();
if (targetButton == null)
{
Debug.LogWarning(
$"[SceneChanger] 親階層から Button を自動取得できませんでした。" +
$" 手動で targetButton を設定してください。" +
$" ゲームオブジェクト: {gameObject.name}",
this
);
}
}
/// <summary>
/// 指定されたシーン名またはパスが Build Settings に登録されているかをざっくりチェックする。
/// 厳密な保証ではないが、入力ミスの早期検出に役立つ。
/// </summary>
private bool IsSceneInBuildSettings(string sceneNameOrPath)
{
int sceneCount = SceneManager.sceneCountInBuildSettings;
for (int i = 0; i < sceneCount; i++)
{
string path = SceneUtility.GetScenePathByBuildIndex(i);
// 完全一致 or パスの末尾(ファイル名部分)比較
if (path == sceneNameOrPath)
{
return true;
}
// "Assets/Scenes/Title.unity" から "Title" を取り出して比較するイメージ
string fileName = System.IO.Path.GetFileNameWithoutExtension(path);
if (fileName == sceneNameOrPath)
{
return true;
}
}
return false;
}
/// <summary>
/// 実行時にシーンパスを差し替えたい場合用の API。
/// 外部スクリプトから呼び出して使えます。
/// </summary>
public void SetScenePath(string newScenePath)
{
scenePath = newScenePath;
}
}
}
使い方の手順
ここでは、典型的な UI の例として「タイトル画面の Start ボタンからゲームシーンへ遷移する」ケースで説明します。
-
シーンを Build Settings に登録する
- メニューから
File > Build Settings...を開く Scenes/Title.unityとScenes/Game.unityを「Scenes In Build」にドラッグ&ドロップ- パスかファイル名をどちらで指定するかを決めておく(例:
Gameだけ、またはScenes/Gameなど)
- メニューから
-
UI ボタンを作成する
- ヒエラルキーで右クリック →
UI > Button (TextMeshPro)などを作成 - ボタンの名前を
StartButtonなどに変更 - ボタンの見た目や配置は自由に調整
- ヒエラルキーで右クリック →
-
SceneChanger をボタンの子オブジェクトとして追加する
StartButtonの子として空の GameObject を作成(例:StartButton_SceneChanger)- その子オブジェクトに
SceneChangerコンポーネントをアタッチ targetButtonが自動で親の Button に設定されていることを確認(されていなければ手動でドラッグ)scenePathに遷移先シーンを指定(例:GameもしくはScenes/Game)loadModeは通常SingleのままでOK
※ Button 本体の
OnClick()イベントには、何も設定しなくて大丈夫です。
SceneChanger が自動でonClickにリスナーを登録します。 -
動作確認する
- タイトルシーンを開き、Play ボタンを押す
StartButtonをクリックする- 指定したシーン(
Game)に切り替われば成功
他の具体例
-
敵に倒されたときの「リトライ」ボタン
- ゲームオーバー用の Canvas に
RetryButtonを配置 - 子オブジェクトに
SceneChangerをアタッチ scenePathに現在のゲームシーン名(例:Game)を指定- これで「リトライボタンを押すと同じシーンをロードしてやり直し」が簡単に実装できます
- ゲームオーバー用の Canvas に
-
ステージ選択画面のステージボタン
Stage1Button,Stage2Button… を用意- それぞれのボタンの子に
SceneChangerをアタッチ scenePathにStage1,Stage2など別々のシーン名を設定- プレハブ化しておけば、新しいステージ追加時もシーン名だけ差し替えて使い回せます
メリットと応用
SceneChanger を使うことで、以下のようなメリットがあります。
-
1ボタン = 1責務の小さなコンポーネント設計
シーン遷移のロジックを巨大な「UI管理スクリプト」にまとめてしまうのではなく、
「このボタンはこのシーンへ飛ぶ」という責務をSceneChangerに閉じ込められます。 -
プレハブ化して量産しやすい
StartButtonやBackToTitleButtonなどをプレハブにしておけば、
他のシーンにドラッグ&ドロップするだけで、同じ挙動を簡単に再利用できます。 -
レベルデザイナー/UIデザイナーがインスペクターで完結できる
シーン名(またはパス)をインスペクターから入力するだけで動くため、
スクリプトを書けないメンバーでも、シーン遷移付きのボタンを配置できます。 -
後からの仕様変更に強い
たとえば「ロード前にフェードアウトを挟みたい」「ロードを非同期にしたい」といった要望が出ても、
SceneChangerの中身だけを差し替えれば、全ボタンに一括で適用できます。
改造案:フェード用マネージャーを噛ませてからシーン遷移する
もう少しリッチにしたい場合、「ボタンを押したらすぐに SceneManager を呼ぶ」のではなく、
フェードアウト用のマネージャーを経由してからシーンを切り替える、という設計もよく使います。
例えば、以下のようなメソッドを SceneChanger に追加して、
フェードマネージャー経由でロードするように差し替えることができます。
/// <summary>
/// フェードマネージャーを経由してシーン遷移する例。
/// ここでは仮想的な FadeManager を想定しています。
/// 実際には自分のプロジェクトのフェード実装に合わせて書き換えてください。
/// </summary>
private void LoadSceneWithFade()
{
if (string.IsNullOrWhiteSpace(scenePath))
{
Debug.LogWarning("[SceneChanger] scenePath が未設定のため、フェード付き遷移をスキップしました。", this);
return;
}
// 例: フェードアウト完了時にシーンをロードする
// FadeManager.Instance.FadeOut(() =>
// {
// SceneManager.LoadScene(scenePath, loadMode);
// });
// このサンプルでは実際の FadeManager 実装は省略しています。
// プロジェクト側のフェードシステムに合わせてコールバック内で LoadScene を呼ぶ形にしましょう。
}
このように、シーン遷移という 1つの責務を小さなコンポーネントに切り出しておくと、
「同期ロード → 非同期ロード」「即時切り替え → フェードを挟む」といった変更を、
UI全体ではなく SceneChanger だけに閉じ込めたまま進化させられます。
まずはシンプルな形で導入し、必要に応じて少しずつ拡張していくスタイルで運用してみてください。




