423 lines
14 KiB
C#
423 lines
14 KiB
C#
using System.Collections;
|
|
using System.Collections.Generic;
|
|
using UnityEngine;
|
|
|
|
namespace Streamingle.Contents.BossRaid
|
|
{
|
|
/// <summary>
|
|
/// 보스 모델의 비주얼 이펙트를 관리합니다.
|
|
/// 머티리얼에 직접 접근하여 피격 플래시, 색상 틴트, 등장/사망 연출.
|
|
/// </summary>
|
|
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<MatOriginal> _originals = new List<MatOriginal>();
|
|
|
|
// 셰이더 프로퍼티 (여러 셰이더 호환)
|
|
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<Renderer>();
|
|
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);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// 모든 머티리얼의 색상과 이미션을 보간합니다.
|
|
/// tintT: 0=원본 색상, 1=tintColor로 완전 대체
|
|
/// </summary>
|
|
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);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// 모든 머티리얼을 강제로 특정 색상으로 덮어씁니다. (플래시용)
|
|
/// </summary>
|
|
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
|
|
}
|
|
}
|