TL;DR
動的にロードされるアセットだけが含まれるフォルダー(ResourcesやStreamingAssets)を対象とした場合、AssetPostprocessor
やPackage Managerのイベントが利用できる可能性がある。
条件が厳しいのはUPMにアセットを含める関係上、元のGUIDを維持できないため。更に細かい挙動が面倒くさく、正直使い勝手が良いとは言えない。Addressablesが使える場面ではAddressables、または大人しくシンボリックリンクなどを使ったほうが事故が少ないかもしれない。
今回は特殊フォルダーを共有するというテーマだが、他にもパッケージをインストールする際になにか処理を加えたい場合には応用できそう。
目次
開発環境
- Unity 2022.3.23f1
AssetPostprocessor
AssetPostprocessor
を利用するとプロジェクトのアセットに変更が加わった際にスクリプトを実行することができるようになる。
これはAssets
配下のみならず、Packages
配下にも有効。
基本的な構造は以下の通り。
public class Postprocessor : AssetPostprocessor { static void OnPostprocessAllAssets( string[] importedAssets, string[] deletedAssets, string[] movedAssets, string[] movedFromAssetPaths) { // 新規作成や更新されたAssetは imported foreach (var asset in importedAssets) Debug.Log("imported:" + asset); // 削除されたら deleted foreach (var asset in deletedAssets) Debug.Log("deleted:" + asset); // 移動されたら movedFrom から moved へ foreach (var asset in movedAssets) Debug.Log("moved:" + asset); foreach (var asset in movedFromAssetPaths) Debug.Log("movedFromAssetPaths:" + asset); } }
OnPostprocessAllAssets
はすべてのアセットのインポートが完了した後に呼び出される。
他にOnPostprocessAnimation
やOnPostprocessAudio
など、アセットの種類毎に呼び出されるメソッドも存在する。
パラメータ | 内容 |
---|---|
importedAssets | 新たに追加されたり既存のアセットが更新されたりしたものが入ってくる |
deletedAssets | 削除されたもの |
movedAssets | 移動されたもの |
movedFromAssetPaths | 上記の移動元、移動先&移動元は対になる |
特殊フォルダーを共有する場合
注意する点は以下の通り。
- UPMで共有した場合、Assets配下にコピーすると元のアセット(Packages配下)と同じものが2重で存在することになる
File.Copy
してもmetaファイルのGUIDは新しい値に更新される- GUIDが維持できないため、GUIDを参照するようなものはAddressablesかシンボリックリンクにするのが良さそう
- インストールするパッケージに同梱した場合、実行順序が定まらないので
AssetPostprocessor
が認識される前にインポートされたアセットはイベントをフックできない - 何かしらエラーでコケた場合、最初からや続きからの再実行ができない
- アセットの変更に随時反応するのでエディタが重くなる
以上を踏まえサンプルは以下の仕様で実装されている。
- 動的にロードされるアセットが含まれるフォルダーを対象とする
- Resources
- StreamingAssets
- アセットに変更があった場合は、ディレクトリ単位で洗い替える
- 再実行可能なようにEditor拡張などを用意しておく
- 総じて使い勝手は微妙
サンプルコード
public class Importer : AssetPostprocessor { private static readonly string packageName = "com.xxx.yyy"; private static readonly string packageRootDir = $"Packages/{packageName}"; private static readonly Dictionary<string, string> targets = new() { //{ $"{packageRootDir}/Plugins", $"Assets/Plugins/{packageName}" }, { $"{packageRootDir}/StreamingAssets", $"Assets/StreamingAssets/{packageName}" }, { $"{packageRootDir}/Resources", $"Assets/Resources/{packageName}" }, }; static void OnPostprocessAllAssets( string[] importedAssets, string[] deletedAssets, string[] movedAssets, string[] movedFromAssetPaths) { var copied = new List<string>(); Action<string> replace = path => { if (!path.StartsWith(packageRootDir)) return; var kvp = targets.FirstOrDefault(x => path.StartsWith(x.Key)); if (kvp.Key != null && !copied.Contains(kvp.Key)) { ReplaceDirectory(kvp.Key, kvp.Value, true); copied.Add(kvp.Key); AssetDatabase.Refresh(); } }; foreach (var asset in importedAssets) replace(asset); foreach (var asset in deletedAssets) replace(asset); foreach (var asset in movedAssets) replace(asset); } static void ReplaceDirectory(string sourceDir, string destinationDir, bool recursive) { DeleteDirectory(destinationDir); CopyDirectory(sourceDir, destinationDir, recursive); } static void DeleteDirectory(string path) { var dir = new DirectoryInfo(path); if (dir.Exists) dir.Delete(true); } static void CopyDirectory(string sourceDir, string destinationDir, bool recursive) { // Get information about the source directory var dir = new DirectoryInfo(sourceDir); // Check if the source directory exists if (!dir.Exists) throw new DirectoryNotFoundException($"Source directory not found: {dir.FullName}"); // Cache directories before we start copying DirectoryInfo[] dirs = dir.GetDirectories(); // Create the destination directory Directory.CreateDirectory(destinationDir); // Get the files in the source directory and copy to the destination directory foreach (FileInfo file in dir.GetFiles()) { string targetFilePath = Path.Combine(destinationDir, file.Name); file.CopyTo(targetFilePath); } // If recursive and copying subdirectories, recursively call this method if (recursive) { foreach (DirectoryInfo subDir in dirs) { string newDestinationDir = Path.Combine(destinationDir, subDir.Name); CopyDirectory(subDir.FullName, newDestinationDir, true); } } } public static void CopyDirectoryAll() { foreach (var kvp in targets) { if (Directory.Exists(kvp.Key)) ReplaceDirectory(kvp.Key, kvp.Value, true); } AssetDatabase.Refresh(); } }
エディタ拡張。
public class ReplaceResourceFolder : Editor { [MenuItem("PackageSample/ReplaceResourceFolder ")] static void Execute() { Importer.CopyDirectoryAll(); } }
Package Manager イベント
Package Manager では、Events
クラスにregisteringPackages
とregisteredPackages
の2つのイベントが用意されている。
ノート: Package Manager はパッケージ内のストリーミングアセットをサポートしていません。代わりに Addressable パッケージをご利用ください。
registeringPackages
はPackage Managerが依存関係のリストを変更する前に発生する。
基本的な構造は、Events.registeringPackages
にイベントハンドラーを登録するだけ。
public class EventSubscribingExample_RegisteringPackages { public EventSubscribingExample_RegisteringPackages() { Events.registeringPackages += RegisteringPackagesEventHandler; } void RegisteringPackagesEventHandler(PackageRegistrationEventArgs packageRegistrationEventArgs) { Debug.Log("The list of registered packages is about to change!"); // Install時 foreach (var asset in packageRegistrationEventArgs.added) Debug.Log("added:" + asset.name); // Remove時 foreach (var asset in packageRegistrationEventArgs.removed) Debug.Log("removed:" + asset.name); // バージョン変更時(Up/Down)、FromとToがペアになる foreach (var asset in packageRegistrationEventArgs.changedFrom) Debug.Log("changedFrom:" + asset.name); foreach (var asset in packageRegistrationEventArgs.changedTo) Debug.Log("changedTo:" + asset.name); } }
registeredPackages
はインポートが終わってコンパイルされた後に発生する。
こちらはInitializeOnLoadMethod
属性が必要になる。
public class EventSubscribingExample_RegisteredPackages { // イベントをサブスクライブするには '[InitializeOnLoadMethod]' か '[InitializeOnLoad]' を使う必要があります [InitializeOnLoadMethod] static void SubscribeToEvent() { Events.registeredPackages += RegisteredPackagesEventHandler; } static void RegisteredPackagesEventHandler(PackageRegistrationEventArgs packageRegistrationEventArgs) { // ここで実行されるコードは、エディターが一連の新しいパッケージのコンパイルを完了したと安全に想定できます。 Debug.Log("The list of registered packages has changed!"); // ローカルのパッケージの中身が更新されただけだと呼ばれない // Install時 foreach (var asset in packageRegistrationEventArgs.added) Debug.Log("added:" + asset.name); // Remove時 foreach (var asset in packageRegistrationEventArgs.removed) Debug.Log("removed:" + asset.name); // バージョン変更時(Up/Down)、FromとToがペアになる foreach (var asset in packageRegistrationEventArgs.changedFrom) Debug.Log("changedFrom:" + asset.name); foreach (var asset in packageRegistrationEventArgs.changedTo) Debug.Log("changedTo:" + asset.name); } }
PackageRegistrationEventArgsのプロパティ | 内容 |
---|---|
added | Package ManagerでInstallした時 |
removed | Removeした時 |
changedFrom | バージョンが変更(Up/Down)した時の前のパッケージ情報 |
changedTo | 上記の変更後のパッケージ情報、前後は対で提供される |
特殊フォルダーを共有する場合
基本的にパッケージレベルの話なので、アセット(ディレクトリ)毎にどうのこうのの話ではなさそう。
注意する点は以下の通り。
- UPMのローカルパッケージ参照の場合、パッケージのアップデートをしなくても中身は更新されるが、そのような状況ではイベントは発生しない
- そのため使い勝手がだいぶ悪い
- インストールするパッケージに同梱した場合、
registeredPackages
であればインストールの最後に走るので誤爆はなさそう - パッケージをいじらなければ実行されないのでエディタが重いということはない
- その他GUIDやエラーでコケた場合などは
AssetPostprocessor
と同様 AssetPostprocessor
と一緒に覚えておくと何かに使えるか
シンボリックリンクを使用する
PluginsのようにプロジェクトのSerializeFieldなどでGUIDを参照してしまうようなものは、これまでの方法は使えない。 そもそもローカルパッケージを参照するために、ディレクトリの相対パスを持っているはずなので、同様にシンボリックリンクを貼ってしまう方が楽かもしれない。
Windowsの場合。
mklink /D destDir srcDir
その他参考。
まとめ
UPMのローカルパッケージ参照で特殊フォルダーを共有する方法を探ってきた。
GUIDを必要としない、動的にロードされるアセットだけが含まれるフォルダー(ResourcesやStreamingAssets)を対象とした場合に限り、AssetPostprocessor
やPackage Managerのイベントが利用できる可能性がある。
正直使い勝手が良いとは言えないため、Addressablesが使える場面ではAddressables、または大人しくシンボリックリンクなどを使ったほうが事故が少ないかもしれない。
今回は特殊フォルダーを共有するというテーマだったが、他にもパッケージをインストールする際になにか処理を加えたい場合には応用できそうである。