using System.Collections; using System.Collections.Generic; using UnityEngine; namespace Streamingle.Contents.BossRaid { /// /// 보스 모델의 비주얼 이펙트를 관리합니다. /// 머티리얼에 직접 접근하여 피격 플래시, 색상 틴트, 등장/사망 연출. /// public class BossVisualEffect : MonoBehaviour { #region Fields [Header("피격 플래시")] [SerializeField] private float flashDuration = 0.12f; [SerializeField] private Color flashColor = Color.white; [SerializeField] [Tooltip("크리티컬 피격 색상")] private Color critFlashColor = new Color(1f, 0.3f, 0.1f); [SerializeField] [Range(0f, 3f)] [Tooltip("피격 시 이미션 강도")] private float flashEmissionIntensity = 2f; [Header("페이즈 틴트")] [SerializeField] [Range(0f, 1f)] [Tooltip("페이즈 색상 틴트 강도 (0=원본, 1=완전 틴트)")] private float phaseTintStrength = 0.3f; [Header("사망 연출")] [SerializeField] private int deathBlinkCount = 6; [SerializeField] private float deathBlinkInterval = 0.15f; [SerializeField] private Color deathTintColor = new Color(0.3f, 0.3f, 0.3f); private Renderer[] _renderers; private Color _baseTint = Color.white; private Coroutine _flashCoroutine; private Coroutine _deathCoroutine; private Coroutine _shakeCoroutine; private Coroutine _phasePulseCoroutine; // 원본 머티리얼 색상 저장 private struct MatOriginal { public Material mat; public Color color; public Color emission; public bool hasColor; public bool hasEmission; } private List _originals = new List(); // 셰이더 프로퍼티 (여러 셰이더 호환) private static readonly int[] ColorProps = { Shader.PropertyToID("_Color"), Shader.PropertyToID("_BaseColor"), Shader.PropertyToID("_MainColor"), }; private static readonly int[] EmissionProps = { Shader.PropertyToID("_EmissionColor"), Shader.PropertyToID("_EmissiveColor"), }; #endregion #region Unity Messages private void Awake() { _renderers = GetComponentsInChildren(); CacheOriginalColors(); } private void OnDestroy() { RestoreOriginalColors(); } #endregion #region Public Methods public void PlayAppearAnimation() { StartCoroutine(AppearCoroutine()); } public void PlayHitFlash(bool isCritical) { if (_flashCoroutine != null) StopCoroutine(_flashCoroutine); Color color = isCritical ? critFlashColor : flashColor; float duration = isCritical ? flashDuration * 1.8f : flashDuration; float emissionMult = isCritical ? flashEmissionIntensity * 1.5f : flashEmissionIntensity; _flashCoroutine = StartCoroutine(FlashCoroutine(color, duration, emissionMult)); // 모델 흔들림 float intensity = isCritical ? 0.15f : 0.08f; float dur = isCritical ? 0.2f : 0.12f; if (_shakeCoroutine != null) StopCoroutine(_shakeCoroutine); _shakeCoroutine = StartCoroutine(ModelShakeCoroutine(intensity, dur)); } public void PlayDeathAnimation() { if (_deathCoroutine != null) StopCoroutine(_deathCoroutine); _deathCoroutine = StartCoroutine(DeathCoroutine()); } public void SetColorTint(Color color) { _baseTint = color; // 페이즈 펄스 시작 if (_phasePulseCoroutine != null) StopCoroutine(_phasePulseCoroutine); if (color != Color.white) _phasePulseCoroutine = StartCoroutine(PhasePulseCoroutine(color)); else LerpMaterialColors(Color.white, Color.black, 0f); } #endregion #region Private Methods - Material private void CacheOriginalColors() { _originals.Clear(); if (_renderers == null) return; foreach (var r in _renderers) { if (r == null) continue; foreach (var mat in r.materials) { var orig = new MatOriginal { mat = mat }; // 컬러 프로퍼티 찾기 foreach (var prop in ColorProps) { if (mat.HasProperty(prop)) { orig.color = mat.GetColor(prop); orig.hasColor = true; break; } } // 이미션 프로퍼티 찾기 foreach (var prop in EmissionProps) { if (mat.HasProperty(prop)) { orig.emission = mat.GetColor(prop); orig.hasEmission = true; break; } } _originals.Add(orig); } } } private void RestoreOriginalColors() { foreach (var orig in _originals) { if (orig.mat == null) continue; if (orig.hasColor) SetMatColor(orig.mat, orig.color); if (orig.hasEmission) SetMatEmission(orig.mat, orig.emission); } } /// /// 모든 머티리얼의 색상과 이미션을 보간합니다. /// tintT: 0=원본 색상, 1=tintColor로 완전 대체 /// private void LerpMaterialColors(Color tintColor, Color emissionColor, float tintT) { foreach (var orig in _originals) { if (orig.mat == null) continue; if (orig.hasColor) { Color c = Color.Lerp(orig.color, tintColor, tintT); SetMatColor(orig.mat, c); } if (orig.hasEmission) SetMatEmission(orig.mat, emissionColor); } } /// /// 모든 머티리얼을 강제로 특정 색상으로 덮어씁니다. (플래시용) /// private void OverrideMaterialColors(Color color, Color emission) { foreach (var orig in _originals) { if (orig.mat == null) continue; if (orig.hasColor) SetMatColor(orig.mat, color); if (orig.hasEmission) SetMatEmission(orig.mat, emission); } } private void SetMatColor(Material mat, Color color) { foreach (var prop in ColorProps) { if (mat.HasProperty(prop)) { mat.SetColor(prop, color); return; } } } private void SetMatEmission(Material mat, Color color) { foreach (var prop in EmissionProps) { if (mat.HasProperty(prop)) { mat.SetColor(prop, color); if (color != Color.black) mat.EnableKeyword("_EMISSION"); return; } } } #endregion #region Private Methods - Coroutines private IEnumerator AppearCoroutine() { Vector3 targetScale = transform.localScale; transform.localScale = Vector3.zero; float elapsed = 0f; float duration = 0.5f; while (elapsed < duration) { elapsed += Time.deltaTime; float t = elapsed / duration; float ease = 1f + 0.3f * Mathf.Sin(t * Mathf.PI) * (1f - t); transform.localScale = targetScale * Mathf.Min(ease * t * 1.2f, ease); yield return null; } transform.localScale = targetScale; } private IEnumerator FlashCoroutine(Color color, float duration, float emissionMult) { Color emission = color * emissionMult; // 순간 흰색/피격색으로 전환 OverrideMaterialColors(color, emission); yield return new WaitForSeconds(duration * 0.3f); // 원래 색상으로 부드럽게 복원 float elapsed = 0f; float fadeDuration = duration * 0.7f; while (elapsed < fadeDuration) { elapsed += Time.deltaTime; float t = elapsed / fadeDuration; float ease = t * t; // EaseIn // 피격색 → 원본 (또는 페이즈 틴트 적용 상태)으로 복원 foreach (var orig in _originals) { if (orig.mat == null) continue; if (orig.hasColor) { Color target = Color.Lerp(orig.color, _baseTint, phaseTintStrength); Color current = Color.Lerp(color, target, ease); SetMatColor(orig.mat, current); } if (orig.hasEmission) { Color emTarget = (_baseTint != Color.white) ? _baseTint * 0.3f : orig.emission; Color emCurrent = Color.Lerp(emission, emTarget, ease); SetMatEmission(orig.mat, emCurrent); } } yield return null; } // 최종 상태 확정 if (_baseTint != Color.white) LerpMaterialColors(_baseTint, _baseTint * 0.3f, phaseTintStrength); else RestoreOriginalColors(); _flashCoroutine = null; } private IEnumerator PhasePulseCoroutine(Color tintColor) { // 페이즈 진입 시 한 번 강하게 번쩍 OverrideMaterialColors(tintColor, tintColor * 2f); yield return new WaitForSeconds(0.15f); // 지속적 미세 펄스 (분노 모드 느낌) float time = 0f; while (true) { time += Time.deltaTime; float pulse = 0.5f + 0.5f * Mathf.Sin(time * 3f); // 천천히 밝아졌다 어두워짐 float strength = Mathf.Lerp(phaseTintStrength * 0.5f, phaseTintStrength, pulse); float emPulse = pulse * 0.4f; foreach (var orig in _originals) { if (orig.mat == null) continue; if (orig.hasColor) { Color c = Color.Lerp(orig.color, tintColor, strength); SetMatColor(orig.mat, c); } if (orig.hasEmission) { Color em = tintColor * emPulse; SetMatEmission(orig.mat, em); } } yield return null; } } private IEnumerator DeathCoroutine() { // 페이즈 펄스 정지 if (_phasePulseCoroutine != null) { StopCoroutine(_phasePulseCoroutine); _phasePulseCoroutine = null; } // 회색 틴트 LerpMaterialColors(deathTintColor, Color.black, 0.6f); // 깜빡임 for (int i = 0; i < deathBlinkCount; i++) { SetRenderersVisible(false); yield return new WaitForSeconds(deathBlinkInterval); SetRenderersVisible(true); yield return new WaitForSeconds(deathBlinkInterval * 0.7f); } // 축소하며 사라짐 Vector3 startScale = transform.localScale; float elapsed = 0f; float shrinkDuration = 0.5f; while (elapsed < shrinkDuration) { elapsed += Time.deltaTime; float t = elapsed / shrinkDuration; transform.localScale = Vector3.Lerp(startScale, Vector3.zero, t * t); // 사라지면서 점점 어두워짐 LerpMaterialColors(Color.black, Color.black, t); yield return null; } transform.localScale = Vector3.zero; _deathCoroutine = null; } private IEnumerator ModelShakeCoroutine(float intensity, float duration) { Vector3 originalPos = transform.localPosition; float elapsed = 0f; while (elapsed < duration) { elapsed += Time.deltaTime; float t = 1f - (elapsed / duration); float x = Random.Range(-intensity, intensity) * t; float y = Random.Range(-intensity, intensity) * t; transform.localPosition = originalPos + new Vector3(x, y, 0f); yield return null; } transform.localPosition = originalPos; _shakeCoroutine = null; } private void SetRenderersVisible(bool visible) { if (_renderers == null) return; foreach (var r in _renderers) { if (r != null) r.enabled = visible; } } #endregion } }