using Spine.Unity; using System.Collections.Generic; using UnityEngine; 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; /// 出现动画保护截止时间:这段时间内 Idle(动画名为空)不 ClearTrack,避免出现动画刚播就被抹掉 private float _appearAnimHoldUntil; /// /// 从资源表查询 TurretResourcesData。表里没有则返回 null,调用方应使用 TurretData 自身的 Inspector 默认值。 /// public TurretResourcesData GetResourcesData() { return TurretResourcesData.GetTurretData(turretNumber); } /// 安全获取攻击音效 ID:表里有就用,没有则返回 0(静音)。 public int GetAttackAudioID() { var res = GetResourcesData(); return res != null ? res.attackAudioID : 0; } /// 安全获取子弹 ID 列表:表里有就用,没有则返回空列表。 public List GetBulletIDs() { var res = GetResourcesData(); return res?.bulletID ?? new List(); } /// 安全获取动画类型:表里有就用,没有则用 Inspector 配置的 currTurretType。 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() { PlayState(TurretState.Idle); } //public Vector2 GetBulletStartWorldPos() //{ // if (tra_BulletStartPosition == null) // return Vector2.zero; // // 如果是 UI RectTransform(UGUI) // RectTransform rect = tra_BulletStartPosition.GetComponent(); // if (rect != null) // { // // 1. UI 本地坐标 → Canvas 坐标 // Vector2 canvasPos = CoordinateTransformation.AnchoredPos_ToCanvas( // rect, // UIManager.Instance.uiRoot.GetComponent() // ); // // 2. Canvas 坐标 → 世界坐标 // return CoordinateTransformation.CanvasToWorld( // canvasPos, // UIManager.Instance.uiRoot.GetComponent(), // 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(); if (rect != null) { Canvas canvas = UIManager.Instance.uiRoot.GetComponent(); 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); } /// /// 播放"出现动画":各 SkeletonGraphic 上配置的 Spine startingAnimation(炮台容器/炮体上配的是 out、trans 等)。 /// ⭐ Spine 只在 Awake(第一次激活)应用一次 startingAnimation,而 OnEnable 的 PlayState(Idle) /// 在动画名为空时会 ClearTrack 把它抹掉,所以切换炮台时补播一次, /// 并在播放期间保护它不被 Idle 清掉(见 PlaySkeleton 的空动画名分支)。 /// public void PlayAppearAnim() { float hold = 0f; foreach (var g in GetComponentsInChildren(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; } 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; } } /// /// 战神 Animator 约定检测:控制器带 PlayFire 参数(14082/14053 等战神状态机)即启用战神模式。 /// 参数实际类型以运行时 Animator.parameters 为准,不写死(个别控制器 PlayFire 可能是 Trigger)。 /// 变体索引(fireIndex/idleIndex)检测完一次性写入。 /// 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() : "无")}"); } /// /// 战神状态切换(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(正常子弹动画)。 /// 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); } } /// /// 激光是否开启:战神锁定中即激光开启(与 SitPosData.UpdateZhanShenLaser 的显示条件一致)。 /// setPosData 由 SitPosData.LoadZhanShen 注入;远程座位的锁定经 SetLockTarget 同步,同样生效。 /// private bool IsLaserActive() { var sit = setPosData; return sit != null && sit.isLock && sit.monCotl_Lock_Position != null && sit.monCotl_Lock_Position.gameObject.activeInHierarchy; } /// /// 锁定状态变化瞬间刷新 FireIndex(0=子弹 Fire0 / 1=激光 Fire1): /// ChangeState 同状态早退不会重跑 PlayWarGodAnimator,边开火边锁定时 FireIndex 会停在旧值, /// 动画不切 Fire1、SMB 事件永不触发,特效只能等 laserFXDelay 兜底定时器“直接激活”。 /// 锁定建立/丢失时调用本方法刷新参数 → Fire0↔Fire1 过渡(FireIndex 条件)立即生效, /// 特效显示由此完全交给 Fire1 上的 ZhanShenLaserLoopSMB 动画事件开门。 /// public void RefreshWarGodFireMode() { if (!_warGodAnimMode || !_hasFireIndex) return; if (currState != TurretState.Attack && currState != TurretState.LaserStart && currState != TurretState.LaserLoop) return; SetNumericParam(fireIndexParam, _fireIndexType, IsLaserActive() ? 1 : 0); } /// /// 战神激光 loop 动画开始通知(ZhanShenLaserLoopSMB 状态机行为 / 剪辑动画事件殊途同归): /// 通知座位立即显示激光及全部特效(提前结束 laserFXDelay 等待)。 /// public void NotifyZhanShenLaserLoopAnim() { setPosData?.NotifyZhanShenLaserLoopAnim(); } /// /// AnimationEvent 接收器:在激光 loop 动画剪辑的第 0 帧配动画事件、函数名填 ZhanShenLaserLoopBegin。 /// ⭐ 前提:Animator 挂在和本组件同一个物体上(动画事件只发给 Animator 所在物体的脚本); /// Animator 在子物体上时改用 ZhanShenLaserLoopSMB(Add Behaviour 到 Animator 的 loop 状态)。 /// public void ZhanShenLaserLoopBegin() { NotifyZhanShenLaserLoopAnim(); } /// 按参数实际类型写数值(Int/Bool/Float 通吃) 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; } } }