// ───────────────────────────────────────────────────────────────────────────── // WLProjectileStart.cs — 투사체 시작 위치 null 타깃 가드 (Q7 D-35 원인 지점) // // PD 지시 #813 · 발주서 WL-813x2 §1-2 (2026-09-09) // // ■ 실측 원인 (메인 에디터 Editor.log · Q7 R1 15:08 구간 · R1_13/R1_14 캡처 사이) // NullReferenceException: Object reference not set to an instance of an object // at ProjectileBase.Set (ProjectileData _data) [0x0026d] in Assets/Script/Character/Projectile/ProjectileBase.cs:233 // at Select_BodyonFire.Set (ProjectileData _data) in .../SelectSkill/Select_BodyonFire.cs:12 // at InGameInfo+<>c__DisplayClass31_0.b__0 (AsyncOperationHandle`1) in Assets/Script/Info/InGameInfo.cs:339 // at DelegateList`1[T].Invoke (Addressables · DelayedActionManager.LateUpdate) // 2회째(15:23 R2b 구간)는 같은 줄에서 `SelectFairyBase.Set` 경유. // // ■ 왜 null 인가 // `InGameInfo.Load_Skill`(:333-340) 은 선택스킬 프리팹이 풀에 없을 때만 도는 **최초 1회 비동기 로드**다. // Addressables 콜백이 다음 LateUpdate 로 밀리는 사이에 `_ptd.target`(=발사 시점의 몹)이 사라지면 // 콜백이 부르는 `ProjectileBase.Set` 안에서 target 이 null 이 된다. // `Set` 은 :184 에서 `isNullTarget` 을 이미 계산해 :213 · :238 은 가드하지만, // :229-236 의 시작 위치 switch 는 **`default:`(=None/Target)와 `TargetCenter` 를 가드하지 않는다**. // → 존 클리어 카드(813o)·레벨업으로 런 도중 선택스킬이 새로 붙는 WL 런에서 특히 잘 걸린다. // // ■ 가드 // target 이 (Unity 오버로드 ==) null 이면 shooter 위치로 대체한다. shooter 마저 null 이면 현재 위치 유지. // target 이 살아 있으면 **원본과 완전히 동일한 식**이다. // SO `projectileNullTargetGuard = 0` 이면 원본 동작 그대로(=역참조)로 되돌린다(C8). // // 🔴 `??` · `?.` 를 쓰지 않는다 — UnityEngine.Object 의 가짜 null 을 못 거른다(813y 교훈). // 🔴 Debug.LogError 금지(813z) — 진단 로그는 Debug.Log 1줄뿐이고 기본 off. // ───────────────────────────────────────────────────────────────────────────── using UnityEngine; namespace WL.Combat.Diagnostics { /// ProjectileBase.Set 의 target 기준 시작 위치 계산(원본 2줄 훅이 부른다). public static class WLProjectileStart { /// 가드가 null target 을 잡은 횟수(프로브·QA 가 읽는다). public static int NullTargetCount; /// 그중 shooter 로도 대체하지 못해 현재 위치를 유지한 횟수. public static int NoShooterCount; /// 마지막으로 잡은 스킬(진단용). public static string LastSkill = ""; /// 카운터 초기화(프로브 전용). public static void ResetCounters() { NullTargetCount = NoShooterCount = 0; LastSkill = ""; } /// /// target 기준 시작 위치. target 이 null 이면 shooter → current 순으로 대체한다. /// /// 발사 데이터(원본 `_data` 그대로). /// 대체 실패 시 유지할 현재 위치. public static Vector3 TargetPos(ProjectileData data, Vector3 current) { if (data == null) return current; var cfg = WLErrorGuardSettings.Instance; bool guard = cfg == null || (cfg.enabled && !WLErrorGuardSettings.RuntimeDisabled && cfg.projectileNullTargetGuard); // C8: 가드 off 면 원본 그대로 역참조한다(= 원본 동작 100%). if (!guard) return data.target.Get_CenterPositionFoward(); if (!(data.target == null)) return data.target.Get_CenterPositionFoward(); // Unity 오버로드 == (가짜 null 포함) NullTargetCount++; LastSkill = data.m_Skill.ToString(); if (!(data.shooter == null)) { if (cfg != null && cfg.projectileGuardLog) Debug.Log("[WL813x2] 투사체 타깃 소실 — shooter 위치로 대체 · skill=" + LastSkill + " · 누적 " + NullTargetCount); return data.shooter.Get_CenterPositionFoward(); } NoShooterCount++; if (cfg != null && cfg.projectileGuardLog) Debug.Log("[WL813x2] 투사체 타깃·발사자 모두 소실 — 현재 위치 유지 · skill=" + LastSkill); return current; } } }