【Unity】InputSystemを使って「溜め攻撃」を超シンプルに実装する方法

この記事では、Unityの新しい入力システム「Input System」を活用して、ゼルダの伝説の回転斬りのような「溜め攻撃」アクションを実装する方法を解説します。

Input Systemには長押しを検知する「Hold Interaction」もありますが、今回はあえてそれを使わず、ボタン入力の基本的なイベントである「押した瞬間 (started)」と「離した瞬間 (canceled)」の時刻差を利用するアプローチを紹介します。

従来のUpdate関数内で毎フレーム入力を監視する方法と比較して、Input Systemのイベント駆動型アプローチは、コードがシンプルになり、特に機能追加や変更に対する保守性が向上するメリットが期待できます。


この記事の内容

  1. 溜め攻撃の実装:Input System (Press) vs Update vs Hold Interaction
  2. Input SystemのインストールとInput Actionsアセットの作成
  3. Input Actions:左クリック(攻撃ボタン)アクションの定義
  4. スクリプト連携:PlayerInputとイベント処理スクリプト
  5. 溜め時間の計算ロジック:startedとcanceledの活用
  6. 中間演出の実装:コルーチンでInput Systemの弱点を補う
  7. まとめ:Pressイベント方式のメリット・デメリットと使い分け

溜め攻撃の実装:Input System (Press) vs Update vs Hold Interaction

溜め攻撃を実装する方法はいくつか考えられます。それぞれの特徴を見てみましょう。

  • 従来のUpdate関数による実装:
    • Update()内で毎フレーム入力をチェックし、ボタンが押されている時間を計測します。
    • 仕組みは直感的ですが、入力の種類や条件分岐が増えるとコードが複雑化しがちです。
  • Input System の Hold Interaction を使う実装:
    • Input ActionsアセットでInteractionを「Hold」に設定し、指定時間(Hold Time)長押しされるとperformedイベントが発生します。
    • 設定は簡単ですが、後述するように「短押し(通常攻撃)」と「長押し(溜め攻撃)」を同じボタンで使い分けたい場合に、実装がやや複雑になることがあります。
  • Input System の Press イベント (started/canceled) を使う実装(本記事のアプローチ):
    • ボタンが押された瞬間(started)と離された瞬間(canceled)のイベントを利用し、その間の時間を計測します。
    • イベント駆動でコードが整理されやすく、短押し・長押しの判定や溜め中の処理を柔軟に実装できます。

Q. なぜHold InteractionではなくPressイベント (started/canceled) を使うのか?

Hold Interactionは指定時間長押しされたことを検知するのに便利ですが、「同じボタンで短押し(通常攻撃)と長押し(溜め攻撃)を使い分けたい」場合や、「溜めている最中に演出を入れたい」場合には、少し扱いにくい側面があります。

  • 短押しと長押しの両立: Hold Interactionでは、長押しが成立してperformedが呼ばれる前に、短押し時の処理(通常攻撃など)をボタンを押した直後に実行させたい場合、工夫が必要です。例えば、「押した瞬間(started)に通常攻撃を出し、Holdが成立したらキャンセルする」といった制御や、別途短押し用のアクションを用意するなど、実装が複雑になりがちです。
  • 押下開始タイミングの活用: ボタンを押した瞬間にキャラクターが構えモーションに入ったり、溜めエフェクトを開始したりといった処理を即座に行いたい場合、Hold Interactionだけではタイミングを計りにくいことがあります。
  • 溜め中の制御: 溜めゲージを表示したり、一定時間ごとに溜めレベルが上がる演出を入れたりする場合、最終的にperformedイベントが発生するまで待つ必要があるHold Interactionでは、溜めている途中の細かな制御がしづらいです。

一方、本記事で紹介するPressイベント (started/canceled) を利用する方法では、

  • ボタンを押した瞬間のstartedイベントと、離した瞬間のcanceledイベントの時刻差を計測することで、押下時間を正確に把握できます。
  • これにより、「一定時間未満なら通常攻撃、一定時間以上なら溜め攻撃」といった判定をcanceledイベント内で簡単に行えます。
  • startedイベント発生時に溜め動作やエフェクトを開始し、canceledイベントで攻撃を発動、という流れを自然に実装できます。
  • (後述するコルーチンなどを組み合わせれば)溜めている最中の演出や状態変化も柔軟に組み込めます。

このように、短押し/長押しの判定や、溜め中の演出・状態管理を柔軟に行いたい場合には、started/canceledイベントを利用するアプローチが適していると言えます。


Input SystemのインストールとInput Actionsアセットの作成

まず、プロジェクトにInput Systemパッケージを導入し、入力を定義するInput Actionsアセットを作成します。

Input Systemパッケージのインストール

Unityエディタのメニューから操作します。

Package ManagerからInput Systemをインストールする手順

  1. Window > Package Manager を開きます。
  2. 左上のドロップダウンメニューで「Packages: Unity Registry」を選択します。
  3. リストから「Input System」を探し、「Install」ボタンをクリックします。
  4. インストール中にプロジェクト設定の変更を促すダイアログが出たら、「Yes」を選択してエディタを再起動します。

Input Actionsアセットファイルの作成

次に、入力アクションを定義するためのアセットファイルを作成します。

ProjectウィンドウでInput Actionsアセットを作成

  1. Projectウィンドウで右クリックし、Create > Input Actions を選択します。
  2. 作成されたアセットファイル(例: `PlayerInputActions.inputactions`)に分かりやすい名前を付けます。
  3. 作成したアセットファイルを選択し、Inspectorウィンドウで「Generate C# Class」にチェックを入れ、「Apply」ボタンを押します。

Generate C# Classにチェックを入れるとC#スクリプトが生成される

「Generate C# Class」にチェックを入れることで、このInput Actionsアセットに対応するC#クラスが自動生成され、スクリプトから入力イベントを扱いやすくなります。


Input Actions:左クリック(攻撃ボタン)アクションの定義

作成したInput Actionsアセットファイル(例: `PlayerInputActions.inputactions`)をダブルクリックして編集ウィンドウを開き、溜め攻撃に使用するアクションを定義します。

Input Actionsエディタで攻撃アクションを設定する

  1. Action Maps 列の「+」ボタンをクリックし、新しいAction Mapを作成します(例: `Gameplay`)。Action Mapは関連するアクションをまとめるグループです。
  2. Actions 列の「+」ボタンをクリックし、新しいActionを作成します(例: `AttackLeft`)。これが具体的な入力操作に対応します。
  3. 作成した`AttackLeft` Actionを選択し、右側のPropertiesパネルで以下を設定します。
    • Action Type:Button」を選択します。これは、押す/離すの単純な入力に適しています。
  4. `AttackLeft` Actionの下にある``を選択し、Propertiesパネルで以下を設定します。これが具体的な入力デバイスのボタンとの紐付け(Binding)です。
    • Path: プルダウンメニューから「Mouse」>「Left Button」を選択します。(ゲームパッドのボタンなどもここで設定可能)
  5. 編集が終わったら、ウィンドウ上部の「Save Asset」ボタンをクリックして変更を保存します。

これで、「マウスの左クリック」が`AttackLeft`という名前のアクションとして、スクリプトからイベントとして受け取れるようになりました。


スクリプト連携:PlayerInputとイベント処理スクリプト

定義したInput Actionsを実際にゲーム内で機能させるために、スクリプトとの連携を設定します。

1. シーン内に空のGameObjectを作成し、分かりやすい名前(例: `InputManager`)を付けます。

2. 作成した`InputManager` GameObjectに「Player Input」コンポーネントを追加します。

3. Player Inputコンポーネントの「Actions」フィールドに、先ほど作成したInput Actionsアセット(例: `PlayerInputActions.inputactions`)をドラッグ&ドロップで設定します。

4. 以下のC#スクリプト(例: `InputManager.cs`)を作成し、`InputManager` GameObjectにアタッチします。

using UnityEngine;
using UnityEngine.InputSystem;

// PlayerInputコンポーネントが必須であることを示す
[RequireComponent(typeof(PlayerInput))]
public class InputManager : MonoBehaviour
{
    // ボタンが押され始めた時刻を記録する変数
    private float buttonPressStartTime;
    // 溜め攻撃と判定する時間のしきい値(例: 1秒)
    private const float specialAttackThreshold = 1.0f;

    // PlayerInputコンポーネントから呼び出されるメソッド
    // メソッド名はInput Actionsで定義したアクション名(例: AttackLeft)に
    // "On" を付けたものにするか、後述のようにInspectorで手動設定する
    public void OnAttackLeft(InputAction.CallbackContext context)
    {
        // ボタンが押された瞬間 (started) の処理
        if (context.started)
        {
            Debug.Log("Attack button pressed (started)");
            // 押下開始時刻を記録
            buttonPressStartTime = Time.time;
            // ここで構えモーションや溜めエフェクト開始などの処理を入れることも可能
        }
        // ボタンが離された瞬間 (canceled) の処理
        else if (context.canceled)
        {
            Debug.Log("Attack button released (canceled)");
            // 押されていた時間を計算
            float pressDuration = Time.time - buttonPressStartTime;
            Debug.Log($"Press duration: {pressDuration} seconds");

            // 押下時間がしきい値を超えていたら溜め攻撃
            if (pressDuration > specialAttackThreshold)
            {
                PerformSpecialAttack(); // 溜め攻撃実行メソッド呼び出し
            }
            // しきい値未満なら通常攻撃
            else
            {
                PerformNormalAttack(); // 通常攻撃実行メソッド呼び出し
            }
        }
        // context.performed は Button タイプでは started とほぼ同じタイミングで呼ばれることが多い
        // Hold Interaction を使わない場合、主に started と canceled を使う
    }

    // 通常攻撃を実行する処理(中身は仮)
    private void PerformNormalAttack()
    {
        Debug.Log("Perform Normal Attack!");
        // ここに実際の通常攻撃ロジックを記述
    }

    // 溜め攻撃を実行する処理(中身は仮)
    private void PerformSpecialAttack()
    {
        Debug.Log("Perform Special Attack!");
        // ここに実際の溜め攻撃ロジックを記述
    }
}

5. `InputManager` GameObjectを選択し、InspectorウィンドウでPlayer Inputコンポーネントの「Behavior」を「Invoke Unity Events」に設定します。

6. 「Events」セクションが展開されるので、設定したAction Map名(例: `Gameplay`)を開き、その中のアクション名(例: `Attack Left`)に対応するイベント欄の「+」ボタンをクリックします。

7. イベント欄に`InputManager` GameObject自体をドラッグ&ドロップし、右側のドロップダウンメニューから「InputManager」>「OnAttackLeft (InputAction.CallbackContext)」を選択します。(スクリプトのメソッド名が `On[アクション名]` であれば自動で認識されることもあります)

これで、マウスの左ボタンがクリックされる(押される、または離される)たびに、`InputManager.cs`スクリプト内の`OnAttackLeft`メソッドが呼び出されるようになります。このイベント駆動の仕組みにより、`Update`関数を使うことなく入力処理を実現できます。


溜め時間の計算ロジック:startedとcanceledの活用

前述のスクリプト (`InputManager.cs`) 内の `OnAttackLeft` メソッドで行っている溜め時間の計算ロジックを詳しく見てみましょう。

  1. 押下開始 (context.started):
    • マウスの左ボタンが押された瞬間にこのブロックが実行されます。
    • 現在の時刻 (`Time.time`) を `buttonPressStartTime` 変数に記録します。これが溜め時間の計測開始点となります。
  2. 押下終了 (context.canceled):
    • 押されていた左ボタンが離された瞬間にこのブロックが実行されます。
    • 現在の時刻 (`Time.time`) から、記録しておいた押下開始時刻 (`buttonPressStartTime`) を引くことで、ボタンが押されていた時間 (`pressDuration`) を計算します。
    • 計算した `pressDuration` と、あらかじめ定義しておいた溜め攻撃のしきい値 (`specialAttackThreshold`) を比較します。
    • pressDuration がしきい値より長ければ `PerformSpecialAttack()` メソッドを、短ければ `PerformNormalAttack()` メソッドを呼び出します。

このように、Input Systemの `started` と `canceled` イベントを利用することで、ボタンが押されていた時間を正確に計測し、それに基づいて通常攻撃と溜め攻撃を振り分けることができます。`Update()` 関数内で毎フレーム時間を加算していく必要がないため、コードがシンプルになり、処理負荷も軽減される可能性があります。


中間演出の実装:コルーチンでInput Systemの弱点を補う

Input Systemのイベント駆動モデルは、「押した」「離した」といった瞬間のイベントを捉えるのは得意ですが、「ボタンを押し続けている間の特定のタイミング(例: 溜め攻撃が可能になる瞬間)」で何か処理を行いたい場合には、少し工夫が必要です。

例えば、「溜め時間がしきい値に達したらキャラクターを光らせる」「溜め完了のSEを鳴らす」といった演出を入れたい場合、`started` と `canceled` イベントだけでは、その「中間点」を直接検知できません。

この問題を解決する一般的な方法の一つが、Unityの「コルーチン (Coroutine)」を利用することです。ボタンが押された (`started`) 時点でコルーチンを開始し、一定時間(溜め攻撃のしきい値)が経過したら、溜め完了の合図(シグナル)を送る、という仕組みを作ります。

以下は、先ほどの `InputManager.cs` にコルーチンを追加し、溜め完了のシグナル(ここではDebugログ出力)を実装した例です。

using UnityEngine;
using UnityEngine.InputSystem;
using System.Collections; // コルーチンのために必要

[RequireComponent(typeof(PlayerInput))]
public class InputManager : MonoBehaviour
{
    private float buttonPressStartTime;
    private const float specialAttackThreshold = 1.0f;
    // 実行中のコルーチンを保持する変数
    private Coroutine chargeCheckCoroutine;
    // 溜め完了シグナルが送られたかどうかのフラグ
    private bool isChargeComplete = false;

    public void OnAttackLeft(InputAction.CallbackContext context)
    {
        if (context.started)
        {
            Debug.Log("Attack button pressed (started)");
            buttonPressStartTime = Time.time;
            isChargeComplete = false; // 溜め開始時にフラグをリセット

            // もし既にコルーチンが動いていたら停止する(連打対策)
            if (chargeCheckCoroutine != null)
            {
                StopCoroutine(chargeCheckCoroutine);
            }
            // 溜め時間監視コルーチンを開始
            chargeCheckCoroutine = StartCoroutine(ChargeTimerCoroutine());
        }
        else if (context.canceled)
        {
            Debug.Log("Attack button released (canceled)");
            // ボタンが離されたら、溜め時間監視コルーチンを停止
            if (chargeCheckCoroutine != null)
            {
                StopCoroutine(chargeCheckCoroutine);
                chargeCheckCoroutine = null; // 保持しているコルーチン参照をクリア
            }

            float pressDuration = Time.time - buttonPressStartTime;
            Debug.Log($"Press duration: {pressDuration} seconds");

            // 溜め完了フラグが立っていれば(=しきい値を超えていれば)溜め攻撃
            if (isChargeComplete) // または pressDuration > specialAttackThreshold でも判定可能
            {
                PerformSpecialAttack();
            }
            else
            {
                PerformNormalAttack();
            }

            // 攻撃実行後にフラグをリセット
            isChargeComplete = false;
        }
    }

    // 溜め時間を監視するコルーチン
    private IEnumerator ChargeTimerCoroutine()
    {
        // しきい値の時間だけ待機
        yield return new WaitForSeconds(specialAttackThreshold);

        // しきい値に到達したら(かつボタンがまだ押されている場合)
        // isChargeComplete フラグを立て、溜め完了の合図を送る
        // ※ context.ReadValue() > 0 などでボタンが押され続けているか確認する方がより厳密
        Debug.Log("Charge Complete threshold reached!");
        isChargeComplete = true;

        // ここで溜め完了エフェクト(光る、SE鳴らすなど)をトリガーする
        TriggerChargeCompleteEffect();

        chargeCheckCoroutine = null; // コルーチン終了
    }

    private void PerformNormalAttack()
    {
        Debug.Log("Perform Normal Attack!");
        // 通常攻撃ロジック
    }

    private void PerformSpecialAttack()
    {
        Debug.Log("Perform Special Attack!");
        // 溜め攻撃ロジック
    }

    private void TriggerChargeCompleteEffect()
    {
         Debug.Log("Play Charge Complete Effect!");
        // 溜め完了時の演出処理(エフェクト表示、SE再生など)
    }
}

このコードでは、ボタンが押されたら `ChargeTimerCoroutine` を開始し、`specialAttackThreshold` 秒後に `isChargeComplete` フラグを `true` にして、溜め完了演出メソッド `TriggerChargeCompleteEffect()` を呼び出します。ボタンが離された (`canceled`) 時点で、このフラグが `true` になっているかどうかで溜め攻撃か通常攻撃かを判断します。

このようにコルーチンを組み合わせることで、Input Systemのイベント駆動のメリットを活かしつつ、溜め攻撃における中間的なタイミングでの処理も実現できます。一見すると `Update` で実装するより複雑に感じるかもしれませんが、入力の種類が増えたり、他のアクションとの連携が必要になったりした場合、Input SystemのAction Mapやイベントによる責務分離がコード全体の整理に役立ちます。


まとめ:Pressイベント方式のメリット・デメリットと使い分け

UnityのInput SystemにおけるPressイベント (started / canceled) を利用することで、イベント駆動に基づいた溜め攻撃の実装が可能になることを見てきました。

メリット:

  • Update関数を使わずに済み、コードがイベント単位で整理されやすい。
  • 押下開始・終了のタイミングを正確に捉えられる。
  • 短押し/長押しの判定や、それに応じた処理の分岐が比較的容易。
  • Input Actionsアセットによる入力マッピング管理が直感的で、キーコンフィグなどへの拡張性が高い。

デメリット(考慮点):

  • 「押され続けている間の特定のタイミング」での処理(溜め完了演出など)には、コルーチンなどの補完的な仕組みが必要になる。
  • 単純な溜め時間計測だけなら、Update関数で実装する方がシンプルに感じる場合もある。

どちらを選ぶべきか?

最終的な実装方法は、プロジェクトの規模や要件、開発チームの好みによって異なります。

  • 小規模なプロジェクトやプロトタイプで、溜め攻撃以外の入力が少ない場合は、Update関数によるシンプルな実装でも十分かもしれません。
  • 中規模以上のプロジェクトで、多様な入力(ゲームパッド対応、キーコンフィグなど)や、他のアクションとの連携、将来的な拡張性・保守性を重視する場合は、Input SystemのPressイベント + コルーチン等による補完という組み合わせが有力な選択肢となるでしょう。Hold Interactionも選択肢ですが、本記事で解説したような柔軟性を求める場合はPressイベント方式が有利な場面があります。

Input Systemは学習コストが少しありますが、慣れれば強力な武器になります。ぜひ、ご自身のプロジェクトに合った方法で溜め攻撃の実装に挑戦してみてください。

【Unity】シーン遷移はインスペクタのリスト選択で!SceneAsset+OnValidateで安全&効率UP

Unityでシーンを切り替える際、SceneManager.LoadScene("シーン名") を使うのが基本的な方法です。しかし、シーン名を直接文字列で指定するこの方法は、シーンファイルの名前を変更した際の修正漏れや、単純なタイプミスによるエラーの原因となりやすく、プロジェクトが大きくなるにつれて保守性を低下させる一因にもなります。

この記事では、こうした文字列指定のリスクを回避し、より安全で効率的にシーン遷移を実装するためのテクニックとして、UnityEditor.SceneAssetOnValidateメソッドを組み合わせる方法を紹介します。この方法を使えば、Unityエディタ上でインスペクタから直感的に遷移先のシーンを選択でき、かつビルド後も問題なく動作するため、日々の開発効率とコードの安全性を高めることができます。


この記事の内容

  1. 従来のシーン遷移(文字列指定)の問題点
  2. 基本的な実装方法(文字列指定)
  3. 改良版:SceneAssetとOnValidateを使った実装
  4. 少し詳しい技術解説
  5. その他のシーン管理アプローチ
  6. まとめ:安全なシーン遷移で開発効率を上げよう

従来のシーン遷移(文字列指定)の問題点

なぜ文字列指定は危険なのか?

SceneManager.LoadScene("MyScene") のようにシーン名を文字列で直接コードに書き込む方法は、一見シンプルですが、以下のような問題を引き起こしがちです。

  • シーンファイル名の変更に弱い: Projectウィンドウでシーンファイルの名前を変更しても、コード内の文字列は自動で更新されません。関連する全てのコードを手動で修正する必要があり、漏れが発生しやすいです。
  • タイプミス(Typo)によるエラー: “MyScene” を “MyScen” など、少しでも間違えて入力すると、実行時にシーンが見つからずエラーになります。コンパイル時にはエラーが出ないため、発見が遅れることがあります。
  • 管理の煩雑化: プロジェクト内のシーン数が増えるにつれて、どこでどのシーン名を文字列で使っているか把握するのが難しくなり、コードの保守性が低下します。

これらの問題は、特にチーム開発や長期にわたるプロジェクトでは、無視できない開発コストの増加につながります。

解決策:SceneAsset + OnValidate の概要

そこで役立つのが、UnityEditor.SceneAssetOnValidate メソッドを組み合わせたテクニックです。この方法には以下のメリットがあります。

  • インスペクタからシーンを選択可能に: SceneAsset 型の変数をスクリプトに用意することで、インスペクタ上でシーンファイルを直接ドラッグ&ドロップして指定できます。これにより、文字列の手入力が不要になり、タイプミスを防げます。
  • ビルド後も安全に動作: SceneAsset はエディタ専用ですが、OnValidate メソッドを使って、エディタでシーンが選択された瞬間にそのシーン名を内部的に文字列として保持します。これにより、ビルド後の実行環境でも正しいシーン名を扱えるようになります。
  • 保守性と開発効率の向上: シーン名の変更があっても、インスペクタで再設定するだけで済み、コード修正の手間が大幅に減ります。

基本的な実装方法(文字列指定)

まずは比較のために、従来の文字列指定による基本的なシーン遷移の実装を見てみましょう。

通常のシーン遷移コード例

以下は、特定のGameObject(例: ゴール地点)にプレイヤーが接触した際に、指定した名前のシーンに遷移するシンプルなスクリプトです。

using UnityEngine;
using UnityEngine.SceneManagement;

public class SceneTransitionTrigger : MonoBehaviour
{
    // インスペクタで遷移先のシーン名を入力する
    [SerializeField] private string sceneToLoad;

    private void OnTriggerEnter2D(Collider2D other)
    {
        // 接触したのがプレイヤーだったら
        if (other.CompareTag("Player"))
        {
            // 文字列で指定したシーンをロード
            SceneManager.LoadScene(sceneToLoad);
        }
    }
}

このスクリプトをアタッチしたGameObjectのインスペクタには、`Scene To Load` という文字列入力フィールドが表示されます。

基本的なシーン遷移コードのインスペクタ表示(文字列入力)

ここに遷移したいシーンの名前(例: “Stage2″)を正確に入力する必要があります。

【重要】Build Settingsへのシーン追加

非常に重要な注意点として、SceneManager.LoadScene でロードするシーンは、必ず事前に Build Settings に追加しておく必要があります。これを行わないと、エディタ上では動作してもビルド後にシーンが見つからずエラーになります。

  1. Unityメニューの File > Build Settings… を開きます。
  2. 「Scenes In Build」のリストに、遷移先のシーンファイル(Projectウィンドウから)をドラッグ&ドロップして追加します。

Build Settingsにシーンファイルを追加する画面

(↑ここにリストアップされているシーンのみ、ビルド後もロード可能です)


改良版:SceneAssetとOnValidateを使った実装

それでは、文字列指定の問題点を解決する改良版の実装を見ていきましょう。

SceneAssetを利用したコード例

以下のスクリプトでは、インスペクタ上にSceneAsset型のフィールドを用意し、そこにシーンファイルを直接ドラッグ&ドロップできるようにします。そして、OnValidateメソッドを使って、選択されたシーンの名前を内部的な文字列変数に自動でコピーします。

改良版コードのインスペクタ表示(SceneAsset選択フィールド)

(↑インスペクタでシーンファイルを直接アタッチできるように!)

using UnityEngine;
using UnityEngine.SceneManagement;
// UnityEditor 名前空間はエディタ専用機能を使う場合に必要
#if UNITY_EDITOR
using UnityEditor;
#endif

public class SceneLoaderSafe : MonoBehaviour
{
    // [HideInInspector] 実行時にはこの文字列だけあれば良いのでインスペクタからは隠す
    [HideInInspector]
    [SerializeField] private string sceneToLoad;

// #if UNITY_EDITOR ~ #endif で囲まれた部分はエディタ上でのみ有効になる
#if UNITY_EDITOR
    // インスペクタに表示するためのSceneAsset型変数
    [Header("遷移先シーン選択")] // インスペクタに見出しを表示
    [SerializeField] private SceneAsset sceneAsset; // ここにシーンファイルをD&Dする
#endif

    private void OnTriggerEnter2D(Collider2D other)
    {
        if (other.CompareTag("Player"))
        {
            // ロード時は保持しておいた文字列変数を使う
            if (!string.IsNullOrEmpty(sceneToLoad))
            {
                SceneManager.LoadScene(sceneToLoad);
            }
            else
            {
                Debug.LogError("遷移先のシーン名が設定されていません!");
            }
        }
    }

// OnValidateメソッドもエディタ専用
#if UNITY_EDITOR
    // インスペクタで値が変更された時などに自動で呼ばれるメソッド
    private void OnValidate()
    {
        // sceneAssetフィールドにシーンが設定されたら
        if (sceneAsset != null)
        {
            // そのシーンの名前(文字列)を sceneToLoad 変数にコピーする
            sceneToLoad = sceneAsset.name;
        }
        else
        {
            // SceneAssetが未設定なら文字列も空にする
            sceneToLoad = "";
        }
    }
#endif
}

コードのポイント:

  • using UnityEditor;#if UNITY_EDITOR / #endif: SceneAssetOnValidate はエディタ専用機能なので、これらを使ってビルドエラーを防ぎます。
  • [SerializeField] private SceneAsset sceneAsset;: インスペクタにシーンファイルを設定するためのフィールドです。
  • [HideInInspector] [SerializeField] private string sceneToLoad;: 実際にSceneManager.LoadSceneで使うシーン名を格納する文字列変数。インスペクタからは隠しておきます。
  • private void OnValidate(): インスペクタでsceneAssetが変更されるたびに呼び出されます。ここでsceneAsset.name(選択されたシーンの名前)を取得し、sceneToLoad変数に代入します。
  • SceneManager.LoadScene(sceneToLoad);: 実際のシーンロード時には、OnValidateによって事前に設定された文字列sceneToLoadを使用します。

なぜOnValidateが必要なのか?ビルドエラー回避の鍵

「インスペクタでSceneAssetを直接指定できるなら、OnValidateでわざわざ文字列に変換しなくても、SceneManager.LoadScene(sceneAsset.name)で直接ロードすれば良いのでは?」と疑問に思うかもしれません。

しかし、それはできません。なぜなら、UnityEditor.SceneAssetはUnityエディタ専用のクラスであり、ビルドされたゲーム内では利用できないからです。もしOnValidateを使わずにsceneAsset.nameを直接ロードしようとすると、エディタ上では動作しても、ビルドしたゲームを実行すると「UnityEditor名前空間が見つからない」というエラーが発生し、シーン遷移が失敗してしまいます。

OnValidateメソッドは、まさにこの問題を解決するための「橋渡し役」です。エディタ上でSceneAssetが選択・変更されるたびにOnValidateが自動的に実行され、その瞬間にシーン名を文字列としてsceneToLoad変数に保存してくれます。このsceneToLoad変数は通常の文字列なので、ビルド後のゲームでも問題なく参照できます。

つまり、OnValidateを使うことで、「エディタ上での安全で直感的なシーン選択(SceneAsset)」と「ビルド後も確実に動作するシーンロード(文字列)」という、両方のメリットを実現できるのです。


少し詳しい技術解説

UnityEditor.SceneAsset とは?

UnityEditor.SceneAssetクラスは、Unityエディタ内でシーンファイル(.unity)そのものをアセットとして参照するためのものです。インスペクタ上でシーンファイルをドラッグ&ドロップで設定できるため、視覚的で間違いが起こりにくいインターフェースを提供します。ただし、前述の通り、UnityEditor名前空間に属しており、ビルド後の実行ファイルには含まれません。

参考:Unity Documentation: UnityEditor.SceneAsset (※リンク先は古いバージョンの場合があるため、お使いのUnityバージョンに合わせて確認してください)

OnValidateメソッドの役割

OnValidate()は、MonoBehaviourを継承したクラス内で定義できる特殊なメソッドの一つです。これは、スクリプトがロードされたとき、またはインスペクタで値が変更されたときに、エディタ上でのみ呼び出されます。この性質を利用することで、インスペクタでの設定変更をトリガーにして、何らかの処理(今回の場合はシーン名の文字列への変換・保存)を自動的に行うことができます。

参考:Unity Documentation: MonoBehaviour.OnValidate()

エディタでの選択からビルド後の動作までの流れ

この改良版の実装における処理の流れを整理すると、以下のようになります。

  1. 開発時(エディタ上):
    1. 開発者は、スクリプトのインスペクタでsceneAssetフィールドに遷移したいシーンファイル(例: `Stage2.unity`)をドラッグ&ドロップする。
    2. 値が変更されたため、OnValidate()メソッドが自動的に実行される。
    3. OnValidate()内で、sceneAsset.name(この場合は “Stage2″)が取得され、sceneToLoad変数に文字列 “Stage2” が保存される。
  2. 実行時(ビルド後):
    1. プレイヤーがトリガーに接触する。
    2. OnTriggerEnter2Dメソッドが実行される。
    3. SceneManager.LoadScene(sceneToLoad) が呼び出される。この時、sceneToLoadにはエディタで設定された文字列 “Stage2” が格納されている。
    4. シーン “Stage2” が正常にロードされる。

このように、エディタ専用の機能と実行時にも有効なデータをOnValidateが繋ぐことで、安全で効率的なシーン遷移が実現します。


その他のシーン管理アプローチ

今回紹介したSceneAsset + OnValidate方式以外にも、Unityのシーン遷移を管理・実装する方法はいくつか存在します。

  • シーン名を定数やenum(列挙型)で管理する: シーン名をコード内で定数として定義したり、enum型で管理したりする方法。文字列のハードコーディングは避けられますが、シーン追加・削除時に定数やenumの定義も更新する必要があります。
  • ScriptableObjectでシーンリストを管理する: 遷移可能なシーンのリストをScriptableObjectアセットとして作成・管理する方法。中央集権的に管理できますが、設定の手間はかかります。
  • Addressable Asset Systemを使用する: シーン自体をAddressableアセットとして扱い、アドレス(文字列キー)でロードする方法。非同期ロードや動的なコンテンツ配信に適していますが、システム自体の学習コストがかかります。

それぞれにメリット・デメリットがありますが、今回紹介したSceneAsset + OnValidate方式は、特別なアセットや複雑なシステムを導入することなく、比較的手軽に実装でき、かつエディタ上での利便性とビルド後の安全性を両立できる点で、特に小~中規模のプロジェクトにおいて有効な選択肢の一つと言えるでしょう。


まとめ:安全なシーン遷移で開発効率を上げよう

Unityにおけるシーン遷移で、シーン名を文字列で直接指定する方法は、タイプミスやシーン名変更時の修正漏れといったリスクを伴い、プロジェクトの保守性を低下させる可能性があります。

この記事で紹介したUnityEditor.SceneAssetOnValidateメソッドを組み合わせる方法は、これらの問題を解決する効果的なアプローチです。

  • インスペクタからシーンファイルを直接選択できるため、タイプミスを防ぎ、設定が直感的になります。
  • OnValidateによって、エディタでの選択情報がビルド後も有効な文字列データとして保持されるため、安全なシーンロードが可能です。
  • シーン名の変更が必要になった場合も、インスペクタでシーンファイルを再設定するだけで済み、コード修正の手間が大幅に削減されます。

これにより、シーン遷移に関するエラーのリスクを低減し、開発効率とコードの保守性を向上させることが期待できます。シーン数が増えてきて管理が煩雑になってきたと感じたら、ぜひこの実装方法を試してみてください。