Files
5.24BuYu/Assets/MyGame/Scripts/Turret/TurretData.cs
T
2026-10-06 19:20:35 +08:00

611 lines
23 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.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()
{
PlayState(TurretState.Idle);
BindAddDownButtons(); // ⭐ XJMH 加减炮倍:炮台上的 +/- 绑定点击(每次激活重绑,幂等)
}
// ==================== 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;
}
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;
}
}
}