Files
5.24BuYu/Assets/MyGame/Scripts/Turret/FlyTurretData.cs
T
2026-10-11 21:57:58 +08:00

466 lines
20 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 System;
using System.Collections;
using Spine.Unity;
using UnityEngine;
/// <summary>
/// 起飞炮台(TurretData 子类):炮台模式 / 起飞模式 两态分离。
/// 【炮台模式(IsFlying=false,默认形态)】就是一门正常炮台:手动瞄准开火/自动开火/锁定瞄准照常,
/// 与普通 TurretData 行为完全一致(轻点屏幕只走正常开火流程,不会起飞)。
/// 【起飞模式(IsFlying=true)】点"起飞按钮"进入(按钮逻辑待完善;默认 allowTapTakeoff=false
/// 时轻点屏幕不触发起飞):炮台飞到目标点 → 到达后持续向上发射子弹,upFireDuration
/// 到时自动降落回原炮台位;起飞时播放特殊 spine 动画(takeoffAnim 播一次,flyLoopAnim 循环衔接);
/// 期间炮口固定朝上、状态切换/开火/按住输入均被拦截(见 ChangeState/各开火入口)。
/// 起飞入口:TakeoffToScreenPos(当前轻点路径,受 allowTapTakeoff 控制);
/// 将来"起飞按钮"接入后走同一入口即可(或在面板上打开 allowTapTakeoff 恢复轻点起飞)。
/// 交互判定在 GameMainPanel_FishBase.HandleInput(轻点=按下短+几乎没移动;按住扫射不触发起飞)。
/// 挂载:XJMH 座位炮台(xjmhTurretList 中的子对象),飞机预制体挂在炮台节点下面(PW_Plane_x,默认隐藏)。
/// 注意:不要在本类定义 Awake/OnEnable,会隐藏基类的同名私有初始化逻辑。
/// </summary>
public class FlyTurretData : TurretData
{
[Header("起飞入口")]
[Tooltip("允许轻点屏幕起飞(默认关=false:没点起飞按钮就不起飞)。\n将来\"起飞按钮\"逻辑接入后走专门入口,不受本开关限制")]
public bool allowTapTakeoff = false;
[Header("起飞(点哪飞哪)")]
[Tooltip("起飞特殊 spine 动画名(起飞时播一次;为空则直接播飞行循环动画)")]
public string takeoffAnim;
[Tooltip("飞行/悬停循环 spine 动画名(为空则起飞动画播完后循环自身)")]
public string flyLoopAnim;
[Tooltip("飞行到目标点的时长(秒)")]
public float flyDuration = 0.6f;
[Tooltip("飞行插值:0=匀速 1=平滑缓入缓出")]
[Range(0f, 1f)]
public float flySmooth = 1f;
[Tooltip("目标点距画布边缘的最小内边距(画布本地单位),防止炮台飞出屏幕")]
public float edgePadding = 60f;
[Header("向上发射子弹")]
[Tooltip("向上开火间隔(秒)")]
public float upFireInterval = 0.4f;
[Tooltip("向上开火总时长(秒),<=0 表示一直开火(手动点击别处可改变目标)")]
public float upFireDuration = 0f;
[Tooltip("到达目标点后延迟多久开始向上开火(秒),通常等起飞动画收尾")]
public float upFireDelay = 0f;
/// <summary>向上开火回调:由 GameMainPanel_FishBase 注入(走服务端同步链路 SyncFire)</summary>
[NonSerialized] public Action onUpwardFire;
/// <summary>是否处于飞行/悬停状态(含飞回降落途中)</summary>
public bool IsFlying { get; private set; }
private RectTransform _rect;
private RectTransform _parentRect;
private RectTransform _canvasRect;
private Camera _uiCam;
private Vector2 _originAnchoredPos; // 首次起飞前的原位(降落飞回这里)
private Vector2 _flyFrom;
private Vector2 _flyTo;
private float _flyTimer;
private bool _returning; // true=正在飞回原位降落
private float _fireDelayTimer;
private float _fireTimer;
private float _fireTotalTimer;
private bool _firing;
/// <summary>懒加载引用(不用 Awake,避免隐藏基类私有 Awake)</summary>
private void EnsureRefs()
{
if (_rect != null) return;
_rect = GetComponent<RectTransform>();
_parentRect = transform.parent as RectTransform;
Canvas canvas = null;
if (UIManager.Instance != null && UIManager.Instance.uiRoot != null)
canvas = UIManager.Instance.uiRoot.GetComponent<Canvas>();
if (canvas != null)
{
_canvasRect = canvas.GetComponent<RectTransform>();
if (canvas.renderMode != RenderMode.ScreenSpaceOverlay)
_uiCam = canvas.worldCamera;
}
}
private void Update()
{
if (!IsFlying) return;
UpdateFly();
UpdateUpFire();
}
/// <summary>
/// 起飞/移动到目标屏幕点(点到哪飞到哪)。
/// 首次调用=进入起飞模式;飞行中再次调用=改目标点;降落中调用=取消降落改飞新目标。
/// ⭐ 轻点起飞总开关:未进入起飞模式时受 allowTapTakeoff 控制——默认 false,
/// 即"没点起飞按钮就不起飞"(轻点松开只会走正常 Idle/开火流程);飞行中改目标不受此限制。
/// 返回 false 表示未起飞(开关未开 / 节点不是 UGUI / 画布引用缺失)。
/// </summary>
public bool TakeoffToScreenPos(Vector2 screenPos)
{
// ⭐ 默认不响应轻点起飞("起飞先不做",起飞按钮逻辑待完善):进入起飞模式前先过开关。
// 已在飞行=起飞模式内部行为(改目标点),不受开关限制
if (!IsFlying && !allowTapTakeoff) return false;
EnsureRefs();
if (_rect == null || _parentRect == null || _canvasRect == null) return false;
if (!IsFlying)
{
_originAnchoredPos = _rect.anchoredPosition;
IsFlying = true;
PlayTakeoffAnim();
PointUp();
}
_returning = false;
_flyFrom = _rect.anchoredPosition;
_flyTo = ScreenPosToAnchoredPos(screenPos);
_flyTimer = 0f;
// 改目标瞬间先停火,到达新目标后按延迟恢复
_firing = false;
_fireDelayTimer = 0f;
return true;
}
/// <summary>降落:停火并飞回起飞前的原位</summary>
public void Land()
{
if (!IsFlying || _returning) return;
_firing = false;
_returning = true;
_flyFrom = _rect.anchoredPosition;
_flyTo = _originAnchoredPos;
_flyTimer = 0f;
}
// ==================== 内部逻辑 ====================
private void UpdateFly()
{
_flyTimer += Time.deltaTime;
float t = flyDuration > 0f ? Mathf.Clamp01(_flyTimer / flyDuration) : 1f;
if (flySmooth > 0f)
t = Mathf.Lerp(t, t * t * (3f - 2f * t), flySmooth);
_rect.anchoredPosition = Vector2.LerpUnclamped(_flyFrom, _flyTo, t);
if (_flyTimer < flyDuration) return;
_rect.anchoredPosition = _flyTo;
if (_returning)
{
// 落地:恢复待机动画,退出飞行状态
IsFlying = false;
_returning = false;
RestoreIdleAnim();
}
else
{
StartUpFire();
}
}
private void StartUpFire()
{
_firing = true;
_fireDelayTimer = upFireDelay;
_fireTimer = upFireInterval; // 延迟结束后立即打第一发
_fireTotalTimer = 0f;
}
private void UpdateUpFire()
{
if (!_firing) return;
// 到达后延迟(等起飞动画收尾)
if (_fireDelayTimer > 0f)
{
_fireDelayTimer -= Time.deltaTime;
return;
}
// 总时长限制(<=0 = 一直开火)
if (upFireDuration > 0f)
{
_fireTotalTimer += Time.deltaTime;
if (_fireTotalTimer >= upFireDuration)
{
Land();
return;
}
}
_fireTimer += Time.deltaTime;
if (_fireTimer < upFireInterval) return;
_fireTimer = 0f;
onUpwardFire?.Invoke();
}
/// <summary>屏幕坐标 → 炮台父节点下的 anchoredPosition(限制在画布范围内)</summary>
private Vector2 ScreenPosToAnchoredPos(Vector2 screenPos)
{
// 屏幕坐标 → 画布根节点本地坐标(相机解析模式与扇形切换一致)
RectTransformUtility.ScreenPointToLocalPointInRectangle(
_canvasRect, screenPos, _uiCam, out Vector2 canvasLocal);
// 限制在画布内(留边距),防止炮台飞出屏幕
Rect r = _canvasRect.rect;
float xMin = r.xMin + edgePadding;
float xMax = r.xMax - edgePadding;
float yMin = r.yMin + edgePadding;
float yMax = r.yMax - edgePadding;
if (xMax < xMin) { xMin = xMax = r.center.x; }
if (yMax < yMin) { yMin = yMax = r.center.y; }
canvasLocal = new Vector2(
Mathf.Clamp(canvasLocal.x, xMin, xMax),
Mathf.Clamp(canvasLocal.y, yMin, yMax));
// 画布本地 → 世界 → 用“世界增量”换算成父节点下的 anchoredPosition。
// 增量法兼容父节点的翻转/旋转/缩放(上方座位 180° 翻转也能正确落点),
// 且不依赖炮台自身的锚点设置。
Vector3 targetWorld = _canvasRect.TransformPoint(canvasLocal);
Vector3 deltaLocal = _parentRect.InverseTransformVector(targetWorld - _rect.position);
return _rect.anchoredPosition + (Vector2)deltaLocal;
}
/// <summary>炮口固定朝上:SetRotation 的 angle 约定 0=正上方(aim-90)</summary>
private void PointUp()
{
if (setPosData != null)
{
setPosData.SetRotation(0f);
return;
}
Transform pivot = tra_RotationPivot != null ? tra_RotationPivot : transform.parent;
if (pivot != null)
pivot.rotation = Quaternion.Euler(0f, 0f, 0f);
}
/// <summary>起飞动画:takeoffAnim 播一次,flyLoopAnim 循环衔接(为空则循环自身兜底)</summary>
private void PlayTakeoffAnim()
{
if (skeletonAnimation == null || skeletonAnimation.AnimationState == null) return;
if (!string.IsNullOrEmpty(takeoffAnim))
{
skeletonAnimation.AnimationState.SetAnimation(0, takeoffAnim, false);
if (!string.IsNullOrEmpty(flyLoopAnim))
skeletonAnimation.AnimationState.AddAnimation(0, flyLoopAnim, true, 0f);
else
skeletonAnimation.AnimationState.AddAnimation(0, takeoffAnim, true, 0f);
}
else if (!string.IsNullOrEmpty(flyLoopAnim))
{
skeletonAnimation.AnimationState.SetAnimation(0, flyLoopAnim, true);
}
}
/// <summary>落地后恢复待机动画(先 None 再 Idle,避免 currState 相同被基类短路)</summary>
private void RestoreIdleAnim()
{
base.ChangeState(TurretState.None);
base.ChangeState(TurretState.Idle);
}
public override void ChangeState(TurretState newState)
{
// 飞行/悬停期间拦截状态动画(Attack/Idle 切换不打断起飞动画)
if (IsFlying) return;
base.ChangeState(newState);
}
// ==================== XJMH 进房入场动画(原版 Battery:EnterAction,进入房间时本地座位播一次) ====================
// 原版链路(PW_Data.InitPlayer 收到自己"大飞机信息"):
// GC.BatteryMgr.PlayEnterAction(chairId, PW_Data.EnterAction) —— 首次 false 播完整动画+声音,
// 之后 true 只立即摆好。完整流程:
// Enter 音效 → 容器(底座)Spine 播 "out" → 炮台本体隐藏、飞机出现 →
// 飞机从下方两段升起(EInQuart 0.5s → 停 0.3s → EOutQuart 0.5s)→
// PlaneBack 音效 + 飞机播 "back" → 约 1 秒后飞机隐藏、炮台本体播 "out" 出现。
[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 音效 → 容器 "out" → 飞机从停靠位下方两段升起(PlaneBack 音效 + "back")→
/// 延迟后飞机隐藏、炮台本体播 "out" 出现。
/// ⭐ 未激活(面板加载中/座位隐藏)时直接返回——SitPosData 会挂起并在激活后补播。
/// 找不到飞机节点时只播炮台出现动画(兜底不报错)。
/// </summary>
public void PlayEnterAction()
{
if (!isActiveAndEnabled) return;
Debug.Log($"[XJMH入场] {name} 开始播放(Enter音效={enterAudioID} 回位音效={enterBackAudioID})");
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)
{
Debug.LogWarning($"[XJMH入场] {name} 未找到飞机节点(名字含 feiji),只播炮台出现动画");
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);
// ⭐ 原版 EnterAction 第 6 步:底部(容器/底座外壳)Spine 播 "out" 出场
// (PW_Battery.txt:EnterAction → bottomSkeleton.AnimationState:SetAnimation(0,"out",false))
PlayContainerOutAnim();
// 原版:入场时炮台本体隐藏、飞机出现(飞机从下方升上来,"变"回炮台)
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>
/// 容器(底座/炮台外壳)出场动画:XJMH 炮台容器自身的 Spine 播 "out"(容器动画为 close/open/out)。
/// 对应原版 EnterAction 第 6 步 bottomSkeleton "out";容器上没有该动画时跳过、不报错。
/// </summary>
private void PlayContainerOutAnim()
{
var shell = GetComponent<SkeletonGraphic>();
if (shell == null || shell.AnimationState == null) return;
if (shell.Skeleton != null && shell.Skeleton.Data != null
&& shell.Skeleton.Data.FindAnimation("out") == null) return; // 没有 out 动画:跳过
shell.AnimationState.SetAnimation(0, "out", false);
shell.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>入场动画清理:停协程 + 复位(飞机隐藏回停靠位、炮台本体恢复显示),禁用/回收不残留</summary>
private 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()
{
// ⭐ 入场动画清理(停协程、飞机复位隐藏、炮台本体恢复显示):必须在提前 return 之前调用
CleanupEnterAction();
// 切炮台/座位被禁用时兜底复位
if (!IsFlying) return;
IsFlying = false;
_returning = false;
_firing = false;
if (_rect != null)
_rect.anchoredPosition = _originAnchoredPos;
}
}