Skip to content

Instantly share code, notes, and snippets.

@partlyhuman
Last active February 23, 2019 00:58
Show Gist options
  • Save partlyhuman/c10438bf4776071153ee310b976d9655 to your computer and use it in GitHub Desktop.
Save partlyhuman/c10438bf4776071153ee310b976d9655 to your computer and use it in GitHub Desktop.
[UnityNotNull] attribute
using System;
using System.Linq;
using System.Reflection;
using UnityEngine;
#if UNITY_EDITOR
using UnityEditor;
using UnityEditor.Callbacks;
#endif
namespace com.partlyhuman
{
/// <summary>
/// For object references that need to be assigned in Unity Editor's inspector.
/// Annotate fields with this flag if they are required to be assigned.
/// The inspector will color these fields red if not assigned, and will throw an error if you
/// enter playmode with any of these references null.
/// Click the error to identify the source object.
/// </summary>
public class UnityNotNull : PropertyAttribute
{
// Should only be used on object-reference fields.
// Ideally use a unique component type to identify the proper reference.
public static bool IsAttributeValidFor(FieldInfo field)
{
return field.FieldType.IsSubclassOf(typeof(UnityEngine.Object));
}
}
public static partial class MonoBehaviourExtensions
{
/// <summary>
/// In the editor, assert all [UnityNotNull] fields in this instance are defined.
/// In builds, does nothing.
/// You can run this on demand but typically this is run at play time in the editor by EditorUnityNotNullEnforcement
/// </summary>
public static void AssertUnityNotNullFields(this MonoBehaviour source)
{
#if UNITY_EDITOR
var fields = source.GetType()
.GetFields(BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance)
.Where(f => Attribute.GetCustomAttribute(f, typeof(UnityNotNull)) != null);
foreach (FieldInfo field in fields)
{
if (!UnityNotNull.IsAttributeValidFor(field))
{
Debug.LogWarningFormat(source, "[UnityNotNull] not allowed for fields of type {0} in {1}::{2}",
field.FieldType.Name, field.DeclaringType.Name, field.Name);
continue;
}
// Debug.LogFormat("Checking {0}", field.Name);
Debug.AssertFormat(field.GetValue(source) != null, source,
"[UnityNotNull] attributed field is null {0}::{1}", field.DeclaringType.Name, field.Name);
}
#endif
}
}
#if UNITY_EDITOR
public static class EditorUnityNotNullEnforcement
{
// Runs when you hit play
[DidReloadScripts]
public static void OnScriptsReloaded()
{
EnforceAllUnityNotNullFields();
}
[MenuItem("GameObject/Check for Null References")]
public static void EnforceAllUnityNotNullFields()
{
foreach (var go in GameObject.FindObjectsOfType<MonoBehaviour>())
{
go.AssertUnityNotNullFields();
}
}
}
#endif
#if UNITY_EDITOR
[CustomPropertyDrawer(typeof(UnityNotNull))]
public class AssertNotNullInspector : PropertyDrawer
{
static readonly Color UNASSIGNED_BG = Color.red;
const float BUTTON_W = 46f;
const string FIND_IN_SCENE = "Scene";
const string FIND_IN_ASSETS = "Prefab";
public override void OnGUI(Rect position, SerializedProperty property, GUIContent label)
{
if (!UnityNotNull.IsAttributeValidFor(fieldInfo) || property.objectReferenceValue != null)
{
EditorGUI.PropertyField(position, property, label);
}
else
{
Color oldColor = GUI.backgroundColor;
GUI.backgroundColor = UNASSIGNED_BG;
position.width -= 2 * BUTTON_W;
var buttonPos = new Rect
{
x = position.x + position.width,
y = position.y,
width = BUTTON_W,
height = position.height,
};
EditorGUI.PropertyField(position, property, label);
GUI.backgroundColor = oldColor;
if (GUI.Button(buttonPos, FIND_IN_SCENE))
{
property.objectReferenceValue = GameObject.FindObjectOfType(fieldInfo.FieldType);
}
buttonPos.x += buttonPos.width;
if (GUI.Button(buttonPos, FIND_IN_ASSETS))
{
property.objectReferenceValue = AssetDatabase.FindAssets("t:Prefab")
.Select(guid => AssetDatabase.LoadAssetAtPath<GameObject>(AssetDatabase.GUIDToAssetPath(guid)))
.FirstOrDefault(asset => asset && asset.GetComponent(fieldInfo.FieldType) != null);
Resources.UnloadUnusedAssets();
}
}
}
}
#endif
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment