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()
{