├── EnumerableExtensions.cs ├── EnumerableExtensions.cs.meta ├── ISerializableRef.cs ├── ISerializableRef.cs.meta ├── InterfaceRef.cs ├── InterfaceRef.cs.meta ├── InterfaceRefPropertyDrawer.cs ├── InterfaceRefPropertyDrawer.cs.meta ├── KBCore.Refs.asmdef ├── KBCore.Refs.asmdef.meta ├── LICENSE ├── LICENSE.meta ├── PrefabUtil.cs ├── PrefabUtil.cs.meta ├── README.md ├── README.md.meta ├── ReflectionUtil.cs ├── ReflectionUtil.cs.meta ├── SceneRefAttribute.cs ├── SceneRefAttribute.cs.meta ├── SceneRefAttributePropertyDrawer.cs ├── SceneRefAttributePropertyDrawer.cs.meta ├── SceneRefAttributeValidator.cs ├── SceneRefAttributeValidator.cs.meta ├── SceneRefFilter.cs ├── SceneRefFilter.cs.meta ├── SceneRefValidatorOnSave.cs ├── SceneRefValidatorOnSave.cs.meta ├── ValidatedMonoBehaviour.cs ├── ValidatedMonoBehaviour.cs.meta ├── package.json └── package.json.meta /EnumerableExtensions.cs: -------------------------------------------------------------------------------- 1 | using System.Collections; 2 | 3 | namespace KBCore.Refs 4 | { 5 | public static class EnumerableExtensions 6 | { 7 | public static int CountEnumerable(this IEnumerable enumerable) 8 | { 9 | int count = 0; 10 | if (enumerable != null) 11 | { 12 | foreach (object _ in enumerable) 13 | { 14 | count++; 15 | } 16 | } 17 | return count; 18 | } 19 | } 20 | } -------------------------------------------------------------------------------- /EnumerableExtensions.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: c691c2a8b039f634abd1431af9bf7a73 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /ISerializableRef.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace KBCore.Refs 4 | { 5 | internal interface ISerializableRef 6 | { 7 | Type RefType { get; } 8 | object SerializedObject { get; } 9 | bool HasSerializedObject { get; } 10 | 11 | /// 12 | /// Callback for serialization. 13 | /// 14 | /// Object to serialize. 15 | /// True if the value has changed. 16 | bool OnSerialize(object value); 17 | 18 | void Clear(); 19 | } 20 | 21 | internal interface ISerializableRef : ISerializableRef 22 | where T : class 23 | { 24 | T Value { get; } 25 | } 26 | } -------------------------------------------------------------------------------- /ISerializableRef.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 662dc17e85b247589e293d75c69fbb59 3 | timeCreated: 1676272103 -------------------------------------------------------------------------------- /InterfaceRef.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using UnityEngine; 3 | 4 | namespace KBCore.Refs 5 | { 6 | /// 7 | /// Allows for serializing Interface types with [SceneRef] attributes. 8 | /// 9 | /// Component type to find and serialize. 10 | [Serializable] 11 | public class InterfaceRef : ISerializableRef 12 | where T : class 13 | { 14 | 15 | /// 16 | /// The serialized interface value. 17 | /// 18 | public T Value 19 | { 20 | get 21 | { 22 | if (!this._hasCast) 23 | { 24 | this._hasCast = true; 25 | this._value = this._implementer as T; 26 | } 27 | return this._value; 28 | } 29 | } 30 | 31 | object ISerializableRef.SerializedObject 32 | => this._implementer; 33 | 34 | public Type RefType => typeof(T); 35 | 36 | public bool HasSerializedObject => this._implementer != null; 37 | 38 | [SerializeField] private Component _implementer; 39 | private bool _hasCast; 40 | private T _value; 41 | 42 | bool ISerializableRef.OnSerialize(object value) 43 | { 44 | Component c = (Component)value; 45 | if (c == this._implementer) 46 | return false; 47 | 48 | this._hasCast = false; 49 | this._value = null; 50 | this._implementer = c; 51 | return true; 52 | } 53 | 54 | void ISerializableRef.Clear() 55 | { 56 | this._hasCast = false; 57 | this._value = null; 58 | this._implementer = null; 59 | } 60 | } 61 | } -------------------------------------------------------------------------------- /InterfaceRef.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 456d8644bc1e4a068630386700a3df1c 3 | timeCreated: 1676271834 -------------------------------------------------------------------------------- /InterfaceRefPropertyDrawer.cs: -------------------------------------------------------------------------------- 1 | #if UNITY_EDITOR 2 | using UnityEditor; 3 | using UnityEngine; 4 | 5 | #if UNITY_2022_2_OR_NEWER 6 | using UnityEngine.UIElements; 7 | using UnityEditor.UIElements; 8 | #endif 9 | 10 | namespace KBCore.Refs 11 | { 12 | [CustomPropertyDrawer(typeof(InterfaceRef<>))] 13 | public class InterfaceRefPropertyDrawer : PropertyDrawer 14 | { 15 | private const string IMPLEMENTER_PROP = "_implementer"; 16 | 17 | // unity 2022.2 makes UIToolkit the default for inspectors 18 | #if UNITY_2022_2_OR_NEWER 19 | public override VisualElement CreatePropertyGUI(SerializedProperty property) 20 | { 21 | return new PropertyField(property.FindPropertyRelative(IMPLEMENTER_PROP), property.displayName); 22 | } 23 | #endif 24 | 25 | public override void OnGUI(Rect position, SerializedProperty property, GUIContent label) 26 | { 27 | EditorGUI.PropertyField(position, property.FindPropertyRelative(IMPLEMENTER_PROP), label, true); 28 | } 29 | } 30 | } 31 | #endif 32 | -------------------------------------------------------------------------------- /InterfaceRefPropertyDrawer.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 7ecbe362b68d4fe38d756ce0bee2464f 3 | timeCreated: 1683488518 -------------------------------------------------------------------------------- /KBCore.Refs.asmdef: -------------------------------------------------------------------------------- 1 | { 2 | "name": "KBCore.Refs", 3 | "rootNamespace": "", 4 | "references": ["GUID:343deaaf83e0cee4ca978e7df0b80d21","GUID:2bafac87e7f4b9b418d9448d219b01ab"], 5 | "includePlatforms": [], 6 | "excludePlatforms": [], 7 | "allowUnsafeCode": false, 8 | "overrideReferences": false, 9 | "precompiledReferences": [], 10 | "autoReferenced": true, 11 | "defineConstraints": [], 12 | "versionDefines": [ 13 | { 14 | "name": "Unity", 15 | "expression": "2021.3.18f", 16 | "define": "UNITY_2021_3_18_OR_NEWER" 17 | } 18 | ], 19 | "noEngineReferences": false 20 | } -------------------------------------------------------------------------------- /KBCore.Refs.asmdef.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 33759803a11f4d538227861a78aba30b 3 | timeCreated: 1676287105 -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Kyle Banks 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /LICENSE.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 1b351be7df0f7d04f9565b5f22916aaf 3 | DefaultImporter: 4 | externalObjects: {} 5 | userData: 6 | assetBundleName: 7 | assetBundleVariant: 8 | -------------------------------------------------------------------------------- /PrefabUtil.cs: -------------------------------------------------------------------------------- 1 | using UnityEngine; 2 | 3 | namespace KBCore.Refs 4 | { 5 | // Subset taken from KBCore.Utils.PrefabUtil for open sourcing 6 | internal class PrefabUtil 7 | { 8 | internal static bool IsUninstantiatedPrefab(GameObject obj) 9 | => obj.scene.rootCount == 0; 10 | } 11 | } -------------------------------------------------------------------------------- /PrefabUtil.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 02aebd5755f749f2b984b01c3c373e2b 3 | timeCreated: 1676287313 -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Scene Reference Attribute 2 | 3 | [![openupm](https://img.shields.io/npm/v/com.kylewbanks.scenerefattribute?label=openupm®istry_uri=https://package.openupm.com)](https://openupm.com/packages/com.kylewbanks.scenerefattribute/) 4 | 5 | Unity C# attribute for serializing **component and interface references** within the scene or prefab during `OnValidate`, rather than using `GetComponent*` functions in `Awake/Start/OnEnable` at runtime. 6 | 7 | This project aims to provide: 8 | 9 | - a simple set of tags for declaring dependency locations 10 | - resolution, validation and serialisation of references **(including interface types!)** 11 | - determinate results so you never have to worry about Unity losing your references, they'll always be safely regenerated 12 | 13 | ### Installation 14 | 15 | **Basic** 16 | 17 | You can simply clone (or download this repo as a zip and unzip) into your `Assets/` directory. 18 | 19 | **UPM** 20 | 21 | [Install with UPM](https://openupm.com/packages/com.kylewbanks.scenerefattribute) on the command-line like so: 22 | 23 | ``` 24 | openupm add com.kylewbanks.scenerefattribute 25 | ``` 26 | 27 | ### Why? 28 | 29 | Mainly to avoid framerate hiccups when additively loading scenes in [Farewell North](https://store.steampowered.com/app/1432850/Farewell_North/), but I also find this to be a much cleaner way to declare references. 30 | 31 | For more details on how this project came about, check out this [Farewell North devlog on YouTube](https://youtu.be/lpBIbmTPDQc). 32 | 33 | ### How? 34 | 35 | Instead of declaring your references, finding them in `Awake` or assigning them in the editor, and then validating them in `OnValidate` like so: 36 | 37 | ```cs 38 | // BEFORE 39 | 40 | [SerializeField] private Player _player; 41 | [SerializeField] private Collider _collider; 42 | [SerializeField] private Renderer _renderer; 43 | [SerializeField] private Rigidbody _rigidbody; 44 | [SerializeField] private ParticleSystem[] _particles; 45 | [SerializeField] private Button _button; 46 | 47 | private void Awake() 48 | { 49 | this._player = Object.FindObjectByType(); 50 | this._collider = this.GetComponent(); 51 | this._renderer = this.GetComponentInChildren(); 52 | this._rigidbody = this.GetComponentInParent(); 53 | this._particles = this.GetComponentsInChildren(); 54 | } 55 | 56 | private void OnValidate() 57 | { 58 | Debug.Assert(this._button != null, "Button must be assigned in the editor"); 59 | } 60 | ``` 61 | 62 | You can declare their location inline using the `Self`, `Child`, `Parent`, `Scene` and `Anywhere` attributes: 63 | 64 | ```cs 65 | // AFTER 66 | 67 | [SerializeField, Scene] private Player _player; 68 | [SerializeField, Self] private Collider _collider; 69 | [SerializeField, Child] private Renderer _renderer; 70 | [SerializeField, Parent] private Rigidbody _rigidbody; 71 | [SerializeField, Child] private ParticleSystem[] _particles; 72 | [SerializeField, Anywhere] private Button _button; 73 | 74 | private void OnValidate() 75 | { 76 | this.ValidateRefs(); 77 | } 78 | ``` 79 | 80 | The `ValidateRefs` function is made available as a `MonoBehaviour` extension for ease of use on any `MonoBehaviour` subclass, and handles finding, validating and serialisating the references in the editor so they're always available at runtime. Alternatively you can extend `ValidatedMonoBehaviour` instead and `ValidateRefs` will be invoked automatically. 81 | 82 | ### Serialising Interfaces 83 | 84 | By default Unity doesn't allow you to serialise interfaces, but this project allows it by wrapping them with the `InterfaceRef` type, like so: 85 | 86 | ```cs 87 | // Single interface 88 | [SerializeField, Self] private InterfaceRef _doorOpener; 89 | 90 | // Array of interfaces 91 | [SerializeField, Self] private InterfaceRef[] _doorOpeners; 92 | ``` 93 | 94 | From here they'll be serialised like any other component reference, with the interface available using `.Value`: 95 | 96 | ```cs 97 | IDoorOpener doorOpener = this._doorOpener.Value; 98 | doorOpener.OpenDoor(); 99 | ``` 100 | 101 | ### Attributes 102 | 103 | - `Self` looks for the reference on the same game object as the attributed component using `GetComponent(s)()` 104 | - `Parent` looks for the reference on the parent hierarchy of the attributed components game object using `GetComponent(s)InParent()` 105 | - `Child` looks for the reference on the child hierarchy of the attributed components game object using `GetComponent(s)InChildren()` 106 | - `Scene` looks for the reference anywhere in the scene using `FindAnyObjectByType` and `FindObjectsOfType` 107 | - `Anywhere` will only validate the reference isn't null, but relies on you to assign the reference yourself. 108 | 109 | ### Flags 110 | 111 | The attributes all allow for optional flags to customise their behaviour: 112 | 113 | ```cs 114 | [SerializeField, Self(Flag.Optional)] 115 | private Collider _collider; 116 | 117 | [SerializeField, Parent(Flag.IncludeInactive)] 118 | private Rigidbody _rigidbody; 119 | ``` 120 | 121 | You can also specify multiple flags: 122 | 123 | ```cs 124 | [SerializeField, Child(Flag.Optional | Flag.IncludeInactive)] 125 | private ParticleSystem[] _particles; 126 | ``` 127 | 128 | - `Optional` won't fail validation for empty (or null in the case of non-array types) results. 129 | - `IncludeInactive` only affects `Parent`, `Child` and `Scene`, and determines whether inactive components are included in the results. By default, only active components will be found, same as `GetComponent(s)InChildren`, `GetComponent(s)InParent` and `FindObjectsOfType`. 130 | - `ExcludeSelf` only affects `Parent` and `Child`, and determines whether components on this `GameObject` are included in the results. By default, components on this `GameObject` are included. 131 | - `Editable` only affects `Parent`, `Child` and `Scene`, and allows you to edit the resulting reference. This is useful, for example, if you have two children who would satisfy a reference but you want to manually select which reference is used. 132 | - `Filter` allows you to implement custom logic to filter references. See **Filters** below. 133 | - `EditableAnywhere` has the same effect as `Editable` but will not validate the supplied reference. This allows you to manually specify a reference with an automatic fallback. 134 | ### Filters 135 | 136 | This project also provides support for filtering references based on custom logic. 137 | 138 | Let's use an example to demonstrate: imagine you have a `StealthAnimations` class which applies stealth-related game logic to all child `Animators`. 139 | 140 | ```cs 141 | public class StealthAnimations : MonoBehaviour 142 | { 143 | private static readonly int ANIMATOR_PARAM_STEALTH_STATE = Animator.StringToHash("StealthState"); 144 | 145 | [SerializeField, Child] private Animator[] _animators; 146 | 147 | private void Update() 148 | { 149 | int state = GetStealthState(); 150 | for (int i = 0; i < this._animators.Length; i++) 151 | this._animators[i].SetInteger(ANIMATOR_PARAM_STEALTH_STATE, state); 152 | } 153 | } 154 | ``` 155 | 156 | This will reference all child animators and apply the `StealthState` integer each frame. The issue here is that not all child animators have a `StealthState` parameter. One way to solve this would be to check each animator to see if it has the parameter, but this would be inefficient, especially if we have many child animators but only a few have the parameter. 157 | 158 | Instead, let's pre-filter the animators by implementing a `SceneRefFilter` and setting the `filter` parameter on the attribute: 159 | 160 | ```cs 161 | public class StealthAnimations : MonoBehaviour 162 | { 163 | 164 | private class AnimatorRefFilter : SceneRefFilter 165 | { 166 | public override bool IncludeSceneRef(Animator animator) 167 | => AnimatorUtils.HasParameter(animator, ANIMATOR_PARAM_STEALTH_STATE); 168 | } 169 | 170 | [SerializeField, Child(filter: typeof(AnimatorRefFilter))] 171 | private Animator[] _animators; 172 | } 173 | ``` 174 | 175 | Our custom `AnimatorRefFilter` will be invoked for each `Animator` matching our ref attribute (`Child`) and flags, but only the ones where `true` is returned will be included. 176 | 177 | You can apply this pattern to any type and any condition, allowing you to pre-filter references to ensure they are what you'll actually need at runtime. 178 | 179 | Additional examples: 180 | - Filter to include/exclude trigger colliders 181 | - Filter to include/exclude if another component is present 182 | - Filter to include/exclude static gameObjects 183 | - etc. 184 | 185 | **Note:** this filter is only invoked at edit time, so you can't rely on any runtime information for filtering. In that case you will still need to filter your references in Awake or similar. 186 | 187 | ### Features 188 | 189 | - Supports all `MonoBehaviour` and `Component` types (basically anything you can use with `GetComponent`), plus interfaces! 190 | - Determinate results, so there's no worry of Unity forgetting all your serialised references (which has happened to me a few times on upgrading editor versions). All references will be reassigned automatically. 191 | - Fast, this tool won't slow down your editor or generate excessive garbage. 192 | - Fully serialised at edit time, so all references are already available at runtime 193 | - The `ValidateRefs()` function is made available as a `MonoBehaviour` extension for ease of use. You can also invoke it outside of a `MonoBehaviour` like so, if preferred: 194 | ```cs 195 | MyScript script = ...; 196 | SceneRefAttributeValidator.Validate(script); 197 | ``` 198 | - One-click to regenerate all references for every script under `Tools > Validate All Refs`. 199 | - Regenerate references for any component by right-clicking the component and selecting `Validate Refs`. 200 | ![image](https://user-images.githubusercontent.com/2164691/215190393-192083fc-4c83-42da-8ca4-a93d2349aaa2.png) 201 | 202 | ### License 203 | 204 | This project is made available under the [MIT License](./LICENSE), so you're free to use it for any purpose. 205 | -------------------------------------------------------------------------------- /README.md.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 7be8e707b0793c34f8fb07636624c793 3 | TextScriptImporter: 4 | externalObjects: {} 5 | userData: 6 | assetBundleName: 7 | assetBundleVariant: 8 | -------------------------------------------------------------------------------- /ReflectionUtil.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Reflection; 4 | 5 | namespace KBCore.Refs 6 | { 7 | internal static class ReflectionUtil 8 | { 9 | internal struct AttributedField 10 | where T : Attribute 11 | { 12 | public T Attribute; 13 | public FieldInfo FieldInfo; 14 | } 15 | 16 | internal static void GetFieldsWithAttributeFromType( 17 | Type classToInspect, 18 | IList> output, 19 | BindingFlags reflectionFlags = BindingFlags.Default 20 | ) 21 | where T : Attribute 22 | { 23 | Type type = typeof(T); 24 | do 25 | { 26 | FieldInfo[] allFields = classToInspect.GetFields(reflectionFlags); 27 | for (int f = 0; f < allFields.Length; f++) 28 | { 29 | FieldInfo fieldInfo = allFields[f]; 30 | Attribute[] attributes = Attribute.GetCustomAttributes(fieldInfo); 31 | for (int a = 0; a < attributes.Length; a++) 32 | { 33 | Attribute attribute = attributes[a]; 34 | if (!type.IsInstanceOfType(attribute)) 35 | continue; 36 | 37 | output.Add(new AttributedField 38 | { 39 | Attribute = attribute as T, 40 | FieldInfo = fieldInfo 41 | }); 42 | break; 43 | } 44 | } 45 | 46 | classToInspect = classToInspect.BaseType; 47 | } 48 | while (classToInspect != null); 49 | } 50 | } 51 | } -------------------------------------------------------------------------------- /ReflectionUtil.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: c4da11995b904a69a4c76be19d32803c 3 | timeCreated: 1673101291 -------------------------------------------------------------------------------- /SceneRefAttribute.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using UnityEngine; 3 | 4 | namespace KBCore.Refs 5 | { 6 | /// 7 | /// RefLoc indicates the expected location of the reference. 8 | /// 9 | internal enum RefLoc 10 | { 11 | /// 12 | /// Anywhere will only validate the reference isn't null, but relies on you to 13 | /// manually assign the reference yourself. 14 | /// 15 | Anywhere = -1, 16 | /// 17 | /// Self looks for the reference on the same game object as the attributed component 18 | /// using GetComponent(s)() 19 | /// 20 | Self = 0, 21 | /// 22 | /// Parent looks for the reference on the parent hierarchy of the attributed components game object 23 | /// using GetComponent(s)InParent() 24 | /// 25 | Parent = 1, 26 | /// 27 | /// Child looks for the reference on the child hierarchy of the attributed components game object 28 | /// using GetComponent(s)InChildren() 29 | /// 30 | Child = 2, 31 | /// 32 | /// Scene looks for the reference anywhere in the scene 33 | /// using GameObject.FindAnyObjectByType() and GameObject.FindObjectsOfType() 34 | /// 35 | Scene = 4, 36 | } 37 | 38 | /// 39 | /// Optional flags offering additional functionality. 40 | /// 41 | [Flags] 42 | public enum Flag 43 | { 44 | /// 45 | /// Default behaviour. 46 | /// 47 | None = 0, 48 | /// 49 | /// Allow empty (or null in the case of non-array types) results. 50 | /// 51 | Optional = 1 << 0, 52 | /// 53 | /// Include inactive components in the results (only applies to Child and Parent). 54 | /// 55 | IncludeInactive = 1 << 1, 56 | /// 57 | /// Allows the user to override the automatic selection. Will still validate that 58 | /// the field location (self, child, etc) matches as expected. 59 | /// 60 | Editable = 1 << 2, 61 | /// 62 | /// Excludes components on current GameObject from search(only applies to Child and Parent). 63 | /// 64 | ExcludeSelf = 1 << 3, 65 | /// 66 | /// Allows the user to manually set the reference and does not validate the location if manually set 67 | /// 68 | EditableAnywhere = 1 << 4 | Editable 69 | } 70 | 71 | /// 72 | /// Attribute allowing you to decorate component reference fields with their search criteria. 73 | /// 74 | [AttributeUsage(AttributeTargets.Field)] 75 | public abstract class SceneRefAttribute : PropertyAttribute 76 | { 77 | internal RefLoc Loc { get; } 78 | internal Flag Flags { get; } 79 | 80 | internal SceneRefFilter Filter 81 | { 82 | get 83 | { 84 | if (this._filterType == null) 85 | return null; 86 | return (SceneRefFilter) Activator.CreateInstance(this._filterType); 87 | } 88 | } 89 | 90 | private readonly Type _filterType; 91 | 92 | internal SceneRefAttribute( 93 | RefLoc loc, 94 | Flag flags, 95 | Type filter 96 | ) 97 | { 98 | this.Loc = loc; 99 | this.Flags = flags; 100 | this._filterType = filter; 101 | } 102 | 103 | internal bool HasFlags(Flag flags) 104 | => (this.Flags & flags) == flags; 105 | } 106 | 107 | /// 108 | /// Anywhere will only validate the reference isn't null, but relies on you to 109 | /// manually assign the reference yourself. 110 | /// 111 | [AttributeUsage(AttributeTargets.Field)] 112 | public class AnywhereAttribute : SceneRefAttribute 113 | { 114 | public AnywhereAttribute(Flag flags = Flag.None, Type filter = null) 115 | : base(RefLoc.Anywhere, flags, filter) 116 | {} 117 | } 118 | 119 | /// 120 | /// Self looks for the reference on the same game object as the attributed component 121 | /// using GetComponent(s)() 122 | /// 123 | [AttributeUsage(AttributeTargets.Field)] 124 | public class SelfAttribute : SceneRefAttribute 125 | { 126 | public SelfAttribute(Flag flags = Flag.None, Type filter = null) 127 | : base(RefLoc.Self, flags, filter) 128 | {} 129 | } 130 | 131 | /// 132 | /// Parent looks for the reference on the parent hierarchy of the attributed components game object 133 | /// using GetComponent(s)InParent() 134 | /// 135 | [AttributeUsage(AttributeTargets.Field)] 136 | public class ParentAttribute : SceneRefAttribute 137 | { 138 | public ParentAttribute(Flag flags = Flag.None, Type filter = null) 139 | : base(RefLoc.Parent, flags, filter) 140 | {} 141 | } 142 | 143 | /// 144 | /// Child looks for the reference on the child hierarchy of the attributed components game object 145 | /// using GetComponent(s)InChildren() 146 | /// 147 | [AttributeUsage(AttributeTargets.Field)] 148 | public class ChildAttribute : SceneRefAttribute 149 | { 150 | public ChildAttribute(Flag flags = Flag.None, Type filter = null) 151 | : base(RefLoc.Child, flags, filter) 152 | {} 153 | } 154 | 155 | /// 156 | /// Scene looks for the reference anywhere in the scene 157 | /// using GameObject.FindAnyObjectByType() and GameObject.FindObjectsOfType() 158 | /// 159 | [AttributeUsage(AttributeTargets.Field)] 160 | public class SceneAttribute : SceneRefAttribute 161 | { 162 | public SceneAttribute(Flag flags = Flag.None, Type filter = null) 163 | : base(RefLoc.Scene, flags, filter) 164 | {} 165 | } 166 | } -------------------------------------------------------------------------------- /SceneRefAttribute.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 1306d5685bce4dceb69f6b4d71d1d093 3 | timeCreated: 1673101477 -------------------------------------------------------------------------------- /SceneRefAttributePropertyDrawer.cs: -------------------------------------------------------------------------------- 1 | #if UNITY_EDITOR 2 | using UnityEditor; 3 | using UnityEngine; 4 | using System; 5 | using System.Linq; 6 | #if UNITY_2022_2_OR_NEWER 7 | using UnityEngine.UIElements; 8 | using UnityEditor.UIElements; 9 | #endif 10 | 11 | namespace KBCore.Refs 12 | { 13 | /// 14 | /// Custom property drawer for the reference attributes, making them read-only. 15 | /// 16 | /// Note: Does not apply to the Anywhere attribute as that needs to remain editable. 17 | /// 18 | [CustomPropertyDrawer(typeof(SelfAttribute))] 19 | [CustomPropertyDrawer(typeof(ChildAttribute))] 20 | [CustomPropertyDrawer(typeof(ParentAttribute))] 21 | [CustomPropertyDrawer(typeof(SceneAttribute))] 22 | public class SceneRefAttributePropertyDrawer : PropertyDrawer 23 | { 24 | 25 | private bool _isInitialized; 26 | private bool _canValidateType; 27 | private Type _elementType; 28 | private string _typeName; 29 | 30 | private SceneRefAttribute _sceneRefAttribute => (SceneRefAttribute) attribute; 31 | private bool _editable => this._sceneRefAttribute.HasFlags(Flag.Editable); 32 | 33 | // unity 2022.2 makes UIToolkit the default for inspectors 34 | #if UNITY_2022_2_OR_NEWER 35 | private const string SCENE_REF_CLASS = "kbcore-refs-sceneref"; 36 | 37 | private PropertyField _propertyField; 38 | private HelpBox _helpBox; 39 | private InspectorElement _inspectorElement; 40 | private SerializedProperty _serializedProperty; 41 | 42 | public override VisualElement CreatePropertyGUI(SerializedProperty property) 43 | { 44 | this._serializedProperty = property; 45 | this.Initialize(property); 46 | 47 | VisualElement root = new(); 48 | root.AddToClassList(SCENE_REF_CLASS); 49 | 50 | this._helpBox = new HelpBox("", HelpBoxMessageType.Error); 51 | this._helpBox.style.display = DisplayStyle.None; 52 | root.Add(this._helpBox); 53 | 54 | this._propertyField = new PropertyField(property); 55 | this._propertyField.SetEnabled(this._editable); 56 | root.Add(this._propertyField); 57 | 58 | if (this._canValidateType) 59 | { 60 | this.UpdateHelpBox(); 61 | this._propertyField.RegisterCallback(this.OnAttach); 62 | } 63 | return root; 64 | } 65 | 66 | private void OnAttach(AttachToPanelEvent attachToPanelEvent) 67 | { 68 | this._propertyField.UnregisterCallback(this.OnAttach); 69 | this._inspectorElement = this._propertyField.GetFirstAncestorOfType(); 70 | if (this._inspectorElement == null) 71 | // not in an inspector, invalid 72 | return; 73 | 74 | // subscribe to SerializedPropertyChangeEvent so we can update when the property changes 75 | this._inspectorElement.RegisterCallback(this.OnSerializedPropertyChangeEvent); 76 | this._propertyField.RegisterCallback(this.OnDetach); 77 | } 78 | 79 | private void OnDetach(DetachFromPanelEvent detachFromPanelEvent) 80 | { 81 | // unregister from all callbacks 82 | this._propertyField.UnregisterCallback(this.OnDetach); 83 | this._inspectorElement.UnregisterCallback(this.OnSerializedPropertyChangeEvent); 84 | this._serializedProperty = null; 85 | } 86 | 87 | private void OnSerializedPropertyChangeEvent(SerializedPropertyChangeEvent changeEvent) 88 | { 89 | if (changeEvent.changedProperty != this._serializedProperty) 90 | return; 91 | this.UpdateHelpBox(); 92 | } 93 | 94 | private void UpdateHelpBox() 95 | { 96 | bool isSatisfied = this.IsSatisfied(this._serializedProperty); 97 | this._helpBox.style.display = isSatisfied ? DisplayStyle.None : DisplayStyle.Flex; 98 | string message = $"Missing {this._serializedProperty.propertyPath} ({this._typeName}) reference on {this._sceneRefAttribute.Loc}!"; 99 | this._helpBox.text = message; 100 | } 101 | #endif 102 | 103 | public override void OnGUI(Rect position, SerializedProperty property, GUIContent label) 104 | { 105 | if (!this._isInitialized) 106 | this.Initialize(property); 107 | 108 | if (!this.IsSatisfied(property)) 109 | { 110 | Rect helpBoxPos = position; 111 | helpBoxPos.height = EditorGUIUtility.singleLineHeight * 2; 112 | string message = $"Missing {property.propertyPath} ({this._typeName}) reference on {this._sceneRefAttribute.Loc}!"; 113 | EditorGUI.HelpBox(helpBoxPos, message, MessageType.Error); 114 | position.height = EditorGUI.GetPropertyHeight(property, label); 115 | position.y += helpBoxPos.height; 116 | } 117 | 118 | bool wasEnabled = GUI.enabled; 119 | GUI.enabled = this._editable; 120 | EditorGUI.PropertyField(position, property, label, true); 121 | GUI.enabled = wasEnabled; 122 | } 123 | 124 | private void Initialize(SerializedProperty property) 125 | { 126 | this._isInitialized = true; 127 | 128 | // the type won't change, so we only need to initialize these values once 129 | this._elementType = this.fieldInfo.FieldType; 130 | if (typeof(ISerializableRef).IsAssignableFrom(this._elementType)) 131 | { 132 | Type interfaceType = this._elementType.GetInterfaces().FirstOrDefault(type => 133 | type.IsGenericType && type.GetGenericTypeDefinition() == typeof(ISerializableRef<>)); 134 | if (interfaceType != null) 135 | this._elementType = interfaceType.GetGenericArguments()[0]; 136 | } 137 | 138 | this._canValidateType = typeof(Component).IsAssignableFrom(this._elementType) 139 | && property.propertyType == SerializedPropertyType.ObjectReference; 140 | 141 | this._typeName = this.fieldInfo.FieldType.Name; 142 | if (this.fieldInfo.FieldType.IsGenericType && this.fieldInfo.FieldType.GenericTypeArguments.Length >= 1) 143 | this._typeName = this._typeName.Replace("`1", $"<{this.fieldInfo.FieldType.GenericTypeArguments[0].Name}>"); 144 | } 145 | 146 | /// Is this field Satisfied with a value or optional 147 | private bool IsSatisfied(SerializedProperty property) 148 | { 149 | if (!this._canValidateType || this._sceneRefAttribute.HasFlags(Flag.Optional)) 150 | return true; 151 | return property.objectReferenceValue != null; 152 | } 153 | 154 | public override float GetPropertyHeight(SerializedProperty property, GUIContent label) 155 | { 156 | float helpBoxHeight = 0; 157 | if (!this.IsSatisfied(property)) 158 | helpBoxHeight = EditorGUIUtility.singleLineHeight * 2; 159 | return EditorGUI.GetPropertyHeight(property, label) + helpBoxHeight; 160 | } 161 | } 162 | } 163 | #endif 164 | -------------------------------------------------------------------------------- /SceneRefAttributePropertyDrawer.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 652a8bf2e8604b1f9b9a1f71280f65c3 3 | timeCreated: 1674300748 -------------------------------------------------------------------------------- /SceneRefAttributeValidator.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Reflection; 5 | using UnityEngine; 6 | using Object = UnityEngine.Object; 7 | using System.Collections; 8 | using System.Diagnostics.CodeAnalysis; 9 | 10 | #if UNITY_EDITOR 11 | 12 | using UnityEditor; 13 | 14 | #endif 15 | 16 | namespace KBCore.Refs 17 | { 18 | public static class SceneRefAttributeValidator 19 | { 20 | private static readonly IList> ATTRIBUTED_FIELDS_CACHE = new List>(); 21 | 22 | #if UNITY_EDITOR 23 | 24 | /// 25 | /// Validate all references for every script and every game object in the scene. 26 | /// 27 | [MenuItem("Tools/KBCore/Validate All Refs")] 28 | public static bool ValidateAllRefs() 29 | { 30 | var validationSuccess = true; 31 | MonoScript[] scripts = MonoImporter.GetAllRuntimeMonoScripts(); 32 | for (int i = 0; i < scripts.Length; i++) 33 | { 34 | MonoScript runtimeMonoScript = scripts[i]; 35 | Type scriptType = runtimeMonoScript.GetClass(); 36 | if (scriptType == null) 37 | { 38 | continue; 39 | } 40 | 41 | try 42 | { 43 | ReflectionUtil.GetFieldsWithAttributeFromType( 44 | scriptType, 45 | ATTRIBUTED_FIELDS_CACHE, 46 | BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance 47 | ); 48 | if (ATTRIBUTED_FIELDS_CACHE.Count == 0) 49 | { 50 | continue; 51 | } 52 | 53 | #if UNITY_2021_3_18_OR_NEWER 54 | Object[] objects = Object.FindObjectsByType(scriptType, FindObjectsInactive.Include, FindObjectsSortMode.InstanceID); 55 | #elif UNITY_2020_1_OR_NEWER 56 | Object[] objects = Object.FindObjectsOfType(scriptType, true); 57 | #else 58 | Object[] objects = Object.FindObjectsOfType(scriptType); 59 | #endif 60 | 61 | if (objects.Length == 0) 62 | { 63 | continue; 64 | } 65 | 66 | Debug.Log($"Validating {ATTRIBUTED_FIELDS_CACHE.Count} field(s) on {objects.Length} {objects[0].GetType().Name} instance(s)"); 67 | for (int o = 0; o < objects.Length; o++) 68 | { 69 | validationSuccess &= Validate(objects[o] as MonoBehaviour, ATTRIBUTED_FIELDS_CACHE, false); 70 | } 71 | } 72 | finally 73 | { 74 | ATTRIBUTED_FIELDS_CACHE.Clear(); 75 | } 76 | } 77 | return validationSuccess; 78 | } 79 | 80 | /// 81 | /// Validate a single components references, attempting to assign missing references 82 | /// and logging errors as necessary. 83 | /// 84 | [MenuItem("CONTEXT/Component/Validate Refs")] 85 | [SuppressMessage("CodeQuality", "IDE0051:Remove unused private members", Justification = "Used as menu item action")] 86 | private static void ValidateRefs(MenuCommand menuCommand) 87 | => Validate(menuCommand.context as Component); 88 | 89 | /// 90 | /// Clean and validate a single components references. Useful in instances where (for example) Unity has 91 | /// incorrectly serialized a scene reference within a prefab. 92 | /// 93 | [MenuItem("CONTEXT/Component/Clean and Validate Refs")] 94 | [SuppressMessage("CodeQuality", "IDE0051:Remove unused private members", Justification = "Used as menu item action")] 95 | private static void CleanValidateRefs(MenuCommand menuCommand) 96 | => CleanValidate(menuCommand.context as Component); 97 | 98 | #endif 99 | 100 | /// 101 | /// Validate a single components references, attempting to assign missing references 102 | /// and logging errors as necessary. 103 | /// 104 | public static void ValidateRefs(this Component c, bool updateAtRuntime = false) 105 | => Validate(c, updateAtRuntime); 106 | 107 | /// 108 | /// Validate a single components references, attempting to assign missing references 109 | /// and logging errors as necessary. 110 | /// 111 | public static void Validate(Component c, bool updateAtRuntime = false) 112 | { 113 | try 114 | { 115 | ReflectionUtil.GetFieldsWithAttributeFromType( 116 | c.GetType(), 117 | ATTRIBUTED_FIELDS_CACHE, 118 | BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance 119 | ); 120 | Validate(c, ATTRIBUTED_FIELDS_CACHE, updateAtRuntime); 121 | } 122 | finally 123 | { 124 | ATTRIBUTED_FIELDS_CACHE.Clear(); 125 | } 126 | } 127 | 128 | /// 129 | /// Clean and validate a single components references. Useful in instances where (for example) Unity has 130 | /// incorrectly serialized a scene reference within a prefab. 131 | /// 132 | public static void CleanValidate(Component c, bool updateAtRuntime = false) 133 | { 134 | try 135 | { 136 | ReflectionUtil.GetFieldsWithAttributeFromType( 137 | c.GetType(), 138 | ATTRIBUTED_FIELDS_CACHE, 139 | BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance 140 | ); 141 | Clean(c, ATTRIBUTED_FIELDS_CACHE); 142 | Validate(c, ATTRIBUTED_FIELDS_CACHE, updateAtRuntime); 143 | } 144 | finally 145 | { 146 | ATTRIBUTED_FIELDS_CACHE.Clear(); 147 | } 148 | } 149 | 150 | private static bool Validate( 151 | Component c, 152 | IList> requiredFields, 153 | bool updateAtRuntime 154 | ) 155 | { 156 | if (requiredFields.Count == 0) 157 | { 158 | Debug.LogWarning($"{c.GetType().Name} has no required fields", c.gameObject); 159 | return true; 160 | } 161 | 162 | var validationSuccess = true; 163 | bool isUninstantiatedPrefab = PrefabUtil.IsUninstantiatedPrefab(c.gameObject); 164 | for (int i = 0; i < requiredFields.Count; i++) 165 | { 166 | ReflectionUtil.AttributedField attributedField = requiredFields[i]; 167 | SceneRefAttribute attribute = attributedField.Attribute; 168 | FieldInfo field = attributedField.FieldInfo; 169 | 170 | if (field.FieldType.IsInterface) 171 | { 172 | throw new Exception($"{c.GetType().Name} cannot serialize interface {field.Name} directly, use InterfaceRef instead"); 173 | } 174 | 175 | object fieldValue = field.GetValue(c); 176 | if (updateAtRuntime || !Application.isPlaying) 177 | { 178 | fieldValue = UpdateRef(attribute, c, field, fieldValue); 179 | } 180 | 181 | if (isUninstantiatedPrefab) 182 | { 183 | continue; 184 | } 185 | 186 | validationSuccess &= ValidateRef(attribute, c, field, fieldValue); 187 | } 188 | return validationSuccess; 189 | } 190 | 191 | private static void Clean( 192 | Component c, 193 | IList> requiredFields 194 | ) 195 | { 196 | for (int i = 0; i < requiredFields.Count; i++) 197 | { 198 | ReflectionUtil.AttributedField attributedField = requiredFields[i]; 199 | SceneRefAttribute attribute = attributedField.Attribute; 200 | if (attribute.Loc == RefLoc.Anywhere) 201 | { 202 | continue; 203 | } 204 | 205 | FieldInfo field = attributedField.FieldInfo; 206 | field.SetValue(c, null); 207 | #if UNITY_EDITOR 208 | EditorUtility.SetDirty(c); 209 | #endif 210 | } 211 | } 212 | 213 | private static object UpdateRef( 214 | SceneRefAttribute attr, 215 | Component component, 216 | FieldInfo field, 217 | object existingValue 218 | ) 219 | { 220 | Type fieldType = field.FieldType; 221 | bool excludeSelf = attr.HasFlags(Flag.ExcludeSelf); 222 | bool isCollection = IsCollectionType(fieldType, out bool _, out bool isList); 223 | bool includeInactive = attr.HasFlags(Flag.IncludeInactive); 224 | 225 | ISerializableRef iSerializable = null; 226 | if (typeof(ISerializableRef).IsAssignableFrom(fieldType)) 227 | { 228 | iSerializable = (ISerializableRef)(existingValue ?? Activator.CreateInstance(fieldType)); 229 | fieldType = iSerializable.RefType; 230 | existingValue = iSerializable.SerializedObject; 231 | } 232 | 233 | if (attr.HasFlags(Flag.Editable)) 234 | { 235 | bool isFilledArray = isCollection && (existingValue as IEnumerable).CountEnumerable() > 0; 236 | if (isFilledArray || existingValue is Object) 237 | { 238 | // If the field is editable and the value has already been set, keep it. 239 | return existingValue; 240 | } 241 | } 242 | 243 | Type elementType = fieldType; 244 | if (isCollection) 245 | { 246 | elementType = GetElementType(fieldType); 247 | if (typeof(ISerializableRef).IsAssignableFrom(elementType)) 248 | { 249 | Type interfaceType = elementType? 250 | .GetInterfaces() 251 | .FirstOrDefault(type => 252 | type.IsGenericType && 253 | type.GetGenericTypeDefinition() == typeof(ISerializableRef<>)); 254 | 255 | if (interfaceType != null) 256 | { 257 | elementType = interfaceType.GetGenericArguments()[0]; 258 | } 259 | } 260 | } 261 | 262 | object value = null; 263 | 264 | //INFO: when minimal unity version will be sufficiently high, explicit casts to object will not be necessary. 265 | switch (attr.Loc) 266 | { 267 | case RefLoc.Anywhere: 268 | if (isCollection ? typeof(ISerializableRef).IsAssignableFrom(fieldType.GetElementType()) : iSerializable != null) 269 | { 270 | value = isCollection 271 | ? (object)(existingValue as ISerializableRef[])?.Select(existingRef => GetComponentIfWrongType(existingRef.SerializedObject, elementType)).ToArray() 272 | : (object)GetComponentIfWrongType(existingValue, elementType); 273 | } 274 | break; 275 | 276 | case RefLoc.Self: 277 | value = isCollection 278 | ? (object)component.GetComponents(elementType) 279 | : (object)component.GetComponent(elementType); 280 | break; 281 | 282 | case RefLoc.Parent: 283 | value = isCollection 284 | ? (object)GetComponentsInParent(component, elementType, includeInactive, excludeSelf) 285 | : (object)GetComponentInParent(component, elementType, includeInactive, excludeSelf); 286 | break; 287 | 288 | case RefLoc.Child: 289 | value = isCollection 290 | ? (object)GetComponentsInChildren(component, elementType, includeInactive, excludeSelf) 291 | : (object)GetComponentInChildren(component, elementType, includeInactive, excludeSelf); 292 | break; 293 | 294 | case RefLoc.Scene: 295 | value = GetComponentsInScene(elementType, includeInactive, isCollection, excludeSelf); 296 | break; 297 | default: 298 | throw new Exception($"Unhandled Loc={attr.Loc}"); 299 | } 300 | 301 | if (value == null) 302 | { 303 | return existingValue; 304 | } 305 | 306 | SceneRefFilter filter = attr.Filter; 307 | 308 | if (isCollection) 309 | { 310 | Type realElementType = GetElementType(fieldType); 311 | 312 | Array componentArray = (Array)value; 313 | if (filter != null) 314 | { 315 | // TODO: probably a better way to do this without allocating a list 316 | IList list = new List(); 317 | foreach (object o in componentArray) 318 | { 319 | if (filter.IncludeSceneRef(o)) 320 | { 321 | list.Add(o); 322 | } 323 | } 324 | componentArray = list.ToArray(); 325 | } 326 | 327 | Array typedArray = Array.CreateInstance( 328 | realElementType ?? throw new InvalidOperationException(), 329 | componentArray.Length 330 | ); 331 | 332 | if (elementType == realElementType) 333 | { 334 | Array.Copy(componentArray, typedArray, typedArray.Length); 335 | value = typedArray; 336 | } 337 | else if (typeof(ISerializableRef).IsAssignableFrom(realElementType)) 338 | { 339 | for (int i = 0; i < typedArray.Length; i++) 340 | { 341 | ISerializableRef elementValue = Activator.CreateInstance(realElementType) as ISerializableRef; 342 | elementValue?.OnSerialize(componentArray.GetValue(i)); 343 | typedArray.SetValue(elementValue, i); 344 | } 345 | value = typedArray; 346 | } 347 | } 348 | else if (filter?.IncludeSceneRef(value) == false) 349 | { 350 | iSerializable?.Clear(); 351 | #if UNITY_EDITOR 352 | if (existingValue != null) 353 | { 354 | EditorUtility.SetDirty(component); 355 | } 356 | #endif 357 | return null; 358 | } 359 | 360 | if (iSerializable == null) 361 | { 362 | bool valueIsEqual = existingValue != null && 363 | isCollection ? Enumerable.SequenceEqual((IEnumerable)value, (IEnumerable)existingValue) : value.Equals(existingValue); 364 | if (valueIsEqual) 365 | { 366 | return existingValue; 367 | } 368 | 369 | if (isList) 370 | { 371 | Type listType = typeof(List<>); 372 | Type[] typeArgs = { fieldType.GenericTypeArguments[0] }; 373 | Type constructedType = listType.MakeGenericType(typeArgs); 374 | 375 | object newList = Activator.CreateInstance(constructedType); 376 | 377 | MethodInfo addMethod = newList.GetType().GetMethod(nameof(List.Add)); 378 | 379 | foreach (object s in (IEnumerable)value) 380 | { 381 | addMethod.Invoke(newList, new [] { s }); 382 | } 383 | 384 | field.SetValue(component, newList); 385 | } 386 | else 387 | { 388 | field.SetValue(component, value); 389 | } 390 | } 391 | else 392 | { 393 | if (!iSerializable.OnSerialize(value)) 394 | { 395 | return existingValue; 396 | } 397 | } 398 | 399 | #if UNITY_EDITOR 400 | EditorUtility.SetDirty(component); 401 | #endif 402 | return value; 403 | } 404 | 405 | private static Type GetElementType(Type fieldType) 406 | { 407 | if (fieldType.IsArray) 408 | { 409 | return fieldType.GetElementType(); 410 | } 411 | return fieldType.GenericTypeArguments[0]; 412 | } 413 | 414 | private static object GetComponentIfWrongType(object existingValue, Type elementType) 415 | { 416 | if (existingValue is Component existingComponent && existingComponent && !elementType.IsInstanceOfType(existingValue)) 417 | { 418 | return existingComponent.GetComponent(elementType); 419 | } 420 | 421 | return existingValue; 422 | } 423 | 424 | private static bool ValidateRef(SceneRefAttribute attr, Component c, FieldInfo field, object value) 425 | { 426 | Type fieldType = field.FieldType; 427 | bool isCollection = IsCollectionType(fieldType, out bool _, out bool _); 428 | bool isOverridable = attr.HasFlags(Flag.EditableAnywhere); 429 | 430 | if (value is ISerializableRef ser) 431 | { 432 | value = ser.SerializedObject; 433 | } 434 | 435 | if (IsEmptyOrNull(value, isCollection)) 436 | { 437 | if (attr.HasFlags(Flag.Optional)) 438 | return true; 439 | 440 | Type elementType = isCollection ? fieldType.GetElementType() : fieldType; 441 | elementType = typeof(ISerializableRef).IsAssignableFrom(elementType) ? elementType?.GetGenericArguments()[0] : elementType; 442 | Debug.LogError($"{c.GetType().Name} missing required {elementType?.Name + (isCollection ? "[]" : "")} ref '{field.Name}'", c.gameObject); 443 | 444 | return false; 445 | } 446 | 447 | if (isCollection) 448 | { 449 | var validationSuccess = true; 450 | IEnumerable a = (IEnumerable)value; 451 | IEnumerator enumerator = a.GetEnumerator(); 452 | 453 | Type elementType = fieldType.GetElementType(); 454 | 455 | while (enumerator.MoveNext()) 456 | { 457 | object o = enumerator.Current; 458 | if (o is ISerializableRef serObj) 459 | { 460 | o = serObj.SerializedObject; 461 | } 462 | 463 | if (o != null) 464 | { 465 | if (isOverridable) 466 | continue; 467 | 468 | if (attr.HasFlags(Flag.ExcludeSelf) && o is Component valueC && 469 | valueC.gameObject == c.gameObject) 470 | Debug.LogError($"{c.GetType().Name} {elementType?.Name}[] ref '{field.Name}' cannot contain component from the same GameObject", c.gameObject); 471 | 472 | validationSuccess &= ValidateRefLocation(attr.Loc, c, field, o); 473 | } 474 | else 475 | { 476 | Debug.LogError($"{c.GetType().Name} missing required element ref in array '{field.Name}'", c.gameObject); 477 | validationSuccess = false; 478 | } 479 | } 480 | return validationSuccess; 481 | } 482 | else 483 | { 484 | if (isOverridable) 485 | return true; 486 | 487 | if (attr.HasFlags(Flag.ExcludeSelf) && value is Component valueC && valueC.gameObject == c.gameObject) 488 | Debug.LogError($"{c.GetType().Name} {fieldType.Name} ref '{field.Name}' cannot be on the same GameObject", c.gameObject); 489 | 490 | return ValidateRefLocation(attr.Loc, c, field, value); 491 | } 492 | } 493 | 494 | private static bool ValidateRefLocation(RefLoc loc, Component c, FieldInfo field, object refObj) 495 | { 496 | switch (refObj) 497 | { 498 | case Component valueC: 499 | return ValidateRefLocation(loc, c, field, valueC); 500 | 501 | case ScriptableObject _: 502 | return ValidateRefLocationAnywhere(loc, c, field); 503 | 504 | case GameObject _: 505 | return ValidateRefLocationAnywhere(loc, c, field); 506 | 507 | default: 508 | throw new Exception($"{c.GetType().Name} has unexpected reference type {refObj?.GetType().Name}"); 509 | } 510 | } 511 | 512 | private static bool ValidateRefLocation(RefLoc loc, Component c, FieldInfo field, Component refObj) 513 | { 514 | switch (loc) 515 | { 516 | case RefLoc.Anywhere: 517 | break; 518 | 519 | case RefLoc.Self: 520 | if (refObj.gameObject != c.gameObject) 521 | { 522 | Debug.LogError($"{c.GetType().Name} requires {field.FieldType.Name} ref '{field.Name}' to be on Self", c.gameObject); 523 | return false; 524 | } 525 | 526 | break; 527 | 528 | case RefLoc.Parent: 529 | if (!c.transform.IsChildOf(refObj.transform)) 530 | { 531 | Debug.LogError($"{c.GetType().Name} requires {field.FieldType.Name} ref '{field.Name}' to be a Parent", c.gameObject); 532 | return false; 533 | } 534 | 535 | break; 536 | 537 | case RefLoc.Child: 538 | if (!refObj.transform.IsChildOf(c.transform)) 539 | { 540 | Debug.LogError($"{c.GetType().Name} requires {field.FieldType.Name} ref '{field.Name}' to be a Child", c.gameObject); 541 | return false; 542 | } 543 | 544 | break; 545 | 546 | case RefLoc.Scene: 547 | if (c == null) 548 | { 549 | Debug.LogError($"{c.GetType().Name} requires {field.FieldType.Name} ref '{field.Name}' to be in the scene", c.gameObject); 550 | return false; 551 | } 552 | break; 553 | 554 | default: 555 | throw new Exception($"Unhandled Loc={loc}"); 556 | } 557 | return true; 558 | } 559 | 560 | private static bool ValidateRefLocationAnywhere(RefLoc loc, Component c, FieldInfo field) 561 | { 562 | switch (loc) 563 | { 564 | case RefLoc.Anywhere: 565 | return true; 566 | 567 | case RefLoc.Self: 568 | case RefLoc.Parent: 569 | case RefLoc.Child: 570 | case RefLoc.Scene: 571 | Debug.LogError($"{c.GetType().Name} requires {field.FieldType.Name} ref '{field.Name}' to be Anywhere", c.gameObject); 572 | return false; 573 | 574 | default: 575 | throw new Exception($"Unhandled Loc={loc}"); 576 | } 577 | } 578 | 579 | private static bool IsEmptyOrNull(object obj, bool isCollection) 580 | { 581 | if (obj is ISerializableRef ser) 582 | { 583 | return !ser.HasSerializedObject; 584 | } 585 | 586 | return obj == null || obj.Equals(null) || (isCollection && ((IEnumerable) obj).CountEnumerable() == 0); 587 | } 588 | 589 | private static bool IsCollectionType(Type t, out bool isArray, out bool isList) 590 | { 591 | isList = t.IsGenericType && t.GetGenericTypeDefinition() == typeof(List<>); 592 | isArray = t.IsArray; 593 | return isList || isArray; 594 | } 595 | 596 | private static Component[] GetComponentsInParent(Component c, Type elementType, bool includeInactive, bool excludeSelf) 597 | { 598 | var element = c; 599 | if (excludeSelf) 600 | element = c.transform.parent; 601 | 602 | return element == null 603 | ? Array.Empty() 604 | : element.GetComponentsInParent(elementType, includeInactive); 605 | } 606 | 607 | private static Component GetComponentInParent(Component c, Type elementType, bool includeInactive, 608 | bool excludeSelf) 609 | { 610 | var element = c; 611 | if (excludeSelf) 612 | element = c.transform.parent; 613 | 614 | return element == null 615 | ? null 616 | : element.GetComponentInParent(elementType, includeInactive); 617 | } 618 | 619 | private static Component[] GetComponentsInChildren(Component c, Type elementType, bool includeInactive, bool excludeSelf) 620 | { 621 | if (!excludeSelf) 622 | return c.GetComponentsInChildren(elementType, includeInactive); 623 | 624 | List components = new List(); 625 | 626 | var transform = c.transform; 627 | int childCount = transform.childCount; 628 | 629 | for (int i = 0; i < childCount; ++i) 630 | { 631 | var child = transform.GetChild(i); 632 | components.AddRange(child.GetComponentsInChildren(elementType, includeInactive)); 633 | } 634 | 635 | return components.ToArray(); 636 | } 637 | 638 | private static Component GetComponentInChildren(Component c, Type elementType, bool includeInactive, 639 | bool excludeSelf) 640 | { 641 | if (!excludeSelf) 642 | return c.GetComponentInChildren(elementType, includeInactive); 643 | 644 | var transform = c.transform; 645 | int childCount = transform.childCount; 646 | 647 | for (int i = 0; i < childCount; ++i) 648 | { 649 | var child = transform.GetChild(i); 650 | var component = child.GetComponentInChildren(elementType, includeInactive); 651 | 652 | if (component != null) 653 | return component; 654 | } 655 | 656 | return null; 657 | } 658 | 659 | private static object GetComponentsInScene(Type elementType, bool includeInactive, bool isCollection, bool excludeSelf) 660 | { 661 | bool isUnityType = elementType.IsSubclassOf(typeof(Object)); 662 | 663 | #if UNITY_2021_3_18_OR_NEWER 664 | if (isUnityType) 665 | return isCollection 666 | ? (object)Object.FindObjectsByType(elementType, includeInactive ? FindObjectsInactive.Include : FindObjectsInactive.Exclude, FindObjectsSortMode.InstanceID) 667 | : (object)Object.FindFirstObjectByType(elementType, includeInactive ? FindObjectsInactive.Include : FindObjectsInactive.Exclude); 668 | 669 | var elements = Object.FindObjectsByType(includeInactive ? FindObjectsInactive.Include : FindObjectsInactive.Exclude, FindObjectsSortMode.InstanceID); 670 | #elif UNITY_2020_1_OR_NEWER 671 | if (isUnityType) 672 | return isCollection 673 | ? (object)Object.FindObjectsOfType(elementType, includeInactive) 674 | : (object)Object.FindObjectOfType(elementType, includeInactive); 675 | 676 | var elements = Object.FindObjectsOfType(includeInactive); 677 | #else 678 | if (isUnityType) 679 | return isCollection 680 | ? (object)Object.FindObjectsOfType(elementType) 681 | : (object)Object.FindObjectOfType(elementType); 682 | var elements = Object.FindObjectsOfType(); 683 | #endif 684 | elements = elements.Where(IsCorrectType).ToArray(); 685 | if (isCollection) 686 | return (object)elements; 687 | 688 | return (object)(elements.Length > 0 ? elements[0] : null); 689 | 690 | bool IsCorrectType(MonoBehaviour e) => elementType.IsInterface ? e.GetType().GetInterfaces().Contains(elementType) : e.GetType().IsSubclassOf(elementType); 691 | } 692 | } 693 | } 694 | -------------------------------------------------------------------------------- /SceneRefAttributeValidator.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: a9821c8ef2ae41699a4a8184030c6737 3 | timeCreated: 1673257812 -------------------------------------------------------------------------------- /SceneRefFilter.cs: -------------------------------------------------------------------------------- 1 | namespace KBCore.Refs 2 | { 3 | public abstract class SceneRefFilter 4 | { 5 | internal abstract bool IncludeSceneRef(object obj); 6 | } 7 | 8 | // ReSharper disable once TypeParameterCanBeVariant 9 | public abstract class SceneRefFilter : SceneRefFilter 10 | where T : class 11 | { 12 | 13 | internal override bool IncludeSceneRef(object obj) 14 | => this.IncludeSceneRef((T) obj); 15 | 16 | /// 17 | /// Returns true if the given object should be included as a reference. 18 | /// 19 | public abstract bool IncludeSceneRef(T obj); 20 | } 21 | } -------------------------------------------------------------------------------- /SceneRefFilter.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: b1220063dc7344b79c82b91fe6d994c0 3 | timeCreated: 1694233805 -------------------------------------------------------------------------------- /SceneRefValidatorOnSave.cs: -------------------------------------------------------------------------------- 1 | #if UNITY_EDITOR 2 | 3 | using UnityEditor; 4 | using UnityEditor.SceneManagement; 5 | using UnityEngine; 6 | using UnityEngine.SceneManagement; 7 | 8 | namespace KBCore.Refs 9 | { 10 | [InitializeOnLoad] 11 | public static class SceneRefValidatorOnSave 12 | { 13 | private const string PrefsKey = "KBCore/ValidateRefsOnSave"; 14 | private const string MenuItemText = "Tools/KBCore/Validate Refs on Save"; 15 | 16 | public static bool ValidateRefsOnSave 17 | { 18 | get => EditorPrefs.GetBool(PrefsKey, false); 19 | private set => EditorPrefs.SetBool(PrefsKey, value); 20 | } 21 | 22 | static SceneRefValidatorOnSave() 23 | { 24 | EditorSceneManager.sceneSaving += OnSceneSaving; 25 | } 26 | 27 | [MenuItem(MenuItemText, false, 1000)] 28 | public static void ToggleValidateRefsOnSave() 29 | { 30 | ValidateRefsOnSave = !ValidateRefsOnSave; 31 | Menu.SetChecked(MenuItemText, ValidateRefsOnSave); 32 | } 33 | 34 | [MenuItem(MenuItemText, true)] 35 | public static bool ToggleValidateRefsOnSaveValidate() 36 | { 37 | Menu.SetChecked(MenuItemText, ValidateRefsOnSave); 38 | 39 | return true; 40 | } 41 | 42 | private static void OnSceneSaving(Scene scene, string path) 43 | { 44 | if (!ValidateRefsOnSave) return; 45 | 46 | SceneRefAttributeValidator.ValidateAllRefs(); 47 | } 48 | } 49 | } 50 | 51 | #endif -------------------------------------------------------------------------------- /SceneRefValidatorOnSave.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 1570ddaa3e00006469eae94b15f8e6e6 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /ValidatedMonoBehaviour.cs: -------------------------------------------------------------------------------- 1 | using UnityEngine; 2 | 3 | namespace KBCore.Refs 4 | { 5 | public class ValidatedMonoBehaviour : MonoBehaviour 6 | { 7 | #if UNITY_EDITOR 8 | protected virtual void OnValidate() 9 | { 10 | this.ValidateRefs(); 11 | } 12 | #else 13 | protected virtual void OnValidate() { } 14 | #endif 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /ValidatedMonoBehaviour.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 49f27fd8bfcb1054d9c27d96b0198286 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "com.kylewbanks.scenerefattribute", 3 | "displayName": "Scene Reference Attribute", 4 | "description": "Unity C# attribute for serializing component and interface references within the scene or prefab during OnValidate, rather than using GetComponent* functions in Awake/Start/OnEnable at runtime.", 5 | "version": "0.13.0", 6 | "author": { 7 | "name": "Kyle Banks", 8 | "url": "https://github.com/KyleBanks/scene-ref-attribute" 9 | }, 10 | "license": "MIT", 11 | "licensesUrl": "https://github.com/KyleBanks/scene-ref-attribute/blob/main/LICENSE", 12 | "documentationUrl": "https://github.com/KyleBanks/scene-ref-attribute/blob/main/README.md" 13 | } 14 | -------------------------------------------------------------------------------- /package.json.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 1d2c1898063e42a45b5affc5b873f967 3 | TextScriptImporter: 4 | externalObjects: {} 5 | userData: 6 | assetBundleName: 7 | assetBundleVariant: --------------------------------------------------------------------------------