This commit is contained in:
2026-09-27 20:34:42 +08:00
parent 21de07a56d
commit 2aa6a8f352
7 changed files with 171 additions and 21 deletions
@@ -19,6 +19,8 @@ public class ConstEventString
// SyncGameData_RequestSyncFish 已废弃:鱼同步改由服务端 OnFishSpawn(type=sync) 下发
/// <summary>爆奖结束帧同步(baoJiangEnd):某玩家爆奖流程完成,其他客户端显示最终 BJEndUI 并加分</summary>
public const string BaoJiangEnd = "BaoJiangEnd";
/// <summary>钻头蟹/激光蟹释放帧同步(zuanTouRelease):击杀者点击(或超时)发射,所有客户端(含击杀者回广播)统一时刻发射钻头/激光</summary>
public const string SyncGameData_ZuanTouRelease = "SyncGameData_ZuanTouRelease";
// ================================================================
// 服务端 → 客户端事件 (新协议)
@@ -59,6 +59,8 @@ namespace Assets.MyGame.Scripts.Net.WebResponses
changeZhanShen=13,
/// <summary>表情包:某玩家发送表情(spine动画/文字),广播给其他客户端在对应座位显示</summary>
biaoQing=14,
/// <summary>钻头蟹/激光蟹释放:击杀者点击(或超时)发射钻头/激光,广播给房间所有客户端(含发送者回广播),各端统一时刻发射</summary>
zuanTouRelease=15,
}
[Serializable]
public class FireCannonSendData
@@ -210,6 +212,22 @@ namespace Assets.MyGame.Scripts.Net.WebResponses
public string content;
}
/// <summary>
/// 钻头蟹/激光蟹释放帧数据(frameType=zuanTouRelease)。
/// 击杀者点击(或超时)时由击杀者客户端发送(机器人击杀由房主代发),
/// 服务器广播给房间所有人(含发送者回广播),各客户端按 fishId 匹配本地爆奖事件统一发射。
/// </summary>
[Serializable]
public class FrameData_ZuanTouRelease : FrameData
{
/// <summary>击杀者 userId</summary>
public string userId;
/// <summary>死亡的特殊鱼 ID(各端用它匹配本地正在等待释放的爆奖事件实例)</summary>
public string fishId;
/// <summary>发射时的炮台源角度(下方座位坐标系,接收端按该座位在本端显示位置用 SeatMirror 还原)</summary>
public float angle;
}
// ================================================================
// OnFishSpawn 事件数据结构 (服务端出鱼,破坏性替代 createFish 帧同步)
// ================================================================
@@ -383,6 +383,10 @@ namespace Assets.MyGame.Scripts.Net
case FrameDataType.baoJiangEnd:
EventCenter.Instance.EventTrigger<SyncGameData>(ConstEventString.BaoJiangEnd, syncGameData);
break;
// ⭐ 钻头蟹/激光蟹释放:击杀者点击(或超时)广播,各端(含击杀者回广播)统一时刻发射钻头/激光
case FrameDataType.zuanTouRelease:
EventCenter.Instance.EventTrigger<SyncGameData>(ConstEventString.SyncGameData_ZuanTouRelease, syncGameData);
break;
default:
break;
}
@@ -310,7 +310,8 @@ public partial class WebSocketSendList
{
case FrameDataType.fireBullet:
case FrameDataType.rotateTurrect:
// ⭐ 开火/旋转实时发送,和开炮一样立即广播,保证炮台旋转流畅
case FrameDataType.zuanTouRelease:
// ⭐ 开火/旋转/钻头释放实时发送,和开炮一样立即广播,保证各端同时刻触发
SendImmediate(BuildJson(sgd));
return;
@@ -190,8 +190,8 @@ public static class BaoJiangNotSelfHandler
Debug.Log($"[BaoJiangNotSelfHandler] 房主已代发 baoJiangEnd 帧同步:key={key} beishu={beishu} score={score}");
}
/// <summary>判断当前本地客户端是否为房主(用于决定是否代发机器人帧同步)</summary>
private static bool IsLocalRoomOwner()
/// <summary>判断当前本地客户端是否为房主(用于决定是否代发机器人帧同步;钻头蟹/激光蟹释放代发也复用)</summary>
public static bool IsLocalRoomOwner()
{
return SitPosData.roomOwnerSit != null
&& GameManager.Instance != null
@@ -2,6 +2,8 @@ using UnityEngine;
/// <summary>
/// 特殊鱼:激光蟹 死亡玩法(继承 ZuanTouXie_DieEvent,只覆写发射部分)。
/// ⭐ 多人释放触发与钻头蟹同源:击杀者点击(或超时) → zuanTouRelease 帧同步 →
/// 服务器广播全房间(含击杀者回广播) → 各端统一时刻启动激光(机器人击杀由房主代发)。
/// 和钻头蟹完全同构,区别仅在"点击发射":
/// 钻头蟹 → 发射钻头子弹(SpecialBulletMove:反弹/碰鱼爆炸)
/// 激光蟹 → 生成 3D 世界横穿屏幕的激光(SpecialLaserMove:蓝→紫两形态,扫到就杀)
@@ -35,9 +37,11 @@ public class JiGuangXie_DieEvent : ZuanTouXie_DieEvent
/// <summary>
/// 覆写发射:直接使用【炮台预制体上配置好的 SpecialLaserMove】组件(特效引用在炮台里拖好),
/// 调它的 Init 启动激光(hit → 主激光 → 切换形态,扫到就杀)。
/// 激光方向由炮台自身旋转决定(各端炮台角度经 rotateTurrect 帧同步,基本一致),
/// 参数 angle 仅供签名统一,激光不使用。
/// 返回 holder:激光跑完 onFinished 时销毁,父类 while(drill != null) 检测到走结算。
/// </summary>
protected override GameObject FireDrill(bool isKiller)
protected override GameObject FireDrill(bool isKiller, float angle)
{
if (_sit == null) return null;
@@ -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 -> 结束爆奖回池。
/// </summary>
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; // 帧同步带来的发射角(已按本端座位镜像还原成世界角度)
/// <summary>
/// 恢复普通炮台的时机: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<string, ZuanTouXie_DieEvent> _waitingByFishId
= new Dictionary<string, ZuanTouXie_DieEvent>();
private static bool _releaseListenerBound;
/// <summary>绑定释放帧同步监听(进程内只绑一次;EventCenter 是纯 C# 单例不随场景销毁)</summary>
private static void BindReleaseListener()
{
if (_releaseListenerBound) return;
_releaseListenerBound = true;
EventCenter.Instance.AddEventListener<SyncGameData>(
ConstEventString.SyncGameData_ZuanTouRelease, OnZuanTouReleaseFrame);
}
/// <summary>收到 zuanTouRelease 帧同步(所有端都会收到,含击杀者自己的回广播):
/// 按 fishId 找到等待中的实例 → 记录角度并放行发射</summary>
private static void OnZuanTouReleaseFrame(SyncGameData sync)
{
var data = FrameDataParser.Parse<FrameData_ZuanTouRelease>(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);
}
/// <summary>实例侧收到释放帧:源角度按击杀者座位在本端的显示位置还原成世界角度,放行 MainRoutine 等待</summary>
private void OnReleaseFrame(float sourceAngle)
{
_releaseAngle = SeatMirror.SourceAngleToWorld(sourceAngle, _sit != null && _sit.IsFlipped);
_releaseReceived = true;
Debug.Log($"[钻头蟹] 收到释放帧同步:fishId={_fishId} 源角度={sourceAngle} 本端世界角度={_releaseAngle}");
}
/// <summary>发送 zuanTouRelease 帧同步:服务器广播给房间所有人(含自己回广播),各端收到后统一发射。
/// 角度转"源角度"(发送方下方座位系)发送,接收端按击杀者座位在本端的显示位置还原。</summary>
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,8 +211,17 @@ 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;
float fireAngle;
if (string.IsNullOrEmpty(_fishId))
{
// 防御:拿不到宿主鱼 guid(理论不会发生)→ 退回本地触发
if (isKiller)
{
float end = Time.time + maxWaitTime;
@@ -146,9 +232,41 @@ public class ZuanTouXie_DieEvent : DieEffect_TSY
{
yield return new WaitForSeconds(otherReleaseDelay);
}
fireAngle = _sit.currentAngle;
}
else
{
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);
}
// ===== 3. 发射钻头(普通子弹的创建方式)=====
GameObject drill = FireDrill(isKiller);
// 所有端(含击杀者,走服务器回广播)统一等帧再发射,角度用帧里带的保证方向一致
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, fireAngle);
// ===== 4. 恢复普通炮台时机:钻头蟹=发射后立刻恢复(爆奖与炮台无关)=====
if (!restoreTurretAfterProjectile)
@@ -248,10 +366,10 @@ public class ZuanTouXie_DieEvent : DieEffect_TSY
/// <summary>
/// 按普通子弹方式发射钻头:位置=炮台子弹起点(3D 世界坐标);
/// 旋转=用预制体默认旋转实例化(保留默认 X 轴姿态),之后只改 Z 轴;
/// 前进方向由当前瞄准角单独算出传给 Init。
/// 前进方向由发射角单独算出传给 Init(角度来自释放帧同步,各端一致)。
/// 子类(激光蟹)可覆写此方法改为激活激光节点+扫鱼,返回临时对象供 while(drill!=null) 等待。
/// </summary>
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<Rigidbody2D>() == 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)
{