388 lines
18 KiB
C#
388 lines
18 KiB
C#
// ─────────────────────────────────────────────────────────────────────────────
|
||
// 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);
|
||
}
|
||
}
|
||
}
|