From eed09cd18e0aa948504af735b9536cb4887765c2 Mon Sep 17 00:00:00 2001 From: swrring Date: Tue, 15 Sep 2026 01:55:37 +0900 Subject: [PATCH] =?UTF-8?q?[WL-816p]=20=ED=94=8C=EB=A0=88=EC=9D=B4?= =?UTF-8?q?=EC=96=B4=20=EC=82=AC=EA=B1=B0=EB=A6=AC=20=EC=9B=90=20=ED=91=9C?= =?UTF-8?q?=EC=8B=9C/=EC=86=8C=EB=A9=B8=20(#816)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 적(MobActor·보스)이 공격 사거리 안에 들어오면 플레이어 발밑에 반투명 흰색 원을 띄우고, 사거리 안에 적이 3초 이상 없으면 소멸시킨다(PD #816 원문). - 반지름 = 게임이 실제 공격 판정에 쓰는 값. MyActor.cs:44/47/222 의 reach 식을 그대로 재현하고, 런타임 NavMeshAgent.stoppingDistance(= PCActor.cs:381 m_ClassData.f_AttackRange) 의 양수 최솟값 + eStat.AttackRange_Up + WLTargetingSettings.immediateAttackMargin(0.2) 로 잡는다. 코드 상수 0(C45). - 그리기 = 815f 예고 판과 같은 URP Unlit 반투명 + 런타임 생성 원판 메시(새 셰이더·에셋 0, 드로우콜 +1). - 값 전부 SO: Assets/WL/Combat/Settings/Resources/WL/WLRangeRingSettings.asset - 원본 Assets/Script/** 0줄 · 설치는 RuntimeInitializeOnLoadMethod(AfterSceneLoad) 1점. Co-Authored-By: Claude Opus 5 (1M context) --- Assets/WL/Combat/RangeRing.meta | 8 + Assets/WL/Combat/RangeRing/WLRangeRing.cs | 382 ++++++++++++++++++ .../WL/Combat/RangeRing/WLRangeRing.cs.meta | 11 + .../Combat/RangeRing/WLRangeRingSettings.cs | 102 +++++ .../RangeRing/WLRangeRingSettings.cs.meta | 11 + .../Resources/WL/WLRangeRingSettings.asset | 37 ++ .../WL/WLRangeRingSettings.asset.meta | 8 + 7 files changed, 559 insertions(+) create mode 100644 Assets/WL/Combat/RangeRing.meta create mode 100644 Assets/WL/Combat/RangeRing/WLRangeRing.cs create mode 100644 Assets/WL/Combat/RangeRing/WLRangeRing.cs.meta create mode 100644 Assets/WL/Combat/RangeRing/WLRangeRingSettings.cs create mode 100644 Assets/WL/Combat/RangeRing/WLRangeRingSettings.cs.meta create mode 100644 Assets/WL/Combat/Settings/Resources/WL/WLRangeRingSettings.asset create mode 100644 Assets/WL/Combat/Settings/Resources/WL/WLRangeRingSettings.asset.meta diff --git a/Assets/WL/Combat/RangeRing.meta b/Assets/WL/Combat/RangeRing.meta new file mode 100644 index 000000000..7d661912a --- /dev/null +++ b/Assets/WL/Combat/RangeRing.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 816f0a1b2c3d4e5f60718293a4b5c6d7 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/WL/Combat/RangeRing/WLRangeRing.cs b/Assets/WL/Combat/RangeRing/WLRangeRing.cs new file mode 100644 index 000000000..3e2a26063 --- /dev/null +++ b/Assets/WL/Combat/RangeRing/WLRangeRing.cs @@ -0,0 +1,382 @@ +// ───────────────────────────────────────────────────────────────────────────── +// WLRangeRing.cs — 플레이어 공격 사거리 원(반투명 흰색) · 표시/소멸 +// +// PD 지시 #816 · 발주서 WL-816p +// 「적 공격 가능한 사정거리에 있을 때는 플레이어의 주변에 공격 가능한 범위를 반투명한 흰색 원이 +// 발생되고, 플레이어의 공격 사정거리 밖에 적이 3초 이상 있을 경우 흰색 원이 소멸되도록 해줘.」 +// +// ■ 반지름의 출처 (실측 · 추정 아님 · C45) +// 게임이 「지금 이 자리에서 때릴 수 있는가」를 판정하는 식은 MyActor.Get_AttackReach / Update 다: +// Assets/Script/Character/MyActor.cs:44 stop = m_NavStopDistance + eStat.AttackRange_Up +// Assets/Script/Character/MyActor.cs:207 agent.stoppingDistance = m_NavStopDistance + AttackRange_Up +// Assets/Script/Character/MyActor.cs:47 reach = stop + target.Get_ScaleSizeDist() + immediateAttackMargin +// Assets/Script/Character/MyActor.cs:222 if (distToTarget > reach) 접근 / else 즉시 공격 +// Assets/Script/Character/PCActor.cs:381 m_NavStopDistance = m_ClassData.f_AttackRange ← 테이블 SOT +// Assets/ResWork/Table/Export/ClassConfig.json f_AttackRange (클래스별 값) +// Assets/WL/Settings/Resources/WL/WLTargetingSettings.asset:19 immediateAttackMargin +// 원본(Assets/Script/**)은 1줄도 고치지 않는다. Get_AttackReach 는 private 이라 +// 같은 값을 **런타임 NavMeshAgent.stoppingDistance 에서 읽어** 쓴다. +// +// 🔴 stoppingDistance 는 상태에 따라 흔들린다 — 스킬 시전 중에는 f_SkillRange 로, +// 덩치 큰 타겟 앞에서는 (타겟 agent.radius + 0.5) 로 잠깐 커진다(MyActor.cs:211~213). +// 그래서 **양수 최솟값**(= 보정이 없는 평시 값 = f_AttackRange)을 기억해 원의 반지름으로 쓰고, +// AttackRange_Up(버프)과 immediateAttackMargin 은 매번 다시 더한다. 상수는 0 개다. +// +// ■ 그리는 법 +// URP Balanced 렌더러에 Decal 피처가 없어 DecalProjector 는 보이지 않는다 +// (Assets/WL/Combat/Telegraph/WLTelegraphSettings.cs:19). 그래서 815f 공격 예고 판과 같은 방식 — +// 런타임 생성 바닥 메시 + URP Unlit 반투명 머티리얼(TelegraphDecal.MakeMat 와 같은 세팅) 1장. +// 새 셰이더 0 · 새 에셋 0 · MeshRenderer 1 개(드로우콜 +1). +// +// ■ 섬에서는 0 +// 적(MobActor)이 사거리 안에 들어온 적이 없으면 아무 것도 그리지 않는다. +// 섬(FarmingIsland)에는 MobActor 가 없으므로 자동으로 0 이다(씬 이름 분기 없음). +// +// 🔴 어셈블리 주의: Assets/WL/Combat/ 에 .asmdef 를 만들지 말 것. +// ───────────────────────────────────────────────────────────────────────────── + +using System.Collections.Generic; +using UnityEngine; +using UnityEngine.AI; +using UnityEngine.SceneManagement; + +namespace WL.Combat.RangeRing +{ + public sealed class WLRangeRing : MonoBehaviour + { + const string RootName = "__WLRangeRing"; + + static WLRangeRing s_ins; + static bool s_sceneHooked; + + /// 진단·A/B 전용 런타임 스위치. 에셋을 건드리지 않고 원만 끈다. + public static bool RuntimeDisabled; + + // ── 검증용 실측 값(프로브가 읽는다) + public static string LastInfo = "(none)"; + public static float LastRadius; // 마지막으로 그린 반지름(m) + public static float LastBaseStop; // 관측한 stoppingDistance 최솟값(= f_AttackRange) + public static float LastShowTime = -1f; // 마지막 표시 시각(Time.time) + public static float LastHideTime = -1f; // 마지막 완전 소멸 시각(Time.time) + public static float LastInRangeTime = -1f; + public static bool Visible; + public static float Alpha; + + public static bool Exists { get { return s_ins != null; } } + + [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.AfterSceneLoad)] + static void Boot() + { + var s = WLRangeRingSettings.Instance; + if (s != null && s.enabled) Ensure(); + } + + public static void Ensure() + { + if (s_ins != null) return; + if (!Application.isPlaying) return; + var go = new GameObject(RootName); + go.hideFlags = HideFlags.DontSave; + DontDestroyOnLoad(go); + s_ins = go.AddComponent(); + if (!s_sceneHooked) { SceneManager.activeSceneChanged += OnSceneChanged; s_sceneHooked = true; } + } + + static void OnSceneChanged(Scene from, Scene to) + { + if (s_ins == null) return; + s_ins._pc = null; s_ins._agent = null; + s_ins._baseStop = 0f; + s_ins._lastInRange = -9999f; + s_ins._alpha = 0f; + s_ins.Apply(0f, 0f); + } + + // ── 상태 + PCActor _pc; + NavMeshAgent _agent; + float _baseStop; // 관측한 stoppingDistance 양수 최솟값 = f_AttackRange + float _nextPoll; + float _lastInRange = -9999f; // 마지막으로 사거리 안에 적이 있던 시각 + float _alpha; + float _radius; + bool _wasVisible; + + Transform _disc; + MeshRenderer _mr; + Material _mat; + Mesh _mesh; + int _meshSegments; + bool _meshRingOnly; + float _meshInner = -1f; + int _colorPropId; + bool _hasColorProp; + bool _shaderTried; + Shader _shader; + + void Update() { Tick(Time.time, Time.deltaTime); } + + void OnDisable() { Apply(0f, 0f); } + void OnApplicationQuit() { Apply(0f, 0f); } + + /// 프로브(배치·에디트)용 — 같은 틱을 1회 돌린다. + public static void ForceTick(float now, float delta) + { + if (s_ins != null) s_ins.Tick(now, delta); + } + + void Tick(float now, float delta) + { + var s = WLRangeRingSettings.Instance; + if (s == null || !s.enabled || RuntimeDisabled) { FadeTo(0f, now, delta, 0f); return; } + + // ── 적 검사(주기) ───────────────────────────────────────────── + if (now >= _nextPoll) + { + _nextPoll = now + Mathf.Max(0.02f, s.pollInterval); + Poll(s, now); + } + + // ── 표시 여부 : 사거리 안에 적이 있었고, 그 뒤 graceSeconds 가 지나지 않았다 ── + float grace = Mathf.Max(0f, s.graceSeconds); + bool want = _pc != null && !_pc.IsDead() && _radius > 0f && + _lastInRange > -9998f && (now - _lastInRange) <= grace; + + FadeTo(want ? Mathf.Clamp01(s.color.a) : 0f, now, delta, want ? s.fadeIn : s.fadeOut); + + // ── 위치 추종(매 프레임) ───────────────────────────────────── + if (_alpha > 0f && _pc != null && _disc != null) + { + EnsureMesh(s); + var p = _pc.Get_position(); + _disc.position = new Vector3(p.x, p.y + s.yOffset, p.z); + _disc.localScale = new Vector3(_radius, 1f, _radius); + LastRadius = _radius; + } + } + + void Poll(WLRangeRingSettings s, float now) + { + // ① 메인 PC + var pc = MyValue.MyPC; + if (pc == null || pc.IsDead()) { _pc = null; _agent = null; return; } + if (!ReferenceEquals(pc, _pc)) { _pc = pc; _agent = pc.Get_Agent(); _baseStop = 0f; } + if (_agent == null) _agent = pc.Get_Agent(); + + // ② 사거리(실측) — stoppingDistance 의 양수 최솟값 = 보정 없는 f_AttackRange + if (_agent != null) + { + float sd = _agent.stoppingDistance; + if (sd > 0.01f && (_baseStop <= 0f || sd < _baseStop)) _baseStop = sd; + } + if (_baseStop <= 0f) { _radius = 0f; return; } + + float up = 0f; + var stat = pc.Get_StatInfo(); + if (stat != null) up = (float)stat.Get_Stat(eStat.AttackRange_Up); + float margin = WL.Settings.WLTargetingSettings.ImmediateAttackMargin; + + float baseReach = _baseStop + up; // MyActor.cs:44 의 stop + float drawReach = baseReach + margin; // MyActor.cs:47 의 reach (타겟 보정 제외) + _radius = Mathf.Max(0f, drawReach * s.radiusScale + s.radiusBonus); + LastBaseStop = _baseStop; + + // ③ 사거리 안에 살아 있는 적(MobActor · 보스 포함)이 있는가 + var info = ActorInfo.Ins; + if (info == null) return; + float search = _radius + Mathf.Max(0f, s.searchPad) + 1f; + List list = info.Get_EnemyActors(pc.IsEnemy(), pc.Get_position(), search); + bool inRange = false; + if (list != null) + { + var me = pc.Get_position(); + for (int i = 0; i < list.Count; i++) + { + var a = list[i]; + if (a == null || a.IsDead()) continue; + if (!(a is MobActor)) continue; // 적 = MobActor(보스 포함) + + // MyActor.Get_AttackReach(MyActor.cs:42~48) 와 같은 식 + float stop = baseReach; + var ag = a.Get_Agent(); + if (ag != null && ag.radius > stop) stop = ag.radius + 0.5f; + float reach = stop + a.Get_ScaleSizeDist() + margin; + + var d = a.Get_position() - me; + d.y = 0f; + if (d.sqrMagnitude <= reach * reach) { inRange = true; break; } + } + info.Release_Actors(list); + } + + if (inRange) { _lastInRange = now; LastInRangeTime = now; } + } + + void FadeTo(float target, float now, float delta, float duration) + { + float prev = _alpha; + if (duration <= 0.0001f) _alpha = target; + else _alpha = Mathf.MoveTowards(_alpha, target, delta / duration); + + if (prev <= 0f && _alpha > 0f) + { + LastShowTime = now; _wasVisible = true; Visible = true; + var s = WLRangeRingSettings.Instance; + if (s != null && s.verboseLog) + Debug.Log(string.Format("[WL-816p] 사거리 원 표시 t={0:F2} r={1:F2}m base={2:F2}", now, _radius, _baseStop)); + } + else if (prev > 0f && _alpha <= 0f && _wasVisible) + { + LastHideTime = now; _wasVisible = false; Visible = false; + var s = WLRangeRingSettings.Instance; + if (s != null && s.verboseLog) + Debug.Log(string.Format("[WL-816p] 사거리 원 소멸 t={0:F2} (마지막 사거리 안 t={1:F2} · 경과 {2:F2}s)", + now, LastInRangeTime, now - LastInRangeTime)); + } + Alpha = _alpha; + Apply(_alpha, now); + } + + void Apply(float alpha, float now) + { + if (alpha <= 0f) + { + if (_mr != null && _mr.enabled) _mr.enabled = false; + return; + } + var s = WLRangeRingSettings.Instance; + if (s == null) return; + if (!EnsureVisual(s)) return; + _mr.enabled = true; + var c = new Color(s.color.r, s.color.g, s.color.b, alpha); + if (_hasColorProp) _mat.SetColor(_colorPropId, c); + _mat.color = c; + } + + // ── 비주얼 생성 (815f TelegraphDecal 과 같은 방식) ─────────────── + bool EnsureVisual(WLRangeRingSettings s) + { + if (_mr != null && _mat != null) { EnsureMesh(s); return true; } + if (!EnsureShader(s)) return false; + + var go = new GameObject("Ring"); + go.hideFlags = HideFlags.DontSave; + go.transform.SetParent(transform, false); + var mf = go.AddComponent(); + _mr = go.AddComponent(); + _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; + + _mat = new Material(_shader); + _mat.name = "__WLRangeRingMat"; + _mat.hideFlags = HideFlags.DontSave; + // URP Unlit 반투명 세팅(런타임) — 렌더링 배관이라 SO 튜닝 값이 아니다(TelegraphDecal.MakeMat 와 동일). + if (_mat.HasProperty("_Surface")) _mat.SetFloat("_Surface", 1f); + if (_mat.HasProperty("_Blend")) _mat.SetFloat("_Blend", 0f); + if (_mat.HasProperty("_SrcBlend")) _mat.SetFloat("_SrcBlend", (float)UnityEngine.Rendering.BlendMode.SrcAlpha); + if (_mat.HasProperty("_DstBlend")) _mat.SetFloat("_DstBlend", (float)UnityEngine.Rendering.BlendMode.OneMinusSrcAlpha); + if (_mat.HasProperty("_ZWrite")) _mat.SetFloat("_ZWrite", 0f); + if (_mat.HasProperty("_AlphaClip")) _mat.SetFloat("_AlphaClip", 0f); + if (_mat.HasProperty("_Cull")) _mat.SetFloat("_Cull", 0f); + _mat.EnableKeyword("_SURFACE_TYPE_TRANSPARENT"); + _mat.DisableKeyword("_ALPHATEST_ON"); + _mat.SetOverrideTag("RenderType", "Transparent"); + _mat.renderQueue = s.renderQueue > 0 ? s.renderQueue : 3040; + _mr.sharedMaterial = _mat; + + _disc = go.transform; + _meshInner = -1f; + EnsureMesh(s); + mf.sharedMesh = _mesh; + LastInfo = "셰이더 " + _shader.name + " · 색 프로퍼티 " + (_hasColorProp ? "O" : "X"); + return true; + } + + bool EnsureShader(WLRangeRingSettings s) + { + if (_shader != null) return true; + if (_shaderTried) return false; + _shaderTried = true; + var names = s.shaderNames; + if (names != null) + for (int i = 0; i < names.Length && _shader == null; i++) + if (!string.IsNullOrEmpty(names[i])) _shader = Shader.Find(names[i]); + if (_shader == null) { LastInfo = "셰이더 후보를 못 찾음 — 사거리 원 비활성"; return false; } + + var props = s.colorProperties; + if (props != null) + for (int i = 0; i < props.Length; i++) + if (!string.IsNullOrEmpty(props[i]) && _shader.FindPropertyIndex(props[i]) >= 0) + { _colorPropId = Shader.PropertyToID(props[i]); _hasColorProp = true; break; } + return true; + } + + /// 반지름 1 의 XZ 원판(또는 링) 메시. 스케일로 실제 반지름을 준다. + void EnsureMesh(WLRangeRingSettings s) + { + int seg = Mathf.Clamp(s.segments, 12, 256); + float inner = 0f; + if (s.ringOnly && _radius > 0.01f) + inner = Mathf.Clamp01(1f - Mathf.Max(0.01f, s.ringWidth) / _radius); + inner = Mathf.Round(inner * 100f) / 100f; // 미세 변화로 매 프레임 재생성하지 않는다 + + if (_mesh != null && seg == _meshSegments && s.ringOnly == _meshRingOnly && Mathf.Approximately(inner, _meshInner)) + return; + + _meshSegments = seg; _meshRingOnly = s.ringOnly; _meshInner = inner; + if (_mesh == null) + { + _mesh = new Mesh(); + _mesh.name = "__WLRangeRingMesh"; + _mesh.hideFlags = HideFlags.DontSave; + } + _mesh.Clear(); + + if (!s.ringOnly || inner <= 0f) + { + var v = new Vector3[seg + 1]; + var uv = new Vector2[seg + 1]; + var n = new Vector3[seg + 1]; + var t = new int[seg * 3]; + v[0] = Vector3.zero; uv[0] = new Vector2(0.5f, 0.5f); n[0] = Vector3.up; + for (int i = 0; i < seg; i++) + { + float a = (float)i / seg * Mathf.PI * 2f; + float cx = Mathf.Cos(a), cz = Mathf.Sin(a); + v[i + 1] = new Vector3(cx, 0f, cz); + uv[i + 1] = new Vector2(cx * 0.5f + 0.5f, cz * 0.5f + 0.5f); + n[i + 1] = Vector3.up; + t[i * 3] = 0; t[i * 3 + 1] = i + 1; t[i * 3 + 2] = (i + 1) % seg + 1; + } + _mesh.vertices = v; _mesh.uv = uv; _mesh.normals = n; _mesh.triangles = t; + } + else + { + var v = new Vector3[seg * 2]; + var uv = new Vector2[seg * 2]; + var n = new Vector3[seg * 2]; + var t = new int[seg * 6]; + for (int i = 0; i < seg; i++) + { + float a = (float)i / seg * Mathf.PI * 2f; + float cx = Mathf.Cos(a), cz = Mathf.Sin(a); + v[i * 2] = new Vector3(cx * inner, 0f, cz * inner); + v[i * 2 + 1] = new Vector3(cx, 0f, cz); + uv[i * 2] = new Vector2(0f, (float)i / seg); + uv[i * 2 + 1] = new Vector2(1f, (float)i / seg); + n[i * 2] = Vector3.up; n[i * 2 + 1] = Vector3.up; + int j = (i + 1) % seg; + t[i * 6] = i * 2; t[i * 6 + 1] = i * 2 + 1; t[i * 6 + 2] = j * 2 + 1; + t[i * 6 + 3] = i * 2; t[i * 6 + 4] = j * 2 + 1; t[i * 6 + 5] = j * 2; + } + _mesh.vertices = v; _mesh.uv = uv; _mesh.normals = n; _mesh.triangles = t; + } + _mesh.RecalculateBounds(); + var mf = _disc != null ? _disc.GetComponent() : null; + if (mf != null) mf.sharedMesh = _mesh; + } + } +} diff --git a/Assets/WL/Combat/RangeRing/WLRangeRing.cs.meta b/Assets/WL/Combat/RangeRing/WLRangeRing.cs.meta new file mode 100644 index 000000000..8bbefd539 --- /dev/null +++ b/Assets/WL/Combat/RangeRing/WLRangeRing.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 816e1a2b3c4d5e6f708192a3b4c5d6e7 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/WL/Combat/RangeRing/WLRangeRingSettings.cs b/Assets/WL/Combat/RangeRing/WLRangeRingSettings.cs new file mode 100644 index 000000000..34c2d0e02 --- /dev/null +++ b/Assets/WL/Combat/RangeRing/WLRangeRingSettings.cs @@ -0,0 +1,102 @@ +// ───────────────────────────────────────────────────────────────────────────── +// WLRangeRingSettings.cs — 플레이어 공격 사거리 원(반투명 흰색)의 튜닝 값(SO) +// +// PD 지시 #816 · 발주서 WL-816p +// 「적 공격 가능한 사정거리에 있을 때는 플레이어의 주변에 공격 가능한 범위를 반투명한 흰색 원이 +// 발생되고, 플레이어의 공격 사정거리 밖에 적이 3초 이상 있을 경우 흰색 원이 소멸되도록 해줘.」 +// +// 에셋 경로(고정): Assets/WL/Combat/Settings/Resources/WL/WLRangeRingSettings.asset +// → Resources.Load("WL/WLRangeRingSettings") 로 1회 로드 후 캐시. +// 에셋이 없거나 enabled = false 면 WLRangeRing 은 아무 것도 하지 않는다(원본 동작 100 %). +// +// 🔴 C45 — 사거리(반지름)는 이 에셋이 갖지 않는다. 게임이 실제 공격 판정에 쓰는 값 +// (NavMeshAgent.stoppingDistance = table_classconfig.f_AttackRange + eStat.AttackRange_Up, +// + WLTargetingSettings.immediateAttackMargin)을 런타임에 읽는다. 여기 있는 것은 배율·가산뿐이다. +// +// 🔴 어셈블리 주의: Assets/WL/Combat/ 에 .asmdef 를 만들지 말 것. +// ───────────────────────────────────────────────────────────────────────────── + +using UnityEngine; + +namespace WL.Combat.RangeRing +{ + [CreateAssetMenu(fileName = "WLRangeRingSettings", menuName = "WL/Range Ring Settings", order = 327)] + public sealed class WLRangeRingSettings : ScriptableObject + { + public const string ResourcesPath = "WL/WLRangeRingSettings"; + + static WLRangeRingSettings s_cached; + static bool s_lookupDone; + + public static WLRangeRingSettings Instance + { + get + { + if (!s_lookupDone) + { + s_cached = Resources.Load(ResourcesPath); + s_lookupDone = true; + } + return s_cached; + } + } + + /// 테스트용 — 에셋 캐시를 버린다(에디터에서 값을 바꾸고 다시 읽을 때). + public static void ClearCache() { s_cached = null; s_lookupDone = false; } + + [Header("① 켜기")] + [Tooltip("0 이면 원을 만들지 않는다(원본 동작 100 %).")] + public bool enabled = true; + + [Tooltip("1 이면 표시/소멸 시각을 Debug.Log 로 남긴다(검증용).")] + public bool verboseLog = false; + + [Header("② 모양 · 색")] + [Tooltip("원의 색. a = 반투명 정도(PD: 반투명한 흰색).")] + public Color color = new Color(1f, 1f, 1f, 0.25f); + + [Tooltip("1 이면 테두리 링만, 0 이면 원판을 채운다.")] + public bool ringOnly = false; + + [Tooltip("ringOnly = 1 일 때 링 두께(m).")] + public float ringWidth = 0.18f; + + [Tooltip("원의 분할 수(많을수록 매끈하고 삼각형이 늘어난다). 모바일 예산 = 64.")] + [Range(12, 256)] public int segments = 64; + + [Tooltip("바닥에서 띄우는 높이(m). Z-fighting 방지.")] + public float yOffset = 0.03f; + + [Tooltip("렌더 큐(반투명 · 3000 이상). 지면 위·캐릭터 아래.")] + public int renderQueue = 3040; + + [Tooltip("셰이더 후보(앞에서부터 Shader.Find).")] + public string[] shaderNames = new[] { "Universal Render Pipeline/Unlit", "Unlit/Transparent", "Sprites/Default" }; + + [Tooltip("색 프로퍼티 후보(앞에서부터 FindPropertyIndex).")] + public string[] colorProperties = new[] { "_BaseColor", "_Color", "_TintColor" }; + + [Header("③ 반지름 (SOT = 게임의 공격 판정 값 · 여기는 보정만)")] + [Tooltip("실측 사거리에 곱하는 배율. 1 = 판정과 같은 크기.")] + public float radiusScale = 1f; + + [Tooltip("실측 사거리에 더하는 값(m). 0 = 판정과 같은 크기.")] + public float radiusBonus = 0f; + + [Tooltip("적 탐색 반경에 더하는 여유(m). 사거리 + 이 값 안의 적만 검사한다(전수 스캔 방지).")] + public float searchPad = 2f; + + [Header("④ 시간")] + [Tooltip("사거리 안에 적이 없어진 뒤 원이 버티는 시간(s). PD: 3초.")] + public float graceSeconds = 3f; + + [Tooltip("나타나는 시간(s).")] + public float fadeIn = 0.15f; + + [Tooltip("사라지는 시간(s).")] + public float fadeOut = 0.4f; + + [Tooltip("적 검사 주기(s). 위치 추종은 매 프레임이라 이 값과 무관하다.")] + public float pollInterval = 0.1f; + } +} diff --git a/Assets/WL/Combat/RangeRing/WLRangeRingSettings.cs.meta b/Assets/WL/Combat/RangeRing/WLRangeRingSettings.cs.meta new file mode 100644 index 000000000..f2c7bef58 --- /dev/null +++ b/Assets/WL/Combat/RangeRing/WLRangeRingSettings.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 816d2a3b4c5d6e7f8091a2b3c4d5e6f7 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/WL/Combat/Settings/Resources/WL/WLRangeRingSettings.asset b/Assets/WL/Combat/Settings/Resources/WL/WLRangeRingSettings.asset new file mode 100644 index 000000000..b88311f94 --- /dev/null +++ b/Assets/WL/Combat/Settings/Resources/WL/WLRangeRingSettings.asset @@ -0,0 +1,37 @@ +%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: 816d2a3b4c5d6e7f8091a2b3c4d5e6f7, type: 3} + m_Name: WLRangeRingSettings + m_EditorClassIdentifier: Assembly-CSharp::WL.Combat.RangeRing.WLRangeRingSettings + enabled: 1 + verboseLog: 1 + color: {r: 1, g: 1, b: 1, a: 0.25} + ringOnly: 0 + ringWidth: 0.18 + segments: 64 + yOffset: 0.03 + renderQueue: 3040 + shaderNames: + - Universal Render Pipeline/Unlit + - Unlit/Transparent + - Sprites/Default + colorProperties: + - _BaseColor + - _Color + - _TintColor + radiusScale: 1 + radiusBonus: 0 + searchPad: 2 + graceSeconds: 3 + fadeIn: 0.15 + fadeOut: 0.4 + pollInterval: 0.1 diff --git a/Assets/WL/Combat/Settings/Resources/WL/WLRangeRingSettings.asset.meta b/Assets/WL/Combat/Settings/Resources/WL/WLRangeRingSettings.asset.meta new file mode 100644 index 000000000..3a7d94845 --- /dev/null +++ b/Assets/WL/Combat/Settings/Resources/WL/WLRangeRingSettings.asset.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 816c3a4b5c6d7e8f9012a3b4c5d6e7f8 +NativeFormatImporter: + externalObjects: {} + mainObjectFileID: 11400000 + userData: + assetBundleName: + assetBundleVariant: