547 lines
40 KiB
C#
547 lines
40 KiB
C#
|
|
// WL811i_Probe.cs — #811i 스킬 스펙터클 검증 프로브 (읽기 전용 · 에셋 무수정 · 가짜 이벤트 발행)
|
|||
|
|
//
|
|||
|
|
// unity command run_script --file AgentScripts/WL811i_Probe.cs --entry WL811i_Probe.Dump // 설정 · 16행 매핑 · 프리팹 실존(AssetDatabase) · 파티클/렌더러 예산 표 (Edit/Play)
|
|||
|
|
// unity command run_script --file AgentScripts/WL811i_Probe.cs --entry WL811i_Probe.EditProbe // Edit 모드: 로드 → 가짜 SkillCast/SkillFired/HitboxHit 16행 → 3단 스폰 · 절두체 밖 스킵 · Heavy 2 · Medium · C8 · GC 100
|
|||
|
|
// unity command run_script --file AgentScripts/WL811i_Probe.cs --entry WL811i_Probe.PlayProbe // Title Play: 위 + 수명 실측(차지 1 s · 본체 자동 · Heavy 반납 시각) · 착탄 발동당 상한 · GC 100 · 폴백 착탄
|
|||
|
|
// unity command run_script --file AgentScripts/WL811i_Probe.cs --entry WL811i_Probe.Status // 카운터
|
|||
|
|
// 결과: Screenshots_WL/WL811i/<이름>_<HHmmss>.txt + Console
|
|||
|
|
|
|||
|
|
using System;
|
|||
|
|
using System.Collections;
|
|||
|
|
using System.Collections.Generic;
|
|||
|
|
using System.Reflection;
|
|||
|
|
using System.Text;
|
|||
|
|
using UnityEngine;
|
|||
|
|
using UnityEngine.AI;
|
|||
|
|
using WL.Combat.Core;
|
|||
|
|
using WL.Combat.Reaction;
|
|||
|
|
|
|||
|
|
public static class WL811i_Probe
|
|||
|
|
{
|
|||
|
|
const string OutDir = "Screenshots_WL/WL811i";
|
|||
|
|
const BindingFlags F = BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public;
|
|||
|
|
|
|||
|
|
// ───────────────────────────────────────────── Dump (Edit/Play)
|
|||
|
|
public static object Dump()
|
|||
|
|
{
|
|||
|
|
var sb = new StringBuilder();
|
|||
|
|
var st = WLSkillSpectacleSettings.Instance;
|
|||
|
|
sb.AppendLine("# WL811i Dump " + DateTime.Now.ToString("HH:mm:ss") + " playing=" + Application.isPlaying + " core=" + WLCombatCoreSettings.Enabled + " spectacle=" + WLSkillSpectacleSettings.Enabled + " subscribed=" + SkillSpectacle.Subscribed);
|
|||
|
|
if (st == null) { sb.AppendLine("asset NULL"); Save("dump", sb); return sb.ToString(); }
|
|||
|
|
var core = WLCombatCoreSettings.Instance;
|
|||
|
|
sb.AppendLine("core heavyEffectMax=" + (core != null ? core.heavyEffectMax.ToString() : "?") + " viewMargin=" + (core != null ? core.viewMargin.ToString() : "?"));
|
|||
|
|
sb.AppendLine("subs: AttackStarted=" + CombatEvents.AttackStarted.Count + " SkillCast=" + CombatEvents.SkillCast.Count + " SkillFired=" + CombatEvents.SkillFired.Count + " HitboxHit=" + CombatEvents.HitboxHit.Count + " HitConfirmed=" + CombatEvents.HitConfirmed.Count);
|
|||
|
|
sb.AppendLine("## 값");
|
|||
|
|
sb.AppendLine("enabled=" + st.enabled + " requireMainPC=" + st.requireMainPC + " requireInView=" + st.requireInView + " preloadOnFirstAttack=" + st.preloadOnFirstAttack + " warm=" + st.warmInstanceOnLoad
|
|||
|
|
+ " chargeFlash=" + st.chargeFlashSeconds + " chargeMax=" + st.chargeMaxLifetime + " heavyUsesBudget=" + st.heavyUsesEffectBudget + " mediumMax=" + st.mediumMax
|
|||
|
|
+ " impactMaxPerFire=" + st.impactMaxPerFire + " impactMinInterval=" + st.impactMinIntervalSeconds + " autoLifetimeMax=" + st.autoLifetimeMax + " loopingLifetime=" + st.loopingLifetime
|
|||
|
|
+ " poolMaxPerPrefab=" + st.poolMaxPerPrefab + " trackCapacity=" + st.trackCapacity + " tint=" + st.tintEnabled + "×" + st.tintStrength);
|
|||
|
|
sb.AppendLine("## 클래스 원소");
|
|||
|
|
for (int i = 0; i < st.classElements.Length; i++)
|
|||
|
|
{
|
|||
|
|
var c = st.classElements[i];
|
|||
|
|
sb.AppendLine(c.classId + " " + c.label + " " + c.element + " tint=" + c.tint.ToString("F2"));
|
|||
|
|
}
|
|||
|
|
sb.AppendLine("## 매핑 16행 (실존 = AssetDatabase.LoadAssetAtPath)");
|
|||
|
|
sb.AppendLine("| 클래스 | 슬롯 | SkillID | 이름 | charge | body | impact | budget | 실존 |");
|
|||
|
|
int missing = 0, total = 0;
|
|||
|
|
var uniq = new List<string>();
|
|||
|
|
for (int i = 0; i < st.rows.Length; i++)
|
|||
|
|
{
|
|||
|
|
var r = st.rows[i];
|
|||
|
|
string ex = "";
|
|||
|
|
foreach (var v in new[] { r.charge, r.body, r.impact })
|
|||
|
|
{
|
|||
|
|
if (string.IsNullOrEmpty(v.prefab)) { ex += "-"; continue; }
|
|||
|
|
total++;
|
|||
|
|
bool ok = Exists(v.prefab);
|
|||
|
|
if (!ok) missing++;
|
|||
|
|
ex += ok ? "Y" : "N";
|
|||
|
|
if (!uniq.Contains(v.prefab)) uniq.Add(v.prefab);
|
|||
|
|
}
|
|||
|
|
sb.AppendLine("| " + r.classId + " | " + r.slot + " | " + r.skillId + " | " + r.label + " | " + V(r.charge) + " | " + V(r.body) + " | " + V(r.impact) + " | " + r.budgetClass + " | " + ex + " |");
|
|||
|
|
}
|
|||
|
|
sb.AppendLine("실존: 사용 " + total + " 건 · 고유 " + uniq.Count + " · 누락 " + missing + " → " + (missing == 0 ? "100 %" : "FAIL"));
|
|||
|
|
|
|||
|
|
sb.AppendLine("## 프리팹 예산 표 (프리팹 에셋 계층 실측 · 파티클 수 = maxParticles 합 · 예상 = Σ min(max, rate×life+burst) 「추정」 · SetPass ≈ 고유 머티리얼 수 「추정」)");
|
|||
|
|
sb.AppendLine(string.Format("{0,-36} {1,3} {2,5} {3,7} {4,5} {5,4} {6,4} {7,5} {8,5} {9}", "prefab", "ps", "maxP", "estP", "dur", "loop", "rend", "mats", "tint", "shaders / TurnOff"));
|
|||
|
|
var totalEst = 0f; var totalMax = 0;
|
|||
|
|
foreach (var name in uniq)
|
|||
|
|
{
|
|||
|
|
var go = Load(name);
|
|||
|
|
if (go == null) { sb.AppendLine(string.Format("{0,-36} MISSING", name)); continue; }
|
|||
|
|
var pss = go.GetComponentsInChildren<ParticleSystem>(true);
|
|||
|
|
int maxP = 0, loops = 0; float dur = 0f, est = 0f;
|
|||
|
|
foreach (var ps in pss)
|
|||
|
|
{
|
|||
|
|
var m = ps.main; maxP += m.maxParticles;
|
|||
|
|
if (m.loop) loops++;
|
|||
|
|
float life = m.startLifetime.constantMax;
|
|||
|
|
float d = m.duration + life + m.startDelay.constantMax;
|
|||
|
|
if (d > dur) dur = d;
|
|||
|
|
var em = ps.emission;
|
|||
|
|
float burst = 0f;
|
|||
|
|
for (int b = 0; b < em.burstCount; b++) burst += em.GetBurst(b).maxCount;
|
|||
|
|
float rate = em.rateOverTimeMultiplier * (m.loop ? life : Mathf.Min(m.duration, life) + 0f);
|
|||
|
|
est += Mathf.Min(m.maxParticles, rate + burst);
|
|||
|
|
}
|
|||
|
|
var rends = go.GetComponentsInChildren<Renderer>(true);
|
|||
|
|
var mats = new List<Material>(); var shaders = new List<string>(); int tintable = 0;
|
|||
|
|
foreach (var r in rends)
|
|||
|
|
{
|
|||
|
|
var mat = r.sharedMaterial;
|
|||
|
|
if (mat == null) continue;
|
|||
|
|
if (!mats.Contains(mat)) mats.Add(mat);
|
|||
|
|
if (mat.shader != null)
|
|||
|
|
{
|
|||
|
|
string sn = mat.shader.name;
|
|||
|
|
bool has = false;
|
|||
|
|
foreach (var p in st.tintProperties) if (mat.shader.FindPropertyIndex(p) >= 0) { has = true; break; }
|
|||
|
|
if (has) tintable++;
|
|||
|
|
if (!shaders.Contains(sn + (has ? "" : "(no-tint)"))) shaders.Add(sn + (has ? "" : "(no-tint)"));
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
var off = go.GetComponent<TurnOff_GO>();
|
|||
|
|
string offs = off != null ? ("TurnOff OffTime=" + off.OffTime + (off.NoUseParticleTime ? " noPS" : "")) : "no-TurnOff";
|
|||
|
|
totalEst += est; totalMax += maxP;
|
|||
|
|
sb.AppendLine(string.Format("{0,-36} {1,3} {2,5} {3,7:F0} {4,5:F2} {5,4} {6,4} {7,5} {8,5} {9} · {10}", name, pss.Length, maxP, est, dur, loops, rends.Length, mats.Count, tintable, string.Join(",", shaders.ToArray()), offs));
|
|||
|
|
}
|
|||
|
|
sb.AppendLine("합계(33종 1회씩) maxParticles=" + totalMax + " 예상 파티클=" + totalEst.ToString("F0") + " 「추정」");
|
|||
|
|
Save("dump", sb); return sb.ToString();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
static string V(SpectacleVfx v)
|
|||
|
|
{
|
|||
|
|
if (string.IsNullOrEmpty(v.prefab)) return "—";
|
|||
|
|
return v.prefab + "(" + (v.scale > 0f ? v.scale : 1f) + "×" + v.anchor + (v.forwardOffset != 0f ? " +" + v.forwardOffset + "m" : "") + (v.useCasterRotation ? " rot" : "") + (v.tint ? " tint" : "") + (v.lifetime > 0f ? " " + v.lifetime + "s" : "") + ")";
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
static string PathOf(string name) { return WLSkillSpectacleSettings.EffectPathPrefix + name + WLSkillSpectacleSettings.EffectPathSuffix; }
|
|||
|
|
static bool Exists(string name) { return Load(name) != null; }
|
|||
|
|
static GameObject Load(string name) { return UnityEditor.AssetDatabase.LoadAssetAtPath<GameObject>(PathOf(name)); } // 에디터 전용 프로브
|
|||
|
|
|
|||
|
|
// ───────────────────────────────────────────── Status
|
|||
|
|
public static object Status()
|
|||
|
|
{
|
|||
|
|
var sb = new StringBuilder();
|
|||
|
|
sb.AppendLine("# WL811i Status " + DateTime.Now.ToString("HH:mm:ss") + " " + Counters());
|
|||
|
|
Save("status", sb); return sb.ToString();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
static string Counters()
|
|||
|
|
{
|
|||
|
|
return "cast=" + SkillSpectacle.CastSeen + " fired=" + SkillSpectacle.FiredSeen + " hit=" + SkillSpectacle.HitSeen
|
|||
|
|
+ " | spawn charge/body/impact=" + SkillSpectacle.ChargeSpawned + "/" + SkillSpectacle.BodySpawned + "/" + SkillSpectacle.ImpactSpawned + " fallback=" + SkillSpectacle.ImpactFallback
|
|||
|
|
+ " | skip disabled/noRow/notMain/outOfView/heavy/medium/impactCap/interval/noDmg/notLoaded/noPrefab/poolFull/trackFull="
|
|||
|
|
+ SkillSpectacle.SkippedDisabled + "/" + SkillSpectacle.SkippedNoRow + "/" + SkillSpectacle.SkippedNotMainPC + "/" + SkillSpectacle.SkippedOutOfView + "/" + SkillSpectacle.SkippedHeavyBudget + "/" + SkillSpectacle.SkippedMediumBudget + "/"
|
|||
|
|
+ SkillSpectacle.SkippedImpactCap + "/" + SkillSpectacle.SkippedImpactInterval + "/" + SkillSpectacle.SkippedNoDamage + "/" + SkillSpectacle.SkippedNotLoaded + "/" + SkillSpectacle.SkippedNoPrefab + "/" + SkillSpectacle.SkippedPoolFull + "/" + SkillSpectacle.SkippedTrackFull
|
|||
|
|
+ " | load req/done/fail=" + SkillSpectacle.LoadRequested + "/" + SkillSpectacle.LoadCompleted + "/" + SkillSpectacle.LoadFailed + " inst=" + SkillSpectacle.Instantiated + " expired=" + SkillSpectacle.Expired
|
|||
|
|
+ " heavy acq/rel/held=" + SkillSpectacle.HeavyAcquired + "/" + SkillSpectacle.HeavyReleased + "/" + SkillSpectacle.HeavyHeldRows + " budgetHeavy=" + EffectBudget.Count(EffectBudgetKind.Heavy) + "/" + EffectBudget.Limit(EffectBudgetKind.Heavy)
|
|||
|
|
+ " active=" + SkillSpectacle.ActiveCount + " medium=" + SkillSpectacle.MediumActive + " tint=" + SkillSpectacle.TintApplied + " preload=" + SkillSpectacle.PreloadCalls + " loading=" + SkillSpectacle.LoadingCount + " entries=" + SkillSpectacle.EntryCount + " runner=" + SkillSpectacleRunner.Exists + " last=" + SkillSpectacle.LastInfo;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ───────────────────────────────────────────── 공용
|
|||
|
|
static void Save(string name, StringBuilder sb)
|
|||
|
|
{
|
|||
|
|
try
|
|||
|
|
{
|
|||
|
|
System.IO.Directory.CreateDirectory(OutDir);
|
|||
|
|
string path = System.IO.Path.Combine(OutDir, name + "_" + DateTime.Now.ToString("HHmmss") + ".txt");
|
|||
|
|
System.IO.File.WriteAllText(path, sb.ToString(), new UTF8Encoding(false));
|
|||
|
|
Debug.Log("[WL811i] 저장 " + path);
|
|||
|
|
}
|
|||
|
|
catch (Exception e) { Debug.LogWarning("[WL811i] 저장 실패 " + e.Message); }
|
|||
|
|
Debug.Log(sb.ToString());
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/// <summary>가짜 PC: m_Role=PC(public) · m_ID(protected → 리플렉션) · Play 면 Awake 가 돌고(arr_Renderer·tf_Top) Edit 면 돌지 않는다(앵커 Bottom/Mid 만 씀).</summary>
|
|||
|
|
static PCActor MakePC(string name, Vector3 pos, int classId, Quaternion rot)
|
|||
|
|
{
|
|||
|
|
var go = new GameObject(name);
|
|||
|
|
go.SetActive(false);
|
|||
|
|
go.transform.position = pos; go.transform.rotation = rot;
|
|||
|
|
go.AddComponent<Rigidbody>().isKinematic = true;
|
|||
|
|
go.AddComponent<CapsuleCollider>();
|
|||
|
|
go.AddComponent<Animator>();
|
|||
|
|
var agent = go.AddComponent<NavMeshAgent>(); agent.enabled = false;
|
|||
|
|
var pc = go.AddComponent<PCActor>(); pc.enabled = false;
|
|||
|
|
pc.m_Role = eRole.PC;
|
|||
|
|
var f = typeof(Actor).GetField("m_ID", F); if (f != null) f.SetValue(pc, classId);
|
|||
|
|
go.SetActive(true);
|
|||
|
|
if (Application.isPlaying) InitCC(pc);
|
|||
|
|
return pc;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
static MobActor MakeMob(string name, Vector3 pos)
|
|||
|
|
{
|
|||
|
|
var go = new GameObject(name);
|
|||
|
|
go.SetActive(false);
|
|||
|
|
go.transform.position = pos;
|
|||
|
|
go.AddComponent<Rigidbody>().isKinematic = true;
|
|||
|
|
go.AddComponent<BoxCollider>();
|
|||
|
|
go.AddComponent<Animator>();
|
|||
|
|
var agent = go.AddComponent<NavMeshAgent>(); agent.enabled = false;
|
|||
|
|
var cube = GameObject.CreatePrimitive(PrimitiveType.Cube);
|
|||
|
|
cube.name = "Body"; cube.transform.SetParent(go.transform, false);
|
|||
|
|
if (Application.isPlaying) UnityEngine.Object.Destroy(cube.GetComponent<Collider>()); else UnityEngine.Object.DestroyImmediate(cube.GetComponent<Collider>());
|
|||
|
|
var mob = go.AddComponent<MobActor>(); mob.enabled = false;
|
|||
|
|
mob.m_Role = eRole.Mob;
|
|||
|
|
go.SetActive(true);
|
|||
|
|
if (Application.isPlaying) InitCC(mob);
|
|||
|
|
return mob;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
static void InitCC(Actor a)
|
|||
|
|
{
|
|||
|
|
var m = typeof(Actor).GetMethod("Init_CC", F);
|
|||
|
|
if (m != null) { try { m.Invoke(a, null); } catch (Exception e) { Debug.LogWarning("[WL811i] Init_CC " + e.Message); } }
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
static SkillListTableData Skill(int id, int classId, float castTime, eSkillTarget target)
|
|||
|
|
{
|
|||
|
|
return new SkillListTableData { n_SkillID = id, n_SkillClassID = classId, f_CastingTime = castTime, e_SkillTarget = target, e_SkillType = eSkillType.Active };
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
static UseMagicInfo Magic(SkillListTableData d, int slot) { return new UseMagicInfo { m_SkillTableData = d, m_uiIndex = slot - 1 }; }
|
|||
|
|
|
|||
|
|
static ProjectileData Pd(Actor shooter, SkillListTableData d) { return new ProjectileData { shooter = shooter, m_SkillListTableData = d }; }
|
|||
|
|
|
|||
|
|
static Camera InstallCamera(Vector3 pos, Vector3 lookAt)
|
|||
|
|
{
|
|||
|
|
var go = new GameObject("__wl811i_cam");
|
|||
|
|
go.tag = "MainCamera";
|
|||
|
|
var cam = go.AddComponent<Camera>();
|
|||
|
|
go.transform.position = pos; go.transform.LookAt(lookAt);
|
|||
|
|
return cam;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
static void Kill(GameObject go) { if (go == null) return; if (Application.isPlaying) UnityEngine.Object.Destroy(go); else UnityEngine.Object.DestroyImmediate(go); }
|
|||
|
|
|
|||
|
|
struct RowDef { public int id, cls, slot; public float cast; public eSkillTarget target; public SpectacleBudgetClass budget; }
|
|||
|
|
static RowDef[] Rows(WLSkillSpectacleSettings st)
|
|||
|
|
{
|
|||
|
|
var list = new RowDef[st.rows.Length];
|
|||
|
|
for (int i = 0; i < st.rows.Length; i++)
|
|||
|
|
{
|
|||
|
|
var r = st.rows[i];
|
|||
|
|
list[i] = new RowDef { id = r.skillId, cls = r.classId, slot = r.slot, cast = r.slot == 4 ? 1f : 0f, target = eSkillTarget.Enemy, budget = r.budgetClass };
|
|||
|
|
}
|
|||
|
|
return list;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/// <summary>로드 완료까지 폴링(Edit 모드는 Tick 을 직접 · Addressables AssetDatabase 모드는 보통 즉시 완료).</summary>
|
|||
|
|
static int PumpLoads(int maxIter)
|
|||
|
|
{
|
|||
|
|
int it = 0;
|
|||
|
|
while (SkillSpectacle.LoadingCount > 0 && it < maxIter) { SkillSpectacle.Tick(Time.time); it++; System.Threading.Thread.Sleep(5); }
|
|||
|
|
if (SkillSpectacle.LoadingCount > 0 && !Application.isPlaying) it += 1000 * SkillSpectacle.CompleteLoadsNow(); // Edit 모드: 프레임 펌프가 없어 동기 완료
|
|||
|
|
return it;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
static string EntryRow(string name)
|
|||
|
|
{
|
|||
|
|
bool loaded, failed, looping; int inst, ps, maxP, rend, tintable; float life;
|
|||
|
|
if (!SkillSpectacle.TryGetEntryInfo(name, out loaded, out failed, out inst, out ps, out maxP, out life, out looping, out rend, out tintable)) return name + ": (no entry)";
|
|||
|
|
return name + ": loaded=" + loaded + " failed=" + failed + " inst=" + inst + " ps=" + ps + " maxP=" + maxP + " life=" + life.ToString("F2") + " loop=" + looping + " rend=" + rend + " tintable=" + tintable;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ───────────────────────────────────────────── Edit 모드 프로브
|
|||
|
|
public static object EditProbe()
|
|||
|
|
{
|
|||
|
|
var sb = new StringBuilder();
|
|||
|
|
var st = WLSkillSpectacleSettings.Instance;
|
|||
|
|
sb.AppendLine("# WL811i EditProbe " + DateTime.Now.ToString("HH:mm:ss") + " playing=" + Application.isPlaying + " core=" + WLCombatCoreSettings.Enabled + " spectacle=" + WLSkillSpectacleSettings.Enabled);
|
|||
|
|
if (st == null || !WLCombatCoreSettings.Enabled) { sb.AppendLine("asset/core 없음 → 중단"); Save("edit", sb); return sb.ToString(); }
|
|||
|
|
SkillSpectacle.EnsureSubscribed();
|
|||
|
|
SkillSpectacle.ResetDiagnostics();
|
|||
|
|
var trash = new List<GameObject>();
|
|||
|
|
try
|
|||
|
|
{
|
|||
|
|
// ① 로드: 16행 전부
|
|||
|
|
SkillSpectacle.PreloadAll();
|
|||
|
|
int it = PumpLoads(20);
|
|||
|
|
sb.AppendLine("① PreloadAll → req=" + SkillSpectacle.LoadRequested + " done=" + SkillSpectacle.LoadCompleted + " fail=" + SkillSpectacle.LoadFailed + " loading=" + SkillSpectacle.LoadingCount + " iter=" + it + " inst(warm)=" + SkillSpectacle.Instantiated + " entries=" + SkillSpectacle.EntryCount);
|
|||
|
|
var names = new List<string>();
|
|||
|
|
foreach (var r in st.rows) foreach (var v in new[] { r.charge, r.body, r.impact }) if (!string.IsNullOrEmpty(v.prefab) && !names.Contains(v.prefab)) names.Add(v.prefab);
|
|||
|
|
foreach (var n in names) sb.AppendLine(" " + EntryRow(n));
|
|||
|
|
if (SkillSpectacle.LoadingCount > 0) sb.AppendLine(" ⚠ 로드 미완료 — Edit 모드 Addressables 비동기 → Play 프로브에서 재확인");
|
|||
|
|
|
|||
|
|
// ② 카메라(절두체) + 가짜 PC 4클래스 + 몹
|
|||
|
|
var cam = InstallCamera(new Vector3(0f, 6f, -6f), Vector3.zero); trash.Add(cam.gameObject);
|
|||
|
|
var pcs = new Dictionary<int, PCActor>();
|
|||
|
|
foreach (var cid in new[] { 10101, 10102, 10501, 10502 }) { var pc = MakePC("__wl811i_pc" + cid, Vector3.zero, cid, Quaternion.identity); pcs[cid] = pc; trash.Add(pc.gameObject); }
|
|||
|
|
var mobIn = MakeMob("__wl811i_mobIn", new Vector3(1.5f, 0f, 0f)); trash.Add(mobIn.gameObject);
|
|||
|
|
var mobOut = MakeMob("__wl811i_mobOut", new Vector3(0f, 0f, -40f)); trash.Add(mobOut.gameObject); // 카메라 뒤 = 절두체 밖
|
|||
|
|
sb.AppendLine("② cam main=" + (Camera.main != null ? Camera.main.name : "NULL") + " inView(0,0,0)=" + EffectBudget.IsInView(Vector3.zero) + " inView(mobOut)=" + EffectBudget.IsInView(mobOut.Get_position()) + " pc10101.IsMainPC=" + pcs[10101].IsMainPC() + " id=" + pcs[10101].Get_ID());
|
|||
|
|
|
|||
|
|
// ③ 16행 × (SkillCast → SkillFired → HitboxHit×2) · 매 행 뒤 만료
|
|||
|
|
sb.AppendLine("③ 16행 가짜 발행 (row: charge/body/impact 스폰 여부 · 스킵 사유)");
|
|||
|
|
var rows = Rows(st);
|
|||
|
|
int c0, b0, i0;
|
|||
|
|
for (int i = 0; i < rows.Length; i++)
|
|||
|
|
{
|
|||
|
|
var d = Skill(rows[i].id, rows[i].cls == 0 ? 0 : rows[i].cls, rows[i].cast, rows[i].target);
|
|||
|
|
var pc = pcs[rows[i].cls];
|
|||
|
|
c0 = SkillSpectacle.ChargeSpawned; b0 = SkillSpectacle.BodySpawned; i0 = SkillSpectacle.ImpactSpawned;
|
|||
|
|
CombatEvents.RaiseSkillCast(pc, Magic(d, rows[i].slot));
|
|||
|
|
string sc = SkillSpectacle.LastInfo;
|
|||
|
|
CombatEvents.RaiseSkillFired(pc, 0, Magic(d, rows[i].slot));
|
|||
|
|
string sf = SkillSpectacle.LastInfo;
|
|||
|
|
CombatEvents.RaiseHitboxHit(null, Pd(pc, d), mobIn, false);
|
|||
|
|
string si = SkillSpectacle.LastInfo;
|
|||
|
|
CombatEvents.RaiseHitboxHit(null, Pd(pc, d), mobIn, false); // 같은 시각 → 간격 스킵
|
|||
|
|
int heavyNow = EffectBudget.Count(EffectBudgetKind.Heavy);
|
|||
|
|
sb.AppendLine(" " + st.rows[i].classId + "/" + st.rows[i].slot + " " + st.rows[i].skillId + " " + st.rows[i].label + ": charge=" + (SkillSpectacle.ChargeSpawned - c0) + "(" + sc + ") body=" + (SkillSpectacle.BodySpawned - b0) + "(" + sf + ") impact=" + (SkillSpectacle.ImpactSpawned - i0) + "(" + si + ") active=" + SkillSpectacle.ActiveCount + " heavyHeld=" + SkillSpectacle.RowHeavyHeld(i) + " budgetHeavy=" + heavyNow + " medium=" + SkillSpectacle.MediumActive + " interval-skip=" + SkillSpectacle.SkippedImpactInterval);
|
|||
|
|
SkillSpectacle.Tick(Time.time + 100f); // 만료
|
|||
|
|
}
|
|||
|
|
sb.AppendLine(" 만료 후 active=" + SkillSpectacle.ActiveCount + " budgetHeavy=" + EffectBudget.Count(EffectBudgetKind.Heavy) + " heavy acq/rel=" + SkillSpectacle.HeavyAcquired + "/" + SkillSpectacle.HeavyReleased + " medium=" + SkillSpectacle.MediumActive);
|
|||
|
|
|
|||
|
|
// ④ 절두체 밖: 카메라 뒤 몹에 착탄 → 스킵
|
|||
|
|
{
|
|||
|
|
var d = Skill(703101, 10101, 0f, eSkillTarget.Enemy); var pc = pcs[10101];
|
|||
|
|
CombatEvents.RaiseSkillFired(pc, 0, Magic(d, 1));
|
|||
|
|
int v0 = SkillSpectacle.SkippedOutOfView, i1 = SkillSpectacle.ImpactSpawned;
|
|||
|
|
CombatEvents.RaiseHitboxHit(null, Pd(pc, d), mobOut, false);
|
|||
|
|
sb.AppendLine("④ 절두체 밖 착탄 → outOfView +" + (SkillSpectacle.SkippedOutOfView - v0) + " impact +" + (SkillSpectacle.ImpactSpawned - i1) + " (기대 1 / 0)");
|
|||
|
|
pc.transform.position = new Vector3(0f, 0f, -40f);
|
|||
|
|
int v1 = SkillSpectacle.SkippedOutOfView, c1 = SkillSpectacle.ChargeSpawned;
|
|||
|
|
CombatEvents.RaiseSkillCast(pc, Magic(Skill(703102, 10101, 0f, eSkillTarget.Enemy), 2));
|
|||
|
|
sb.AppendLine(" 절두체 밖 시전(차지) → outOfView +" + (SkillSpectacle.SkippedOutOfView - v1) + " charge +" + (SkillSpectacle.ChargeSpawned - c1) + " (기대 1 / 0)");
|
|||
|
|
pc.transform.position = Vector3.zero;
|
|||
|
|
SkillSpectacle.Tick(Time.time + 100f);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ⑤ Heavy 2: 슬롯 4 3개 동시 → 2 스폰 + 1 heavy 스킵 · 만료 후 반납
|
|||
|
|
{
|
|||
|
|
int h0 = SkillSpectacle.SkippedHeavyBudget, c1 = SkillSpectacle.ChargeSpawned;
|
|||
|
|
CombatEvents.RaiseSkillCast(pcs[10101], Magic(Skill(702006, 0, 1f, eSkillTarget.Enemy), 4));
|
|||
|
|
CombatEvents.RaiseSkillCast(pcs[10102], Magic(Skill(702009, 0, 1f, eSkillTarget.Self), 4));
|
|||
|
|
CombatEvents.RaiseSkillCast(pcs[10501], Magic(Skill(702005, 0, 1f, eSkillTarget.Enemy), 4));
|
|||
|
|
sb.AppendLine("⑤ Heavy 3 동시 시전 → charge +" + (SkillSpectacle.ChargeSpawned - c1) + " heavy-skip +" + (SkillSpectacle.SkippedHeavyBudget - h0) + " budgetHeavy=" + EffectBudget.Count(EffectBudgetKind.Heavy) + "/" + EffectBudget.Limit(EffectBudgetKind.Heavy) + " heldRows=" + SkillSpectacle.HeavyHeldRows + " (기대 2 / 1 / 2)");
|
|||
|
|
// 같은 행의 본체는 이미 잡은 슬롯을 재사용(추가 획득 0)
|
|||
|
|
int a0 = SkillSpectacle.HeavyAcquired, b1 = SkillSpectacle.BodySpawned;
|
|||
|
|
CombatEvents.RaiseSkillFired(pcs[10101], 0, Magic(Skill(702006, 0, 1f, eSkillTarget.Enemy), 4));
|
|||
|
|
sb.AppendLine(" 같은 행 본체 → body +" + (SkillSpectacle.BodySpawned - b1) + " heavy 추가 획득 +" + (SkillSpectacle.HeavyAcquired - a0) + " budgetHeavy=" + EffectBudget.Count(EffectBudgetKind.Heavy) + " (기대 1 / 0 / 2)");
|
|||
|
|
SkillSpectacle.Tick(Time.time + 100f);
|
|||
|
|
sb.AppendLine(" 만료 → budgetHeavy=" + EffectBudget.Count(EffectBudgetKind.Heavy) + " heldRows=" + SkillSpectacle.HeavyHeldRows + " active=" + SkillSpectacle.ActiveCount + " (기대 0 / 0 / 0)");
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ⑥ Medium 상한: Medium 본체 5개 → mediumMax 까지만
|
|||
|
|
{
|
|||
|
|
int m0 = SkillSpectacle.SkippedMediumBudget, b1 = SkillSpectacle.BodySpawned;
|
|||
|
|
var meds = new[] { new[] { 703102, 10101 }, new[] { 703103, 10101 }, new[] { 703105, 10102 }, new[] { 703503, 10501 }, new[] { 703506, 10502 } };
|
|||
|
|
foreach (var m in meds) CombatEvents.RaiseSkillFired(pcs[m[1]], 0, Magic(Skill(m[0], m[1], 0f, eSkillTarget.Enemy), 2));
|
|||
|
|
sb.AppendLine("⑥ Medium 본체 5 → body +" + (SkillSpectacle.BodySpawned - b1) + " medium-skip +" + (SkillSpectacle.SkippedMediumBudget - m0) + " mediumActive=" + SkillSpectacle.MediumActive + " (기대 " + st.mediumMax + " / " + (5 - st.mediumMax) + " / " + st.mediumMax + ")");
|
|||
|
|
SkillSpectacle.Tick(Time.time + 100f);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ⑦ C8: RuntimeDisabled → 전부 0
|
|||
|
|
{
|
|||
|
|
WLSkillSpectacleSettings.RuntimeDisabled = true;
|
|||
|
|
int c1 = SkillSpectacle.ChargeSpawned, b1 = SkillSpectacle.BodySpawned, i1 = SkillSpectacle.ImpactSpawned;
|
|||
|
|
var d = Skill(703101, 10101, 0f, eSkillTarget.Enemy); var pc = pcs[10101];
|
|||
|
|
CombatEvents.RaiseSkillCast(pc, Magic(d, 1)); CombatEvents.RaiseSkillFired(pc, 0, Magic(d, 1)); CombatEvents.RaiseHitboxHit(null, Pd(pc, d), mobIn, false);
|
|||
|
|
sb.AppendLine("⑦ RuntimeDisabled → spawn +" + (SkillSpectacle.ChargeSpawned - c1 + SkillSpectacle.BodySpawned - b1 + SkillSpectacle.ImpactSpawned - i1) + " (기대 0) active=" + SkillSpectacle.ActiveCount);
|
|||
|
|
WLSkillSpectacleSettings.RuntimeDisabled = false;
|
|||
|
|
CombatEvents.RaiseSkillFired(pc, 0, Magic(d, 1));
|
|||
|
|
sb.AppendLine(" 재활성 → body +" + (SkillSpectacle.BodySpawned - b1) + " (기대 1)");
|
|||
|
|
SkillSpectacle.Tick(Time.time + 100f);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ⑧ GC 100회 (풀 워밍 후 · SkillCast+SkillFired+HitboxHit+Tick)
|
|||
|
|
{
|
|||
|
|
var d = Skill(703102, 10101, 0f, eSkillTarget.Enemy); var pc = pcs[10101];
|
|||
|
|
for (int w = 0; w < 5; w++) { CombatEvents.RaiseSkillCast(pc, Magic(d, 2)); CombatEvents.RaiseSkillFired(pc, 0, Magic(d, 2)); CombatEvents.RaiseHitboxHit(null, Pd(pc, d), mobIn, false); SkillSpectacle.Tick(Time.time + 100f); }
|
|||
|
|
var magic = Magic(d, 2); var pd = Pd(pc, d);
|
|||
|
|
GC.Collect(); GC.WaitForPendingFinalizers(); GC.Collect();
|
|||
|
|
long m0 = GC.GetTotalMemory(false); long mono0 = UnityEngine.Profiling.Profiler.GetMonoUsedSizeLong();
|
|||
|
|
for (int n = 0; n < 100; n++) { CombatEvents.RaiseSkillCast(pc, magic); CombatEvents.RaiseSkillFired(pc, 0, magic); CombatEvents.RaiseHitboxHit(null, pd, mobIn, false); SkillSpectacle.Tick(Time.time + 100f); }
|
|||
|
|
long m1 = GC.GetTotalMemory(false); long mono1 = UnityEngine.Profiling.Profiler.GetMonoUsedSizeLong();
|
|||
|
|
sb.AppendLine("⑧ GC 100회(cast+fire+hit+tick · 페이로드 재사용) → GetTotalMemory Δ=" + (m1 - m0) + " B · MonoUsed Δ=" + (mono1 - mono0) + " B (기대 0)");
|
|||
|
|
// 이벤트 없이 Tick 만 100회(기준선)
|
|||
|
|
m0 = GC.GetTotalMemory(false);
|
|||
|
|
for (int n = 0; n < 100; n++) SkillSpectacle.Tick(Time.time + 100f);
|
|||
|
|
sb.AppendLine(" 기준선 Tick 100회 Δ=" + (GC.GetTotalMemory(false) - m0) + " B");
|
|||
|
|
}
|
|||
|
|
sb.AppendLine("# 종료 " + Counters());
|
|||
|
|
}
|
|||
|
|
catch (Exception e) { sb.AppendLine("EXCEPTION " + e); }
|
|||
|
|
finally
|
|||
|
|
{
|
|||
|
|
SkillSpectacle.ClearAll();
|
|||
|
|
foreach (var g in trash) Kill(g);
|
|||
|
|
if (!Application.isPlaying) SkillSpectacle.DestroyPools();
|
|||
|
|
SkillSpectacle.ResetDiagnostics();
|
|||
|
|
if (!Application.isPlaying) { var r = GameObject.Find("__WLSkillSpectacle"); if (r != null) UnityEngine.Object.DestroyImmediate(r); }
|
|||
|
|
}
|
|||
|
|
Save("edit", sb); return sb.ToString();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ───────────────────────────────────────────── Title Play 프로브
|
|||
|
|
public static void PlayProbe()
|
|||
|
|
{
|
|||
|
|
if (!Application.isPlaying) { Debug.LogWarning("[WL811i] Play 모드에서만(Title Play). Edit 모드는 Dump/EditProbe."); return; }
|
|||
|
|
var old = GameObject.Find("__wl811i_probe");
|
|||
|
|
if (old != null) UnityEngine.Object.Destroy(old);
|
|||
|
|
var go = new GameObject("__wl811i_probe");
|
|||
|
|
var h = go.AddComponent<Host>();
|
|||
|
|
h.StartCoroutine(h.Run());
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
public class Host : MonoBehaviour
|
|||
|
|
{
|
|||
|
|
StringBuilder _sb; float _t0;
|
|||
|
|
readonly List<GameObject> _trash = new List<GameObject>();
|
|||
|
|
string Pre() { return string.Format("{0,7:F0} ms | f{1,-5} | ", (Time.unscaledTime - _t0) * 1000f, Time.frameCount); }
|
|||
|
|
IEnumerator Wait(float s) { float t = Time.unscaledTime; while (Time.unscaledTime - t < s) yield return null; }
|
|||
|
|
|
|||
|
|
public IEnumerator Run()
|
|||
|
|
{
|
|||
|
|
_sb = new StringBuilder(); _t0 = Time.unscaledTime;
|
|||
|
|
var st = WLSkillSpectacleSettings.Instance;
|
|||
|
|
_sb.AppendLine("# WL811i PlayProbe " + DateTime.Now.ToString("HH:mm:ss") + " scene=" + UnityEngine.SceneManagement.SceneManager.GetActiveScene().name + " core=" + WLCombatCoreSettings.Enabled + " spectacle=" + WLSkillSpectacleSettings.Enabled + " subscribed=" + SkillSpectacle.Subscribed + " ts=" + Time.timeScale);
|
|||
|
|
if (st == null || !WLCombatCoreSettings.Enabled) { _sb.AppendLine("asset/core 없음 → 중단"); Save("play", _sb); Destroy(gameObject); yield break; }
|
|||
|
|
SkillSpectacle.ResetDiagnostics();
|
|||
|
|
var cam = InstallCamera(new Vector3(0f, 6f, -6f), Vector3.zero); _trash.Add(cam.gameObject);
|
|||
|
|
yield return null;
|
|||
|
|
|
|||
|
|
// ① 로드(런타임 Addressables) — 프레임 폴링
|
|||
|
|
SkillSpectacle.PreloadAll();
|
|||
|
|
float tl = Time.unscaledTime;
|
|||
|
|
while (SkillSpectacle.LoadingCount > 0 && Time.unscaledTime - tl < 20f) yield return null;
|
|||
|
|
_sb.AppendLine(Pre() + "① PreloadAll → req=" + SkillSpectacle.LoadRequested + " done=" + SkillSpectacle.LoadCompleted + " fail=" + SkillSpectacle.LoadFailed + " loading=" + SkillSpectacle.LoadingCount + " " + ((Time.unscaledTime - tl) * 1000f).ToString("F0") + " ms · warm inst=" + SkillSpectacle.Instantiated + " runner=" + SkillSpectacleRunner.Exists);
|
|||
|
|
var names = new List<string>();
|
|||
|
|
foreach (var r in st.rows) foreach (var v in new[] { r.charge, r.body, r.impact }) if (!string.IsNullOrEmpty(v.prefab) && !names.Contains(v.prefab)) names.Add(v.prefab);
|
|||
|
|
foreach (var n in names) _sb.AppendLine(" " + EntryRow(n));
|
|||
|
|
|
|||
|
|
var pcs = new Dictionary<int, PCActor>();
|
|||
|
|
foreach (var cid in new[] { 10101, 10102, 10501, 10502 }) { var pc = MakePC("__wl811i_pc" + cid, Vector3.zero, cid, Quaternion.identity); pcs[cid] = pc; _trash.Add(pc.gameObject); }
|
|||
|
|
var mobIn = MakeMob("__wl811i_mobIn", new Vector3(1.5f, 0f, 0f)); _trash.Add(mobIn.gameObject);
|
|||
|
|
var mobOut = MakeMob("__wl811i_mobOut", new Vector3(0f, 0f, -40f)); _trash.Add(mobOut.gameObject);
|
|||
|
|
yield return null;
|
|||
|
|
_sb.AppendLine(Pre() + "② cam main=" + (Camera.main != null ? Camera.main.name : "NULL") + " inView(0)=" + EffectBudget.IsInView(Vector3.zero) + " inView(out)=" + EffectBudget.IsInView(mobOut.Get_position()) + " pc.IsMainPC=" + pcs[10101].IsMainPC() + " id=" + pcs[10101].Get_ID());
|
|||
|
|
|
|||
|
|
// ③ 16행 실스폰 + 수명 실측(활성 수가 0 이 되는 시각)
|
|||
|
|
_sb.AppendLine(Pre() + "③ 16행 (charge/body/impact 스폰 · 활성 0 도달 ms · Heavy 반납)");
|
|||
|
|
var rows = Rows(st);
|
|||
|
|
for (int i = 0; i < rows.Length; i++)
|
|||
|
|
{
|
|||
|
|
var d = Skill(rows[i].id, rows[i].cls, rows[i].cast, rows[i].target);
|
|||
|
|
var pc = pcs[rows[i].cls];
|
|||
|
|
int c0 = SkillSpectacle.ChargeSpawned, b0 = SkillSpectacle.BodySpawned, i0 = SkillSpectacle.ImpactSpawned;
|
|||
|
|
float t = Time.unscaledTime;
|
|||
|
|
CombatEvents.RaiseSkillCast(pc, Magic(d, rows[i].slot)); string sc = SkillSpectacle.LastInfo;
|
|||
|
|
yield return null;
|
|||
|
|
CombatEvents.RaiseSkillFired(pc, 0, Magic(d, rows[i].slot)); string sf = SkillSpectacle.LastInfo;
|
|||
|
|
yield return null;
|
|||
|
|
CombatEvents.RaiseHitboxHit(null, Pd(pc, d), mobIn, false); string si = SkillSpectacle.LastInfo;
|
|||
|
|
int peak = SkillSpectacle.ActiveCount;
|
|||
|
|
float tz = -1f; float tEnd = t + 6f;
|
|||
|
|
while (Time.unscaledTime < tEnd) { yield return null; if (SkillSpectacle.ActiveCount > peak) peak = SkillSpectacle.ActiveCount; if (SkillSpectacle.ActiveCount == 0) { tz = Time.unscaledTime - t; break; } }
|
|||
|
|
_sb.AppendLine(Pre() + " " + st.rows[i].classId + "/" + st.rows[i].slot + " " + st.rows[i].skillId + " " + st.rows[i].label + ": charge=" + (SkillSpectacle.ChargeSpawned - c0) + "(" + sc + ") body=" + (SkillSpectacle.BodySpawned - b0) + "(" + sf + ") impact=" + (SkillSpectacle.ImpactSpawned - i0) + "(" + si + ") peakActive=" + peak + " zeroAt=" + (tz >= 0 ? (tz * 1000f).ToString("F0") + " ms" : "미도달(6 s)") + " budgetHeavy=" + EffectBudget.Count(EffectBudgetKind.Heavy) + " heldRows=" + SkillSpectacle.HeavyHeldRows);
|
|||
|
|
if (tz < 0f) SkillSpectacle.ClearAll();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ④ 착탄 발동당 상한: 0.05 s 간격 6타 → impactMaxPerFire 까지
|
|||
|
|
{
|
|||
|
|
var d = Skill(703101, 10101, 0f, eSkillTarget.Enemy); var pc = pcs[10101];
|
|||
|
|
CombatEvents.RaiseSkillFired(pc, 0, Magic(d, 1));
|
|||
|
|
int i0 = SkillSpectacle.ImpactSpawned, cap0 = SkillSpectacle.SkippedImpactCap;
|
|||
|
|
for (int k = 0; k < 6; k++) { CombatEvents.RaiseHitboxHit(null, Pd(pc, d), mobIn, false); yield return Wait(0.06f); }
|
|||
|
|
_sb.AppendLine(Pre() + "④ 착탄 6타(0.06 s 간격) → impact +" + (SkillSpectacle.ImpactSpawned - i0) + " cap-skip +" + (SkillSpectacle.SkippedImpactCap - cap0) + " (기대 " + st.impactMaxPerFire + " / " + (6 - st.impactMaxPerFire) + ") thisFire=" + SkillSpectacle.RowImpactsThisFire(st.FindRow(703101, 10101)));
|
|||
|
|
yield return Wait(3.2f);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ⑤ 절두체 밖
|
|||
|
|
{
|
|||
|
|
var d = Skill(703101, 10101, 0f, eSkillTarget.Enemy); var pc = pcs[10101];
|
|||
|
|
CombatEvents.RaiseSkillFired(pc, 0, Magic(d, 1));
|
|||
|
|
int v0 = SkillSpectacle.SkippedOutOfView, i1 = SkillSpectacle.ImpactSpawned;
|
|||
|
|
CombatEvents.RaiseHitboxHit(null, Pd(pc, d), mobOut, false);
|
|||
|
|
_sb.AppendLine(Pre() + "⑤ 절두체 밖 착탄 → outOfView +" + (SkillSpectacle.SkippedOutOfView - v0) + " impact +" + (SkillSpectacle.ImpactSpawned - i1) + " (기대 1 / 0)");
|
|||
|
|
yield return Wait(3.2f);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ⑥ Heavy 2 동시 + 차지 1 s 수명 + 반납 시각
|
|||
|
|
{
|
|||
|
|
int h0 = SkillSpectacle.SkippedHeavyBudget, c1 = SkillSpectacle.ChargeSpawned;
|
|||
|
|
float t = Time.unscaledTime;
|
|||
|
|
CombatEvents.RaiseSkillCast(pcs[10101], Magic(Skill(702006, 0, 1f, eSkillTarget.Enemy), 4));
|
|||
|
|
CombatEvents.RaiseSkillCast(pcs[10502], Magic(Skill(702012, 0, 1f, eSkillTarget.Enemy), 4));
|
|||
|
|
CombatEvents.RaiseSkillCast(pcs[10501], Magic(Skill(702005, 0, 1f, eSkillTarget.Enemy), 4));
|
|||
|
|
_sb.AppendLine(Pre() + "⑥ Heavy 3 동시 시전 → charge +" + (SkillSpectacle.ChargeSpawned - c1) + " heavy-skip +" + (SkillSpectacle.SkippedHeavyBudget - h0) + " budgetHeavy=" + EffectBudget.Count(EffectBudgetKind.Heavy) + "/" + EffectBudget.Limit(EffectBudgetKind.Heavy) + " (기대 2 / 1 / 2)");
|
|||
|
|
float tRel = -1f;
|
|||
|
|
while (Time.unscaledTime - t < 3f) { yield return null; if (EffectBudget.Count(EffectBudgetKind.Heavy) == 0) { tRel = Time.unscaledTime - t; break; } }
|
|||
|
|
_sb.AppendLine(Pre() + " 차지 만료 → Heavy 0 도달 " + (tRel >= 0 ? (tRel * 1000f).ToString("F0") + " ms (기대 ≈1000 = f_CastingTime)" : "미도달") + " active=" + SkillSpectacle.ActiveCount + " rel=" + SkillSpectacle.HeavyReleased);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ⑦ 폴백 착탄(HitConfirmed · Beater_pd = 스킬 · HitboxHit 없이)
|
|||
|
|
{
|
|||
|
|
var d = Skill(703505, 10502, 0f, eSkillTarget.Enemy); var pc = pcs[10502];
|
|||
|
|
CombatEvents.RaiseSkillFired(pc, 0, Magic(d, 1));
|
|||
|
|
int fb0 = SkillSpectacle.ImpactFallback, i1 = SkillSpectacle.ImpactSpawned;
|
|||
|
|
var pd = Pd(pc, d);
|
|||
|
|
var dinfo = new DamageInfo { Beater = pc, Beater_pd = pd, Critical = eStat.None }; dinfo.Damage = 10; dinfo.BaseDmg = 10;
|
|||
|
|
yield return null;
|
|||
|
|
CombatEvents.RaiseHitConfirmed(mobIn, dinfo);
|
|||
|
|
_sb.AppendLine(Pre() + "⑦ HitConfirmed 폴백 → fallback +" + (SkillSpectacle.ImpactFallback - fb0) + " impact +" + (SkillSpectacle.ImpactSpawned - i1) + " (기대 1 / 1)");
|
|||
|
|
// HitboxHit 직후 같은 프레임 HitConfirmed → 중복 0
|
|||
|
|
yield return Wait(0.1f);
|
|||
|
|
fb0 = SkillSpectacle.ImpactFallback; i1 = SkillSpectacle.ImpactSpawned;
|
|||
|
|
CombatEvents.RaiseHitboxHit(null, pd, mobIn, false);
|
|||
|
|
CombatEvents.RaiseHitConfirmed(mobIn, dinfo);
|
|||
|
|
_sb.AppendLine(Pre() + " HitboxHit+HitConfirmed 같은 프레임 → fallback +" + (SkillSpectacle.ImpactFallback - fb0) + " impact +" + (SkillSpectacle.ImpactSpawned - i1) + " (기대 0 / 1)");
|
|||
|
|
yield return Wait(3.2f);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ⑧ C8
|
|||
|
|
{
|
|||
|
|
WLSkillSpectacleSettings.RuntimeDisabled = true;
|
|||
|
|
int c1 = SkillSpectacle.ChargeSpawned, b1 = SkillSpectacle.BodySpawned, i1 = SkillSpectacle.ImpactSpawned;
|
|||
|
|
var d = Skill(703101, 10101, 0f, eSkillTarget.Enemy); var pc = pcs[10101];
|
|||
|
|
CombatEvents.RaiseSkillCast(pc, Magic(d, 1)); CombatEvents.RaiseSkillFired(pc, 0, Magic(d, 1)); CombatEvents.RaiseHitboxHit(null, Pd(pc, d), mobIn, false);
|
|||
|
|
yield return null;
|
|||
|
|
_sb.AppendLine(Pre() + "⑧ RuntimeDisabled → spawn +" + (SkillSpectacle.ChargeSpawned - c1 + SkillSpectacle.BodySpawned - b1 + SkillSpectacle.ImpactSpawned - i1) + " (기대 0) active=" + SkillSpectacle.ActiveCount);
|
|||
|
|
WLSkillSpectacleSettings.RuntimeDisabled = false;
|
|||
|
|
CombatEvents.RaiseSkillFired(pc, 0, Magic(d, 1));
|
|||
|
|
_sb.AppendLine(Pre() + " 재활성 → body +" + (SkillSpectacle.BodySpawned - b1) + " (기대 1)");
|
|||
|
|
yield return Wait(3.2f);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ⑨ GC 100회 (프레임마다 cast+fire+hit · 만료는 러너 Update · 페이로드 재사용)
|
|||
|
|
{
|
|||
|
|
var d = Skill(703102, 10101, 0f, eSkillTarget.Enemy); var pc = pcs[10101];
|
|||
|
|
var magic = Magic(d, 2); var pd = Pd(pc, d);
|
|||
|
|
for (int w = 0; w < 5; w++) { CombatEvents.RaiseSkillCast(pc, magic); CombatEvents.RaiseSkillFired(pc, 0, magic); CombatEvents.RaiseHitboxHit(null, pd, mobIn, false); yield return null; }
|
|||
|
|
yield return Wait(3.2f);
|
|||
|
|
GC.Collect(); GC.WaitForPendingFinalizers(); GC.Collect();
|
|||
|
|
yield return null;
|
|||
|
|
long m0 = GC.GetTotalMemory(false); long mono0 = UnityEngine.Profiling.Profiler.GetMonoUsedSizeLong();
|
|||
|
|
int sk0 = SkillSpectacle.SkippedPoolFull + SkillSpectacle.SkippedMediumBudget;
|
|||
|
|
for (int n = 0; n < 100; n++) { CombatEvents.RaiseSkillCast(pc, magic); CombatEvents.RaiseSkillFired(pc, 0, magic); CombatEvents.RaiseHitboxHit(null, pd, mobIn, false); }
|
|||
|
|
long m1 = GC.GetTotalMemory(false); long mono1 = UnityEngine.Profiling.Profiler.GetMonoUsedSizeLong();
|
|||
|
|
_sb.AppendLine(Pre() + "⑨ GC 100회(같은 프레임 · cast+fire+hit) → GetTotalMemory Δ=" + (m1 - m0) + " B · MonoUsed Δ=" + (mono1 - mono0) + " B (기대 0) · 풀/상한 스킵 +" + (SkillSpectacle.SkippedPoolFull + SkillSpectacle.SkippedMediumBudget - sk0));
|
|||
|
|
// 프레임 분산 100회(만료가 끼어드는 경로)
|
|||
|
|
yield return Wait(3.2f);
|
|||
|
|
GC.Collect(); yield return null;
|
|||
|
|
m0 = GC.GetTotalMemory(false);
|
|||
|
|
for (int n = 0; n < 100; n++) { CombatEvents.RaiseSkillCast(pc, magic); CombatEvents.RaiseSkillFired(pc, 0, magic); CombatEvents.RaiseHitboxHit(null, pd, mobIn, false); yield return null; }
|
|||
|
|
m1 = GC.GetTotalMemory(false);
|
|||
|
|
_sb.AppendLine(Pre() + " 프레임 분산 100회 → Δ=" + (m1 - m0) + " B (러너 Update 포함 · 다른 시스템 잡음 가능)");
|
|||
|
|
}
|
|||
|
|
_sb.AppendLine(Pre() + "# 종료 " + Counters());
|
|||
|
|
Save("play", _sb);
|
|||
|
|
Cleanup();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
void Cleanup()
|
|||
|
|
{
|
|||
|
|
SkillSpectacle.ClearAll();
|
|||
|
|
foreach (var g in _trash) if (g != null) Destroy(g);
|
|||
|
|
_trash.Clear();
|
|||
|
|
SkillSpectacle.ResetDiagnostics();
|
|||
|
|
Destroy(gameObject);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|