172 lines
8.9 KiB
C#
172 lines
8.9 KiB
C#
// ─────────────────────────────────────────────────────────────────────────────
|
|
// WLPartSkillFx.cs — 파츠 스킬 이펙트 풀 (PD 지시 #879 · 2026-10-16)
|
|
//
|
|
// 보유 VFX(MasterStylizedFX · EricWang · StylizedFantasyFxPack — 힘민지/WL 레포 안 에셋)를 Resources/WL/PartSkill/Fx/ 로 복사해 쓴다
|
|
// (복사 = AgentScripts/WL879_PartSkillAssets.cs · 원본 경로 표 = Tools/PartSkill/partskill_assets.json).
|
|
// 모바일: 키마다 풀(fxPoolPerKey) · 파티클 stopAction 끔(풀이 재사용) · 크기는 Hierarchy 스케일 · 프리팹이 없으면 조용히 건너뛴다(기능은 그대로).
|
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
|
|
using System.Collections.Generic;
|
|
using UnityEngine;
|
|
|
|
namespace WL.PartSkill
|
|
{
|
|
public static class WLPartSkillFx
|
|
{
|
|
static readonly Dictionary<string, GameObject> s_prefabs = new Dictionary<string, GameObject>();
|
|
static readonly Dictionary<string, Stack<GameObject>> s_pool = new Dictionary<string, Stack<GameObject>>();
|
|
static readonly Dictionary<GameObject, string> s_keyOf = new Dictionary<GameObject, string>();
|
|
static readonly Dictionary<GameObject, ParticleSystem[]> s_ps = new Dictionary<GameObject, ParticleSystem[]>();
|
|
static readonly Dictionary<GameObject, Vector3> s_baseScale = new Dictionary<GameObject, Vector3>();
|
|
struct Timed { public GameObject go; public float at; }
|
|
static readonly List<Timed> s_timed = new List<Timed>(64);
|
|
// #881 켜져 있는 인스턴스 전부 — 스킬 코루틴이 장비 변경(StopAllCoroutines)으로 끊기면 끝에서 Stop 을 못 해
|
|
// 역병 장판 등이 영구히 남았다(실측). 실행기 정리 때 ReleaseAll 이 이 목록을 모두 거둔다.
|
|
static readonly HashSet<GameObject> s_live = new HashSet<GameObject>();
|
|
static Transform s_root;
|
|
public static int Spawned, Reused, MissingPrefab, Live, NullMatHidden;
|
|
public static readonly List<string> MissingKeys = new List<string>();
|
|
|
|
static Transform Root()
|
|
{
|
|
if (s_root == null)
|
|
{
|
|
var go = new GameObject("WL_PartSkillFx");
|
|
Object.DontDestroyOnLoad(go);
|
|
s_root = go.transform;
|
|
}
|
|
return s_root;
|
|
}
|
|
|
|
public static GameObject Prefab(string key)
|
|
{
|
|
if (string.IsNullOrEmpty(key)) return null;
|
|
GameObject p;
|
|
if (s_prefabs.TryGetValue(key, out p)) return p;
|
|
p = Resources.Load<GameObject>(WLPartSkillData.S.fxFolder + key);
|
|
s_prefabs[key] = p;
|
|
if (p == null) { MissingPrefab++; if (MissingKeys.Count < 40) MissingKeys.Add(key); }
|
|
return p;
|
|
}
|
|
|
|
/// <summary>인스턴스 하나(활성 · 재생 시작). 호출자가 Release 로 돌려준다.</summary>
|
|
public static GameObject Get(string key, Vector3 pos, Quaternion rot, float scale)
|
|
{
|
|
var prefab = Prefab(key);
|
|
if (prefab == null) return null;
|
|
GameObject go = null;
|
|
Stack<GameObject> st;
|
|
if (s_pool.TryGetValue(key, out st))
|
|
while (st.Count > 0 && go == null) go = st.Pop(); // 파괴된 항목(씬 정리 등)은 건너뛴다
|
|
if (go == null)
|
|
{
|
|
go = Object.Instantiate(prefab, Root());
|
|
go.name = "PS_" + key;
|
|
s_keyOf[go] = key;
|
|
s_baseScale[go] = prefab.transform.localScale;
|
|
var ps = go.GetComponentsInChildren<ParticleSystem>(true);
|
|
for (int i = 0; i < ps.Length; i++)
|
|
{
|
|
var m = ps[i].main;
|
|
m.stopAction = ParticleSystemStopAction.None; // 풀이 다시 쓴다(자체 파괴 금지)
|
|
m.scalingMode = ParticleSystemScalingMode.Hierarchy; // 루트 배율로 크기 조절
|
|
}
|
|
s_ps[go] = ps;
|
|
foreach (var c in go.GetComponentsInChildren<Collider>(true)) c.enabled = false; // 연출 전용(물리 간섭 0)
|
|
// #881 원본 팩 재질이 이 프로젝트에 없는 렌더러(재질 빈칸)는 분홍으로만 그려진다 → 끈다
|
|
// (실측: fx_hit_star 의 Ring(1)·Ring(2)·CoreRound(2) 가 큰 분홍 덩어리 · 이펙트 39 중 빈칸 6)
|
|
foreach (var r in go.GetComponentsInChildren<Renderer>(true))
|
|
{
|
|
if (r.sharedMaterial == null) { r.enabled = false; NullMatHidden++; continue; }
|
|
var psr = r as ParticleSystemRenderer;
|
|
var rps = psr != null ? r.GetComponent<ParticleSystem>() : null;
|
|
if (rps != null && rps.trails.enabled && psr.trailMaterial == null) { var tr = rps.trails; tr.enabled = false; NullMatHidden++; }
|
|
}
|
|
List<string> hide;
|
|
var fh = WLPartSkillData.S.fxHide;
|
|
if (fh != null && fh.TryGetValue(key, out hide) && hide != null)
|
|
foreach (var t2 in go.GetComponentsInChildren<Transform>(true))
|
|
if (t2 != go.transform && hide.Contains(t2.name)) t2.gameObject.SetActive(false);
|
|
ps = go.GetComponentsInChildren<ParticleSystem>(false); // 끈 하위는 재생 목록에서 뺀다
|
|
s_ps[go] = ps;
|
|
Spawned++;
|
|
}
|
|
else Reused++;
|
|
var t = go.transform;
|
|
t.SetPositionAndRotation(pos, rot);
|
|
Vector3 bs; if (!s_baseScale.TryGetValue(go, out bs)) bs = Vector3.one;
|
|
t.localScale = bs * Mathf.Max(0.01f, scale);
|
|
go.SetActive(true);
|
|
s_live.Add(go);
|
|
ParticleSystem[] arr;
|
|
if (s_ps.TryGetValue(go, out arr))
|
|
for (int i = 0; i < arr.Length; i++) { if (arr[i] == null) continue; arr[i].Clear(true); arr[i].Play(true); }
|
|
Live++;
|
|
return go;
|
|
}
|
|
|
|
/// <summary>한 번 재생 후 life 초 뒤 자동 반환.</summary>
|
|
public static GameObject Play(string key, Vector3 pos, Quaternion rot, float scale, float life)
|
|
{
|
|
var go = Get(key, pos, rot, scale);
|
|
if (go != null) ReleaseLater(go, life);
|
|
return go;
|
|
}
|
|
|
|
public static GameObject Play(string key, Vector3 pos, float scale, float life) { return Play(key, pos, Quaternion.identity, scale, life); }
|
|
|
|
public static void ReleaseLater(GameObject go, float delay)
|
|
{
|
|
if (go == null) return;
|
|
s_timed.Add(new Timed { go = go, at = Time.time + Mathf.Max(0f, delay) });
|
|
}
|
|
|
|
/// <summary>방출을 멈추고 fade 초 뒤 풀로(남은 입자가 자연스럽게 사라지게).</summary>
|
|
public static void Stop(GameObject go, float fade)
|
|
{
|
|
if (go == null) return;
|
|
ParticleSystem[] arr;
|
|
if (s_ps.TryGetValue(go, out arr)) for (int i = 0; i < arr.Length; i++) if (arr[i] != null) arr[i].Stop(true, ParticleSystemStopBehavior.StopEmitting);
|
|
ReleaseLater(go, fade);
|
|
}
|
|
|
|
public static void Release(GameObject go)
|
|
{
|
|
if (go == null) return;
|
|
s_live.Remove(go);
|
|
string key;
|
|
if (!s_keyOf.TryGetValue(go, out key)) { Object.Destroy(go); return; }
|
|
if (!go.activeSelf) return; // 이미 반환됨
|
|
go.SetActive(false);
|
|
Live = Mathf.Max(0, Live - 1);
|
|
Stack<GameObject> st;
|
|
if (!s_pool.TryGetValue(key, out st)) s_pool[key] = st = new Stack<GameObject>();
|
|
if (st.Count < Mathf.Max(1, WLPartSkillData.S.fxPoolPerKey)) st.Push(go);
|
|
else { s_keyOf.Remove(go); s_ps.Remove(go); s_baseScale.Remove(go); Object.Destroy(go); }
|
|
}
|
|
|
|
/// <summary>러너 Update 가 부른다 — 시간 지난 것 반환.</summary>
|
|
public static void Tick()
|
|
{
|
|
float now = Time.time;
|
|
for (int i = s_timed.Count - 1; i >= 0; i--)
|
|
{
|
|
if (s_timed[i].at > now) continue;
|
|
var go = s_timed[i].go;
|
|
s_timed.RemoveAt(i);
|
|
if (go != null) Release(go);
|
|
}
|
|
}
|
|
|
|
/// <summary>켜져 있는 이펙트를 전부 풀로 돌린다(예약 반환 포함) — 실행기 정리 · 장비 변경 때.</summary>
|
|
public static void ReleaseAll()
|
|
{
|
|
s_timed.Clear();
|
|
if (s_live.Count == 0) return;
|
|
var arr = new GameObject[s_live.Count]; s_live.CopyTo(arr);
|
|
for (int i = 0; i < arr.Length; i++) if (arr[i] != null) Release(arr[i]);
|
|
s_live.Clear();
|
|
}
|
|
}
|
|
}
|