[WL-816p] 플레이어 사거리 원 표시/소멸 (#816)
적(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) <noreply@anthropic.com>
This commit is contained in:
parent
2fb0b8489e
commit
eed09cd18e
|
|
@ -0,0 +1,8 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 816f0a1b2c3d4e5f60718293a4b5c6d7
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
|
@ -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;
|
||||
|
||||
/// <summary>진단·A/B 전용 런타임 스위치. 에셋을 건드리지 않고 원만 끈다.</summary>
|
||||
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<WLRangeRing>();
|
||||
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); }
|
||||
|
||||
/// <summary>프로브(배치·에디트)용 — 같은 틱을 1회 돌린다.</summary>
|
||||
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<Actor> 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<MeshFilter>();
|
||||
_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;
|
||||
|
||||
_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;
|
||||
}
|
||||
|
||||
/// <summary>반지름 1 의 XZ 원판(또는 링) 메시. 스케일로 실제 반지름을 준다.</summary>
|
||||
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<MeshFilter>() : null;
|
||||
if (mf != null) mf.sharedMesh = _mesh;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 816e1a2b3c4d5e6f708192a3b4c5d6e7
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
|
@ -0,0 +1,102 @@
|
|||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// WLRangeRingSettings.cs — 플레이어 공격 사거리 원(반투명 흰색)의 튜닝 값(SO)
|
||||
//
|
||||
// PD 지시 #816 · 발주서 WL-816p
|
||||
// 「적 공격 가능한 사정거리에 있을 때는 플레이어의 주변에 공격 가능한 범위를 반투명한 흰색 원이
|
||||
// 발생되고, 플레이어의 공격 사정거리 밖에 적이 3초 이상 있을 경우 흰색 원이 소멸되도록 해줘.」
|
||||
//
|
||||
// 에셋 경로(고정): Assets/WL/Combat/Settings/Resources/WL/WLRangeRingSettings.asset
|
||||
// → Resources.Load<WLRangeRingSettings>("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<WLRangeRingSettings>(ResourcesPath);
|
||||
s_lookupDone = true;
|
||||
}
|
||||
return s_cached;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>테스트용 — 에셋 캐시를 버린다(에디터에서 값을 바꾸고 다시 읽을 때).</summary>
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 816d2a3b4c5d6e7f8091a2b3c4d5e6f7
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
|
@ -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
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 816c3a4b5c6d7e8f9012a3b4c5d6e7f8
|
||||
NativeFormatImporter:
|
||||
externalObjects: {}
|
||||
mainObjectFileID: 11400000
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
Loading…
Reference in New Issue