Files
5.24BuYu/Assets/MyGame/Scripts/Turret/FlyTurretData.cs
T
2026-10-12 00:37:28 +08:00

766 lines
34 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_等级,含 Body 骨架/尾迹/护盾/免费特效——特效由本类按原版控制),找不到才退回飞机骨架节点
/// (lvptfeiji/lanptfeiji/huangjinfeiji/vipfeiji01)。飞行期间飞机临时挂到炮台下显示、结束还原。
/// 注意:不要在本类定义 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 Transform _enterPlaneRoot; // 入场中的飞机节点(PW_Plane_x 完整预制体实例;找不到时=骨架节点兜底)
private SkeletonGraphic _enterPlaneSg; // 飞机骨架(临时挂载后播 "stand01"/"back")
private bool _enterBoardHidden; // 入场期间 Board 被暂时隐藏(清理时只恢复自己隐藏的)
// ⭐ 飞机原始挂载信息:飞机在座位的变体容器节点里(该节点默认未激活,直接显隐看不到),
// 入场时临时挂到炮台下显示、结束/清理时按原样还原(父节点/层级/本地变换)
private Transform _enterPlaneOrigParent;
private int _enterPlaneOrigSibling;
private Vector3 _enterPlaneOrigLocalPos;
private Quaternion _enterPlaneOrigLocalRot;
private Vector3 _enterPlaneOrigLocalScale;
/// <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);
// ⭐ 飞机:优先完整飞机预制体实例 PW_Plane_等级(含 Body 骨架/尾迹/护盾/免费特效——由本类控制);
// 找不到才退回"骨架节点"(只有骨架、没有特效)
_enterPlaneRoot = FindPlaneRoot();
if (_enterPlaneRoot == null)
{
Debug.LogWarning($"[XJMH入场] {name} 未找到飞机节点(PW_Plane_x 或名字含 feiji),只播炮台出现动画");
PlayTurretOutAnim();
_enterRoutine = null;
yield break;
}
bool isFullPlane = _enterPlaneRoot.name.StartsWith("PW_Plane");
_enterPlaneSg = isFullPlane
? _enterPlaneRoot.GetComponentInChildren<SkeletonGraphic>(true)
: _enterPlaneRoot.GetComponent<SkeletonGraphic>();
Debug.Log($"[XJMH入场] {name} 飞机={_enterPlaneRoot.name}(完整飞机={isFullPlane} 骨架={(_enterPlaneSg != null ? _enterPlaneSg.name : "无")})");
// ⭐ 飞机临时挂到炮台下并保持停靠位(原挂载点=座位下的变体容器节点,默认未激活,不挂过来显示不了);
// 完整飞机:停靠位=炮台的 Aprk 节点(原版 self.aprkPos)
AttachPlaneForEnter();
Vector3 home = _enterPlaneRoot.localPosition;
Vector3 start = home + new Vector3(0f, enterStartOffsetY, 0f);
Vector3 mid = home + new Vector3(0f, enterStartOffsetY * 0.5f, 0f);
// ⭐ 原版 EnterAction 第 6 步:底部(容器/底座外壳)Spine 播 "out" 出场
// (PW_Battery.txt:EnterAction → bottomSkeleton.AnimationState:SetAnimation(0,"out",false))
PlayContainerOutAnim();
// 原版:入场时炮台本体隐藏、飞机出现(飞机从下方升上来,"变"回炮台);Board(分板)一并隐藏
if (skeletonAnimation != null)
skeletonAnimation.gameObject.SetActive(false);
Transform board = transform.Find("Board");
if (board != null) { board.gameObject.SetActive(false); _enterBoardHidden = true; }
// ⭐ 飞机特效控制(原版 EnterAction):开 Body(骨架+尾迹)与护盾;
// 免费/暴击/加速三类特效一律关(原版只在免费模式玩法里开,prefab 默认全开会一起炸出来)
SetPlaneChildActive("Body", true);
SetPlaneChildActive("PW_Effect_Defend", true);
SetPlaneChildActive("FreeEffect", false);
SetPlaneChildActive("FreeEffect_Crit", false);
SetPlaneChildActive("FreeEffect_Speedup", false);
_enterPlaneRoot.gameObject.SetActive(true);
_enterPlaneRoot.localPosition = start;
if (_enterPlaneSg != null && _enterPlaneSg.AnimationState != null)
{
// 原版飞机常驻飞行循环(PlaneFlyAnim:"stand01" 循环);清残留(对象池/重复入场)后先播它
_enterPlaneSg.AnimationState.ClearTrack(0);
if (HasSpineAnim(_enterPlaneSg, "stand01"))
_enterPlaneSg.AnimationState.SetAnimation(0, "stand01", true);
}
// 第一段:升到中途位(原版 EInQuart)
yield return RiseTo(_enterPlaneRoot, start, mid, enterRiseTime, true);
// 停顿(原版 delay 0.3)
if (enterHoldTime > 0f) yield return new WaitForSeconds(enterHoldTime);
// 第二段:升到停靠位(原版 EOutQuart)
yield return RiseTo(_enterPlaneRoot, mid, home, enterBackRiseTime, false);
// 原版 PlaneBack:护盾先收;PlaneBack 音效 + 飞机回位动画(PlaneBackAnim:"back")
SetPlaneChildActive("PW_Effect_Defend", false);
if (enterBackAudioID > 0 && AudioManager.Instance != null)
AudioManager.Instance.PlayerFX(enterBackAudioID);
if (_enterPlaneSg != null && _enterPlaneSg.AnimationState != null && HasSpineAnim(_enterPlaneSg, "back"))
_enterPlaneSg.AnimationState.SetAnimation(0, "back", false);
// 原版:back 动画约 1 秒后飞机本体(Body)隐藏、炮台出现(ShowGun → ShowBoard)
if (enterBackHoldTime > 0f) yield return new WaitForSeconds(enterBackHoldTime);
SetPlaneChildActive("Body", false);
DetachPlaneAfterEnter(); // 飞机隐藏 + 还原父节点/本地变换
_enterPlaneRoot = null;
_enterPlaneSg = null;
PlayTurretOutAnim();
if (board != null) board.gameObject.SetActive(true);
_enterBoardHidden = false;
_enterRoutine = null;
}
/// <summary>协程位移:EInQuart / EOutQuart 缓动(与原版 GC.Action 一致;用 localPosition 兼容完整飞机/骨架节点)</summary>
private static IEnumerator RiseTo(Transform tr, Vector3 from, Vector3 to, float time, bool easeIn)
{
if (tr == null) yield break;
if (time <= 0f) { tr.localPosition = 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);
tr.localPosition = Vector3.LerpUnclamped(from, to, e);
yield return null;
}
tr.localPosition = 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>
/// 容器(底座/炮台外壳)出场动画:容器 Spine 播 "out"(容器动画为 close/open/out)。
/// 对应原版 EnterAction 第 6 步 bottomSkeleton "out"(battery/Bottom 上的骨架);没有该动画时跳过、不报错。
/// </summary>
private void PlayContainerOutAnim()
{
var shell = FindContainerSkeleton();
if (shell == null || shell.AnimationState == null) return;
if (!HasSpineAnim(shell, "out")) return; // 没有 out 动画:跳过
shell.AnimationState.SetAnimation(0, "out", false);
shell.Update(0.1f);
}
/// <summary>
/// 找容器(底座外壳)骨架:① 炮台根节点自带(容器节点布局);
/// ② "Bottom" 子节点(原版 PW_Battery 结构,容器骨架在 Bottom 上,根节点没有);
/// ③ 兜底:子树里带 "out" 动画、且不是炮口本体/飞机的骨架。
/// </summary>
private SkeletonGraphic FindContainerSkeleton()
{
var self = GetComponent<SkeletonGraphic>();
if (self != null && self != skeletonAnimation && HasSpineAnim(self, "out")) return self;
Transform bottom = transform.Find("Bottom");
if (bottom != null)
{
var sg = bottom.GetComponent<SkeletonGraphic>();
if (sg != null && sg != skeletonAnimation) return sg;
}
foreach (var sg in GetComponentsInChildren<SkeletonGraphic>(true))
{
if (sg == null || sg == skeletonAnimation) continue;
if (sg.gameObject.name.ToLowerInvariant().Contains("feiji")) continue; // 飞机不算容器
if (HasSpineAnim(sg, "out")) return sg;
}
return null;
}
/// <summary>骨架数据里是否有该动画(SkeletonGraphic 已初始化才有效)</summary>
private static bool HasSpineAnim(SkeletonGraphic sg, string animName)
{
var data = sg != null && sg.Skeleton != null ? sg.Skeleton.Data : null;
return data != null && data.FindAnimation(animName) != null;
}
/// <summary>
/// 找入场飞机:① 完整飞机预制体实例(座位下名字以 "PW_Plane_等级" 开头,如 PW_Plane_1——含特效);
/// ② 骨架节点兜底(炮台/座位子树内名字含 "feiji" 的 SkeletonGraphic,如 lvptfeiji——只有骨架)。
/// </summary>
private Transform FindPlaneRoot()
{
var full = FindFullPlaneRoot();
if (full != null) return full;
return FindPlaneSkeletonRect();
}
/// <summary>找完整飞机预制体实例:先炮台子树、再座位子树;按当前炮台等级精确匹配,匹配不到用任意一架</summary>
private Transform FindFullPlaneRoot()
{
var t = FindFullPlaneUnder(transform);
if (t != null) return t;
var sit = GetComponentInParent<SitPosData>();
return sit != null ? FindFullPlaneUnder(sit.transform) : null;
}
private Transform FindFullPlaneUnder(Transform root)
{
int lv = ParseTurretLevel();
Transform any = null;
foreach (var t in root.GetComponentsInChildren<Transform>(true))
{
if (t == null || !t.name.StartsWith("PW_Plane")) continue;
if (lv > 0 && t.name.StartsWith("PW_Plane_" + lv)) return t;
if (any == null) any = t;
}
return any;
}
/// <summary>炮台的飞机停靠位节点(原版 self.aprk = battery:FindChild("Aprk"))</summary>
private Transform FindAprkNode()
{
Transform t = transform.Find("Aprk");
if (t != null) return t;
foreach (var c in GetComponentsInChildren<Transform>(true))
if (c.name.ToLowerInvariant().Contains("aprk")) return c;
return null;
}
/// <summary>
/// 飞机子节点显隐(只对完整飞机 PW_Plane 生效;骨架兜底路径没有这些特效节点,自动跳过):
/// Body=飞机本体(骨架+尾迹)、PW_Effect_Defend=护盾、FreeEffect/FreeEffect_Crit/FreeEffect_Speedup=免费模式三类特效。
/// </summary>
private void SetPlaneChildActive(string childName, bool active)
{
SetPlaneChildActive(_enterPlaneRoot, childName, active);
}
private static void SetPlaneChildActive(Transform planeRoot, string childName, bool active)
{
if (planeRoot == null || !planeRoot.name.StartsWith("PW_Plane")) return;
Transform t = planeRoot.Find(childName);
if (t != null) t.gameObject.SetActive(active);
}
// ==================== 入场飞机预热(进房加载收尾时由面板调用,卡顿被进度条遮住) ====================
// XJMH 进入房间时"命中"的那架完整飞机(骨架 + 尾迹/护盾十多个粒子系统 + 贴图首次上传)首帧初始化很重;
// 全部堆在"加载到100%、进度条已隐藏"那一帧做,玩家会看到明显卡顿(其他玩法没有这套 XJMH 独有资产)。
// 预热 = 在进度条还在走时把飞机临时挂到激活的父节点下、缩到 0 隐藏,激活一两帧完成初始化,再原样还原。
private Coroutine _prewarmRoutine;
private Transform _prewarmPlane; // 预热中的飞机(被打断时还原用)
private Transform _prewarmOrigParent;
private int _prewarmOrigSibling;
private Vector3 _prewarmOrigPos;
private Quaternion _prewarmOrigRot;
private Vector3 _prewarmOrigScale;
private bool _prewarmOrigActive;
/// <summary>入场飞机预热(面板在加载收尾阶段调用;座位/炮台需已激活)。幂等:重复调用只跑一次。</summary>
public void PrewarmEnterPlane()
{
if (!isActiveAndEnabled) return;
if (_prewarmRoutine != null) return;
_prewarmRoutine = StartCoroutine(PrewarmPlaneRoutine());
}
private IEnumerator PrewarmPlaneRoutine()
{
Transform plane = FindPlaneRoot();
if (plane == null) { _prewarmRoutine = null; yield break; }
_prewarmPlane = plane;
_prewarmOrigParent = plane.parent;
_prewarmOrigSibling = plane.GetSiblingIndex();
_prewarmOrigPos = plane.localPosition;
_prewarmOrigRot = plane.localRotation;
_prewarmOrigScale = plane.localScale;
_prewarmOrigActive = plane.gameObject.activeSelf;
// 只开入场要用的 Body+护盾、关掉免费三类——避免把五十来个粒子系统全部初始化(反而更重)
SetPlaneChildActive(plane, "Body", true);
SetPlaneChildActive(plane, "PW_Effect_Defend", true);
SetPlaneChildActive(plane, "FreeEffect", false);
SetPlaneChildActive(plane, "FreeEffect_Crit", false);
SetPlaneChildActive(plane, "FreeEffect_Speedup", false);
// 临时挂到根节点(激活中)并缩到 0:既能触发初始化,又完全不可见
Transform holder = transform.root;
if (holder != null && holder != _prewarmOrigParent)
plane.SetParent(holder, true);
plane.localScale = Vector3.zero;
plane.gameObject.SetActive(true);
yield return null;
yield return null; // 让骨架首帧网格、粒子首帧模拟都跑到
FinishPrewarmRestore();
Debug.Log($"[XJMH入场] 飞机预热完成({plane.name})");
_prewarmRoutine = null;
}
/// <summary>预热还原:飞机隐藏并恢复到预热前的挂载/变换/激活状态(协程正常收尾或中途被打断都用它)</summary>
private void FinishPrewarmRestore()
{
if (_prewarmPlane == null) return;
Transform plane = _prewarmPlane;
_prewarmPlane = null;
plane.gameObject.SetActive(false);
if (_prewarmOrigParent != null && plane.parent != _prewarmOrigParent)
{
plane.SetParent(_prewarmOrigParent, false);
plane.SetSiblingIndex(_prewarmOrigSibling);
}
plane.localPosition = _prewarmOrigPos;
plane.localRotation = _prewarmOrigRot;
plane.localScale = _prewarmOrigScale;
plane.gameObject.SetActive(_prewarmOrigActive);
}
/// <summary>预热中途被打断(座位/炮台被禁用):停协程并把飞机还原回原挂载点,避免残留</summary>
private void CleanupPrewarm()
{
if (_prewarmRoutine != null) { StopCoroutine(_prewarmRoutine); _prewarmRoutine = null; }
FinishPrewarmRestore();
}
/// <summary>
/// 找飞机骨架:① 炮台子树(飞机挂在炮台下的布局);
/// ② 座位子树(当前 XJMH 布局:飞机与炮台平级、挂在座位的变体容器节点里,
/// 如 lvptfeiji/lanptfeiji/huangjinfeiji/vipfeiji01)——先按当前炮台等级挑对应飞机,挑不到用座位下任意飞机兜底。
/// </summary>
private RectTransform FindPlaneSkeletonRect()
{
var plane = FindPlaneSkeletonUnder(transform, null);
if (plane != null) return plane;
var sit = GetComponentInParent<SitPosData>();
if (sit == null) return null;
return FindPlaneSkeletonUnder(sit.transform, GetPlaneKeyword());
}
/// <summary>在 root 子树里找名字含 "feiji" 的 SkeletonGraphic;keyword 非空时优先匹配(如 lvpt)</summary>
private static RectTransform FindPlaneSkeletonUnder(Transform root, string keyword)
{
RectTransform any = null;
foreach (var sg in root.GetComponentsInChildren<SkeletonGraphic>(true))
{
if (sg == null) continue;
string n = sg.gameObject.name.ToLowerInvariant();
if (!n.Contains("feiji")) continue;
if (string.IsNullOrEmpty(keyword) || n.Contains(keyword)) return sg.rectTransform;
if (any == null) any = sg.rectTransform;
}
return any;
}
/// <summary>当前炮台等级 → 座位内飞机名关键字(原版 PW_Plane_1~4 = 绿/蓝/金/VIP 机)</summary>
private string GetPlaneKeyword()
{
switch (ParseTurretLevel())
{
case 1: return "lvpt";
case 2: return "lanpt";
case 3: return "huangjin";
case 4: return "vip";
default: return null;
}
}
/// <summary>炮台等级:优先 turretID(变体容器炮台配了 1~4),否则从名字里取数字(PW_Battery_2 → 2)</summary>
private int ParseTurretLevel()
{
if (!string.IsNullOrEmpty(turretID) && int.TryParse(turretID.Trim(), out int id) && id > 0)
return id;
foreach (char c in name)
if (char.IsDigit(c)) return c - '0';
return 0;
}
/// <summary>入场开始:飞机临时挂到炮台下(保留世界位置/旋转/缩放)——原挂载点(座位下的变体容器节点)默认未激活,直接显隐看不到;
/// 完整飞机额外摆到炮台 Aprk 停靠位(原版 aprkPos)</summary>
private void AttachPlaneForEnter()
{
_enterPlaneOrigParent = _enterPlaneRoot.parent;
_enterPlaneOrigSibling = _enterPlaneRoot.GetSiblingIndex();
_enterPlaneOrigLocalPos = _enterPlaneRoot.localPosition;
_enterPlaneOrigLocalRot = _enterPlaneRoot.localRotation;
_enterPlaneOrigLocalScale = _enterPlaneRoot.localScale;
if (_enterPlaneRoot.parent != transform)
_enterPlaneRoot.SetParent(transform, true); // true=保留世界变换,停靠位不跳
if (_enterPlaneRoot.name.StartsWith("PW_Plane"))
{
Transform aprk = FindAprkNode();
if (aprk != null) _enterPlaneRoot.position = aprk.position; // 停靠位=炮台 Aprk(原版)
}
}
/// <summary>入场结束/清理:飞机隐藏并还原父节点与本地变换,不残留运行时改动</summary>
private void DetachPlaneAfterEnter()
{
if (_enterPlaneRoot == null) return;
_enterPlaneRoot.gameObject.SetActive(false);
if (_enterPlaneOrigParent != null && _enterPlaneRoot.parent != _enterPlaneOrigParent)
{
_enterPlaneRoot.SetParent(_enterPlaneOrigParent, false);
_enterPlaneRoot.SetSiblingIndex(_enterPlaneOrigSibling);
}
_enterPlaneRoot.localPosition = _enterPlaneOrigLocalPos;
_enterPlaneRoot.localRotation = _enterPlaneOrigLocalRot;
_enterPlaneRoot.localScale = _enterPlaneOrigLocalScale;
}
/// <summary>入场动画清理:停协程 + 飞机隐藏并还原挂载 + Board/炮台本体恢复显示,禁用/回收不残留</summary>
private void CleanupEnterAction()
{
if (_enterRoutine != null) { StopCoroutine(_enterRoutine); _enterRoutine = null; }
if (_enterPlaneRoot != null)
{
DetachPlaneAfterEnter();
_enterPlaneRoot = null;
_enterPlaneSg = null;
}
// 入场中途被打断:恢复被本类暂时隐藏的 Board(只恢复自己隐藏的,不动其他逻辑的显隐)
if (_enterBoardHidden)
{
Transform board = transform.Find("Board");
if (board != null) board.gameObject.SetActive(true);
_enterBoardHidden = false;
}
if (skeletonAnimation != null && !skeletonAnimation.gameObject.activeSelf)
skeletonAnimation.gameObject.SetActive(true);
}
private void OnDisable()
{
// ⭐ 入场动画清理(停协程、飞机复位隐藏、炮台本体恢复显示):必须在提前 return 之前调用
CleanupEnterAction();
// ⭐ 预热中途被打断:还原临时挂载的飞机,避免残留
CleanupPrewarm();
// 切炮台/座位被禁用时兜底复位
if (!IsFlying) return;
IsFlying = false;
_returning = false;
_firing = false;
if (_rect != null)
_rect.anchoredPosition = _originAnchoredPos;
}
}