Meta programming in Unity

Meta programming in Unity

Welcome to this article that will talk about writing C# scripts that will generate other C# scripts in Unity !
I learned about meta programming when creating the Concurrent Value asset.

This can seems complex at first but it’s actually a simple matter of using C# file system classes.
This article won’t talk about running custom code at runtime (having some piece of code dynamically compiled and executed so that for example the player could write C# code that your game will run) which is totally doable in C# but through means not discussed here.

Everything in this article will work on Unity version 2018.3 or newer.

Why would you want to do this ?

Meta programming is useful:

  • When writing simple, easy to automate scripts that should be replicated multiple times with small differences
  • When writing scripts that require tests that the computer can do for you
  • When you need to generate scripts based on some user inputs

It should be considered when you feel like what you are programming is repetitive and prone to error that a computer could easily avoid, or if you need to generate scripts from data you don’t have yet.

A metaprogram every programmer knows about is a compiler: it will take as an input some user data (a higher level programming language code) and generate an executable from it (a machine language code).

In this post we will see two examples of useful meta programming in unity, every sources are included at the end.

Serializing a generic class

This example is no longer relevant now that Unity can serialize generic (template) class (since version 2020.1) but it use to be that:

[SerializeField] Dictionary<string, int> cannotSerializeDico;

Wouldn’t work, the Dictionary would not be serialized and would not appear in the inspector because it is a generic class (only exception to this was a List).

A good workaround for this issue is creating a non generic class inheriting the generic class we want to serialize, for example:

[Serializable] public class Dictionary_string_int : Dictionary<string, int>
{
	
}

We can then use our non generic class in place of the generic one:

[SerializeField] Dictionary_string_int canSerializeDico;

We are going to automate this process using a meta programming editor window.

Let’s start with a simple EditorWindow script that we are going to fill. This script should be in an Editor folder.

using UnityEngine;
using UnityEditor;
using System.IO;

public class NonGenericDictionaryWindow : EditorWindow
{
    const string windowName = "Non Generic Dictionary Generator";

    void OnGUI()
    {

    }

    void Awake()
    {
        titleContent = new GUIContent(windowName);
    }

    [MenuItem("Juste Tools/" + windowName)]
    static void Init()
    {
        NonGenericDictionaryWindow window = (NonGenericDictionaryWindow)GetWindow(typeof(NonGenericDictionaryWindow), true);
        window.Show();
    }
}

First we will define the OnGUI method, all we need is 2 fields where the user can input the type to use as the Dictionary key type and value type, and a button that will generate the script.

    string keyType;
    string valueType;

    void OnGUI()
    {
        keyType = EditorGUILayout.TextField("Key Type Name", keyType);
        valueType = EditorGUILayout.TextField("Value Type Name", valueType);
        if (GUILayout.Button("Generate"))
            GenerateDico();
    }

    void GenerateDico()
    {

    }

Now comes the interesting part ! We are going to generate a C# script when the button is pressed.

Most of the script we will generate is fixed, the only variable parts are the key type name and the value type name.
There is a perfect function to fill the hole from a given text, the string.Format method !

Let’s create a new .txt file, the content of which we will use as our first parameter for the format method.
NonGenericDictionary.txt :

using System.Collections.Generic;
using System;

[Serializable] public class Dictionary_{0}_{1} : Dictionary<{0}, {1}>
{{
	
}}

We use {0} for the key type name, and {1} for the value type name.
Note that we had to double the class brackets otherwise it wouldn’t be a valid format (the string.Format method will replace them with simple brackets).

Now all we need is to have a way for our window to know where the format text file is stored and where should we generate new scripts.
We will define a new static class (in the Editor folder) that we will also use for the other example of this article.

using UnityEngine;

public static class MetaGenUtility
{
    const string relativeTemplatePath = "/"; // template files path relative to the Assets folder
    const string relativeGeneratePath = "/"; // where should we generate the new scripts relative to the Assets folder
    public static string templatePath => Application.dataPath + relativeTemplatePath;
    public static string generatePath => Application.dataPath + relativeGeneratePath;
}

Everything is ready for us to fill the GenerateDico method in our window script !
All we have to do is read the text file, use the string.Format method, and write the result in a file with the .cs extension.

    void GenerateDico()
    {
        string template = File.ReadAllText(MetaGenUtility.templatePath + "NonGenericDictionary.txt");
        string scriptText = string.Format(template, keyType, valueType);
        File.WriteAllText(MetaGenUtility.generatePath + $"Dictionary_{keyType}_{valueType}.cs", scriptText);
    }

We now successfully generate a new C# script from a C# script !


One last detail, we need to let unity know that the assets have changed so that it can start the compilation process, we simply have to call AssetDatabase.Refresh.
We will do it when closing the window.

    void OnDestroy()
    {
        AssetDatabase.Refresh();
    }

This example doesn’t include any errors checking, you could add verifications that keyType and valueType are valid type name and that a non generic Dictionary with the same template parameters doesn’t already exist.

Setting Animator parameters with an enum

When using the Set methods of an Animator, using a string to specify which parameter to edit is prone to errors. We will generate new C# scripts allowing us to use an enum as an argument instead.
This example will only apply to float parameters but you can easily extend it to work on any parameter type.

First let’s create extension methods for the Animator component that will let us call SetFloat and GetFloat with an enum instead of a string as the first argument.

using System;
using System.Collections.Generic;
using UnityEngine;

public static class AnimatorEnumExtension
{
    public static Dictionary<Enum, string> floatEnumToName = new Dictionary<Enum, string>()
    {
/// EXTEND_FLOAT do not delete !
    };

    public static void SetFloat(this Animator anim, Enum parameter, float value)
    {
        anim.SetFloat(floatEnumToName[parameter], value);
    }

    public static float GetFloat(this Animator anim, Enum parameter)
    {
        return anim.GetFloat(floatEnumToName[parameter]);
    }
}

This class works by using the floatEnumToName dictionary to get a parameter name from the enum that we used the methods with.
We will fill this dictionary programmatically. This is why we have the /// EXTEND_FLOAT do not delete ! line, it will help us easily locate where we should add some text.

Now let’s create a new EditorWindow:

using UnityEngine;
using UnityEditor;
using System.IO;
using System.Linq
using System;

public class EnumBasedAnimatorWindow : EditorWindow
{
    const string windowName = "Enum Based Animator Generator";

    void OnGUI()
    {

    }
    void Awake()
    {
        titleContent = new GUIContent(windowName);
    }

    void OnDestroy()
    {
        AssetDatabase.Refresh();
    }

    [MenuItem("Juste Tools/" + windowName)]
    static void Init()
    {
        EnumBasedAnimatorWindow window = (EnumBasedAnimatorWindow)GetWindow(typeof(EnumBasedAnimatorWindow), true);
        window.Show();
    }
}

Like in the previous example, we will use a .txt file to use in a string.Format method.
FloatEnum.txt:

namespace {0}
{{
    public enum Float
    {{
        {1}
    }}
}}

{0} will be a namespace that we will use to differentiate between multiples Float enums and {1} will be the different enum values that we will create dynamically.

Let’s now fill the OnGUI method :

    Animator target;
    string namespaceName;
    string[] enumValueNames;

    void OnGUI()
    {
        EditorGUI.BeginChangeCheck();
        target = (Animator)EditorGUILayout.ObjectField("Target Animator", target, typeof(Animator), true);
        if (EditorGUI.EndChangeCheck() && target != null)
            enumValueNames = new string[target.parameters.Where((p) => p.type == AnimatorControllerParameterType.Float).ToArray().Length];
        if (target != null)
        {
            namespaceName = EditorGUILayout.TextField("Namespace", namespaceName);
            AnimatorControllerParameter[] floatParameters = target.parameters.Where((p) => p.type == AnimatorControllerParameterType.Float).ToArray();
            for (int i = 0; i < floatParameters.Length; i++)
                enumValueNames[i] = EditorGUILayout.TextField(floatParameters[i].name, enumValueNames[i]);
            if (GUILayout.Button("Generate"))
                GenerateEnumAndAddToDico();
        }
    }

    void GenerateEnumAndAddToDico()
    {

    }

We have a field for the Animator we will generate enums for, a text field in which we can specify the namespace to use, and some text fields for every Float parameters of the Animator so that we can choose the enum name.

Now for the interesting part ! Let’s fill the GenerateEnumAndAddToDico method:

    const string relativeAnimExtensionFileName = "/AnimatorEnumExtension.cs"; // path of the AnimatorEnumExtension script relative to Assets

    void GenerateEnumAndAddToDico()
    {
        // create the enum file
        string template = File.ReadAllText(MetaGenUtility.templatePath + "FloatEnum.txt");
        string scriptText = string.Format(template, namespaceName, string.Join(", ", enumValueNames));
        File.WriteAllText(MetaGenUtility.generatePath + $"AnimatorEnum_{target.runtimeAnimatorController.name}.cs", scriptText);
        // add some entries to the floatEnumToName dictionary
        AnimatorControllerParameter[] floatParameters = target.parameters.Where((p) => p.type == AnimatorControllerParameterType.Float).ToArray();
        string dicoEntries = "";
        for (int i = 0; i < floatParameters.Length; i++)
            dicoEntries += $"{{ {namespaceName}.Float.{enumValueNames[i]}, \"{floatParameters[i].name}\" }}, {Environment.NewLine}";
        dicoEntries += Environment.NewLine;
        AddToFileAtMarker(Application.dataPath + relativeAnimExtensionFileName, dicoEntries, "/// EXTEND_FLOAT");
    }

    void AddToFileAtMarker(string fileName, string toAdd, string extendMarker)
    {
        string text = File.ReadAllText(fileName);
        if (!text.Contains(extendMarker))
            throw new FormatException(string.Format("Couldnt find marker {0} in {1} !", extendMarker, fileName));
        text = text.Replace(extendMarker, toAdd + extendMarker);
        File.WriteAllText(fileName, text);
    }

Creating the enum script is pretty straight forward, we do the same we did for the previous example.

To write in the AnimatorEnumExtension.cs file we use the AddToFileAtMarker method that will insert some text at the marker position in a given file.
For every enum values we generated we create a string in the form:

{ (namespace).Float.(enum value name), “(corresponding parameter name)” }

And then add all of those strings in the script.

We can now use the SetFloat and GetFloat method of an animator using an enum, removing the possibility of making a mistake when typing a parameter name !

Conclusion

With these 2 examples you should now have a good idea of how to meta program in Unity !
A needed amelioration would be checking if the fields values will create valid scripts in the editor window before generating.

In the Concurrent Value asset I use this technique to create new Calculator class (discussed in the previous article). Meta programming can be very useful in some situations !

I hope you’ve learned a bit, and happy coding !

Sources

https://gist.github.com/Slyp05/b3c2f971819180decec403b56f4ce204

Leave a Comment

Your email address will not be published. Required fields are marked *