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)
{