This commit is contained in:
2026-09-02 16:27:35 +08:00
parent e85459257a
commit 63b1eb772e
6 changed files with 2304 additions and 7 deletions
@@ -0,0 +1,277 @@
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;
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: e2fbda49303fb1543a68b553ec721872
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
+2 -2
View File
@@ -31,7 +31,7 @@ public class TurretData : MonoBehaviour
public Transform tra_BulletStartPosition;
[Tooltip("炮台旋转节点。拖入后 SitPosData.tra_CurrTurret 用此节点旋转;不拖则用炮台自身的父节点(兼容旧逻辑)")]
public Transform tra_RotationPivot;
public GameObject bulletPrefab_Two;
public GameObject bulletPrefab_Two;
public GameObject bulletPrefab;
[Header("战神激光(锁定时显示,普通炮台不配置即无此效果)")]
@@ -244,7 +244,7 @@ public class TurretData : MonoBehaviour
public void ChangeState(TurretState newState)
public virtual void ChangeState(TurretState newState)
{
if (currState == newState) return;