From 2aa6a8f352a2fb0129abedc580aa4da20cd26e89 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=A3=AE2003?= <2384017988@qq.com> Date: Sun, 27 Sep 2026 20:34:42 +0800 Subject: [PATCH] 0 --- Assets/Framework/Event/ConstEventString.cs | 2 + .../Scripts/Net/WebResponses/SyncGameData.cs | 18 ++ .../Scripts/Net/WebSocketReponseList.cs | 4 + .../Scripts/Net/WebSocketSendList_Fish.cs | 3 +- .../Game/FLZY/BOSS/BaoJiangNotSelfHandler.cs | 4 +- .../FLZY/TSY/JiGuang/JiGuangXie_DieEvent.cs | 6 +- .../FLZY/TSY/ZuanTou/ZuanTouXie_DieEvent.cs | 155 ++++++++++++++++-- 7 files changed, 171 insertions(+), 21 deletions(-) diff --git a/Assets/Framework/Event/ConstEventString.cs b/Assets/Framework/Event/ConstEventString.cs index 25f0de58c..349dad6a0 100644 --- a/Assets/Framework/Event/ConstEventString.cs +++ b/Assets/Framework/Event/ConstEventString.cs @@ -19,6 +19,8 @@ public class ConstEventString // SyncGameData_RequestSyncFish 已废弃:鱼同步改由服务端 OnFishSpawn(type=sync) 下发 /// 爆奖结束帧同步(baoJiangEnd):某玩家爆奖流程完成,其他客户端显示最终 BJEndUI 并加分 public const string BaoJiangEnd = "BaoJiangEnd"; + /// 钻头蟹/激光蟹释放帧同步(zuanTouRelease):击杀者点击(或超时)发射,所有客户端(含击杀者回广播)统一时刻发射钻头/激光 + public const string SyncGameData_ZuanTouRelease = "SyncGameData_ZuanTouRelease"; // ================================================================ // 服务端 → 客户端事件 (新协议) diff --git a/Assets/MyGame/Scripts/Net/WebResponses/SyncGameData.cs b/Assets/MyGame/Scripts/Net/WebResponses/SyncGameData.cs index 42f394fb7..d14f17b38 100644 --- a/Assets/MyGame/Scripts/Net/WebResponses/SyncGameData.cs +++ b/Assets/MyGame/Scripts/Net/WebResponses/SyncGameData.cs @@ -59,6 +59,8 @@ namespace Assets.MyGame.Scripts.Net.WebResponses changeZhanShen=13, /// 表情包:某玩家发送表情(spine动画/文字),广播给其他客户端在对应座位显示 biaoQing=14, + /// 钻头蟹/激光蟹释放:击杀者点击(或超时)发射钻头/激光,广播给房间所有客户端(含发送者回广播),各端统一时刻发射 + zuanTouRelease=15, } [Serializable] public class FireCannonSendData @@ -210,6 +212,22 @@ namespace Assets.MyGame.Scripts.Net.WebResponses public string content; } + /// + /// 钻头蟹/激光蟹释放帧数据(frameType=zuanTouRelease)。 + /// 击杀者点击(或超时)时由击杀者客户端发送(机器人击杀由房主代发), + /// 服务器广播给房间所有人(含发送者回广播),各客户端按 fishId 匹配本地爆奖事件统一发射。 + /// + [Serializable] + public class FrameData_ZuanTouRelease : FrameData + { + /// 击杀者 userId + public string userId; + /// 死亡的特殊鱼 ID(各端用它匹配本地正在等待释放的爆奖事件实例) + public string fishId; + /// 发射时的炮台源角度(下方座位坐标系,接收端按该座位在本端显示位置用 SeatMirror 还原) + public float angle; + } + // ================================================================ // OnFishSpawn 事件数据结构 (服务端出鱼,破坏性替代 createFish 帧同步) // ================================================================ diff --git a/Assets/MyGame/Scripts/Net/WebSocketReponseList.cs b/Assets/MyGame/Scripts/Net/WebSocketReponseList.cs index 262ab2bfc..c61c463c4 100644 --- a/Assets/MyGame/Scripts/Net/WebSocketReponseList.cs +++ b/Assets/MyGame/Scripts/Net/WebSocketReponseList.cs @@ -383,6 +383,10 @@ namespace Assets.MyGame.Scripts.Net case FrameDataType.baoJiangEnd: EventCenter.Instance.EventTrigger(ConstEventString.BaoJiangEnd, syncGameData); break; + // ⭐ 钻头蟹/激光蟹释放:击杀者点击(或超时)广播,各端(含击杀者回广播)统一时刻发射钻头/激光 + case FrameDataType.zuanTouRelease: + EventCenter.Instance.EventTrigger(ConstEventString.SyncGameData_ZuanTouRelease, syncGameData); + break; default: break; } diff --git a/Assets/MyGame/Scripts/Net/WebSocketSendList_Fish.cs b/Assets/MyGame/Scripts/Net/WebSocketSendList_Fish.cs index 0a341d977..efa1096a6 100644 --- a/Assets/MyGame/Scripts/Net/WebSocketSendList_Fish.cs +++ b/Assets/MyGame/Scripts/Net/WebSocketSendList_Fish.cs @@ -310,7 +310,8 @@ public partial class WebSocketSendList { case FrameDataType.fireBullet: case FrameDataType.rotateTurrect: - // ⭐ 开火/旋转实时发送,和开炮一样立即广播,保证炮台旋转流畅 + case FrameDataType.zuanTouRelease: + // ⭐ 开火/旋转/钻头释放实时发送,和开炮一样立即广播,保证各端同时刻触发 SendImmediate(BuildJson(sgd)); return; diff --git a/Assets/MyGame/aa/Game/FLZY/BOSS/BaoJiangNotSelfHandler.cs b/Assets/MyGame/aa/Game/FLZY/BOSS/BaoJiangNotSelfHandler.cs index 26768ada2..f88832051 100644 --- a/Assets/MyGame/aa/Game/FLZY/BOSS/BaoJiangNotSelfHandler.cs +++ b/Assets/MyGame/aa/Game/FLZY/BOSS/BaoJiangNotSelfHandler.cs @@ -190,8 +190,8 @@ public static class BaoJiangNotSelfHandler Debug.Log($"[BaoJiangNotSelfHandler] 房主已代发 baoJiangEnd 帧同步:key={key} beishu={beishu} score={score}"); } - /// 判断当前本地客户端是否为房主(用于决定是否代发机器人帧同步) - private static bool IsLocalRoomOwner() + /// 判断当前本地客户端是否为房主(用于决定是否代发机器人帧同步;钻头蟹/激光蟹释放代发也复用) + public static bool IsLocalRoomOwner() { return SitPosData.roomOwnerSit != null && GameManager.Instance != null diff --git a/Assets/MyGame/aa/Game/FLZY/TSY/JiGuang/JiGuangXie_DieEvent.cs b/Assets/MyGame/aa/Game/FLZY/TSY/JiGuang/JiGuangXie_DieEvent.cs index 8781c8925..bfe70e60b 100644 --- a/Assets/MyGame/aa/Game/FLZY/TSY/JiGuang/JiGuangXie_DieEvent.cs +++ b/Assets/MyGame/aa/Game/FLZY/TSY/JiGuang/JiGuangXie_DieEvent.cs @@ -2,6 +2,8 @@ using UnityEngine; /// /// 特殊鱼:激光蟹 死亡玩法(继承 ZuanTouXie_DieEvent,只覆写发射部分)。 +/// ⭐ 多人释放触发与钻头蟹同源:击杀者点击(或超时) → zuanTouRelease 帧同步 → +/// 服务器广播全房间(含击杀者回广播) → 各端统一时刻启动激光(机器人击杀由房主代发)。 /// 和钻头蟹完全同构,区别仅在"点击发射": /// 钻头蟹 → 发射钻头子弹(SpecialBulletMove:反弹/碰鱼爆炸) /// 激光蟹 → 生成 3D 世界横穿屏幕的激光(SpecialLaserMove:蓝→紫两形态,扫到就杀) @@ -35,9 +37,11 @@ public class JiGuangXie_DieEvent : ZuanTouXie_DieEvent /// /// 覆写发射:直接使用【炮台预制体上配置好的 SpecialLaserMove】组件(特效引用在炮台里拖好), /// 调它的 Init 启动激光(hit → 主激光 → 切换形态,扫到就杀)。 + /// 激光方向由炮台自身旋转决定(各端炮台角度经 rotateTurrect 帧同步,基本一致), + /// 参数 angle 仅供签名统一,激光不使用。 /// 返回 holder:激光跑完 onFinished 时销毁,父类 while(drill != null) 检测到走结算。 /// - protected override GameObject FireDrill(bool isKiller) + protected override GameObject FireDrill(bool isKiller, float angle) { if (_sit == null) return null; diff --git a/Assets/MyGame/aa/Game/FLZY/TSY/ZuanTou/ZuanTouXie_DieEvent.cs b/Assets/MyGame/aa/Game/FLZY/TSY/ZuanTou/ZuanTouXie_DieEvent.cs index 3d39db8c3..cf8fa62a2 100644 --- a/Assets/MyGame/aa/Game/FLZY/TSY/ZuanTou/ZuanTouXie_DieEvent.cs +++ b/Assets/MyGame/aa/Game/FLZY/TSY/ZuanTou/ZuanTouXie_DieEvent.cs @@ -1,5 +1,7 @@ +using Assets.MyGame.Scripts.Net.WebResponses; using System; using System.Collections; +using System.Collections.Generic; using UnityEngine; using UnityEngine.InputSystem; @@ -10,15 +12,19 @@ using UnityEngine.InputSystem; /// 1. 死亡 → 普通炮台被钻头效果覆盖:关闭 turretParent, /// 在 tSYPTParent 下生成钻头炮台(localPos=0,不改默认旋转), /// isDrillActive=true 禁用普通开火(期间点击/拖动只瞄准); -/// 2. 点击屏幕 → 按普通子弹的创建方式发射钻头 -/// (位置=GetBulletStartWorldPos,旋转=Euler(0,0,当前瞄准角)); +/// 2. ⭐多人帧同步触发:击杀者点击(或超时 maxWaitTime) → 发 zuanTouRelease 帧同步 → +/// 服务器广播给房间所有人(含击杀者回广播),所有端收到帧才发射(角度用帧里带的,按座位镜像还原) +/// → 自己和别人界面同一时刻发射;机器人击杀由房主代发(otherReleaseDelay 延迟); +/// 长时间没收到帧(击杀者掉线)则各端兜底本地发射,避免卡死; +/// 3. 收到帧同步 → 发射钻头(按普通子弹的创建方式 +/// 位置=GetBulletStartWorldPos,旋转=Euler(0,0,帧同步角度)); /// 发射后【立刻】恢复普通炮台(turretParent=true)——爆奖与炮台无关; -/// 3. 钻头飞行三状态(表现=钻头子对象,Inspector 配在钻头预制体上): +/// 4. 钻头飞行三状态(表现=钻头子对象,Inspector 配在钻头预制体上): /// 状态1 反弹杀鱼:撞墙反弹 peng 次,碰鱼回调杀小鱼(Id_DieFish_FX 帧同步),显示 state1Fx; /// 状态2 乱转纯反弹:再反弹 peng2 次,碰鱼穿过不杀,显示 state2Fx 并驱动持续自转(spin2Speed); /// 状态3 收尾:飞向世界原点(0,0) → 到达后最终爆炸,显示 state3Fx; /// 最终爆炸炸不炸鱼按 bigPrizeFishMode 判断(KillFish=炸范围小鱼,NoFish/CoinNoFish=只爆不炸); -/// 4. 爆炸后(无特效则画范围圈圈) 圈圈淡出完成(或兜底) -> ShowEndUI:加分 + 显示 BJEndUI -> 结束爆奖回池。 +/// 5. 爆炸后(无特效则画范围圈圈) 圈圈淡出完成(或兜底) -> ShowEndUI:加分 + 显示 BJEndUI -> 结束爆奖回池。 /// public class ZuanTouXie_DieEvent : DieEffect_TSY { @@ -47,14 +53,19 @@ public class ZuanTouXie_DieEvent : DieEffect_TSY [Header("====== 时序 ======")] [Tooltip("等待击杀者点击释放的最长时间(秒),超时自动释放")] public float maxWaitTime = 10f; - [Tooltip("非击杀者端自动释放延迟(秒,纯表现)")] + [Tooltip("机器人击杀时房主代发释放帧的延迟(秒);也是无鱼guid防御回退时他人端的等待")] public float otherReleaseDelay = 1.5f; + [Tooltip("等释放帧同步的网络余量(秒):超过 maxWaitTime+该时长仍未收到帧(击杀者掉线没发帧)则本地兜底发射")] + public float releaseNetMargin = 3f; [Tooltip("爆炸后到显示结算的延迟(秒)")] public float endDelay = 0.5f; protected SitPosData _sit; // 击杀者座位 protected GameObject _paoTai; // 生成的钻头炮台 protected bool _endShown; // 结算(BJEndUI)只显示一次 + private string _fishId; // 宿主鱼 guid(释放帧同步的匹配键,Ws_DieFish 注入 monster) + private bool _releaseReceived; // 已收到释放帧同步(服务器回广播) + private float _releaseAngle; // 帧同步带来的发射角(已按本端座位镜像还原成世界角度) /// /// 恢复普通炮台的时机:false=发射后立刻恢复(钻头蟹);true=发射物跑完才恢复(激光蟹,炮台持续到激光结束)。 @@ -94,6 +105,64 @@ public class ZuanTouXie_DieEvent : DieEffect_TSY protected virtual void OnEnable() { _routineStarted = false; + _releaseReceived = false; // 对象池复用:释放帧状态复位 + } + + // ================================================================ + // ⭐ 释放帧同步(多人一致性):fishId → 正在等待释放的事件实例 + // 击杀者端点击/超时 → 发 zuanTouRelease 帧 → 服务器广播全房间(含击杀者回广播) + // → 各端按 fishId 找到本地实例 → 统一时刻、统一角度发射钻头/激光 + // ================================================================ + private static readonly Dictionary _waitingByFishId + = new Dictionary(); + private static bool _releaseListenerBound; + + /// 绑定释放帧同步监听(进程内只绑一次;EventCenter 是纯 C# 单例不随场景销毁) + private static void BindReleaseListener() + { + if (_releaseListenerBound) return; + _releaseListenerBound = true; + EventCenter.Instance.AddEventListener( + ConstEventString.SyncGameData_ZuanTouRelease, OnZuanTouReleaseFrame); + } + + /// 收到 zuanTouRelease 帧同步(所有端都会收到,含击杀者自己的回广播): + /// 按 fishId 找到等待中的实例 → 记录角度并放行发射 + private static void OnZuanTouReleaseFrame(SyncGameData sync) + { + var data = FrameDataParser.Parse(sync.frameData); + if (data == null || string.IsNullOrEmpty(data.fishId)) return; + ZuanTouXie_DieEvent evt; + if (_waitingByFishId.TryGetValue(data.fishId, out evt) && evt != null) + evt.OnReleaseFrame(data.angle); + } + + /// 实例侧收到释放帧:源角度按击杀者座位在本端的显示位置还原成世界角度,放行 MainRoutine 等待 + private void OnReleaseFrame(float sourceAngle) + { + _releaseAngle = SeatMirror.SourceAngleToWorld(sourceAngle, _sit != null && _sit.IsFlipped); + _releaseReceived = true; + Debug.Log($"[钻头蟹] 收到释放帧同步:fishId={_fishId} 源角度={sourceAngle} 本端世界角度={_releaseAngle}"); + } + + /// 发送 zuanTouRelease 帧同步:服务器广播给房间所有人(含自己回广播),各端收到后统一发射。 + /// 角度转"源角度"(发送方下方座位系)发送,接收端按击杀者座位在本端的显示位置还原。 + private void SendReleaseFrame(float worldAngle) + { + SyncGameData sgd = new SyncGameData + { + sitPos = SeatMirror.ToServer(_sit.posID), + frameType = FrameDataType.zuanTouRelease, + time = BeijingTimeManager.Instance.CurrentTimestampMs, + frameData = new FrameData_ZuanTouRelease + { + userId = _sit.user, + fishId = _fishId, + angle = SeatMirror.WorldAngleToSource(worldAngle, _sit.IsFlipped), + } + }; + WebSocketSendList.SyncGameData(sgd); + Debug.Log($"[钻头蟹] 已发送释放帧同步:fishId={_fishId} userId={_sit.user} 世界角度={worldAngle}"); } protected virtual IEnumerator MainRoutine() @@ -117,6 +186,14 @@ public class ZuanTouXie_DieEvent : DieEffect_TSY if (paoBei <= 0 && _sit.consumeGoldNum > 0) paoBei = (int)_sit.consumeGoldNum; + // ⭐ 释放帧同步注册:fishId=宿主鱼 guid(Ws_DieFish 注入 monster),收到 zuanTouRelease 帧时按它找到本实例 + _fishId = monster != null ? monster.guid : null; + if (!string.IsNullOrEmpty(_fishId)) + { + BindReleaseListener(); + _waitingByFishId[_fishId] = this; + } + // ===== 1. 炮台被钻头覆盖 → 生成钻头炮台 → 禁普通开火 ===== // ⭐ 炮台/战神二选一:隐藏的是"当前显示的那个"——战神出战(currentZhanShen!=null)时隐藏战神 // (turretParent 本来就是隐藏状态),否则隐藏普通炮台 turretParent @@ -134,21 +211,62 @@ public class ZuanTouXie_DieEvent : DieEffect_TSY } _sit.isDrillActive = true; - // ===== 2. 等待点击释放钻头(击杀者点击;他人/超时自动释放)===== + // ===== 2. 等待释放钻头(⭐ 帧同步统一触发,多人界面一致)===== + // 击杀者:点击(或超时 maxWaitTime) → 发 zuanTouRelease 帧同步; + // 服务器广播给房间所有人(含击杀者回广播) → 所有端收到帧才发射 → 自己和别人界面同一时刻发射。 + // 机器人击杀:机器人不会点击,由房主延迟 otherReleaseDelay 代发(同 baoJiangEnd 房主代发模式)。 + // 其他真人玩家:不发帧,只等帧同步。 + // 兜底:等帧超过 maxWaitTime + releaseNetMargin 仍未收到(击杀者掉线没发帧) → 本地按当前瞄准角直接发射,避免卡死。 bool isKiller = _sit.user == GameManager.Instance.user.id; - if (isKiller) + float fireAngle; + if (string.IsNullOrEmpty(_fishId)) { - float end = Time.time + maxWaitTime; - while (Time.time < end && !PressedThisFrame()) - yield return null; + // 防御:拿不到宿主鱼 guid(理论不会发生)→ 退回本地触发 + if (isKiller) + { + float end = Time.time + maxWaitTime; + while (Time.time < end && !PressedThisFrame()) + yield return null; + } + else + { + yield return new WaitForSeconds(otherReleaseDelay); + } + fireAngle = _sit.currentAngle; } else { - yield return new WaitForSeconds(otherReleaseDelay); + if (isKiller) + { + float end = Time.time + maxWaitTime; + while (Time.time < end && !PressedThisFrame()) + yield return null; + SendReleaseFrame(_sit.currentAngle); // 点击或超时 → 广播(含自己回广播) + } + else if ((_sit.isBot || (_sit.user != null && _sit.user.StartsWith("bot_"))) + && BaoJiangNotSelfHandler.IsLocalRoomOwner()) + { + // 机器人击杀:房主代发释放帧(机器人端无人点击) + yield return new WaitForSeconds(otherReleaseDelay); + SendReleaseFrame(_sit.currentAngle); + } + + // 所有端(含击杀者,走服务器回广播)统一等帧再发射,角度用帧里带的保证方向一致 + float fallbackEnd = Time.time + maxWaitTime + releaseNetMargin; + while (!_releaseReceived && Time.time < fallbackEnd) + yield return null; + if (!_releaseReceived) + Debug.LogWarning($"[钻头蟹] ⚠未收到释放帧同步(击杀者可能掉线),本地兜底发射 fishId={_fishId}"); + fireAngle = _releaseReceived ? _releaseAngle : _sit.currentAngle; + + // 已到发射时机,注销等待(防御:确认注册的还是本实例才删,避免误删后来覆盖注册的其他实例) + ZuanTouXie_DieEvent registered; + if (_waitingByFishId.TryGetValue(_fishId, out registered) && registered == this) + _waitingByFishId.Remove(_fishId); } - // ===== 3. 发射钻头(普通子弹的创建方式)===== - GameObject drill = FireDrill(isKiller); + // ===== 3. 发射钻头(普通子弹的创建方式;角度统一来自帧同步)===== + GameObject drill = FireDrill(isKiller, fireAngle); // ===== 4. 恢复普通炮台时机:钻头蟹=发射后立刻恢复(爆奖与炮台无关)===== if (!restoreTurretAfterProjectile) @@ -248,10 +366,10 @@ public class ZuanTouXie_DieEvent : DieEffect_TSY /// /// 按普通子弹方式发射钻头:位置=炮台子弹起点(3D 世界坐标); /// 旋转=用预制体默认旋转实例化(保留默认 X 轴姿态),之后只改 Z 轴; - /// 前进方向由当前瞄准角单独算出传给 Init。 + /// 前进方向由发射角单独算出传给 Init(角度来自释放帧同步,各端一致)。 /// 子类(激光蟹)可覆写此方法改为激活激光节点+扫鱼,返回临时对象供 while(drill!=null) 等待。 /// - protected virtual GameObject FireDrill(bool isKiller) + protected virtual GameObject FireDrill(bool isKiller, float angle) { if (bulletPrefab == null) return null; @@ -264,8 +382,8 @@ public class ZuanTouXie_DieEvent : DieEffect_TSY // 旋转:预制体默认旋转(保留默认 X 轴,不重置),Z 轴由 SpecialBulletMove 按 heading 设置 GameObject drill = Instantiate(bulletPrefab, muzzle, bulletPrefab.transform.rotation); - // 前进方向:当前瞄准角 → 方向(同普通子弹的 angle 语义) - Vector2 heading = Quaternion.Euler(0, 0, _sit.currentAngle) * Vector2.up; + // 前进方向:释放帧同步带来的角度 → 方向(同普通子弹的 angle 语义) + Vector2 heading = Quaternion.Euler(0, 0, angle) * Vector2.up; // 无物理组件时补触发器,保证碰墙/碰鱼检测生效 if (drill.GetComponent() == null) @@ -328,6 +446,9 @@ public class ZuanTouXie_DieEvent : DieEffect_TSY private void OnDestroy() { StopAllCoroutines(); + // 释放帧同步:注销等待(事件被外部销毁时防止悬空引用) + if (_fishId != null && _waitingByFishId.ContainsValue(this)) + _waitingByFishId.Remove(_fishId); // 兜底:事件被外部销毁时恢复座位状态(显示统一交给 RestoreTurretVisual 按当前装备恢复) if (_sit != null) {