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;
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 typeGetFields(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 ;)