この記事は モバイルファクトリー Advent Calendar 2017 13日目の記事です。
前日は @yashims85 さんの「小規模アプリで実装するシングルストアの強さを語る」でした。
今日はゆるくBuildSettingsのScene定義をよしなにします。

実行環境

Unity 2017.2.0p1

背景

1つのプロダクトにおいて、ゲームジャムやカジュアルゲーム(not ステージ系)では機能自体が少ないため、UnityのSceneの数は片手で数えられる程度で済むことが多々あります。

一方、機能が徐々に増えて行くと、ページの出し分けの実装方針によっては、Sceneの数は平気で3,40まで達することもあるかと思います (特にマルチシーンでUIパーツを構成したりするケースでは顕著で、一方でPrefabを出し分けるような管理においては該当しません)。

BuildSettings - Scene

scene_build.png

BuildSettingsScenes in Build の項目に含まれるシーンがビルドの対象となります。逆に言えば、 BuildSettings に含まれていないシーンは読み込むことが出来ません。
Unity開発者は SceneManager.LoadScene 実行時に必ず1度は以下の表示を見たことがあると信じています。

スクリーンショット 2017-12-12 15.07.11.png

BuildSettings - Scene が disable になるケース

UnityEditor上で BuildSettings に追加済みのSceneの名前を変更したり、Sceneを削除する場合、変更/削除がフックとなり BuildSettings は追従します。
一方でUnityEditor外 (Finderやコマンドライン) でSceneに変更した場合、BuildSettings は追従してくれません。

BuildSettings の中身を確認すると、ファイルとしては削除したSceneは残り続けているのが分かります。

buildsettings.gif

BuildSettings - Scene を生成するスクリプト

Sceneの追加を忘れ都度追加しなくてはいけなくなるのは手間ですし、削除済みのSceneが残り続けるのは害は無いですが少し気持ち悪いです。コンフリクトしたときにも手作業で解消するのは面倒でしょう。

そこでEditor拡張を使い、既存のScene群から BuildSettings を追加するコマンドを作成します。
以下のScriptを Editor フォルダ以下に配置することで動作します。

https://gist.github.com/lycoris102/cf16ef82419e3184604f681a94090fd9

BuildSettingScenesCreator.cs
using System.IO;
using System.Linq;
using UnityEditor;
using UnityEngine;

public class BuildSettingScenesCreator
{
    private const string sceneDir = "Scenes";
    private const string initialLoadScene = "Scenes/Title.unity";

    [MenuItem("Edit/BuildSettings/CreateScenes")]
    public static void CreateBuildSettingScenes()
    {
        if (!CheckExists())
            return;

        Create();
    }

    private static bool CheckExists()
    {
        string sceneDirFullPath = GetFullPath(sceneDir);
        string initialLoadSceneFullPath = GetFullPath(initialLoadScene);

        // 存在チェック
        if (!Directory.Exists(sceneDirFullPath))
        {
            Debug.LogError("Not Found Dir :" + sceneDirFullPath);
            return false;
        }

        if (!File.Exists(initialLoadSceneFullPath))
        {
            Debug.LogError("Not Found Inital Load Scene : " + initialLoadSceneFullPath);
            return false;
        }

        return true;
    }

    private static void Create()
    {
        string sceneDirAssetsPath = GetAssetsPath(sceneDir);
        string initialLoadSceneAssetsPath = GetAssetsPath(initialLoadScene);

        var scenes = AssetDatabase.FindAssets("t:Scene", new string[] {sceneDirAssetsPath})
            .Select(guid => AssetDatabase.GUIDToAssetPath(guid))
            .OrderBy(path => path)
            .Where(path => path != initialLoadSceneAssetsPath)
            .Select(path => new EditorBuildSettingsScene(path, true))
            .ToList();

        // 初回に呼び込まれて欲しいシーンを先頭に配置する
        scenes.Insert(0, new EditorBuildSettingsScene(initialLoadSceneAssetsPath, true));

        EditorBuildSettings.scenes = scenes.ToArray();
        AssetDatabase.SaveAssets();

        Debug.Log("Created BuildSettings.");
    }

    private static string GetFullPath(string path)
    {
        return Application.dataPath + "/" + path;
    }

    private static string GetAssetsPath(string path)
    {
        return "Assets/" + path;
    }
}

以下をScript上で指定して実行する必要があります。

  • sceneDir : シーンを格納するフォルダ
  • initialLoadScene : ゲーム起動時に開きたいシーン

buildsettings2.gif

Scene変更時に動的にBuildSettingsを更新する

さらに効率化するために、任意のSceneが更新(追加/削除)された時に、それが該当ディレクトリ以下であれば自動的に更新するように変更します。

https://gist.github.com/lycoris102/553a0e60cbaa6b462dc18cba01b127f7

📝 AssetPostprocessor

AssetPostprocessor を継承し OnPostprocessAllAssets を定義することで、アセットの更新完了時に任意の処理を実行することが出来ます。
アセットインポート時の処理の呼び出し順序は以下のサイトにて詳しく紹介されており、参考になりました。

Editor拡張入門 - 第28章 AssetPostprocessor

BuildSettingScenesUpdater.cs
using System.Collections.Generic;
using System.IO;
using System.Linq;
using UnityEditor;
using UnityEngine;

public class BuildSettingScenesUpdater : AssetPostprocessor
{
    private const string sceneDir = "Scenes";
    private const string initialLoadScene = "Scenes/Title.unity";

    public static void OnPostprocessAllAssets(
        string[] importedAssets,
        string[] deletedAssets,
        string[] movedAssets,
        string[] movedFromAssetPaths)
    {
        if (!CheckExists())
            return;

        var assets = importedAssets
            .Union(deletedAssets)
            .Union(movedAssets)
            .Union(movedFromAssetPaths);

        if (CheckInSceneDir(assets))
        {
            Create();
        }
    }

    // Sceneディレクトリ以下のアセットが編集されたか
    private static bool CheckInSceneDir(IEnumerable<string> assets)
    {
        return assets.Any(asset => Path.GetDirectoryName(asset) == GetAssetsPath(sceneDir));
    }

    // 存在チェック
    private static bool CheckExists()
    {
        string sceneDirFullPath = GetFullPath(sceneDir);
        string initialLoadSceneFullPath = GetFullPath(initialLoadScene);

        if (!Directory.Exists(sceneDirFullPath))
        {
            Debug.LogError("Not Found Dir :" + sceneDirFullPath);
            return false;
        }

        if (!File.Exists(initialLoadSceneFullPath))
        {
            Debug.LogError("Not Found Inital Load Scene : " + initialLoadSceneFullPath);
            return false;
        }

        return true;
    }

    private static void Create()
    {
        string sceneDirAssetsPath = GetAssetsPath(sceneDir);
        string initialLoadSceneAssetsPath = GetAssetsPath(initialLoadScene);

        var scenes = AssetDatabase.FindAssets("t:Scene", new string[] {sceneDirAssetsPath})
            .Select(guid => AssetDatabase.GUIDToAssetPath(guid))
            .OrderBy(path => path)
            .Where(path => path != initialLoadSceneAssetsPath)
            .Select(path => new EditorBuildSettingsScene(path, true))
            .ToList();

        // 初回に呼び込まれて欲しいシーンを先頭に配置する
        scenes.Insert(0, new EditorBuildSettingsScene(initialLoadSceneAssetsPath, true));

        EditorBuildSettings.scenes = scenes.ToArray();
        AssetDatabase.SaveAssets();

        Debug.Log("Created BuildSettings.");
    }

    private static string GetFullPath(string path)
    {
        return Application.dataPath + "/" + path;
    }

    private static string GetAssetsPath(string path)
    {
        return "Assets/" + path;
    }
}

buildsettings3.gif

最後に

以上、BuildSettingsに関する小ネタでした。
明日は @shioiyan さんの「RubyでGoogleAPIを利用してみる」お話です。
楽しみですね!

Sign up for free and join this conversation.
Sign Up
If you already have a Qiita account log in.