Unity を検索

ScriptableObject を使ってシーンワークフローを改善しよう

2020年7月1日 カテゴリ: Engine & platform | 9 分 で読めます
シェア

Is this article helpful for you?

Thank you for your feedback!

Unity で複数のシーンを管理することは困難であり、このワークフローを改善することは、ゲームのパフォーマンスとチームの生産性の両方の観点から、非常に重要です。ここでは、大規模なプロジェクトに合わせた方法でシーンのワークフローを設定するためのヒントをいくつか紹介します。

ほとんどのゲームには複数のレベルがあり、レベルには複数のシーンが含まれていることがよくあります。シーンが比較的小さいゲームでは、プレハブを使用して異なるセクションに分割することができます。しかし、ゲーム中にそれらを有効にしたり、インスタンス化したりするには、すべてのプレハブを参照する必要があります。つまり、ゲームが大きくなり、これらの参照がメモリ上でより多くのスペースを占めるようになると、シーンを使う方が効率的になるということです。

レベルは 1 つ以上の Unity シーンに分割することができます。それらをすべて管理する最適な方法を見つけることが重要になります。エディターでは、複数のシーンを開いたり、複数シーン編集を使って実行時に複数のシーンを開くことができます。レベルを複数のシーンに分割することで、Git や SVN、Unity Collaborate などのコラボレーションツールを使う時にマージ時のコンフリクトを回避できるので、チームワークが楽になるという利点もあります。

複数のシーンを管理してレベルを構成する

以下のビデオでは、ゲームのロジックとレベルのさまざまな部分をいくつかの異なる Unity シーンに分割して、レベルをより効率的にロードする方法を紹介しています。そして、これらのシーンをロードするときに、追加シーン読み込みモードを使用して、永続的なゲームロジックと一緒に必要な部分をロードしたりアンロードしたりします。プレハブをシーンの「アンカー」として機能させる形で使っていますが、これは、各シーンがレベルの一部を表し、個別に編集することができるので、チームで作業する際にも高い柔軟性を発揮します。

編集モードでこれらのシーンをロードして、いつでも再生モードに入ることができるのは変わらないので、レベルデザインを作成するときにすべてのシーンを一度に見渡すことも可能です。

これらのシーンをロードするには、2 つの異なる方法があります。最初の方法は距離ベースの方法で、オープンワールドのような屋外のレベルに適しています。このテクニックを使うときは、いくらか視覚効果(例えば霧など)を使ってロードとアンロードのプロセスを隠すことも有効です。

2 番目の方法は、どのシーンをロードするかをチェックするためにトリガーを使用する方法で、これは屋内のレベルに関して最初の方法より効率的な方法と言えます。

このコンテンツはサードパーティのプロバイダーによってホストされており、Targeting Cookiesを使用することに同意しない限り動画の視聴が許可されません。これらのプロバイダーの動画の視聴を希望する場合は、Targeting Cookiesのクッキーの設定をオンにしてください。

これでレベルの内部ですべてのシーンを管理できるようになりました。ここからさらにレイヤーを追加して、レベルのより良い管理を実現することができます。

ScriptableObject を使ってゲーム内で複数のレベルを管理する

私たちは、ゲームプレイの間じゅう、すべてのレベルに加えて、各レベルのさまざまなシーンを追跡したいと考えています。これを行う方法の一つとして、MonoBehaviour スクリプトで静的な変数とシングルトンパターンを使用することが考えられますが、この方法にはいくつかの問題があります。シングルトンパターンを使用すると、システム間が密接に繋がってしまい、厳密な意味でモジュール化されなくなってしまうのです。システムは別々に存在することはできず、常にお互いに依存する形になります。 もう 1 つの問題は、静的な変数の使用です。これはインスペクターには表示されないため、設定のためにコードを変更する必要があり、アーティストやレベルデザイナーがゲームを簡単にテストできなくなります。異なるシーン間でデータを共有する必要があるときは、静的な変数と DontDestroyOnLoad を組み合わせて使うやり方をよく見かけますが、DontDestoryOnLoad は可能な限り避けるべきです。 異なるシーンの情報を格納するには、主にデータを格納するために使われる、シリアライズ可能なクラスである ScriptableObject を使うことができます。GameObject にアタッチされたコンポーネントとして利用される MonoBehaviour スクリプトとは異なり、ScriptableObject は GameObject にアタッチされないため、プロジェクト全体の異なるシーン間で共有することができます。 この構造をレベルだけでなく、ゲーム内のメニューシーンにも使えるようにしたいものです。そのためには、レベルとメニューの間の共通のプロパティを含む GameScene クラスを作成します。


public class GameScene : ScriptableObject
{
    [Header("Information")]
    public string sceneName;
    public string shortDescription;

    [Header("Sounds")]
    public AudioClip music;
    [Range(0.0f, 1.0f)]
    public float musicVolume;

    [Header("Visuals")]
    public PostProcessProfile postprocess;
}

このクラスが継承するのは ScriptableObject であり、MonoBehaviour ではないことに注意してください。ここには、ゲームで必要なだけのプロパティを追加することができます。このステップの後、先ほど作成した GameScene クラスを継承した Level クラスと Menu クラスを作成することができます。これらのクラスもまた、ScriptableObject です。


[CreateAssetMenu(fileName = "NewLevel", menuName = "Scene Data/Level")]
public class Level : GameScene
{
    // レベルのみに固有の設定
    [Header("Level specific")]
    public int enemiesCount;
}

最初に CreateAssetMenu 属性を追加すると、Unity の Assets メニューから新しいレベルを作成することができます。Menu クラスでも同じことができます。また、インスペクターからメニューの種類を選択できるようにするために列挙型を含めることもできます。


public enum Type
{
    Main_Menu,
    Pause_Menu
}

[CreateAssetMenu(fileName = "NewMenu", menuName = "Scene Data/Menu")]
public class Menu : GameScene
{
    // メニューのみに固有の設定
    [Header("Menu specific")]
    public Type type;
}

レベルとメニューを作成できるようになったので、参照しやすいようにレベルとメニューを一覧表示するデータベースを追加してみましょう。また、プレイヤーが現在いるレベルを追跡するためのインデックスを追加します。そして、新しいゲームをロードしたり(この場合は最初のレベルがロードされます)、現在のレベルをリプレイしたり、次のレベルに進むためのメソッドを追加できます。これら 3 つのメソッドの間ではインデックスのみが変化するので、インデックス付きのレベルをロードするメソッドを作成して、それを使い回せるということに注目してください。


[CreateAssetMenu(fileName = "sceneDB", menuName = "Scene Data/Database")]
public class ScenesData : ScriptableObject
{
    public List<Level> levels = new List<Level>();
    public List<Menu> menus = new List<Menu>();
    public int CurrentLevelIndex=1;

    /*
     * Levels
     */

    //Load a scene with a given index
    public void LoadLevelWithIndex(int index)
    {
        if (index <= levels.Count)
        {
            //Load Gameplay scene for the level
            SceneManager.LoadSceneAsync("Gameplay" + index.ToString());
            //Load first part of the level in additive mode
            SceneManager.LoadSceneAsync("Level" + index.ToString() + "Part1", LoadSceneMode.Additive);
        }
        //reset the index if we have no more levels
        else CurrentLevelIndex =1;
    }
    //Start next level
    public void NextLevel()
    {
        CurrentLevelIndex++;
        LoadLevelWithIndex(CurrentLevelIndex);
    }
    //Restart current level
    public void RestartLevel()
    {
        LoadLevelWithIndex(CurrentLevelIndex);
    }
    //New game, load level 1
    public void NewGame()
    {
        LoadLevelWithIndex(1);
    }

    /*
     * Menus
     */

    //Load main Menu
    public void LoadMainMenu()
    {
        SceneManager.LoadSceneAsync(menus[(int)Type.Main_Menu].sceneName);
    }
    //Load Pause Menu
    public void LoadPauseMenu()
    {
        SceneManager.LoadSceneAsync(menus[(int)Type.Pause_Menu].sceneName);
    }

メニューのためのメソッドもあり、前に作成した列挙型を使用して、必要な特定のメニューをロードできます。列挙型の順序とメニューリストの順序が同じであることを確認してください。 これで、プロジェクトウィンドウで右クリックして Assets メニューからレベル、メニュー、データベースの ScriptableObject を作成できるようになりました。

あとは、必要なレベルやメニューを追加して、設定を調整してから Scenes データベースに追加していくだけです。下の例では、Level1、MainMenu、そして Scenes Data を追加したところを示しています。

では、これらのメソッドを呼び出してみましょう。この例では、プレイヤーがレベルの最後に到達したときに表示されるユーザーインターフェース(UI)上の次のレベルボタンが NextLevel メソッドを呼び出しています。ボタンにメソッドをアタッチするには、Button コンポーネントの On Click イベントの「+」ボタンをクリックして新しいイベントを追加し、次に Scenes Data の ScriptableObject をオブジェクトフィールドにドラッグアンドドロップして、以下のように ScenesData から NextLevel メソッドを選択します。

他のボタンについても同じ処理を行うことができます。ボタンごとに、レベルをリプレイしたり、メインメニューに移動したりなどの動作を設定することができます。また、他のスクリプトから ScriptableObject を参照して、BGM 用の AudioClip や後処理のプロファイルなど、さまざまなプロパティにアクセスし、レベル内でそれらを使用することもできます。

プロセスのエラーを防止するためのヒント

  • ロード/アンロードをできるだけ控える

動画で示されている ScenePartLoader スクリプトでは、プレイヤーがコライダーに複数回出入りし続けて、シーンのロードとアンロードが繰り返しトリガーされていることがわかります。これを避けるために、スクリプト内のシーンのロードとアンロードのメソッドを呼び出す前にコルーチンを追加し、プレイヤーがトリガーから離れた場合にコルーチンを停止させることができます。

  • 命名規則

もう 1 つの一般的に通用するヒントは、プロジェクトの中でしっかりとした命名規則を使うことです。スクリプトやシーン、マテリアルやその他諸々の物に至るまで、プロジェクト内のさまざまな種類のアセットにどのように名前を付けるかについて、チームで事前に合意しておく必要があります。そうすることで、皆さんご自身だけでなく、チームメイトがプロジェクトで作業したり、プロジェクトを保守したりすることが簡単になります。このような習慣はどのような場合にも良いアイデアと言えるものですが、今回の例のような ScriptableObject を使ったシーンの管理を行う場合には特にシビアに守るべきものとなります。今回示した例ではシーン名をベースにした簡単なアプローチを使用しましたが、シーン名に依存しない方法も多数あります。文字列ベースのアプローチは避けるべきです。それは、ある特定の文脈に基づいて Unity シーンの名前を変更すると、ゲームの別の部分ではそのシーンがロードされなくなってしまうからです。

  • カスタムツールの作成

ゲーム全体で名前の依存関係を回避する方法の 1 つは、スクリプトを用意してシーンを Object タイプとして参照することです。これにより、インスペクターでシーンのアセットをドラッグアンドドロップして、スクリプトで安全に名前を取得することができます。しかし、これはエディタークラスであり、実行時には AssetDatabase クラスにアクセスできません。エディターで動作させてヒューマンエラーを防ぎつつ、実行時にも動作するソリューションとするために、両方のデータを組み合わせる必要があります。シリアライズ時にシーンのアセットから文字列パスを抽出し、実行時に使用するためにそれを保存できるオブジェクトを実装する方法の例については、ISerializationCallbackReceiver インターフェースを参照してください。

さらに、カスタムインスペクターを作成して、メニューから手動でシーンを追加して同期を保つのではなく、ボタンを使って素早く簡単にシーンをビルド設定に追加できるようにする方法もあります。

このタイプのツールの例として、JohannesMP による素晴らしいオープンソース実装をチェックしてみてください(これは Unity の公式リソースではありません)。

ご意見をお聞かせください

本記事では、複数のシーンと Prefabs を組み合わせて作業する際に ScriptableObject を使うことでワークフローを改善する方法の例を紹介しました。ゲームによってシーンを管理する方法は大きく異なります。すべてのゲームの構造に適した、たった 1 つのソリューションというのはありません。プロジェクトの構成に合わせて独自のカスタムツールを実装することは、非常に理にかなっています。

今回お届けした情報が皆さんのプロジェクトにとって有益に働いたり、あるいは皆さんが独自のシーン管理ツールを作成するためのヒントになったりすることがあれば幸いです。

2020年7月1日 カテゴリ: Engine & platform | 9 分 で読めます

Is this article helpful for you?

Thank you for your feedback!