Unity エディターでの Undo 操作の登録を便利するツールを公開しました

はじめに

Unity エディターにおいて、Undo 操作の登録を簡単に行うことができるようになるツールを公開しました! 🚀

github.com

Unity Undo Extensions の使いどころ

Unity での開発では、エディター拡張を書いてメニューにオリジナルの項目を追加したり、Inspector にボタンを置いたりして、特定の操作を効率的に行えるようにするということをよくやると思います。

このとき、これらの拡張から普通に GameObject.AddComponent のようなメソッドを実行すると、Undo 操作が登録されず、Ctrl + Z などで取り消すことができません。また、オブジェクトが Dirty としてマーク*1されることもないため、Ctrl + S などで保存をすることもできません。

Undo や保存ができるようにするには、 Undo.AddComponent のような専用のメソッドを実行する必要がありますが、これらは UnityEditor 名前空間内にあるため、ビルドするとエラーになってしまいます。

これらを加味すると、以下のようなコードにすることが考えられます。

// 自身に SpriteRenderer をアタッチする
// エディター上かつ非再生時のみ、Undo 版のメソッドを使用する

using UnityEngine;
#if UNITY_EDITOR
using UnityEditor;
#endif

public class ExampleMonoBehaviour : MonoBehaviour
{
    public void ExampleButton()
    {
#if UNITY_EDITOR
        if (!EditorApplication.isPlaying)
        {
            Undo.AddComponent<SpriteRenderer>(gameObject);
        }
#endif

        gameObject.AddComponent<SpriteRenderer>();
    }
}

このようにすることで、ビルドエラーを回避しつつ Undo 登録ができるようになります……が、かなりコードが煩雑になってしまい、毎回これを書くのは結構しんどい。

そこで、Unity Undo Extensions の出番です。このパッケージを導入することで、以下のように書くことができるようになります。

using GigaCreation.Tools.UndoExtensions;
using UnityEngine;

public class ExampleMonoBehaviour : MonoBehaviour
{
    public void ExampleButton()
    {
        gameObject.AddComponentAsUndoable<SpriteRenderer>();
    }
}

AddComponentAsUndoable を使用することで、一行で書くことができるようになりました!

使い方

現在のバージョン (2.0.0) では、以下の 3 つの拡張メソッドが含まれています。

  • AddComponentAsUndoable
  • DestroyAsUndoable
  • DoActionAsUndoable

それぞれ、以下のように使用します。

using GigaCreation.Tools.UndoExtensions;
using UnityEngine;

public class ExampleMonoBehaviour : MonoBehaviour
{
    public int Counter;

    public void Example01()
    {
        // GameObject にコンポーネントをアタッチする
        // エディター上かつ非再生時のみ、Undo 操作の登録および Dirty マークを行う
        gameObject.AddComponentAsUndoable<SpriteRenderer>();
    }

    public void Example02()
    {
        if (TryGetComponent(out SpriteRenderer spriteRenderer))
        {
            // オブジェクトを破棄する
            // エディター上かつ非再生時のみ、Undo 操作の登録および Dirty マークを行う
            spriteRenderer.DestroyAsUndoable();
        }
    }

    public void Example03()
    {
        // 任意のアクションを実行する
        // エディター上かつ非再生時のみ、Undo 操作の登録および Dirty マークを行う
        this.DoActionAsUndoable("Increment Counter", x =>
        {
            x.Counter++;
        });
    }
}

DoActionAsUndoable の第 1 引数の文字列は、Undo History ウィンドウで表示されるタイトル名です。お好きな文字列で大丈夫です。

API リファレンス

英語にはなりますが、API リファレンスもございます

インストール方法

Package Manager を使用する方法

導入したい Unity プロジェクトで Package Manager を開き、「+」ボタンを押して「Add package from git URL...」を選択、以下の URL を入力して「Add」ボタンを押すとインストールできます。

https://github.com/gigacreation/UnityUndoExtensions.git?path=Assets/UndoExtensions

手動でインストールする方法

GitHub のリポジトリからソースコードをダウンロードし、その中の Assets/MissingFinder/ ディレクトリを導入したい Unity プロジェクトにコピーします。

*1:シーン名に「*」マークが付いている状態

Unity プロジェクト内の Missing を検索するツールを公開しました

はじめに

Unity のシーンやプレハブの中にある、Missing になってしまっている参照やコンポーネントを検索し、コンソールに表示するツールを公開しました! 🚀

github.com

使い方

このパッケージをインストールすると、メニューに Tools/GIGA CREATION/Missing Finder/ が追加され、以下のコマンドを実行できるようになります。

  • Find Missing in Current Scene
    • 現在開かれているシーンの中にある Missing を検索します。
  • Find Missing in Enabled Scenes
    • 有効なシーンの中にある Missing を検索します。有効なシーンとは、Build Settings ウィンドウの「Scenes In Build」に追加されているシーンのことを指します。
  • Find Missing in All Scenes
    • プロジェクト内のすべてのシーンから Missing を検索します。
  • Find Missing in Current Prefab Stage
    • 現在開かれているプレハブの中にある Missing を検索します(プレハブモードでのみ使用できます)。
  • Find Missing in All Prefab Assets
    • プロジェクト内のすべてのプレハブから Missing を検索します。

インストール方法

Package Manager を使用する方法

導入したい Unity プロジェクトで Package Manager を開き、「+」ボタンを押して「Add package from git URL...」を選択、以下の URL を入力して「Add」ボタンを押すとインストールできます。

https://github.com/gigacreation/UnityMissingFinder.git?path=Assets/MissingFinder

手動でインストールする方法

GitHub のリポジトリからソースコードをダウンロードし、その中の Assets/MissingFinder/ ディレクトリを導入したい Unity プロジェクトにコピーします。

CSV の特定の列を抽出して、リストや辞書を生成できるツールを公開しました

はじめに

CSV の特定の列をリストにしたり、特定の列をキー・値に持つ辞書を生成できる、Unity 向けのツールを公開しました! 🚀

github.com

CSV を辞書にして表示させたもの

使い方

CSV からリストを生成する

1. 抽出したい CSV を準備します。

var csv = @"
name,model_number,release_date
Family Computer,HVC-001,1983-07-15
SUPER Famicom,SHVC-001,1990-11-21
NINTENDO64,NUS-001,1996-06-23
"

上記のように CSV のテキストを直接コードに書いてもいいですし、 Resources.Load<TextAsset>(path).text などでファイルから取得しても大丈夫です。

2. リクエスト用のデータ CsvExtractRequest を作成します。

var request = new CsvExtractRequest(csv, true, "", 1);

3. 最後に、 CsvUtility.ExtractIntoList() でリクエストを投げれば、生成されたリストが取得できます。

List<string> list = CsvUtility.ExtractIntoList(request);

CSV から辞書を生成する

1. 抽出したい CSV を準備します(リストのときと同じ)。

2. リクエスト用のデータ CsvExtractRequest を作成します。 このとき、SetKeyColumnIndexes() で辞書のキーにする列を設定するのを忘れないでください!

var request = new CsvExtractRequest(csv, true, "", 1);
request.SetKeyColumnIndexes(0); // <- キーとして抽出する列のインデックスを設定

3. 最後に、 CsvUtility.ExtractIntoDictionary() でリクエストを投げれば、生成された辞書が取得できます。

Dictionary<string, string> dictionary = CsvUtility.ExtractIntoDictionary(request);

リクエストデータ CsvExtractRequest について

CsvExtractRequest は、以下のメンバーを持っています。

/// <summary>
/// CSV のテキスト。
/// </summary>
public string Csv { get; }

/// <summary>
/// true なら、CSV の 1 行目をヘッダーとして扱います。
/// </summary>
public bool HasHeader { get; }

/// <summary>
/// 複数の列が指定された際、この文字で列同士を繋げます。
/// </summary>
public string Separator { get; }

/// <summary>
/// 値として抽出する列のインデックス。
/// </summary>
public int[] ValueColumnIndexes { get; }

/// <summary>
/// キーとして抽出する列のインデックス。辞書を生成するときのみ使用します。
/// </summary>
public int[] KeyColumnIndexes { get; private set; }

public CsvExtractRequest(string csv, bool hasHeader = false, string separator = null, params int[] valueIndexes)
{
    Csv = csv;
    HasHeader = hasHeader;
    Separator = separator;
    ValueColumnIndexes = valueIndexes;
}

public void SetKeyColumnIndexes(params int[] indexes)
{
    KeyColumnIndexes = indexes;
}

インストール方法

Package Manager を使用する方法

導入したい Unity プロジェクトで Package Manager を開き、「+」ボタンを押して「Add package from git URL...」を選択、以下の URL を入力して「Add」ボタンを押すとインストールできます。

https://github.com/gigacreation/Csv2CollectionsForUnity.git?path=Assets/Csv2Collections

手動でインストールする方法

GitHub のリポジトリからソースコードをダウンロードし、その中の Assets/Csv2Collections/ ディレクトリを導入したい Unity プロジェクトにコピーします。

Unity 向けのサービスロケーターを公開しました

はじめに

このたび、Unity でサービスロケーターを使用できるようにするパッケージを公開いたしました! 🚀

github.com

今回はこのパッケージの紹介を、サービスロケーター自体の紹介も合わせつつ、させていただければと思います。

概要

【サービスロケーター】とは、

  • どこからでもアクセスできる格納場所(サービスコンテナ)を一つだけ用意し、
  • その格納場所に任意のクラス(サービス)を登録することで、
  • 他のクラスから格納場所に登録されたクラスへどこからでもアクセスできるようにする

というデザインパターンです。

例えば、ゲームデータのセーブ・ロードを司るクラスの SaveLoadService があったとして、それをあらかじめサービスコンテナに登録しておけば、他の様々なクラスからサービスコンテナを介し SaveLoadService へアクセスしてセーブやロードが行えるようになります。

これと似たようなことを実現できるものとして、【シングルトン】【DI (Dependency Injection)】パターンというのもありますが、

  • シングルトンだと SaveLoadService などの各サービスに他の各クラスが直接アクセスする形になり、サービスの数が増えていくと依存関係がスパゲッティ化してしまう
  • DI はサービスロケーターよりも依存関係を整理できるが、仕組みがやや難解で学習コストが高い

といった特徴があり、サービスロケーターはその中間的な立ち位置で使いやすさと実用性を兼ね揃えたデザインパターンと言えます。

インストール

それでは、パッケージのインストール方法から説明いたします。

Package Manager を使用する方法

導入したい Unity プロジェクトで Package Manager を開き、「+」ボタンを押して「Add package from git URL...」を選択、以下の URL を入力して「Add」ボタンを押すとインストールできます。

https://github.com/gigacreation/ServiceLocatorForUnity.git?path=Assets/Service

手動でインストールする方法

GitHub のリポジトリからソースコードをダウンロードし、その中の Assets/Service/ ディレクトリを導入したい Unity プロジェクトにコピーします。

使い方

基本の使い方

1. サービスコンテナへ登録したいクラスに IService を実装します。

using GigaCreation.Tools.Service;
using UnityEngine;

public class SampleService : IService
{
    public void Bark()
    {
        Debug.Log("Bowwow!");
    }
}

2. 次に、そのクラスをサービスコンテナに登録します。*1 *2

using GigaCreation.Tools.Service;
using UnityEngine;

public class RegisterServiceSample : MonoBehaviour
{
    private SampleService _sampleService;

    private void Awake()
    {
        // サービスを生成し……
        _sampleService = new SampleService();

        // サービスコンテナに登録する
        ServiceLocator.Register(_sampleService);
    }

    private void OnDestroy()
    {
        // GameObject が破棄されたら、サービスコンテナへの登録を解除する
        ServiceLocator.Unregister(_sampleService);
    }
}

3. これで、どこからでもアクセスできるようになりました!

using GigaCreation.Tools.Service;
using UnityEngine;

public class UseServiceSample : MonoBehaviour
{
    private void Start()
    {
        // 型を指定してサービスを取得できる
        var sampleService = ServiceLocator.Get<SampleService>();

        sampleService.Bark();
    }
}

IDisposable とセットで使う

サービスコンテナに登録されたクラスが IDisposable を実装していた場合、登録解除時に Dispose() が呼ばれます。

using System;
using GigaCreation.Tools.Service;
using UnityEngine;

public class SampleService : IService, IDisposable
{
    public void Bark()
    {
        Debug.Log("Bowwow!");
    }

    public void Dispose()
    {
        // 登録解除時に呼ばれる
        Debug.Log("SampleService disposed.");
    }
}

自作のインターフェイスとセットで使う

クラスをサービスコンテナへ登録する際、クラスを直接登録するのではなく、インターフェイスを介して登録することもできます。これにより、他から呼ばれるメソッドを限定することが可能です。

using GigaCreation.Tools.Service;
using UnityEngine;

public class SampleService : ISampleService
{
    public void Bark()
    {
        Debug.Log("Meow!");
    }

    public void Scratch()
    {
        // このメソッドは public であるが、ISampleService を介してアクセスすることはできない
        Debug.Log("Ouch!");
    }
}

public interface ISampleService : IService
{
    void Bark();
}

こんな形でサービスに自作のインターフェイスを実装し、

// 登録
// 左辺は ISampleService であることに注意
ISampleService sampleService = new SampleService();
ServiceLocator.Register(sampleService);

// アクセス
// ISampleService を指定して取得する
var sampleService = ServiceLocator.Get<ISampleService>();
sampleService.Bark();
sampleService.Scratch(); // <- これはエラー!

という感じでインターフェイスを介して登録やアクセスができます。*3

インターフェイスをさらに細分化して、各クラスで使用できるメソッドを個別に限定することなども可能です(SaveLoadService で、あるクラスはセーブだけ、あるクラスはロードだけできるようにする、みたいな)。

API リファレンス

一応リファレンスも用意していて、英語にはなりますが こちら で読むことができます。

補遺

シングルトンとの使い分け

「あるサービスへどこからでもアクセスしたい」という用途に関して言えば、シングルトンとサービスロケーターは使用感が結構似ていて、特にサービスの数が 1 つだけの場合、サービスコンテナを介すか介さないかの違いくらいしかありません。

サービスが 1 つの場合
あまりサービスロケーターのメリットが無さそうに見える

ごく小規模なミニゲームを作るときなどで、サービスの数が 1 つ程度で済むことが確定している場合は、シングルトンを使ってもそれほど大きな問題にはならないかと思います。

ただまぁ、ゲーム開発は得てして「あれも追加しよう、これも追加しよう」となりがちなので、後々の拡張性を考えれば、最初からサービスロケーターを使うのをオススメしたいですね。サービスの数が増えていけばいくほど、サービスロケーターで依存関係を整理できるのが活きてくるでしょう。

サービスが 3 つの場合
サービスロケーターならスッキリ!

先述の「自作のインターフェイスとセットで使う」をやれば、例えば同じインターフェイスを実装したデバッグ用のサービスを別で作り、デバッグモードの時はそちらを登録するようにすれば、使用者側のほうは何もせずともデバッグ用のコードを実行できる、といったことも実現可能です。

登録や破棄のタイミングも、サービスロケーターなら自由に変更できますしね(シングルトンだと基底クラスを変更したりしないといけないのでちょっと面倒)。

サービスロケーターはアンチパターンなのか?

DI パターンを説明する文脈では、サービスロケーターはアンチパターンとして紹介されることが多いようです。

実際これは一理あって、サービスロケーターは依存関係をスッキリさせられるとはいえ、結局サービスコンテナというグローバルな存在に依存することになりますし、コンテナにアクセスできるクラスは自動的に、コンテナに登録されているすべてのサービスにアクセスできてしまいます。例えば前項の図だと、Class B は SaveLoadService にだけアクセスできれば良いのに、すべてのサービスへのアクセス権を得られてしまう……ちょっと微妙ですよね。

ですので、必要なクラスに必要な分だけ依存性を注入する DI パターンのほうが、よりスマートだと言えそうです。Unity 開発であれば、 VContainer という素晴らしい DI フレームワークが存在していますので、もし DI を使いたいという場合はぜひ一度目を通すことをオススメします。DI とは何か? というところから、日本語で詳細に説明してくれています。

一方、DI の難点としては、やはり学習コストが高いことが挙げられると思います。VContainer のマニュアルを見ていただければ分かりますが、結構覚えないといけないことが多く、サービスロケーターやシングルトンに比べて導入はかなり難しめです。

サービスロケーターはグローバルなコンテナに依存するという点がマイナスポイントですが、各サービスはローカルな存在のまま保たれるので、十分実用に耐えられるデザインパターンだと個人的には考えています。シングルトンだとサービス自体がグローバルな存在になってしまうので、個人的にはデメリットのほうが大きいかなという印象です。

まとめると、サービスロケーターは手軽さと実用性を兼ね揃えた良いデザインパターンですので、シングルトンを使うくらいならぜひこちらを使っていってほしいと思います。より堅牢な設計を求めるのであれば、DI の導入を検討したい、といった感じですね。

*1:ここでは MonoBehaviour の Awake() で登録していますが、別に他のタイミングで登録しても良いですし、MonoBehaviour ではない Pure C# クラスで登録してももちろん大丈夫です

*2:OnDestroy() で破棄することで、そのシーン内でのみサービスが有効になります。これを無くすとシーンを切り替えたりしてもサービスが生き続けるので、シーン間のデータのやり取りが可能になります

*3: (sampleService as SampleService).Scratch() って書いたら無理やりクラスへアクセスできたりもしますが……

Rainbow Folders と一緒に使える、Unity 用のフォルダアイコンを公開しました

概要

Unity プロジェクトに Rainbow Folders を導入すると、Project View のフォルダアイコンがフォルダ名に応じたものになり、視認性が向上します。

このたび、その Rainbow Folders のフォルダアイコンのデザインに沿って作成した、自作のアイコンをいくつか公開しました。

github.com

Rainbow Folders と一緒に使うのがオススメですが、エディター拡張を書けばこれ単体でも使用できます。

ダークテーマ

https://user-images.githubusercontent.com/5264444/219367565-2cdbfc53-aeaa-4d91-9966-de6e5877494a.png

ライトテーマ

https://user-images.githubusercontent.com/5264444/219367571-7f0158f9-cdca-4e82-90f5-e450c82c2c2c.png

Addressables.DownloadDependenciesAsync() によるダウンロードを、アセットバンドルごとに分けて行う

はじめに

Addressables のダウンロードを行う場合、普通にコードを書くと以下のようになります。

// ※ラベル名を指定してダウンロードする場合

// Addressable アセットのラベル名
string labelName = "LABEL_NAME";

// 指定したラベルを持つすべての Addressable アセットの IResourceLocation を取得する
IList<IResourceLocation> locations = await Addressables.LoadResourceLocationsAsync(labelName);

// 取得した IResourceLocation が含まれるアセットバンドルをすべてダウンロードする
await Addressables.DownloadDependenciesAsync(locations);

この場合、ダウンロードするアセットバンドルが複数あっても一度にダウンロードを行うため、以下のような問題が起こります。

  • 現在ダウンロード中のアセットバンドルの情報が取得できない。
    • 例えば、「全体のうち何 % のダウンロードが完了したか」は取得できるが、「今ダウンロードしているアセットバンドルのうち何 % のダウンロードが完了したか」は分からない。
  • サーバーの設定によっては、ダウンロードに時間がかかりすぎるとサーバー側から通信を打ち切られる場合がある。

アセットバンドルごとにダウンロードする

こうした問題を防ぐため、各アセットバンドルごとに別々にダウンロードを行うようにします。

これは、以下のようなコードで実現できます。

// Addressable アセットのラベル名
string labelName = "LABEL_NAME";

// 指定したラベルを持つすべての Addressable アセットの IResourceLocation を取得する
IList<IResourceLocation> locations = await Addressables.LoadResourceLocationsAsync(labelName);

// DependencyHashCode でグルーピングする
foreach (IGrouping<int, IResourceLocation> groupedLocations in locations.GroupBy(x => x.DependencyHashCode))
{
    // グループごとに IResourceLocation のダウンロードを行う
    await Addressables.DownloadDependenciesAsync(groupedLocations.ToList());
}

解説

IResourceLocation は、 DependencyHashCode というパラメータを持ちます。

これは他の IResourceLocation との依存関係を示すハッシュコードなのですが、依存関係が同じ=所属しているアセットバンドルが同じなので、この値をもとにグループ分けすれば、アセットバンドルごとに IResourceLocation を分けることができます。

あとは、分けたグループごとに Addressables.DownloadDependenciesAsync() を実行してやれば OK です。

補遺

これを応用すると、以下のようなダウンロード画面を実装できます。

f:id:Gigacee:20211228195551g:plain
プログレスバー 2 本のダウンロード画面。上:全体の進行度、下:アセットバンドルごとの進行度

Addressables.UpdateCatalogs() でカタログのアップデートに失敗しても try ~ catch をすり抜ける問題

発生した問題

サーバーに、新しいコンテンツカタログの hash だけあり、json は無いという状況で Addressables.UpdateCatalogs() を実行すると、エラーは発生するもののなぜか try ~ catch をすり抜けてしまう。

try
{
    // これがエラーになっても……
    await Addressables.UpdateCatalogs();
}
catch
{
    // ここに到達しない
    Debug.LogError("コンテンツカタログのアップデートに失敗しました。");
}

解決方法

アップデートに失敗した場合、Result の List<IResourceLocator> が空なので、以下のようにリストの中身の有無で成功したかどうかを判断する。

// カタログをアップデートする
List<IResourceLocator> locators = await Addressables.UpdateCatalogs();

// リストが空なら失敗扱いにする
if (!locators.Any())
{
    Debug.LogError("コンテンツカタログのアップデートに失敗しました。");
}

説明

新しいコンテンツカタログのハッシュが見つかった時点で、 Addressables.UpdateCatalogs() は成功扱いになるようです。返ってくる AsyncOperationHandle の Status も Succeeded になります。なぜ……。

AsyncOperationHandle の Result にはアップデートされたカタログの IResourceLocator のリストが入るのですが、実際にはカタログのアップデートには失敗しているので、空のリストが入ってきます。なのでこのリストの中身を見れば、本当にカタログがアップデートできたのかが判断できます。

補遺

Addressables.UpdateCatalogs(true) というように第一引数に true を指定すると、カタログのアップデート後に使われなくなったアセットバンドルのキャッシュの削除を行ってくれます。*1

通常、カタログのアップデートが行われないとキャッシュの削除も行われないのですが、今回のように実際は失敗していても内部的には成功扱いになってしまっている場合、キャッシュの削除処理も走ってしまい、以下のエラーが発生します。

OperationException : UnityEngine.AddressableAssets.CleanBundleCacheOperation, result='', status='Failed', status=Failed, result=False catalogs updated, but failed to clean bundle cache.

現状、これを回避する方法は無さそうなので、以下のようにアップデートが本当に成功したかどうかを確認してから別途キャッシュクリアを行うようにするのが良いでしょう。

// カタログをアップデートする
List<IResourceLocator> locators = await Addressables.UpdateCatalogs();

// リストが空なら失敗扱いにする
if (!locators.Any())
{
    Debug.LogError("コンテンツカタログのアップデートに失敗しました。");

    // 処理を打ち止め
    return false;
}

// キャッシュをクリアする
Addressables.CleanBundleCache();

*1:バージョン 1.19.4 以降。

The coloring of this site is Dracula PRO🧛🏻‍♂️
This website uses the FontAwesome icons licensed under CC BY 4.0.

2020 GIGA CREATION