using System; using UnityEngine; /// /// 起飞炮台(TurretData 子类): /// 平时是一门正常炮台(照常手动瞄准开火/自动开火/锁定瞄准); /// 额外支持起飞模式:轻点空白处 → 炮台飞到该点(点到哪飞到哪)→ 到达后持续向上发射子弹, /// upFireDuration 到时自动降落回原位恢复正常开火;起飞时播放特殊 spine 动画 /// (takeoffAnim 播一次,flyLoopAnim 循环衔接)。 /// 交互判定在 GameMainPanel_FishBase.HandleInput(轻点=按下短+几乎没移动;按住扫射不触发起飞)。 /// 挂在 XJMH 座位炮台(xjmhTurretList 中的子对象)上即可,无需改预制体结构。 /// 注意:不要在本类定义 Awake/OnEnable,会隐藏基类的同名私有初始化逻辑。 /// public class FlyTurretData : TurretData { [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; /// 向上开火回调:由 GameMainPanel_FishBase 注入(走服务端同步链路 SyncFire) [NonSerialized] public Action onUpwardFire; /// 是否处于飞行/悬停状态(含飞回降落途中) 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; /// 懒加载引用(不用 Awake,避免隐藏基类私有 Awake) private void EnsureRefs() { if (_rect != null) return; _rect = GetComponent(); _parentRect = transform.parent as RectTransform; Canvas canvas = null; if (UIManager.Instance != null && UIManager.Instance.uiRoot != null) canvas = UIManager.Instance.uiRoot.GetComponent(); if (canvas != null) { _canvasRect = canvas.GetComponent(); if (canvas.renderMode != RenderMode.ScreenSpaceOverlay) _uiCam = canvas.worldCamera; } } private void Update() { if (!IsFlying) return; UpdateFly(); UpdateUpFire(); } /// /// 起飞/移动到目标屏幕点(点到哪飞到哪)。 /// 首次调用=起飞;飞行中再次调用=改目标点;降落中调用=取消降落改飞新目标。 /// 返回 false 表示未起飞(节点不是 UGUI 或画布引用缺失)。 /// public bool TakeoffToScreenPos(Vector2 screenPos) { 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; } /// 降落:停火并飞回起飞前的原位 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(); } /// 屏幕坐标 → 炮台父节点下的 anchoredPosition(限制在画布范围内) 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; } /// 炮口固定朝上:SetRotation 的 angle 约定 0=正上方(aim-90) 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); } /// 起飞动画:takeoffAnim 播一次,flyLoopAnim 循环衔接(为空则循环自身兜底) 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); } } /// 落地后恢复待机动画(先 None 再 Idle,避免 currState 相同被基类短路) 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); } private void OnDisable() { // ⭐ 基类入场动画清理:子类 OnDisable 会覆盖基类的私有 OnDisable(Unity 只调最派生), // 必须显式调用,保证"飞机升起/炮台出场"的清理不被漏掉(在提前 return 之前调用) CleanupEnterAction(); // 切炮台/座位被禁用时兜底复位 if (!IsFlying) return; IsFlying = false; _returning = false; _firing = false; if (_rect != null) _rect.anchoredPosition = _originAnchoredPos; } }