クラスやパッケージの有無で処理を切り替える

Unity 標準機能のパッケージ化によって、`#if UNITY_2018_4_OR_NEWER` 等、バージョンだけ見れば良いという状況から、「インストールされているか？」「バージョンは？」など、パッケージのインストール状況やバージョン、クラスの有無で処理を切り替えたい、というのは結構あるあるなシチュエーションになってきましたね。



### もくじ

- [はじめに](#はじめに)
- [Unity 2019 以降でパッケージ化されている機能限定の手法](#unity-2019-以降でパッケージ化されている機能限定の手法)
- [古い Unity や連携対象がパッケージ化されていない場合](#古い-unity-や連携対象がパッケージ化されていない場合)
    - [連携元の実装](#連携元の実装)
    - [連携機能の本体](#連携機能の本体)
    - [クラスの有無で連携機能の切り替えを行う](#クラスの有無で連携機能の切り替えを行う)
- [起きうるエラーへの対応](#起きうるエラーへの対応)
- [おわりに](#おわりに)




## はじめに

<!--
VRM Tool Kit というパッケージで、Pose Edtior というエディター拡張がプロジェクトにインストールされていれば連携する、という機能を付けるにあたっての手法です。

Pose Editor は Unity 公式の Animation Rigging（現在ベータ）が無い時に作った物なので、ゆくゆくは切り替えたい。だけど。。。という状況。
-->

方法は様々あると思いますが、ここでは、

- **エンドユーザーにファイルのコピーや削除以上の操作を求めない**
- **連携元だけで完結できる（連携される側は相手の存在を知らなくて良い）**

という点に主眼を置いています。


--


結局よくある形の実装に着地したので、なんだこの内容って感じで書き捨てていたんですが、以下に示す `Assembly Definition > Version Defines` を Unity 2019 のマニュアルで見かけて掘り起こしました。

Version Defines でまかなえない場合の手法も合わせて紹介していますよ。


<!--
元々は Pose Editor という自作のパッケージと、Animation Rigging という Unity 公式パッケージのどちらとも連携できるようにするための手法なので、紹介する方法はひと手間多い、かも。
-->


## Unity 2019 以降でパッケージ化されている機能限定の手法

Unity 2019 以降かつパッケージ化されている機能であれば、`Assembly Definition` にプリプロセッサーシンボルを設定する機能が追加されているので、その機能を利用して実現可能です。

- [Unity - Manual: Assembly definitions # Defining symbols based on project packages](https://docs.unity3d.com/2019.4/Documentation/Manual/ScriptCompilationAssemblyDefinitionFiles.html#define-symbols)


設定箇所は `Assembly Definition` のインスペクターですが、対象の機能に `Assembly Definition` が設定されていれば良いだけではなく、`package.json` が存在し `Packages/` 以下に読み込まれている必要があります。


> ※アセットストア由来のモノだとパッケージ化されていない事が殆どなので使いどころが難しいですね。







## 古い Unity や連携対象がパッケージ化されていない場合


上に示した `Assembly Definition > Version Defines` ではまかなえない場合にどうするか。

**[VRM Tool Kit](https://sator-imaging.booth.pm/items/2254383)** というパッケージで **[Pose Editor](https://sator-imaging.booth.pm/items/1381822)** と連携する機能を実現した際の手法は以下の通りです。

- VRM Tool Kit（連携元アセット）
    - `PluginManager`

- **Pose Editor Plugin**（このフォルダーがあれば連携する）
    - `PluginInitializer`
    - `PoseEditorPlugin`（連携機能の本体）

- Pose Editor
    - ※ 連携先はあっても無くても良い


--

この構成でユーザーが行うインストール作業は、

- 連携する場合は `Pose Editor` と `Plugin` をインストール
- 連携させたくない場合は `Pose Editor` および `Plugin` を削除
- それぞれ単独で使いたい場合は `Plugin` を削除

どのように使う場合でも、どんなユーザーでもミスが起きにくい状態です。


--


開発する上でも恩恵があって、連携するパッケージのバージョンによって変更が必要な部分があっても、プラグインだけを複数バージョン用意すれば良いので手間を最小限に抑えられます。

※ Unity 公式パッケージと連携する場合、プロジェクト都合で使用するバージョンが選べない状況がある。


### 連携元の実装

連携元は以下の通り。

※ Unity 依存の `Action` や `Event` ではなく `delegate` でも大丈夫。

```csharp
public static class PluginManager
{
    public static Action DoSomething;
}
```


```csharp
void DoIt()
{
    // 連携先の呼び出し、なければ何もしない
    PluginManager?.DoSomething?.Invoke();

    // UIの無効化など
    if(PluginManager?.DoSomething == null)
    {
        DisableSomething();
    }

}
```



### 連携機能の本体

みんな大好き `InitializeOnLoadMethod` / `RuntimeInitializeOnLoadMethod` で連携機能を登録。

```csharp
[InitializeOnLoadMethod]
[RuntimeInitializeOnLoadMethod]
static void Initialize()
{
    PluginManager.DoSomething = DoSomething_Impl;
}

void DoSomething_Impl()
{
    ...
}
```


### クラスの有無で連携機能の切り替えを行う

**[VRM Tool Kit](https://sator-imaging.booth.pm/items/2254383)** では考えすぎていて、実際のところ複雑な連携はしなかったのでプラグインも本体に同梱してしまっています。

↓ ↓ ↓

パッケージとして読み込まれているとは限らない、または必要に応じて `.asmdef` を削除している可能性も考慮して、手あたり次第に対象クラスを探して対象パッケージが存在すれば連携機能を登録します。


```csharp
static Type TPoseEditorManager =
    Type.GetType("PoseEditorManager") ??
    Type.GetType("PoseEditorManager, Assembly-CSharp") ??
    Type.GetType("PoseEditorManager, Assembly-CSharp-Editor") ??
    Type.GetType("PoseEditorManager, PoseEditor");

if(TPoseEditorManager)
{
    PluginManager.DoSomething = DoSomething_Impl;
}
```


#### C# の対象バージョン

Unity 2017 LTS が EOL でサポート対象外になって、現行の Unity はすべて C# 7.3 以上を採用という情勢です。

が、現行バージョンよりも古い Unity にも対応する場合、`?.`（null条件演算子）や `??`（null合体演算子）は C# バージョン６で追加された演算子なので修正する必要があります。



## 起きうるエラーへの対応

この構成で開発を進めた場合、連携元と連携プラグインだけがある状態や、プラグインだけを読み込んだ場合にエラーが出ます。

が、「プラグインを削除してください」でユーザーは対応可能なので許容しています。


> ※上で示したように文字列ベースで処理をすればエラーを防げますが、入力補完など開発環境の恩恵を受けることが出来なくなるデバフがかかってしまいます。


そもそもそういう状態になりにくい構成＆エラー対応に必要な操作もファイルの追加や削除だけなのであまり問題とはならず、また、ユーザーもフォルダー構成や名前から容易にエラーの原因を特定できるので大丈夫でしょう。

Bridge、Adapter、Provider 等々（？）界隈的には適切な命名規則があると思いますが、エンドユーザーに馴染みのある「プラグイン」としているのもそのあたりが理由です。



## おわりに

Unity に限らず C# 界隈でもクラスの有無でプリプロセッサーシンボルの定義をしたがってる人、結構いるみたいですね。調べても方法は見つかりませんでしたが。。。

C# はそもそもプリプロセッサーにあまり機能を持たせたくない、という設計思想があるようで、今後もそんな機能が実装されることは無さそうです。


Unity の `Assembly Definition > Version Defines` は必要だけど C# にその機能がない、という問題に対処するために追加された感じがしますね。こういう機能ないと困る！ という人は多かったハズ。


--


以上です。お疲れ様でした。






<!--
## 目指すもの

色々と考えすぎている感もありますが、


- どんなユーザー層でも対応できる管理方法。
    - 連携するパッケージをインストールするだけで連携機能がオンになる。削除すればオフになる。
    - 「`Scripting Define Symbols` をーー」とか「環境変数を設定するとですね」なんかは避けたい。こういうのはわからない人が意外と多い。

- 連携が必要ない時にも「使用するには○○をインストールする必要があります。」にしない。
    - 機能の継ぎ足しで大きくなった（なってしまった）パッケージだと、一部の機能しか使わないのに余計なもの入れなきゃいけない事になる。

- コチラの求める連携先のパッケージのバージョンが、プロジェクトで使っているバージョンと違っていてエラーが出たりすると厄介。

    - そういう場合も、連携機能は動かなくても良いから本体の機能は使えるようにしたい。

- Unity 公式の機能もパッケージマネージャーでの管理に移行しているので、`if UNITY_2019_4_OR_NEWER` のように本体のバージョンだけを見ても Animation Rigging がインストールされているかは分からない。
    - （UPMが自動でプリプロセッサーシンボルを設定すると見た気もするけど失念）

- 簡単な連携なら C# のリフレクションとか `SerializedObject / SerializedProperty` とか文字列ベースの処理でも良いけど、がっつり密に連携する場合は文字列指定するのはイヤ。
    - 入力補完やらなんやら開発環境の恩恵を受けたい。





### 実現するには


条件を満たす方法はプラグイン方式しかないだろうと。


連携機能をクラスやファイルとして分けるだけじゃなく、がっつりパッケージ（フォルダー）を分けてしまおう、プラグイン形式にしてしまえば良いだろうと。


#### フォルダー構成

> - **本体**（VRM Tool Kit）
> - **本体を別のパッケージと連携させるプラグイン**（Pose Editor Plug-in）
> - **連携先のパッケージ**（Pose Editor）



軽い気持ちで連携機能を付けたりした場合、連携先がきまぐれな大型アップデート、まだ v1.0 ではないので破壊的変更が入りました。なんて場合。

それに巻き込まれて本体の機能まで使えなくなることがあっても、プラグイン方式なら取り急ぎ連携用のパッケージを消せば最低限動くように。

本体部分は共通、連携部分だけ大型アップデート前と後に向けて複数用意、とかも出来る。


大部分は共通の処理で、Built-in、URP、HDRP それぞれで処理が分かれる、なんてのも気軽に対応できる。


#### 許容するエラー

プラグインは開発環境の恩恵を受けたいので、特定のパッケージがないと動かない状態で開発をすることになります。


-->