using UnityEngine; using WL.Player; namespace WL.Combat { /// /// 피격 시 대상 전신을 지정 색으로 물들였다가 되돌리는 하이라이트. /// /// 힘민지는 같은 연출을 `Actor.cs:1595-1600` · `:2045-2066` 에서 /// `Renderer.material` / `sharedMaterials` 리스트 조작으로 했다가 /// **GC Alloc 2.3KB 문제로 전량 주석 처리**했다(`Actor.cs:2046` 주석 명시 · himminji-reuse 02 §7-2). /// → WL 은 처음부터 **MaterialPropertyBlock** 으로 구현한다(머티리얼 인스턴스 생성 0 · 할당 0). /// /// 대상 셰이더 `Toon/Toon`(Unity Chan Toon Shader)의 색 프로퍼티를 실측해 /// 베이스색 2종(_BaseColor·_Color) + 그림자색 2종(_1st_ShadeColor·_2nd_ShadeColor)을 함께 덮는다. /// 베이스만 바꾸면 그늘진 면이 원래 색으로 남아 플래시가 반쪽만 보인다. /// [DisallowMultipleComponent] public class HitFlash : MonoBehaviour { private static readonly string[] ColorProps = { "_BaseColor", "_Color", "_1st_ShadeColor", "_2nd_ShadeColor" }; [Header("수치 설정 (ScriptableObject)")] [SerializeField] private CombatSettings settings; [Header("참조 (비우면 자식 렌더러 전체)")] [SerializeField] private Renderer[] renderers; private MaterialPropertyBlock _block; private int[] _propIds; private Color[][] _originals; // [renderer][prop] private bool[] _hasProp; // [prop] — 셰이더가 그 프로퍼티를 가지고 있는가 private float _timer; private bool _active; private Enemy _enemy; private void Awake() { if (renderers == null || renderers.Length == 0) renderers = GetComponentsInChildren(true); _enemy = GetComponent(); if (settings == null && _enemy != null) settings = _enemy.Settings; _block = new MaterialPropertyBlock(); _propIds = new int[ColorProps.Length]; _hasProp = new bool[ColorProps.Length]; for (int i = 0; i < ColorProps.Length; i++) _propIds[i] = Shader.PropertyToID(ColorProps[i]); _originals = new Color[renderers.Length][]; for (int r = 0; r < renderers.Length; r++) { _originals[r] = new Color[ColorProps.Length]; var m = renderers[r] != null ? renderers[r].sharedMaterial : null; for (int i = 0; i < ColorProps.Length; i++) { bool has = m != null && m.HasProperty(ColorProps[i]); _originals[r][i] = has ? m.GetColor(ColorProps[i]) : Color.white; if (has) _hasProp[i] = true; } } } private void OnEnable() { if (_enemy != null) _enemy.Damaged += OnDamaged; } private void OnDisable() { if (_enemy != null) _enemy.Damaged -= OnDamaged; Clear(); } private void OnDamaged(Enemy e, float amount) { Flash(); } /// 피격 색 플래시 시작. 재피격 시 타이머만 되감는다. public void Flash() { float dur = settings != null ? settings.hitFlashSeconds : 0.15f; if (dur <= 0f) return; _timer = dur; _active = true; Apply(1f); } private void LateUpdate() { if (!_active) return; float dur = settings != null ? settings.hitFlashSeconds : 0.15f; _timer -= Time.deltaTime; if (_timer <= 0f) { Clear(); return; } Apply(dur > 0f ? Mathf.Clamp01(_timer / dur) : 0f); } /// t = 1 이면 플래시 최대, 0 이면 원래 색. private void Apply(float t) { Color flash = settings != null ? settings.hitFlashColor : new Color(1f, 0.3f, 0.22f, 1f); float strength = (settings != null ? settings.hitFlashStrength : 0.85f) * t; for (int r = 0; r < renderers.Length; r++) { var rend = renderers[r]; if (rend == null) continue; rend.GetPropertyBlock(_block); for (int i = 0; i < _propIds.Length; i++) { if (!_hasProp[i]) continue; Color baseCol = _originals[r][i]; Color c = Color.Lerp(baseCol, flash, strength); c.a = baseCol.a; _block.SetColor(_propIds[i], c); } rend.SetPropertyBlock(_block); } } /// 덮어쓴 프로퍼티를 걷어내 원래 머티리얼 값으로 되돌린다. public void Clear() { _active = false; _timer = 0f; if (renderers == null) return; for (int r = 0; r < renderers.Length; r++) if (renderers[r] != null) renderers[r].SetPropertyBlock(null); } } }