475 lines
12 KiB
C#
475 lines
12 KiB
C#
using UnityEngine;
|
||
using DarkTonic.MasterAudio;
|
||
using DG.Tweening;
|
||
using System;
|
||
|
||
/// <summary>
|
||
/// 水晶雕像控制器 - 单个雕像的控制逻辑
|
||
/// 击打产生裂痕,使用动画帧控制
|
||
/// </summary>
|
||
public class CrystalStatue : MonoBehaviour
|
||
{
|
||
#region 配置
|
||
|
||
[Header("雕像配置")]
|
||
[Tooltip("是否为目标雕像(需要击碎的雕像)")]
|
||
public bool isTargetStatue = false;
|
||
|
||
[Header("动画配置")]
|
||
[Tooltip("裂痕动画控制器")]
|
||
public Animator crackAnimator;
|
||
|
||
[Tooltip("裂痕动画名称")]
|
||
public string crackAnimName = "Crack";
|
||
|
||
[Tooltip("动画总帧数")]
|
||
public int totalAnimFrames = 6;
|
||
|
||
[Tooltip("第一次击打目标帧")]
|
||
public int firstHitFrame = 2;
|
||
|
||
[Tooltip("第二次击打目标帧")]
|
||
public int secondHitFrame = 4;
|
||
|
||
[Header("击打配置")]
|
||
[Tooltip("击打需要的次数")]
|
||
public int hitCountToBreak = 3;
|
||
|
||
[Header("音效配置")]
|
||
[Tooltip("击打音效")]
|
||
public string hitSound = "2.1";
|
||
|
||
//[Tooltip("碎裂音效")]
|
||
//public string breakSound = "2.2";
|
||
|
||
[Tooltip("狞笑音效(击打错误时)")]
|
||
public string evilLaughSound = "2.3";
|
||
|
||
[Header("粒子效果")]
|
||
[Tooltip("击打粒子效果")]
|
||
public ParticleSystem hitParticle;
|
||
|
||
[Tooltip("碎裂粒子效果")]
|
||
public ParticleSystem breakParticle;
|
||
|
||
[Tooltip("狞笑粒子效果")]
|
||
public ParticleSystem evilLaughParticle;
|
||
|
||
public GameObject hitObj;
|
||
|
||
[Header("震动配置")]
|
||
public float hitShakeDuration = 0.2f;
|
||
public float hitShakeStrength = 0.05f;
|
||
|
||
[Header("碎裂配置")]
|
||
public float breakDuration = 1f;
|
||
public float disappearDelay = 0.3f;
|
||
|
||
[Header("调试")]
|
||
public bool debugMode = true;
|
||
|
||
#endregion
|
||
|
||
#region 状态
|
||
|
||
public enum StatueState
|
||
{
|
||
Idle, // 正常状态
|
||
Hit, // 被击打中
|
||
Breaking, // 正在碎裂
|
||
Broken, // 已碎裂
|
||
EvilLaugh // 狞笑(击打错误)
|
||
}
|
||
|
||
public StatueState currentState { get; private set; } = StatueState.Idle;
|
||
|
||
// 当前击打次数
|
||
private int currentHitCount = 0;
|
||
|
||
// 是否可被击打
|
||
private bool canHit = true;
|
||
|
||
// 是否允许显示裂痕(语音期间为false,特效出现后为true)
|
||
private bool canShowCrack = true;
|
||
|
||
// 动画哈希
|
||
private int crackAnimHash;
|
||
|
||
// 事件回调
|
||
/// <summary>
|
||
/// 雕像碎裂回调 (雕像, 是否是目标雕像)
|
||
/// </summary>
|
||
public Action<CrystalStatue, bool> OnStatueBroken;
|
||
|
||
#endregion
|
||
|
||
#region Unity生命周期
|
||
|
||
private void Awake()
|
||
{
|
||
if (crackAnimator == null)
|
||
crackAnimator = GetComponent<Animator>();
|
||
|
||
// 缓存动画哈希
|
||
crackAnimHash = Animator.StringToHash(crackAnimName);
|
||
}
|
||
|
||
private void Start()
|
||
{
|
||
currentHitCount = 0;
|
||
canHit = true;
|
||
currentState = StatueState.Idle;
|
||
|
||
// 初始化动画状态
|
||
ResetAnimatorState();
|
||
}
|
||
|
||
#endregion
|
||
|
||
#region 公开方法
|
||
|
||
/// <summary>
|
||
/// 初始化雕像
|
||
/// </summary>
|
||
/// <param name="isTarget">是否为目标雕像</param>
|
||
public void Initialize(bool isTarget)
|
||
{
|
||
isTargetStatue = isTarget;
|
||
currentHitCount = 0;
|
||
canHit = true;
|
||
currentState = StatueState.Idle;
|
||
|
||
// 重置动画
|
||
ResetAnimatorState();
|
||
|
||
// 重置缩放
|
||
transform.localScale = Vector3.one;
|
||
|
||
// 显示对象
|
||
gameObject.SetActive(true);
|
||
|
||
hitObj.SetActive(false);
|
||
if (debugMode)
|
||
Debug.Log($"[CrystalStatue] 初始化雕像: {name}, 是否目标: {isTarget}");
|
||
}
|
||
|
||
/// <summary>
|
||
/// 玩家击中雕像
|
||
/// </summary>
|
||
public void OnPlayerHit()
|
||
{
|
||
if (!canHit || currentState == StatueState.Broken)
|
||
{
|
||
if (debugMode)
|
||
Debug.Log($"[CrystalStatue] {name} 不可被击打");
|
||
return;
|
||
}
|
||
|
||
// 播放击打音效和特效(语音期间也有)
|
||
PlayHitSound();
|
||
PlayHitEffect();
|
||
|
||
// 如果不允许显示裂痕(语音期间),直接返回
|
||
if (!canShowCrack)
|
||
{
|
||
if (debugMode)
|
||
Debug.Log($"[CrystalStatue] {name} 语音期间敲打,仅播放特效不显示裂痕");
|
||
return;
|
||
}
|
||
|
||
currentHitCount++;
|
||
currentState = StatueState.Hit;
|
||
|
||
if (debugMode)
|
||
Debug.Log($"[CrystalStatue] {name} 被击打! 第{currentHitCount}次击打, 是否目标: {isTargetStatue}");
|
||
|
||
// 更新动画帧
|
||
UpdateCrackAnimation();
|
||
|
||
// 检查是否碎裂
|
||
if (currentHitCount >= hitCountToBreak)
|
||
{
|
||
Break();
|
||
}
|
||
else
|
||
{
|
||
// 短暂冷却
|
||
canHit = false;
|
||
DOVirtual.DelayedCall(0.3f, () =>
|
||
{
|
||
canHit = true;
|
||
if (currentState != StatueState.Broken)
|
||
currentState = StatueState.Idle;
|
||
});
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 播放邪恶狞笑(击打错误时)
|
||
/// </summary>
|
||
public void PlayEvilLaugh()
|
||
{
|
||
currentState = StatueState.EvilLaugh;
|
||
canHit = false;
|
||
|
||
if (debugMode)
|
||
Debug.Log($"[CrystalStatue] {name} 播放邪恶狞笑");
|
||
|
||
// 播放狞笑音效
|
||
if (!string.IsNullOrEmpty(evilLaughSound))
|
||
{
|
||
MasterAudio.PlaySound3DAtTransform(evilLaughSound, transform);
|
||
}
|
||
|
||
// 播放狞笑粒子
|
||
if (evilLaughParticle != null)
|
||
{
|
||
evilLaughParticle.Play();
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 强制消失
|
||
/// </summary>
|
||
public void ForceDisappear(float duration = 0.5f)
|
||
{
|
||
canHit = false;
|
||
|
||
// 缩放消失
|
||
transform.DOScale(Vector3.zero, duration)
|
||
.SetEase(Ease.InBack)
|
||
.OnComplete(() =>
|
||
{
|
||
currentState = StatueState.Broken;
|
||
gameObject.SetActive(false);
|
||
});
|
||
}
|
||
|
||
/// <summary>
|
||
/// 重置雕像状态
|
||
/// </summary>
|
||
public void Reset()
|
||
{
|
||
currentHitCount = 0;
|
||
canHit = true;
|
||
currentState = StatueState.Idle;
|
||
|
||
// 重置缩放
|
||
transform.localScale = Vector3.one;
|
||
|
||
// 重置动画
|
||
ResetAnimatorState();
|
||
|
||
// 显示对象
|
||
gameObject.SetActive(true);
|
||
}
|
||
|
||
/// <summary>
|
||
/// 是否已碎裂
|
||
/// </summary>
|
||
public bool IsBroken()
|
||
{
|
||
return currentState == StatueState.Broken;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 获取当前击打次数
|
||
/// </summary>
|
||
public int GetHitCount()
|
||
{
|
||
return currentHitCount;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 设置是否可被击打
|
||
/// </summary>
|
||
public void SetCanHit(bool value)
|
||
{
|
||
canHit = value;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 设置是否允许显示裂痕(语音期间为false,特效出现后为true)
|
||
/// </summary>
|
||
public void SetCanShowCrack(bool value)
|
||
{
|
||
canShowCrack = value;
|
||
hitObj.SetActive(value);
|
||
}
|
||
|
||
#endregion
|
||
|
||
#region 私有方法
|
||
|
||
/// <summary>
|
||
/// 重置动画状态到初始帧
|
||
/// </summary>
|
||
private void ResetAnimatorState()
|
||
{
|
||
if (crackAnimator != null)
|
||
{
|
||
// 播放到第0帧
|
||
crackAnimator.Play(crackAnimHash, 0, 0f);
|
||
crackAnimator.speed = 0f; // 暂停在第0帧
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 更新裂痕动画帧
|
||
/// </summary>
|
||
private void UpdateCrackAnimation()
|
||
{
|
||
if (crackAnimator == null)
|
||
{
|
||
if (debugMode)
|
||
Debug.LogWarning($"[CrystalStatue] {name} 没有动画组件");
|
||
return;
|
||
}
|
||
|
||
float targetFrame = 0f;
|
||
bool playFull = false;
|
||
|
||
switch (currentHitCount)
|
||
{
|
||
case 1:
|
||
targetFrame = firstHitFrame;
|
||
break;
|
||
case 2:
|
||
targetFrame = secondHitFrame;
|
||
break;
|
||
case 3:
|
||
// 第三次击打,完整播放动画
|
||
playFull = true;
|
||
break;
|
||
}
|
||
|
||
if (playFull)
|
||
{
|
||
// 完整播放动画
|
||
crackAnimator.speed = 1f;
|
||
crackAnimator.Play(crackAnimHash, 0, 0f);
|
||
}
|
||
else
|
||
{
|
||
// 播放到指定帧并暂停
|
||
float normalizedTime = targetFrame / (float)totalAnimFrames;
|
||
|
||
// 使用DoTween平滑过渡到目标帧
|
||
DOVirtual.Float(
|
||
crackAnimator.GetCurrentAnimatorStateInfo(0).normalizedTime,
|
||
normalizedTime,
|
||
0.2f,
|
||
(value) =>
|
||
{
|
||
crackAnimator.Play(crackAnimHash, 0, value);
|
||
crackAnimator.speed = 0f;
|
||
}
|
||
);
|
||
}
|
||
|
||
if (debugMode)
|
||
Debug.Log($"[CrystalStatue] {name} 更新动画帧: 第{currentHitCount}次击打, 帧: {targetFrame}");
|
||
}
|
||
|
||
/// <summary>
|
||
/// 雕像碎裂
|
||
/// </summary>
|
||
private void Break()
|
||
{
|
||
currentState = StatueState.Breaking;
|
||
canHit = false;
|
||
|
||
if (debugMode)
|
||
Debug.Log($"[CrystalStatue] {name} 碎裂! 是否目标: {isTargetStatue}");
|
||
|
||
// 播放碎裂粒子
|
||
if (breakParticle != null)
|
||
{
|
||
breakParticle.Play();
|
||
}
|
||
|
||
// 碎裂缩放动画
|
||
transform.DOScale(Vector3.zero, breakDuration)
|
||
.SetEase(Ease.InBack)
|
||
.OnComplete(() =>
|
||
{
|
||
currentState = StatueState.Broken;
|
||
gameObject.SetActive(false);
|
||
|
||
// 触发回调
|
||
OnStatueBroken?.Invoke(this, isTargetStatue);
|
||
});
|
||
}
|
||
|
||
/// <summary>
|
||
/// 播放击打音效
|
||
/// </summary>
|
||
private void PlayHitSound()
|
||
{
|
||
if (!string.IsNullOrEmpty(hitSound))
|
||
{
|
||
MasterAudio.PlaySound3DAtTransform(hitSound, transform);
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 播放击打特效
|
||
/// </summary>
|
||
private void PlayHitEffect()
|
||
{
|
||
// 播放粒子
|
||
if (hitParticle != null)
|
||
{
|
||
hitParticle.Play();
|
||
}
|
||
|
||
// 播放震动
|
||
transform.DOShakePosition(
|
||
hitShakeDuration,
|
||
hitShakeStrength,
|
||
10,
|
||
90f,
|
||
false,
|
||
true
|
||
);
|
||
}
|
||
|
||
#endregion
|
||
|
||
#region 编辑器辅助
|
||
|
||
#if UNITY_EDITOR
|
||
[ContextMenu("测试:击打雕像")]
|
||
private void TestHit()
|
||
{
|
||
OnPlayerHit();
|
||
}
|
||
|
||
[ContextMenu("测试:碎裂雕像")]
|
||
private void TestBreak()
|
||
{
|
||
currentHitCount = hitCountToBreak;
|
||
Break();
|
||
}
|
||
|
||
[ContextMenu("测试:重置雕像")]
|
||
private void TestReset()
|
||
{
|
||
Reset();
|
||
}
|
||
|
||
[ContextMenu("测试:播放狞笑")]
|
||
private void TestEvilLaugh()
|
||
{
|
||
PlayEvilLaugh();
|
||
}
|
||
|
||
[ContextMenu("测试:强制消失")]
|
||
private void TestForceDisappear()
|
||
{
|
||
ForceDisappear();
|
||
}
|
||
#endif
|
||
|
||
#endregion
|
||
} |