API を呼び出せる、あるいは呼び出すべき条件が不明確な場合があります。このような情報を容易に入手できない場合、しばしば意図しない副作用のあるコードを書くことになります。
Unity エディターのユースケースが増えれば増えるほど、API の使われ方も多様になっていきます。初期化されていないシステムを API が直接利用しようとすると、不安定になる可能性があります。シーンを開いたまま削除したり、インポートの途中でアセットをロードしようとすると、結果に一貫性がなくなり、最悪の場合クラッシュすることもあります。
アセットをエディター内で操作する際のメッセージングを改善する一環として、私たちは特定の API を特定の条件下で呼び出すことを制限することを始めました。この取り組みにより、修正困難なバグ、一貫性のない結果、整理されていないワークフローが習慣化するのを防ぐことができます。
新しい内部属性を作り、Unity エディターに同梱されている API に追加しました。この属性は、エディターのコードをコンパイルする際に、C# と C++ の間のネイティブバインディングレイヤーにコードを自動生成します。
[PreventExecutionInState]
具体的には、次のようなことを実現するために、このソリューションを設計しました。
例えば、AssetDatabase.bindings.cs 内の AssetDatabase.CreateAsset は以下のように修飾されています。
[PreventExecutionInState
(
AssetDatabasePreventExecution.kGatheringDependencies
FromSourceFile, PreventExecutionSeverity.PreventExecution_ManagedException,
"Assets may not be created during gathering of import dependencies"
)]
コールバック GatherDependenciesFromSourceFile の間に AssetDatabase.CreateAsset を呼び出してアセットを作成することはできません。これは、動的な依存関係のみが AssetDatabase に返されることになっていて、インポートキューのどこにアセットをインポートすべきかを決定できるためです。この属性を配置しておくと、Unity エディターに同梱されているコード生成を駆動するために使用できます。
Unity エディターのソースコード(C# と C++ の両方)をコンパイルすると、すべての C# の関数を定義する C++ のコードレイヤーが自動生成されます。これらの関数は .bindings.cs ファイル内部で「extern」としてマークされている一方で、生成された C++ ファイルは自動生成されたファイルであることが明確にマークされています。
この制御を API に追加できるようにするため、新しい C# 属性を考慮し、C++ 側で実行可能なものに変換できるように Unity のコード自動生成ツールは拡張されました。これは、API を使うべきでないときに使ってしまった場合に備えて、API の使い方についてより良い情報を提供するためのレイヤーとして機能します。
C# コード(AssetDatabase.bindings.cs)
[NativeThrows]
[PreventExecutionInState(
AssetDatabasePreventExecution.kGatheringDependenciesFromSourceFile,
PreventExecutionSeverity.PreventExecution_ManagedException,
"Assets may not be created during gathering of import dependencies"
)]
extern public static void CreateAsset([NotNull] Object asset, string path);
自動生成されたコードはスタンドアロン C# ツールを介して書かれており、PreventExecutionInStateAttribute に対して、以下のように実行されます。
private string GetNameFromCustomAttribute(IBindingsPreventExecution curCheck)
{
var name = curCheck.someObject.GetType().GetProperty("Type").GetValue(curCheck.someObject).ToString();
//Generally speaking, the name will look like UnityEditor.AssetDatabasePreventExecution,
//However, we could have an enum inside a class which could take the form of:
//UnityEditor.MyClass/MyEnum and we need to make sure we can
//extract the "MyEnum" portion here by handling that case
var splitName = name.Split('.');
var lastEntry = splitName[splitName.Length - 1];
int start = lastEntry.LastIndexOf("/") + 1;
var fixedName = lastEntry.Substring(start, lastEntry.Length - start);
return fixedName;
}
//Getting the value is more straightforward, as we expect it to always be an integer
//and more specifically a power or two value since we must be able to report
//which condition failed, so having a single bit per condition is required
var flag = curCheck.someObject.GetType().GetProperty("Value").GetValue(curCheck.someObject);
int flagValue = int.Parse(flag.ToString());
さらに、関数の上に PreventExecutionInStateAttribute のインスタンスが複数ある場合、これらの API を呼び出す際にパフォーマンスへの影響をできるだけ少なくするために、それらの条件は単一のチェックに統合されます。
すべての情報を入手したら、そこから等価な C++ コードを生成する必要があります。自動生成された C++ コードの各部分は、C# の元の属性定義に遡ることができます(つまり、メソッド CreateAsset は、以下の自動生成 C++ コードの文字列「CreateAsset」 にマッピングされています)。
最後に、すべての情報が抽出されると、変数、名前空間、および PreventExecutionInStateAttribute からの命名を使用するクラスとともに、自動生成コードが作成されます。
自動生成された C++ コード
if(g_AssetDatabasePreventExecutionBitField & AssetDatabasePreventExecutionChecks::kGatheringDependenciesFromSourceFile)
AssetDatabasePreventExecution::ReportExecutionPrevention(
"CreateAsset",
AssetDatabasePreventExecutionChecks::kGatheringDependenciesFromSourceFile,
PreventExecutionSeverity::PreventExecution_ManagedException,
"Assets may not be created during gathering of import dependencies",
&exception
);
その後、次のようになります。
この結果、API に対応する C++ コードは、想定外の時に実行されることはなく、状況を解決するのに役立つであろう妥当なエラーを提供するようになります。
また、g_AssetDatabasePreventExecutionBitField の名前に AssetDatabasePreventExecution が含まれていることに気づかれたかもしれません。これは、Unity 内の他のシステムでも同様に API 保護を定義し、望ましくない API 使用を前もって防ぐことができるようにするためのものです。
この API の挙動に対する変更を確実にキャッチするために、私たちは AssetDatabase Test Suite に多数のテストを追加しています。GatherDependenciesFromSourceFile の呼び出しの中から CreateAsset が呼び出されるたびに、マネージド例外がスローされることが予想されます。
その結果、このシナリオ(のみならず、他のさまざまなシナリオ)に対するテストカバレッジが確保され、ひいては、仕組みがあるべき姿で動くことへの信頼が高まったのです。
API 保護の変更を取り込むことは、挙動の変更とみなすことができます。そのため、強いメッセージングを行うように配慮しています。しかし、現在直面している最大の課題は、さまざまなアセットストアパッケージ、ユーザープロジェクト、ライブラリ、API サンプルなどが API に依存しているため、これが私たちのコミュニティにどのような影響を与えるかということです。まず、変更を行う際に内部でどれだけのテストが失敗しているかを調べ、その情報を使って API 保護の変更の影響の大きさを測ります(実際には、あまりにも多くのシステムが現在の動作に依存している場合 、変更を行わないことを検討する可能性もあることに注意してください)。
幸い、GatherDependenciesFromSourceFile は比較的新しいコールバックで、ScriptedImporter でこれを使用しているものは多くありません。しかし、OnPostProcessAllAssets のようなものが AssetDatabase.FindAssets の呼び出しに制限を受けるとしたら(実際にこれを行う予定はないのでご安心ください)、OnPostProcessAllAssets 内から AssetDatabase.FindAssets を呼ぶことはよくあるので、内部のテストやユーザーから多くの反発を受けることになるでしょう。
この記事について議論したい、または記事を読んだ後にアイデアを共有したい場合は、私たちの Asset Database フォーラムのディスカッションスレッドをご覧ください。Twitter で、私のアカウント @jav_dev をぜひフォローしてください。Tech from the Trenches シリーズにはこれから Unity の開発者による技術ブログが続々と追加されるので、こちらもチェックしてください。
Is this article helpful for you?
Thank you for your feedback!