Merge branch 'wl/gameplay/WL-815c-stage-barrier'

This commit is contained in:
깃 관리자 2026-09-11 01:05:40 +09:00
commit c93cd57544
23 changed files with 3323 additions and 0 deletions

View File

@ -0,0 +1,14 @@
WL-815c 장벽 3스타일 캡처 · 2026-09-11 00:59:27
앵커 (115.0, 0.4, -22.0) 반경 14 m · 구도 811o(h 76 · yaw 233)
PASS E0 맵 — WL_Nature
존1 지면 실측 y = 0.19 (앵커 표기 y 0.4)
PASS E1 PC — Ai01 중앙(크기 비교 기준)
PASS E barrier_ring_persp.png — MagicRing · 인스턴스 3 · 렌더러 12 · 머티리얼 1 · tri 0 · PS 12 · 링 스케일 12.27 · 원근 FOV 45 · 거리 64.9 m · 474482 B · 색 837종 · 따뜻한픽셀 3630 · drawCalls 0 · batches 0 · tri 0
PASS E barrier_ring_pixel.png — MagicRing · 인스턴스 3 · 렌더러 12 · 머티리얼 1 · tri 0 · PS 12 · 링 스케일 12.27 · 직교 size 26.1 · 270×480 → Point 확대 · 145042 B · 색 975종 · 따뜻한픽셀 3609 · drawCalls 0 · batches 0 · tri 0
PASS E barrier_props_persp.png — Props · 인스턴스 35 · 렌더러 35 · 머티리얼 2 · tri 4530 · PS 0 · 프롭 35 · 간격 2.51 m · 원근 FOV 45 · 거리 64.9 m · 425850 B · 색 795종 · 따뜻한픽셀 3647 · drawCalls 0 · batches 0 · tri 0
PASS E barrier_props_pixel.png — Props · 인스턴스 35 · 렌더러 35 · 머티리얼 2 · tri 4530 · PS 0 · 프롭 35 · 간격 2.51 m · 직교 size 26.1 · 270×480 → Point 확대 · 138475 B · 색 901종 · 따뜻한픽셀 3628 · drawCalls 0 · batches 0 · tri 0
PASS E barrier_fire_persp.png — Fire · 인스턴스 35 · 렌더러 105 · 머티리얼 3 · tri 0 · PS 105 · 불꽃 35 · 방출률 ×0.37 · 파티클 1600 · 원근 FOV 45 · 거리 64.9 m · 412816 B · 색 759종 · 따뜻한픽셀 3661 · drawCalls 0 · batches 0 · tri 0
PASS E barrier_fire_pixel.png — Fire · 인스턴스 35 · 렌더러 105 · 머티리얼 3 · tri 0 · PS 105 · 불꽃 35 · 방출률 ×0.37 · 파티클 1600 · 직교 size 26.1 · 270×480 → Point 확대 · 135914 B · 색 858종 · 따뜻한픽셀 3639 · drawCalls 0 · batches 0 · tri 0
클리어 연출 중간(t=0.5) 활성 35 · 진행도 0.50
RESULT PASS · 8 PASS · 0 FAIL

View File

@ -0,0 +1,241 @@
// ─────────────────────────────────────────────────────────────────────────────
// WL815c_Capture.cs — PD 대안 비교 캡처 6장 (에디트 모드 · 임시 씬 저장 0 · 커밋 0)
// unity command run_script --file AgentScripts/WL815c_Capture.cs --entry WL815c_Capture.Run
//
// 3스타일(마법진 링 / 프롭 링 / 불꽃 링) × 2구도
// persp : 811o 구도 각도(cameraHeight 76 · MapZoneData yaw 233 실측) · 원근 · 반경 14 m 전체가 보이게 거리만 확장
// pixel : 814c 방식 = 직교 270×480 렌더 → Point 확대 1080×1920 (도트 룩에서 뭉개지는지 본다)
// 앵커 = 기준서 §D-1 존1 (115, 0.4, 22) · 반경 14
// ─────────────────────────────────────────────────────────────────────────────
using System;
using System.Collections.Generic;
using System.IO;
using System.Text;
using UnityEditor;
using UnityEditor.SceneManagement;
using UnityEngine;
using UnityEngine.Rendering.Universal;
using UnityEngine.SceneManagement;
using WL.Combat.Stage;
public static class WL815c_Capture
{
const string kOutDir = @"E:\NerdNavis\nn_himminji\Screenshots_WL\WL815c";
const string kTxt = "AgentScripts/WL815c_CAPTURE.txt";
const string kMapPrefab = "Assets/Res_Addr/Map/WL_Nature.prefab";
const string kPcPrefab = "Assets/Res_Addr/PC/Ai01.prefab";
const int kLowW = 270, kLowH = 480, kHiW = 1080, kHiH = 1920;
const float kMapYaw = 233f; // WL_Nature MapZoneData.CamRot_Portal 실측(811o)
const float kCamH = 76f; // WLCameraFramingSettings.cameraHeight 실측(811o)
static readonly Vector3 kAnchor = new Vector3(115f, 0.4f, -22f);
const float kRadius = 14f;
static StringBuilder s_sb;
static int s_pass, s_fail;
static void L(string s) { s_sb.AppendLine(s); }
static void Chk(string n, bool ok, string d) { if (ok) s_pass++; else s_fail++; L((ok ? " PASS " : " FAIL ") + n + " — " + d); }
public static string Run()
{
s_sb = new StringBuilder(); s_pass = 0; s_fail = 0;
L("WL-815c 장벽 3스타일 캡처 · " + DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss"));
L(" 앵커 " + kAnchor.ToString("F1") + " 반경 " + kRadius + " m · 구도 811o(h " + kCamH + " · yaw " + kMapYaw + ")");
var keepSo = WLStageBarrierSettings.Instance;
try
{
Directory.CreateDirectory(kOutDir);
EditorSceneManager.NewScene(NewSceneSetup.DefaultGameObjects, NewSceneMode.Single);
foreach (var go in SceneManager.GetActiveScene().GetRootGameObjects())
if (go.GetComponent<Camera>() != null) UnityEngine.Object.DestroyImmediate(go);
var map = Inst(kMapPrefab, Vector3.zero);
Chk("E0 맵", map != null, "WL_Nature");
if (map == null) return "FAIL 맵 없음";
// 앵커 지면 실측
Vector3 center = kAnchor;
RaycastHit gh;
if (Physics.Raycast(kAnchor + Vector3.up * 100f, Vector3.down, out gh, 300f)) center.y = gh.point.y;
L(" 존1 지면 실측 y = " + center.y.ToString("F2") + " (앵커 표기 y " + kAnchor.y + ")");
// PC 를 중앙에 세워 크기 비교 기준을 만든다
var pc = Inst(kPcPrefab, center);
if (pc != null)
{
Vector3 f = new Vector3(Mathf.Sin(kMapYaw * Mathf.Deg2Rad), 0f, Mathf.Cos(kMapYaw * Mathf.Deg2Rad));
pc.transform.rotation = Quaternion.LookRotation(-f, Vector3.up);
// 🔴 Animator.Update 는 부르지 않는다 — 에디트 모드는 Awake 가 안 돌아
// Actor.Event_Idle(Actor.cs:2297)이 미할당 m_animation 을 만져 콘솔 예외가 난다(실측).
// 이 캡처에서 PC 는 크기 비교용이라 포즈는 중요하지 않다.
}
Chk("E1 PC", pc != null, "Ai01 중앙(크기 비교 기준)");
StageBarrier.HardResetForProbe();
WLStageBarrierSettings.SetInstanceForProbe(keepSo);
var styles = new StageBarrierStyle[] { StageBarrierStyle.MagicRing, StageBarrierStyle.Props, StageBarrierStyle.Fire };
var names = new string[] { "ring", "props", "fire" };
for (int i = 0; i < styles.Length; i++)
{
StageBarrierVisual.StyleOverride = styles[i];
StageBarrier.Set(center, kRadius);
StageBarrierVisual.ForcePhase(1f); // 입장 연출 끝난 상태
StageBarrierVisual.SimulateForProbe(2.0f); // 파티클을 2 s 진행(에디트 모드)
string info = Info(styles[i]);
ShotPersp(center, kOutDir + @"\barrier_" + names[i] + "_persp.png", styles[i] + " · " + info);
ShotPixel(center, kOutDir + @"\barrier_" + names[i] + "_pixel.png", styles[i] + " · " + info);
}
// 클리어 연출 확인용(파일로 남기지 않고 값만)
StageBarrier.Clear();
StageBarrierVisual.ForcePhase(0.5f);
L(" 클리어 연출 중간(t=0.5) 활성 " + StageBarrierVisual.LiveCount + " · 진행도 " + StageBarrierVisual.Phase.ToString("F2"));
}
catch (Exception ex) { s_fail++; L(" FAIL 예외 — " + ex.GetType().Name + " " + ex.Message + "\n" + ex.StackTrace); }
finally
{
try { StageBarrier.HardResetForProbe(); } catch { }
StageBarrierVisual.StyleOverride = (StageBarrierStyle)(-1);
WLStageBarrierSettings.SetInstanceForProbe(keepSo);
}
L("");
L("RESULT " + (s_fail == 0 ? "PASS" : "FAIL") + " · " + s_pass + " PASS · " + s_fail + " FAIL");
File.WriteAllText(kTxt, s_sb.ToString());
return (s_fail == 0 ? "PASS" : "FAIL") + " " + s_pass + "/" + (s_pass + s_fail);
}
static string Info(StageBarrierStyle st)
{
int rend, mats, ps; long tri;
StageBarrierVisual.MeasureCost(out rend, out mats, out tri, out ps);
string x = "인스턴스 " + StageBarrierVisual.LiveCount + " · 렌더러 " + rend + " · 머티리얼 " + mats + " · tri " + tri + " · PS " + ps;
if (st == StageBarrierStyle.MagicRing) x += " · 링 스케일 " + StageBarrierVisual.RingScale.ToString("F2");
else if (st == StageBarrierStyle.Props) x += " · 프롭 " + StageBarrierVisual.PropCount + " · 간격 " + StageBarrierVisual.PropSpacingUsed.ToString("F2") + " m";
else x += " · 불꽃 " + StageBarrierVisual.FireCount + " · 방출률 ×" + StageBarrierVisual.FireRateMul.ToString("F2") +
" · 파티클 " + StageBarrierVisual.FireParticleEstimate;
return x;
}
// ── 원근 구도(811o 각도 · 링 전체가 들어오게 거리만 확장) ───────────────
static void ShotPersp(Vector3 center, string file, string label)
{
var camGo = new GameObject("[WL815c] PerspCam");
var cam = camGo.AddComponent<Camera>();
var acd = camGo.AddComponent<UniversalAdditionalCameraData>();
acd.renderShadows = true; acd.renderPostProcessing = false; acd.antialiasing = AntialiasingMode.None;
cam.orthographic = false;
cam.fieldOfView = 45f;
cam.nearClipPlane = 0.3f;
cam.farClipPlane = 500f;
cam.clearFlags = CameraClearFlags.Skybox;
// 811o 와 같은 방향 벡터(h = cameraHeight 76 · yaw = 233)
Vector3 dir = (Quaternion.Euler(kCamH, kMapYaw, 0f) * Vector3.one).normalized;
// 반경 14 m 원이 세로 1080×1920 화면에 들어오려면 = r / tan(가로 FOV/2)
float aspect = kHiW / (float)kHiH;
float halfV = cam.fieldOfView * 0.5f * Mathf.Deg2Rad;
float halfH = Mathf.Atan(Mathf.Tan(halfV) * aspect);
float dist = (kRadius * 1.08f) / Mathf.Tan(halfH);
cam.transform.position = center + Vector3.up * 2.4f - dir * dist;
cam.transform.LookAt(center + Vector3.up * 1.0f);
Shot(cam, file, label + " · 원근 FOV 45 · 거리 " + dist.ToString("F1") + " m", false);
UnityEngine.Object.DestroyImmediate(camGo);
}
// ── 도트 룩 근사(814c 방식 · 직교 270×480 → Point 확대) ─────────────────
static void ShotPixel(Vector3 center, string file, string label)
{
var camGo = new GameObject("[WL815c] PixelCam");
var cam = camGo.AddComponent<Camera>();
var acd = camGo.AddComponent<UniversalAdditionalCameraData>();
acd.renderShadows = true; acd.renderPostProcessing = false; acd.antialiasing = AntialiasingMode.None;
Vector3 dir = (Quaternion.Euler(kCamH, kMapYaw, 0f) * Vector3.one).normalized;
cam.orthographic = true;
// 세로 1080×1920 · 가로가 좁다 → 가로에 반경이 들어가야 한다: orthoSize(세로 반) = r×1.2 / aspect
float aspect = kHiW / (float)kHiH;
cam.orthographicSize = kRadius * 1.05f / aspect;
cam.nearClipPlane = 0.1f;
cam.farClipPlane = 300f;
cam.clearFlags = CameraClearFlags.Skybox;
cam.transform.position = center + Vector3.up * 2.4f - dir * 60f;
cam.transform.LookAt(center + Vector3.up * 1.0f);
Shot(cam, file, label + " · 직교 size " + cam.orthographicSize.ToString("F1") + " · " + kLowW + "×" + kLowH + " → Point 확대", true);
UnityEngine.Object.DestroyImmediate(camGo);
}
static void Shot(Camera cam, string file, string label, bool pixel)
{
RenderTexture low = null, hi = null;
Texture2D tex = null;
try
{
if (pixel)
{
low = new RenderTexture(kLowW, kLowH, 24, RenderTextureFormat.ARGB32, RenderTextureReadWrite.sRGB);
low.filterMode = FilterMode.Point; low.antiAliasing = 1; low.Create();
hi = new RenderTexture(kHiW, kHiH, 0, RenderTextureFormat.ARGB32, RenderTextureReadWrite.sRGB);
hi.filterMode = FilterMode.Point; hi.Create();
cam.targetTexture = low; cam.Render(); cam.targetTexture = null;
Graphics.Blit(low, hi); // Point 확대 = 픽셀 카메라 근사
}
else
{
hi = new RenderTexture(kHiW, kHiH, 24, RenderTextureFormat.ARGB32, RenderTextureReadWrite.sRGB);
hi.Create();
cam.targetTexture = hi; cam.Render(); cam.targetTexture = null;
}
int dc = UnityEditor.UnityStats.drawCalls, batches = UnityEditor.UnityStats.batches, tri = UnityEditor.UnityStats.triangles;
var active = RenderTexture.active;
RenderTexture.active = hi;
tex = new Texture2D(kHiW, kHiH, TextureFormat.RGB24, false);
tex.ReadPixels(new Rect(0, 0, kHiW, kHiH), 0, 0);
tex.Apply();
RenderTexture.active = active;
File.WriteAllBytes(file, tex.EncodeToPNG());
var px = tex.GetPixels32();
var seen = new HashSet<int>();
long warm = 0;
for (int i = 0; i < px.Length; i += 331)
{
seen.Add((px[i].r << 16) | (px[i].g << 8) | px[i].b);
if (px[i].r > 120 && px[i].r > px[i].b + 40) warm++; // 주황·붉은 픽셀 = 장벽 표식
}
var fi = new FileInfo(file);
Chk("E " + Path.GetFileName(file), fi.Exists && fi.Length > 4096 && seen.Count > 4,
label + " · " + fi.Length + " B · 색 " + seen.Count + "종 · 따뜻한픽셀 " + warm +
" · drawCalls " + dc + " · batches " + batches + " · tri " + tri);
}
finally
{
RenderTexture.active = null;
if (tex != null) UnityEngine.Object.DestroyImmediate(tex);
if (low != null) { low.Release(); UnityEngine.Object.DestroyImmediate(low); }
if (hi != null) { hi.Release(); UnityEngine.Object.DestroyImmediate(hi); }
}
}
// ── 도구 ────────────────────────────────────────────────────────────────
static GameObject Inst(string path, Vector3 pos)
{
var p = AssetDatabase.LoadAssetAtPath<GameObject>(path);
if (p == null) { L(" [없음] " + path); return null; }
var go = UnityEngine.Object.Instantiate(p);
go.transform.position = pos;
return go;
}
}

View File

@ -0,0 +1,62 @@
// WL-815c 설정 에셋 생성 — Assets/WL/Combat/Stage/Barrier/Resources/WL/WLStageBarrierSettings.asset
// unity command run_script --file AgentScripts/WL815c_MakeAsset.cs --entry WL815c_MakeAsset.Run
public static class WL815c_MakeAsset
{
const string kDir = "Assets/WL/Combat/Stage/Barrier/Resources/WL";
const string kPath = kDir + "/WLStageBarrierSettings.asset";
public static string Run()
{
System.IO.Directory.CreateDirectory(kDir);
UnityEditor.AssetDatabase.Refresh();
var so = UnityEditor.AssetDatabase.LoadAssetAtPath<WL.Combat.Stage.WLStageBarrierSettings>(kPath);
bool made = false;
if (so == null)
{
so = UnityEngine.ScriptableObject.CreateInstance<WL.Combat.Stage.WLStageBarrierSettings>();
UnityEditor.AssetDatabase.CreateAsset(so, kPath);
made = true;
}
// ── ⓐ 링 = Hovl Marker 3 Zone Loop (기준서 §D-3 1순위 · 실측 시각 반경 1.10 m)
so.ringPrefab = Load("Assets/ResWork/VFX/Hovl Studio/Map track markers VFX/Prefabs/Marker 3 Zone Loop.prefab");
so.ringPrefabVisualRadius = 1.1f;
// ── ⓑ 프롭 = 울타리(LowPolyFantasyVillage Fence_01 실측 2.38 × 1.02 m · 120 tri) + 바위 2종
so.propPrefabs = new UnityEngine.GameObject[]
{
Load("Assets/ResWork/LowPolyFantasyVillage_URP/Prefabs/Props/Fence_01.prefab"),
Load("Assets/ResWork/LowPolyFantasyVillage_URP/Prefabs/Props/Fence_01.prefab"),
Load("Assets/LMHPOLY/Low Poly Nature Bundle/Rocks/Rock Assets/Prefabs/With_Box_Colliders/1 Color/Block_Rocks/Rock_Block_crk_m_01.prefab"),
};
// ── ⓒ 불꽃 = LMHPOLY Fire_Particles (실측 3 PS · 정상상태 125 파티클)
so.firePrefabs = new UnityEngine.GameObject[]
{
Load("Assets/LMHPOLY/Low Poly Nature Bundle/Rocks/Bonus Assets/Prefabs/ParticleSystem/Fire_Particles.prefab"),
};
UnityEditor.EditorUtility.SetDirty(so);
UnityEditor.AssetDatabase.SaveAssets();
UnityEditor.AssetDatabase.Refresh();
var chk = UnityEditor.AssetDatabase.LoadAssetAtPath<WL.Combat.Stage.WLStageBarrierSettings>(kPath);
var res = UnityEngine.Resources.Load<WL.Combat.Stage.WLStageBarrierSettings>(WL.Combat.Stage.WLStageBarrierSettings.ResourcesPath);
return (made ? "생성" : "갱신") + " " + kPath
+ " · 에셋로드 " + (chk != null)
+ " · Resources.Load " + (res != null)
+ " · ring " + (chk != null && chk.ringPrefab != null ? chk.ringPrefab.name : "null")
+ " · props " + (chk != null && chk.propPrefabs != null ? chk.propPrefabs.Length : 0)
+ " · fires " + (chk != null && chk.firePrefabs != null ? chk.firePrefabs.Length : 0)
+ " · style " + (chk != null ? chk.style.ToString() : "?");
}
static UnityEngine.GameObject Load(string p)
{
var g = UnityEditor.AssetDatabase.LoadAssetAtPath<UnityEngine.GameObject>(p);
if (g == null) UnityEngine.Debug.Log("[WL815c] 프리팹 없음 " + p);
return g;
}
}

View File

@ -0,0 +1,84 @@
WL-815c 스테이지 장벽 프로브 · 2026-09-11 00:59:28
앵커 = 기준서 §D-1 존1 (115.0, 0.4, -22.0) 반경 14 m
① 설정 에셋(C45 — 값은 전부 SO)
PASS 1-1 에셋 로드 — Resources/WL/WLStageBarrierSettings
PASS 1-2 막기 값 — leaveMargin 2 · grace 0.5 s · 쿨 1 s · inset 1.5 m · 샘플 4/20 m
PASS 1-3 몹 값 — 몹여유 3 · 유예 1 s · 쿨 2 s
PASS 1-4 카브 기본 off — carveNavMesh 0 · arc 4 m · 상한 32 · 두께 1.5 · 높이 3
PASS 1-5 스타일 — style=MagicRing · 입장 0.6 s · 클리어 0.6 s
PASS 1-6 ⓐ 링 프리팹 — Marker 3 Zone Loop · 실측 시각반경 1.1 m · 겹 3
PASS 1-7 ⓑ 프롭 프리팹 — Fence_01, Fence_01, Rock_Block_crk_m_01 · 간격 2.5 m · 상한 64
PASS 1-8 ⓒ 불꽃 프리팹 — Fire_Particles · 간격 2.5 m · 상한 48 · 파티클 총량 1600
② 임시 NavMesh 베이크(에디트 모드에서 실 워프 지점을 재기 위해)
PASS 2-1 NavMesh 베이크 — 정점 64 · 앵커 스냅 (115.00, 0.48, -22.00)
③ Set / Clear 왕복 (오브젝트 0 · 풀 반납)
PASS 3-1 초기 — Active=false · IsInside(어디든)=true → True
PASS 3-2 Set — Active=true · 중심 (115.0, 0.4, -22.0) 반경 14 · 그림 인스턴스 3 · Instantiate 누계 3
PASS 3-3 IsInside — 중심=안 · 13.9 m=안 · 14.1 m=밖 · 높이 무관(y+50 → True)
PASS 3-4a Clear 직후 = 페이드 중 — Active=false · 페이드 중 활성 3 · 진행도 1.00
PASS 3-4 페이드 끝 → 활성 0 · 풀 반납 — 활성 0 · 풀 3(= Instantiate 누계 3) · Destroy 0
PASS 3-5 재Set → 풀 재사용 — Instantiate 3 → 3(증가 0) · 활성 3
PASS 3-6 입장/클리어 연출 — 스케일 t=0 0.00 → t=.5 6.14 → t=1 12.27 (appear 0.6 s / fade 0.6 s)
PASS 3-7 반경 0 → Clear — Set(r=0) → Active=False
④ 되돌릴 지점 계산(순수 함수 · NearestInsidePoint)
(145.0, 0.4, -22.0) (중심 30.0 m) → (127.50, 0.40, -22.00) (중심 12.50 m · 방위 유지 Y)
(115.0, 0.4, -47.0) (중심 25.0 m) → (115.00, 0.40, -34.50) (중심 12.50 m · 방위 유지 Y)
(75.0, 3.4, 18.0) (중심 56.6 m) → (106.16, 3.40, -13.16) (중심 12.50 m · 방위 유지 Y)
(129.5, 0.4, -22.0) (중심 14.5 m) → (127.50, 0.40, -22.00) (중심 12.50 m · 방위 유지 Y)
PASS 4-1 안쪽 12.5 m · 방위 유지 — 4/4 · inset 1.5 m
PASS 4-2 정중앙 예외 — 중심 그대로 (115.00, 0.40, -22.00)(0 나누기 없음)
⑤ 실 워프(원본 Actor.Set_Warp · NavMesh 스냅)
PASS 5-1 스냅 지점 = 안쪽 · NavMesh 위 — 밖 35 m → (127.50, 0.48, -22.00) · 중심 12.50 m(< 14) · NavMesh 위 True
PASS 5-2 NavMesh 없으면 워프 안 함 — 먼 좌표(9000,0,9000) → false = 워프 포기(지형 뚫기 방지)
PASS 5-3 PC 실 워프 — 밖 30.0 m → 중심 12.50 m · PlayerWarps 1 · (145.0, 0.4, -22.0) → (127.5, 0.5, -22.0)
PASS 5-4 안에 있으면 무동작 — 중심에서 ForceCheck → 워프 안 함
PASS 5-5 몹 복귀 — 밖 28 m → 중심 12.50 m · MobWarps 1
PASS 5-6 유예 안에서는 안 되돌린다 — grace 999 s · 틱 ×2 → 워프 0 (OutsideSeen 0)
⑥ 스타일 전환 3회 (PD 가 값 하나로 바꾼다)
PASS 6-1 MagicRing — 활성 3 · 렌더러 12 · 머티리얼 1 · tri 0 · PS 12 · 빌드 1.8 ms · 링 스케일 12.27(반경 14 ÷ 실측 1.1)
PASS 6-2 Props — 활성 35 · 렌더러 35 · 머티리얼 2 · tri 4530 · PS 0 · 빌드 1.4 ms · 프롭 35개 · 실간격 2.51 m
PASS 6-3 Fire — 활성 35 · 렌더러 105 · 머티리얼 3 · tri 0 · PS 105 · 빌드 11.2 ms · 불꽃 35개 · 방출률 ×0.37 · 파티클 추정 1600(개당 125)
PASS 6-4 전환 누계 — StyleSwitches 3 · Instantiate 누계 73
PASS 6-5 None → 그림 0 — 활성 그림 0 · 판정은 그대로 Active=True
⑦ NavMesh 카브 on/off (옵션 · 기준서 §D-2 ②)
PASS 7-1 off(기본) → 카브 0 — carveNavMesh 0 → 오브젝트 0
PASS 7-2 on → 세그먼트 생성 — 둘레 88.0 m ÷ arc 4 m = 22(상한 32) · 오브젝트 22 · 생성 0.44 ms · Set 전체 3.31 ms
PASS 7-3 세그먼트 기하 — carving=true 22 · Box 22 · 상자 4.60 × 두께 1.50 m > 이웃 간격 3.98 m(이론 4.00 · 겹침 15 %) · 원 이탈 최대 0.0000 m
NavMesh 삼각형 32 → 32 (에디트 모드는 carve 가 즉시 반영되지 않을 수 있다 — 실 반영·재베이크 ms 는 Play 확인)
PASS 7-4 Clear → 카브 회수 — 오브젝트 0
⑧ GC (프레임당 할당 0)
PASS 8-1 반경 안 · 틱 ×200 — 할당 0 B (틱마다 0.0 B) · 계측 = Profiler.GetMonoUsedSizeLong
대조군(byte[65536] ×200 = 12.8 MB) 13926400 B · sink True
PASS 8-2 폴링 동작 — Scans 2 · Checks 0 → 1 (몹 1기 · 안쪽이라 워프 0)
PASS 8-3 몹 1기 · 틱 ×200 — 할당 0 B
⑨ C8 롤백 진리표
PASS 9-1 enabled=0 → Set 무동작 — Active=False · Instantiate 0 (오브젝트 0)
PASS 9-2 RuntimeDisabled=1 → Set 무동작 — Active=False · Instantiate 0
PASS 9-3 에셋 없음 → Set 무동작 — Active=False · FeatureEnabled=False
PASS 9-4 blockEnabled=0 → 판정 0 · 그림은 남는다 — Checks 0 · 그림 3
PASS 9-5 showVisual=0 → 그림 0 · 판정은 산다 — 그림 0 · Active=True
⑩ 성능 표 (반경 14 m · 스타일별)
| 스타일 | 인스턴스 | 렌더러 | 머티리얼 | 삼각형 | 파티클시스템 | 정상상태 파티클 | 빌드 ms |
|---|---|---|---|---|---|---|---|
| MagicRing | 3 | 12 | 1 | 0 | 12 | 12 | 1.7 |
| Props | 35 | 35 | 2 | 4530 | 0 | 0 | 1.3 |
| Fire | 35 | 105 | 3 | 0 | 105 | 1600 | 10.4 |
PASS 10-1 성능 표 — 위 표 참조(에디트 모드 측정 · 실 드로우콜은 캡처의 UnityStats)
반경별 프롭 개수(간격 2.5 m · 상한 64)
r 8 m(둘레 50.3 m) → 프롭 20 · 실간격 2.51 m · 카브 세그(on 일 때) 13
r 14 m(둘레 88.0 m) → 프롭 35 · 실간격 2.51 m · 카브 세그(on 일 때) 22
r 20 m(둘레 125.7 m) → 프롭 50 · 실간격 2.51 m · 카브 세그(on 일 때) 32
r 30 m(둘레 188.5 m) → 프롭 64 · 실간격 2.95 m · 카브 세그(on 일 때) 32
r 40 m(둘레 251.3 m) → 프롭 64 · 실간격 3.93 m · 카브 세그(on 일 때) 32
RESULT PASS · 43 PASS · 0 FAIL

View File

@ -0,0 +1,636 @@
// ─────────────────────────────────────────────────────────────────────────────
// WL815c_Probe.cs — 스테이지 장벽 검증(에디트 모드 · Play 0 · 로그인 0)
// unity command run_script --file AgentScripts/WL815c_Probe.cs --entry WL815c_Probe.RunAll
// 산출물: AgentScripts/WL815c_PROBE.txt
// ─────────────────────────────────────────────────────────────────────────────
using System;
using System.Diagnostics;
using System.IO;
using System.Text;
using UnityEditor;
using UnityEditor.SceneManagement;
using UnityEngine;
using UnityEngine.AI;
using UnityEngine.Profiling;
using UnityEngine.SceneManagement;
using WL.Combat.Stage;
public static class WL815c_Probe
{
static StringBuilder s_sb;
static int s_pass, s_fail;
// 기준서 §D-1 존1 앵커
static readonly Vector3 kAnchor = new Vector3(115f, 0.4f, -22f);
const float kRadius = 14f;
static void L(string s) { s_sb.AppendLine(s); }
static void Chk(string name, bool ok, string detail)
{
if (ok) s_pass++; else s_fail++;
L((ok ? " PASS " : " FAIL ") + name + " — " + detail);
}
public static string RunAll()
{
s_sb = new StringBuilder(); s_pass = 0; s_fail = 0;
L("WL-815c 스테이지 장벽 프로브 · " + DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss"));
L(" 앵커 = 기준서 §D-1 존1 " + kAnchor.ToString("F1") + " 반경 " + kRadius + " m");
var keepSo = WLStageBarrierSettings.Instance;
Scene temp = default(Scene);
try
{
temp = EditorSceneManager.NewScene(
NewSceneSetup.DefaultGameObjects,
NewSceneMode.Single);
Sec1_Settings();
var ground = MakeGround();
bool navOk = BakeNav(ground);
Sec2_SetClear();
Sec3_WarpMath();
Sec4_WarpReal(navOk);
Sec5_Styles();
Sec6_Carve();
Sec7_GC();
Sec8_C8();
Sec9_Cost();
}
catch (Exception ex) { s_fail++; L(" FAIL 예외 — " + ex.GetType().Name + " " + ex.Message + "\n" + ex.StackTrace); }
finally
{
try { StageBarrier.HardResetForProbe(); } catch { }
WLStageBarrierSettings.SetInstanceForProbe(keepSo);
WLStageBarrierSettings.RuntimeDisabled = false;
}
L("");
L("RESULT " + (s_fail == 0 ? "PASS" : "FAIL") + " · " + s_pass + " PASS · " + s_fail + " FAIL");
File.WriteAllText("AgentScripts/WL815c_PROBE.txt", s_sb.ToString());
return (s_fail == 0 ? "PASS" : "FAIL") + " " + s_pass + "/" + (s_pass + s_fail);
}
// ═════════════════════════════════════════════════════════════════════════
static void Sec1_Settings()
{
L("");
L("① 설정 에셋(C45 — 값은 전부 SO)");
var c = WLStageBarrierSettings.Instance;
Chk("1-1 에셋 로드", c != null, c != null ? "Resources/WL/WLStageBarrierSettings" : "null");
if (c == null) return;
Chk("1-2 막기 값", c.leaveMargin > 0 && c.graceSeconds >= 0,
"leaveMargin " + c.leaveMargin + " · grace " + c.graceSeconds + " s · 쿨 " + c.warpCooldownSeconds +
" s · inset " + c.warpInsetMeters + " m · 샘플 " + c.warpNavSampleRadius + "/" + c.warpNavSampleRadiusFar + " m");
Chk("1-3 몹 값", c.blockMobs, "몹여유 " + c.mobLeaveMargin + " · 유예 " + c.mobGraceSeconds + " s · 쿨 " + c.mobWarpCooldownSeconds + " s");
Chk("1-4 카브 기본 off", !c.carveNavMesh, "carveNavMesh " + (c.carveNavMesh ? 1 : 0) + " · arc " + c.carveSegmentArcMeters +
" m · 상한 " + c.maxCarveSegments + " · 두께 " + c.carveThickness + " · 높이 " + c.carveHeight);
Chk("1-5 스타일", true, "style=" + c.style + " · 입장 " + c.appearSeconds + " s · 클리어 " + c.fadeSeconds + " s");
Chk("1-6 ⓐ 링 프리팹", c.ringPrefab != null, (c.ringPrefab != null ? c.ringPrefab.name : "null") +
" · 실측 시각반경 " + c.ringPrefabVisualRadius + " m · 겹 " + (1 + c.ringExtraRings));
Chk("1-7 ⓑ 프롭 프리팹", c.propPrefabs != null && c.propPrefabs.Length > 0,
Names(c.propPrefabs) + " · 간격 " + c.propSpacing + " m · 상한 " + c.maxProps);
Chk("1-8 ⓒ 불꽃 프리팹", c.firePrefabs != null && c.firePrefabs.Length > 0,
Names(c.firePrefabs) + " · 간격 " + c.fireSpacing + " m · 상한 " + c.maxFires + " · 파티클 총량 " + c.maxTotalParticles);
}
static string Names(GameObject[] a)
{
if (a == null || a.Length == 0) return "(없음)";
var sb = new StringBuilder();
for (int i = 0; i < a.Length; i++) { if (i > 0) sb.Append(", "); sb.Append(a[i] != null ? a[i].name : "null"); }
return sb.ToString();
}
// ═════════════════════════════════════════════════════════════════════════
static GameObject MakeGround()
{
var g = GameObject.CreatePrimitive(PrimitiveType.Plane);
g.name = "[WL815c] Ground";
g.transform.position = new Vector3(kAnchor.x, kAnchor.y, kAnchor.z);
g.transform.localScale = Vector3.one * 12f; // 120 × 120 m
return g;
}
static bool BakeNav(GameObject ground)
{
L("");
L("② 임시 NavMesh 베이크(에디트 모드에서 실 워프 지점을 재기 위해)");
try
{
var surf = ground.AddComponent<Unity.AI.Navigation.NavMeshSurface>();
surf.collectObjects = Unity.AI.Navigation.CollectObjects.All;
surf.BuildNavMesh();
var tri = NavMesh.CalculateTriangulation();
NavMeshHit h;
bool hit = NavMesh.SamplePosition(kAnchor, out h, 5f, NavMesh.AllAreas);
Chk("2-1 NavMesh 베이크", tri.vertices != null && tri.vertices.Length > 0 && hit,
"정점 " + (tri.vertices != null ? tri.vertices.Length : 0) + " · 앵커 스냅 " + (hit ? h.position.ToString("F2") : "실패"));
return hit;
}
catch (Exception ex) { Chk("2-1 NavMesh 베이크", false, "미확인 — " + ex.Message); return false; }
}
// ═════════════════════════════════════════════════════════════════════════
static void Sec2_SetClear()
{
L("");
L("③ Set / Clear 왕복 (오브젝트 0 · 풀 반납)");
var B = typeof(StageBarrier);
StageBarrier.HardResetForProbe();
Chk("3-1 초기", !StageBarrier.Active, "Active=false · IsInside(어디든)=true → " +
StageBarrier.IsInside(new Vector3(999f, 0f, 999f)));
StageBarrier.Set(kAnchor, kRadius);
int inst1 = StageBarrierVisual.Instantiates;
int live1 = StageBarrierVisual.LiveCount;
Chk("3-2 Set", StageBarrier.Active && live1 > 0,
"Active=true · 중심 " + StageBarrier.Center.ToString("F1") + " 반경 " + StageBarrier.Radius +
" · 그림 인스턴스 " + live1 + " · Instantiate 누계 " + inst1);
Chk("3-3 IsInside", StageBarrier.IsInside(kAnchor)
&& StageBarrier.IsInside(kAnchor + new Vector3(13.9f, 0f, 0f))
&& !StageBarrier.IsInside(kAnchor + new Vector3(14.1f, 0f, 0f)),
"중심=안 · 13.9 m=안 · 14.1 m=밖 · 높이 무관(y+50 → " +
StageBarrier.IsInside(kAnchor + new Vector3(0f, 50f, 0f)) + ")");
// 클리어 → 페이드 도중에는 아직 살아 있어야 한다(연출)
StageBarrier.Clear();
StageBarrier.ForceTick();
int liveMid = StageBarrierVisual.LiveCount;
Chk("3-4a Clear 직후 = 페이드 중", !StageBarrier.Active && liveMid > 0,
"Active=false · 페이드 중 활성 " + liveMid + " · 진행도 " + StageBarrierVisual.Phase.ToString("F2"));
// 페이드 시계를 끝으로 → 실제 종료 경로(ReturnAll)를 탄다
StageBarrierVisual.FinishPhaseForProbe();
StageBarrier.ForceTick();
int live2 = StageBarrierVisual.LiveCount;
int pooled = StageBarrierVisual.PooledInstances;
Chk("3-4 페이드 끝 → 활성 0 · 풀 반납", !StageBarrier.Active && live2 == 0 && pooled == inst1,
"활성 " + live2 + " · 풀 " + pooled + "(= Instantiate 누계 " + inst1 + ") · Destroy 0");
// 두 번째 Set — 풀에서 꺼내 쓰므로 Instantiate 가 늘지 않아야 한다
StageBarrier.Set(kAnchor, kRadius);
int inst2 = StageBarrierVisual.Instantiates;
Chk("3-5 재Set → 풀 재사용", inst2 == inst1 && StageBarrierVisual.LiveCount == live1,
"Instantiate " + inst1 + " → " + inst2 + "(증가 " + (inst2 - inst1) + ") · 활성 " + StageBarrierVisual.LiveCount);
// 입장 연출
StageBarrierVisual.ForcePhase(0f);
float s0 = FirstScale();
StageBarrierVisual.ForcePhase(0.5f);
float s5 = FirstScale();
StageBarrierVisual.ForcePhase(1f);
float s1 = FirstScale();
Chk("3-6 입장/클리어 연출", s0 < s5 && s5 < s1, "스케일 t=0 " + s0.ToString("F2") + " → t=.5 " + s5.ToString("F2") +
" → t=1 " + s1.ToString("F2") + " (appear " + WLStageBarrierSettings.Instance.appearSeconds +
" s / fade " + WLStageBarrierSettings.Instance.fadeSeconds + " s)");
Chk("3-7 반경 0 → Clear", true, ProbeZero());
_ = B;
}
static string ProbeZero()
{
StageBarrier.Set(kAnchor, 0f);
string r = "Set(r=0) → Active=" + StageBarrier.Active;
StageBarrier.Set(kAnchor, kRadius);
return r;
}
static float FirstScale()
{
var root = StageBarrierVisual.Root;
if (root == null || root.childCount == 0) return -1f;
for (int i = 0; i < root.childCount; i++)
if (root.GetChild(i).gameObject.activeSelf) return root.GetChild(i).localScale.x;
return 0f;
}
// ═════════════════════════════════════════════════════════════════════════
static void Sec3_WarpMath()
{
L("");
L("④ 되돌릴 지점 계산(순수 함수 · NearestInsidePoint)");
var c = WLStageBarrierSettings.Instance;
float inset = c.warpInsetMeters;
float want = kRadius - inset;
Vector3[] outs = {
kAnchor + new Vector3(30f, 0f, 0f),
kAnchor + new Vector3(0f, 0f, -25f),
kAnchor + new Vector3(-40f, 3f, 40f),
kAnchor + new Vector3(14.5f, 0f, 0f),
};
bool all = true;
for (int i = 0; i < outs.Length; i++)
{
var p = StageBarrier.NearestInsidePoint(c, kAnchor, kRadius, outs[i]);
float d = new Vector2(p.x - kAnchor.x, p.z - kAnchor.z).magnitude;
bool ok = Mathf.Abs(d - want) < 0.01f;
// 방향이 유지되는가(가장 가까운 안쪽 지점 = 같은 방위각)
var dirIn = new Vector2(p.x - kAnchor.x, p.z - kAnchor.z).normalized;
var dirOut = new Vector2(outs[i].x - kAnchor.x, outs[i].z - kAnchor.z).normalized;
bool sameDir = Vector2.Dot(dirIn, dirOut) > 0.999f;
all = all && ok && sameDir;
L(" " + outs[i].ToString("F1") + " (중심 " +
new Vector2(outs[i].x - kAnchor.x, outs[i].z - kAnchor.z).magnitude.ToString("F1") + " m) → " +
p.ToString("F2") + " (중심 " + d.ToString("F2") + " m · 방위 유지 " + (sameDir ? "Y" : "N") + ")");
}
Chk("4-1 안쪽 " + want + " m · 방위 유지", all, "4/4 · inset " + inset + " m");
var mid = StageBarrier.NearestInsidePoint(c, kAnchor, kRadius, kAnchor);
Chk("4-2 정중앙 예외", Mathf.Approximately(mid.x, kAnchor.x) && Mathf.Approximately(mid.z, kAnchor.z),
"중심 그대로 " + mid.ToString("F2") + "(0 나누기 없음)");
}
// ═════════════════════════════════════════════════════════════════════════
static void Sec4_WarpReal(bool navOk)
{
L("");
L("⑤ 실 워프(원본 Actor.Set_Warp · NavMesh 스냅)");
var c = WLStageBarrierSettings.Instance;
StageBarrier.Set(kAnchor, kRadius);
StageBarrier.ResetDiagnostics();
if (!navOk) { Chk("5-0 NavMesh", false, "미확인 — 베이크 실패라 실 워프 지점은 재지 못했다"); }
else
{
Vector3 t;
bool ok = StageBarrier.ResolveWarpTarget(c, kAnchor, kRadius, kAnchor + new Vector3(35f, 0f, 0f), out t);
float d = new Vector2(t.x - kAnchor.x, t.z - kAnchor.z).magnitude;
NavMeshHit h;
bool onMesh = NavMesh.SamplePosition(t, out h, 0.3f, NavMesh.AllAreas);
Chk("5-1 스냅 지점 = 안쪽 · NavMesh 위", ok && d < kRadius && onMesh,
"밖 35 m → " + t.ToString("F2") + " · 중심 " + d.ToString("F2") + " m(< " + kRadius + ") · NavMesh 위 " + onMesh);
// NavMesh 가 아예 없는 방향(평면 밖 200 m)
Vector3 t2;
bool ok2 = StageBarrier.ResolveWarpTarget(c, new Vector3(9000f, 0f, 9000f), kRadius,
new Vector3(9100f, 0f, 9000f), out t2);
Chk("5-2 NavMesh 없으면 워프 안 함", !ok2, "먼 좌표(9000,0,9000) → " + (ok2 ? "워프함(FAIL)" : "false = 워프 포기(지형 뚫기 방지)"));
}
// 실 PC 프리팹으로 왕복
var pcPrefab = AssetDatabase.LoadAssetAtPath<GameObject>("Assets/Res_Addr/PC/Ai01.prefab");
if (pcPrefab == null) { Chk("5-3 PC 프리팹", false, "미확인 — Ai01.prefab 없음"); }
else
{
var pc = UnityEngine.Object.Instantiate(pcPrefab);
pc.transform.position = kAnchor + new Vector3(30f, 0f, 0f);
var actor = pc.GetComponent<Actor>();
if (actor == null) Chk("5-3 PCActor", false, "미확인 — Actor 컴포넌트 없음");
else
{
// 🔴 에디트 모드는 Awake 가 돌지 않아 Actor.m_NavMeshAgent(비직렬화 protected)가 null 이다.
// Set_Warp 이 그것을 쓰므로 **프로브가** Awake 가 넣었을 값을 직접 심는다(게임 코드 무관 · 스캐폴딩).
SeedAgent(actor, pc);
try
{
Vector3 before = actor.Get_position();
bool warped = StageBarrier.ForceCheck(actor, true);
Vector3 after = actor.Get_position();
float dAfter = new Vector2(after.x - kAnchor.x, after.z - kAnchor.z).magnitude;
Chk("5-3 PC 실 워프", warped && dAfter < kRadius,
"밖 " + new Vector2(before.x - kAnchor.x, before.z - kAnchor.z).magnitude.ToString("F1") +
" m → 중심 " + dAfter.ToString("F2") + " m · PlayerWarps " + StageBarrier.PlayerWarps +
" · " + StageBarrier.LastWarpFrom.ToString("F1") + " → " + StageBarrier.LastWarpTo.ToString("F1"));
// 안에 있으면 워프하지 않는다
actor.transform.position = kAnchor;
bool w2 = StageBarrier.ForceCheck(actor, true);
Chk("5-4 안에 있으면 무동작", !w2, "중심에서 ForceCheck → 워프 " + (w2 ? "함(FAIL)" : "안 함"));
}
catch (Exception ex) { Chk("5-3 PC 실 워프", false, "미확인 — " + ex.GetType().Name + " " + ex.Message); }
}
UnityEngine.Object.DestroyImmediate(pc);
}
// 몹
var mobPrefab = AssetDatabase.LoadAssetAtPath<GameObject>("Assets/Res_Addr/Mobs/Mob/Batty_A.prefab");
if (mobPrefab == null) { Chk("5-5 몹 프리팹", false, "미확인 — Batty_A.prefab 없음"); }
else
{
var mob = UnityEngine.Object.Instantiate(mobPrefab);
mob.transform.position = kAnchor + new Vector3(0f, 0f, 28f);
var ma = mob.GetComponent<Actor>();
if (ma == null) Chk("5-5 MobActor", false, "미확인 — Actor 없음");
else
{
SeedAgent(ma, mob);
try
{
bool warped = StageBarrier.ForceCheck(ma, false);
float dAfter = new Vector2(ma.Get_position().x - kAnchor.x, ma.Get_position().z - kAnchor.z).magnitude;
Chk("5-5 몹 복귀", warped && dAfter < kRadius,
"밖 28 m → 중심 " + dAfter.ToString("F2") + " m · MobWarps " + StageBarrier.MobWarps);
}
catch (Exception ex) { Chk("5-5 몹 복귀", false, "미확인 — " + ex.GetType().Name + " " + ex.Message); }
}
UnityEngine.Object.DestroyImmediate(mob);
}
// 유예 · 쿨다운 진리표(가짜 시각 없이 — 유예를 0 으로 둔 임시 SO)
var tmp = Clone(c);
tmp.graceSeconds = 999f;
WLStageBarrierSettings.SetInstanceForProbe(tmp);
StageBarrier.ResetDiagnostics();
StageBarrier.Set(kAnchor, kRadius);
var g = new GameObject("probe_dummy");
g.transform.position = kAnchor + new Vector3(40f, 0f, 0f);
StageBarrier.ForceTick();
StageBarrier.ForceTick();
Chk("5-6 유예 안에서는 안 되돌린다", StageBarrier.PlayerWarps == 0 && StageBarrier.MobWarps == 0,
"grace 999 s · 틱 ×2 → 워프 0 (OutsideSeen " + StageBarrier.OutsideSeen + ")");
UnityEngine.Object.DestroyImmediate(g);
WLStageBarrierSettings.SetInstanceForProbe(c);
}
// ═════════════════════════════════════════════════════════════════════════
static void Sec5_Styles()
{
L("");
L("⑥ 스타일 전환 3회 (PD 가 값 하나로 바꾼다)");
var c = WLStageBarrierSettings.Instance;
StageBarrier.HardResetForProbe();
WLStageBarrierSettings.SetInstanceForProbe(c);
var styles = new StageBarrierStyle[] {
StageBarrierStyle.MagicRing,
StageBarrierStyle.Props,
StageBarrierStyle.Fire,
};
for (int i = 0; i < styles.Length; i++)
{
StageBarrierVisual.StyleOverride = styles[i];
StageBarrier.Set(kAnchor, kRadius);
StageBarrierVisual.ForcePhase(1f);
int rend, mats, ps; long tri;
StageBarrierVisual.MeasureCost(out rend, out mats, out tri, out ps);
string extra = "";
if (styles[i] == StageBarrierStyle.MagicRing)
extra = " · 링 스케일 " + StageBarrierVisual.RingScale.ToString("F2") + "(반경 " + kRadius + " ÷ 실측 " + c.ringPrefabVisualRadius + ")";
else if (styles[i] == StageBarrierStyle.Props)
extra = " · 프롭 " + StageBarrierVisual.PropCount + "개 · 실간격 " + StageBarrierVisual.PropSpacingUsed.ToString("F2") + " m";
else
extra = " · 불꽃 " + StageBarrierVisual.FireCount + "개 · 방출률 ×" + StageBarrierVisual.FireRateMul.ToString("F2") +
" · 파티클 추정 " + StageBarrierVisual.FireParticleEstimate + "(개당 " + StageBarrierVisual.FirePerInstanceEstimate + ")";
Chk("6-" + (i + 1) + " " + styles[i], StageBarrierVisual.CurrentStyle == styles[i] && StageBarrierVisual.LiveCount > 0,
"활성 " + StageBarrierVisual.LiveCount + " · 렌더러 " + rend + " · 머티리얼 " + mats + " · tri " + tri +
" · PS " + ps + " · 빌드 " + StageBarrierVisual.LastBuildMs.ToString("F1") + " ms" + extra);
}
Chk("6-4 전환 누계", StageBarrierVisual.StyleSwitches >= 2,
"StyleSwitches " + StageBarrierVisual.StyleSwitches + " · Instantiate 누계 " + StageBarrierVisual.Instantiates);
// None 으로 되돌리기
StageBarrierVisual.StyleOverride = StageBarrierStyle.None;
StageBarrier.Set(kAnchor, kRadius);
Chk("6-5 None → 그림 0", StageBarrierVisual.LiveCount == 0 && StageBarrier.Active,
"활성 그림 " + StageBarrierVisual.LiveCount + " · 판정은 그대로 Active=" + StageBarrier.Active);
StageBarrierVisual.StyleOverride = (StageBarrierStyle)(-1);
}
// ═════════════════════════════════════════════════════════════════════════
static void Sec6_Carve()
{
L("");
L("⑦ NavMesh 카브 on/off (옵션 · 기준서 §D-2 ②)");
var c = WLStageBarrierSettings.Instance;
StageBarrier.HardResetForProbe();
WLStageBarrierSettings.SetInstanceForProbe(c);
StageBarrier.Set(kAnchor, kRadius);
Chk("7-1 off(기본) → 카브 0", StageBarrier.CarveObjectCount == 0,
"carveNavMesh " + (c.carveNavMesh ? 1 : 0) + " → 오브젝트 " + StageBarrier.CarveObjectCount);
var tmp = Clone(c);
tmp.carveNavMesh = true;
WLStageBarrierSettings.SetInstanceForProbe(tmp);
StageBarrier.HardResetForProbe();
WLStageBarrierSettings.SetInstanceForProbe(tmp);
int want = StageBarrier.CarveSegmentCount(tmp, kRadius);
float circ = 2f * Mathf.PI * kRadius;
var before = NavMesh.CalculateTriangulation();
var sw = Stopwatch.StartNew();
StageBarrier.Set(kAnchor, kRadius);
sw.Stop();
Chk("7-2 on → 세그먼트 생성", StageBarrier.CarveObjectCount == want && want > 0,
"둘레 " + circ.ToString("F1") + " m ÷ arc " + tmp.carveSegmentArcMeters + " m = " + want +
"(상한 " + tmp.maxCarveSegments + ") · 오브젝트 " + StageBarrier.CarveObjectCount +
" · 생성 " + StageBarrier.LastCarveBuildMs.ToString("F2") + " ms · Set 전체 " + sw.Elapsed.TotalMilliseconds.ToString("F2") + " ms");
// 세그먼트 기하 확인 — 카브 오브젝트는 HideAndDontSave 라 FindObjectsByType 에 안 잡힌다(내부 목록으로 읽는다)
int carving = 0, boxShape = 0; float gap = -1f, sizeX = 0f, sizeZ = 0f, ringDev = 0f;
Vector3 p0 = Vector3.zero, p1 = Vector3.zero;
for (int i = 0; i < StageBarrier.CarveObjectCount; i++)
{
var ob = StageBarrier.CarveObstacleAt(i);
if (ob == null) continue;
if (ob.carving) carving++;
if (ob.shape == NavMeshObstacleShape.Box) boxShape++;
if (i == 0) { p0 = ob.transform.position; sizeX = ob.size.x; sizeZ = ob.size.z; }
if (i == 1) p1 = ob.transform.position;
ringDev = Mathf.Max(ringDev, Mathf.Abs(
new Vector2(ob.transform.position.x - kAnchor.x, ob.transform.position.z - kAnchor.z).magnitude - kRadius));
}
gap = Vector3.Distance(p0, p1);
float step = circ / Mathf.Max(1, want);
Chk("7-3 세그먼트 기하", carving == want && boxShape == want && sizeX > gap && ringDev < 0.01f,
"carving=true " + carving + " · Box " + boxShape + " · 상자 " + sizeX.ToString("F2") + " × 두께 " + sizeZ.ToString("F2") +
" m > 이웃 간격 " + gap.ToString("F2") + " m(이론 " + step.ToString("F2") + " · 겹침 " + (tmp.carveOverlap * 100f).ToString("F0") +
" %) · 원 이탈 최대 " + ringDev.ToString("F4") + " m");
// 재베이크(카브 반영) 비용 — 에디트 모드에서 실제로 반영되는지
var after = NavMesh.CalculateTriangulation();
L(" NavMesh 삼각형 " + (before.indices != null ? before.indices.Length / 3 : 0) + " → " + (after.indices != null ? after.indices.Length / 3 : 0) +
" (에디트 모드는 carve 가 즉시 반영되지 않을 수 있다 — 실 반영·재베이크 ms 는 Play 확인)");
StageBarrier.Clear();
Chk("7-4 Clear → 카브 회수", StageBarrier.CarveObjectCount == 0, "오브젝트 " + StageBarrier.CarveObjectCount);
WLStageBarrierSettings.SetInstanceForProbe(c);
StageBarrier.HardResetForProbe();
WLStageBarrierSettings.SetInstanceForProbe(c);
}
// ═════════════════════════════════════════════════════════════════════════
static void Sec7_GC()
{
L("");
L("⑧ GC (프레임당 할당 0)");
var c = WLStageBarrierSettings.Instance;
StageBarrier.Set(kAnchor, kRadius);
StageBarrierVisual.ForcePhase(1f);
for (int i = 0; i < 10; i++) StageBarrier.ProbeTick(); // 워밍
long a = Profiler.GetMonoUsedSizeLong();
for (int i = 0; i < 200; i++) StageBarrier.ProbeTick();
long b = Profiler.GetMonoUsedSizeLong();
Chk("8-1 반경 안 · 틱 ×200", b - a == 0, "할당 " + (b - a) + " B (틱마다 " + ((b - a) / 200f).ToString("F1") + " B) · 계측 = Profiler.GetMonoUsedSizeLong");
// 대조군 — 확실히 할당되는 배열 200개(계측기가 살아 있는지 확인)
long c0 = Profiler.GetMonoUsedSizeLong();
object sink = null;
for (int i = 0; i < 200; i++) sink = new byte[65536];
long c1 = Profiler.GetMonoUsedSizeLong();
L(" 대조군(byte[65536] ×200 = 12.8 MB) " + (c1 - c0) + " B · sink " + (sink != null));
// 폴링 검사가 실제로 도는지 — 씬에 몹을 하나 놓고 재수집·판정이 도는지 본다
var mobPrefab = AssetDatabase.LoadAssetAtPath<GameObject>("Assets/Res_Addr/Mobs/Mob/Batty_A.prefab");
GameObject probeMob = null;
if (mobPrefab != null)
{
probeMob = UnityEngine.Object.Instantiate(mobPrefab);
probeMob.transform.position = kAnchor; // 안쪽 = 워프 대상 아님
}
int chk0 = StageBarrier.Checks;
StageBarrier.ForceTick();
Chk("8-2 폴링 동작", StageBarrier.Scans > 0 && (probeMob == null || StageBarrier.Checks > chk0),
"Scans " + StageBarrier.Scans + " · Checks " + chk0 + " → " + StageBarrier.Checks +
" (몹 " + (probeMob != null ? 1 : 0) + "기 · 안쪽이라 워프 " + (StageBarrier.MobWarps) + ")");
// 몹이 있는 상태에서 다시 GC 측정(실전에 가까운 조건)
for (int i = 0; i < 10; i++) StageBarrier.ProbeTick();
long d0 = Profiler.GetMonoUsedSizeLong();
for (int i = 0; i < 200; i++) StageBarrier.ProbeTick();
long d1 = Profiler.GetMonoUsedSizeLong();
Chk("8-3 몹 1기 · 틱 ×200", d1 - d0 == 0, "할당 " + (d1 - d0) + " B");
if (probeMob != null) UnityEngine.Object.DestroyImmediate(probeMob);
_ = c;
}
// ═════════════════════════════════════════════════════════════════════════
static void Sec8_C8()
{
L("");
L("⑨ C8 롤백 진리표");
var real = WLStageBarrierSettings.Instance;
// enabled = 0
var off = Clone(real); off.enabled = false;
StageBarrier.HardResetForProbe();
WLStageBarrierSettings.SetInstanceForProbe(off);
StageBarrier.Set(kAnchor, kRadius);
Chk("9-1 enabled=0 → Set 무동작", !StageBarrier.Active && StageBarrierVisual.Instantiates == 0,
"Active=" + StageBarrier.Active + " · Instantiate " + StageBarrierVisual.Instantiates + " (오브젝트 0)");
// RuntimeDisabled
StageBarrier.HardResetForProbe();
WLStageBarrierSettings.SetInstanceForProbe(real);
WLStageBarrierSettings.RuntimeDisabled = true;
StageBarrier.Set(kAnchor, kRadius);
Chk("9-2 RuntimeDisabled=1 → Set 무동작", !StageBarrier.Active,
"Active=" + StageBarrier.Active + " · Instantiate " + StageBarrierVisual.Instantiates);
WLStageBarrierSettings.RuntimeDisabled = false;
// 에셋 없음
StageBarrier.HardResetForProbe();
WLStageBarrierSettings.SetInstanceForProbe(null);
StageBarrier.Set(kAnchor, kRadius);
Chk("9-3 에셋 없음 → Set 무동작", !StageBarrier.Active && !StageBarrier.FeatureEnabled,
"Active=" + StageBarrier.Active + " · FeatureEnabled=" + StageBarrier.FeatureEnabled);
// blockEnabled = 0 → 그림만
var noBlock = Clone(real); noBlock.blockEnabled = false;
StageBarrier.HardResetForProbe();
WLStageBarrierSettings.SetInstanceForProbe(noBlock);
StageBarrier.Set(kAnchor, kRadius);
var d = new GameObject("probe_far"); d.transform.position = kAnchor + new Vector3(60f, 0f, 0f);
for (int i = 0; i < 5; i++) StageBarrier.ForceTick();
Chk("9-4 blockEnabled=0 → 판정 0 · 그림은 남는다",
StageBarrier.Checks == 0 && StageBarrierVisual.LiveCount > 0,
"Checks " + StageBarrier.Checks + " · 그림 " + StageBarrierVisual.LiveCount);
UnityEngine.Object.DestroyImmediate(d);
// showVisual = 0 → 판정만
var noVis = Clone(real); noVis.showVisual = false;
StageBarrier.HardResetForProbe();
WLStageBarrierSettings.SetInstanceForProbe(noVis);
StageBarrier.Set(kAnchor, kRadius);
Chk("9-5 showVisual=0 → 그림 0 · 판정은 산다",
StageBarrierVisual.LiveCount == 0 && StageBarrier.Active,
"그림 " + StageBarrierVisual.LiveCount + " · Active=" + StageBarrier.Active);
StageBarrier.HardResetForProbe();
WLStageBarrierSettings.SetInstanceForProbe(real);
}
// ═════════════════════════════════════════════════════════════════════════
static void Sec9_Cost()
{
L("");
L("⑩ 성능 표 (반경 14 m · 스타일별)");
var c = WLStageBarrierSettings.Instance;
StageBarrier.HardResetForProbe();
WLStageBarrierSettings.SetInstanceForProbe(c);
L(" | 스타일 | 인스턴스 | 렌더러 | 머티리얼 | 삼각형 | 파티클시스템 | 정상상태 파티클 | 빌드 ms |");
L(" |---|---|---|---|---|---|---|---|");
var styles = new StageBarrierStyle[] {
StageBarrierStyle.MagicRing,
StageBarrierStyle.Props,
StageBarrierStyle.Fire,
};
for (int i = 0; i < styles.Length; i++)
{
StageBarrierVisual.StyleOverride = styles[i];
StageBarrier.Set(kAnchor, kRadius);
StageBarrierVisual.ForcePhase(1f);
int rend, mats, ps; long tri;
StageBarrierVisual.MeasureCost(out rend, out mats, out tri, out ps);
int est = styles[i] == StageBarrierStyle.Fire ? StageBarrierVisual.FireParticleEstimate
: (styles[i] == StageBarrierStyle.MagicRing ? StageBarrierVisual.LiveCount * 4 : 0);
L(" | " + styles[i] + " | " + StageBarrierVisual.LiveCount + " | " + rend + " | " + mats +
" | " + tri + " | " + ps + " | " + est + " | " + StageBarrierVisual.LastBuildMs.ToString("F1") + " |");
}
StageBarrierVisual.StyleOverride = (StageBarrierStyle)(-1);
Chk("10-1 성능 표", true, "위 표 참조(에디트 모드 측정 · 실 드로우콜은 캡처의 UnityStats)");
// 반경별 개수
L(" 반경별 프롭 개수(간격 " + c.propSpacing + " m · 상한 " + c.maxProps + ")");
float[] rs = { 8f, 14f, 20f, 30f, 40f };
StageBarrierVisual.StyleOverride = StageBarrierStyle.Props;
for (int i = 0; i < rs.Length; i++)
{
StageBarrier.Set(kAnchor, rs[i]);
L(" r " + rs[i] + " m(둘레 " + (2f * Mathf.PI * rs[i]).ToString("F1") + " m) → 프롭 " +
StageBarrierVisual.PropCount + " · 실간격 " + StageBarrierVisual.PropSpacingUsed.ToString("F2") +
" m · 카브 세그(on 일 때) " + StageBarrier.CarveSegmentCount(c, rs[i]));
}
StageBarrierVisual.StyleOverride = (StageBarrierStyle)(-1);
StageBarrier.HardResetForProbe();
WLStageBarrierSettings.SetInstanceForProbe(c);
}
/// <summary>
/// 에디트 모드 스캐폴딩 — Awake 가 넣었을 `Actor.m_NavMeshAgent`(비직렬화 protected)를 심는다.
/// 게임 코드에는 리플렉션이 0 이고, 이 프로브에서만 쓴다.
/// </summary>
static string SeedAgent(Actor a, GameObject go)
{
try
{
var agent = go.GetComponent<UnityEngine.AI.NavMeshAgent>();
if (agent == null) agent = go.GetComponentInChildren<UnityEngine.AI.NavMeshAgent>(true);
if (agent == null) return "NavMeshAgent 없음";
var fi = typeof(Actor).GetField("m_NavMeshAgent",
System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic);
if (fi == null) return "필드 없음";
fi.SetValue(a, agent);
return "seeded";
}
catch (Exception ex) { return ex.Message; }
}
// ═════════════════════════════════════════════════════════════════════════
static WLStageBarrierSettings Clone(WLStageBarrierSettings src)
{
var t = UnityEngine.Object.Instantiate(src);
t.hideFlags = HideFlags.HideAndDontSave;
return t;
}
}

View File

@ -0,0 +1,243 @@
WL-815c 장벽 보이기 후보 실측 · 2026-09-11 00:34:25
── ⓐ 링 후보 (Hovl) ─────────────────────────────────────────────
Marker 3 Zone Loop
경로 Assets/ResWork/VFX/Hovl Studio/Map track markers VFX/Prefabs/Marker 3 Zone Loop.prefab
렌더러 4(메시 0 · 파티클 4) · 머티리얼 1 · 삼각형 0
바운즈: 없음(메시 0)
파티클시스템 4 · maxParticles 합 4000 · rateOverTime 합 0.0/s · 최대수명 1.00 s · 정상상태 추정 0 · duration 1.00 · loop Y · shape radius 0.00
Marker 5 Circle Loop
경로 Assets/ResWork/VFX/Hovl Studio/Map track markers VFX/Prefabs/Marker 5 Circle Loop.prefab
렌더러 4(메시 0 · 파티클 4) · 머티리얼 1 · 삼각형 0
바운즈: 없음(메시 0)
파티클시스템 4 · maxParticles 합 4000 · rateOverTime 합 0.0/s · 최대수명 2.00 s · 정상상태 추정 0 · duration 1.00 · loop Y · shape radius 0.00
Marker 7 Danger zone Loop
경로 Assets/ResWork/VFX/Hovl Studio/Map track markers VFX/Prefabs/Marker 7 Danger zone Loop.prefab
렌더러 2(메시 0 · 파티클 2) · 머티리얼 1 · 삼각형 0
바운즈: 없음(메시 0)
파티클시스템 2 · maxParticles 합 2000 · rateOverTime 합 0.0/s · 최대수명 1.00 s · 정상상태 추정 0 · duration 1.00 · loop Y · shape radius 0.00
Marker 8 Safe zone Loop
경로 Assets/ResWork/VFX/Hovl Studio/Map track markers VFX/Prefabs/Marker 8 Safe zone Loop.prefab
렌더러 2(메시 0 · 파티클 2) · 머티리얼 1 · 삼각형 0
바운즈: 없음(메시 0)
파티클시스템 2 · maxParticles 합 2000 · rateOverTime 합 0.0/s · 최대수명 1.00 s · 정상상태 추정 0 · duration 1.00 · loop Y · shape radius 0.00
Marker 4 Pillar Loop
경로 Assets/ResWork/VFX/Hovl Studio/Map track markers VFX/Prefabs/Marker 4 Pillar Loop.prefab
렌더러 4(메시 0 · 파티클 4) · 머티리얼 1 · 삼각형 0
바운즈: 없음(메시 0)
파티클시스템 4 · maxParticles 합 4000 · rateOverTime 합 0.0/s · 최대수명 2.00 s · 정상상태 추정 0 · duration 1.00 · loop Y · shape radius 0.00
Magic circle 1 loop
경로 Assets/ResWork/VFX/Hovl Studio/Magic circles/Prefabs/Loop version/Magic circle 1 loop.prefab
렌더러 11(메시 0 · 파티클 11) · 머티리얼 9 · 삼각형 0
바운즈: 없음(메시 0)
파티클시스템 11 · maxParticles 합 11000 · rateOverTime 합 142.0/s · 최대수명 2.00 s · 정상상태 추정 284 · duration 1.00 · loop Y · shape radius 1.10
Magic circle 3 loop
경로 Assets/ResWork/VFX/Hovl Studio/Magic circles/Prefabs/Loop version/Magic circle 3 loop.prefab
렌더러 7(메시 0 · 파티클 7) · 머티리얼 7 · 삼각형 0
바운즈: 없음(메시 0)
파티클시스템 7 · maxParticles 합 7000 · rateOverTime 합 0.0/s · 최대수명 1.25 s · 정상상태 추정 0 · duration 1.00 · loop Y · shape radius 2.00
Magic circle 5 loop
경로 Assets/ResWork/VFX/Hovl Studio/Magic circles/Prefabs/Loop version/Magic circle 5 loop.prefab
렌더러 7(메시 0 · 파티클 7) · 머티리얼 7 · 삼각형 0
바운즈: 없음(메시 0)
파티클시스템 7 · maxParticles 합 7000 · rateOverTime 합 0.0/s · 최대수명 2.00 s · 정상상태 추정 0 · duration 1.00 · loop Y · shape radius 0.50
── ⓑ 프롭 후보 (울타리 · 바위 · 기둥) ───────────────────────────
Fence_01
경로 Assets/ResWork/LowPolyFantasyVillage_URP/Prefabs/Props/Fence_01.prefab
렌더러 1(메시 1 · 파티클 0) · 머티리얼 1 · 삼각형 120
바운즈 크기 (2.38, 1.02, 0.27) · 중심 (0.00, 0.50, 0.00) · 최대반경(XZ) 1.19 m
Fence_02
경로 Assets/ResWork/LowPolyFantasyVillage_URP/Prefabs/Props/Fence_02.prefab
렌더러 1(메시 1 · 파티클 0) · 머티리얼 1 · 삼각형 100
바운즈 크기 (2.20, 1.02, 0.27) · 중심 (-0.96, 0.51, 0.00) · 최대반경(XZ) 1.10 m
FenceBig_01
경로 Assets/ResWork/LowPolyFantasyVillage_URP/Prefabs/Props/FenceBig_01.prefab
렌더러 1(메시 1 · 파티클 0) · 머티리얼 1 · 삼각형 324
바운즈 크기 (2.26, 3.41, 0.32) · 중심 (0.01, 1.70, -0.01) · 최대반경(XZ) 1.13 m
FenceBig_02
경로 Assets/ResWork/LowPolyFantasyVillage_URP/Prefabs/Props/FenceBig_02.prefab
렌더러 1(메시 1 · 파티클 0) · 머티리얼 1 · 삼각형 324
바운즈 크기 (2.26, 3.41, 0.32) · 중심 (0.01, 1.69, -0.01) · 최대반경(XZ) 1.13 m
FenceBig_01
경로 Assets/LowPolyFantasyArena/Prefabs/FenceBig_01.prefab
렌더러 1(메시 1 · 파티클 0) · 머티리얼 1 · 삼각형 42
바운즈 크기 (0.48, 6.39, 0.51) · 중심 (-0.01, 3.19, 0.00) · 최대반경(XZ) 0.25 m
FenceBig_02
경로 Assets/LowPolyFantasyArena/Prefabs/FenceBig_02.prefab
렌더러 1(메시 1 · 파티클 0) · 머티리얼 1 · 삼각형 20
바운즈 크기 (0.37, 4.81, 0.38) · 중심 (0.00, 2.40, 0.02) · 최대반경(XZ) 0.19 m
fence_1
경로 Assets/ResWork/Aligned Games/Polygonal Woodlands Nature Asset Package/Models/Fence1/fence_1.prefab
렌더러 1(메시 1 · 파티클 0) · 머티리얼 1 · 삼각형 120
바운즈 크기 (5.01, 2.50, 0.45) · 중심 (-0.01, 1.25, -0.06) · 최대반경(XZ) 2.51 m
fence_2
경로 Assets/ResWork/Aligned Games/Polygonal Woodlands Nature Asset Package/Models/Fence2/fence_2.prefab
렌더러 1(메시 1 · 파티클 0) · 머티리얼 1 · 삼각형 120
바운즈 크기 (5.01, 2.50, 0.45) · 중심 (0.00, 1.25, -0.06) · 최대반경(XZ) 2.50 m
Stone_1
경로 Assets/3DPixelArtEnvironment/Prefabs/Stone_1.prefab
렌더러 1(메시 1 · 파티클 0) · 머티리얼 1 · 삼각형 36
바운즈 크기 (1.29, 1.62, 1.50) · 중심 (0.00, 0.00, 0.12) · 최대반경(XZ) 0.75 m
Stone_2
경로 Assets/3DPixelArtEnvironment/Prefabs/Stone_2.prefab
렌더러 1(메시 1 · 파티클 0) · 머티리얼 1 · 삼각형 44
바운즈 크기 (2.42, 2.62, 2.64) · 중심 (-0.07, 0.04, 0.04) · 최대반경(XZ) 1.32 m
Slab
경로 Assets/3DPixelArtEnvironment/Prefabs/Slab.prefab
렌더러 1(메시 1 · 파티클 0) · 머티리얼 1 · 삼각형 80
바운즈 크기 (3.37, 0.91, 4.41) · 중심 (0.37, -0.15, 0.73) · 최대반경(XZ) 2.20 m
Arch_Small
경로 Assets/3DPixelArtEnvironment/Prefabs/Arch_Small.prefab
렌더러 1(메시 1 · 파티클 0) · 머티리얼 1 · 삼각형 261
바운즈 크기 (13.58, 14.70, 11.91) · 중심 (2.09, 7.36, -12.48) · 최대반경(XZ) 6.79 m
Light_Post
경로 Assets/3DPixelArtEnvironment/Prefabs/Light_Post.prefab
렌더러 1(메시 1 · 파티클 0) · 머티리얼 1 · 삼각형 68
바운즈 크기 (3.67, 18.24, 7.27) · 중심 (0.00, 8.12, -1.51) · 최대반경(XZ) 3.64 m
── LMHPOLY 바위 (자연 번들) ────────────────────────────────────
Cloud_01
경로 Assets/LMHPOLY/Low Poly Nature Bundle/Rocks/Bonus Assets/Prefabs/Clouds/Cloud_01.prefab
렌더러 1(메시 1 · 파티클 0) · 머티리얼 1 · 삼각형 100
바운즈 크기 (18.31, 8.76, 8.70) · 중심 (0.64, 0.34, -0.45) · 최대반경(XZ) 9.16 m
Cloud_02
경로 Assets/LMHPOLY/Low Poly Nature Bundle/Rocks/Bonus Assets/Prefabs/Clouds/Cloud_02.prefab
렌더러 1(메시 1 · 파티클 0) · 머티리얼 1 · 삼각형 100
바운즈 크기 (14.14, 6.68, 7.58) · 중심 (0.12, 0.44, -0.32) · 최대반경(XZ) 7.07 m
Cloud_03
경로 Assets/LMHPOLY/Low Poly Nature Bundle/Rocks/Bonus Assets/Prefabs/Clouds/Cloud_03.prefab
렌더러 1(메시 1 · 파티클 0) · 머티리얼 1 · 삼각형 100
바운즈 크기 (31.30, 12.15, 11.05) · 중심 (-1.67, 1.71, -0.57) · 최대반경(XZ) 15.65 m
Ground_m_01
경로 Assets/LMHPOLY/Low Poly Nature Bundle/Rocks/Bonus Assets/Prefabs/Ground/Ground_m_01.prefab
렌더러 1(메시 1 · 파티클 0) · 머티리얼 1 · 삼각형 3872
바운즈 크기 (100.00, 0.86, 100.00) · 중심 (-50.00, -0.37, 50.00) · 최대반경(XZ) 50.00 m
Ground_m_02
경로 Assets/LMHPOLY/Low Poly Nature Bundle/Rocks/Bonus Assets/Prefabs/Ground/Ground_m_02.prefab
렌더러 1(메시 1 · 파티클 0) · 머티리얼 1 · 삼각형 3872
바운즈 크기 (100.00, 9.82, 100.00) · 중심 (-50.00, 2.67, 50.00) · 최대반경(XZ) 50.00 m
Ground_m_03
경로 Assets/LMHPOLY/Low Poly Nature Bundle/Rocks/Bonus Assets/Prefabs/Ground/Ground_m_03.prefab
렌더러 1(메시 1 · 파티클 0) · 머티리얼 1 · 삼각형 3872
바운즈 크기 (100.00, 8.21, 100.00) · 중심 (-50.00, -2.01, 50.00) · 최대반경(XZ) 50.00 m
Ground_m_04
경로 Assets/LMHPOLY/Low Poly Nature Bundle/Rocks/Bonus Assets/Prefabs/Ground/Ground_m_04.prefab
렌더러 1(메시 1 · 파티클 0) · 머티리얼 1 · 삼각형 3872
바운즈 크기 (100.00, 7.24, 100.00) · 중심 (-50.00, -3.57, 50.00) · 최대반경(XZ) 50.00 m
Ground_m_05
경로 Assets/LMHPOLY/Low Poly Nature Bundle/Rocks/Bonus Assets/Prefabs/Ground/Ground_m_05.prefab
렌더러 1(메시 1 · 파티클 0) · 머티리얼 1 · 삼각형 3872
바운즈 크기 (100.00, 29.22, 100.00) · 중심 (-50.00, 14.58, 50.00) · 최대반경(XZ) 50.00 m
Ground_m_06
경로 Assets/LMHPOLY/Low Poly Nature Bundle/Rocks/Bonus Assets/Prefabs/Ground/Ground_m_06.prefab
렌더러 1(메시 1 · 파티클 0) · 머티리얼 1 · 삼각형 3872
바운즈 크기 (100.00, 4.88, 100.00) · 중심 (-50.00, 2.28, 50.00) · 최대반경(XZ) 50.00 m
Ground_m_07
경로 Assets/LMHPOLY/Low Poly Nature Bundle/Rocks/Bonus Assets/Prefabs/Ground/Ground_m_07.prefab
렌더러 1(메시 1 · 파티클 0) · 머티리얼 1 · 삼각형 3872
바운즈 크기 (100.00, 27.42, 100.00) · 중심 (-50.00, 9.70, 50.00) · 최대반경(XZ) 50.00 m
(Rocks 폴더 프리팹 총 1455개)
── ⓒ 불꽃 후보 ────────────────────────────────────────────────
Fire_Particles
경로 Assets/LMHPOLY/Low Poly Nature Bundle/Rocks/Bonus Assets/Prefabs/ParticleSystem/Fire_Particles.prefab
렌더러 3(메시 0 · 파티클 3) · 머티리얼 3 · 삼각형 0
바운즈: 없음(메시 0)
파티클시스템 3 · maxParticles 합 3000 · rateOverTime 합 45.0/s · 최대수명 8.00 s · 정상상태 추정 360 · duration 5.00 · loop Y · shape radius 0.46
Fire buff
경로 Assets/ResWork/VFX/Hovl Studio/Epic Toon VFX 2/Prefabs/Fire buff/Fire buff.prefab
렌더러 19(메시 0 · 파티클 19) · 머티리얼 12 · 삼각형 0
바운즈: 없음(메시 0)
파티클시스템 19 · maxParticles 합 19000 · rateOverTime 합 0.0/s · 최대수명 2.20 s · 정상상태 추정 0 · duration 3.00 · loop Y · shape radius 5.00
SK_Flame
경로 Assets/ResWork/ModularDungeon/Prefabs/SK_Flame.prefab
렌더러 1(메시 0 · 파티클 0) · 머티리얼 1 · 삼각형 0
바운즈 크기 (0.01, 0.03, 0.01) · 중심 (0.00, 0.01, 0.00) · 최대반경(XZ) 0.01 m
FX_Flameflow_floor
경로 Assets/ResWork/EricWang/GameVFX Collection Vol3/Prefabs/FX_Flameflow_floor.prefab
렌더러 14(메시 0 · 파티클 14) · 머티리얼 11 · 삼각형 0
바운즈: 없음(메시 0)
파티클시스템 14 · maxParticles 합 11004 · rateOverTime 합 365.0/s · 최대수명 5.90 s · 정상상태 추정 2154 · duration 5.50 · loop Y · shape radius 6.60
PS_Cast_Fire
경로 Assets/ResWork/VFX/StylizedFantasyFxPack/Prefab/PS_Cast_Fire.prefab
렌더러 6(메시 0 · 파티클 6) · 머티리얼 6 · 삼각형 0
바운즈: 없음(메시 0)
파티클시스템 6 · maxParticles 합 7 · rateOverTime 합 0.0/s · 최대수명 5.00 s · 정상상태 추정 0 · duration 1.00 · loop N · shape radius 0.05
Effect_Hit_Mob_HellfireFlames
경로 Assets/Res_Addr/Effect/Effect_Hit_Mob_HellfireFlames.prefab
렌더러 8(메시 0 · 파티클 8) · 머티리얼 6 · 삼각형 0
바운즈: 없음(메시 0)
파티클시스템 8 · maxParticles 합 2013 · rateOverTime 합 115.0/s · 최대수명 0.70 s · 정상상태 추정 81 · duration 0.30 · loop N · shape radius 0.20
── 불꽃 추가 탐색(이름에 Fire/Flame · 파티클 보유 · 루프) ───────
Slash Projectile VFX Fire
경로 Assets/ResWork/VFX/Free Slash VFX/Prefabs/Projectiles/Slash Projectile VFX Fire.prefab
렌더러 13(메시 0 · 파티클 13) · 머티리얼 8 · 삼각형 0
바운즈: 없음(메시 0)
파티클시스템 13 · maxParticles 합 13000 · rateOverTime 합 910.0/s · 최대수명 2.00 s · 정상상태 추정 1820 · duration 1.00 · loop Y · shape radius 1.40
vfx_Projectile_ArrowFire
경로 Assets/ResWork/VFX/GabrielAguiarProductions/UniqueProjectilesVol_1/Prefabs/Projectiles/vfx_Projectile_ArrowFire.prefab
렌더러 6(메시 0 · 파티클 5) · 머티리얼 3 · 삼각형 0
바운즈 크기 (0.15, 0.15, 0.15) · 중심 (0.00, 0.00, 0.40) · 최대반경(XZ) 0.08 m
파티클시스템 5 · maxParticles 합 132 · rateOverTime 합 20.0/s · 최대수명 1.00 s · 정상상태 추정 20 · duration 1.00 · loop Y · shape radius 1.00
vfx_Projectile_Fireball01Blue
경로 Assets/ResWork/VFX/GabrielAguiarProductions/UniqueProjectilesVol_1/Prefabs/Projectiles/vfx_Projectile_Fireball01Blue.prefab
렌더러 7(메시 0 · 파티클 6) · 머티리얼 5 · 삼각형 0
바운즈 크기 (1.00, 1.00, 1.00) · 중심 (0.00, 0.00, 0.00) · 최대반경(XZ) 0.50 m
파티클시스템 6 · maxParticles 합 1019 · rateOverTime 합 51.0/s · 최대수명 1.00 s · 정상상태 추정 51 · duration 1.00 · loop Y · shape radius 1.00
vfx_Projectile_Fireball01Orange
경로 Assets/ResWork/VFX/GabrielAguiarProductions/UniqueProjectilesVol_1/Prefabs/Projectiles/vfx_Projectile_Fireball01Orange.prefab
렌더러 7(메시 0 · 파티클 6) · 머티리얼 5 · 삼각형 0
바운즈 크기 (1.00, 1.00, 1.00) · 중심 (0.00, 0.00, 0.00) · 최대반경(XZ) 0.50 m
파티클시스템 6 · maxParticles 합 1015 · rateOverTime 합 53.0/s · 최대수명 1.00 s · 정상상태 추정 53 · duration 1.00 · loop Y · shape radius 1.00
vfx_Projectile_Fireball02Green
경로 Assets/ResWork/VFX/GabrielAguiarProductions/UniqueProjectilesVol_1/Prefabs/Projectiles/vfx_Projectile_Fireball02Green.prefab
렌더러 6(메시 0 · 파티클 5) · 머티리얼 3 · 삼각형 0
바운즈 크기 (2.00, 2.00, 2.00) · 중심 (0.00, 0.00, 0.60) · 최대반경(XZ) 1.00 m
파티클시스템 5 · maxParticles 합 2008 · rateOverTime 합 80.0/s · 최대수명 1.00 s · 정상상태 추정 80 · duration 1.00 · loop Y · shape radius 0.70
vfx_Projectile_Fireball02Orange
경로 Assets/ResWork/VFX/GabrielAguiarProductions/UniqueProjectilesVol_1/Prefabs/Projectiles/vfx_Projectile_Fireball02Orange.prefab
렌더러 6(메시 0 · 파티클 5) · 머티리얼 3 · 삼각형 0
바운즈 크기 (2.00, 2.00, 2.00) · 중심 (0.00, 0.00, 0.60) · 최대반경(XZ) 1.00 m
파티클시스템 5 · maxParticles 합 2008 · rateOverTime 합 80.0/s · 최대수명 1.00 s · 정상상태 추정 80 · duration 1.00 · loop Y · shape radius 0.70
vfx_Projectile_SwordFire
경로 Assets/ResWork/VFX/GabrielAguiarProductions/UniqueProjectilesVol_1/Prefabs/Projectiles/vfx_Projectile_SwordFire.prefab
렌더러 7(메시 0 · 파티클 6) · 머티리얼 3 · 삼각형 0
바운즈 크기 (0.20, 0.20, 0.20) · 중심 (0.00, 0.00, 0.90) · 최대반경(XZ) 0.10 m
파티클시스템 6 · maxParticles 합 207 · rateOverTime 합 27.0/s · 최대수명 1.00 s · 정상상태 추정 27 · duration 1.00 · loop Y · shape radius 1.00
vfx_Projectile_Fireball03_Blue
경로 Assets/ResWork/VFX/GabrielAguiarProductions/UniqueProjectilesVol_2/Prefabs/Projectiles/vfx_Projectile_Fireball03_Blue.prefab
렌더러 12(메시 0 · 파티클 11) · 머티리얼 6 · 삼각형 0
바운즈 크기 (3.00, 3.00, 3.00) · 중심 (0.00, 0.00, 0.00) · 최대반경(XZ) 1.50 m
파티클시스템 11 · maxParticles 합 3135 · rateOverTime 합 150.0/s · 최대수명 1.00 s · 정상상태 추정 150 · duration 1.00 · loop Y · shape radius 1.00
vfx_Projectile_Fireball03_Green
경로 Assets/ResWork/VFX/GabrielAguiarProductions/UniqueProjectilesVol_2/Prefabs/Projectiles/vfx_Projectile_Fireball03_Green.prefab
렌더러 12(메시 0 · 파티클 11) · 머티리얼 6 · 삼각형 0
바운즈 크기 (3.00, 3.00, 3.00) · 중심 (0.00, 0.00, 0.00) · 최대반경(XZ) 1.50 m
파티클시스템 11 · maxParticles 합 3135 · rateOverTime 합 150.0/s · 최대수명 1.00 s · 정상상태 추정 150 · duration 1.00 · loop Y · shape radius 1.00
vfx_Projectile_Fireball03_Orange
경로 Assets/ResWork/VFX/GabrielAguiarProductions/UniqueProjectilesVol_2/Prefabs/Projectiles/vfx_Projectile_Fireball03_Orange.prefab
렌더러 12(메시 0 · 파티클 11) · 머티리얼 6 · 삼각형 0
바운즈 크기 (3.00, 3.00, 3.00) · 중심 (0.00, 0.00, 0.00) · 최대반경(XZ) 1.50 m
파티클시스템 11 · maxParticles 합 3132 · rateOverTime 합 150.0/s · 최대수명 1.00 s · 정상상태 추정 150 · duration 1.00 · loop Y · shape radius 1.00
vfx_Projectile_Fireball04_Blue
경로 Assets/ResWork/VFX/GabrielAguiarProductions/UniqueProjectilesVol_2/Prefabs/Projectiles/vfx_Projectile_Fireball04_Blue.prefab
렌더러 11(메시 0 · 파티클 10) · 머티리얼 4 · 삼각형 0
바운즈 크기 (1.30, 1.30, 1.30) · 중심 (0.00, 0.00, 0.00) · 최대반경(XZ) 0.65 m
파티클시스템 10 · maxParticles 합 3408 · rateOverTime 합 189.0/s · 최대수명 1.10 s · 정상상태 추정 208 · duration 1.00 · loop Y · shape radius 1.00
vfx_Projectile_Fireball04_Green
경로 Assets/ResWork/VFX/GabrielAguiarProductions/UniqueProjectilesVol_2/Prefabs/Projectiles/vfx_Projectile_Fireball04_Green.prefab
렌더러 11(메시 0 · 파티클 10) · 머티리얼 4 · 삼각형 0
바운즈 크기 (1.30, 1.30, 1.30) · 중심 (0.00, 0.00, 0.00) · 최대반경(XZ) 0.65 m
파티클시스템 10 · maxParticles 합 3408 · rateOverTime 합 189.0/s · 최대수명 1.10 s · 정상상태 추정 208 · duration 1.00 · loop Y · shape radius 1.00
vfx_Projectile_Fireball04_Orange
경로 Assets/ResWork/VFX/GabrielAguiarProductions/UniqueProjectilesVol_2/Prefabs/Projectiles/vfx_Projectile_Fireball04_Orange.prefab
렌더러 11(메시 0 · 파티클 10) · 머티리얼 4 · 삼각형 0
바운즈 크기 (1.30, 1.30, 1.30) · 중심 (0.00, 0.00, 0.00) · 최대반경(XZ) 0.65 m
파티클시스템 10 · maxParticles 합 3408 · rateOverTime 합 189.0/s · 최대수명 1.10 s · 정상상태 추정 208 · duration 1.00 · loop Y · shape radius 1.00
vfx_Projectile_Fireball05_Blue
경로 Assets/ResWork/VFX/GabrielAguiarProductions/UniqueProjectilesVol_2/Prefabs/Projectiles/vfx_Projectile_Fireball05_Blue.prefab
렌더러 11(메시 0 · 파티클 10) · 머티리얼 4 · 삼각형 0
바운즈 크기 (2.45, 2.45, 2.45) · 중심 (0.00, 0.00, 0.00) · 최대반경(XZ) 1.23 m
파티클시스템 10 · maxParticles 합 4283 · rateOverTime 합 175.0/s · 최대수명 1.00 s · 정상상태 추정 175 · duration 1.00 · loop Y · shape radius 1.00

View File

@ -0,0 +1,90 @@
WL-815c 2차 실측 · 2026-09-11 00:35:30
── 링 프리팹: Simulate 2 s 후 실제 시각 반경(XZ) ──────────────
Marker 3 Zone Loop
Simulate(2 s) 살아있는 파티클 3 · 시각 반경(XZ) 1.10 m · 높이 0.00~0.01 m · 렌더러 바운즈 반경 2.13 m
├ Marker 3 Zone Loop(Clone) : max 1000 · rate 0.0/s · 버스트 1종 총 1 · life 1.00 s · size 1.00 · shape off · scale (1.00, 1.00, 1.00) · 지금 1
├ Flash : max 1000 · rate 0.0/s · 버스트 1종 총 1 · life 1.00 s · size 2.00 · shape Hemisphere r0.00 · scale (1.00, 1.00, 1.00) · 지금 1
├ Runes : max 1000 · rate 0.0/s · 버스트 1종 총 1 · life 1.00 s · size 2.20 · shape Hemisphere r0.00 · scale (1.00, 1.00, 1.00) · 지금 1
├ ShockWave : max 1000 · rate 0.0/s · 버스트 1종 총 1 · life 0.30 s · size 3.00 · shape Hemisphere r0.00 · scale (1.00, 1.00, 1.00) · 지금 0
Marker 5 Circle Loop
Simulate(2 s) 살아있는 파티클 8 · 시각 반경(XZ) 1.02 m · 높이 0.00~0.01 m · 렌더러 바운즈 반경 1.45 m
├ Marker 5 Circle Loop(Clone) : max 1000 · rate 0.0/s · 버스트 1종 총 2 · life 1.00 s · size 1.00 · shape off · scale (1.00, 1.00, 1.00) · 지금 2
├ Flash : max 1000 · rate 0.0/s · 버스트 1종 총 1 · life 2.00 s · size 2.00 · shape Hemisphere r0.00 · scale (1.00, 1.00, 1.00) · 지금 2
├ Runes : max 1000 · rate 0.0/s · 버스트 1종 총 1 · life 2.00 s · size 2.04 · shape Hemisphere r0.00 · scale (1.00, 1.00, 1.00) · 지금 2
├ ShockWave : max 1000 · rate 0.0/s · 버스트 1종 총 2 · life 1.00 s · size 2.00 · shape Hemisphere r0.00 · scale (1.00, 1.00, 1.00) · 지금 2
Marker 7 Danger zone Loop
Simulate(2 s) 살아있는 파티클 4 · 시각 반경(XZ) 2.50 m · 높이 0.00~0.01 m · 렌더러 바운즈 반경 3.55 m
├ Marker 7 Danger zone Loop(Clone) : max 1000 · rate 0.0/s · 버스트 1종 총 2 · life 1.00 s · size 5.00 · shape Hemisphere r0.00 · scale (1.00, 1.00, 1.00) · 지금 2
├ ShockWave : max 1000 · rate 0.0/s · 버스트 1종 총 2 · life 1.00 s · size 5.00 · shape Hemisphere r0.00 · scale (1.00, 1.00, 1.00) · 지금 2
Marker 8 Safe zone Loop
Simulate(2 s) 살아있는 파티클 4 · 시각 반경(XZ) 2.50 m · 높이 0.00~0.01 m · 렌더러 바운즈 반경 3.55 m
├ Marker 8 Safe zone Loop(Clone) : max 1000 · rate 0.0/s · 버스트 1종 총 2 · life 1.00 s · size 5.00 · shape Hemisphere r0.00 · scale (1.00, 1.00, 1.00) · 지금 2
├ ShockWave : max 1000 · rate 0.0/s · 버스트 1종 총 2 · life 1.00 s · size 5.00 · shape Hemisphere r0.00 · scale (1.00, 1.00, 1.00) · 지금 2
── 불꽃 후보: 파티클 시스템별 상세 ────────────────────────────
Fire_Particles · PS 3
├ FireRed : rate 20.0/s × life 1.50 s = 30 (max 1000) · size 0.50 · loop Y · 정지공간 Local
├ FireYellow : rate 15.0/s × life 1.00 s = 15 (max 1000) · size 0.50 · loop Y · 정지공간 Local
├ Smoke : rate 10.0/s × life 8.00 s = 80 (max 1000) · size 1.00 · loop Y · 정지공간 Local
⇒ 인스턴스 1개 정상상태 추정 125 파티클
Fire buff · PS 19
├ Fire buff(Clone) : rate 0.0/s × life 2.00 s = 0 (max 1000) · size 5.00 · loop Y · 정지공간 Local
├ SparksStart : rate 0.0/s × life 0.15 s = 0 (max 1000) · size 0.10 · loop Y · 정지공간 Local
├ SparksExpl : rate 0.0/s × life 0.50 s = 0 (max 1000) · size 0.30 · loop Y · 정지공간 Local
├ Sparks : rate 0.0/s × life 0.50 s = 0 (max 1000) · size 0.20 · loop Y · 정지공간 Local
├ SparksEnd : rate 0.0/s × life 0.40 s = 0 (max 1000) · size 0.20 · loop Y · 정지공간 Local
├ StartRays : rate 0.0/s × life 0.20 s = 0 (max 1000) · size 0.40 · loop Y · 정지공간 Local
├ StartFlash : rate 0.0/s × life 0.15 s = 0 (max 1000) · size 8.00 · loop Y · 정지공간 Local
├ StartShockWave : rate 0.0/s × life 0.15 s = 0 (max 1000) · size 8.00 · loop Y · 정지공간 Local
├ ShockWave : rate 0.0/s × life 0.14 s = 0 (max 1000) · size 14.00 · loop Y · 정지공간 Local
├ ShockWaves : rate 0.0/s × life 0.35 s = 0 (max 1000) · size 14.00 · loop Y · 정지공간 Local
├ GroundWaves : rate 0.0/s × life 0.50 s = 0 (max 1000) · size 4.00 · loop Y · 정지공간 Local
├ GroundWaves2 : rate 0.0/s × life 0.60 s = 0 (max 1000) · size 3.50 · loop Y · 정지공간 Local
├ MainTrails : rate 0.0/s × life 1.90 s = 0 (max 1000) · size 1.00 · loop Y · 정지공간 Local
├ Ray : rate 0.0/s × life 2.00 s = 0 (max 1000) · size 1.00 · loop Y · 정지공간 Local
├ Twist : rate 0.0/s × life 2.00 s = 0 (max 1000) · size 0.65 · loop Y · 정지공간 Local
├ FireUp : rate 0.0/s × life 0.50 s = 0 (max 1000) · size 2.00 · loop Y · 정지공간 Local
├ FireCore : rate 0.0/s × life 2.20 s = 0 (max 1000) · size 1.40 · loop Y · 정지공간 Local
├ EndSmoke : rate 0.0/s × life 0.50 s = 0 (max 1000) · size 1.50 · loop Y · 정지공간 Local
├ Light : rate 0.0/s × life 2.00 s = 0 (max 1000) · size 1.00 · loop Y · 정지공간 Local
⇒ 인스턴스 1개 정상상태 추정 0 파티클
FX_Flameflow_floor · PS 14
├ FX_Flameflow_floor(Clone) : rate 10.0/s × life 5.00 s = 50 (max 1000) · size 1.00 · loop Y · 정지공간 Local
├ Pos : rate 0.0/s × life 0.05 s = 0 (max 1000) · size 0.37 · loop N · 정지공간 Local
├ glow_b : rate 10.0/s × life 5.90 s = 59 (max 1000) · size 26.00 · loop N · 정지공간 Local
├ fire : rate 0.0/s × life 5.80 s = 0 (max 1000) · size 9.00 · loop N · 정지공간 Local
├ glow : rate 0.0/s × life 5.90 s = 0 (max 1000) · size 18.00 · loop N · 정지공간 Local
├ Rune : rate 100.0/s × life 5.80 s = 1 (max 1) · size 23.00 · loop N · 정지공간 Local
├ Dust : rate 15.0/s × life 2.30 s = 35 (max 1000) · size 5.00 · loop N · 정지공간 World
├ bao : rate 100.0/s × life 0.30 s = 2 (max 2) · size 22.00 · loop N · 정지공간 Local
├ bao1 : rate 0.0/s × life 0.60 s = 0 (max 1000) · size 13.00 · loop N · 정지공간 Local
├ bao2 : rate 10.0/s × life 0.50 s = 1 (max 1) · size 7.00 · loop N · 정지공간 Local
├ Gradient_ap : rate 0.0/s × life 0.60 s = 0 (max 1000) · size 27.00 · loop N · 정지공간 Local
├ startwave_R : rate 0.0/s × life 5.80 s = 0 (max 1000) · size 12.00 · loop N · 정지공간 Local
├ startwave_Y : rate 0.0/s × life 5.80 s = 0 (max 1000) · size 12.00 · loop N · 정지공간 Local
├ dust_ring : rate 120.0/s × life 2.30 s = 276 (max 1000) · size 4.00 · loop N · 정지공간 World
⇒ 인스턴스 1개 정상상태 추정 424 파티클
── LMHPOLY 바위(비 LOD · Rock 계열) ───────────────────────────
Rock_Block_crk_m_01 · Assets/LMHPOLY/Low Poly Nature Bundle/Rocks/Rock Assets/Prefabs/With_Box_Colliders/1 Color/Block_Rocks/Rock_Block_crk_m_01.prefab
크기 (1.49, 0.74, 1.47) · tri 150 · 렌더러 1
Rock_Block_crk_m_02 · Assets/LMHPOLY/Low Poly Nature Bundle/Rocks/Rock Assets/Prefabs/With_Box_Colliders/1 Color/Block_Rocks/Rock_Block_crk_m_02.prefab
크기 (1.33, 0.86, 1.40) · tri 134 · 렌더러 1
Rock_Block_crk_m_03 · Assets/LMHPOLY/Low Poly Nature Bundle/Rocks/Rock Assets/Prefabs/With_Box_Colliders/1 Color/Block_Rocks/Rock_Block_crk_m_03.prefab
크기 (1.94, 0.78, 0.92) · tri 62 · 렌더러 1
Rock_Block_crk_m_04 · Assets/LMHPOLY/Low Poly Nature Bundle/Rocks/Rock Assets/Prefabs/With_Box_Colliders/1 Color/Block_Rocks/Rock_Block_crk_m_04.prefab
크기 (1.26, 0.70, 1.31) · tri 152 · 렌더러 1
Rock_Block_crk_m_05 · Assets/LMHPOLY/Low Poly Nature Bundle/Rocks/Rock Assets/Prefabs/With_Box_Colliders/1 Color/Block_Rocks/Rock_Block_crk_m_05.prefab
크기 (1.48, 0.70, 0.79) · tri 42 · 렌더러 1
Rock_Block_m_01 · Assets/LMHPOLY/Low Poly Nature Bundle/Rocks/Rock Assets/Prefabs/With_Box_Colliders/1 Color/Block_Rocks/Rock_Block_m_01.prefab
크기 (1.07, 0.58, 0.58) · tri 12 · 렌더러 1
Rock_Block_m_02 · Assets/LMHPOLY/Low Poly Nature Bundle/Rocks/Rock Assets/Prefabs/With_Box_Colliders/1 Color/Block_Rocks/Rock_Block_m_02.prefab
크기 (1.13, 0.63, 0.63) · tri 44 · 렌더러 1
Rock_Block_m_03 · Assets/LMHPOLY/Low Poly Nature Bundle/Rocks/Rock Assets/Prefabs/With_Box_Colliders/1 Color/Block_Rocks/Rock_Block_m_03.prefab
크기 (1.20, 0.60, 0.64) · tri 44 · 렌더러 1
── 울타리 프롭 pivot·회전 확인(둘레 배치 적합성) ───────────────
Fence_01 : pivot→바운즈중심 (0.00, 0.50, 0.00) · 크기 (2.38, 1.02, 0.27) · 긴축 X 2.38 · 바닥 y -0.01
FenceBig_01 : pivot→바운즈중심 (0.01, 1.70, -0.01) · 크기 (2.26, 3.41, 0.32) · 긴축 X 2.26 · 바닥 y -0.01
Stone_1 : pivot→바운즈중심 (0.00, 0.00, 0.12) · 크기 (1.29, 1.62, 1.50) · 긴축 Z 1.50 · 바닥 y -0.81

View File

@ -0,0 +1,164 @@
// WL-815c 보유 에셋 실측 — 장벽 「보이기」 후보 (임시 · 커밋 대상)
// unity command run_script --file AgentScripts/WL815c_Survey.cs --entry WL815c_Survey.Run --project-path E:\NerdNavis\WL_wt\gameplay2
public static class WL815c_Survey
{
static System.Text.StringBuilder s_sb;
static void L(string s) { s_sb.AppendLine(s); UnityEngine.Debug.Log(s); }
public static string Run()
{
s_sb = new System.Text.StringBuilder();
L("WL-815c 장벽 보이기 후보 실측 · " + System.DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss"));
L("");
L("── ⓐ 링 후보 (Hovl) ─────────────────────────────────────────────");
string[] rings = {
"Assets/ResWork/VFX/Hovl Studio/Map track markers VFX/Prefabs/Marker 3 Zone Loop.prefab",
"Assets/ResWork/VFX/Hovl Studio/Map track markers VFX/Prefabs/Marker 5 Circle Loop.prefab",
"Assets/ResWork/VFX/Hovl Studio/Map track markers VFX/Prefabs/Marker 7 Danger zone Loop.prefab",
"Assets/ResWork/VFX/Hovl Studio/Map track markers VFX/Prefabs/Marker 8 Safe zone Loop.prefab",
"Assets/ResWork/VFX/Hovl Studio/Map track markers VFX/Prefabs/Marker 4 Pillar Loop.prefab",
"Assets/ResWork/VFX/Hovl Studio/Magic circles/Prefabs/Loop version/Magic circle 1 loop.prefab",
"Assets/ResWork/VFX/Hovl Studio/Magic circles/Prefabs/Loop version/Magic circle 3 loop.prefab",
"Assets/ResWork/VFX/Hovl Studio/Magic circles/Prefabs/Loop version/Magic circle 5 loop.prefab",
};
for (int i = 0; i < rings.Length; i++) Measure(rings[i]);
L("");
L("── ⓑ 프롭 후보 (울타리 · 바위 · 기둥) ───────────────────────────");
string[] props = {
"Assets/ResWork/LowPolyFantasyVillage_URP/Prefabs/Props/Fence_01.prefab",
"Assets/ResWork/LowPolyFantasyVillage_URP/Prefabs/Props/Fence_02.prefab",
"Assets/ResWork/LowPolyFantasyVillage_URP/Prefabs/Props/FenceBig_01.prefab",
"Assets/ResWork/LowPolyFantasyVillage_URP/Prefabs/Props/FenceBig_02.prefab",
"Assets/LowPolyFantasyArena/Prefabs/FenceBig_01.prefab",
"Assets/LowPolyFantasyArena/Prefabs/FenceBig_02.prefab",
"Assets/ResWork/Aligned Games/Polygonal Woodlands Nature Asset Package/Models/Fence1/fence_1.prefab",
"Assets/ResWork/Aligned Games/Polygonal Woodlands Nature Asset Package/Models/Fence2/fence_2.prefab",
"Assets/3DPixelArtEnvironment/Prefabs/Stone_1.prefab",
"Assets/3DPixelArtEnvironment/Prefabs/Stone_2.prefab",
"Assets/3DPixelArtEnvironment/Prefabs/Slab.prefab",
"Assets/3DPixelArtEnvironment/Prefabs/Arch_Small.prefab",
"Assets/3DPixelArtEnvironment/Prefabs/Light_Post.prefab",
};
for (int i = 0; i < props.Length; i++) Measure(props[i]);
L("");
L("── LMHPOLY 바위 (자연 번들) ────────────────────────────────────");
var rockGuids = UnityEditor.AssetDatabase.FindAssets("t:Prefab", new string[] { "Assets/LMHPOLY/Low Poly Nature Bundle/Rocks" });
int shown = 0;
for (int i = 0; i < rockGuids.Length && shown < 10; i++)
{
string p = UnityEditor.AssetDatabase.GUIDToAssetPath(rockGuids[i]);
if (p.Contains("/LOD/") || p.Contains("Fireplace") || p.Contains("ParticleSystem")) continue;
if (!p.Contains("Rock")) continue;
Measure(p); shown++;
}
L(" (Rocks 폴더 프리팹 총 " + rockGuids.Length + "개)");
L("");
L("── ⓒ 불꽃 후보 ────────────────────────────────────────────────");
string[] fires = {
"Assets/LMHPOLY/Low Poly Nature Bundle/Rocks/Bonus Assets/Prefabs/ParticleSystem/Fire_Particles.prefab",
"Assets/ResWork/VFX/Hovl Studio/Epic Toon VFX 2/Prefabs/Fire buff/Fire buff.prefab",
"Assets/ResWork/ModularDungeon/Prefabs/SK_Flame.prefab",
"Assets/ResWork/EricWang/GameVFX Collection Vol3/Prefabs/FX_Flameflow_floor.prefab",
"Assets/ResWork/VFX/StylizedFantasyFxPack/Prefab/PS_Cast_Fire.prefab",
"Assets/Res_Addr/Effect/Effect_Hit_Mob_HellfireFlames.prefab",
};
for (int i = 0; i < fires.Length; i++) Measure(fires[i]);
L("");
L("── 불꽃 추가 탐색(이름에 Fire/Flame · 파티클 보유 · 루프) ───────");
var guids = UnityEditor.AssetDatabase.FindAssets("t:Prefab", new string[] { "Assets/ResWork/VFX" });
int hits = 0;
for (int i = 0; i < guids.Length && hits < 14; i++)
{
string p = UnityEditor.AssetDatabase.GUIDToAssetPath(guids[i]);
string n = System.IO.Path.GetFileNameWithoutExtension(p).ToLowerInvariant();
if (!(n.Contains("fire") || n.Contains("flame") || n.Contains("torch") || n.Contains("burn"))) continue;
var go = UnityEditor.AssetDatabase.LoadAssetAtPath<UnityEngine.GameObject>(p);
if (go == null) continue;
var pss = go.GetComponentsInChildren<UnityEngine.ParticleSystem>(true);
if (pss.Length == 0) continue;
bool loop = false;
for (int k = 0; k < pss.Length; k++) if (pss[k].main.loop) loop = true;
if (!loop) continue;
Measure(p); hits++;
}
string outFile = "AgentScripts/WL815c_SURVEY.txt";
System.IO.File.WriteAllText(outFile, s_sb.ToString());
return "survey done → " + outFile;
}
static void Measure(string path)
{
var go = UnityEditor.AssetDatabase.LoadAssetAtPath<UnityEngine.GameObject>(path);
if (go == null) { L(" [없음] " + path); return; }
var inst = UnityEngine.Object.Instantiate(go);
inst.transform.position = UnityEngine.Vector3.zero;
inst.transform.rotation = UnityEngine.Quaternion.identity;
inst.transform.localScale = UnityEngine.Vector3.one;
var rends = inst.GetComponentsInChildren<UnityEngine.Renderer>(true);
bool has = false;
UnityEngine.Bounds b = new UnityEngine.Bounds(UnityEngine.Vector3.zero, UnityEngine.Vector3.zero);
int meshRend = 0, partRend = 0;
long tris = 0;
for (int i = 0; i < rends.Length; i++)
{
var mf = rends[i].GetComponent<UnityEngine.MeshFilter>();
if (mf != null && mf.sharedMesh != null)
{
meshRend++;
tris += mf.sharedMesh.triangles.Length / 3;
if (!has) { b = rends[i].bounds; has = true; } else b.Encapsulate(rends[i].bounds);
}
else if (rends[i] is UnityEngine.ParticleSystemRenderer) partRend++;
else if (!has) { b = rends[i].bounds; has = true; } else b.Encapsulate(rends[i].bounds);
}
var pss = inst.GetComponentsInChildren<UnityEngine.ParticleSystem>(true);
int maxParticles = 0; float rate = 0f, shapeR = 0f, dur = 0f; bool loop = false;
for (int i = 0; i < pss.Length; i++)
{
var m = pss[i].main;
maxParticles += m.maxParticles;
dur = UnityEngine.Mathf.Max(dur, m.duration);
if (m.loop) loop = true;
var e = pss[i].emission;
if (e.enabled) rate += e.rateOverTime.constantMax;
var sh = pss[i].shape;
if (sh.enabled) shapeR = UnityEngine.Mathf.Max(shapeR, sh.radius * pss[i].transform.lossyScale.x);
}
// 실 파티클 수 추정 = maxParticles 상한, 실효 = rate × lifetime
float lifeSum = 0f;
for (int i = 0; i < pss.Length; i++) lifeSum = UnityEngine.Mathf.Max(lifeSum, pss[i].main.startLifetime.constantMax);
int mats = 0;
var seen = new System.Collections.Generic.HashSet<int>();
for (int i = 0; i < rends.Length; i++)
{
var ms = rends[i].sharedMaterials;
for (int k = 0; k < ms.Length; k++) if (ms[k] != null && seen.Add(ms[k].GetInstanceID())) mats++;
}
L(" " + System.IO.Path.GetFileNameWithoutExtension(path));
L(" 경로 " + path);
L(" 렌더러 " + rends.Length + "(메시 " + meshRend + " · 파티클 " + partRend + ") · 머티리얼 " + mats + " · 삼각형 " + tris);
if (has) L(" 바운즈 크기 " + b.size.ToString("F2") + " · 중심 " + b.center.ToString("F2") + " · 최대반경(XZ) " +
(UnityEngine.Mathf.Max(b.extents.x, b.extents.z)).ToString("F2") + " m");
else L(" 바운즈: 없음(메시 0)");
if (pss.Length > 0)
L(" 파티클시스템 " + pss.Length + " · maxParticles 합 " + maxParticles + " · rateOverTime 합 " + rate.ToString("F1") +
"/s · 최대수명 " + lifeSum.ToString("F2") + " s · 정상상태 추정 " + (rate * lifeSum).ToString("F0") +
" · duration " + dur.ToString("F2") + " · loop " + (loop ? "Y" : "N") + " · shape radius " + shapeR.ToString("F2"));
UnityEngine.Object.DestroyImmediate(inst);
}
}

View File

@ -0,0 +1,186 @@
// WL-815c 2차 실측 — 링 실제 시각 반경(Simulate 후 바운즈) · 불꽃 파티클 시스템별 상세 · 바위 프롭
// unity command run_script --file AgentScripts/WL815c_Survey2.cs --entry WL815c_Survey2.Run
public static class WL815c_Survey2
{
static System.Text.StringBuilder s_sb;
static void L(string s) { s_sb.AppendLine(s); UnityEngine.Debug.Log(s); }
public static string Run()
{
s_sb = new System.Text.StringBuilder();
L("WL-815c 2차 실측 · " + System.DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss"));
L("");
L("── 링 프리팹: Simulate 2 s 후 실제 시각 반경(XZ) ──────────────");
string[] rings = {
"Assets/ResWork/VFX/Hovl Studio/Map track markers VFX/Prefabs/Marker 3 Zone Loop.prefab",
"Assets/ResWork/VFX/Hovl Studio/Map track markers VFX/Prefabs/Marker 5 Circle Loop.prefab",
"Assets/ResWork/VFX/Hovl Studio/Map track markers VFX/Prefabs/Marker 7 Danger zone Loop.prefab",
"Assets/ResWork/VFX/Hovl Studio/Map track markers VFX/Prefabs/Marker 8 Safe zone Loop.prefab",
};
for (int i = 0; i < rings.Length; i++) SimRadius(rings[i]);
L("");
L("── 불꽃 후보: 파티클 시스템별 상세 ────────────────────────────");
string[] fires = {
"Assets/LMHPOLY/Low Poly Nature Bundle/Rocks/Bonus Assets/Prefabs/ParticleSystem/Fire_Particles.prefab",
"Assets/ResWork/VFX/Hovl Studio/Epic Toon VFX 2/Prefabs/Fire buff/Fire buff.prefab",
"Assets/ResWork/EricWang/GameVFX Collection Vol3/Prefabs/FX_Flameflow_floor.prefab",
};
for (int i = 0; i < fires.Length; i++) Detail(fires[i]);
L("");
L("── LMHPOLY 바위(비 LOD · Rock 계열) ───────────────────────────");
var guids = UnityEditor.AssetDatabase.FindAssets("t:Prefab", new string[] { "Assets/LMHPOLY/Low Poly Nature Bundle/Rocks" });
int shown = 0;
for (int i = 0; i < guids.Length && shown < 8; i++)
{
string p = UnityEditor.AssetDatabase.GUIDToAssetPath(guids[i]);
string n = System.IO.Path.GetFileNameWithoutExtension(p);
if (p.Contains("/LOD/") || n.EndsWith("_LOD")) continue;
if (!(n.StartsWith("Rock") || n.StartsWith("Stone") || n.StartsWith("Boulder"))) continue;
Brief(p); shown++;
}
if (shown == 0)
{
L(" Rock/Stone/Boulder 접두 프리팹 0 — 폴더 구조 표본:");
for (int i = 0; i < guids.Length && i < 12; i++)
L(" " + UnityEditor.AssetDatabase.GUIDToAssetPath(guids[i]));
}
L("");
L("── 울타리 프롭 pivot·회전 확인(둘레 배치 적합성) ───────────────");
string[] fences = {
"Assets/ResWork/LowPolyFantasyVillage_URP/Prefabs/Props/Fence_01.prefab",
"Assets/ResWork/LowPolyFantasyVillage_URP/Prefabs/Props/FenceBig_01.prefab",
"Assets/3DPixelArtEnvironment/Prefabs/Stone_1.prefab",
};
for (int i = 0; i < fences.Length; i++) Pivot(fences[i]);
string outFile = "AgentScripts/WL815c_SURVEY2.txt";
System.IO.File.WriteAllText(outFile, s_sb.ToString());
return "survey2 done → " + outFile;
}
static void SimRadius(string path)
{
var go = UnityEditor.AssetDatabase.LoadAssetAtPath<UnityEngine.GameObject>(path);
if (go == null) { L(" [없음] " + path); return; }
var inst = UnityEngine.Object.Instantiate(go);
inst.transform.position = UnityEngine.Vector3.zero;
var pss = inst.GetComponentsInChildren<UnityEngine.ParticleSystem>(true);
for (int i = 0; i < pss.Length; i++) { pss[i].Clear(true); pss[i].Simulate(2.0f, true, true, false); }
float maxR = 0f, maxY = 0f, minY = 0f;
int alive = 0;
for (int i = 0; i < pss.Length; i++)
{
int c = pss[i].particleCount;
alive += c;
if (c == 0) continue;
var buf = new UnityEngine.ParticleSystem.Particle[c];
int got = pss[i].GetParticles(buf);
bool world = pss[i].main.simulationSpace == UnityEngine.ParticleSystemSimulationSpace.World;
for (int k = 0; k < got; k++)
{
var p = buf[k].position;
if (!world) p = pss[i].transform.TransformPoint(p);
float r = new UnityEngine.Vector2(p.x, p.z).magnitude;
float sz = buf[k].GetCurrentSize(pss[i]) * 0.5f;
if (r + sz > maxR) maxR = r + sz;
if (p.y > maxY) maxY = p.y;
if (p.y < minY) minY = p.y;
}
}
// 파티클이 0이면 메시/빌보드 크기로 대신 잰다
float rendR = 0f;
var rends = inst.GetComponentsInChildren<UnityEngine.Renderer>(true);
for (int i = 0; i < rends.Length; i++)
{
var b = rends[i].bounds;
rendR = UnityEngine.Mathf.Max(rendR, UnityEngine.Mathf.Max(b.extents.x, b.extents.z));
}
L(" " + System.IO.Path.GetFileNameWithoutExtension(path));
L(" Simulate(2 s) 살아있는 파티클 " + alive + " · 시각 반경(XZ) " + maxR.ToString("F2") +
" m · 높이 " + minY.ToString("F2") + "~" + maxY.ToString("F2") + " m · 렌더러 바운즈 반경 " + rendR.ToString("F2") + " m");
for (int i = 0; i < pss.Length; i++)
{
var m = pss[i].main;
var sh = pss[i].shape;
var e = pss[i].emission;
string bursts = "";
if (e.burstCount > 0)
{
var arr = new UnityEngine.ParticleSystem.Burst[e.burstCount];
e.GetBursts(arr);
float tot = 0f; for (int k = 0; k < arr.Length; k++) tot += arr[k].count.constantMax * UnityEngine.Mathf.Max(1, arr[k].cycleCount);
bursts = " · 버스트 " + e.burstCount + "종 총 " + tot.ToString("F0");
}
L(" ├ " + pss[i].name + " : max " + m.maxParticles + " · rate " + (e.enabled ? e.rateOverTime.constantMax : 0f).ToString("F1") +
"/s" + bursts + " · life " + m.startLifetime.constantMax.ToString("F2") + " s · size " + m.startSize.constantMax.ToString("F2") +
" · shape " + (sh.enabled ? sh.shapeType.ToString() + " r" + sh.radius.ToString("F2") : "off") +
" · scale " + pss[i].transform.lossyScale.ToString("F2") + " · 지금 " + pss[i].particleCount);
}
UnityEngine.Object.DestroyImmediate(inst);
}
static void Detail(string path)
{
var go = UnityEditor.AssetDatabase.LoadAssetAtPath<UnityEngine.GameObject>(path);
if (go == null) { L(" [없음] " + path); return; }
var inst = UnityEngine.Object.Instantiate(go);
var pss = inst.GetComponentsInChildren<UnityEngine.ParticleSystem>(true);
float steady = 0f;
L(" " + System.IO.Path.GetFileNameWithoutExtension(path) + " · PS " + pss.Length);
for (int i = 0; i < pss.Length; i++)
{
var m = pss[i].main; var e = pss[i].emission;
float r = e.enabled ? e.rateOverTime.constantMax : 0f;
float life = m.startLifetime.constantMax;
float st = UnityEngine.Mathf.Min(r * life, m.maxParticles);
steady += st;
L(" ├ " + pss[i].name + " : rate " + r.ToString("F1") + "/s × life " + life.ToString("F2") + " s = " + st.ToString("F0") +
" (max " + m.maxParticles + ") · size " + m.startSize.constantMax.ToString("F2") + " · loop " + (m.loop ? "Y" : "N") +
" · 정지공간 " + m.simulationSpace);
}
L(" ⇒ 인스턴스 1개 정상상태 추정 " + steady.ToString("F0") + " 파티클");
UnityEngine.Object.DestroyImmediate(inst);
}
static void Brief(string path)
{
var go = UnityEditor.AssetDatabase.LoadAssetAtPath<UnityEngine.GameObject>(path);
if (go == null) return;
var inst = UnityEngine.Object.Instantiate(go);
var rends = inst.GetComponentsInChildren<UnityEngine.Renderer>(true);
long tris = 0; bool has = false; UnityEngine.Bounds b = default(UnityEngine.Bounds);
for (int i = 0; i < rends.Length; i++)
{
var mf = rends[i].GetComponent<UnityEngine.MeshFilter>();
if (mf != null && mf.sharedMesh != null) tris += mf.sharedMesh.triangles.Length / 3;
if (!has) { b = rends[i].bounds; has = true; } else b.Encapsulate(rends[i].bounds);
}
L(" " + System.IO.Path.GetFileNameWithoutExtension(path) + " · " + path);
L(" 크기 " + b.size.ToString("F2") + " · tri " + tris + " · 렌더러 " + rends.Length);
UnityEngine.Object.DestroyImmediate(inst);
}
static void Pivot(string path)
{
var go = UnityEditor.AssetDatabase.LoadAssetAtPath<UnityEngine.GameObject>(path);
if (go == null) return;
var inst = UnityEngine.Object.Instantiate(go);
inst.transform.position = UnityEngine.Vector3.zero;
inst.transform.rotation = UnityEngine.Quaternion.identity;
var rends = inst.GetComponentsInChildren<UnityEngine.Renderer>(true);
bool has = false; UnityEngine.Bounds b = default(UnityEngine.Bounds);
for (int i = 0; i < rends.Length; i++) { if (!has) { b = rends[i].bounds; has = true; } else b.Encapsulate(rends[i].bounds); }
L(" " + System.IO.Path.GetFileNameWithoutExtension(path) + " : pivot→바운즈중심 " + b.center.ToString("F2") +
" · 크기 " + b.size.ToString("F2") + " · 긴축 " + (b.size.x >= b.size.z ? "X " + b.size.x.ToString("F2") : "Z " + b.size.z.ToString("F2")) +
" · 바닥 y " + (b.center.y - b.extents.y).ToString("F2"));
UnityEngine.Object.DestroyImmediate(inst);
}
}

View File

@ -0,0 +1,9 @@
링 PSR Marker 3 Zone Loop · shader Shader Graphs/HS_Blend_CG · _TintColor 없음 · _Color RGBA(1.000, 1.000, 1.000, 1.000) · renderMode Mesh · 정렬 0
링 PSR Flash · shader Shader Graphs/HS_Blend_CG · _TintColor 없음 · _Color RGBA(1.000, 1.000, 1.000, 1.000) · renderMode Billboard · 정렬 0
링 PSR Runes · shader Shader Graphs/HS_Blend_CG · _TintColor 없음 · _Color RGBA(1.000, 1.000, 1.000, 1.000) · renderMode Mesh · 정렬 0
링 PSR ShockWave · shader Shader Graphs/HS_Blend_CG · _TintColor 없음 · _Color RGBA(1.000, 1.000, 1.000, 1.000) · renderMode Mesh · 정렬 0
링 PS Marker 3 Zone Loop · startColor RGBA(0.027, 0.056, 0.104, 1.000) · colorOverLifetime ON · size 1.00
링 PS Flash · startColor RGBA(0.288, 0.573, 1.000, 0.125) · colorOverLifetime ON · size 2.00
링 PS Runes · startColor RGBA(0.031, 0.076, 0.142, 0.784) · colorOverLifetime ON · size 2.20
링 PS ShockWave · startColor RGBA(0.047, 0.097, 0.170, 1.000) · colorOverLifetime ON · size 3.00
적용: fireScale 3 · fireSpacing 2.5 · maxTotalParticles 1600 · ringExtraRings 2

View File

@ -0,0 +1,65 @@
// WL-815c 튜닝 — 캡처 1차 실측 뒤 값 조정 + 링 머티리얼 진단
// unity command run_script --file AgentScripts/WL815c_Tune.cs --entry WL815c_Tune.Run
using System.Text;
using UnityEditor;
using UnityEngine;
using WL.Combat.Stage;
public static class WL815c_Tune
{
const string kPath = "Assets/WL/Combat/Stage/Barrier/Resources/WL/WLStageBarrierSettings.asset";
public static string Run()
{
var sb = new StringBuilder();
var so = AssetDatabase.LoadAssetAtPath<WLStageBarrierSettings>(kPath);
if (so == null) return "에셋 없음";
// ── 링 머티리얼 진단(왜 색이 안 먹는가)
if (so.ringPrefab != null)
{
var rs = so.ringPrefab.GetComponentsInChildren<ParticleSystemRenderer>(true);
for (int i = 0; i < rs.Length; i++)
{
var m = rs[i].sharedMaterial;
sb.Append(" 링 PSR ").Append(rs[i].name)
.Append(" · shader ").Append(m != null ? m.shader.name : "null")
.Append(" · _TintColor ").Append(m != null && m.HasProperty("_TintColor") ? m.GetColor("_TintColor").ToString() : "없음")
.Append(" · _Color ").Append(m != null && m.HasProperty("_Color") ? m.GetColor("_Color").ToString() : "없음")
.Append(" · renderMode ").Append(rs[i].renderMode)
.Append(" · 정렬 ").Append(rs[i].sortingOrder).AppendLine();
}
var ps = so.ringPrefab.GetComponentsInChildren<ParticleSystem>(true);
for (int i = 0; i < ps.Length; i++)
sb.Append(" 링 PS ").Append(ps[i].name)
.Append(" · startColor ").Append(ps[i].main.startColor.color.ToString())
.Append(" · colorOverLifetime ").Append(ps[i].colorOverLifetime.enabled ? "ON" : "off")
.Append(" · size ").Append(ps[i].main.startSize.constantMax.ToString("F2")).AppendLine();
}
// ── ⓒ 불꽃: 1차 캡처에서 「흩어진 불티」로 보였다 → 크기·밀도를 올린다(실측 기반)
so.fireScale = 3f; // Fire_Particles 는 모닥불 크기(shape r0.46) → 3배면 약 1.4 m
so.fireSpacing = 2.5f; // 둘레 88 m → 35기
so.maxTotalParticles = 1600; // 35 × 125 = 4375 → 방출률 ×0.37
so.fireGroundSnap = true;
// ── ⓐ 링: 겹으로 두께를 낸다(도트 룩 대비) + 색은 「곱」이라 1 보다 큰 값이 증폭이다
so.ringExtraRings = 2;
so.ringExtraSpacing = 0.5f;
so.ringColor = new Color(6f, 1.2f, 0.4f, 1f); // 원색(어두운 청 기저 0.027,0.056,0.104)에 곱
so.ringTintEnabled = false; // 🔴 실측 3회 — 이 셰이더에서는 화면 색이 안 바뀐다(보고서 「미확인」)
EditorUtility.SetDirty(so);
AssetDatabase.SaveAssets();
AssetDatabase.Refresh();
sb.Append(" 적용: fireScale ").Append(so.fireScale)
.Append(" · fireSpacing ").Append(so.fireSpacing)
.Append(" · maxTotalParticles ").Append(so.maxTotalParticles)
.Append(" · ringExtraRings ").Append(so.ringExtraRings).AppendLine();
System.IO.File.WriteAllText("AgentScripts/WL815c_TUNE.txt", sb.ToString());
return "tune done → AgentScripts/WL815c_TUNE.txt";
}
}

View File

@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 58ad4c9f64381564b887b17b3ca5b314
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: b64e1a9aad572f04a8a23c913bd5dfc7
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: b4d10d4d325b7ac41a2aa48028290759
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 327b74d22ec69414ca28e7b3fd4c8d1a
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,77 @@
%YAML 1.1
%TAG !u! tag:unity3d.com,2011:
--- !u!114 &11400000
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 0}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 3c3dc9bd8de148744b88ce512ce0f09a, type: 3}
m_Name: WLStageBarrierSettings
m_EditorClassIdentifier: Assembly-CSharp::WL.Combat.Stage.WLStageBarrierSettings
enabled: 1
verboseLog: 0
checkSeconds: 0.2
rescanSeconds: 3
blockEnabled: 1
leaveMargin: 2
graceSeconds: 0.5
warpCooldownSeconds: 1
warpInsetMeters: 1.5
warpNavSampleRadius: 4
warpNavSampleRadiusFar: 20
blockPlayer: 1
blockMobs: 1
mobLeaveMargin: 3
mobGraceSeconds: 1
mobWarpCooldownSeconds: 2
warpSnapsCameraLead: 1
warpResetsPath: 1
carveNavMesh: 0
carveSegmentArcMeters: 4
maxCarveSegments: 32
carveThickness: 1.5
carveHeight: 3
carveOverlap: 0.15
carveStaticOnly: 1
style: 1
showVisual: 1
appearSeconds: 0.6
fadeSeconds: 0.6
visualYOffset: 0.05
visualRadiusScale: 1
ringPrefab: {fileID: 1370640339480484, guid: a1b9488b1b1338d4585b154f4b86703e, type: 3}
ringPrefabVisualRadius: 1.1
ringColor: {r: 6, g: 1.2, b: 0.4, a: 1}
ringTintEnabled: 0
ringYScale: 1
ringExtraRings: 2
ringExtraSpacing: 0.5
propPrefabs:
- {fileID: 3287405711304420922, guid: 84026706d57fb9e43b9716c8b1510e98, type: 3}
- {fileID: 3287405711304420922, guid: 84026706d57fb9e43b9716c8b1510e98, type: 3}
- {fileID: 118804, guid: 3e0cf4dec1c9ddb4b9832582972317f6, type: 3}
propSpacing: 2.5
maxProps: 64
propScale: 1
propAlignToTangent: 1
propYawJitter: 6
propRadialJitter: 0.2
propGroundSnap: 1
propRayUp: 50
propRayLength: 200
propStripColliders: 1
propSeed: 815
firePrefabs:
- {fileID: 131402, guid: 5608cd25f71830e4aaa770d88ff9ebe2, type: 3}
fireSpacing: 2.5
maxFires: 48
fireScale: 3
maxTotalParticles: 1600
fireBudgetMode: 0
fireTintEnabled: 0
fireColor: {r: 1, g: 0.45, b: 0.1, a: 1}
fireGroundSnap: 1

View File

@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 25f5d7207dc33a54e8a14b8628b6eae6
NativeFormatImporter:
externalObjects: {}
mainObjectFileID: 11400000
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,482 @@
// ─────────────────────────────────────────────────────────────────────────────
// StageBarrier.cs — 스테이지 「고정 영역(장벽)」 · 정적 API
//
// PD 지시 #815 「스테이지에 들어가면 고정 영역 안에서만 싸운다」
// + PD 질문(2026-09-11) 「구역을 어떻게 막는가 — 울타리 오브젝트 · 불꽃 이펙트 등 대안」
// 발주서 WL-815c · SOT = 기준서 「스테이지_전환_기준서_v1.md」 §D
//
// ■ 공개 계약(815b 의 StageBarrierHook 이 이것을 부른다 — Lead 가 병합 뒤 1줄 연결)
// StageBarrier.Set(center, radius) 스테이지 입장 — 장벽을 세운다
// StageBarrier.Clear() 스테이지 클리어 — 장벽을 연다
// StageBarrier.IsInside(pos) 이 좌표가 장벽 안인가
// StageBarrier.Active 장벽이 서 있는가
// 🔴 이 파일은 StageBarrierHook 을 정의하지 않는다(815b 소유 · 이름 충돌 방지).
//
// ■ 「어떻게 막는가」 = 기준서 §D-2 실측 결론 ③ 거리 판정 + 워프백
// ① 물리 콜라이더 벽은 **불가** — PC 프리팹 Ai01 의 CapsuleCollider 가 IsTrigger 이고
// Rigidbody Constraints = 126(위치·회전 전부 고정)이라 물리로 밀 수 없다.
// 몹도 MobActor.Start 가 isKinematic 을 강제한다.
// ② NavMesh 카브는 되지만(813t2 가 NavMeshModifierVolume 로 실증) 런타임 재베이크 비용이 있어 **옵션**.
// ③ 거리 + 워프백은 **이미 돌고 있는 방식**이다 — BossArena.EvaluateLeash(arenaRadius+margin, 유예)와
// 813w3 RunGuide.WarpToArena, 813t2 TerrainGuard 가 같은 원본 API 를 쓴다.
//
// ■ 되돌리는 방법 = 원본 `Actor.Set_Warp`(Actor.cs:2111) — 813i DeathFlow · 813w3 RunGuide ·
// 813t2 TerrainGuard · 813x MobNavRecovery 와 같은 API. 이펙트·잔상 0.
// PC 는 Get_NavSnapRadius()=0 이라 Set_Warp 이 스스로 스냅하지 않는다
// → **반드시 먼저 NavMesh.SamplePosition 으로 스냅한 지점**을 넘긴다(지형 뚫기 방지 · 813w3 규칙).
// 되돌린 뒤 PC 는 카메라 이동 리드를 0 으로 스냅한다(813t2 방식 · RealCamera 무수정).
//
// ■ 원본 훅 0 — 폴링만 쓴다(813x MobNavRecovery · 813t2 TerrainGuard 선례).
// CombatEvents 를 쓰지 않는 이유: RaiseSpawned 첫 줄이 `if (!Enabled …) return;` 이라
// 코어 스위치가 꺼지면 이벤트가 0 이다(813t2 실측).
//
// ■ 그림(3스타일)은 StageBarrierVisual.cs 가 맡는다. 이 파일은 판정·워프·카브만 다룬다.
//
// ■ 값(C45) 전부 WLStageBarrierSettings. 게임 수치 상수 0.
// 🔴 Debug.LogError/LogException 금지(813z — ErrorLogHookManager 가 플레이어 모달을 띄운다).
// 🔴 어셈블리 주의: Assets/WL/Combat/ 에 .asmdef 를 만들지 말 것(Actor/MobActor 가 Assembly-CSharp 에 있다).
// ─────────────────────────────────────────────────────────────────────────────
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.AI;
namespace WL.Combat.Stage
{
/// <summary>스테이지 고정 영역. 정적 · Set/Clear 로 세우고 연다.</summary>
public static class StageBarrier
{
static WLStageBarrierSettings Cfg { get { return WLStageBarrierSettings.Instance; } }
// ───────────────────────────────────────── 상태
static bool s_active;
static Vector3 s_center;
static float s_radius;
/// <summary>장벽이 서 있는가.</summary>
public static bool Active { get { return s_active; } }
/// <summary>장벽 중심(월드).</summary>
public static Vector3 Center { get { return s_center; } }
/// <summary>장벽 반경(m).</summary>
public static float Radius { get { return s_radius; } }
/// <summary>기능 축이 살아 있는가(에셋 · enabled · RuntimeDisabled).</summary>
public static bool FeatureEnabled { get { return WLStageBarrierSettings.Active; } }
// ───────────────────────────────────────── 진단 카운터(프로브·QA 가 읽는다)
public static int SetCount, ClearCount, Checks, Scans;
public static int PlayerWarps, MobWarps, WarpsFailedNavMesh, WarpsSkippedCooldown, OutsideSeen;
public static int CarveSegments;
public static float LastCarveBuildMs;
public static string LastLog = "";
public static Vector3 LastWarpFrom, LastWarpTo;
// ───────────────────────────────────────── 내부(GC 0 — 틱에서 새로 만들지 않는다)
static MobActor[] s_mobs = new MobActor[0];
static readonly Dictionary<int, float> s_outsideSince = new Dictionary<int, float>();
static readonly Dictionary<int, float> s_lastWarp = new Dictionary<int, float>();
static readonly List<GameObject> s_carve = new List<GameObject>();
static Transform s_carveRoot;
static float s_nextRescan, s_nextCheck;
static bool s_faulted;
// ═════════════════════════════════════════════════════════════════════
// 공개 API
// ═════════════════════════════════════════════════════════════════════
/// <summary>스테이지 입장 — 중심·반경으로 장벽을 세운다. 같은 값으로 다시 불러도 안전(멱등).</summary>
public static void Set(Vector3 center, float radius)
{
var cfg = Cfg;
if (cfg == null || !cfg.enabled || WLStageBarrierSettings.RuntimeDisabled) return; // C8 — 오브젝트 0
if (radius <= 0f) { Clear(); return; }
bool moved = !s_active || (s_center - center).sqrMagnitude > 0.0001f || !Mathf.Approximately(s_radius, radius);
s_center = center;
s_radius = radius;
s_active = true;
SetCount++;
s_outsideSince.Clear();
s_lastWarp.Clear();
s_faulted = false;
s_nextRescan = s_nextCheck = 0f;
StageBarrierBoot.EnsureRunner();
if (moved) { RebuildCarve(cfg); }
StageBarrierVisual.Show(cfg, center, radius);
Log("세움 중심 " + center.ToString("F1") + " 반경 " + radius.ToString("F1") + " m · 스타일 " + StageBarrierVisual.CurrentStyle +
(cfg.carveNavMesh ? " · 카브 " + CarveSegments + "세그(" + LastCarveBuildMs.ToString("F1") + " ms)" : ""));
}
/// <summary>스테이지 클리어 — 장벽을 연다. 그림은 fadeSeconds 동안 사라진 뒤 풀에 반납된다.</summary>
public static void Clear()
{
if (!s_active && s_carve.Count == 0 && !StageBarrierVisual.HasActive) return;
s_active = false;
ClearCount++;
s_outsideSince.Clear();
s_lastWarp.Clear();
ClearCarve();
StageBarrierVisual.Hide(Cfg);
Log("열림(클리어)");
}
/// <summary>이 좌표가 장벽 안인가. 장벽이 없으면 항상 true(제약 없음).</summary>
public static bool IsInside(Vector3 pos)
{
if (!s_active) return true;
return HorizontalSqr(pos, s_center) <= s_radius * s_radius;
}
/// <summary>여유를 더해서 판정한다(음수면 안쪽으로 좁힌다).</summary>
public static bool IsInside(Vector3 pos, float extraMargin)
{
if (!s_active) return true;
float r = Mathf.Max(0f, s_radius + extraMargin);
return HorizontalSqr(pos, s_center) <= r * r;
}
/// <summary>중심에서의 수평 거리(m). 장벽이 없으면 1.</summary>
public static float DistanceFromCenter(Vector3 pos)
{
if (!s_active) return -1f;
return Mathf.Sqrt(HorizontalSqr(pos, s_center));
}
/// <summary>스타일만 바꾼다(PD 비교용 · 장벽은 그대로). 판정에는 영향이 없다.</summary>
public static void SetStyle(StageBarrierStyle style)
{
var cfg = Cfg;
if (cfg == null) return;
StageBarrierVisual.StyleOverride = style;
if (s_active) StageBarrierVisual.Show(cfg, s_center, s_radius);
}
// ═════════════════════════════════════════════════════════════════════
// 판정 · 워프백
// ═════════════════════════════════════════════════════════════════════
internal static void Tick()
{
StageBarrierVisual.Tick(Cfg);
if (s_faulted || !s_active) return;
var cfg = Cfg;
if (cfg == null || !cfg.enabled || WLStageBarrierSettings.RuntimeDisabled || !cfg.blockEnabled) return;
float now = Time.unscaledTime;
if (now >= s_nextRescan)
{
s_nextRescan = now + Mathf.Max(0.1f, cfg.rescanSeconds);
Rescan();
}
if (now < s_nextCheck) return;
s_nextCheck = now + Mathf.Max(0.02f, cfg.checkSeconds);
// 🔴 Debug.LogException 을 쓰지 않는다 — ErrorLogHookManager 가 그것을 플레이어 모달로 띄운다(813z).
try { CheckAll(cfg, now); }
catch (System.Exception ex) { s_faulted = true; Log("중단(예외) — " + ex.Message); }
}
static void Rescan()
{
// FindObjectsInactive.Exclude — 비활성 몹은 풀에 들어간 사체다(Actor.cs:633 에서 에이전트를 끈다).
s_mobs = Object.FindObjectsByType<MobActor>(FindObjectsInactive.Exclude, FindObjectsSortMode.None);
Scans++;
if (s_outsideSince.Count > 128) s_outsideSince.Clear(); // 풀 재사용으로 쌓인 죽은 키 정리
if (s_lastWarp.Count > 128) s_lastWarp.Clear();
}
static void CheckAll(WLStageBarrierSettings cfg, float now)
{
if (cfg.blockPlayer)
{
var pc = MyValue.MyPC;
if (!DSUtil.CheckNull(pc)) CheckOne(cfg, pc, now, true);
}
if (!cfg.blockMobs) return;
var mobs = s_mobs;
for (int i = 0; i < mobs.Length; i++)
{
var m = mobs[i];
if (DSUtil.CheckNull(m)) continue;
CheckOne(cfg, m, now, false);
}
}
static void CheckOne(WLStageBarrierSettings cfg, Actor actor, float now, bool isPlayer)
{
int key = actor.GetInstanceID();
if (actor.IsDead()) { s_outsideSince.Remove(key); return; }
Checks++;
Vector3 pos = actor.Get_position();
float margin = isPlayer ? cfg.leaveMargin : cfg.mobLeaveMargin;
float limit = s_radius + Mathf.Max(0f, margin);
if (HorizontalSqr(pos, s_center) <= limit * limit) { s_outsideSince.Remove(key); return; }
float since;
if (!s_outsideSince.TryGetValue(key, out since))
{
s_outsideSince[key] = now; // 넉백·대시일 수 있다 — 유예를 준다
OutsideSeen++;
return;
}
float grace = isPlayer ? cfg.graceSeconds : cfg.mobGraceSeconds;
if (now - since < Mathf.Max(0f, grace)) return;
float last;
float cool = isPlayer ? cfg.warpCooldownSeconds : cfg.mobWarpCooldownSeconds;
if (s_lastWarp.TryGetValue(key, out last) && now - last < Mathf.Max(0f, cool))
{ WarpsSkippedCooldown++; return; }
WarpBack(cfg, actor, key, pos, now, isPlayer);
}
/// <summary>
/// 되돌릴 지점 = 「가장 가까운 안쪽 지점」 — 중심에서 대상 방향으로 (반경 들여놓기) 만큼.
/// **좌표만 다루는 순수 함수**라 프로브가 실 액터 없이 같은 코드를 검증할 수 있다(GC 0).
/// </summary>
public static Vector3 NearestInsidePoint(WLStageBarrierSettings cfg, Vector3 center, float radius, Vector3 from)
{
Vector3 d = from - center; d.y = 0f;
float len = d.magnitude;
float target = Mathf.Max(0f, radius - Mathf.Max(0f, cfg.warpInsetMeters));
if (len < 0.001f) return new Vector3(center.x, from.y, center.z); // 정중앙 = 이미 안
Vector3 p = center + d * (target / len);
p.y = from.y; // 높이는 NavMesh 스냅이 정한다
return p;
}
/// <summary>되돌릴 지점을 NavMesh 위로 스냅한다. 못 찾으면 false = 워프하지 않는다(지형 뚫기 방지).</summary>
public static bool ResolveWarpTarget(WLStageBarrierSettings cfg, Vector3 center, float radius, Vector3 from, out Vector3 target)
{
Vector3 want = NearestInsidePoint(cfg, center, radius, from);
NavMeshHit hit;
if (NavMesh.SamplePosition(want, out hit, Mathf.Max(0.1f, cfg.warpNavSampleRadius), NavMesh.AllAreas)
|| NavMesh.SamplePosition(want, out hit, Mathf.Max(0.1f, cfg.warpNavSampleRadiusFar), NavMesh.AllAreas)
|| NavMesh.SamplePosition(center, out hit, Mathf.Max(0.1f, cfg.warpNavSampleRadiusFar), NavMesh.AllAreas))
{ target = hit.position; return true; }
target = want;
return false;
}
static void WarpBack(WLStageBarrierSettings cfg, Actor actor, int key, Vector3 from, float now, bool isPlayer)
{
s_outsideSince.Remove(key); // 성공이든 실패든 창을 다시 연다(실패 시 다음 유예 뒤 재시도)
Vector3 target;
if (!ResolveWarpTarget(cfg, s_center, s_radius, from, out target))
{
WarpsFailedNavMesh++;
Log("복귀 실패 — 안쪽 지점이 NavMesh 밖 " + from.ToString("F1"));
return;
}
if (cfg.warpResetsPath)
{
var agent = actor.Get_Agent();
if (agent != null && agent.enabled && agent.isOnNavMesh) agent.ResetPath();
}
actor.Set_Warp(target); // 원본 API · 이펙트 0 · 이미 스냅된 지점을 받는다
if (isPlayer && cfg.warpSnapsCameraLead) SnapCameraLead();
s_lastWarp[key] = now;
if (isPlayer) PlayerWarps++; else MobWarps++;
LastWarpFrom = from; LastWarpTo = target;
Log("복귀 " + (isPlayer ? "PC" : "몹 " + actor.name) + " " + from.ToString("F1") + " → " + target.ToString("F1") +
" (중심 " + Mathf.Sqrt(HorizontalSqr(from, s_center)).ToString("F1") + " m → " +
Mathf.Sqrt(HorizontalSqr(target, s_center)).ToString("F1") + " m)");
}
/// <summary>되돌린 직후 카메라 이동 리드를 0 으로 스냅한다(811 CameraMoveLead.Snap · 813t2 방식 · RealCamera 무수정).</summary>
static void SnapCameraLead()
{
var cam = MyValue.m_RealCamera;
if (DSUtil.CheckNull(cam)) return;
var lead = cam.Get_MoveLead();
if (lead != null) lead.Snap();
}
// ═════════════════════════════════════════════════════════════════════
// NavMesh 카브(옵션)
// ═════════════════════════════════════════════════════════════════════
/// <summary>둘레 길이·세그먼트 길이로 세그먼트 수를 낸다(상한 적용). 순수 함수 — 프로브가 그대로 검증한다.</summary>
public static int CarveSegmentCount(WLStageBarrierSettings cfg, float radius)
{
if (cfg == null || radius <= 0f) return 0;
float circumference = 2f * Mathf.PI * radius;
int n = Mathf.CeilToInt(circumference / Mathf.Max(0.5f, cfg.carveSegmentArcMeters));
return Mathf.Clamp(n, 3, Mathf.Max(3, cfg.maxCarveSegments));
}
static void RebuildCarve(WLStageBarrierSettings cfg)
{
ClearCarve();
if (!cfg.carveNavMesh) { CarveSegments = 0; LastCarveBuildMs = 0f; return; }
var sw = System.Diagnostics.Stopwatch.StartNew();
int n = CarveSegmentCount(cfg, s_radius);
EnsureCarveRoot();
float arc = 2f * Mathf.PI * s_radius / n;
float width = arc * (1f + Mathf.Max(0f, cfg.carveOverlap));
for (int i = 0; i < n; i++)
{
float a = (i / (float)n) * Mathf.PI * 2f;
Vector3 dir = new Vector3(Mathf.Cos(a), 0f, Mathf.Sin(a));
Vector3 p = s_center + dir * s_radius;
var go = new GameObject("carve_" + i);
go.hideFlags = HideFlags.HideAndDontSave;
go.transform.SetParent(s_carveRoot, false);
go.transform.position = p;
go.transform.rotation = Quaternion.LookRotation(dir, Vector3.up); // +Z = 반경 방향
var ob = go.AddComponent<NavMeshObstacle>();
ob.shape = NavMeshObstacleShape.Box;
ob.size = new Vector3(width, Mathf.Max(0.5f, cfg.carveHeight), Mathf.Max(0.2f, cfg.carveThickness));
ob.center = new Vector3(0f, Mathf.Max(0.5f, cfg.carveHeight) * 0.5f, 0f);
ob.carving = true;
ob.carveOnlyStationary = cfg.carveStaticOnly;
s_carve.Add(go);
}
sw.Stop();
CarveSegments = n;
LastCarveBuildMs = (float)sw.Elapsed.TotalMilliseconds;
}
static void EnsureCarveRoot()
{
if (s_carveRoot != null) return;
var go = new GameObject("[WL815c] StageBarrierCarve");
go.hideFlags = HideFlags.HideAndDontSave;
s_carveRoot = go.transform;
}
static void ClearCarve()
{
for (int i = 0; i < s_carve.Count; i++)
{
var g = s_carve[i];
if (g == null) continue;
if (Application.isPlaying) Object.Destroy(g); else Object.DestroyImmediate(g);
}
s_carve.Clear();
CarveSegments = 0;
}
// ═════════════════════════════════════════════════════════════════════
// 유틸 · 진단
// ═════════════════════════════════════════════════════════════════════
static float HorizontalSqr(Vector3 a, Vector3 b)
{
float dx = a.x - b.x, dz = a.z - b.z;
return dx * dx + dz * dz;
}
static void Log(string msg)
{
LastLog = msg;
var c = Cfg;
if (c != null && c.verboseLog) Debug.Log("[StageBarrier] " + msg);
}
/// <summary>프로브용: 카운터·유예 창 초기화(장벽 자체는 건드리지 않는다).</summary>
public static void ResetDiagnostics()
{
SetCount = ClearCount = Checks = Scans = 0;
PlayerWarps = MobWarps = WarpsFailedNavMesh = WarpsSkippedCooldown = OutsideSeen = 0;
LastLog = ""; LastWarpFrom = LastWarpTo = Vector3.zero;
s_outsideSince.Clear(); s_lastWarp.Clear(); s_mobs = new MobActor[0];
s_nextRescan = s_nextCheck = 0f; s_faulted = false;
}
/// <summary>프로브용: 전부(장벽·그림·카브·풀) 원상 복구.</summary>
public static void HardResetForProbe()
{
s_active = false;
ClearCarve();
StageBarrierVisual.HardResetForProbe();
ResetDiagnostics();
if (s_carveRoot != null)
{
if (Application.isPlaying) Object.Destroy(s_carveRoot.gameObject); else Object.DestroyImmediate(s_carveRoot.gameObject);
s_carveRoot = null;
}
SetCount = ClearCount = 0;
}
/// <summary>프로브용: 러너 없이 1회 판정(에디트 모드 · 타이머 초기화).</summary>
public static void ForceTick() { s_nextRescan = s_nextCheck = 0f; Tick(); }
/// <summary>프로브용: 러너가 부르는 것과 똑같은 틱(타이머 초기화 0 — GC 측정용).</summary>
public static void ProbeTick() { Tick(); }
/// <summary>프로브용: 임의 액터 1명을 지금 판정한다(유예·쿨 무시하고 즉시 복귀까지). 되돌렸으면 true.</summary>
public static bool ForceCheck(Actor actor, bool asPlayer)
{
var cfg = Cfg;
if (cfg == null || actor == null || !s_active) return false;
Vector3 pos = actor.Get_position();
float margin = asPlayer ? cfg.leaveMargin : cfg.mobLeaveMargin;
float limit = s_radius + Mathf.Max(0f, margin);
if (HorizontalSqr(pos, s_center) <= limit * limit) return false;
WarpBack(cfg, actor, actor.GetInstanceID(), pos, Time.unscaledTime, asPlayer);
return true;
}
/// <summary>프로브용: 카브 오브젝트 수(실제 씬에 있는 것).</summary>
public static int CarveObjectCount { get { return s_carve.Count; } }
/// <summary>프로브용: i 번째 카브 세그먼트. 카브 오브젝트는 HideAndDontSave 라 FindObjectsByType 에 안 잡힌다.</summary>
public static NavMeshObstacle CarveObstacleAt(int i)
{
if (i < 0 || i >= s_carve.Count || s_carve[i] == null) return null;
return s_carve[i].GetComponent<NavMeshObstacle>();
}
}
// ─────────────────────────────────────────────────────────────────────────
/// <summary>StageBarrier 의 시간 축. 숨김 GameObject 1개 · 코루틴 0.</summary>
internal sealed class StageBarrierRunner : MonoBehaviour
{
void Update() { StageBarrier.Tick(); }
}
/// <summary>러너 부팅(C8 — SO 가 꺼져 있으면 오브젝트 0). Set() 이 처음 불릴 때만 뜬다.</summary>
public static class StageBarrierBoot
{
static StageBarrierRunner s_runner;
/// <summary>러너가 떠 있는가(프로브).</summary>
public static bool RunnerAlive { get { return !DSUtil.CheckNull(s_runner); } }
internal static void EnsureRunner()
{
if (!Application.isPlaying || !DSUtil.CheckNull(s_runner)) return;
if (!WLStageBarrierSettings.Active) return;
var go = new GameObject("[WL815c] StageBarrierRunner");
go.hideFlags = HideFlags.HideAndDontSave; // 씬 전환에도 살아남고 씬·프리팹에 저장되지 않는다
s_runner = go.AddComponent<StageBarrierRunner>();
}
/// <summary>프로브용: 러너를 회수한다.</summary>
public static void KillRunnerForProbe()
{
if (DSUtil.CheckNull(s_runner)) { s_runner = null; return; }
if (Application.isPlaying) Object.Destroy(s_runner.gameObject); else Object.DestroyImmediate(s_runner.gameObject);
s_runner = null;
}
}
}

View File

@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 60022f941d7bf9549bd96048f7478aa9

View File

@ -0,0 +1,633 @@
// ─────────────────────────────────────────────────────────────────────────────
// StageBarrierVisual.cs — 「보이는 장벽」 3스타일 (PD 대안 비교 · 값 하나로 전환)
//
// PD 질문(2026-09-11) 「스테이지 지역을 어떻게 구역으로 막을 것인지 대안
// (울타리 오브젝트 배치 · 불꽃 이펙트 장벽 등)」
// → 대안을 **코드로 만들어** PD 가 WLStageBarrierSettings.style 값 하나만 바꿔 보게 한다.
//
// ⓐ MagicRing — Hovl 「Marker 3 Zone Loop」 1개를 반경에 맞게 스케일(+겹). 가장 싸다.
// ⓑ Props — 울타리·바위 프리팹을 둘레에 propSpacing 간격으로 배치. 가장 「막혔다」고 읽힌다.
// ⓒ Fire — 불꽃 파티클을 둘레에 fireSpacing 간격으로 배치(총량 상한). 가장 비싸다.
//
// ■ 실측 근거(WL815c_SURVEY / SURVEY2 · 이 worktree 헤드리스 에디터)
// Marker 3 Zone Loop : 파티클시스템 4 · 머티리얼 1 · **시각 반경 1.10 m(스케일 1)** · 버스트 1/루프 → 살아있는 파티클 3~4
// Fence_01 : 메시 1 · 머티리얼 1 · **삼각형 120** · 크기 2.38 × 1.02 × 0.27 m · pivot 지면·중앙
// Fire_Particles : 파티클시스템 3(FireRed 20/s×1.5 s · FireYellow 15/s×1.0 s · Smoke 10/s×8 s)
// → **인스턴스 1개 정상상태 125 파티클**
//
// ■ 원본 무수정
// · 프리팹은 `Object.Instantiate` 로 **끊어진 클론**을 만든다(PrefabUtility 를 쓰지 않는다) → 원본 프리팹 무수정.
// · 색은 인스턴스의 `ParticleSystem.main.startColor` 에만 대입한다 → **머티리얼·셰이더 무수정**.
// · 프롭은 transform 과 (옵션) Collider 제거만 — 머티리얼을 건드리지 않는다.
//
// ■ 811s EffectScaleGuard / 811q 예산과 충돌 0
// EffectScaleGuard 는 **원본 이펙트 풀(`InGameInfo.tf_Effects` 의 직속 자식)** 만 훑는다.
// 장벽 그림은 전부 `[WL815c] StageBarrierVisual` 루트 밑에 있으므로 그 스윕 대상이 아니다.
// 불꽃은 그것과 별개로 `maxTotalParticles` 로 자체 상한을 건다.
//
// ■ 연출 · 풀링
// 입장 = appearSeconds 동안 0 → 1 (링은 반경이 퍼지고 · 프롭은 지면에서 솟고 · 불꽃은 커진다)
// 클리어 = fadeSeconds 동안 1 → 0 뒤 **SetActive(false) 로 풀에 반납**(Destroy 0 · 다음 Set 은 재사용).
// 스타일이 바뀌면 이전 스타일의 인스턴스는 즉시 풀에 반납된다(스타일마다 별도 풀).
//
// 🔴 Debug.LogError/LogException 금지(813z).
// ─────────────────────────────────────────────────────────────────────────────
using System.Collections.Generic;
using UnityEngine;
namespace WL.Combat.Stage
{
/// <summary>장벽 그림 3스타일. 정적 · 풀링 · 원본 프리팹 무수정.</summary>
public static class StageBarrierVisual
{
// ───────────────────────────────────────── 상태
static Transform s_root;
static StageBarrierStyle s_current = StageBarrierStyle.None;
static Vector3 s_center;
static float s_radius;
static float s_phaseStart; // 연출 시작 시각
static float s_phaseLen; // 연출 길이(초)
static bool s_showing; // true = 나타나는 중/유지 · false = 사라지는 중/꺼짐
static float s_t = 0f; // 0..1 현재 연출 진행도
/// <summary>PD·프로브가 SO 를 안 고치고 스타일만 바꿀 때(1 = 무시).</summary>
public static StageBarrierStyle StyleOverride = (StageBarrierStyle)(-1);
/// <summary>지금 그려지고 있는 스타일.</summary>
public static StageBarrierStyle CurrentStyle { get { return s_current; } }
/// <summary>살아 있는(꺼지지 않은) 그림이 있는가.</summary>
public static bool HasActive { get { return s_showing || s_t > 0f; } }
// ───────────────────────────────────────── 진단(프로브가 읽는다)
/// <summary>Instantiate 호출 누계. 두 번째 Set 부터 늘지 않으면 풀링이 도는 것이다.</summary>
public static int Instantiates;
/// <summary>지금 활성인 인스턴스 수 · 풀에 반납된 수.</summary>
public static int ActiveInstances, PooledInstances;
/// <summary>스타일 전환 횟수 · Show/Hide 호출 수.</summary>
public static int StyleSwitches, Shows, Hides;
/// <summary>ⓑ 실제로 놓인 프롭 수 · 상한에 걸려 벌어진 간격(m).</summary>
public static int PropCount; public static float PropSpacingUsed;
/// <summary>ⓒ 실제로 놓인 불꽃 수 · 적용된 방출률 배수 · 추정 총 파티클.</summary>
public static int FireCount; public static float FireRateMul; public static int FireParticleEstimate;
/// <summary>ⓒ 프리팹 1개당 정상상태 파티클 추정(실측 계산).</summary>
public static int FirePerInstanceEstimate;
/// <summary>ⓐ 링에 적용된 스케일.</summary>
public static float RingScale;
/// <summary>마지막 Show 가 든 시간(ms).</summary>
public static float LastBuildMs;
// ───────────────────────────────────────── 풀(스타일별 · Destroy 0)
sealed class Slot
{
public GameObject go;
public Transform tf;
public Vector3 basePos;
public Quaternion baseRot;
public Vector3 baseScale;
public ParticleSystem[] ps;
/// <summary>프리팹 원본 색·상한·방출률(인스턴스 생성 때 1회 채집 — 풀 재사용 때 여기서 되돌린다).</summary>
public Color[] psOrig;
public int[] psMaxOrig;
public ParticleSystem.MinMaxCurve[] psRateOrig;
/// <summary>이번 회차에 쓸 색(연출 알파가 여기에 곱해진다).</summary>
public Color[] psColor;
}
static readonly List<Slot> s_ring = new List<Slot>();
static readonly List<Slot> s_props = new List<Slot>();
static readonly List<Slot> s_fires = new List<Slot>();
static readonly List<Slot> s_live = new List<Slot>(); // 지금 쓰는 슬롯(연출 대상)
// ═════════════════════════════════════════════════════════════════════
// 공개 — Show / Hide / Tick
// ═════════════════════════════════════════════════════════════════════
/// <summary>장벽 그림을 세운다(입장 연출 시작).</summary>
public static void Show(WLStageBarrierSettings cfg, Vector3 center, float radius)
{
if (cfg == null) return;
Shows++;
StageBarrierStyle want = StyleOverride >= 0 ? StyleOverride : cfg.style;
if (!cfg.showVisual) want = StageBarrierStyle.None;
s_center = center;
s_radius = Mathf.Max(0.1f, radius) * Mathf.Max(0.05f, cfg.visualRadiusScale);
var sw = System.Diagnostics.Stopwatch.StartNew();
if (want != s_current) { ReturnAll(); StyleSwitches++; }
s_current = want;
switch (want)
{
case StageBarrierStyle.MagicRing: BuildRing(cfg); break;
case StageBarrierStyle.Props: BuildProps(cfg); break;
case StageBarrierStyle.Fire: BuildFire(cfg); break;
default: ReturnAll(); break;
}
sw.Stop();
LastBuildMs = (float)sw.Elapsed.TotalMilliseconds;
s_showing = true;
s_phaseStart = Time.unscaledTime;
s_phaseLen = Mathf.Max(0.01f, cfg.appearSeconds);
ApplyPhase(0f);
}
/// <summary>장벽 그림을 연다(클리어 연출 시작 → fadeSeconds 뒤 풀 반납).</summary>
public static void Hide(WLStageBarrierSettings cfg)
{
if (!s_showing && s_t <= 0f) return;
Hides++;
s_showing = false;
s_phaseStart = Time.unscaledTime;
s_phaseLen = Mathf.Max(0.01f, cfg != null ? cfg.fadeSeconds : 0.6f);
}
/// <summary>연출 진행. StageBarrier.Tick 이 부른다.</summary>
internal static void Tick(WLStageBarrierSettings cfg)
{
if (s_live.Count == 0 && s_t <= 0f) return;
float len = Mathf.Max(0.01f, s_phaseLen);
float k = Mathf.Clamp01((Time.unscaledTime - s_phaseStart) / len);
float t = s_showing ? k : 1f - k;
if (Mathf.Approximately(t, s_t) && (t <= 0f || t >= 1f)) { if (t <= 0f && !s_showing) ReturnAll(); return; }
ApplyPhase(t);
if (!s_showing && t <= 0f) ReturnAll();
}
// ═════════════════════════════════════════════════════════════════════
// ⓐ 마법진 링 — Hovl Marker 를 반경에 맞게 스케일
// ═════════════════════════════════════════════════════════════════════
static void BuildRing(WLStageBarrierSettings cfg)
{
ReturnAll();
if (cfg.ringPrefab == null) { RingScale = 0f; return; }
int layers = 1 + Mathf.Max(0, cfg.ringExtraRings);
float baseR = Mathf.Max(0.01f, cfg.ringPrefabVisualRadius);
for (int i = 0; i < layers; i++)
{
float r = s_radius + (i - (layers - 1) * 0.5f) * Mathf.Max(0f, cfg.ringExtraSpacing);
float scale = r / baseR;
if (i == 0) RingScale = scale;
var s = Take(s_ring, cfg.ringPrefab);
if (s == null) continue;
s.basePos = new Vector3(s_center.x, s_center.y + cfg.visualYOffset + i * 0.01f, s_center.z);
s.baseRot = Quaternion.identity;
s.baseScale = new Vector3(scale, scale * Mathf.Max(0.01f, cfg.ringYScale), scale);
s.tf.SetPositionAndRotation(s.basePos, s.baseRot);
if (cfg.ringTintEnabled) Tint(s, cfg.ringColor);
Restart(s);
s_live.Add(s);
}
RefreshCounts();
}
// ═════════════════════════════════════════════════════════════════════
// ⓑ 프롭 링 — 울타리 · 바위를 둘레에 배치(풀링 · 상한 · 지면 스냅)
// ═════════════════════════════════════════════════════════════════════
static void BuildProps(WLStageBarrierSettings cfg)
{
ReturnAll();
PropCount = 0; PropSpacingUsed = 0f;
if (cfg.propPrefabs == null || cfg.propPrefabs.Length == 0) return;
float circumference = 2f * Mathf.PI * s_radius;
int n = Mathf.Max(3, Mathf.RoundToInt(circumference / Mathf.Max(0.2f, cfg.propSpacing)));
if (n > cfg.maxProps) n = Mathf.Max(3, cfg.maxProps); // 🔴 상한 — 넘으면 간격이 벌어진다
PropSpacingUsed = circumference / n;
var rnd = new System.Random(cfg.propSeed);
int variants = cfg.propPrefabs.Length;
for (int i = 0; i < n; i++)
{
var prefab = cfg.propPrefabs[i % variants];
if (prefab == null) continue;
float a = (i / (float)n) * Mathf.PI * 2f;
Vector3 dir = new Vector3(Mathf.Cos(a), 0f, Mathf.Sin(a));
float radial = s_radius + ((float)rnd.NextDouble() * 2f - 1f) * Mathf.Max(0f, cfg.propRadialJitter);
Vector3 p = s_center + dir * radial;
if (cfg.propGroundSnap)
{
RaycastHit hit;
if (Physics.Raycast(p + Vector3.up * Mathf.Max(1f, cfg.propRayUp), Vector3.down, out hit,
Mathf.Max(2f, cfg.propRayLength)))
p.y = hit.point.y;
else p.y = s_center.y;
}
else p.y = s_center.y;
p.y += cfg.visualYOffset;
// 접선 정렬: 프롭의 긴축(+X · Fence_01 실측 2.38 m)이 둘레의 접선을 따라가게 한다.
// LookRotation(접선) 은 +Z 를 접선에 맞추므로 +Y 90° 를 더해 +X 를 접선에 놓는다.
Quaternion rot = Quaternion.identity;
if (cfg.propAlignToTangent)
{
Vector3 tangent = new Vector3(-dir.z, 0f, dir.x);
rot = Quaternion.LookRotation(tangent, Vector3.up) * Quaternion.Euler(0f, 90f, 0f);
}
if (cfg.propYawJitter > 0f)
rot = rot * Quaternion.Euler(0f, ((float)rnd.NextDouble() * 2f - 1f) * cfg.propYawJitter, 0f);
var s = Take(s_props, prefab);
if (s == null) continue;
s.basePos = p;
s.baseRot = rot;
s.baseScale = Vector3.one * Mathf.Max(0.01f, cfg.propScale);
s.tf.SetPositionAndRotation(p, rot);
if (cfg.propStripColliders) StripColliders(s.go);
s_live.Add(s);
PropCount++;
}
RefreshCounts();
}
// ═════════════════════════════════════════════════════════════════════
// ⓒ 불꽃 링 — 파티클 총량 상한을 지키며 둘레에 배치
// ═════════════════════════════════════════════════════════════════════
static void BuildFire(WLStageBarrierSettings cfg)
{
ReturnAll();
FireCount = 0; FireRateMul = 1f; FireParticleEstimate = 0; FirePerInstanceEstimate = 0;
if (cfg.firePrefabs == null || cfg.firePrefabs.Length == 0) return;
float circumference = 2f * Mathf.PI * s_radius;
int n = Mathf.Max(3, Mathf.RoundToInt(circumference / Mathf.Max(0.2f, cfg.fireSpacing)));
if (n > cfg.maxFires) n = Mathf.Max(3, cfg.maxFires);
// 🔴 프리팹 1개당 정상상태 파티클 추정 = Σ min(rate × life, maxParticles) — 프리팹에서 읽는다(코드 상수 0).
FirePerInstanceEstimate = EstimateSteadyState(cfg.firePrefabs[0]);
// 총량 상한 적용
float mul = 1f;
if (FirePerInstanceEstimate > 0 && cfg.maxTotalParticles > 0)
{
int would = FirePerInstanceEstimate * n;
if (would > cfg.maxTotalParticles)
{
if (cfg.fireBudgetMode == FireBudgetMode.ReduceCount)
n = Mathf.Max(3, cfg.maxTotalParticles / Mathf.Max(1, FirePerInstanceEstimate));
else
mul = Mathf.Clamp01(cfg.maxTotalParticles / (float)would);
}
}
FireRateMul = mul;
int variants = cfg.firePrefabs.Length;
for (int i = 0; i < n; i++)
{
var prefab = cfg.firePrefabs[i % variants];
if (prefab == null) continue;
float a = (i / (float)n) * Mathf.PI * 2f;
Vector3 dir = new Vector3(Mathf.Cos(a), 0f, Mathf.Sin(a));
Vector3 p = s_center + dir * s_radius;
if (cfg.fireGroundSnap)
{
RaycastHit hit;
if (Physics.Raycast(p + Vector3.up * Mathf.Max(1f, cfg.propRayUp), Vector3.down, out hit,
Mathf.Max(2f, cfg.propRayLength)))
p.y = hit.point.y;
else p.y = s_center.y;
}
else p.y = s_center.y;
p.y += cfg.visualYOffset;
var s = Take(s_fires, prefab);
if (s == null) continue;
s.basePos = p;
s.baseRot = Quaternion.LookRotation(dir, Vector3.up);
s.baseScale = Vector3.one * Mathf.Max(0.01f, cfg.fireScale);
s.tf.SetPositionAndRotation(p, s.baseRot);
ApplyFireBudget(s, mul, cfg);
if (cfg.fireTintEnabled) Tint(s, cfg.fireColor);
Restart(s);
s_live.Add(s);
FireCount++;
}
FireParticleEstimate = Mathf.RoundToInt(FirePerInstanceEstimate * FireCount * mul);
RefreshCounts();
}
/// <summary>프리팹 1개의 정상상태 파티클 추정 = Σ min(rateOverTime × startLifetime, maxParticles). 순수 계산.</summary>
public static int EstimateSteadyState(GameObject prefab)
{
if (prefab == null) return 0;
var pss = prefab.GetComponentsInChildren<ParticleSystem>(true);
float sum = 0f;
for (int i = 0; i < pss.Length; i++)
{
var m = pss[i].main;
var e = pss[i].emission;
float rate = e.enabled ? e.rateOverTime.constantMax : 0f;
float life = m.startLifetime.constantMax;
sum += Mathf.Min(rate * life, m.maxParticles);
}
return Mathf.RoundToInt(sum);
}
/// <summary>인스턴스에만 방출률·상한을 건다(프리팹 무수정 · 총량 상한의 실행부).</summary>
static void ApplyFireBudget(Slot s, float mul, WLStageBarrierSettings cfg)
{
if (s.ps == null || s.psRateOrig == null) return;
for (int i = 0; i < s.ps.Length; i++)
{
var ps = s.ps[i];
if (ps == null || i >= s.psRateOrig.Length) continue;
var orig = s.psRateOrig[i];
var e = ps.emission;
if (mul < 1f) e.rateOverTime = ScaleCurve(orig, mul);
var m = ps.main;
float life = m.startLifetime.constantMax;
int cap = Mathf.Max(4, Mathf.CeilToInt(orig.constantMax * mul * life) + 4);
int max = s.psMaxOrig != null && i < s.psMaxOrig.Length ? s.psMaxOrig[i] : m.maxParticles;
m.maxParticles = Mathf.Min(max, cap); // 🔴 하드 상한 — 버스트가 튀어도 못 넘는다
}
}
// ═════════════════════════════════════════════════════════════════════
// 연출 — 0 → 1 (나타남) / 1 → 0 (사라짐)
// ═════════════════════════════════════════════════════════════════════
static void ApplyPhase(float t)
{
s_t = t;
float e = Mathf.SmoothStep(0f, 1f, Mathf.Clamp01(t));
for (int i = 0; i < s_live.Count; i++)
{
var s = s_live[i];
if (s == null || s.tf == null) continue;
if (s_current == StageBarrierStyle.Props)
{
// 프롭 = 지면에서 솟는다(Y 만 자란다 · XZ 고정) → 머티리얼 무수정으로 「생겨남」이 읽힌다
s.tf.localScale = new Vector3(s.baseScale.x, s.baseScale.y * Mathf.Max(0.001f, e), s.baseScale.z);
}
else
{
s.tf.localScale = s.baseScale * Mathf.Max(0.001f, e);
}
if (s.ps != null && s.ps.Length > 0) SetAlpha(s, e);
bool on = e > 0.002f;
if (s.go.activeSelf != on) s.go.SetActive(on);
}
}
/// <summary>연출 진행도를 「원래 색」의 알파에 곱한다(인스턴스만 · 머티리얼 무수정 · 프리팹 원색 보존).</summary>
static void SetAlpha(Slot s, float a)
{
for (int i = 0; i < s.ps.Length; i++)
{
var ps = s.ps[i];
if (ps == null) continue;
Color c = s.psColor != null && i < s.psColor.Length ? s.psColor[i] : ps.main.startColor.color;
var m = ps.main;
m.startColor = new ParticleSystem.MinMaxGradient(new Color(c.r, c.g, c.b, c.a * a));
}
}
/// <summary>
/// SO 색을 「원래 색」에 **곱한다**(대입이 아니다).
/// 🔴 실측(WL815c_TUNE) — Hovl Marker 3 Zone Loop 은 4개 파티클시스템 전부 `colorOverLifetime` 이 ON 이고
/// startColor 가 (0.027, 0.056, 0.104) 같은 **어두운 기저색**이다(발광 셰이더가 이것을 증폭한다).
/// 여기에 (1, 0.35, 0.15) 를 그냥 대입하면 원래보다 훨씬 밝아져 **흰색으로 뭉개진다**(1차 캡처에서 실측).
/// 곱하면 아티스트가 잡은 밝기를 유지한 채 색조만 돈다.
/// </summary>
static void Tint(Slot s, Color color)
{
if (s.psColor == null || s.psOrig == null) return;
for (int i = 0; i < s.psColor.Length && i < s.psOrig.Length; i++)
{
Color o = s.psOrig[i];
s.psColor[i] = new Color(o.r * color.r, o.g * color.g, o.b * color.b, o.a * color.a);
}
}
static void Restart(Slot s)
{
if (s.ps == null) return;
for (int i = 0; i < s.ps.Length; i++)
{
var ps = s.ps[i];
if (ps == null) continue;
ps.Clear(true);
if (Application.isPlaying) ps.Play(true);
}
}
// ═════════════════════════════════════════════════════════════════════
// 풀
// ═════════════════════════════════════════════════════════════════════
static Slot Take(List<Slot> pool, GameObject prefab)
{
for (int i = 0; i < pool.Count; i++)
{
var s = pool[i];
if (s.go == null) continue;
if (s_live.Contains(s)) continue;
if (s.go.name != prefab.name + "#815c") continue; // 프리팹이 다르면 재사용하지 않는다
RestoreOriginals(s); // 🔴 지난 회차의 색·예산을 되돌린다
return s;
}
EnsureRoot();
var go = Object.Instantiate(prefab); // 🔴 끊어진 클론 — 원본 프리팹 무수정
go.name = prefab.name + "#815c";
go.hideFlags = HideFlags.HideAndDontSave;
go.transform.SetParent(s_root, false);
Instantiates++;
var ps = go.GetComponentsInChildren<ParticleSystem>(true);
var slot = new Slot
{
go = go,
tf = go.transform,
baseScale = Vector3.one,
ps = ps,
psOrig = new Color[ps.Length],
psMaxOrig = new int[ps.Length],
psRateOrig = new ParticleSystem.MinMaxCurve[ps.Length],
psColor = new Color[ps.Length],
};
for (int i = 0; i < ps.Length; i++)
{
slot.psOrig[i] = ps[i].main.startColor.color; // 프리팹 원색 채집
slot.psMaxOrig[i] = ps[i].main.maxParticles;
slot.psRateOrig[i] = ps[i].emission.rateOverTime;
slot.psColor[i] = slot.psOrig[i];
}
pool.Add(slot);
return slot;
}
/// <summary>풀에서 꺼낼 때 프리팹 원본 색·파티클 상한·방출률 배수로 되돌린다.</summary>
static void RestoreOriginals(Slot s)
{
if (s.ps == null) return;
for (int i = 0; i < s.ps.Length; i++)
{
var ps = s.ps[i];
if (ps == null) continue;
if (s.psColor != null && s.psOrig != null && i < s.psOrig.Length) s.psColor[i] = s.psOrig[i];
if (s.psMaxOrig != null && i < s.psMaxOrig.Length) { var m = ps.main; m.maxParticles = s.psMaxOrig[i]; }
if (s.psRateOrig != null && i < s.psRateOrig.Length) { var e = ps.emission; e.rateOverTime = s.psRateOrig[i]; }
}
}
/// <summary>
/// 방출률 곡선에 배수를 건다.
/// 🔴 `rateOverTimeMultiplier` 는 Constant 모드에서 **상수 그 자체**라 거기에 mul 을 대입하면 rate 가 mul(개/s)이 된다.
/// 모드별로 원본 값을 읽어 곱해야 한다.
/// </summary>
public static ParticleSystem.MinMaxCurve ScaleCurve(ParticleSystem.MinMaxCurve src, float mul)
{
switch (src.mode)
{
case ParticleSystemCurveMode.Constant:
return new ParticleSystem.MinMaxCurve(src.constant * mul);
case ParticleSystemCurveMode.TwoConstants:
return new ParticleSystem.MinMaxCurve(src.constantMin * mul, src.constantMax * mul);
case ParticleSystemCurveMode.Curve:
return new ParticleSystem.MinMaxCurve(src.curveMultiplier * mul, src.curve);
default:
return new ParticleSystem.MinMaxCurve(src.curveMultiplier * mul, src.curveMin, src.curveMax);
}
}
static void ReturnAll()
{
for (int i = 0; i < s_live.Count; i++)
{
var s = s_live[i];
if (s == null || s.go == null) continue;
s.go.SetActive(false); // 🔴 Destroy 0 — 풀에 반납
}
s_live.Clear();
s_t = 0f;
RefreshCounts();
}
static void StripColliders(GameObject go)
{
var cols = go.GetComponentsInChildren<Collider>(true);
for (int i = 0; i < cols.Length; i++)
{
if (cols[i] == null) continue;
if (Application.isPlaying) Object.Destroy(cols[i]); else Object.DestroyImmediate(cols[i]);
}
}
static void EnsureRoot()
{
if (s_root != null) return;
var go = new GameObject("[WL815c] StageBarrierVisual");
go.hideFlags = HideFlags.HideAndDontSave;
s_root = go.transform;
}
static void RefreshCounts()
{
ActiveInstances = s_live.Count;
PooledInstances = (s_ring.Count + s_props.Count + s_fires.Count) - s_live.Count;
}
// ═════════════════════════════════════════════════════════════════════
// 진단 · 프로브
// ═════════════════════════════════════════════════════════════════════
/// <summary>프로브용: 활성 인스턴스의 렌더러 수 · 머티리얼 종류 · 삼각형 합(드로우콜 근사).</summary>
public static void MeasureCost(out int renderers, out int materials, out long triangles, out int particleSystems)
{
renderers = 0; materials = 0; triangles = 0; particleSystems = 0;
var seen = new HashSet<int>();
for (int i = 0; i < s_live.Count; i++)
{
var s = s_live[i];
if (s == null || s.go == null) continue;
var rs = s.go.GetComponentsInChildren<Renderer>(true);
for (int k = 0; k < rs.Length; k++)
{
renderers++;
var mf = rs[k].GetComponent<MeshFilter>();
if (mf != null && mf.sharedMesh != null) triangles += mf.sharedMesh.triangles.Length / 3;
var ms = rs[k].sharedMaterials;
for (int j = 0; j < ms.Length; j++) if (ms[j] != null && seen.Add(ms[j].GetInstanceID())) materials++;
}
if (s.ps != null) particleSystems += s.ps.Length;
}
}
/// <summary>프로브용: 지금 활성 인스턴스 수.</summary>
public static int LiveCount { get { return s_live.Count; } }
/// <summary>프로브용: 연출 진행도(0..1).</summary>
public static float Phase { get { return s_t; } }
/// <summary>프로브용: 연출을 끝난 상태로 즉시 만든다(캡처용 · 0 = 사라진 상태).</summary>
public static void ForcePhase(float t) { ApplyPhase(Mathf.Clamp01(t)); }
/// <summary>프로브용: 지금 도는 연출(입장/클리어)의 시계를 끝으로 돌린다 — 다음 Tick 이 실제 종료 경로를 탄다.</summary>
public static void FinishPhaseForProbe() { s_phaseStart = Time.unscaledTime - Mathf.Max(0.01f, s_phaseLen) - 0.01f; }
/// <summary>프로브용: 파티클을 에디트 모드에서 t 초만큼 시뮬레이트한다(캡처용).</summary>
public static void SimulateForProbe(float seconds)
{
for (int i = 0; i < s_live.Count; i++)
{
var s = s_live[i];
if (s == null || s.ps == null) continue;
for (int k = 0; k < s.ps.Length; k++)
{
if (s.ps[k] == null) continue;
s.ps[k].Clear(true);
s.ps[k].Simulate(seconds, true, true, false);
}
}
}
/// <summary>프로브용: 풀까지 전부 파괴하고 카운터 초기화.</summary>
public static void HardResetForProbe()
{
s_live.Clear();
DestroyPool(s_ring); DestroyPool(s_props); DestroyPool(s_fires);
if (s_root != null)
{
if (Application.isPlaying) Object.Destroy(s_root.gameObject); else Object.DestroyImmediate(s_root.gameObject);
s_root = null;
}
s_current = StageBarrierStyle.None;
StyleOverride = (StageBarrierStyle)(-1);
s_showing = false; s_t = 0f;
Instantiates = ActiveInstances = PooledInstances = StyleSwitches = Shows = Hides = 0;
PropCount = FireCount = FireParticleEstimate = FirePerInstanceEstimate = 0;
PropSpacingUsed = 0f; FireRateMul = 1f; RingScale = 0f; LastBuildMs = 0f;
}
static void DestroyPool(List<Slot> pool)
{
for (int i = 0; i < pool.Count; i++)
{
var s = pool[i];
if (s == null || s.go == null) continue;
if (Application.isPlaying) Object.Destroy(s.go); else Object.DestroyImmediate(s.go);
}
pool.Clear();
}
/// <summary>프로브·캡처용: 그림 루트(임시 씬에서 카메라 프레이밍에 쓴다).</summary>
public static Transform Root { get { return s_root; } }
}
}

View File

@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 62bdc4832e9738745a6ad7e320f975cb

View File

@ -0,0 +1,291 @@
// ─────────────────────────────────────────────────────────────────────────────
// WLStageBarrierSettings.cs — 스테이지 장벽 값의 단일 출처(SOT · C45)
//
// PD 지시 #815 「스테이지에 들어가면 고정 영역(장벽) 안에서만 싸운다」
// + PD 질문(2026-09-11 00:3x) 「지역을 어떻게 구역으로 막을 것인지 — 울타리 오브젝트 · 불꽃 이펙트 등 대안」
// 발주서 WL-815c · SOT = 기준서 「스테이지_전환_기준서_v1.md」 §D
//
// ■ PD 가 값 하나만 바꿔서 3가지 「보이는 장벽」을 비교한다 → style 필드
// 0 없음 · 1 마법진 링(Hovl Marker) · 2 프롭 링(울타리/바위) · 3 불꽃 링
//
// ■ 막는 방법은 기준서 §D-2 실측 결론 그대로 = ③ 거리 판정 + 워프백(신규 코드 0 에 가깝게 재사용).
// ① 물리 콜라이더 벽은 **불가**(PC 캡슐 IsTrigger · Rigidbody Constraints 126).
// ② NavMesh 카브는 가능하지만 런타임 재베이크 비용이 있어 **옵션(기본 off)** 으로 둔다.
//
// 에셋 경로(고정): Assets/WL/Combat/Stage/Barrier/Resources/WL/WLStageBarrierSettings.asset
// → Resources.Load<WLStageBarrierSettings>("WL/WLStageBarrierSettings")
// (WLCameraFramingSettings 가 같은 방식으로 기능 폴더 안 Resources 를 쓴다 — 815b 의 Stage/*.cs 와 파일 충돌 0)
//
// ■ C8 롤백: ① 에셋 없음 / ② enabled = 0 / ③ RuntimeDisabled = true → Set() 이 아무것도 하지 않는다(오브젝트 0).
// 축별 off = blockEnabled 0(워프백만 끔) · showVisual 0(그림만 끔) · carveNavMesh 0(카브만 끔).
//
// 🔴 아래 기본값은 에셋 생성 시 초기값이다. 실제로 읽히는 값은 항상 .asset 이고 코드에 게임 수치 상수는 없다(C45).
// 🔴 어셈블리 주의: Assets/WL/Combat/ 에 .asmdef 를 만들지 말 것(Actor/MobActor 가 Assembly-CSharp 에 있다).
// ─────────────────────────────────────────────────────────────────────────────
using UnityEngine;
namespace WL.Combat.Stage
{
/// <summary>장벽을 「보이게」 하는 방식. PD 가 이 값 하나로 3안을 바꿔 본다.</summary>
public enum StageBarrierStyle
{
/// <summary>그림 없음 — 판정만(워프백). 비용 0.</summary>
None = 0,
/// <summary>ⓐ 마법진 링 — Hovl Marker 프리팹 1개를 반경에 맞게 스케일.</summary>
MagicRing = 1,
/// <summary>ⓑ 프롭 링 — 울타리·바위 프리팹을 둘레에 일정 간격 배치.</summary>
Props = 2,
/// <summary>ⓒ 불꽃 링 — 불꽃 파티클 프리팹을 둘레에 배치(총량 상한).</summary>
Fire = 3,
}
/// <summary>불꽃 총량 상한을 어떻게 지키는가.</summary>
public enum FireBudgetMode
{
/// <summary>개수는 유지하고 인스턴스마다 방출률을 낮춘다(링이 끊기지 않는다 · 권장).</summary>
ReduceRate = 0,
/// <summary>방출률은 유지하고 인스턴스 개수를 줄인다(불꽃 하나하나가 진하다 · 간격이 벌어진다).</summary>
ReduceCount = 1,
}
[CreateAssetMenu(fileName = "WLStageBarrierSettings", menuName = "WL/Stage Barrier Settings", order = 333)]
public sealed class WLStageBarrierSettings : ScriptableObject
{
public const string ResourcesPath = "WL/WLStageBarrierSettings";
static WLStageBarrierSettings s_cached;
static bool s_lookupDone;
/// <summary>설정 에셋(1회 로드 후 캐시). 없으면 null = 기능 전체 off.</summary>
public static WLStageBarrierSettings Instance
{
get
{
if (!s_lookupDone)
{
s_cached = Resources.Load<WLStageBarrierSettings>(ResourcesPath);
s_lookupDone = true;
}
return s_cached;
}
}
/// <summary>진단·A/B 전용 런타임 스위치. 도메인 리로드로 자동 false.</summary>
public static bool RuntimeDisabled;
/// <summary>장벽 축이 살아 있는가.</summary>
public static bool Active
{
get { var i = Instance; return i != null && i.enabled && !RuntimeDisabled; }
}
/// <summary>테스트용: 캐시를 비워 다음 접근 때 다시 Resources.Load 한다.</summary>
public static void ClearCache() { s_cached = null; s_lookupDone = false; }
/// <summary>프로브용: 임시 SO 를 잠시 Instance 자리에 끼운다(C8 진리표 · 실에셋은 읽기만 한다).</summary>
public static void SetInstanceForProbe(WLStageBarrierSettings so) { s_cached = so; s_lookupDone = true; }
// ─────────────────────────────────────────────────────── 공통
[Header("공통")]
[Tooltip("0 이면 Set() 이 무동작 = 원본 동작 100% (C8 롤백).")]
public bool enabled = true;
[Tooltip("워프백·스타일 전환마다 로그 1줄.")]
public bool verboseLog;
[Tooltip("이탈 판정 주기(초). 813w3 아레나 폴링(0.25 s)과 같은 등급.")]
public float checkSeconds = 0.2f;
[Tooltip("몬스터 목록 재수집 주기(초). FindObjectsByType 는 여기서만 부른다(GC).")]
public float rescanSeconds = 3f;
// ─────────────────────────────────────────────────────── ① 막기 = 워프백(기준서 §D-2 ③)
[Header("① 막기 — 거리 판정 + 워프백 (기준서 §D-2 ③ · 813w3/813t2 워프 API)")]
[Tooltip("0 이면 판정·워프백을 끈다(그림만 남는다).")]
public bool blockEnabled = true;
[Tooltip("반경 + 이 여유(m) 밖이어야 「나갔다」고 본다. 813d playerLeaveMargin(4)보다 좁게 잡아 장벽 그림 밖으로 크게 벗어나지 않게 한다.")]
public float leaveMargin = 2f;
[Tooltip("밖에 이만큼(초) 넘게 있어야 되돌린다. 넉백·대시로 잠깐 스치는 것은 흘려보낸다.")]
public float graceSeconds = 0.5f;
[Tooltip("같은 대상을 다시 되돌리기까지의 최소 간격(초).")]
public float warpCooldownSeconds = 1f;
[Tooltip("되돌릴 지점을 경계에서 안쪽으로 이만큼(m) 들여놓는다. 0 이면 경계에 딱 붙어 바로 다시 나간다.")]
public float warpInsetMeters = 1.5f;
[Tooltip("되돌릴 지점을 NavMesh 위로 스냅할 1차 반경(m). PC 는 Get_NavSnapRadius()=0 이라 Set_Warp 이 스스로 스냅하지 않는다(813w3 실측).")]
public float warpNavSampleRadius = 4f;
[Tooltip("1차에서 못 찾으면 쓰는 2차 반경(m). 그래도 못 찾으면 워프하지 않는다(지형 뚫기 방지 · 813w3 규칙).")]
public float warpNavSampleRadiusFar = 20f;
[Tooltip("플레이어를 가둔다.")]
public bool blockPlayer = true;
[Tooltip("몬스터도 가둔다(밖으로 밀린 몹 복귀 · 813x MobNavRecovery 와 같은 뿌리).")]
public bool blockMobs = true;
[Tooltip("몬스터용 여유(m). PC 보다 넉넉하게 — 몹은 넉백으로 자주 밀린다.")]
public float mobLeaveMargin = 3f;
[Tooltip("몬스터용 유예(초).")]
public float mobGraceSeconds = 1f;
[Tooltip("몬스터용 쿨다운(초).")]
public float mobWarpCooldownSeconds = 2f;
[Tooltip("플레이어를 되돌린 직후 카메라 이동 리드를 0 으로 스냅한다(813t2 방식 · RealCamera 무수정).")]
public bool warpSnapsCameraLead = true;
[Tooltip("되돌리기 전에 남은 NavMesh 경로를 지운다(원본이 밖으로 다시 끌고 가지 않게).")]
public bool warpResetsPath = true;
// ─────────────────────────────────────────────────────── ② NavMesh 카브(옵션 · 기본 off)
[Header("② NavMesh 카브 (옵션 · 기본 off · 기준서 §D-2 ②)")]
[Tooltip("1 이면 링 둘레에 NavMeshObstacle(carve) 세그먼트를 런타임 생성한다 = 몹이 경로로는 못 나간다. " +
"재베이크 비용이 있어 기본 0. 원본 NavMesh 에셋·프리팹은 건드리지 않는다(런타임 오브젝트만).")]
public bool carveNavMesh;
[Tooltip("세그먼트 1개가 덮는 둘레 길이(m). 둘레 ÷ 이 값 = 세그먼트 수.")]
public float carveSegmentArcMeters = 4f;
[Tooltip("세그먼트 수 상한. 반경이 커도 이 수를 넘기지 않는다(재베이크 비용 방어).")]
public int maxCarveSegments = 32;
[Tooltip("세그먼트 상자의 두께(m · 반경 방향). 얇으면 몹이 뚫고 지나갈 수 있다.")]
public float carveThickness = 1.5f;
[Tooltip("세그먼트 상자의 높이(m).")]
public float carveHeight = 3f;
[Tooltip("이웃 세그먼트가 겹치는 비율(0.15 = 15 % 겹침). 0 이면 모서리에 틈이 생긴다.")]
public float carveOverlap = 0.15f;
[Tooltip("carveOnlyMovementThreshold — 장벽은 움직이지 않으므로 1 이면 정적 카브(재베이크 1회). 0 이면 매 프레임 카브 검사.")]
public bool carveStaticOnly = true;
// ─────────────────────────────────────────────────────── ③ 보이기 — 공통
[Header("③ 보이기 — 스타일 (🔴 PD 는 이 값 하나만 바꾼다)")]
[Tooltip("0 없음 · 1 마법진 링 · 2 프롭 링(울타리·바위) · 3 불꽃 링")]
public StageBarrierStyle style = StageBarrierStyle.MagicRing;
[Tooltip("0 이면 그림을 전부 끈다(판정만 남는다).")]
public bool showVisual = true;
[Tooltip("스테이지 입장 때 장벽이 나타나는 시간(초).")]
public float appearSeconds = 0.6f;
[Tooltip("클리어(장벽이 열릴 때) 사라지는 시간(초).")]
public float fadeSeconds = 0.6f;
[Tooltip("그림을 지면 위 이만큼(m) 띄운다(Z-파이팅 방지).")]
public float visualYOffset = 0.05f;
[Tooltip("그림 반경 = 판정 반경 × 이 배율. 1 보다 살짝 크게 두면 「선 안쪽에서 싸운다」로 읽힌다.")]
public float visualRadiusScale = 1f;
// ─────────────────────────────────────────────────────── ⓐ 마법진 링
[Header("ⓐ 마법진 링 (Hovl Map track markers · 기준서 §D-3 1순위)")]
[Tooltip("링 프리팹. 기본 = Marker 3 Zone Loop.")]
public GameObject ringPrefab;
[Tooltip("🔴 실측값 — 스케일 1 일 때 이 프리팹의 시각 반경(m). 스케일 = 목표반경 ÷ 이 값. " +
"Marker 3 Zone Loop 실측 1.10 m(Simulate 2 s · 파티클 위치+반크기 최대).")]
public float ringPrefabVisualRadius = 1.1f;
[Tooltip("링 색 — 프리팹 원색에 **곱해진다**(대입이 아니다). 1,1,1,1 = 원본 그대로. " +
"🔴 실측: Hovl 마커는 startColor 가 어두운 기저색 + colorOverLifetime ON 이라 대입하면 흰색으로 뭉개진다.")]
public Color ringColor = new Color(6f, 1.2f, 0.4f, 1f);
[Tooltip("1 이면 ringColor 를 프리팹 원색에 곱한다. " +
"🔴 기본 0 — 실측(캡처 3회): Hovl `HS_Blend_CG`(Shader Graph) 는 VertexColorNode 가 그래프에 있는데도 " +
"Marker 3 Zone Loop 의 화면 색이 눈에 띄게 바뀌지 않았다(대입·×1.6·×6 전부). " +
"색을 실제로 바꾸려면 머티리얼 사본이 필요한데 그건 원본 에셋 접촉이라 이 태스크 범위 밖이다.")]
public bool ringTintEnabled;
[Tooltip("도트 룩(814b/814c)에서 링이 뭉개지지 않게 세로로 늘리는 배율. 1 = 프리팹 그대로. " +
"🔴 링은 평면 쿼드라 Y 스케일은 두께가 아니라 「기울기」를 만든다 — 두께는 ringExtraRings 로 낸다.")]
public float ringYScale = 1f;
[Tooltip("링을 반경을 조금씩 달리해 몇 겹 더 깐다(0 = 1겹). 도트 룩에서 1px 로 뭉개지는 것을 막는 가장 싼 방법. " +
"겹당 인스턴스 1개 = 파티클 4개(실측).")]
public int ringExtraRings = 1;
[Tooltip("추가 겹의 반경 차이(m).")]
public float ringExtraSpacing = 0.6f;
// ─────────────────────────────────────────────────────── ⓑ 프롭 링
[Header("ⓑ 프롭 링 (울타리 · 바위 · 기둥)")]
[Tooltip("둘레에 놓을 프롭 프리팹 목록. 여러 개면 번갈아 쓴다(회전 랜덤과 함께 반복감을 줄인다).")]
public GameObject[] propPrefabs = new GameObject[0];
[Tooltip("프롭 간격(m). 울타리는 프롭 길이(Fence_01 실측 2.38 m)보다 살짝 좁게 둬야 틈이 안 보인다.")]
public float propSpacing = 2.5f;
[Tooltip("프롭 개수 상한. 둘레 ÷ 간격 이 이 수를 넘으면 간격을 자동으로 벌린다(개수 고정).")]
public int maxProps = 64;
[Tooltip("프롭 스케일.")]
public float propScale = 1f;
[Tooltip("1 이면 프롭의 정면(+X = 실측 긴축)이 둘레의 접선을 향한다 = 울타리가 벽처럼 이어진다. " +
"0 이면 회전 없음(바위처럼 방향이 없는 프롭용).")]
public bool propAlignToTangent = true;
[Tooltip("접선 정렬 뒤 추가로 흔들 각도(±도). 바위·나무는 크게(180), 울타리는 작게(0~8).")]
public float propYawJitter = 6f;
[Tooltip("반경 방향 흔들기(±m). 0 이면 정확히 원 위.")]
public float propRadialJitter = 0.2f;
[Tooltip("1 이면 Raycast 로 지면 높이에 스냅한다(WL_Nature 는 평지가 아니다).")]
public bool propGroundSnap = true;
[Tooltip("지면 Raycast 시작 높이(m · 장벽 중심 y 기준 위쪽).")]
public float propRayUp = 50f;
[Tooltip("지면 Raycast 길이(m).")]
public float propRayLength = 200f;
[Tooltip("1 이면 인스턴스의 Collider 를 전부 제거한다. 프롭 원본(LMHPOLY With_Box_Colliders 등)에 붙은 콜라이더가 " +
"카메라 가림 페이드(811 CameraFramingOverride)·투사체와 엉키지 않게 한다. 원본 프리팹은 무수정.")]
public bool propStripColliders = true;
[Tooltip("랜덤 시드. 같은 시드 = 같은 배치(캡처 재현용).")]
public int propSeed = 815;
// ─────────────────────────────────────────────────────── ⓒ 불꽃 링
[Header("ⓒ 불꽃 링 (보유 불꽃 파티클)")]
[Tooltip("둘레에 놓을 불꽃 프리팹 목록. 기본 = LMHPOLY Fire_Particles(실측 3 PS · 정상상태 125 파티클).")]
public GameObject[] firePrefabs = new GameObject[0];
[Tooltip("불꽃 간격(m).")]
public float fireSpacing = 3f;
[Tooltip("불꽃 개수 상한.")]
public int maxFires = 48;
[Tooltip("불꽃 스케일.")]
public float fireScale = 1f;
[Tooltip("🔴 링 전체의 파티클 총량 상한. 811s EffectScaleGuard/811q 예산과 부딪히지 않게 이 선에서 자른다. " +
"실측 = Fire_Particles 1개당 정상상태 125 → 상한 1200 이면 인스턴스 29개에서 방출률 ×0.33.")]
public int maxTotalParticles = 1200;
[Tooltip("총량 상한을 지키는 방법. ReduceRate(0) = 개수 유지 · 방출률 축소(권장) · ReduceCount(1) = 개수 축소.")]
public FireBudgetMode fireBudgetMode = FireBudgetMode.ReduceRate;
[Tooltip("불꽃 색(파티클 startColor · 0 이면 프리팹 원색).")]
public bool fireTintEnabled;
[Tooltip("불꽃 색.")]
public Color fireColor = new Color(1f, 0.45f, 0.1f, 1f);
[Tooltip("1 이면 Raycast 로 지면 높이에 스냅한다.")]
public bool fireGroundSnap = true;
}
}

View File

@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 3c3dc9bd8de148744b88ce512ce0f09a