見出し画像

VRChat SDK3のU#でエディタ拡張を使う

はじめに

この記事は「ョョョねこ Advent Calendar 2021」22日目の記事です。

近頃、インターネットチャットを騒がせている概念のひとつに「YKKのK」があります。
そして、なぜか「ョョョねこ Advent Calendar 2021」のテーマの一つにも「YKKのK」が含まれています。
これはいったい何なのでしょうか。

結論から言うと、最初4文字の「YKKの」については諸説あってよくわかりませんでしたが、最後の「」は「(VRChat SDK3のU#における)エディタ拡張」を意味することが最近の研究でわかってきました。
(日本語: editor akutyou / 英語: editor eusutensyon)

そのため、今回は「VRChat SDK3のU#でエディタ拡張を活用する」ことについて記事を書こうと思います。

注意事項

  • 記事の内容は2021/12/20現在のもので、今後の機能改修などで情報が不要になったり、使えなくなったりする可能性があります

  • 記事の内容は情報の提供のみを目的にしております。本記事の筆者や関係者は、情報を利用した結果の責任を負いません

前提知識

  • 基本的なC# / U#の仕様について理解していること

  • Unity / VRCSDK3 / 最新版U#の導入ができること

U#でエディタ拡張を使うメリット

基本的に、VRChat SDK3で作られたワールドにギミックを導入するには、Udonを利用する必要があります。

ただ、Udonには、UnityでC#を使うのに比べて動作が重かったり、C#の一部機能や関数(ジェネリクス、LINQなど)が使えないといった、制限が多いことも事実です。

今回、U#でエディタ拡張を利用すると、(VRChatワールドアップロード前に限り)Udonで本来使えない機能をワールド制作に活かすことが可能です。

データ処理の一部をエディタ拡張側で持つことで、ワールド実行時の負荷が減ったり、スクリプトの記述が楽になったりするといったメリットが発生するかもしれません。

全部が全部エディタ拡張を使えばいいというものでもありませんが、
私自身、ワールド制作をする上でエディタ拡張に助けられたこともあり、この機会に使い方を紹介できればと思います。

U#でエディタ拡張を利用する

どのような機能を作るか

今回は、(現在、Udon側で扱えない)ジェネリクスやLINQを活用しつつ、UdonSharpScript側にデータをセットするチュートリアルをできればと思います。

前準備

前提事項

下記が済んでいるものとします。

  • VRChat SDK3の導入

  • U#最新版の導入

  • UnityのSceneに「VRCWorld」プレハブを追加しての、初期設定

    • Collision MatrixとかLayerの設定を指します

Hierarchyの設定

Hierarchy

最初にシーンに「DataHolder」というオブジェクトを作ってください。
このオブジェクトにUdon Behaviorを追加し、エディタ拡張の練習をする予定です。

上記画像の「Default」ゲームオブジェクト以下にまとめられているゲームオブジェクトは、初期からシーンに存在するものなので、気にしなくても大丈夫です。まとめてもまとめなくてもいいです。

DataHolderにUdonBehaviorをアタッチ

前項で作ったゲームオブジェクト「DataHolder」にUdonBehaviorをアタッチしてください。

続けて、UdonSharpのスクリプトも作っていただければと思います。
スクリプト名は、そのまま「DataHolder」とします。

Projectの整理 (1)

Project

ここまでで、下記のファイルが生成されていると思います。

  • Sceneファイル

  • DataHolder Udon C# Program Asset

  • DataHolder.cs (中身にUdonSharpBehaviorが継承されているDataHolderクラスが存在する)

わかりやすいように自分の名前のフォルダなど作って、その下に格納しておいてください。

エディタ拡張用スクリプトの作成

Project

Assetsフォルダ直下に「Editor」という名前のフォルダを作ります。
(このフォルダ名だけ、名前が固定です。間違えないように注意してください)
その下に「Scripts」フォルダを作り、右クリック > Create > C# Script から「DataHolderExtention」という名前のスクリプトを作っておきます。

Projectの整理 (2)

Project

ここまでの手順を踏んだ後、(若干ファイル/フォルダ名に違いはあるかもしれませんが)Projectは上記画像の通りになっていると思います。

UdonSharpScript側の記述

早速「データをセットされる」対象のUdonSharpScript側の記述を行います。Projectから「DataHolder.cs」を開いてください。
やることは単純で、データをセットされる対象の配列をひとつ定義するだけです。

▼ DataHolder.cs

using UdonSharp;
using UnityEngine;
using VRC.SDKBase;
using VRC.Udon;

public class DataHolder : UdonSharpBehaviour
{
    public int[] intArray;
}

スクリプトを上記のようにして、コンパイルしてください。

Inspector > DataHolder

Inspectorから「DataHolder」を見たときに、Int Arrayフィールドが存在していればOKです。

エディタ拡張側の記述

次に「データをセットする」エディタ拡張側の記述を行います。Projectから「DataHolderExtention.cs」を開いてください。

▼ DataHolderExtention.cs

using System;
using System.Linq;
using System.Collections.Generic;
using UnityEngine;
using UnityEditor;
using VRC.Udon;
using VRC.Udon.Common.Interfaces;

[CustomEditor(typeof(DataHolder))]
public class DataHolderExtention : Editor
{
    public override void OnInspectorGUI()
    {
        base.OnInspectorGUI();

        if (GUILayout.Button("Set Data"))
        {
            Debug.Log("intArrayにデータをセットするねこョ");

            // 適当な整数値のリスト[1]を作る
            List<int> nums = GetSortedRandomNumbers();

            // データを入れる対象を最新にする
            serializedObject.Update();

            // データを入れるUdonSharp側のフィールド(=int配列)[2]を取得
            SerializedProperty intArraySO = serializedObject.FindProperty(nameof(DataHolder.intArray));

            // 取得した配列[2]をクリア
            // なお、フィールドは配列と決め打ちしている
            intArraySO.ClearArray();

            // 配列[2]のサイズを作ったリスト[1]のサイズと同じにする
            intArraySO.arraySize = nums.Count;

            // 配列[2]にリスト[1]のデータを代入する
            for (var i = 0; i < nums.Count; i++)
            {
                // 配列[2]の要素をひとつ取得
                var arrayElem = intArraySO.GetArrayElementAtIndex(i);

                // 取得した配列[2]の要素に対応するリスト[1]要素の値を代入
                arrayElem.intValue = nums[i];
            }
               
            // SerializedObjectに記録する
            serializedObject.ApplyModifiedProperties();

            Debug.Log("intArrayにデータをセットするのが終わったねこョ");
        }
    }

    // ソートされたランダムな値をリストで取得する
    // エディタ拡張スクリプト側ではLinqも使えるというサンプル
    List<int> GetSortedRandomNumbers()
    {
        var nums = new List<int>();

        for (var i = 0; i < 10; i++)
        {
            int randNum = UnityEngine.Random.Range(0, 10);
            nums.Add(randNum);
        }

        // Linqで昇順ソート
        nums = nums.OrderBy(_ => _).ToList();

        return nums;
    }
}

急に"速"が上がりましたね…。

とりあえず、スクリプトを保存しておいてください。まずは動作確認をします。

Inspector > DataHolder

ここまでの内容が上手くいっていれば、UdonBehaviorコンポーネントの一番下に「Set Data」というボタンが増えていると思います。

Inspector > DataHolder

「Set Data」ボタンを押すと、Int ArrayのSizeが10になります。
また、0 ~ 9までのソートされたランダムな値が配列の要素に入っていくと思います。

※ 配列の要素は乱数で生成されるため、必ずしも画像と同じ結果になるとは限りません。

エディタ拡張側の解説

先ほど記述したスクリプト「DataHolderExtention.cs」の内容について解説を行います。

using System;
using System.Linq;
using System.Collections.Generic;
using UnityEngine;
using UnityEditor;
using VRC.Udon;
using VRC.Udon.Common.Interfaces;

これはおまじない

[CustomEditor(typeof(DataHolder))]
public class DataHolderExtention : Editor
{
    ...
}

UdonSharp側の「DataHolder」に、このスクリプト「DataHolderExtention.cs」を関連付けるために必要な記述です。

なお「DataHolderExtention」クラスは「(UnityEditor.)Editor」を継承することに注意してください。

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

        if (GUILayout.Button("Set Data"))
        {
            // 「Set Data」ボタンが押されたときの処理
            ...
        }
    }

UdonSharp側の「DataHolder」に「Set Data」ボタンを追加するために必要な記述です。

また、if (GUILayout.Button("Set Data")) 以下の { } で囲まれている部分に記述された命令が「Set Data」ボタンが押されたときに実行される内容となります。

            Debug.Log("intArrayにデータをセットするねこョ");

            // 適当な整数値のリスト[1]を作る
            List<int> nums = GetSortedRandomNumbers();

            // データを入れる対象を最新にする
            serializedObject.Update();

            // データを入れるUdonSharp側のフィールド(=int配列)[2]を取得
            SerializedProperty intArraySO = serializedObject.FindProperty(nameof(DataHolder.intArray));

            // 取得した配列[2]をクリア
            // なお、フィールドは配列と決め打ちしている
            intArraySO.ClearArray();

            // 配列[2]のサイズを作ったリスト[1]のサイズと同じにする
            intArraySO.arraySize = nums.Count;

            // 配列[2]にリスト[1]のデータを代入する
            for (var i = 0; i < nums.Count; i++)
            {
                // 配列[2]の要素をひとつ取得
                var arrayElem = intArraySO.GetArrayElementAtIndex(i);

                // 取得した配列[2]の要素に対応するリスト[1]要素の値を代入
                arrayElem.intValue = nums[i];
            }
               
            // SerializedObjectに記録する
            serializedObject.ApplyModifiedProperties();

            Debug.Log("intArrayにデータをセットするのが終わったねこョ");

基本的に、Publicフィールドで公開した変数には、エディタ拡張側から「SerializedProperty」としてアクセスすることができます。

エディタ拡張側で「SerializedProperty intArraySO」を宣言し、この変数を通してUdonSharpBehavior側で公開している「intArray」の中身を操作します。

今回はint配列の変数を操作していますが、SerializedPropertyクラスを通すと、int変数やVector3変数など、さまざまな変数を操作できます。

操作したい型に応じてSerializedPropertyクラス内の変数にアクセスする必要があるため、詳しくはUnityのDocumentationを参照してください。

// ソートされたランダムな値をリストで取得する
// エディタ拡張スクリプト側ではLinqも使えるというサンプル
List<int> GetSortedRandomNumbers()
{
    var nums = new List<int>();

    for (var i = 0; i < 10; i++)
    {
        int randNum = UnityEngine.Random.Range(0, 10);
        nums.Add(randNum);
    }

    // Linqで昇順ソート
    nums = nums.OrderBy(_ => _).ToList();

    return nums;
}

これはコメントそのままです。
LINQで扱う関数を変えてみるなどして、(現時点でのUdonでは簡単にできない)色々なデータ処理を試してみてください。

おわりに

ということで、U# + エディタ拡張の紹介でした。

最後に少し宣伝になりますが、私はVRChatで下のツイートのようなワールドを作っています。

この動画で流れてくるオブジェクト(ノーツ)の制御は、U#とエディタ拡張を併用して行っています。
すべてワールド実行時にやっていては動作が重くなるためです。

また、楽曲に対する拍順にソートしたり、流れてくるレーンで分類したりするのがUdonの機能のみだと少々しんどいので、ワールドアップロード前にジェネリクスやLINQでデータを整えて、U#側に流しています。

最初に書いた通り、エディタ拡張を上手く使えばワールド制作において楽ができるので、もし自分のプロジェクトに合うかも…と思ったら導入してみてはいかがでしょうか。

それでは、各位いい感じに「YKKの」をやっていきましょう!

サポート(記事に対する投げ銭)いただけますと今後の創作活動・note更新の励みになります。