Files
5.24BuYu/Assets/MyGame/Scripts/Turret/TurretData.cs
T
2026-10-11 18:20:05 +08:00

775 lines
31 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using Spine.Unity;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
public enum TurretAnimType
{
None = 0,
skeletonAnimation = 1,
animation = 2,
animator = 3
}
public enum TurretState
{
None = 0,
Idle = 1,
Attack = 2,
LaserStart = 3,
LaserLoop = 4,
LaserEnd = 5
}
public class TurretData : MonoBehaviour
{
[Header("基础")]
public string turretID;
public SitPosData setPosData;
public Transform tra_BulletStartPosition;
[Tooltip("炮台旋转节点。拖入后 SitPosData.tra_CurrTurret 用此节点旋转;不拖则用炮台自身的父节点(兼容旧逻辑)")]
public Transform tra_RotationPivot;
public GameObject bulletPrefab_Two;
public GameObject bulletPrefab;
[Header("战神激光(锁定时显示,普通炮台不配置即无此效果)")]
[Tooltip("激光预制体。非空=预制体形态(JiGuangControl 工具类驱动);为空=LineRenderer 兜底形态(自动创建)")]
public GameObject laserPrefab;
[Tooltip("LineRenderer 兜底形态的线宽")]
public float laserWidth = 0.3f;
[Tooltip("LineRenderer 兜底形态的颜色")]
public Color laserColor = new Color(1f, 0.3f, 0.3f, 0.9f);
[Tooltip("预制体形态激光的长度校正值(不同激光预制体设置不同值)")]
public float laserLengthCorrect = 1f;
[Header("UI面板属性")]
[Tooltip("炮台编号,对应 TurretResourcesData.turretNumber")]
public int turretNumber;
[Tooltip("1=普通炮台 2=战神 3=翅膀")]
public int turretType = 1;
[Tooltip("UI 展示名称")]
public string displayName;
[Tooltip("品质 1=普通 2=稀有 3=史诗 4=传说")]
[Range(1, 5)]
public int quality = 1;
[Tooltip("解锁所需等级(0=默认拥有)")]
public int unlockLevel;
[Tooltip("UI 列表/详情用的缩略图")]
public Sprite uiIconSprite;
[TextArea(2, 4)]
[Tooltip("描述文字")]
public string description;
// ---- 运行时状态(由服务器/UI面板读写) ----
[System.NonSerialized] public bool isUnlocked;
[System.NonSerialized] public bool isEquipped;
[System.NonSerialized] public int ownStarLevel;
[Header("动画组件")]
public SkeletonGraphic skeletonAnimation;
public Animation old_CurrAnimation;
public Animator new_CurrAnimation;
public TurretAnimType currTurretType;
public bool isFrenzy=false;
[Header("动画名")]
public string idleAnim;
public string attackAnim;
// 新增:狂暴专用攻击动画(已经是二倍速帧动画)
public string frenzyAttackAnim;
private bool useFrenzyAnim = false;
public string laserStartAnim;
public string laserLoopAnim;
public string laserEndAnim;
[Header("战神 Animator 参数")]
[Tooltip("战神状态机开火参数名(Bool:Idle0 --PlayFire--> Fire状态机,Fire --PlayFire=false--> Exit回Idle)。" +
"检测到此参数自动启用战神模式,无需手动配置")]
public string playFireParam = "PlayFire";
[Tooltip("开火动画变体参数名(Int):0=Fire0(正常子弹) 1=Fire1(激光)。锁定时自动切1,非锁定开火用0")]
public string fireIndexParam = "FireIndex";
[Tooltip("待机动画变体参数名(Int:IdleIndex=0选Idle0...)")]
public string idleIndexParam = "IdleIndex";
[Tooltip("开火动画变体索引默认值:0=Fire0(正常子弹) 1=Fire1(激光)。运行时按锁定状态自动切换,此值仅为无锁定信息时的兜底")]
public int fireIndex = 0;
[Tooltip("待机动画变体索引:0=Idle0 1=Idle1...")]
public int idleIndex = 0;
[Header("XJMH加减炮倍")]
public GameObject obj_Add;
public GameObject obj_Down;
// ---- 战神模式运行时缓存(首次 PlayAnimator 时检测)----
private bool _warGodAnimChecked; // 是否已检测过参数
private bool _warGodAnimMode; // true=战神 PlayFire 约定
private bool _playFireIsTrigger; // PlayFire 实际类型是否 Trigger(个别控制器用触发器)
private bool _hasFireIndex;
private bool _hasIdleIndex;
private AnimatorControllerParameterType _fireIndexType = AnimatorControllerParameterType.Int;
private AnimatorControllerParameterType _idleIndexType = AnimatorControllerParameterType.Int;
private TurretState currState;
/// <summary>出现动画保护截止时间:这段时间内 Idle(动画名为空)不 ClearTrack,避免出现动画刚播就被抹掉</summary>
private float _appearAnimHoldUntil;
/// <summary>
/// 从资源表查询 TurretResourcesData。表里没有则返回 null,调用方应使用 TurretData 自身的 Inspector 默认值。
/// </summary>
public TurretResourcesData GetResourcesData()
{
return TurretResourcesData.GetTurretData(turretNumber);
}
/// <summary>安全获取攻击音效 ID:表里有就用,没有则返回 0(静音)。</summary>
public int GetAttackAudioID()
{
var res = GetResourcesData();
return res != null ? res.attackAudioID : 0;
}
/// <summary>安全获取子弹 ID 列表:表里有就用,没有则返回空列表。</summary>
public List<int> GetBulletIDs()
{
var res = GetResourcesData();
return res?.bulletID ?? new List<int>();
}
/// <summary>安全获取动画类型:表里有就用,没有则用 Inspector 配置的 currTurretType。</summary>
public TurretAnimType GetAnimationType()
{
var res = GetResourcesData();
return res != null ? (TurretAnimType)res.animationType : currTurretType;
}
private void Awake()
{
//if (setPosData != null)
// setPosData.OnStateChange = ChangeState;
// 自动判断:如果狂暴攻击动画名不为空,就启用切换动画模式
useFrenzyAnim = !string.IsNullOrEmpty(frenzyAttackAnim);
}
private void OnEnable()
{
// ⭐ 状态缓存必须跟着 OnEnable 实际播的动画走:这里直接播 Idle(不走 ChangeState),
// 所以必须手动把 currState 一起重置。否则反复激活(切炮台/座位隐藏恢复/对象池复用)后
// currState 会停在 Attack,之后真正的 ChangeState(Attack) 被"同状态早退"吃掉 →
// 炮台停在错误状态(表现为该攻击时不出手 / 不该攻击时一直在攻击状态)。
currState = TurretState.Idle;
PlayState(TurretState.Idle);
BindAddDownButtons(); // ⭐ XJMH 加减炮倍:炮台上的 +/- 绑定点击(每次激活重绑,幂等)
}
/// <summary>
/// 当前动画状态(只读)。调用方(GameMainPanel)用它在"这一帧没开火"时判断要不要补发 Idle,
/// 避免每帧重复广播状态。注意:状态由 OnStateChange 指向的那个 TurretData 接收
/// (战神装备中指向战神,见 SitPosData.LoadZhanShen)。
/// </summary>
public TurretState CurrState => currState;
// ==================== XJMH 加减炮倍按钮绑定 ====================
// 炮台上的 obj_Add / obj_Down(物体名 Up / Down)在预制体里没有 Button 组件、也没有任何代码绑定
// → 点击无反应。这里在炮台激活时统一补 Button + 绑定:
// + → 座位 ChangeTurretAddConsumeNum(发 ChangeGunMultiple,等服务端确认后应用新炮倍)
// - → 座位 ChangeTurretMinusConsumeNum
// 未拖引用跳过(可要可不要);非本地座位由 SitPosData.HideTurretAddMinusUI 隐藏。
private SitPosData ResolveSit()
{
return setPosData != null ? setPosData : GetComponentInParent<SitPosData>();
}
private void OnClickTurretAdd() { ResolveSit()?.ChangeTurretAddConsumeNum(); }
private void OnClickTurretMinus() { ResolveSit()?.ChangeTurretMinusConsumeNum(); }
private void BindAddDownButtons()
{
BindAddDownButton(obj_Add, OnClickTurretAdd);
BindAddDownButton(obj_Down, OnClickTurretMinus);
}
private static void BindAddDownButton(GameObject go, UnityEngine.Events.UnityAction action)
{
if (go == null) return; // 未拖引用:跳过,不报错
var btn = go.GetComponent<Button>();
if (btn == null) btn = go.GetComponentInChildren<Button>(true);
if (btn == null) btn = go.AddComponent<Button>(); // 没有 Button 就程序补一个(物体自带的 Image 提供射线检测)
btn.onClick.RemoveAllListeners(); // 重绑前清掉,避免重复触发
btn.onClick.AddListener(action);
}
//public Vector2 GetBulletStartWorldPos()
//{
// if (tra_BulletStartPosition == null)
// return Vector2.zero;
// // 如果是 UI RectTransform(UGUI)
// RectTransform rect = tra_BulletStartPosition.GetComponent<RectTransform>();
// if (rect != null)
// {
// // 1. UI 本地坐标 → Canvas 坐标
// Vector2 canvasPos = CoordinateTransformation.AnchoredPos_ToCanvas(
// rect,
// UIManager.Instance.uiRoot.GetComponent<Canvas>()
// );
// // 2. Canvas 坐标 → 世界坐标
// return CoordinateTransformation.CanvasToWorld(
// canvasPos,
// UIManager.Instance.uiRoot.GetComponent<RectTransform>(),
// Camera.main
// );
// }
// // 如果不是 UI(比如 3D Transform)
// return tra_BulletStartPosition.position;
//}
public Vector2 GetBulletStartWorldPos()
{
if (tra_BulletStartPosition == null)
return Vector2.zero;
// 如果是 UI(UGUI)
RectTransform rect = tra_BulletStartPosition.GetComponent<RectTransform>();
if (rect != null)
{
Canvas canvas = UIManager.Instance.uiRoot.GetComponent<Canvas>();
Camera cam = Camera.main;
// 方式1:通过屏幕坐标直接转世界坐标(推荐)
Vector3 screenPos = RectTransformUtility.WorldToScreenPoint(
canvas.renderMode == RenderMode.ScreenSpaceOverlay ? null : canvas.worldCamera,
rect.position
);
// 需要计算 Z 轴距离(物体到相机的距离),这里假设子弹打在 Z=0 平面
float distance = Mathf.Abs(cam.transform.position.z - 0f);
Vector3 worldPos = cam.ScreenToWorldPoint(new Vector3(screenPos.x, screenPos.y, distance));
worldPos.z = 0f;
return worldPos;
}
// 非 UI(3D 物体)
return tra_BulletStartPosition.position;
}
public void SetFrenzy(bool value)
{
isFrenzy = value;
if (!useFrenzyAnim) // 没有狂暴动画 → 用速度加倍
{
float speed = isFrenzy ? 2f : 1f;
if (skeletonAnimation != null)
skeletonAnimation.timeScale = speed;
if (new_CurrAnimation != null)
new_CurrAnimation.speed = speed;
if (old_CurrAnimation != null)
{
foreach (AnimationState state in old_CurrAnimation)
state.speed = speed;
}
}
else
{
// 有狂暴动画 → 不做速度调整,保持 1 倍速
if (skeletonAnimation != null)
skeletonAnimation.timeScale = 1f;
if (new_CurrAnimation != null)
new_CurrAnimation.speed = 1f;
if (old_CurrAnimation != null)
{
foreach (AnimationState state in old_CurrAnimation)
state.speed = 1f;
}
}
}
public virtual void ChangeState(TurretState newState)
{
if (currState == newState) return;
currState = newState;
PlayState(newState);
}
/// <summary>
/// 播放"出现动画":各 SkeletonGraphic 上配置的 Spine startingAnimation(炮台容器/炮体上配的是 out、trans 等)。
/// ⭐ Spine 只在 Awake(第一次激活)应用一次 startingAnimation,而 OnEnable 的 PlayState(Idle)
/// 在动画名为空时会 ClearTrack 把它抹掉,所以切换炮台时补播一次,
/// 并在播放期间保护它不被 Idle 清掉(见 PlaySkeleton 的空动画名分支)。
/// </summary>
public void PlayAppearAnim()
{
float hold = 0f;
foreach (var g in GetComponentsInChildren<SkeletonGraphic>(true))
{
if (g == null || string.IsNullOrEmpty(g.startingAnimation) || g.AnimationState == null) continue;
var track = g.AnimationState.SetAnimation(0, g.startingAnimation, g.startingLoop);
if (!g.startingLoop && track != null)
hold = Mathf.Max(hold, track.Animation.Duration);
}
_appearAnimHoldUntil = Time.time + hold;
}
// ==================== XJMH 入场动画(原版 PW_Battery:EnterAction,进入房间时本地座位播一次) ====================
[Header("XJMH入场动画(原版 EnterAction:飞机从下方升起 → 变回炮台)")]
[Tooltip("入场音效 id(原版 PW_Enter,tbaudio 60003);0=不播")]
public int enterAudioID = 60003;
[Tooltip("飞机回位音效 id(原版 PW_PlaneBack,tbaudio 61106);0=不播")]
public int enterBackAudioID = 61106;
[Tooltip("飞机入场起点相对停靠位的 Y 偏移(负值=从更下方升起,原版从画面下方飞入)")]
public float enterStartOffsetY = -260f;
[Tooltip("第一段升起时长(秒),原版 0.5")]
public float enterRiseTime = 0.5f;
[Tooltip("两段之间的停顿(秒),原版 0.3")]
public float enterHoldTime = 0.3f;
[Tooltip("第二段升到停靠位时长(秒),原版 0.5")]
public float enterBackRiseTime = 0.5f;
[Tooltip("飞机 \"back\" 动画开始后,飞机隐藏、炮台出现的延迟(秒),原版 1.0")]
public float enterBackHoldTime = 1f;
private Coroutine _enterRoutine;
private RectTransform _enterPlane; // 入场中的飞机节点(离场清理复位用)
private Vector2 _enterPlaneHome; // 飞机停靠位(预制体配好的 anchoredPosition)
/// <summary>
/// XJMH 入场动画(移植原版 Battery:EnterAction 的非立即分支,进入房间时由本地座位调用一次):
/// Enter 音效 → 飞机从停靠位下方升起 → 停顿 → 升到停靠位(PlaneBack 音效 + 飞机播 "back")
/// → 延迟后飞机隐藏、炮台本体出现并播 "out"(原版 ShowGun)。
/// 找不到飞机节点时只播炮台出现动画(兜底不报错)。
/// </summary>
public void PlayEnterAction()
{
if (!isActiveAndEnabled) return;
if (_enterRoutine != null) StopCoroutine(_enterRoutine);
_enterRoutine = StartCoroutine(EnterActionRoutine());
}
private IEnumerator EnterActionRoutine()
{
if (enterAudioID > 0 && AudioManager.Instance != null)
AudioManager.Instance.PlayerFX(enterAudioID);
_enterPlane = FindPlaneSkeletonRect();
if (_enterPlane == null)
{
PlayTurretOutAnim();
_enterRoutine = null;
yield break;
}
SkeletonGraphic planeSg = _enterPlane.GetComponent<SkeletonGraphic>();
_enterPlaneHome = _enterPlane.anchoredPosition;
Vector2 home = _enterPlaneHome;
Vector2 start = home + new Vector2(0f, enterStartOffsetY);
Vector2 mid = home + new Vector2(0f, enterStartOffsetY * 0.5f);
// 原版:入场时炮台本体隐藏、飞机出现(飞机从下方升上来,"变"回炮台)
if (skeletonAnimation != null)
skeletonAnimation.gameObject.SetActive(false);
_enterPlane.gameObject.SetActive(true);
_enterPlane.anchoredPosition = start;
if (planeSg != null && planeSg.AnimationState != null)
planeSg.AnimationState.ClearTrack(0); // 清残留(对象池/重复入场)
// 第一段:升到中途位(原版 EInQuart)
yield return RiseTo(_enterPlane, start, mid, enterRiseTime, true);
// 停顿(原版 delay 0.3)
if (enterHoldTime > 0f) yield return new WaitForSeconds(enterHoldTime);
// 第二段:升到停靠位(原版 EOutQuart)
yield return RiseTo(_enterPlane, mid, home, enterBackRiseTime, false);
// PlaneBack 音效 + 飞机回位动画(原版 PlaneBack → PlaneBackAnim)
if (enterBackAudioID > 0 && AudioManager.Instance != null)
AudioManager.Instance.PlayerFX(enterBackAudioID);
if (planeSg != null && planeSg.AnimationState != null)
planeSg.AnimationState.SetAnimation(0, "back", false);
// 原版:back 动画 1 秒后飞机隐藏、炮台出现(ShowGun)
if (enterBackHoldTime > 0f) yield return new WaitForSeconds(enterBackHoldTime);
_enterPlane.gameObject.SetActive(false);
_enterPlane.anchoredPosition = home;
PlayTurretOutAnim();
_enterPlane = null;
_enterRoutine = null;
}
/// <summary>协程位移:EInQuart / EOutQuart 缓动(与原版 GC.Action 一致)</summary>
private static IEnumerator RiseTo(RectTransform rt, Vector2 from, Vector2 to, float time, bool easeIn)
{
if (rt == null) yield break;
if (time <= 0f) { rt.anchoredPosition = to; yield break; }
float t = 0f;
while (t < time)
{
t += Time.deltaTime;
float p = Mathf.Clamp01(t / time);
float e = easeIn ? p * p * p * p : 1f - Mathf.Pow(1f - p, 4f);
rt.anchoredPosition = Vector2.LerpUnclamped(from, to, e);
yield return null;
}
rt.anchoredPosition = to;
}
/// <summary>炮台本体出现:恢复显示并播 startingAnimation(预制体配的 "out";为空时兜底 "out")</summary>
private void PlayTurretOutAnim()
{
if (skeletonAnimation == null) return;
skeletonAnimation.gameObject.SetActive(true);
if (skeletonAnimation.AnimationState == null) return;
string anim = string.IsNullOrEmpty(skeletonAnimation.startingAnimation) ? "out" : skeletonAnimation.startingAnimation;
skeletonAnimation.AnimationState.SetAnimation(0, anim, skeletonAnimation.startingLoop);
skeletonAnimation.Update(0.1f);
}
/// <summary>找飞机节点:名字含 "feiji" 的 SkeletonGraphic(四种炮台各配一架:lvptfeiji/lanptfeiji/huangjinfeiji/vipfeiji01)</summary>
private RectTransform FindPlaneSkeletonRect()
{
foreach (var sg in GetComponentsInChildren<SkeletonGraphic>(true))
{
if (sg == null) continue;
if (sg.gameObject.name.ToLowerInvariant().Contains("feiji"))
return sg.rectTransform;
}
return null;
}
/// <summary>
/// 入场动画清理:停协程 + 复位(飞机隐藏回停靠位、炮台本体恢复显示),禁用/回收不残留。
/// ⭐ protected(不直接放 private OnDisable 里):子类(如 FlyTurretData)自带 OnDisable 时,
/// Unity 只会调用最派生的 OnDisable、基类同名私有方法被跳过,
/// 子类需在自己的 OnDisable 里显式调用本方法,保证入场动画清理不被漏掉。
/// </summary>
protected void CleanupEnterAction()
{
if (_enterRoutine != null) { StopCoroutine(_enterRoutine); _enterRoutine = null; }
if (_enterPlane != null)
{
_enterPlane.anchoredPosition = _enterPlaneHome;
_enterPlane.gameObject.SetActive(false);
_enterPlane = null;
}
if (skeletonAnimation != null && !skeletonAnimation.gameObject.activeSelf)
skeletonAnimation.gameObject.SetActive(true);
}
private void OnDisable()
{
CleanupEnterAction();
}
private void PlayState(TurretState state)
{
switch (currTurretType)
{
case TurretAnimType.skeletonAnimation:
PlaySkeleton(state);
break;
case TurretAnimType.animation:
PlayLegacy(state);
break;
case TurretAnimType.animator:
PlayAnimator(state);
break;
}
}
//private void PlaySkeleton(TurretState state)
//{
// if (skeletonAnimation == null) return;
// string anim = state switch
// {
// TurretState.Idle => idleAnim,
// TurretState.Attack => (isFrenzy && useFrenzyAnim) ? frenzyAttackAnim : attackAnim,
// TurretState.LaserStart => laserStartAnim,
// TurretState.LaserLoop => laserLoopAnim,
// TurretState.LaserEnd => laserEndAnim,
// _ => idleAnim
// };
// bool loop = state == TurretState.Attack || state == TurretState.LaserLoop;
// skeletonAnimation.AnimationState.SetAnimation(0, anim, loop);
// if (state == TurretState.LaserStart)
// {
// skeletonAnimation.AnimationState.AddAnimation(0, laserLoopAnim, true, 0f);
// }
//}
//private void PlayLegacy(TurretState state)
//{
// if (old_CurrAnimation == null) return;
// string anim = state switch
// {
// TurretState.Idle => idleAnim,
// TurretState.Attack => (isFrenzy && useFrenzyAnim) ? frenzyAttackAnim : attackAnim,
// TurretState.LaserStart => laserStartAnim,
// TurretState.LaserLoop => laserLoopAnim,
// TurretState.LaserEnd => laserEndAnim,
// _ => idleAnim
// };
// old_CurrAnimation.Play(anim);
//}
//private void PlayAnimator(TurretState state)
//{
// if (new_CurrAnimation == null) return;
// if (state == TurretState.Attack && isFrenzy && useFrenzyAnim)
// {
// new_CurrAnimation.SetTrigger("FrenzyAttack");
// }
// else
// {
// new_CurrAnimation.SetInteger("State", (int)state);
// }
//}
private void PlaySkeleton(TurretState state)
{
if (skeletonAnimation == null) return;
string anim = state switch
{
TurretState.Idle => idleAnim,
TurretState.Attack => (isFrenzy && useFrenzyAnim) ? frenzyAttackAnim : attackAnim,
TurretState.LaserStart => laserStartAnim,
TurretState.LaserLoop => laserLoopAnim,
TurretState.LaserEnd => laserEndAnim,
_ => idleAnim
};
// 如果动画名为空 → 停止当前动画(暂停),不再播放任何动画
if (string.IsNullOrEmpty(anim))
{
// ⭐ 出现动画播放期间不覆盖:ClearTrack 会把刚播的出现动画抹掉。
// 典型场景:切炮台后松手,HandleInput 会发一次 ChangeState(Idle),
// 而 idleAnim 为空正好走这个分支,出现动画一帧就被清掉。
if (Time.time < _appearAnimHoldUntil) return;
skeletonAnimation.AnimationState.ClearTrack(0);
skeletonAnimation.AnimationState.SetEmptyAnimation(0, 0f);
return;
}
bool loop = state == TurretState.Attack || state == TurretState.LaserLoop;
skeletonAnimation.AnimationState.SetAnimation(0, anim, loop);
if (state == TurretState.LaserStart && !string.IsNullOrEmpty(laserLoopAnim))
{
skeletonAnimation.AnimationState.AddAnimation(0, laserLoopAnim, true, 0f);
}
}
private void PlayLegacy(TurretState state)
{
if (old_CurrAnimation == null) return;
string anim = state switch
{
TurretState.Idle => idleAnim,
TurretState.Attack => (isFrenzy && useFrenzyAnim) ? frenzyAttackAnim : attackAnim,
TurretState.LaserStart => laserStartAnim,
TurretState.LaserLoop => laserLoopAnim,
TurretState.LaserEnd => laserEndAnim,
_ => idleAnim
};
if (string.IsNullOrEmpty(anim))
{
old_CurrAnimation.Stop();
return;
}
old_CurrAnimation.Play(anim);
}
private void PlayAnimator(TurretState state)
{
if (new_CurrAnimation == null) return;
// 战神约定(PlayFire/FireIndex/IdleIndex):控制器带 PlayFire 参数时自动启用
if (!_warGodAnimChecked) DetectWarGodAnimator();
if (_warGodAnimMode)
{
PlayWarGodAnimator(state);
return;
}
// ---- 旧约定(State 整数 / FrenzyAttack 触发器),保持不变 ----
// 如果缺少对应动画,暂停 animator(速度为0)或者设置一个空状态
if (state == TurretState.Attack && isFrenzy && useFrenzyAnim && !string.IsNullOrEmpty(frenzyAttackAnim))
{
new_CurrAnimation.SetTrigger("FrenzyAttack");
}
else if (!string.IsNullOrEmpty(attackAnim) || state == TurretState.Idle && !string.IsNullOrEmpty(idleAnim))
{
new_CurrAnimation.SetInteger("State", (int)state);
}
else
{
// 没有可用动画时暂停 animator,避免残留动作
new_CurrAnimation.speed = 0f;
}
}
/// <summary>
/// 战神 Animator 约定检测:控制器带 PlayFire 参数(14082/14053 等战神状态机)即启用战神模式。
/// 参数实际类型以运行时 Animator.parameters 为准,不写死(个别控制器 PlayFire 可能是 Trigger)。
/// 变体索引(fireIndex/idleIndex)检测完一次性写入。
/// </summary>
private void DetectWarGodAnimator()
{
// 控制器未就绪时不标记,下次 PlayAnimator 再检测
if (new_CurrAnimation.runtimeAnimatorController == null) return;
_warGodAnimChecked = true;
_warGodAnimMode = false;
foreach (var p in new_CurrAnimation.parameters)
{
if (p.name == playFireParam)
{
_warGodAnimMode = true;
_playFireIsTrigger = p.type == AnimatorControllerParameterType.Trigger;
}
else if (p.name == fireIndexParam)
{
_hasFireIndex = true;
_fireIndexType = p.type;
}
else if (p.name == idleIndexParam)
{
_hasIdleIndex = true;
_idleIndexType = p.type;
}
}
if (!_warGodAnimMode) return;
// 变体索引一次性写入(Inspector 配置,运行中不变)
if (_hasFireIndex) SetNumericParam(fireIndexParam, _fireIndexType, fireIndex);
if (_hasIdleIndex) SetNumericParam(idleIndexParam, _idleIndexType, idleIndex);
Debug.Log($"[TurretData] 战神Animator模式 #{turretNumber}: PlayFire={(_playFireIsTrigger ? "Trigger" : "Bool")}, " +
$"FireIndex={(_hasFireIndex ? fireIndex.ToString() : "无")}, IdleIndex={(_hasIdleIndex ? idleIndex.ToString() : "无")}");
}
/// <summary>
/// 战神状态切换(14082 等状态机结构):
/// Idle0 --PlayFire=true--> Fire状态机;Fire0/Fire1 --PlayFire=false--> Exit 回 Idle。
/// - Bool 型(标准):Attack 置 true 持续开火,Idle 置 false 停火退出;
/// - Trigger 型(兜底):Attack 设触发器,Idle 复位。
/// LaserStart/LaserLoop 视为开火中,None/LaserEnd 视为待机。
/// ⭐ FireIndex 动态切换:锁定中(激光开启)=1 走 Fire1(激光动画);非锁定=0 走 Fire0(正常子弹动画)。
/// </summary>
private void PlayWarGodAnimator(TurretState state)
{
bool attacking = state == TurretState.Attack
|| state == TurretState.LaserStart
|| state == TurretState.LaserLoop;
// ⭐ 开火变体:0=正常子弹 1=激光(锁定中)。进 Fire 状态机前先设好值
if (attacking && _hasFireIndex)
SetNumericParam(fireIndexParam, _fireIndexType, IsLaserActive() ? 1 : 0);
if (_playFireIsTrigger)
{
if (attacking) new_CurrAnimation.SetTrigger(playFireParam);
else new_CurrAnimation.ResetTrigger(playFireParam);
}
else
{
new_CurrAnimation.SetBool(playFireParam, attacking);
}
}
/// <summary>
/// 激光是否开启:战神锁定中即激光开启(与 SitPosData.UpdateZhanShenLaser 的显示条件一致)。
/// setPosData 由 SitPosData.LoadZhanShen 注入;远程座位的锁定经 SetLockTarget 同步,同样生效。
/// </summary>
private bool IsLaserActive()
{
var sit = setPosData;
return sit != null
&& sit.isLock
&& sit.monCotl_Lock_Position != null
&& sit.monCotl_Lock_Position.gameObject.activeInHierarchy;
}
/// <summary>
/// 锁定状态变化瞬间刷新 FireIndex(0=子弹 Fire0 / 1=激光 Fire1):
/// ChangeState 同状态早退不会重跑 PlayWarGodAnimator,边开火边锁定时 FireIndex 会停在旧值,
/// 动画不切 Fire1、SMB 事件永不触发,特效只能等 laserFXDelay 兜底定时器“直接激活”。
/// 锁定建立/丢失时调用本方法刷新参数 → Fire0↔Fire1 过渡(FireIndex 条件)立即生效,
/// 特效显示由此完全交给 Fire1 上的 ZhanShenLaserLoopSMB 动画事件开门。
/// </summary>
public void RefreshWarGodFireMode()
{
if (!_warGodAnimMode || !_hasFireIndex) return;
if (currState != TurretState.Attack
&& currState != TurretState.LaserStart
&& currState != TurretState.LaserLoop) return;
SetNumericParam(fireIndexParam, _fireIndexType, IsLaserActive() ? 1 : 0);
}
/// <summary>
/// 战神激光 loop 动画开始通知(ZhanShenLaserLoopSMB 状态机行为 / 剪辑动画事件殊途同归):
/// 通知座位立即显示激光及全部特效(提前结束 laserFXDelay 等待)。
/// </summary>
public void NotifyZhanShenLaserLoopAnim()
{
setPosData?.NotifyZhanShenLaserLoopAnim();
}
/// <summary>
/// AnimationEvent 接收器:在激光 loop 动画剪辑的第 0 帧配动画事件、函数名填 ZhanShenLaserLoopBegin。
/// ⭐ 前提:Animator 挂在和本组件同一个物体上(动画事件只发给 Animator 所在物体的脚本);
/// Animator 在子物体上时改用 ZhanShenLaserLoopSMB(Add Behaviour 到 Animator 的 loop 状态)。
/// </summary>
public void ZhanShenLaserLoopBegin()
{
NotifyZhanShenLaserLoopAnim();
}
/// <summary>按参数实际类型写数值(Int/Bool/Float 通吃)</summary>
private void SetNumericParam(string paramName, AnimatorControllerParameterType type, int value)
{
switch (type)
{
case AnimatorControllerParameterType.Int:
new_CurrAnimation.SetInteger(paramName, value);
break;
case AnimatorControllerParameterType.Bool:
new_CurrAnimation.SetBool(paramName, value != 0);
break;
case AnimatorControllerParameterType.Float:
new_CurrAnimation.SetFloat(paramName, value);
break;
}
}
}