278 lines
9.7 KiB
C#
278 lines
9.7 KiB
C#
using System;
|
||
using UnityEngine;
|
||
|
||
/// <summary>
|
||
/// 起飞炮台(TurretData 子类):
|
||
/// 平时是一门正常炮台(照常手动瞄准开火/自动开火/锁定瞄准);
|
||
/// 额外支持起飞模式:轻点空白处 → 炮台飞到该点(点到哪飞到哪)→ 到达后持续向上发射子弹,
|
||
/// upFireDuration 到时自动降落回原位恢复正常开火;起飞时播放特殊 spine 动画
|
||
/// (takeoffAnim 播一次,flyLoopAnim 循环衔接)。
|
||
/// 交互判定在 GameMainPanel_FishBase.HandleInput(轻点=按下短+几乎没移动;按住扫射不触发起飞)。
|
||
/// 挂在 XJMH 座位炮台(xjmhTurretList 中的子对象)上即可,无需改预制体结构。
|
||
/// 注意:不要在本类定义 Awake/OnEnable,会隐藏基类的同名私有初始化逻辑。
|
||
/// </summary>
|
||
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;
|
||
|
||
/// <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>
|
||
/// 起飞/移动到目标屏幕点(点到哪飞到哪)。
|
||
/// 首次调用=起飞;飞行中再次调用=改目标点;降落中调用=取消降落改飞新目标。
|
||
/// 返回 false 表示未起飞(节点不是 UGUI 或画布引用缺失)。
|
||
/// </summary>
|
||
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;
|
||
}
|
||
|
||
/// <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);
|
||
}
|
||
|
||
private void OnDisable()
|
||
{
|
||
// 切炮台/座位被禁用时兜底复位(基类无 OnDisable,不会冲突)
|
||
if (!IsFlying) return;
|
||
|
||
IsFlying = false;
|
||
_returning = false;
|
||
_firing = false;
|
||
if (_rect != null)
|
||
_rect.anchoredPosition = _originAnchoredPos;
|
||
}
|
||
}
|