using System; using UnityEngine; using Object = UnityEngine.Object; #if UNITY_EDITOR using UnityEditor; using UnityEditor.SceneManagement; #endif namespace Jovian.InspectorTools { public class RequiredAttribute : PropertyAttribute, IPropertyCondition { public enum Scope { None, SelfOrChild, SceneOnly, SceneOrParentPrefabOnly // ProjectOnly /* * ProjectOnly has a quirk I can't figure out. If you reference a prefab in the project then it works fine. * However if you also have an instance of that prefab if the same scene/prefab then it will become a reference to that instance * instead of the one in the project. Even the icon changes in Unity. I think this may be a Unity Bug. */ } public readonly Scope scope; public RequiredAttribute() { this.scope = Scope.None; } public RequiredAttribute(Scope scope) { this.scope = scope; } #if UNITY_EDITOR public bool DoesPropertyMeetCondition(SerializedProperty property, out string description) { description = string.Empty; if (property.propertyType != SerializedPropertyType.ObjectReference) { description = $"{property.name} is not an ObjectReference"; return false; } bool hasValue = property.objectReferenceValue != null; if (scope == Scope.None) { if (!hasValue) { description = $"{property.name} requires a value"; return false; } return true; } if (scope == Scope.SelfOrChild) { if (!hasValue) { description = $"{property.name} requires a value"; return false; } if (!TryGetTargetGameObject(property, out GameObject parentGameObject)) { description = $"{property.name} does not have a GameObject parent"; return false; } if (!TryGetGameObject(property.objectReferenceValue, out GameObject childGameObject)) { description = $"{property.name} has a non-GameObject value"; return false; } if (!IsObjectSelfOrChild(parentGameObject, childGameObject)) { description = $"{property.name} value is not itself or it's child"; return false; } return true; } if (scope == Scope.SceneOnly) { if (property.serializedObject.targetObject is ScriptableObject) { description = $"{property.name} SceneOnly is not supported on ScriptableObjects"; return false; } if (!TryGetTargetGameObject(property, out GameObject thisGameObject)) { description = $"{property.name} does not have a GameObject parent"; return false; } // scene is invalid - this is a prefab in the project. if (thisGameObject.scene.IsValid() == false) { // scene isn't valid, can't check description = $"{property.name} has a reference to GameObject not in a Scene"; return true; } PrefabStage prefabStage = PrefabStageUtility.GetCurrentPrefabStage(); // scene is valid - prefab stage exists - this is a prefab if (prefabStage != null) { return true; } if (!hasValue) { description = $"{property.name} requires a value"; return false; } return true; } if (scope == Scope.SceneOrParentPrefabOnly) { if (property.serializedObject.targetObject is ScriptableObject) { description = $"{property.name} SceneOrParentPrefabOnly is not supported on ScriptableObjects"; return false; } if (!TryGetTargetGameObject(property, out GameObject thisGameObject)) { description = $"{property.name} does not have a GameObject parent"; return false; } // scene is invalid - this is a prefab in the project. if (thisGameObject.scene.IsValid() == false) { // scene isn't valid, can't check description = $"{property.name} has a reference to GameObject not in a Scene"; return true; } bool hasTargetGameObject = TryGetGameObject(property.objectReferenceValue, out GameObject targetGameObject); PrefabStage prefabStage = PrefabStageUtility.GetCurrentPrefabStage(); // scene is valid - prefab stage exists - this is a prefab if (prefabStage != null) { // if the prefab open has this requirement on its root object if (prefabStage.prefabContentsRoot == thisGameObject) { if (hasTargetGameObject) { // if the value assigned is a gameObject if (!IsObjectSelfOrChild(thisGameObject, targetGameObject)) { // then fail if it's within the prefab description = $"{property.name} requires an object external to its prefab / hierarchy"; return false; } else { return true; } } else { return true; // if we don't have a target then it is valid (only inside this prefab of ourselves) } } // if the prefab open is a different prefab, then we're valid if a value is assigned if (!IsObjectSelfOrChild(thisGameObject, targetGameObject)) { description = $"{property.name} requires an object external to its prefab / hierarchy"; return false; } else { return true; } } // not in a prefab, value must be set to an object not within the scope of this attribute if (!(hasValue && !IsObjectSelfOrChild(thisGameObject, targetGameObject))) { description = $"{property.name} requires an object external to its prefab / hierarchy"; return false; } else { return true; } } /* See note above why this is not enabled if (scope == Scope.ProjectOnly) { if (!hasValue) { return false; } bool isPersistent = EditorUtility.IsPersistent(property.objectReferenceValue); bool objectHasAssetPath = !string.IsNullOrEmpty(AssetDatabase.GetAssetPath(property.objectReferenceValue)); bool hasNoScene = false; bool isPrefabRoot = false; bool isInAssetDatabase = AssetDatabase.Contains(property.objectReferenceValue); if (TryGetGameObject(property.objectReferenceValue, out GameObject targetGameObject)) { isPrefabRoot = PrefabUtility.IsOutermostPrefabInstanceRoot(targetGameObject); if (targetGameObject.scene.IsValid() == false) { // scene isn't valid, can't check //return true; hasNoScene = true; } } // if the target has a path, then it is an asset and that is valid! return objectHasAssetPath; }*/ if (!hasValue) { description = $"{property.name} requires a value"; return false; } return true; } private static bool TryGetTargetGameObject(SerializedProperty property, out GameObject gameObject) { return TryGetGameObject(property.serializedObject.targetObject, out gameObject); } private static bool TryGetGameObject(Object obj, out GameObject gameObject) { if (obj == null) { gameObject = null; return false; } gameObject = obj switch { Component component => component.gameObject, GameObject targetGameObject => targetGameObject, _ => null }; return gameObject != null; } private static bool IsObjectSelfOrChild(GameObject parentObject, GameObject queryObject) { if (parentObject == null || queryObject == null) { return false; } return parentObject == queryObject || queryObject.transform.IsChildOf(parentObject.transform); } #endif } } #if UNITY_EDITOR namespace Jovian.InspectorTools.Internal { [CustomPropertyDrawer(typeof(RequiredAttribute))] public class RequiredAttributePropertyDrawer : PropertyDrawer { public override void OnGUI(Rect position, SerializedProperty property, GUIContent label) { if (property.propertyType != SerializedPropertyType.ObjectReference) { Debug.LogError($"Required should only be used on Unity Object references. Target='{property.serializedObject.targetObject.name}.{property.propertyPath}'"); EditorGUI.PropertyField(position, property, label); return; } RequiredAttribute requiredAttribute = (RequiredAttribute)attribute; Color guiColor = GUI.color; if (!requiredAttribute.DoesPropertyMeetCondition(property, out string text)) { RequiredUtil.LayoutRequired(ref position, text, false); RequiredUtil.DrawRequired(ref position, text, false); } label = EditorGUI.BeginProperty(position, label, property); position = EditorGUI.PrefixLabel(position, label); EditorGUI.BeginChangeCheck(); EditorGUI.PropertyField(position, property, GUIContent.none); if (EditorGUI.EndChangeCheck()) { property.serializedObject.ApplyModifiedProperties(); } EditorGUI.EndProperty(); GUI.color = guiColor; } } } #endif