// MIT License - Copyright (c) 2025 wallstop // Full license text: https://github.com/wallstop/unity-helpers/blob/main/LICENSE namespace WallstopStudios.UnityHelpers.Utils { using System; using System.Collections.Generic; using Core.Extension; using Core.Helper; using UnityEngine; using UnityEngine.Serialization; /// /// Selects the MonoBehaviour lifecycle events that should trigger prefab instantiation. /// [Flags] public enum ChildSpawnMethod { /// /// No child creation will occur. Mainly retained for serialization compatibility. /// [Obsolete] None = 0, /// /// Spawn children during . /// Awake = 1 << 0, /// /// Spawn children when the component is enabled. /// OnEnabled = 1 << 1, /// /// Spawn children during . /// Start = 1 << 2, } /// /// Instantiates a curated list of prefabs as children of the current GameObject while ensuring /// duplicates across scenes are avoided and optional DontDestroyOnLoad behaviour is applied. /// [DisallowMultipleComponent] public sealed class ChildSpawner : MonoBehaviour { private static readonly HashSet SpawnedPrefabs = new(); [FormerlySerializedAs("dontDestroyOnLoad")] [SerializeField] internal bool _dontDestroyOnLoad = true; [SerializeField] internal ChildSpawnMethod _spawnMethod = ChildSpawnMethod.Start; /// /// Prefabs that are spawned in all environments where the component executes. /// [SerializeField] internal GameObject[] _prefabs = Array.Empty(); /// /// Prefabs spawned when running inside the Unity editor only. /// [SerializeField] internal GameObject[] _editorOnlyPrefabs = Array.Empty(); /// /// Prefabs spawned when running in the editor or a development build. /// [SerializeField] internal GameObject[] _developmentOnlyPrefabs = Array.Empty(); private readonly HashSet _spawnedPrefabs = new(); [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.BeforeSceneLoad)] private static void ClearSpawnedPrefabs() { SpawnedPrefabs.Clear(); } private void Awake() { if (_spawnMethod.HasFlagNoAlloc(ChildSpawnMethod.Awake)) { Spawn(); } } private void OnEnable() { if (_spawnMethod.HasFlagNoAlloc(ChildSpawnMethod.OnEnabled)) { Spawn(); } } private void Start() { if (_spawnMethod.HasFlagNoAlloc(ChildSpawnMethod.Start)) { Spawn(); } } /// /// Checks all prefab arrays for duplicates and logs an error if any are found. /// Uses pooled collections to avoid allocations. Null prefabs are skipped. /// private void CheckForDuplicatePrefabs() { GameObject[] prefabs = _prefabs ?? Array.Empty(); GameObject[] editorOnlyPrefabs = _editorOnlyPrefabs ?? Array.Empty(); GameObject[] developmentOnlyPrefabs = _developmentOnlyPrefabs ?? Array.Empty(); int totalCount = prefabs.Length + editorOnlyPrefabs.Length + developmentOnlyPrefabs.Length; if (totalCount == 0) { return; } using PooledResource> seenLease = Buffers.HashSet.Get( out HashSet seen ); using PooledResource> duplicatesLease = Buffers.HashSet.Get(out HashSet duplicates); for (int i = 0; i < prefabs.Length; i++) { GameObject prefab = prefabs[i]; if (prefab == null) { continue; } if (!seen.Add(prefab)) { duplicates.Add(prefab); } } for (int i = 0; i < editorOnlyPrefabs.Length; i++) { GameObject prefab = editorOnlyPrefabs[i]; if (prefab == null) { continue; } if (!seen.Add(prefab)) { duplicates.Add(prefab); } } for (int i = 0; i < developmentOnlyPrefabs.Length; i++) { GameObject prefab = developmentOnlyPrefabs[i]; if (prefab == null) { continue; } if (!seen.Add(prefab)) { duplicates.Add(prefab); } } if (duplicates.Count == 0) { return; } using PooledResource> namesLease = Buffers.GetList( duplicates.Count, out List duplicateNames ); foreach (GameObject prefab in duplicates) { duplicateNames.Add(prefab.name); } this.LogError($"Duplicate child prefab detected: {string.Join(",", duplicateNames)}"); } /// /// Performs the spawning process for all configured prefab collections, applying naming /// suffixes and duplicate checks for each group. /// private void Spawn() { TrySetDontDestroyOnLoad(); CheckForDuplicatePrefabs(); int count = 0; foreach (GameObject prefab in _prefabs) { GameObject child = Spawn(prefab); if (child != null) { child.name = $"{child.name} ({count++:00})"; } } foreach (GameObject prefab in _prefabs) { if (prefab != null) { _spawnedPrefabs.Add(prefab); } } #if UNITY_EDITOR if (Application.isEditor) { foreach (GameObject prefab in _editorOnlyPrefabs) { GameObject child = Spawn(prefab); if (child != null) { child.name = $"{child.name} (EDITOR-ONLY {count++:00})"; } } foreach (GameObject prefab in _editorOnlyPrefabs) { if (prefab != null) { _spawnedPrefabs.Add(prefab); } } } #endif if (Application.isEditor || Debug.isDebugBuild) { foreach (GameObject prefab in _developmentOnlyPrefabs) { GameObject child = Spawn(prefab); if (child != null) { child.name = $"{child.name} (DEVELOPMENT-ONLY {count++:00})"; } } foreach (GameObject prefab in _developmentOnlyPrefabs) { if (prefab != null) { _spawnedPrefabs.Add(prefab); } } } } /// /// Removes Unity's default "(Clone)" suffix from instantiated prefab names. /// /// The instantiated child whose name should be cleaned. private static void CleanName(GameObject child) { child.name = child.name.Replace("(Clone)", string.Empty); } /// /// Instantiates as a child of this component if it has not been /// spawned previously, guarding against duplicate DontDestroyOnLoad instances. /// /// Prefab to spawn. /// The instantiated child instance, or null if the spawn is skipped. private GameObject Spawn(GameObject prefab) { if (prefab == null) { this.LogError($"Unexpectedly null prefab - cannot spawn."); return null; } if (_spawnedPrefabs.Contains(prefab)) { return null; } if (SpawnedPrefabs.Contains(prefab)) { return null; } GameObject child = Instantiate(prefab, transform); CleanName(child); if ( child.IsDontDestroyOnLoad() || gameObject.IsDontDestroyOnLoad() || prefab.IsDontDestroyOnLoad() ) { SpawnedPrefabs.Add(prefab); } return child; } /// /// Applies when configured to /// keep the spawner alive between scene loads. /// private void TrySetDontDestroyOnLoad() { if (_dontDestroyOnLoad && Application.isPlaying && !gameObject.IsDontDestroyOnLoad()) { DontDestroyOnLoad(gameObject); } } } }