Project_WL/Assets/WL/Combat/Telegraph/TelegraphDecal.cs

388 lines
18 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// ─────────────────────────────────────────────────────────────────────────────
// TelegraphDecal.cs — 지면 예고 판(① 기준서 §G-1) · 판정 범위와 100 % 같은 크기 · 풀링 · GC 0
//
// PD 지시 #815-5 · 발주서 WL-815f §1-2
//
// ■ 왜 DecalProjector 가 아닌가 (기준서 §G-1 실측)
// URP-Balanced-Renderer 의 RendererFeatures 는 **SSAO 1개뿐**이라 Decal Renderer Feature 가 없다 →
// DecalProjector 는 화면에 아예 안 나온다. 보유 인디케이터 20종이 나오는 이유는 그것들이 **파티클/쿼드**이기 때문이다.
// → 여기서도 같은 방식(런타임 쿼드 2장 · 새 셰이더/머티리얼/메시 에셋 0)을 쓴다.
//
// ■ 판 구성 (드로우 +2 / 판 · 동시 상한으로 묶는다)
// 바깥 판(범위) = 콜라이더 크기 그대로 · 낮은 알파 → 「여기 맞는다」
// 안쪽 판(진행) = 같은 자리에서 0 → 100 % 로 **채워진다** · 높은 알파 → 「언제 맞는다」
// 두 판 모두 몹의 회전을 따라간다(원본 Play_Attack 이 transform.LookAt 으로 계속 돌린다).
//
// ■ 크기의 출처 = TelegraphShape (투사체 프리팹 BoxCollider) — 코드 상수 0.
// 근접: size.x × size.z 사각형, 중심 = 발사 위치 + 회전된 collider.center 의 XZ.
// 원거리: 폭 = size.x, 길이 = 투사체 속도 × 수명(기준서 §G-6 「직선 데칼」 · rangedMaxLength 로 자른다).
//
// ■ 색 = 위험 색 규약(기준서 §G-3): 노랑 = 피할 수 있다 · 빨강 = 못 피한다.
// ■ IndicatorInfo.Show_Indicator 재사용 = SO 의 indicatorPrefabName 이 비어 있지 않을 때 덧칠로 켠다(§G-1 「원본 공개 API」).
//
// 🔴 어셈블리 주의: Assets/WL/Combat/ 에 .asmdef 를 만들지 말 것.
// ─────────────────────────────────────────────────────────────────────────────
using UnityEngine;
namespace WL.Combat.Telegraph
{
public static class TelegraphDecal
{
sealed class Plate
{
public GameObject root;
public Transform tf, baseTf, fillTf;
public MeshRenderer baseMr, fillMr;
public bool inUse;
public bool ranged;
public Vector3 localSize; // x = 폭 · z = 길이(실측 콜라이더에서 온다)
public Vector3 localOffset; // 몹 로컬 기준 판 중심(콜라이더 center + 전방 오프셋)
public GameObject indicator; // IndicatorInfo 풀에서 빌린 덧칠(없으면 null)
}
static Plate[] s_pool;
static int s_inUse;
static Mesh s_quad;
static Material[] s_matBase, s_matFill; // [0] = Avoidable · [1] = Unavoidable
static Shader s_shader;
static int s_colorPropId;
static bool s_hasColorProp, s_shaderTried;
static Transform s_root;
// ── 진단(프로브가 읽는다)
public static int AcquireCount, ReleaseCount, BudgetSkipCount, PoolGrowCount, IndicatorBorrowCount, IndicatorMissCount;
public static string LastInfo = "";
public static int InUse { get { return s_inUse; } }
public static int PoolSize { get { return s_pool == null ? 0 : s_pool.Length; } }
public static bool ShaderReady { get { return s_shader != null; } }
public static string ShaderName { get { return s_shader == null ? "(없음)" : s_shader.name; } }
static WLTelegraphSettings St { get { return WLTelegraphSettings.Instance; } }
// ───────────────────────────────────────── 공개 API
/// <summary>판을 하나 켠다. 반환 = 풀 인덱스(-1 = 예산 초과 또는 비활성).</summary>
public static int Acquire(Actor actor, in TelegraphBox box, bool ranged, float projectileLifetime, TelegraphDanger danger)
{
var st = St;
if (st == null || !st.decalEnabled || actor == null) return -1;
if (!EnsureShader(st)) return -1;
if (s_inUse >= Mathf.Max(1, st.maxConcurrent)) { BudgetSkipCount++; return -1; }
int idx = Take(st);
if (idx < 0) return -1;
var p = s_pool[idx];
// ── 크기(판정과 같은 값) ────────────────────────────────────────
float scale = st.sizeScale <= 0f ? 1f : st.sizeScale;
float width = Mathf.Max(0.05f, box.size.x * scale);
float length;
if (ranged)
{
float travel = box.speed > 0f ? box.speed * Mathf.Max(0f, projectileLifetime) : box.size.z;
length = Mathf.Clamp(travel * scale, box.size.z * scale, Mathf.Max(1f, st.rangedMaxLength));
}
else length = Mathf.Max(0.05f, box.size.z * scale);
p.ranged = ranged;
p.localSize = new Vector3(width, 1f, length);
// 판 중심 = 발사 위치(전방 forwardOffset) + 콜라이더 center 의 XZ.
// 원거리는 투사체가 앞으로 나아가므로 진행 구간의 한가운데로 민다.
float centerZ = st.forwardOffset + box.center.z * scale + (ranged ? length * 0.5f : 0f);
p.localOffset = new Vector3(box.center.x * scale, 0f, centerZ);
int di = danger == TelegraphDanger.Unavoidable ? 1 : 0;
p.baseMr.sharedMaterial = s_matBase[di];
p.fillMr.sharedMaterial = s_matFill[di];
p.baseTf.localScale = new Vector3(width, 1f, length);
p.root.SetActive(true);
Follow(idx, actor);
SetFill(idx, 0f);
// ── IndicatorInfo.Show_Indicator 재사용(덧칠 · 원본 공개 API · 로드/풀링은 원본이 한다)
BorrowIndicator(st, p, Mathf.Max(width, length));
AcquireCount++;
return idx;
}
/// <summary>몹의 현재 위치·회전을 따라간다(회전 추종).</summary>
public static void Follow(int idx, Actor actor)
{
if (idx < 0 || s_pool == null || idx >= s_pool.Length) return;
var p = s_pool[idx];
if (!p.inUse || actor == null) return;
var st = St;
if (st == null) return;
var atf = actor.transform;
Quaternion rot = st.followRotation ? Quaternion.Euler(0f, atf.eulerAngles.y, 0f) : p.tf.rotation;
Vector3 pos = atf.position + rot * p.localOffset;
pos.y = atf.position.y + st.groundYOffset;
p.tf.SetPositionAndRotation(pos, rot);
if (p.indicator != null) p.indicator.transform.SetPositionAndRotation(pos, rot);
}
/// <summary>진행도 0~1 을 판에 반영한다(채워지는 연출).</summary>
public static void SetFill(int idx, float t01)
{
if (idx < 0 || s_pool == null || idx >= s_pool.Length) return;
var p = s_pool[idx];
if (!p.inUse) return;
var st = St;
if (st == null) return;
float t = Mathf.Clamp01(t01);
float w = p.localSize.x, l = p.localSize.z;
if (st.fillMode == TelegraphFill.CenterExpand)
{
p.fillTf.localScale = new Vector3(w * t, 1f, l * t);
p.fillTf.localPosition = Vector3.zero;
}
else // ForwardSweep — 몹 쪽 가장자리에서 앞으로 채워진다
{
p.fillTf.localScale = new Vector3(w, 1f, Mathf.Max(0.0001f, l * t));
p.fillTf.localPosition = new Vector3(0f, 0.01f, -l * 0.5f + l * t * 0.5f);
}
}
/// <summary>판을 끈다(풀 반납).</summary>
public static void Release(int idx)
{
if (idx < 0 || s_pool == null || idx >= s_pool.Length) return;
var p = s_pool[idx];
if (!p.inUse) return;
p.inUse = false;
s_inUse--;
if (p.root != null) p.root.SetActive(false);
if (p.indicator != null) { p.indicator.SetActive(false); p.indicator = null; }
ReleaseCount++;
}
/// <summary>전부 끈다(씬 전환 · C8 · 프로브 정리).</summary>
public static void ReleaseAll()
{
if (s_pool == null) return;
for (int i = 0; i < s_pool.Length; i++) Release(i);
s_inUse = 0;
}
/// <summary>풀 오브젝트까지 파기한다(프로브 종료 · 에디트 모드 잔재 0).</summary>
public static void DestroyAll()
{
ReleaseAll();
if (s_pool != null)
for (int i = 0; i < s_pool.Length; i++)
if (s_pool[i] != null && s_pool[i].root != null) DestroyObj(s_pool[i].root);
s_pool = null;
if (s_root != null) { DestroyObj(s_root.gameObject); s_root = null; }
if (s_matBase != null) for (int i = 0; i < s_matBase.Length; i++) if (s_matBase[i] != null) DestroyObj(s_matBase[i]);
if (s_matFill != null) for (int i = 0; i < s_matFill.Length; i++) if (s_matFill[i] != null) DestroyObj(s_matFill[i]);
if (s_quad != null) DestroyObj(s_quad);
s_matBase = s_matFill = null; s_quad = null; s_shader = null; s_shaderTried = false;
}
public static void ResetDiagnostics()
{
AcquireCount = ReleaseCount = BudgetSkipCount = PoolGrowCount = IndicatorBorrowCount = IndicatorMissCount = 0;
LastInfo = "";
}
/// <summary>프로브용 — 판의 월드 크기(x = 폭 · z = 길이)와 중심을 돌려준다(콜라이더 대조용).</summary>
public static bool TryGetPlate(int idx, out Vector3 size, out Vector3 center, out float yaw)
{
size = Vector3.zero; center = Vector3.zero; yaw = 0f;
if (idx < 0 || s_pool == null || idx >= s_pool.Length) return false;
var p = s_pool[idx];
if (!p.inUse) return false;
size = new Vector3(p.baseTf.lossyScale.x, 0f, p.baseTf.lossyScale.z);
center = p.tf.position;
yaw = p.tf.eulerAngles.y;
return true;
}
// ───────────────────────────────────────── 내부
static int Take(WLTelegraphSettings st)
{
EnsurePool(st);
for (int i = 0; i < s_pool.Length; i++)
if (!s_pool[i].inUse && s_pool[i].root != null) { s_pool[i].inUse = true; s_inUse++; return i; }
return -1;
}
static void EnsurePool(WLTelegraphSettings st)
{
int want = Mathf.Max(1, Mathf.Max(st.maxConcurrent, st.poolWarmCount));
if (s_pool != null && s_pool.Length >= want) return;
if (s_root == null)
{
var rootGo = new GameObject("__WLTelegraphDecals");
rootGo.hideFlags = HideFlags.DontSave;
s_root = rootGo.transform;
if (Application.isPlaying) Object.DontDestroyOnLoad(rootGo);
}
int from = s_pool == null ? 0 : s_pool.Length;
var next = new Plate[want];
if (s_pool != null) System.Array.Copy(s_pool, next, s_pool.Length);
for (int i = from; i < want; i++) next[i] = MakePlate(i);
s_pool = next;
PoolGrowCount++;
}
static Plate MakePlate(int i)
{
var p = new Plate();
p.root = new GameObject("WLTelegraphPlate_" + i);
p.root.hideFlags = HideFlags.DontSave;
p.root.transform.SetParent(s_root, false);
p.tf = p.root.transform;
p.baseTf = MakeQuad("Range", p.tf, out p.baseMr);
p.fillTf = MakeQuad("Fill", p.tf, out p.fillMr);
p.fillTf.localPosition = new Vector3(0f, 0.01f, 0f);
p.root.SetActive(false);
return p;
}
static Transform MakeQuad(string name, Transform parent, out MeshRenderer mr)
{
var go = new GameObject(name);
go.hideFlags = HideFlags.DontSave;
go.transform.SetParent(parent, false);
var mf = go.AddComponent<MeshFilter>();
mf.sharedMesh = EnsureQuad();
mr = go.AddComponent<MeshRenderer>();
mr.shadowCastingMode = UnityEngine.Rendering.ShadowCastingMode.Off;
mr.receiveShadows = false;
mr.lightProbeUsage = UnityEngine.Rendering.LightProbeUsage.Off;
mr.reflectionProbeUsage = UnityEngine.Rendering.ReflectionProbeUsage.Off;
mr.motionVectorGenerationMode = MotionVectorGenerationMode.ForceNoMotion;
return go.transform;
}
/// <summary>XZ 평면 1×1 쿼드(위를 본다). 내장 Quad.fbx 의존 0 · 메시 에셋 0.</summary>
static Mesh EnsureQuad()
{
if (s_quad != null) return s_quad;
var m = new Mesh();
m.name = "__WLTelegraphQuad";
m.hideFlags = HideFlags.DontSave;
m.vertices = new[]
{
new Vector3(-0.5f, 0f, -0.5f), new Vector3(-0.5f, 0f, 0.5f),
new Vector3( 0.5f, 0f, 0.5f), new Vector3( 0.5f, 0f, -0.5f),
};
m.uv = new[] { new Vector2(0f, 0f), new Vector2(0f, 1f), new Vector2(1f, 1f), new Vector2(1f, 0f) };
m.normals = new[] { Vector3.up, Vector3.up, Vector3.up, Vector3.up };
m.triangles = new[] { 0, 1, 2, 0, 2, 3 };
m.RecalculateBounds();
s_quad = m;
return m;
}
static bool EnsureShader(WLTelegraphSettings st)
{
if (s_shader != null && s_matBase != null) return true;
if (s_shaderTried && s_shader == null) return false;
s_shaderTried = true;
var names = st.decalShaderNames;
if (names != null)
for (int i = 0; i < names.Length && s_shader == null; i++)
if (!string.IsNullOrEmpty(names[i])) s_shader = Shader.Find(names[i]);
if (s_shader == null) { LastInfo = "셰이더 후보를 못 찾음 — 데칼 비활성"; return false; }
s_hasColorProp = false;
var props = st.decalColorProperties;
if (props != null)
for (int i = 0; i < props.Length; i++)
if (!string.IsNullOrEmpty(props[i]) && s_shader.FindPropertyIndex(props[i]) >= 0)
{ s_colorPropId = Shader.PropertyToID(props[i]); s_hasColorProp = true; break; }
s_matBase = new Material[2];
s_matFill = new Material[2];
for (int d = 0; d < 2; d++)
{
Color c = d == 1 ? st.colorUnavoidable : st.colorAvoidable;
s_matBase[d] = MakeMat(st, "__WLTelegraphBase" + d, c, st.plateAlpha);
s_matFill[d] = MakeMat(st, "__WLTelegraphFill" + d, c, st.fillAlpha);
}
LastInfo = "셰이더 " + s_shader.name + " · 색 프로퍼티 " + (s_hasColorProp ? "O" : "X");
return true;
}
static Material MakeMat(WLTelegraphSettings st, string name, Color c, float alpha)
{
var m = new Material(s_shader);
m.name = name;
m.hideFlags = HideFlags.DontSave;
// URP Unlit 반투명 세팅(런타임) — 렌더링 배관이라 SO 튜닝 값이 아니다(811gh DashAfterImage 와 같은 방식).
if (m.HasProperty("_Surface")) m.SetFloat("_Surface", 1f);
if (m.HasProperty("_Blend")) m.SetFloat("_Blend", 0f);
if (m.HasProperty("_SrcBlend")) m.SetFloat("_SrcBlend", (float)UnityEngine.Rendering.BlendMode.SrcAlpha);
if (m.HasProperty("_DstBlend")) m.SetFloat("_DstBlend", (float)UnityEngine.Rendering.BlendMode.OneMinusSrcAlpha);
if (m.HasProperty("_ZWrite")) m.SetFloat("_ZWrite", 0f);
if (m.HasProperty("_AlphaClip")) m.SetFloat("_AlphaClip", 0f);
if (m.HasProperty("_Cull")) m.SetFloat("_Cull", 0f);
m.EnableKeyword("_SURFACE_TYPE_TRANSPARENT");
m.DisableKeyword("_ALPHATEST_ON");
m.SetOverrideTag("RenderType", "Transparent");
m.renderQueue = st.decalRenderQueue > 0 ? st.decalRenderQueue : 3050;
var col = new Color(c.r, c.g, c.b, Mathf.Clamp01(alpha));
if (s_hasColorProp) m.SetColor(s_colorPropId, col);
m.color = col;
return m;
}
static void BorrowIndicator(WLTelegraphSettings st, Plate p, float footprint)
{
if (string.IsNullOrEmpty(st.indicatorPrefabName)) return;
if (!IndicatorInfo.isIns || IndicatorInfo.Ins == null) { IndicatorMissCount++; return; }
var plate = p;
float want = footprint;
IndicatorInfo.Ins.Make_Indicator(st.indicatorPrefabName, go =>
{
if (go == null || !plate.inUse) return;
go.transform.SetPositionAndRotation(plate.tf.position, plate.tf.rotation);
float s = st.indicatorAutoScale ? IndicatorScale(go, want) : 1f;
go.transform.localScale = new Vector3(s, s, s);
go.SetActive(true);
plate.indicator = go;
IndicatorBorrowCount++;
});
}
/// <summary>인디케이터의 파티클 시작 크기를 「고유 지름」으로 보고, 원하는 발자국 크기에 맞는 배수를 낸다(실측 · 811s 방식의 축약).</summary>
static float IndicatorScale(GameObject go, float wantDiameter)
{
float native = 0f;
var systems = go.GetComponentsInChildren<ParticleSystem>(true);
for (int i = 0; i < systems.Length; i++)
{
var main = systems[i].main;
float sz = main.startSizeMultiplier * Mathf.Max(0.0001f, systems[i].transform.lossyScale.x);
if (sz > native) native = sz;
}
if (native <= 0.0001f) return 1f;
return Mathf.Clamp(wantDiameter / native, 0.05f, 20f);
}
static void DestroyObj(Object o)
{
if (o == null) return;
if (Application.isPlaying) Object.Destroy(o); else Object.DestroyImmediate(o);
}
}
}