はじめに
Unityでパッケージの開発をしていると、プロジェクトにDefine Symbol(#if xxxxxのxxxxx部分の独自定義)を付与して処理を切り替えたりしたい場面があるかと思われます。
また、このような運用をすると、そのパッケージがない場合はDefine Symbolを除去しないと「そんなものはない」とコンパイラに怒られることもありますよね。そんな時に手動で該当するDefine Symbolを除去するのも手間になるかと思われます。さらに利用者にとっても負荷になることはまず間違いなさそうです。
せっかくなので、すべてが自分自身で完結して、簡単に使えるパッケージにしたいですよね。
動作を確認した環境
-
OS
Windows 11 24H2 -
Unity
2021.3.45f1 / 2022.3.62f1 / 6000.0.54f1 / 6000.1.13f1 -
Api Compatibility Level
.NET Standard 2.1
実装について
定数値について
以下のソースコード中のPackageSymbolName、PackageNameおよびPackageInfoGuidの定数値は適用するパッケージにあわせて適宜読み替え・変更をお願いします。
| 定数 | 割り当てる値 |
|---|---|
PackageSymbolName |
パッケージ導入時のみ有効にしたいDefine Symbol |
PackageName |
パッケージ名(package.jsonのnameフィールドの値) |
PackageInfoGuid |
package.jsonのアセットGUID(package.json.metaから確認できる) |
コード全体
コード全体
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using UnityEditor;
using UnityEditor.Build;
using UnityEditor.Compilation;
// プロジェクト設定にプロジェクト用のDefine Symbolを追加するクラス
internal static class PackageSymbol
{
// 自身のパッケージが存在する時に定義されるシンボル
private const string PackageSymbolName = "YOUR_PROJECT_S_DEFINE_SYMBOL";
// 自身のパッケージ名
private const string PackageName = "com.your.package_name";
// 自身のパッケージ情報のアセットのGUID
private const string PackageInfoGuid = "93638273717363513223817441824331";
// Define Symbolを処理する
[InitializeOnLoadMethod]
private static void ProcessSymbol()
{
// 再コンパイル直前にDefine Symbol除去判定が実行されるようにする
CompilationPipeline.compilationStarted += RemoveSymbol;
// Define Symbolを追加する
AddSymbol();
}
// Define Symbolを追加する
private static void AddSymbol()
{
// スタンドアロン用のDefine Symbolを取得する
var standaloneTarget = NamedBuildTarget.Standalone;
PlayerSettings.GetScriptingDefineSymbols(standaloneTarget, out var defines);
// 既に追加予定のDefine Symbolが定義されている場合は何もしない
if (defines.Contains(PackageSymbolName)) return;
// Define Symbolを追加する
var newDefines = new string[defines.Length + 1];
defines.AsSpan().CopyTo(newDefines.AsSpan(0, defines.Length));
newDefines[^1] = PackageSymbolName;
PlayerSettings.SetScriptingDefineSymbols(standaloneTarget, newDefines);
}
// Define Symbolを除去する
private static void RemoveSymbol(object obj)
{
// スタンドアロン用のDefine Symbolを取得する
var standaloneTarget = NamedBuildTarget.Standalone;
PlayerSettings.GetScriptingDefineSymbols(standaloneTarget, out var defineArray);
// 除去予定のDefine Symbolが定義されていない場合は何もしない
if (!defineArray.Contains(PackageSymbolName)) return;
// パッケージ情報ファイルの存在を確認する
var packagePath = string.Concat("Packages/", PackageName, "/package.json");
if (File.Exists(Path.GetFullPath(packagePath)) &&
AssetDatabase.AssetPathToGUID(packagePath).ToLower() == PackageInfoGuid)
{
// パッケージ情報ファイルが存在し、パッケージ情報のGUIDが想定されたものである場合は
// パッケージが存在するものとして扱い、何もしない
return;
}
// Define Symbolを除去する
List<string> defines = new(defineArray);
defines.Remove(PackageSymbolName);
PlayerSettings.SetScriptingDefineSymbols(standaloneTarget, defines.ToArray());
}
}
解説
この機能のエントリーポイント的存在
// Define Symbolを処理する
[InitializeOnLoadMethod]
private static void ProcessSymbol()
[InitializeOnLoadMethod]は、アセンブリが読み込まれた後に処理を実行するように設定するための属性です。
これを利用して、パッケージのインストール(パッケージを追加して、コンパイルが成功してアセンブリが再読み込みされる)時に処理を実行します。
定義されているDefine Symbolを取得する
// スタンドアロン用のDefine Symbolを取得する
var standaloneTarget = NamedBuildTarget.Standalone;
PlayerSettings.GetScriptingDefineSymbols(standaloneTarget, out var defines);
NamedBuildTargetから、スタンドアロン(Windows、macOSおよびLinuxのデスクトップ)向けの設定を取得し、PlayerSettings.GetScriptingDefineSymbolsに設定を渡してDefine Symbolを取得します。取得方法は「stringの配列」または「;でDefine Symnbolを接続した単体のstring」のどちらかから選択できます。今回はstringの配列で取得します。
そして、この処理で取得できる値は、設定画面でいうところのここの値です。

Define Symbolを追加して反映する
// Define Symbolを追加する
var newDefines = new string[defines.Length + 1];
defines.AsSpan().CopyTo(newDefines.AsSpan(0, defines.Length));
newDefines[^1] = PackageSymbolName;
PlayerSettings.SetScriptingDefineSymbols(standaloneTarget, newDefines);
元の配列よりも要素数が1多い配列を生成し、元の配列のデータをコピーします。
そして、最後の要素に追加するDefine Symbolを書き込んで、PlayerSettings.SetScriptingDefineSymbolsに配列を渡して設定を反映します。
パッケージ削除後にDefine Symbolが残ることによるコンパイルエラーが発生するよりも前に除去判定を実行させる
// 再コンパイル直前にDefine Symbol除去判定が実行されるようにする
CompilationPipeline.compilationStarted += RemoveSymbol;
CompilationPipeline.compilationStartedイベントに除去判定および除去処理を登録します。
このイベントはコンパイル開始時に実行されます。つまり、コンパイル結果に左右されずに何らかの処理を行うことができることを意味します。
このコールバックに渡すメソッドの型はAction<object>で、引数にはCompilationPipeline.compilationFinishedとセットで扱うためのコンテキストオブジェクトが渡されますが、今回は開始時のコールバックのみを利用するため無視します。
また、似たようなコールバックにAssemblyReloadEvents.beforeAssemblyReloadイベントがありますが、こちらはアセンブリのリロード(コンパイル後のマネージドコードを読み込む)直前に実行されるため、コンパイルが失敗した時には実行されません。
そのため、今回はCompilationPipeline.compilationStartedを選択しました。
除去時のパッケージの存在判定
// パッケージ情報ファイルの存在を確認する
var packagePath = string.Concat("Packages/", PackageName, "/package.json");
if (File.Exists(Path.GetFullPath(packagePath)) &&
AssetDatabase.AssetPathToGUID(packagePath).ToLower() == PackageInfoGuid)
{
// パッケージ情報ファイルが存在し、パッケージ情報のGUIDが想定されたものである場合は
// パッケージが存在するものとして扱い、何もしない
return;
}
パッケージ情報ファイル(package.json)は、Unity上では必ずPackages/パッケージ名/package.jsonに存在するため、string.Concat("Packages/", PackageName, "/package.json")によってパッケージ名からUnity上でのパッケージ情報ファイルのパスを構築しています。
Path.GetFullPathにUnityのアセットパスを渡すと、そのアセットのファイルの絶対パスを取得できます。これはパッケージ内のアセットに対しても有効で、たとえば、Gitからインストールしたパッケージであればキャッシュファイルの絶対パスを取得できます。
最初にC#側のファイル存在判定処理であるFile.Exists(Path.GetFullPath(packagePath))で判定するのは、コンパイル開始時に処理を行っている都合でアセットデータベースの更新がまだ行われていなく、Unityでアセットを取得できるかどうか等で判断する方法ではアセットが存在する時の結果が返されてしまうためです。
パッケージ情報ファイルが存在しなければパッケージは存在しないため、ここで判定を終了して次の処理(Define Symbolを除去する処理)に進みます。
パッケージ情報ファイルが存在する場合は、そのアセットのGUIDを判定します。GUIDが一致した場合は想定するパッケージであると判定して処理を終了し、GUIDが一致しなければ想定するパッケージではない(同じ名前のパッケージが偶然存在した)と判定して次の処理(Define Symbolを除去する処理)に進みます。
結果
-
パッケージのインストール時
- 指定されたDefine Symbolが存在しない場合はスタンドアロンのプレイヤー設定に指定されたDefine Symbolが追加される
- 指定されたDefine Symbolが存在する場合は何もしない
-
パッケージのアンインストール時
- 指定されたDefine Symbolが存在する場合
- 自身のパッケージが存在する場合は何もしない
- 自身のパッケージが存在しない場合は指定されたスタンドアロンのプレイヤー設定から指定されたDefine Symbolが除去される
- 指定されたDefine Symbolが存在しない場合は何もしない
- 指定されたDefine Symbolが存在する場合