├── 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 | [](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 | 
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