チカラの技術

電子工作やプログラミング

U#の闇クラスを改良しました。【生成ツール有り】

こんにちは、元気です!

本記事は前回の記事で紹介した闇クラスの改良版の解説記事になります。
(前記事は読まなくても本記事は理解できます。)
【VRChat】UdonSharpでユーザー定義クラスをNewする黒魔術【闇クラス】 - チカラの技術

また闇クラスを自動生成する生成ツールの扱い方も解説します。

闇クラスとは

VRChatのワールドギミック開発言語「UdonSharp(U#)」は制限により通常のC#のようにカスタムクラスを作成できません。 ただし技術的な黒魔術を使えば作成は可能で、それを闇クラスと呼んでいます。

//闇クラスのインスタンス作成例
var myDarkClass = MyDarkClass.New("Jane Doe", 23);

前回からの改良内容


利用する型をobjectからDataListへ変更したことにより闇クラスの配列に対して特別な書き方をする必要がなくなり大幅に扱いやすくなりました。
また要素のindex値をintのリテラル値からenum型に変更することで編集が容易になりました。

闇クラスの生成方法

① VCCから生成ツールをインポート

① nuruwoのリポジトリーリストを以下のリンクからインストールします。(VCCアプリを閉じた状態で行います)
Install nuruwo's vpm repositories



② VCCからUnityプロジェクトにインポートします。

② 生成ツールの起動

UnityのメニューからTools -> Nuruwo -> DarkClassGeneratorを選択しします。

③ 闇クラスのファイル作成

ProjectにC#スクリプト(U#スクリプトではありません)を新規作成します。ファイル名は「MyDarkClass」とします。
(U#スクリプトとして生成した場合は同時生成されたUdon Sharp Program Assetを削除してください)

④ 闇クラスの生成

図のように生成ツールに必要な情報を入力して生成します。


生成ボタンを押すと生成されたコードがクリップボードにコピーされるので③のスクリプトへ上書きペーストします。
以上で闇クラスの作成は完了です🎉

生成結果は以下の通りです。

using UnityEngine;
using VRC.SDK3.Data;

namespace Nuruwo.Dev
{
    // Enum for assigning index of field DataTokens
    enum MyDarkClassField
    {
        Name,
        Age,
        Position,
        
        Count
    }
    
    public class MyDarkClass : DataList
    {
        // Constructor
        public static MyDarkClass New(string name, int age, Vector3 position)
        {
            var data = new DataToken[(int)MyDarkClassField.Count];
            
            data[(int)MyDarkClassField.Name] = name;
            data[(int)MyDarkClassField.Age] = age;
            data[(int)MyDarkClassField.Position] = new DataToken(position);
            
            return (MyDarkClass)new DataList(data);
        }
    }
    
    public static class MyDarkClassExt
    {
        // Get methods
        public static string Name(this MyDarkClass instance)
            => (string)instance[(int)MyDarkClassField.Name];
        public static int Age(this MyDarkClass instance)
            => (int)instance[(int)MyDarkClassField.Age];
        public static Vector3 Position(this MyDarkClass instance)
            => (Vector3)instance[(int)MyDarkClassField.Position].Reference;
        
        // Set methods
        public static void Name(this MyDarkClass instance, string arg)
            => instance[(int)MyDarkClassField.Name] = arg;
        public static void Age(this MyDarkClass instance, int arg)
            => instance[(int)MyDarkClassField.Age] = arg;
        public static void Position(this MyDarkClass instance, Vector3 arg)
            => instance[(int)MyDarkClassField.Position] = new DataToken(arg);
    }
}

闇クラスの利用方法

それでは通常のU#スクリプトから闇クラスを利用します。 U#スクリプト「DarkClassTest」を作成し、以下のコードをペーストします。

using UdonSharp;
using UnityEngine;

namespace Nuruwo.Dev
{
    public class DarkClassTest : UdonSharpBehaviour
    {
        void Start()
        {
            //Make instance with constructor
            var myDarkClass = MyDarkClass.New("Jane Doe", 23, new Vector3(0.5f, 0.4f, 0.9f));

            //Get
            Debug.Log(myDarkClass.Name());      //"Jane Doe"
            Debug.Log(myDarkClass.Age());       //23
            Debug.Log(myDarkClass.Position());  //(0.50, 0.40, 0.90)

            //Set
            myDarkClass.Name("Strong Power");
            Debug.Log(myDarkClass.Name());      //"Strong Power"
        }
    }
}

適当なGameObjectにアタッチしてUnityでPlay、Debug.Logに結果が表示されれば成功です👍

闇クラス使用上の注意点

闇クラスは一般的なクラスとは異なる扱いと注意が必要です。

インスタンスのフィールド

闇クラスのインスタンスを、利用クラスのフィールド(メンバー変数)として定義すると、ClientSim実行時の初回にエラーが発生します。
(ただし致命エラーではないのでスクリプトはhaltしません。)

public class DarkClassTest : UdonSharpBehaviour
{
    //このようにフィールドに直接定義するとエラーが発生する。
    private MyDarkClass _myDarkClass;
}


対策として、以下のようにフィールドをプロパティとしてラップすればエラーを回避できます。

public class DarkClassTest : UdonSharpBehaviour
{
        //プロパティにラップ。コードからは_myDarkClassにアクセスする。
        private MyDarkClass _myDarkClass
        {
            get { return (MyDarkClass)d_myDarkClass; }
            set { d_myDarkClass = value; }
        }
        //object型のフィールドとする。d_を接頭辞とする。こちらにはアクセスしない。
        private object d_myDarkClass;
}

② 生成ツールの型認識

生成ツールは生成ボタンを押したときにプロジェクトに存在する型を自動認識します。つまりそのときに存在しない型をフィールドとして入力すると正しく生成されないことになります。
ユーザーカスタムのEnumや闇クラスをフィールドに指定するときは、それらが作成済みであることを確認してください。例えば、闇クラスをフィールドに持つ親子構造の闇クラスを生成する場合は、親の闇クラスより先に子の闇クラスを作成してください。

闇クラス生成ツールの便利な機能

スクリプトのロード機能

生成済みの闇クラスから各種パラメーターを読み出せます。後から編集したいときに便利です。闇クラスのスクリプトファイルをドロップしてください。

JSONシリアライズモード

JSONを闇クラスにするコードを生成します。
(正確にはJSON文字列をVRC JSONでデシリアライズしたDataDictionaryを闇クラスに変換します。)
闇クラスの入れ子にも対応しています。


以下生成コードです。(TextureFormatは組み込みのenum型)

using UnityEngine;
using VRC.SDK3.Data;

namespace Nuruwo.Dev
{
    // Enum for assigning index of field DataTokens
    enum MyDarkClassJsonField
    {
        Name,
        Format,
        Positions,
        
        Count
    }
    
    public class MyDarkClassJson : DataList
    {
        // Constructor
        // This comments for loading this script by generator : 
        // public static MyDarkClassJson New(string name nm, TextureFormat format, Vector3[] positions pn)
        public static MyDarkClassJson New(DataDictionary dic)
        {
            var name = dic["nm"].String;
            var format = (TextureFormat)(int)dic["format"].Number;
            
            var positionsList = dic["pn"].DataList;
            var positionsCount = positionsList.Count;
            var positions = new Vector3[positionsCount];
            for (int i = 0; i < positionsCount; i++)
            {
                var positionsData = positionsList[i].DataDictionary;
                var positionsX = (float)positionsData["x"].Number;
                var positionsY = (float)positionsData["y"].Number;
                var positionsZ = (float)positionsData["z"].Number;
                positions[i] = new Vector3(positionsX, positionsY, positionsZ);
            }
            
            // Make DataTokens
            var data = new DataToken[(int)MyDarkClassJsonField.Count];
            
            data[(int)MyDarkClassJsonField.Name] = name;
            data[(int)MyDarkClassJsonField.Format] = new DataToken(format);
            data[(int)MyDarkClassJsonField.Positions] = new DataToken(positions);
            
            return (MyDarkClassJson)new DataList(data);
        }
    }
    
    public static class MyDarkClassJsonExt
    {
        // Get methods
        public static string Name(this MyDarkClassJson instance)
            => (string)instance[(int)MyDarkClassJsonField.Name];
        public static TextureFormat Format(this MyDarkClassJson instance)
            => (TextureFormat)instance[(int)MyDarkClassJsonField.Format].Reference;
        public static Vector3[] Positions(this MyDarkClassJson instance)
            => (Vector3[])instance[(int)MyDarkClassJsonField.Positions].Reference;
        
        // Set methods
        public static void Name(this MyDarkClassJson instance, string arg)
            => instance[(int)MyDarkClassJsonField.Name] = arg;
        public static void Format(this MyDarkClassJson instance, TextureFormat arg)
            => instance[(int)MyDarkClassJsonField.Format] = new DataToken(arg);
        public static void Positions(this MyDarkClassJson instance, Vector3[] arg)
            => instance[(int)MyDarkClassJsonField.Positions] = new DataToken(arg);
    }
}

U#からは以下のように利用します。

using UdonSharp;
using UnityEngine;

namespace Nuruwo.Dev
{
    public class DarkClassTest : UdonSharpBehaviour
    {
        void Start()
        {
            // format = 4 is TextureFormat.RGBA32
            var jsonString = "{\"nm\":\"jsonUser\",\"format\":4,\"pn\":[{\"x\":1.0,\"y\":2.0,\"z\":3.0},{\"x\":4.0,\"y\":5.0,\"z\":6.0}]}";
            if (VRC.SDK3.Data.VRCJson.TryDeserializeFromJson(jsonString, out VRC.SDK3.Data.DataToken result))
            {
                Debug.Log("json: " + jsonString);
                //Make instance with constructor
                var myDarkClassJson = MyDarkClassJson.New(result.DataDictionary);
                Debug.Log("nm: " + myDarkClassJson.Name());
                Debug.Log("format: " + myDarkClassJson.Format());
                for (int i = 0; i < myDarkClassJson.Positions().Length; i++)
                {
                    Debug.Log("positions: " + i + " / " + myDarkClassJson.Positions()[i]);
                }
            }
        }
    }
}

闇クラスという名前

私の造語です。以下の二つの意味から来ています。

  • U#の制限を回避するためにC#の黒魔術的な手法を用いている 🧙
  • DataTokenにブラックホールのようにあらゆるデータが吸い込まれている 🕳️

まとめ

  • 闇クラスで複雑なデータもU#で扱えるようになるよ。

  • 改善によって配列の扱いが素直になったよ。

  • ツールで簡単に生成できるよ。JSONも対応!

それでは皆さん楽しいU#開発を!🚀

謝辞

TheHelpfulHelper様

闇クラスの改良の際、以下のコードを参考にさせて頂きました。ありがとうございます!

U# Fake Custom Classes Pattern · GitHub

ureishi様

参考コードの提示や生成ツールに対して多岐にわたる改良提案を頂きました。いつもお世話になっています!

https://x.com/aivrc

ちう様

型の自動判別コードを提供頂きました。ツール上のユーザー定義が不要になり非常に使いやすくなりました。ありがとうございます!

https://x.com/ChiuGameProject

koyashiro様

前回のobject型闇クラスの開発者です。いつもお世話になっています!

https://x.com/koyashiro