Search Unity

  1. Unity support for visionOS is now available. Learn more in our blog post.
    Dismiss Notice

Question OnValidate alternative to allow structural changes?

Discussion in 'Scripting' started by Thygrrr, Nov 29, 2023.

  1. Thygrrr

    Thygrrr

    Joined:
    Sep 23, 2013
    Posts:
    698
    With all these new scripting changes, what place / callback is there, if any, to make structural changes to a game object or scene at validation time?

    E.g. a component that requires N (inspector editable value) Audio sources to be present on an object, needs a way of adding or removing the appropriate number of components, and serializing their references into an array so the behaviour doesn't have to run this logic in Awake?

    OnValidate "no longer" allows structural changes such as AddComponent (perhaps it never really did, but the new import workers in 2022/2023 are what really cause some warning spam now)

    So what do we use? Make all these scripts ExecuteInEditMode and run the validation code in Update? What about prefabs not open in a stage? What about runtime code in these scripts that also needs to run in Awake/Update? Just pepper them with nested ifs?

    I've worked with OnPostprocessScene and OnPostprocessAsset but these also seem to run at a strange time and would need to perform these potentially expensive workloads for the entire scene every time.
     
    Last edited: Nov 29, 2023
  2. spiney199

    spiney199

    Joined:
    Feb 11, 2021
    Posts:
    7,358
    Depends what you mean by 'validation time'? Before a build? When saving a scene? Or just while editing stuff in the inspector?

    For the latter, that's what custom editors, editor windows, and other editor tools are for. For the former, I guess the post process callbacks, or even build pipeline hooks are for.
     
    Bunny83 and Thygrrr like this.
  3. Thygrrr

    Thygrrr

    Joined:
    Sep 23, 2013
    Posts:
    698
    All of the above. But especially during editing and it must be guaranteed by build time that it's exactly configured as desired.

    I didn't think about using the cusom inspector for it, I'll try that. (I usually need one anyway).
    Thank you ^^

    I wonder if hideflags work on Components... :eek:
     
    Last edited: Nov 29, 2023
  4. Bunny83

    Bunny83

    Joined:
    Oct 18, 2010
    Posts:
    3,854
    They do. You can hide or disable editing of components that way.
     
    SisusCo and spiney199 like this.
  5. SisusCo

    SisusCo

    Joined:
    Jan 29, 2019
    Posts:
    1,266
    This pattern works:
    Code (CSharp):
    1. #if UNITY_EDITOR
    2. void OnValidate()
    3. {
    4.     EditorApplication.delayCall += PerformValidation;
    5.  
    6.     // this gets called on the main thread
    7.     void PerformValidation()
    8.     {
    9.         // ensure object wasn't destroyed during the delay
    10.         if(this == null)
    11.         {
    12.             return;
    13.         }
    14.  
    15.         // do stuff...
    16.     }
    17. }
    18. #endif
     
    Thygrrr and DragonCoder like this.
  6. Thygrrr

    Thygrrr

    Joined:
    Sep 23, 2013
    Posts:
    698
    I'll try yours, Sisus - great idea, and feels a lot better than this.

    Code (CSharp):
    1.  
    2. using System;
    3. using System.Collections.Generic;
    4. using Tiger.Util;
    5. using UnityEngine;
    6.  
    7. namespace Tiger.Audio
    8. {
    9.     /// <summary>
    10.     /// A basic component that creates audio sources on an object and serves as a point to access them.
    11.     /// </summary>
    12.     public class AudioPool : ValidatedBehaviour
    13.     {
    14.         [SerializeField]
    15.         private int capacity = 5;
    16.        
    17.         public List<AudioSource> sources => sourceComponents;
    18.        
    19.         [SerializeField]
    20.         private List<AudioSource> sourceComponents = new();
    21.        
    22.         private void OnValidate()
    23.         {
    24.             if (capacity != sources.Count) Debug.LogError("AudioPool: Capacity and source count do not match, open the inspector to fix.", this);
    25.         }
    26.  
    27.         /// <summary>
    28.         /// Provides an AudioSource from the pool for purposes of holding it for a longer time for later use.
    29.         /// Example use case: playing a sound on a loop, but then stopping it later.
    30.         /// </summary>
    31.         /// <remarks>
    32.         /// Only allowed at Runtime.
    33.         /// </remarks>
    34.         /// <returns>The source</returns>
    35.         public AudioSource Take()
    36.         {
    37.             if (!Application.isPlaying) throw new InvalidOperationException("AudioPool: Cannot take sources at edit time.");
    38.             return sources.Take();  
    39.         }
    40.  
    41.         public override void Validate(out bool dirty)
    42.         {
    43.             dirty = false;
    44.  
    45.             sources.Clear();
    46.             sources.AddRange(GetComponents<AudioSource>());
    47.            
    48.             for (var i = sources.Count; i < capacity; i++)
    49.             {
    50.                 dirty = true;
    51.                 sources.Add(gameObject.AddComponent<AudioSource>());
    52.             }
    53.  
    54.             for (var i = capacity; i < sources.Count; i++)
    55.             {
    56.                 dirty = true;
    57.                 DestroyImmediate(sources[i]);
    58.             }
    59.            
    60.             sources.RemoveRange(capacity, sources.Count - capacity);
    61.         }
    62.     }
    63. }
    ...
    with this:
    Code (CSharp):
    1. using Tiger.Util;
    2. using UnityEditor;
    3. using UnityEngine;
    4.  
    5. namespace Editor
    6. {
    7.     [CustomEditor(typeof(ValidatedBehaviour), true)]
    8.     public class ValidatedBehaviourEditor : UnityEditor.Editor
    9.     {
    10.         public override void OnInspectorGUI()
    11.         {
    12.             DrawDefaultInspector();
    13.             if (target is not ValidatedBehaviour validated) return;
    14.             validated.Validate(out var dirty);
    15.             if (dirty) EditorUtility.SetDirty(validated);
    16.         }
    17.     }
    18. }
     
    Last edited: Nov 29, 2023
  7. Thygrrr

    Thygrrr

    Joined:
    Sep 23, 2013
    Posts:
    698
    Yep, that works for me. This component is kind of special but it's really useful here.

    Code (CSharp):
    1. using System;
    2. using System.Collections.Generic;
    3. using System.Linq;
    4. using Tiger.Util;
    5. using UnityEngine;
    6.  
    7. namespace Tiger.Audio
    8. {
    9.     /// <summary>
    10.     /// A basic component that creates audio sources on an object and serves as a point to access them.
    11.     /// </summary>
    12.     public class AudioPool : MonoBehaviour
    13.     {
    14.         [SerializeField]
    15.         private int capacity = 5;
    16.      
    17.         public List<AudioSource> sources => sourceComponents;
    18.      
    19.         [SerializeField]
    20.         private List<AudioSource> sourceComponents = new();
    21.  
    22.  
    23. #if UNITY_EDITOR
    24.         private void OnValidate()
    25.         {
    26.             if (capacity == sources.Count && sources.All(s => s)) return;
    27.             Debug.LogWarning("AudioPool: Capacity and source count do not match, trying to fix.", this);
    28.             UnityEditor.EditorApplication.delayCall += EnsureComponents;
    29.         }
    30.  
    31.         private void EnsureComponents()
    32.         {
    33.             if (!this) return;
    34.          
    35.             sources.Clear();
    36.             var components = GetComponents<AudioSource>();
    37.  
    38.             for (var i = 0; i < capacity && i < components.Length; i++)
    39.             {
    40.                 sources.Add(components[i]);
    41.             }
    42.          
    43.             for (var i = sources.Count; i < capacity; i++)
    44.             {
    45.                 var source = gameObject.AddComponent<AudioSource>();
    46.                 sources.Add(source);
    47.             }
    48.  
    49.             for (var i = capacity; i < components.Length; i++)
    50.             {
    51.                 DestroyImmediate(components[i], true);
    52.             }
    53.          
    54.             sources.RemoveRange(capacity, sources.Count - capacity);
    55.          
    56.             UnityEditor.EditorUtility.SetDirty(this);
    57.         }
    58. #endif
    59.     }
    60. }
    61.  
     
    Last edited: Nov 29, 2023
    SisusCo likes this.