Hi there - here’s another learning note!!
This one is for programming in Unity, and requires you to know a bit of the fundamentals. Enjoy! <3

Context

What I needed

I had a “LocalizedString” struct and it was contained in a few classes of my game.
All variables needed to be validated, for example checking whether all dialogues in a scene had a valid ID, in order to prevent the game having missing texts.

The issue

Since only a few scripts used them, it was simple enough to create an interface with a method like “ValidateData” and make these classes implement it, printing any error from there.

Get All Components in the scene with that interface -> Invoke “ValidateData” -> Manage any error based on the above

However, whenever I created something new that required LocalizedStrings, I had to implement the same ‘ValidateData’ method over and over. This also meant that if a class “to be validated” was inside another, then their parent needed to be validated as well.

Example:

[System.Serializable]
class Child : IValidate
{
	[SerializeField] LocalizedString dialogueID;
	
	public void ValidateData() //Called by test
	{
		// Error if the dialogue is not valid	
	}
}

[System.Serializable]
class Parent : MonoBehaviour, IValidate
{
	[SerializeField] Child child;
	
	public void ValidateData() //Called by test
	{
		child.ValidateData();
		// having a child forced the parent to be
		// validated as well - and who knows how many
		// children will be added in the future!! AAAAAHHH!!!
		
		// PS. The above has a different meaning outside Unity. 
		// Google, please, index my game development page well, thank you.
	}
}

As you can see, it wasn’t doable.
Error prone, no flexibility, no scalability. Just a mess!

The Solution

Since I didn’t have a few simple scripts any more, but “who-knows what and where”, I needed to start going towards a generic approach.

Serialized Objects and Properties

Unity needs to serialize GameObjects in the scene, ideally with scripts that contain many different types of variables (except for dictionaries - but luckily we don’t need them now).

In fact, this is what you see if you open a “.scene” file in a text editor:

https: www.febucci.com 2022 unity scene serialization info

Conveniently, we also need to access said serialized data of any different type, since we don’t know which class is using a LocalizedString and where it’s stored.

From Unity’s docs page: “SerializedObject and SerializedProperty are classes for editing serialized fields on Unity objects in a completely generic way.” BINGO!

Now that we have a simple overview/outline of how things work, let’s jump straight into the solution code.

The code

First, we need a snippet that detects all variables of type in a script.

// Using the following namespaces:
using System;
using UnityEditor;
using UnityEngine;

// Later in your class [...] 
public static void FindAllVariablesInScript<T>(MonoBehaviour script)
{
	//gets the serialized object data from the script
	var serializedObject = new SerializedObject(script);
	
	//gets the target type name (without namespaces etc.)
	string targetVariableTypeName = typeof(T).Name;
	
	//Starts searching for all serialized variables in this object
	var property = serializedObject.GetIterator();
	while (property.NextVisible(true)) //enters nested classes etc.
	{
	    //Skips for properties that do not match our target type
	    if (property.type != targetVariableTypeName) continue;
	    
	    // We found our property!
	    // Do stuff here
	}
}

Now that we have our properties, we could make this metod return a list of them and then perform our validations.
However, given how Unity’s Serialized Properties work, we cannot use previous properties if we move on to the “NextVisible” one.

This means that we must validate a variable as soon as we find it, and cannot actually build a list of them. Otherwise, we would incur in an error that says something like: "InvalidOperationException: The operation is not possible when moved past all properties (Next returned false)".

To make the code generic and allow for any invocation, we can pass a delegate as parameter. (Read more about System.Action here.)

public static void ActionOnAllVariablesInScript<T>
	(
	MonoBehaviour script, 
	Action<SerializedProperty> OnVariable //generic method
	)
{
    var serializedObject = new SerializedObject(script);
    
    string targetVariableTypeName = typeof(T).Name;
    
    var property = serializedObject.GetIterator();
    
    while (property.NextVisible(true))
    {
        if (property.type != targetVariableTypeName) continue;
        
        // We found our property!
        // Invokes a custom method, which
        // can validate & do other stuff
        
        if (property.isArray) //also manages arrays
        {
            for (int i = 0; i < property.arraySize; i++)
                OnVariable.Invoke(property.GetArrayElementAtIndex(i));
        }
        else
        {
            OnVariable.Invoke(property);
        }
    }
}

Done! We can now call this method from our scripts, and perform any action/validation we want for any variable of type <T> contained in the scenes.

[System.Serializable]
struct LocalizedString
{
	public string locKey;
	public int characterId;
}
public static void ValidateLocalizedStringsInScene(Scene scene)
{
    //Gets all gameObjects in the scene
    var gameObjects = scene.GetRootGameObjects();

    foreach (var go in gameObjects)
    {
        //Gets all scripts in the scene, searching in all GameObjects
        foreach (var script in go.GetComponentsInChildren<MonoBehaviour>(true))
        {
        	//skips in case some scripts are not valid
        	//e.g. file name / class mismatch, or deletion
            if (script == null) continue;

            var scriptTypeName = script.GetType().ToString();
            
            //Skips scripts unrelated to the game
            //since they're built-in and don't contain our custom structs
            if (scriptTypeName.StartsWith("UnityEngine.")) continue;
            if (scriptTypeName.StartsWith("Cinemachine.")) continue;
            //[...]
            

            //The method that gets invoked on all LocalizedString properties
            void ProcessProperty(SerializedProperty locStringProperty)
            {
                //Gets actual data from Serialized Property
                SerializedProperty locKeyProperty = locStringProperty.FindPropertyRelative(nameof(LocalizedString.locKey));
                SerializedProperty characterIdProperty = locStringProperty.FindPropertyRelative(nameof(LocalizedString.characterId));
				
				//Checks if everything is ok
                if (string.IsNullOrEmpty(locKeyProperty.stringValue))
                {
                    Debug.LogError("Localization Key is null or empty for [...]");
                    //[...]
                }
                
                if (characterIdProperty.intValue <= -1)
                {
                    Debug.LogError("Character is not assigned to [...]");
                    //[...]
                }
            }

            //Finds and validates all Localized Strings in this script
            ActionOnAllVariablesInScript<LocalizedString>(script, ProcessProperty);
        }
    }
}

That’s it!! You can even store the current gameObject that is getting inspected, and point errors directly from there.

Expansions

After creating this script, I started using it everywhere.

For example:

  • Search for all “Areas” in ALL scenes and check if their size components are greater than zero
  • Replace dialogues in ALL scenes that have ID “A”, and make them use ID “B” instead.
  • […]

You can customize it and search for any scene, replace stuff, modify variables and much more.


Hope this helps! Please let me know if it did, so that I get really happy and start creating new stuff as well. Cheers!