チカラの技術

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

Improved U# DarkClass (Generation tool available)

Hello, I am fine!

This article is an explanation of an improved version of the DarkClass introduced in the previous article.
(You do not need to read the previous article to understand this one.)
【VRChat】UdonSharpでユーザー定義クラスをNewする黒魔術【闇クラス】 - チカラの技術

It also explains how to handle generation tools that automatically generate DarkClass.

What is the DarkClass?

UdonSharp (U#) which is VRChat's world gimmick development language, it can't the creation of custom classes like regular C# due to limitations. However, it is possible to create custom classes using a technical black magic called DarkClass.

// Example of creating an instance of the DarkClass
var myDarkClass = MyDarkClass.New("Jane Doe", 23);

Improvements since last time


By changing type to be used from object to DataList, it is no longer necessary to write special instructions for arrays of DarkClass, making them much easier to handle.
In addition, changing the index value of an element from an int literal value to an enum type has made editing easier.

How to generate DarkClass

① Import the generation tool from VCC

①Install the nuruwo repository list from the link below. (Do this with the VCC app closed.)
Install nuruwo's vpm repositories



② Import from VCC into your Unity project.

② Start the Generation Tool

Select Tools -> Nuruwo -> DarkClassGenerator from the Unity menu.

③ Create a file for the DarkClasss

Create a new C# script (not a U# script) in Project. Name the file “MyDarkClass”.
(If generated as a U# script, delete the simultaneously generated Udon Sharp Program Asset)

④ DarkClass generation

Enter the required information in the generation tool as shown in the figure to generate it.


Click the “Generate DarkClass” button to copy the generated code to the clipboard and paste it over the script in ③.
This completes the creation of the DarkClass🎉

The generated results are as follows

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);
    }
}

How to use the DarkClass

Now we will use the DarkClass from a normal U# script. Create a U# script “DarkClassTest” and paste the following code.

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"
        }
    }
}

Attach it to the appropriate GameObject, play it in Unity, and if the result appears in Debug.Log, you have succeeded👍

Cautions for using the DarkClass

The DarkClass should be treated and noted differently than the general class.

① Fields of instances

Defining an instance of a DarkClass as a field (member variable) of the using class will cause an error the first time ClientSim is executed.


(However, it is not a fatal error, so the script will not halt.)

public class DarkClassTest : UdonSharpBehaviour
{
    // Defining directly in the field in this manner will result in an error.
    private MyDarkClass _myDarkClass;
}


As a countermeasure, you can avoid the error by wrapping the field as a property as follows.

public class DarkClassTest : UdonSharpBehaviour
{
        // Wrap to property. Access _myDarkClass from code.
        private MyDarkClass _myDarkClass
        {
            get { return (MyDarkClass)d_myDarkClass; }
            set { d_myDarkClass = value; }
        }
        // object type field, prefixed with d_. This field should not be accessed.
        private object d_myDarkClass;
}

② Type Recognition in the Generation Tool

The Generate tool automatically recognizes the types that exist in the project when the Generate button is pressed. In other words, if you enter a type that does not exist at that time as a field, it will not be generated correctly.
When specifying a user-custom Enum or DarkClass as a field, make sure that they have already been created. For example, when generating a parent-child structure DarkClass with a DarkClass as a field, create the child DarkClass before the parent DarkClass.

Useful features of the DarkClass Generator

Script load function

Various parameters can be read from the generated DarkClass script. This is useful when you want to edit them later. Drop a script file of the DarkClass.

JSON Deserialization Mode

Generates code to turn JSON into a DarkClass.
(To be precise, it converts a DataDictionary deserializing a JSON string with VRC JSON into a DarkClass)
Nesting of DarkClass is also supported.

The following is the generated code. (TextureFormat is a built-in enum type)

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);
    }
}

From U#, use the following

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]);
                }
            }
        }
    }
}

The name "DarkClass"

The name of DarkClass is a term I coined. It comes from the following two meanings

  • It is a black magic technique in C# to get around the limitations of U#. 🧙
  • All kinds of data are sucked into the DataToken like a black hole. 🕳️

Summary

  • You'll be able to handle complex data with DarkClass in U#.

  • Arrays are now handled in a straightforward manner with the improvements.

  • JSON is also supported!

Have fun with U# development!🚀

Acknowledgements

Dear TheHelpfulHelper

I used the following code as a reference when improving the DarkClass. Thank you very much!

U# Fake Custom Classes Pattern · GitHub

Dear ureishi

I have received a wide variety of suggestions for improvements to the reference code and the generation tool. I'm always grateful for your help!

https://x.com/aivrc

Dear Chiu

Chiu-san provided me with the code for automatic type identification. It is now very easy to use, eliminating the need for user definitions on the tool. Thank you very much!

https://x.com/ChiuGameProject

Dear koyashiro

He is the developer of the previous object-type DarkClass. I'm always grateful for your help!

https://x.com/koyashiro