Searching any variable in any script in Unity
Read how to search for any variable in any script in Unity, using Serialized Objects and Properties.
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:
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!
Help us stay independent!
Make sure you never miss other content and help us stay independent and keep posting what we love. It's free! ♥️
Deep dive
Almost five years ago I released a plugin called “Text Animator for Unity”, a tool I made/needed for my own games - and Today it is also getting used in other games like “Dredge, Cult of The Lamb”, “Slime Rancher 2” and many more!! I’d love if you could check it out! you’d also support us with this blog and new assets! ✨
Thanks so much for reading!