Search code examples
c#unity-game-engineattributes

Custom Validator in Unity


Hey I have a simple class UiManager I wanna validate all variables so they should not be null

[SerializeField]    private    SceneLoaderManager.SceneName   gamePlaySceneName;
[SerializeField]    private    TextMeshProUGUI                coinCountUI;
[SerializeField]    private    TextMeshProUGUI                energyCountUI;

for now, I have only 3 variables but in the future, It can be more than 1000 I will probably validate them by checking for null but it's a lot of work to do

if (coinCountUi == null) Debug.Log ("you may forget to assign `CoinCountUi` "); 

How can I validate all variables at once and throw some kind of message to the user? I have an idea to do this but don't know how to execute it like [WarnOnNull] [SerializeField] private TextMeshProUGUI energyCountUI; please guide me how I can create Attribute like this which can validate those stuff

Part2:

namespace Randoms.Reflection 
{
 [AttributeUsage (AttributeTargets.All)]
 public class WarnOnNullAttribute : Attribute {}

public static class TypeChecker 
{

public static void WarnOnNull (this MonoBehaviour other) 
{
  Type type  = other.GetType ();
  
  BindingFlags bindingFlags = 
  BindingFlags.Instance  |
  BindingFlags.Public    |
  BindingFlags.NonPublic |
  BindingFlags.FlattenHierarchy;
  
  FieldInfo[] fieldInfos = type
  .GetFields (bindingFlags)
  .Where (_ => _.IsDefined (typeof (WarnOnNullAttribute),true))
  .ToArray();
  
  foreach (FieldInfo info in fieldInfos)
  {
    var val = info.GetValue (other);
    if (val == null)
      Debug.LogWarning($"{info.Name} is not referenced!");
  }
}

 }
}

After DerHugo answer I have this code so far this code is working but still I have to write one addition line.

private void Awake ()
{
  this.WarnOnNull ();
}

how I can make this work only using attributes something like this

[WarnOnNull] [SerializeField]    private    TextMeshProUGUI                energyCountUI;

Solution

  • How can I validate all variables at once when exactly? On compile time these are most probably always gonna be null.

    You could of course go for reflection and use a certain attribute - or simply use the existing one SerializeField and only check those like e.g.

    var type = GetType();
    var serializedFields = type.GetFields(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.FlattenHierarchy).Where(field => (field.FieldType.IsSubclassOf(typeof(UnityEngine.Object)) || field.FieldType == typeof(UnityEngine.Object)) && (field.IsPublic || field.IsDefined(typeof(SerializeField)))).ToArray();
    
    foreach (var field in serializedFields)
    {
        var value = field.GetValue(this);
    
        if (value == null)
        {
            Debug.LogError($"{field.Name} is not referenced in {type.Name} on {this}!", this);
        }
    }
    

    So what does this do:

    • GetType(): Gives us this components type
    • GetFields(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.FlattenHierarchy): Gives us all instace fields including those implemented by a parent type
    • (field.FieldType.IsSubclassOf(typeof(UnityEngine.Object)) || field.FieldType == typeof(UnityEngine.Object)): since we are only interested in UnityEngine.Object reference fields. We don't care about "normal" Serializable classes, strings, etc
    • (field.IsPublic || field.IsDefined(typeof(SerializeField))): Fields that are serialized into the Inspector are either public or have the attribute [SerializeField]

    Update

    As you want to use only the attribute you would need a more centralized way of calling the check method.

    You can of course throw even more reflection in to simply march through all your classes.

    There might be better ways to do this but this is what I came up with on my phone ^^

    [AttributeUsage(AttributeTargets.Field)]
    public class WarnOnNullAttribute : PropertyAttribute { }
    
    #if UNITY_EDITOR
    // Just a custom drawer to display an error if used on fields
    // that are not of type UnityEngine.Object
    [CustomPropertyDrawer(typeof(WarnOnNullAttribute))]
    public class WarnOnNullAttributeDrawer : PropertyDrawer
    {
        public override void OnGUI(Rect position, SerializedProperty property, GUIContent label)
        {
            WarnOnNullAttribute warn = attribute as WarnOnNullAttribute;
    
            if (property.propertyType != SerializedPropertyType.ObjectReference)
            {
                EditorGUI.HelpBox(position, label.text, "[WarnOnNull] is only valid on UnityEngine.Object references!", MessageType.Error);
            }
            else
            {
                EditorGUI.PropertyField(position, property);
            }
        }
    }
    
    
    public static class WarnOnNullAttributeCheck
    {
        // This is called as soon as you open the project in Unity
        // and after script compilations
        [InitializeOnLoadMethod]
        private static void Init()
        {
            // Attach a listener to get notified whenever the play mode is changed
            EditorApplication.playModeStateChanged -= OnEnterPlayMode;
            EditorApplication.playModeStateChanged += OnEnterPlayMode;
    
        }
    
        private static void OnEnterPlayMode(PlayModeStateChange state)
        {
            // only run checks once when leaving edit mode before entering play mode
            if(state != PlayModeStateChange.ExitingEditMode) return;
    
            // Get all types that inherit from ScriptableObject or Component
            // as these are basically all that can have an Inspector and serialized fields
            var componentTypes = GetAllDerivedFrom(typeof(Component));
            var scriptabeObjectTypes = GetAllDerivedFrom(typeof(ScriptableObject));
    
            // Now we can check both lists
            // For the components we check the instances from the scene
            CheckComponents(componentTypes);
            // For the ScriptableObjects we check the assets
            CheckScriptableObjects(scriptableObjecttypes
        }
    
        private void CheckComponents (Type[] componentTypes)
        {
            foreach(var type in componentTypes)
            {
                // Luckily in newer Unity versions there is finally
                // a simple call to get all instances of a type from the scene
                // including even disabled/inactive ones
                var instances = UnityEngine.Object.FindObjectsOfType(type, true);
    
                foreach(var instance in instances)
                {
                    ValidateInstance (instance, type);
                }
            }
        }
    
        private static void CheckScriptableObjects (Type[] types)
        {
            foreach(var type in types)
            {
                var instances = GetAllInstaces(type);
    
                foreach(var instance in instances)
                {
                    ValidateInstance (instance, type);
                }
            }
        }
    
        private static UnityEngine.Object[] GetAllInstances(Type type)
        {
            // For ScriptableObjects we need a different approach
            // since these are assets and we need to actually load them
            var guids = AssetDatabase.FindAssets("t:"+ typeof(T).Name);
            var instances = new UnityEngine.Object[guids.Length];
            for(int i =0;i<guids.Length;i++)
            {
                var path = AssetDatabase.GUIDToAssetPath(guids[i]);
                instances[i] = AssetDatabase.LoadAssetAtPath(path);
            }
    
            return instances;
        }
    
        private static void ValidateInstance(UnityEngine.Object instance, Type type)
        {
            // Basically a slightly adjusted version of above
            var serializedFields = type.GetFields(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.FlattenHierarchy).Where(field => (field.FieldType.IsSubclassOf(typeof(UnityEngine.Object)) || field.FieldType == typeof(UnityEngine.Object)) && field.IsDefined(typeof(WarnOnNullAttribute)))).ToArray();
    
            foreach (var field in serializedFields)
            {
                var value = field.GetValue(instance);
                if (value == null)
                {
                    Debug.LogError($"{field.Name} is not referenced for {instance}!", instance);
                }
            }
        }
    
         private static Type[] GetAllDerivedFrom(Type baseType)
         {
             return AppDomain.CurrentDomain.GetAssemblies()
                 .SelectMany(assembly => assembly.GetTypes())
                 .Where(type => type.IsSubclassOf(baseType)).ToArray();
        }
    }
    #endif
    

    It might also be possible to use Resources.FindObjectsOfTypeAll instead which would even include prefabs etc but then it gets more complex and also way more expensive ;)