【Unity】ScriptableObjectを使った「シーン管理」のやり方まとめ

Unity ScriptableObjectでシーン管理を楽にしよう ゲーム開発

今回はUnityにおけるシーン管理に関する中級者向けの話題で

ScriptableObjectを使って楽にシーンを管理する方法

をご紹介するという内容になっております。

Unityでは「シーン」とよばれるアセットにキャラクターや障害物などのオブジェクトを配置してステージを作っていきます。ただUnityの弱点としてシーンを管理する機能に乏しいことが挙げられるので、何も考えずにシーンの数を増やすと管理が大変になるという問題点があります。

もちろん、小規模な自作ゲームであれば力業でシーンを管理してもあまり問題にならないかもしれません。しかし中規模以上のゲームや品質重視の有料ゲームなどを作る際には堅牢なシーン管理の仕組みは必須ですからできればきちんと実装しておきたいものです。

そこでここではシーン管理の一つの方法として便利で私も普段からよく使っている「ScriptableObjectを使ったシーン管理方法」を解説していきますね。

前提:そもそもなぜ「シーン管理」が必要なのか?

まずはじめに、そもそもなぜUnityではシーン管理が必要なのか?という点を整理しておこうと思います。主な理由はズバリ次のとおり。

Unityのシーン遷移関数には「ビルドインデックス」か「シーン名」しか使えないから

では上記によってどのような弊害があるのかを確認してみましょう。

Unityのシーン遷移関数にはビルドインデックスかシーン名しか使えない

例えば「SampleScene1」から「SampleScene2」に遷移するには、SampleScene1で下記のようにLoadSceneAsync関数を呼びます(※LoadScene関数もありますがほとんどの場合は非同期版のLoadSceneAsyncを使うでしょう)。

SceneManager.LoadSceneAsync("SampleScene2");

このときこの関数の引数に渡せるのは

  • ビルドインデックス
    →ビルドプロファイルのシーンリストのインデックス
  • シーン名の文字列

の2つだけです。どういうわけかシーンアセットを直接渡すことはできません。したがって特に工夫しなければシーンはビルドインデックスかシーン名で管理することになります。

ビルドインデックスまたはシーン名でシーン管理を行う方法の問題点

ところがその2つでシーンを管理しようとすると問題が発生します。例えば

  • ビルドインデックスで管理
    →シーンリストの順番が変わると別のシーンに遷移してしまう
  • シーン名で管理
    →シーン名の記入を間違えたり、シーン名を変えたときに対応し忘れたりする可能性がある

といった感じですね。いずれの方法もシーンファイルが増えれば増えるほどちょっとしたミスでシーン遷移がうまくいかなくなってしまうので大変です。

…ここまでの話をまとめるとシーンファイルをLoadSceneAsync関数に直接に渡すことができれば話は簡単なのですが、なぜかUnityではそれができないのでシーン数が増えると管理がややこしいことになるというわけです。

ScriptableObjectベースのシーン管理について

さてシーンをビルドインデックスまたはシーン名で管理することの問題点についてご理解いただいたところで、本題である「ScriptableObjectベースのシーン管理」のメリットやこの場合のシーン遷移のやり方をご紹介していきます。

ScriptableObjectとは?

まず、今回の方法を使いこなすには前提としてScriptableObjectについて知っておく必要があります。これは簡単に言うとゲームに使うデータを入れておくためのコンテナです。ScriptableObjectに関する詳しい話は別の記事で丁寧に解説していますので、「なんだそれ…」という方は下記の記事も併せてご覧いただければと思います。

ScriptableObjectを使ったシーン管理の仕様・C#スクリプト

次に今回のシーン管理方法の仕様と具体的なC#スクリプトについてザックリとご説明します。

  1. 各シーンを表すScriptableObjectを作る
  2. 上記1. のScriptableObjectをリスト化するScriptableObjectを作る

各シーンを表すScriptableObject

はじめに、各シーンを表すScriptableObject(以下、SceneReferenceクラスとします)を作ります。これは内部的にはシーン名を記録する変数が入っているだけです。

[CreateAssetMenu(fileName = "New SceneReference", menuName = "Scenes/SceneReference")]
public class SceneReference : ScriptableObject
{
    [SerializeField, HideInInspector] private string sceneName;
#if UNITY_EDITOR
    [SerializeField] private UnityEditor.SceneAsset sceneAsset;

    private void OnValidate()
    {
        if (sceneAsset != null)
            sceneName = sceneAsset.name;
    }
#endif

    public string SceneName => sceneName;
}

ただし単純にシーン名を手動入力する方式だと普通にシーン名で管理するのと同じデメリット(シーン名を打ち間違えるなど)が出てしまうので、エディタ上でのみシーンアセットを参照できるようにしてOnValidate関数のタイミングで自動的にシーン名を取得するようにしてあります。これなら手動でシーン名を打ち込む必要はなく、シーンアセットを登録するだけで済みます。

なおゲームをビルドすると「#if UNITY_EDITOR」~「#endif」の間の処理は削除されて予め保存されたシーン名のみが使用されます。

シーンをリスト化するためのScriptableObject

次に、先ほどのSceneReferenceをまとめて登録し必要なSceneReferenceを取得するためのScriptableObject(SceneFlowクラス)を作ります。小規模なゲームならSceneReferenceを個別に使ってシーン遷移を行ってもいいのですが、中規模以上のゲームになるとシーンの流れがわかりやすいほうがいいので一覧化しておいたほうがゲームを作りやすくなります(また、複数のSceneFlowを使うことでより複雑なシーンの流れを作ることもできます)。

そこで例えばステージを順番にクリアしていくタイプのゲームなら下記のようなSceneFlowクラスを作っておくと便利です(※下記はあくまでもサンプルなのでご自身のゲームに合わせて改造してください)。

using UnityEngine;
using System.Collections.Generic;

[CreateAssetMenu(fileName = "New SceneFlow", menuName = "Scenes/SceneFlow", order = 1)]
public class SceneFlow : ScriptableObject
{
    [SerializeField]
    private List<SceneReference> scenes = new();

    public IReadOnlyList<SceneReference> Scenes => scenes;

    public SceneReference GetNextScene(SceneReference currentScene)
    {
        int index = scenes.IndexOf(currentScene);
        if (index >= 0 && index < scenes.Count - 1)
        {
            return scenes[index + 1];
        }
        return null;
    }

    public SceneReference GetPreviousScene(SceneReference currentScene)
    {
        int index = scenes.IndexOf(currentScene);
        if (index > 0)
        {
            return scenes[index - 1];
        }
        return null;
    }

    public SceneReference GetSceneAt(int index)
    {
        if (index >= 0 && index < scenes.Count)
        {
            return scenes[index];
        }
        return null;
    }
}

これにより「次のステージに進む」といった処理を簡単に行うことができるようになります。

シーン遷移の具体的なやり方

さてこれで必要なScriptableObjectがそろいました。ただしあくまでもシーン管理に使うScriptableObjectを用意しただけでシーン遷移を行う処理は実装していないので、必要な時に遷移処理を呼び出さなければなりません。

とはいえやり方は簡単で、例えば特定のシーンに遷移するだけならSceneReferenceに格納されているシーン名をLoadSceneAsync関数に渡すだけです。

[SerializeField]
SceneReference sceneReference;

SceneManager.LoadSceneAsync(sceneReference.SceneName);

また、現在のシーンがわかっているならSceneFlowを使って次のシーンに遷移することもできます。

[SerializeField]
SceneFlow flow;
[SerializeField]
SceneReference currentScene;

SceneManager.LoadSceneAsync(flow.GetNextScene(currentScene).SceneName);

ScriptableObjectを使ったシーン管理のメリット

では最後に今回の方法を使ったシーン管理のメリットをまとめると次のようなものが挙げられます。

  • インスペクター上で参照を登録できる
  • (内部的にはシーン名で管理するが)シーン名を自動的に反映するので間違えずに済む
  • ScriptableObjectなので特定のシーンに依存せずどこからでも参照できる

ScriptableObjectベースのシーン管理方法は、ドラッグ&ドロップで参照を登録できて便利な点や特定のシーンに依存せずどこからでも参照できる点が強みです。うまく使えばシーン管理が大幅に楽になるでしょう。

おわりに

以上、ScriptableObjectを活用したシーン管理方法について解説しました。Unityでは何も工夫しなければシーン管理は煩わしい作業になるので、ぜひ今回ご紹介した内容を参考にしていただきシーン管理の手間を削減していただければと思います。

この記事がUnityでのゲーム開発のお役に立てば幸いです。