エディタ拡張でUdonBehaviourを編集しよう

## はじめに
UdonがVRChatに現れてから早いもので1年が経過しました。
近頃はUdonを書く人も大分増えて、日本界隈でも市民権を得てきているなぁ、と思う今日この頃です。
本格的にUdonを触るようになって、エディタ拡張で自動化したいシチュエーションが増えてきたのですが、VRCTriggerと違ってUdonBehaviourはserializedObjectからUdonの変数に触る事は出来ないようです。
そんなわけでエディタ拡張からUdonの変数を設定する方法を調べてみました。


## Udonの構造
UdonはUdonAssemblyProgramAssetを継承したクラス(Udonコンパイラ)によってコンパイルされます。
その際、シリアライズされた変数は型と変数名を合わせた**Symbol**という形で保存されます。
このSymbolに値は含まれておらず、値は**PublicVariables**という、変数名と紐づけされた形でUdonBehaviourに保存されます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/266653/f4dcee3b-cd68-92fa-f3b2-b5cdc7e43588.png "Udonの構造簡易図")


## Symbolの取得
SymbolはUdonコンパイラによって生成されるSerializedUdonProgramAssetが持っています。UdonBehaviourからは下記の経路でSymbolにアクセスできます。

```
using VRC.Udon;
using VRC.Udon.Common.Interfaces;

public class SymbolTable
{
    public UdonBehaviour udonBehaviour;

    public IUdonSymbolTable GetSymbolTable()
    {
        IUdonSymbolTable symbolTable = udonBehaviour.programSource.SerializedProgramAsset.RetrieveProgram().SymbolTable;

        return symbolTable;
    }
}

```
SymbolTableから下記関数を使用してSymbolの情報を取得できます。

- `ImmutableArray<string> GetExportedSymbols()`
    - 変数名の配列を返す。
- `bool HasExportedSymbole(string publicVariableSymbol)`
    - 指定した変数名がテーブルに含まれるならTrueを返す。
- `Type GetSymbolType(string exportedSymbol)`
    - 変数名からSymbolの型を返す。

## PublicVariablesの編集
PublicVariablesはUdonBehaviourのメンバ変数から取得できます。
`IUdonVariableTable publicVariables = udonBehaviour.publicVariables;`

PublicVariablesが持つ下記関数から変数の値にアクセスできます。

- `bool TryGetVariableValue(string exportedSymbol, out object variableValue)`
    - 変数名からSymbolの値をobject型で取得する。失敗したらFalseを返す。
- `bool TrySetVariableValue(string exportedSymbol, object variableValue)`
    - 変数名からSymbolの値をSetする。失敗したらFalseを返す。

## サンプルスクリプト
Scene内の全てのAudioSourceを取得してUdonBehaviourに設定するエディタ拡張です。

```
using UnityEngine;
using UnityEditor;
using VRC.Udon;
using VRC.Udon.Common.Interfaces;

public class UdonAudioSetup : MonoBehaviour
{
    public UdonBehaviour udonBehaviour;
    public string symbolName;
}

[CustomEditor(typeof(UdonAudioSetup))]
public class UdonAudioSetupEditor : Editor
{
    UdonAudioSetup udonAudioSetup;

    private void OnEnable()
    {
        udonAudioSetup = (UdonAudioSetup)target;
    }

    public override void OnInspectorGUI()
    {
        base.OnInspectorGUI();

        EditorGUILayout.Space();
        EditorGUILayout.Space();

        // Symbolを取得出来なければボタンを無効化
        using (new EditorGUI.DisabledScope(!IsSetupReady(udonAudioSetup)))
        {
            if (GUILayout.Button("Setup"))
            {
                SetupUdonBehaviour(udonAudioSetup.udonBehaviour, udonAudioSetup.symbolName);
            }
        }
    }

    private void SetupUdonBehaviour(UdonBehaviour udonBehaviour, string exportedSymbol)
    {
        // PublicVariables取得
        IUdonVariableTable publicVariables = udonBehaviour.publicVariables;

        var audioSources = GameObject.FindObjectsOfType<AudioSource>();

        // Undo用意
        Undo.RecordObject(udonBehaviour, "Modify Public Variable");
        // 値をPublicVariablesに設定
        if (!publicVariables.TrySetVariableValue(exportedSymbol, audioSources))
        {
            Debug.Log("Error! Failed Setting Public Variables.");
            return;
        }

        Debug.Log("Setup Completed.");
    }

    private bool IsSetupReady(UdonAudioSetup udonAudioSetup)
    {
        // Symbolテーブル取得
        IUdonSymbolTable symbolTable = udonAudioSetup.udonBehaviour?.programSource?.SerializedProgramAsset?.RetrieveProgram()?.SymbolTable;
        if (symbolTable == null)
            return false;

        // Symbolが存在しなければFalse
        if (!symbolTable.HasExportedSymbol(udonAudioSetup.symbolName))
            return false;

        // Symbolの型がAudioSource[]でなければFalse
        if (symbolTable.GetSymbolType(udonAudioSetup.symbolName) != typeof(AudioSource[]))
            return false;

        return true;
    }
}
```

#### 使用バージョン
- VRCSDK3-WORLD-2021.03.22.18.27_Public
- UdonSharp_v0.19.8