diff --git a/Assets/Scripts/Skills/Effectors/Projectile.cs b/Assets/Scripts/Skills/Effectors/Projectile.cs index b78f5e2..6157682 100644 --- a/Assets/Scripts/Skills/Effectors/Projectile.cs +++ b/Assets/Scripts/Skills/Effectors/Projectile.cs @@ -36,6 +36,27 @@ namespace EerieVillage.Skills.Effectors protected float[] _baseAlphas; const float FADE_START_RATIO = 0.85f; + // PD 지시 2026-05-14 — Awake 시점 _data == null 잔존 GameObject 자기 destroy. + // 게임 재실행·Scene reload 시 이전 Play 잔존 Projectile (Initialize 미호출 상태) 자가 정리. + protected virtual void Awake() + { + if (_data == null && _runtime == null) + { + // Initialize 호출 전 (= 잔존) → 자기 destroy. 정상 spawn 은 Awake 직후 Initialize 호출이라 _data set 정합. + // Awake 직후 Initialize 가 별도 frame 에 호출되는 케이스 대비 1 frame 유예 + StartCoroutine(_DestroyIfStale()); + } + } + + System.Collections.IEnumerator _DestroyIfStale() + { + yield return null; // 1 frame 유예 (정상 Initialize 시점 보장) + if (_data == null && _runtime == null) + { + Destroy(gameObject); + } + } + /// /// ProjectileSpawner.Trigger 에서 Instantiate 직후 호출. /// diff --git a/Assets/Scripts/Skills/Runtime/PlayerSkillInventory.cs b/Assets/Scripts/Skills/Runtime/PlayerSkillInventory.cs index b059b93..76ad933 100644 --- a/Assets/Scripts/Skills/Runtime/PlayerSkillInventory.cs +++ b/Assets/Scripts/Skills/Runtime/PlayerSkillInventory.cs @@ -104,43 +104,58 @@ namespace EerieVillage.Skills static int DoCleanupStalePooledSpawns(bool useImmediate) { int removed = 0; - // (1) Projectile 및 파생 component GameObject — root 단위 destroy - var projs = Resources.FindObjectsOfTypeAll(); - foreach (var p in projs) + // PD 지시 2026-05-14 — Scene root 재귀 탐색 (Resources.FindObjectsOfTypeAll 영역 hideFlags hidden GO 누락 우려·정확도 ↑) + var scene = UnityEngine.SceneManagement.SceneManager.GetActiveScene(); + if (!scene.IsValid()) return 0; + var roots = scene.GetRootGameObjects(); + // 재귀 cleanup 대상 수집 (직접 destroy 시 enumeration 중 collection 변경 회피) + var targets = new System.Collections.Generic.List(); + foreach (var root in roots) { - if (p == null || p.gameObject == null) continue; - if (!p.gameObject.scene.IsValid()) continue; // prefab asset 제외 - if (useImmediate) DestroyImmediate(p.gameObject); else Destroy(p.gameObject); - removed++; + CollectStaleRecursive(root, targets); } - // (2) 박스·Range 시각화 + FX clone — name 기반 - var allGOs = Resources.FindObjectsOfTypeAll(); - foreach (var go in allGOs) + foreach (var go in targets) { if (go == null) continue; - if (!go.scene.IsValid()) continue; - bool matchName = StaleSpawnNames.Contains(go.name); - bool matchPrefix = false; - if (!matchName) - { - foreach (var pre in StaleClonePrefixes) - { - if (go.name.StartsWith(pre) && go.name.EndsWith("(Clone)")) - { - matchPrefix = true; - break; - } - } - } - if (matchName || matchPrefix) - { - if (useImmediate) DestroyImmediate(go); else Destroy(go); - removed++; - } + if (useImmediate) DestroyImmediate(go); else Destroy(go); + removed++; } return removed; } + static void CollectStaleRecursive(GameObject root, System.Collections.Generic.List targets) + { + if (root == null) return; + // Projectile 및 파생 component 부착 → 이 root 단위 destroy + if (root.GetComponent() != null) + { + targets.Add(root); + return; // 자식 재귀 불필요 (root destroy 시 자식 자동 destroy) + } + // Name 매칭 (박스·Range·FX clone) + if (IsStaleByName(root.name)) + { + targets.Add(root); + return; + } + // 자식 재귀 + foreach (Transform child in root.transform) + { + CollectStaleRecursive(child.gameObject, targets); + } + } + + static bool IsStaleByName(string name) + { + if (StaleSpawnNames.Contains(name)) return true; + if (!name.EndsWith("(Clone)")) return name.StartsWith("Projectile_"); + foreach (var pre in StaleClonePrefixes) + { + if (name.StartsWith(pre)) return true; + } + return false; + } + // PD 지시 2026-05-13 — Start 시점에 기본 습득 스킬 자동 장착 (Resources 로드 완료 후·Awake 영역 아님) void Start() {