diff --git a/Assets/Framework/Event/ConstEventString.cs b/Assets/Framework/Event/ConstEventString.cs index d98b3354b..3cdcea560 100644 --- a/Assets/Framework/Event/ConstEventString.cs +++ b/Assets/Framework/Event/ConstEventString.cs @@ -3,7 +3,7 @@ public class ConstEventString { public const string SyncGameData_Rotate = "SyncGameData_Rotate"; public const string SyncGameData_FireBullet = "SyncGameData_FireBullet"; - public const string SyncGameData_CreateFish = "SyncGameData_CreateFish"; + // SyncGameData_CreateFish 已废弃:鱼创建改由服务端 OnFishSpawn 事件下发 public const string SyncGameData_ChangeTargetMonster = "SyncGameData_ChangeTargetMonster"; public const string SyncGameData_RendromDieFish = "SyncGameData_RenderDieFish"; public const string SyncGameData_TurretState = "SyncGameData_TurretState"; @@ -12,6 +12,29 @@ public class ConstEventString public const string SyncGameData_ChangeCannonMultiple = "SyncGameData_ChangeCannonMultiple"; public const string SyncGameData_ChangeWing = "SyncGameData_ChangeWing"; public const string SyncGameData_BotFireAngle = "SyncGameData_BotFireAngle"; - public const string SyncGameData_RequestSyncFish = "SyncGameData_RequestSyncFish"; -} + // SyncGameData_RequestSyncFish 已废弃:鱼同步改由服务端 OnFishSpawn(type=sync) 下发 + // ================================================================ + // 服务端 → 客户端事件 (新协议) + // ================================================================ + /// 服务端出鱼事件(破坏性替代 createFish 帧同步) + public const string OnFishSpawn = "OnFishSpawn"; + /// 鱼列表刷新 + public const string OnFishList = "OnFishList"; + /// 冰冻道具生效(40003),鱼存活时间延长 15 秒,客户端写死 15s 倒计时 + public const string FishFrozen = "FishFrozen"; + /// 玩家挂机通知 + public const string PlayerAfkNotice = "PlayerAfkNotice"; + /// 玩家取消挂机通知 + public const string PlayerCancelAfkNotice = "PlayerCancelAfkNotice"; + /// 全局击杀公告(真人击杀/机器人模拟) + public const string KillNotice = "KillNotice"; + /// 机器人退出房间 + public const string BotLeaveRoomNotice = "BotLeaveRoomNotice"; + /// 房主断线,其他玩家可接受机器人控制权 + public const string HostDisconnect = "HostDisconnect"; + /// 座位变更 + public const string OnSeatChange = "OnSeatChange"; + /// 房主控制权移交 + public const string ChangeControllerUser = "ChangeControllerUser"; +} diff --git a/Assets/MyGame/Scripts/Manager/CreateFishManager.cs b/Assets/MyGame/Scripts/Manager/CreateFishManager.cs index 8e643daa7..97099a609 100644 --- a/Assets/MyGame/Scripts/Manager/CreateFishManager.cs +++ b/Assets/MyGame/Scripts/Manager/CreateFishManager.cs @@ -189,8 +189,7 @@ public class CreateFishManager : MonoSingleton private int hjQuanZhongCount; // 本轮同时启用黄金鱼数量 - /// 是否开启放鱼(false 时 Update 不执行生成逻辑) - public bool fangzhu; + // fangzhu 字段已删除:鱼创建改由服务端 OnFishSpawn 统一下发,客户端无需房主出鱼 public bool isResources = false; // ================================================================ // 懒加载属性 @@ -251,36 +250,28 @@ public class CreateFishManager : MonoSingleton fishRoot = new GameObject("FishRoot").transform; if (poolRoot == null) fishRoot = new GameObject("poolRoot").transform; - // 监听服务器下发的创鱼事件 - EventCenter.Instance.AddEventListener( - ConstEventString.SyncGameData_CreateFish, CreateFish); - - // 监听新玩家"请求同步鱼"事件:房主收到后广播场上活鱼 - EventCenter.Instance.AddEventListener( - ConstEventString.SyncGameData_RequestSyncFish, OnRequestSyncFish); - - + // ⭐ 鱼创建改由服务端 OnFishSpawn 事件统一下发,客户端不再监听 createFish 帧同步 + EventCenter.Instance.AddEventListener( + ConstEventString.OnFishSpawn, OnFishSpawn); } // ================================================================ - // Update:定时调度 + Job 驱动生成 + // Update:冰冻时不做任何事(鱼创建由服务端 OnFishSpawn 驱动) // ================================================================ private void Update() { - if (!isFrozen) - isCreateFish = BroadCastCreateFish(); + // 鱼创建由服务端 OnFishSpawn 事件驱动,客户端 Update 不再出鱼 + // (保留 isFrozen 字段供 FreezeAllFish/UnfreezeAllFish 使用) } private void OnDestroy() { - EventCenter.Instance.RemoveEventListener( - ConstEventString.SyncGameData_CreateFish, CreateFish); - EventCenter.Instance.RemoveEventListener( - ConstEventString.SyncGameData_RequestSyncFish, OnRequestSyncFish); + EventCenter.Instance.RemoveEventListener( + ConstEventString.OnFishSpawn, OnFishSpawn); // 释放 NativeArray,避免内存泄漏 DisposeNativeArrays(); @@ -594,656 +585,97 @@ public class CreateFishManager : MonoSingleton } #endregion - #region 创建鱼 广播部分 + #region OnFishSpawn 服务端出鱼接收处理 + // ⭐ 鱼创建改由服务端 OnFishSpawn 事件统一下发 + // 客户端 BroadCastCreateFish / TickXxxSpawn / SendCreateFishRequest / + // CreateInitialFishes / BroadcastExistingFishes / OnRequestSyncFish / CreateFish + // 全部删除,改为接收 OnFishSpawnData 后调用 SpawnFishMainThread 生成鱼实例 + // + // type: + // "init" - 首次出鱼(玩家进入房间后服务端推送初始鱼) + // "spawn" - 定时出鱼(游戏中持续刷怪) + // "sync" - 重连/取消挂机同步(把当前场上活鱼推送给回归玩家) + /// - /// 广播创建鱼 + /// 接收服务端 OnFishSpawn 事件,生成鱼实例。 + /// 遍历 data.fish 列表,对每条鱼: + /// 1. guid 去重(已有此 guid 的鱼跳过,防 sync 模式重复 Spawn) + /// 2. 构造 FrameData_CreateFish 复用 SpawnFishMainThread 生成 + /// 3. 启动 expireAt 过期自动移除协程 /// - /// 此玩家是否可以创建鱼 - private bool BroadCastCreateFish() + public void OnFishSpawn(OnFishSpawnData data) { - if (!isResources) + if (data == null || data.fish == null || data.fish.Count == 0) return; + if (isFrozen) return; // 冰冻中不创建鱼 + if (spawnDic == null) return; // 场景卸载后忽略 + + foreach (var item in data.fish) { - return false; - } - // 未初始化 或 未开启放鱼 → 跳过 - if (spawnList == null || !fangzhu) return false; + if (item == null) continue; + if (string.IsNullOrEmpty(item.fishNumber)) continue; - float dt = Time.deltaTime; + // guid 用服务端 fishId + string guid = item.fishId.ToString(); - // ---- 1. 用 Job System 并行更新所有鱼的计时器 ---- - var job = new FishSpawnTimerJob - { - deltaTime = dt, - timers = timers, - frequencies = frequencies, - shouldSpawn = shouldSpawn, - }; - job.Schedule().Complete(); - - // ---- 2. Boss 定时激活 ---- - TickBossSpawn(dt); - - // ---- 3. 鱼阵定时激活 ---- - TickFishArraySpawn(dt); - - // ---- 4. 普通鱼定时批量激活 ---- - TickPTFishSpawn(dt); - - // ---- 5. 黄金鱼定时激活 ---- - TickHJFishSpawn(dt); - - // ---- 5. 特殊鱼定时激活 ---- - TickTSFishSpawn(dt); - - - // ---- 6. 特殊boss定时激活 ---- - TickTSBossSpawn(dt); - - // ---- 7. 遍历 shouldSpawn,向服务器发送生成请求 ---- - ProcessSpawnRequests(); - return true; - } - - // ================================================================ - // 定时 Tick 方法(由 Update 调用) - // ================================================================ - - /// 每 25 秒随机激活一条 Boss 鱼。 - private void TickBossSpawn(float dt) - { - bossTimer -= dt; - if (bossTimer > 0f) return; - - bossCount = Random.Range(bossCreateData.countRange[0], bossCreateData.countRange[1]); - bossTimer = Random.Range(bossCreateData.timeRange[0], bossCreateData.timeRange[1]); - - //bossTimer = 3f; - if (!fishTypeConfig.TryGetValue(FishTypeEnum.Boss鱼, out var bossList) || bossList.Count == 0) - return; - foreach (var item in bossList) - { - //if (item.fishNumber == "11026") - // item.isCreate = 2; - - //if (item.fishNumber == "11035") - // item.isCreate = 2; - // if (item.fishNumber == "11030") - // item.isCreate = 2; - - } - - StartCoroutine(buchongfu(bossList, bossCount, fishDatas_hashCodekey_Boss, bossRecentList, bossRecentMax)); - - - } - - - /// 每 25 秒随机激活一条 特殊Boss 鱼。 - private void TickTSBossSpawn(float dt) - { - tsbossTimer -= dt; - if (tsbossTimer > 0f) return; - tsbossCount = Random.Range(tsbossCreateData.countRange[0], tsbossCreateData.countRange[1]); - - tsbossTimer = Random.Range(tsbossCreateData.timeRange[0], tsbossCreateData.timeRange[1]); - - if (!fishTypeConfig.TryGetValue(FishTypeEnum.特殊Boss, out var tsbossList) || tsbossList.Count == 0) - return; - - //foreach (var item in tsbossList) - //{ - // if (item.fishNumber == "11041") - // { - // item.isCreate = 2; - // } - //} - StartCoroutine(buchongfu(tsbossList, tsbossCount, fishDatas_hashCodekey_TS_Boss, tsbossRecentList, tsbossRecentMax)); - - - } - private RoomFishDetailConfig GetRandomFishByWeight(List configs) - { - if (configs == null || configs.Count == 0) return null; - - int totalWeight = configs.Sum(c => c.fishWeight); - int randWeight = Random.Range(0, totalWeight + 1); - - foreach (var cfg in configs) - { - randWeight -= cfg.fishWeight; - if (randWeight <= 0) // 注意这里是 <= 0 - { - return cfg; - } - } - - return configs[configs.Count - 1]; // 理论上不会到这里 - } - /// - /// 从配置列表中按权重随机选择指定数量的鱼,不重复。 - /// - private List GetRandomFishByWeight(List configs, int count) - { - var result = new List(); - if (configs == null || configs.Count == 0 || count <= 0) return result; - - // 拷贝一份候选集合,避免修改原始列表 - var candidates = new List(configs); - - count = Mathf.Min(count, candidates.Count); // 不超过候选数量 - - for (int k = 0; k < count; k++) - { - int totalWeight = candidates.Sum(c => c.fishWeight); - int randWeight = Random.Range(0, totalWeight); - - RoomFishDetailConfig chosen = null; - foreach (var cfg in candidates) - { - randWeight -= cfg.fishWeight; - if (randWeight < 0) // 注意这里是 <= 0 - { - chosen = cfg; - break; - } - } - - if (chosen != null) - { - result.Add(chosen); - candidates.Remove(chosen); // 移除已选,保证不重复 - } - } - - return result; - } - - IEnumerator buchongfu(List bossList, int count, Dictionary targetDict, List recentList = null, int recentMax = 0) - { - var batchPicked = new HashSet(); // 本轮已选,防止同批重复 - for (int j = 0; j < count; j++) - { - // 1. 先筛选出未重复的候选集合(排除场上已存在的同 fishNumber 的鱼 + 最近出现过的 + 本轮已选的) - var candidates = bossList - .Where(b => !targetDict.Values - .Any(existing => existing.fishTypeID.ToString() == b.fishNumber)) - .Where(b => !batchPicked.Contains(b.fishNumber)) - .ToList(); - - // 排除最近出现过的鱼 - if (recentList != null && recentList.Count > 0) - candidates = candidates.Where(c => !recentList.Contains(c.fishNumber)).ToList(); - - if (candidates.Count == 0) - { - // 没有候选时清空最近列表 + 本轮记录重试 - if (recentList != null) recentList.Clear(); - batchPicked.Clear(); - candidates = bossList - .Where(b => !targetDict.Values - .Any(existing => existing.fishTypeID.ToString() == b.fishNumber)) - .ToList(); - if (candidates.Count == 0) - yield break; - } - - // 2. 在候选集合里做加权随机 - var chosen = GetRandomFishByWeight(candidates); - batchPicked.Add(chosen.fishNumber); - - // 3. 记录到最近列表 - if (recentList != null && recentMax > 0) - { - recentList.Add(chosen.fishNumber); - while (recentList.Count > recentMax) - recentList.RemoveAt(0); - } - - // 4. 标记激活 - chosen.isCreate = 2; - - yield return null; // 每帧只激活一条,避免同帧生成过多 - } - } - - //IEnumerator buchongfu( - //List bossList, - //int count, - //Dictionary targetDict, - //List recentList = null, - //int recentMax = 0) - //{ - // Debug.Log($"===【调试开始】本次需要生成数量: {count},配置总数: {bossList.Count} ==="); - - // // 打印场上已有 Boss - // Debug.Log($"当前场上已有 Boss 数量: {targetDict.Count}"); - // foreach (var kv in targetDict) - // Debug.Log($"场上已有 Boss fishNumber = {kv.Value.fishTypeID}"); - - // var batchPicked = new HashSet(); // 本轮已选 - - // for (int j = 0; j < count; j++) - // { - // Debug.Log($"--- 第 {j + 1}/{count} 次抽取 ---"); - - // // 1. 先筛选候选池 - // var candidates = bossList - // .Where(b => !targetDict.Values - // .Any(existing => existing.fishTypeID.ToString() == b.fishNumber)) - // .Where(b => !batchPicked.Contains(b.fishNumber)) - // .ToList(); - - // // 打印被排除的原因 - // foreach (var b in bossList) - // { - // bool inTarget = targetDict.Values.Any(e => e.fishTypeID.ToString() == b.fishNumber); - // bool inBatch = batchPicked.Contains(b.fishNumber); - // bool inRecent = recentList != null && recentList.Contains(b.fishNumber); - - // if (inTarget || inBatch || inRecent) - // { - // Debug.Log( - // $"排除 fishNumber={b.fishNumber} | " + - // $"场上存在={inTarget} | 本轮已选={inBatch} | 最近出现={inRecent}" - // ); - // } - // } - - // // 排除最近出现过的鱼 - // if (recentList != null && recentList.Count > 0) - // candidates = candidates.Where(c => !recentList.Contains(c.fishNumber)).ToList(); - - // Debug.Log($"候选池数量: {candidates.Count}"); - - // // 2. 候选池为空 → 触发重置逻辑 - // if (candidates.Count == 0) - // { - // Debug.LogWarning("候选池为空,触发 recentList + batchPicked 清空重置"); - - // recentList?.Clear(); - // batchPicked.Clear(); - - // candidates = bossList - // .Where(b => !targetDict.Values - // .Any(existing => existing.fishTypeID.ToString() == b.fishNumber)) - // .ToList(); - - // Debug.Log($"重置后候选池数量: {candidates.Count}"); - - // if (candidates.Count == 0) - // { - // Debug.LogError("重置后仍然没有候选鱼 → 说明 targetDict 永远占着某条鱼,导致永远少生成 1 条!"); - // yield break; - // } - // } - - // // 3. 加权随机 - // var chosen = GetRandomFishByWeight(candidates); - // Debug.Log($"选中 fishNumber = {chosen.fishNumber}"); - - // batchPicked.Add(chosen.fishNumber); - - // // 4. 记录最近出现 - // if (recentList != null && recentMax > 0) - // { - // recentList.Add(chosen.fishNumber); - // while (recentList.Count > recentMax) - // recentList.RemoveAt(0); - // } - - // // 5. 标记激活 - // chosen.isCreate = 2; - - // yield return null; - // } - - // Debug.Log("===【调试结束】本轮生成完成 ==="); - //} - - - - /// 每 1~3 秒随机激活若干组鱼阵。 - private void TickFishArraySpawn(float dt) - { - yzTimer -= dt; - if (yzTimer > 0f) return; - - - yzCount = Random.Range(yzCreateData.countRange[0], yzCreateData.countRange[1]); - - yzTimer = Random.Range(yzCreateData.timeRange[0], yzCreateData.timeRange[1]); - - - int lineCount = yz_FishConfig_Line_List.Count; - if (lineCount == 0) return; - - // 随机选 yzCount 个不重复的阵型下标 - var selectedIndices = RandomUniqueNumbers( - Mathf.Min(yzCount, lineCount), 0, lineCount); - - foreach (int idx in selectedIndices) - { - string arrayNumber = yz_FishConfig_Line_Dic[idx]; - if (!yz_FishConfig_Line.TryGetValue(arrayNumber, out var lineList) || lineList.Count == 0) + // guid 去重:已有此 guid 的鱼跳过 + if (!string.IsNullOrEmpty(guid) && + fishDatas_hashCodekey != null && + fishDatas_hashCodekey.ContainsKey(guid)) continue; - // 随机选一条配置并标记为待生成 - var chosen = lineList[Random.Range(0, lineList.Count)]; - chosen.isCreate = 2; - - - - - // 同步更新 spawnDic 中的状态 - if (spawnDic.TryGetValue(chosen.fishNumber, out var spawnEntry)) - spawnEntry.isCreate = 2; - } - } - - /// 每 3 秒随机批量激活若干条普通鱼。 - private void TickPTFishSpawn(float dt) - { - ptTimer -= dt; - if (ptTimer > 0f) return; - - - ptCount = Random.Range(ptCreateData.countRange[0], ptCreateData.countRange[1]); - - - ptTimer = Random.Range(ptCreateData.timeRange[0], ptCreateData.timeRange[1]); - - if (!fishTypeConfig.TryGetValue(FishTypeEnum.普通鱼, out var normalList) || normalList.Count == 0) - return; - // 随机选 ptCount 个不重复下标 - var indices = GetRandomFishByWeight(normalList, ptCount); - - foreach (var fish in indices) - fish.isCreate = 2; - } - - /// 每 3 秒随机批量激活若干条黄金鱼。 - private void TickHJFishSpawn(float dt) - { - hjTimer -= dt; - if (hjTimer > 0f) return; - - hjCount = Random.Range(hjCreateData.countRange[0], hjCreateData.countRange[1]); - - - hjTimer = Random.Range(hjCreateData.timeRange[0], hjCreateData.timeRange[1]); - - if (!fishTypeConfig.TryGetValue(FishTypeEnum.黄金鱼, out var hjlist) || hjlist.Count == 0) - return; - // 随机选 hjCount 个不重复下标 - var indices = GetRandomFishByWeight(hjlist, hjCount); - foreach (var fish in indices) - fish.isCreate = 2; - } - - /// 每 3 秒随机批量激活若干条特殊鱼。 - private void TickTSFishSpawn(float dt) - { - tsTimer -= dt; - if (tsTimer > 0f) return; - - tsCount = Random.Range(tsCreateData.countRange[0], tsCreateData.countRange[1]); - - - tsTimer = Random.Range(tsCreateData.timeRange[0], tsCreateData.timeRange[1]); - - if (!fishTypeConfig.TryGetValue(FishTypeEnum.特殊鱼, out var tslist) || tslist.Count == 0) - return; - // 随机选 tsCount 个不重复下标 - var indices = GetRandomFishByWeight(tslist, tsCount); - - foreach (var fish in indices) - fish.isCreate = 2; - } - - /// - /// 遍历 shouldSpawn 数组,对本帧到期且被标记 isCreate==2 的鱼 - /// 向服务器发送 WebSocket 生成同步包。 - /// - private void ProcessSpawnRequests() - { - for (int i = 0; i < spawnList.Count; i++) - { - if (shouldSpawn[i] != 1) continue; - - var fish = spawnList[i]; - - // isCreate==0:刚到期,重置为冷却中,等待外部计时器激活 - if (fish.isCreate == 0) + // 构造 FrameData_CreateFish 复用 SpawnFishMainThread + var createFishData = new FrameData_CreateFish { - fish.isCreate = 1; // 标记为"已处理但未激活" - continue; - } - - // isCreate==2:已被激活,向服务器同步一次生成请求 - if (fish.isCreate == 2) - { - SendCreateFishRequest(fish); - fish.isCreate = 0; - } - } - } - - /// - /// 构建并发送生成鱼的 WebSocket 同步数据包。 - /// - private void SendCreateFishRequest(RoomFishDetailConfig fish) - { - int pathId = 0; - - // 普通鱼(type==1)才从 pathList 随机选路径;鱼阵(type==2)由后续逻辑决定 - if (fish.type == 1) - { - var ft = FishDatas[fish.fishNumber]; - var pathList = ft.pathList; - if (pathList != null && pathList.Count > 0) - { - // Boss鱼(2)/特殊Boss(5):路线奇偶交替,避免同奇/同偶路径相互覆盖 - if (ft != null && (ft.fishType == 2 || ft.fishType == 5)) - { - var filtered = pathList.Where(p => (p % 2 != 0) == bossNextPathOdd).ToList(); - if (filtered.Count == 0) - filtered = pathList; - pathId = filtered[Random.Range(0, filtered.Count)]; - bossNextPathOdd = !bossNextPathOdd; - } - else - { - pathId = pathList[Random.Range(0, pathList.Count)]; - } - } - } - else - { - - if (!fishArrayConfigDic.TryGetValue(fish.fishNumber, out var yz)) - { - Debug.LogError($"[CreateFishManager] 找不到 fishArrayConfig: {fish.fishNumber}"); - return; - } - if (!FishYZ.dicfishYZ.TryGetValue(yz.arrayNumber, out var pathList) || pathList.Count == 0) - { - Debug.LogWarning($"[CreateFishManager] 鱼阵路线为空: {yz.arrayNumber}"); - return; - } - if (pathList != null && pathList.Count > 0) - pathId = pathList[Random.Range(0, pathList.Count)]; - } - var sgd = new SyncGameData - { - sitPos = GameManager.Instance.gameData.roomJoinNotice.position, - frameType = FrameDataType.createFish, - time = BeijingTimeManager.Instance.CurrentTimestampMs, - frameData = new FrameData_CreateFish - { - fishNumber = fish.fishNumber, - guid = Guid.NewGuid().ToString(), - moveLineId = pathId.ToString() - } - }; - - WebSocketSendList.SyncGameData(sgd); - } - - /// - /// 进度条加载结束时房主调用,创建一批初始鱼(进度 0.2~0.8),让屏幕开局就有鱼不会显得空。 - /// 直接复用 CreateFishManager 原有的出鱼机制: - /// - GetRandomFishByWeight 按权重选鱼(和 TickNormalFishSpawn 一致) - /// - WebSocketSendList.SyncGameData 帧同步广播(和 SendCreateFishRequest 一致) - /// - 通过 time 字段控制创建时间,让所有客户端收到后 progress 自然在 [0.2, 0.8] - /// mc.moveTime = (CurrentTimestampMs - time) / 1000f - /// progress = moveTime / times - /// → time = CurrentTimestampMs - progress * times * 1000 - /// - /// 只选普通鱼(fishTypeConfig[普通鱼]),Boss/特殊Boss/黄金鱼/特殊鱼由原有 Tick 逻辑 - /// 按时间间隔触发,不在这里批量生成,避免开局一堆 Boss。 - /// - /// 要创建的鱼数量 - public void CreateInitialFishes(int count) - { - if (!fangzhu) return; - if (fishTypeConfig == null) return; - if (!fishTypeConfig.TryGetValue(FishTypeEnum.普通鱼, out var normalList) || normalList.Count == 0) return; - - for (int i = 0; i < count; i++) - { - // ⭐ 用原有的 GetRandomFishByWeight 按权重选鱼 - var fish = GetRandomFishByWeight(normalList); - if (fish == null) continue; - - // 从该鱼的路径列表里随机选一条 - if (!FishDatas.TryGetValue(fish.fishNumber, out var ft)) continue; - if (ft.pathList == null || ft.pathList.Count == 0) continue; - - int pathId = ft.pathList[Random.Range(0, ft.pathList.Count)]; - - // ⭐ 通过 time 字段控制创建时间,让 progress ∈ [0.2, 0.8] - float progress = Random.Range(0.2f, 0.8f); - long timeOffsetMs = (long)(progress * fish.times * 1000f); - long sendTime = BeijingTimeManager.Instance.CurrentTimestampMs - timeOffsetMs; - - var sgd = new SyncGameData - { - sitPos = GameManager.Instance.gameData.roomJoinNotice.position, - frameType = FrameDataType.createFish, - time = sendTime, - frameData = new FrameData_CreateFish - { - fishNumber = fish.fishNumber, - guid = Guid.NewGuid().ToString(), - moveLineId = pathId.ToString() - } + fishNumber = item.fishNumber, + guid = guid, + moveLineId = item.routeId.ToString() }; - // ⭐ 走原有的帧同步广播链路 - WebSocketSendList.SyncGameData(sgd); + // 用 data.timestamp 作为生成时间,SpawnFishMainThread 内部会算 moveTime + SpawnFishMainThread(createFishData, data.timestamp); + + // expireAt 到期自动移除(remainingSeconds 秒后销毁) + if (item.remainingSeconds > 0) + StartCoroutine(ExpireFishCoroutine(guid, item.remainingSeconds)); } - Debug.Log($"[CreateFishManager] 初始鱼已广播: {count} 条 (房主,走原有出鱼机制)"); + Debug.Log($"[CreateFishManager] OnFishSpawn type={data.type}, count={data.fish.Count}"); } /// - /// 房主把当前场上所有活鱼广播一遍,用于新玩家加入房间时同步场上鱼状态。 - /// 直接遍历 fishDatas_hashCodekey(全部活鱼),反推 time 字段让新玩家收到后 - /// 通过 (CurrentTimestampMs - time)/1000f 算出与房主一致的 moveTime, - /// 鱼会出现在路径的正确中段位置,与老玩家看到的一致。 - /// guid 复用原 guid,保证后续杀鱼/锁定帧同步不会失同步。 + /// 鱼过期自动移除协程。 + /// remainingSeconds 秒后调用 Id_DieFish_NotFX 销毁鱼(无特效)。 /// - public void BroadcastExistingFishes() + private IEnumerator ExpireFishCoroutine(string guid, int remainingSeconds) { - if (!fangzhu) return; - if (fishDatas_hashCodekey == null || fishDatas_hashCodekey.Count == 0) return; - - // ⭐ 复制一份快照,避免遍历过程中鱼死亡修改字典导致 InvalidOperationException - var snapshot = new List>(fishDatas_hashCodekey); - long nowMs = BeijingTimeManager.Instance.CurrentTimestampMs; - int count = 0; - - foreach (var kv in snapshot) - { - var mc = kv.Value; - if (mc == null) continue; - if (mc.monsterData == null || mc.monsterData.fishTypeConfig == null) continue; - if (mc.monsterData.moveLine == null) continue; - if (string.IsNullOrEmpty(mc.guid)) continue; - if (string.IsNullOrEmpty(mc.lastMoveLineId)) continue; - - // ⭐ 跳过鱼阵鱼:鱼阵鱼 guid 是 原guid+i,单条广播会让新玩家收到后走 - // SpawnFishArray 又生成 N 条,导致鱼阵指数爆炸(16×16=256)。 - // 鱼阵鱼的同步需要鱼阵级别的广播(发鱼阵配置+原guid+数量), - // 这里不处理,鱼阵鱼不同步只影响视觉阵型,不影响游戏核心逻辑。 - if (mc.isyz) continue; - - // 总时长,用于过滤快死的鱼(避免新玩家刚进入就看到鱼死) - float totalTime = mc.monsterData.moveLine.moveTime; - if (totalTime <= 0f) continue; - // 跳过还没初始化的鱼(moveTime < 0)和快死的鱼(进度 >= 0.9) - if (mc.moveTime < 0f) continue; - if (mc.moveTime >= totalTime * 0.9f) continue; - - // ⭐ 反推 sendTime:让新玩家收到后 (CurrentTimestampMs - sendTime)/1000f ≈ mc.moveTime - long sendTime = nowMs - (long)(mc.moveTime * 1000f); - - var sgd = new SyncGameData - { - sitPos = GameManager.Instance.gameData.roomJoinNotice.position, - frameType = FrameDataType.createFish, - time = sendTime, - frameData = new FrameData_CreateFish - { - fishNumber = mc.monsterData.fishTypeConfig.fishNumber, - guid = mc.guid, // ⭐ 复用原 guid - moveLineId = mc.lastMoveLineId - } - }; - - WebSocketSendList.SyncGameData(sgd); - count++; - } - - Debug.Log($"[CreateFishManager] 广播场上活鱼: {count} 条 (房主,新玩家加入同步)"); - } - - /// - /// 房主收到新玩家"请求同步鱼"事件后,广播场上活鱼。 - /// 新玩家资源加载完成才会发请求,保证收到广播时 spawnDic 已初始化。 - /// 非房主收到直接忽略。 - /// - private void OnRequestSyncFish(SyncGameData syncGameData) - { - // 仅房主响应,且不响应自己发的请求(syncGameData.sitPos 是发送者座位) - if (!fangzhu) return; - BroadcastExistingFishes(); + yield return new WaitForSeconds(remainingSeconds); + Id_DieFish_NotFX(guid); } #endregion - #region 创建鱼 - /// - /// AI优化过的 - /// - /// - public void CreateFish(SyncGameData syncGameData) - { - if (isFrozen) return; // 冰冻中不创建鱼 - // ⭐ 场景卸载/CloseSpawnSystem 后 spawnDic 会变 null,此时收到的广播直接忽略 - if (spawnDic == null) return; - var rotateData = FrameDataParser.Parse(syncGameData.frameData); - if (rotateData == null) return; + #region 旧客户端出鱼链路(已删除) + // 以下方法已删除,鱼创建改由服务端 OnFishSpawn 事件下发: + // - BroadCastCreateFish / TickBossSpawn / TickTSBossSpawn / TickFishArraySpawn + // - TickPTFishSpawn / TickHJFishSpawn / TickTSFishSpawn + // - ProcessSpawnRequests / SendCreateFishRequest + // - CreateInitialFishes / BroadcastExistingFishes / OnRequestSyncFish + // - CreateFish(SyncGameData) + // 保留方法:SpawnFishMainThread / OnlyFish / SpawnFishArray 等(被 OnFishSpawn 调用) + #endregion - // ⭐ guid 去重:房主广播活鱼给新玩家时,服务器会广播给所有客户端(含房主自己)。 - // 老玩家已存在该 guid 的鱼 → 跳过,避免重复 Spawn 同一条鱼。 - // 仅本地无该 guid 的客户端(新加入玩家)才会真正 Spawn。 - if (!string.IsNullOrEmpty(rotateData.guid) && - fishDatas_hashCodekey != null && - fishDatas_hashCodekey.ContainsKey(rotateData.guid)) - return; + // ================================================================ + // 旧定时 Tick 方法(已删除,鱼创建改由服务端 OnFishSpawn 事件下发) + // ================================================================ + // TickBossSpawn / TickTSBossSpawn / TickFishArraySpawn / TickPTFishSpawn + // TickHJFishSpawn / TickTSFishSpawn / buchongfu / ProcessSpawnRequests + // SendCreateFishRequest / CreateInitialFishes / BroadcastExistingFishes + // OnRequestSyncFish / CreateFish(SyncGameData) + // —— 全部删除,改由 OnFishSpawn(OnFishSpawnData) 接收服务端出鱼事件 —— + + // ================================================================ + // 单体鱼 / 鱼阵生成(被 OnFishSpawn 调用) + // ================================================================ - SpawnFishMainThread(rotateData, syncGameData.time); - } /// /// 主线程实例化鱼对象(单体鱼 / 鱼阵分发)。 /// @@ -1524,8 +956,6 @@ public class CreateFishManager : MonoSingleton #endregion - #endregion - #region 结束游戏退出房间 /// 退出房间:关闭生成系统。 public void ExitRoom() diff --git a/Assets/MyGame/Scripts/Net/APIManager.cs b/Assets/MyGame/Scripts/Net/APIManager.cs index 859924aaf..1db73f130 100644 --- a/Assets/MyGame/Scripts/Net/APIManager.cs +++ b/Assets/MyGame/Scripts/Net/APIManager.cs @@ -18,7 +18,7 @@ namespace Assets.MyGame.Scripts.Net //public const string baseURL = "http://game5241.wzaxr.top"; #if UNITY_EDITOR //public const string baseURL = "http://8.137.163.153"; - public const string baseURL = "http://47.109.34.50"; + public const string baseURL = "http://47.109.132.12"; #else //public const string baseURL = "http://8.137.8.46"; diff --git a/Assets/MyGame/Scripts/Net/WebResponses/SyncGameData.cs b/Assets/MyGame/Scripts/Net/WebResponses/SyncGameData.cs index 1e76ecf58..c07aa6212 100644 --- a/Assets/MyGame/Scripts/Net/WebResponses/SyncGameData.cs +++ b/Assets/MyGame/Scripts/Net/WebResponses/SyncGameData.cs @@ -44,7 +44,7 @@ namespace Assets.MyGame.Scripts.Net.WebResponses none = 0, rotateTurrect=1, fireBullet=2, - createFish=3, + // createFish=3 已废弃:鱼创建改由服务端 OnFishSpawn 事件下发 changeAutoTarget=4, randomDieFish=5, changeTurretState=6, @@ -52,8 +52,7 @@ namespace Assets.MyGame.Scripts.Net.WebResponses freezeFish=8, changeCannonMultiple=9, changeWing=10, - /// 新玩家资源加载完成,请求房主广播场上活鱼 - requestSyncFish=11 + // requestSyncFish=11 已废弃:鱼同步改由服务端 OnFishSpawn(type=sync) 事件下发 } [Serializable] public class FireCannonSendData @@ -145,4 +144,93 @@ namespace Assets.MyGame.Scripts.Net.WebResponses { public int cannonMultiple; } + + // ================================================================ + // OnFishSpawn 事件数据结构 (服务端出鱼,破坏性替代 createFish 帧同步) + // ================================================================ + + /// + /// 服务端出鱼事件 OnFishSpawn 的数据载体。 + /// type: + /// "init" - 首次出鱼(玩家进入房间后服务端推送初始鱼) + /// "spawn" - 定时出鱼(游戏中持续刷怪) + /// "sync" - 重连/取消挂机同步(把当前场上活鱼推送给回归玩家) + /// + [Serializable] + public class OnFishSpawnData + { + /// "init" | "spawn" | "sync" + public string type; + /// 服务器当前时间戳(毫秒) + public long timestamp; + /// 本批次出鱼列表 + public List fish; + } + + /// + /// OnFishSpawn 单条鱼描述。 + /// 客户端据此调用 SpawnFishMainThread 生成鱼实例。 + /// + [Serializable] + public class OnFishSpawnFishItem + { + /// 服务端分配的全局唯一鱼 ID(作为 guid 使用) + public long fishId; + /// 鱼编号(配置 fishNumber) + public string fishNumber; + /// 路径编号 + public int routeId; + /// 鱼阵编号(非鱼阵时为空字符串) + public string fishArrayNumber; + /// 鱼阵的 Boss 鱼编号 + public string bossFishNumber; + /// 鱼阵普通鱼数量 + public int fishCount; + /// 鱼阵 Boss 数量 + public int bossCount; + /// 鱼过期时间戳(毫秒),到期自动移除 + public long expireAt; + /// 鱼剩余存活秒数(客户端做倒计时用) + public int remainingSeconds; + } + + /// + /// 机器人退出房间通知数据 BotLeaveRoomNotice: { userId } + /// + [Serializable] + public class BotLeaveRoomNoticeData + { + public string userId; + } + + /// + /// 玩家挂机/取消挂机通知数据: { userId } + /// + [Serializable] + public class PlayerAfkNoticeData + { + public string userId; + } + + /// + /// 全局击杀公告 KillNotice 数据。 + /// 真人击杀时只填基础字段;机器人模拟时额外填 fishName/multiplier/sceneName。 + /// + [Serializable] + public class KillNoticeData + { + /// 鱼编号 + public string FishId; + /// 玩家名 + public string userName; + /// 金币 + public long gold; + // —— 机器人模拟时额外字段 —— + /// 鱼名(机器人模拟时填充) + public string fishName; + /// 倍数(机器人模拟时填充) + public int? multiplier; + /// 场景名(机器人模拟时填充) + public string sceneName; + } } diff --git a/Assets/MyGame/Scripts/Net/WebSocketManager.cs b/Assets/MyGame/Scripts/Net/WebSocketManager.cs index 84efa660b..a83bc607e 100644 --- a/Assets/MyGame/Scripts/Net/WebSocketManager.cs +++ b/Assets/MyGame/Scripts/Net/WebSocketManager.cs @@ -33,7 +33,7 @@ namespace Assets.MyGame.Scripts.Net //private const string baseUrl = "ws://game5241.wzaxr.top"; #if UNITY_EDITOR //private const string baseUrl = "ws://8.137.163.153"; - private const string baseUrl = "ws://47.109.34.50"; + private const string baseUrl = "ws://47.109.132.12"; #else //private const string baseUrl = "ws://8.137.8.46"; diff --git a/Assets/MyGame/Scripts/Net/WebSocketManager_Message.cs b/Assets/MyGame/Scripts/Net/WebSocketManager_Message.cs index 8337682e4..2811388d0 100644 --- a/Assets/MyGame/Scripts/Net/WebSocketManager_Message.cs +++ b/Assets/MyGame/Scripts/Net/WebSocketManager_Message.cs @@ -453,31 +453,20 @@ namespace Assets.MyGame.Scripts.Net break; case "ChangeControllerUser": { + // ⭐ 房主控制权移交:仅维护 roomOwnerSit,不再设置 fangzhu + // (鱼创建已由服务端 OnFishSpawn 统一下发,客户端无需房主出鱼) var userId = JsonConvert.DeserializeObject>(msg).arguments[0]; if (userId == GameManager.Instance.user.id) { - // 本地玩家成为房主:开启出鱼 + 同步本地房主引用 - CreateFishManager.Instance.fangzhu = true; if (SitPosData.localPlayerSit != null) SitPosData.roomOwnerSit = SitPosData.localPlayerSit; } else if (userId != null && userId.StartsWith("bot_")) { - // 房主是人机:人机无法真正发包,由本地首个非人机玩家代发 + // 房主是人机:由本地首个非人机玩家代行房主显示职责 SitPosData.TransferRoomOwner(); - CreateFishManager.Instance.fangzhu = - SitPosData.roomOwnerSit != null && - SitPosData.roomOwnerSit.user == GameManager.Instance.user.id; - } - else - { - // ⭐ 只在 userId 非空且确实是真人玩家时才停止本地出鱼 - // userId 为空(异常情况)时保持原状,避免误关出鱼导致 bot 退出后不出鱼 - if (!string.IsNullOrEmpty(userId)) - CreateFishManager.Instance.fangzhu = false; } } - break; case "Exception": { @@ -590,18 +579,8 @@ namespace Assets.MyGame.Scripts.Net break; case "KillNotice": - { - var data = JsonConvert.DeserializeObject>>(msg).arguments[0]; - if (data == null) break; - // { FishId, userName, gold } - string userName = data.TryGetValue("userName", out var un) ? un?.ToString() : ""; - string fishId = data.TryGetValue("FishId", out var fi) ? fi?.ToString() : ""; - long gold = data.TryGetValue("gold", out var g) ? Convert.ToInt64(g) : 0; - string roomName = GameManager.Instance.gameData.currRoomData?.roomName ?? ""; - string roomLevel = GameManager.Instance.gameData.currRoomData?.roomLevel.ToString() ?? ""; - string msgText = $"恭喜用户{userName} 在 {roomName} {roomLevel}场 捕获了{fishId}*{gold}倍"; - //UIManager.Instance.ShowBoardGongGao(msgText); - } + // 全局击杀公告(真人击杀/机器人模拟) + WebSocketReponseList.KillNotice(msg); break; case "OnFishExpired": @@ -612,49 +591,48 @@ namespace Assets.MyGame.Scripts.Net break; case "OnFishList": - { - var fishList = JsonConvert.DeserializeObject>(msg).arguments[0]; - Debug.Log($"[FishGameHub] 鱼列表刷新, 数量={fishList?.Length}"); - } + // 鱼列表刷新 + WebSocketReponseList.OnFishList(msg); break; // ======== FishGameHub / FishRoomHub 补充事件 ======== case "BotLeaveRoomNotice": - { - // 机器人退出房间,数据 { userId } - var data = JsonConvert.DeserializeObject>>(msg).arguments[0]; - string botUserId = data != null && data.TryGetValue("userId", out var buid) ? buid?.ToString() : ""; - Debug.Log($"[FishGameHub] 机器人退出: {botUserId}"); - WebSocketReponseList.BotLeaveRoom(botUserId); - } + // 机器人退出房间,数据 { userId } + WebSocketReponseList.BotLeaveRoomNotice(msg); break; case "HostDisconnect": - { - // 房主断线,其他玩家可接受机器人控制权 - Debug.Log("[FishGameHub] 房主断线,可接受机器人控制权"); - WebSocketReponseList.HostDisconnect(); - } + // 房主断线,其他玩家可接受机器人控制权 + WebSocketReponseList.HostDisconnectNotice(msg); break; case "OnSeatChange": - { - // 座位变更 { type, userId, position, showWaiting? } - var data = JsonConvert.DeserializeObject>>(msg).arguments[0]; - if (data != null) - { - // 注意:JSON 字段可能是 long/double/string/bool 等任意类型, - // 直接 Convert.ToInt32(object) 对字符串会走 int.Parse 抛 FormatException。 - // 这里统一通过 ToString() + TryParse 容错。 - int seatType = data.TryGetValue("type", out var t) && int.TryParse(t?.ToString(), out var st) ? st : 0; - string userId = data.TryGetValue("userId", out var u) ? u?.ToString() : ""; - int position = data.TryGetValue("position", out var p) && int.TryParse(p?.ToString(), out var pos) ? pos : -1; - bool showWaiting = data.TryGetValue("showWaiting", out var sw) && (sw is bool b ? b : (sw != null && bool.TryParse(sw.ToString(), out var bv) && bv)); - Debug.Log($"[FishGameHub] 座位变更: type={seatType}, userId={userId}, position={position}, showWaiting={showWaiting}"); - WebSocketReponseList.SeatChange(seatType, userId, position, showWaiting); - } - } + // 座位变更 { type, userId, position, showWaiting? } + WebSocketReponseList.OnSeatChange(msg); + break; + + // ======== 新协议事件 ======== + + case "OnFishSpawn": + // ⭐ 服务端出鱼事件(破坏性替代 createFish 帧同步) + // type: "init"=首次出鱼 / "spawn"=定时出鱼 / "sync"=重连或取消挂机同步 + WebSocketReponseList.OnFishSpawn(msg); + break; + + case "FishFrozen": + // ⭐ 冰冻道具(40003)生效:鱼存活时间延长 15 秒,客户端写死 15s 倒计时 + WebSocketReponseList.FishFrozen(msg); + break; + + case "PlayerAfkNotice": + // 玩家挂机通知 { userId } + WebSocketReponseList.PlayerAfkNotice(msg); + break; + + case "PlayerCancelAfkNotice": + // 玩家取消挂机通知 { userId } + WebSocketReponseList.PlayerCancelAfkNotice(msg); break; case "RoomChange": diff --git a/Assets/MyGame/Scripts/Net/WebSocketReponseList.cs b/Assets/MyGame/Scripts/Net/WebSocketReponseList.cs index a7ae71707..c182aaaa1 100644 --- a/Assets/MyGame/Scripts/Net/WebSocketReponseList.cs +++ b/Assets/MyGame/Scripts/Net/WebSocketReponseList.cs @@ -233,9 +233,7 @@ namespace Assets.MyGame.Scripts.Net // 与 RoomFireNotice(FireCannonSendData) 分开处理,仅用于存储角度到 botLastAngle EventCenter.Instance.EventTrigger(ConstEventString.SyncGameData_BotFireAngle, syncGameData); break; - case FrameDataType.createFish: - EventCenter.Instance.EventTrigger(ConstEventString.SyncGameData_CreateFish, syncGameData); - break; + // createFish=3 已废弃:鱼创建改由服务端 OnFishSpawn 事件下发 case FrameDataType.changeAutoTarget: EventCenter.Instance.EventTrigger(ConstEventString.SyncGameData_ChangeTargetMonster, syncGameData); break; @@ -258,15 +256,123 @@ namespace Assets.MyGame.Scripts.Net case FrameDataType.changeWing: EventCenter.Instance.EventTrigger(ConstEventString.SyncGameData_ChangeWing, syncGameData); break; - case FrameDataType.requestSyncFish: - EventCenter.Instance.EventTrigger(ConstEventString.SyncGameData_RequestSyncFish, syncGameData); - break; + // requestSyncFish=11 已废弃:鱼同步改由服务端 OnFishSpawn(type=sync) 下发 default: break; } } + + // ================================================================ + // 新协议事件分发 (OnFishSpawn / PlayerAfk / FishFrozen / KillNotice 等) + // ================================================================ + + /// + /// 服务端出鱼事件 OnFishSpawn 处理。 + /// 破坏性替代旧 createFish 帧同步:服务端统一出鱼,客户端只接收并生成。 + /// + public static void OnFishSpawn(string msg) + { + var data = JsonConvert.DeserializeObject>(msg).arguments[0]; + if (data == null) return; + EventCenter.Instance.EventTrigger(ConstEventString.OnFishSpawn, data); + } + + /// 鱼列表刷新 + public static void OnFishList(string msg) + { + var fishList = JsonConvert.DeserializeObject>(msg).arguments[0]; + EventCenter.Instance.EventTrigger(ConstEventString.OnFishList, fishList); + } + + /// + /// 冰冻道具(40003)生效事件 FishFrozen。 + /// 鱼存活时间延长 15 秒,客户端写死 15s 倒计时。 + /// + public static void FishFrozen(string msg) + { + EventCenter.Instance.EventTrigger(ConstEventString.FishFrozen); + } + + /// 玩家挂机通知 + public static void PlayerAfkNotice(string msg) + { + var data = JsonConvert.DeserializeObject>(msg).arguments[0]; + if (data == null) return; + EventCenter.Instance.EventTrigger(ConstEventString.PlayerAfkNotice, data); + } + + /// 玩家取消挂机通知 + public static void PlayerCancelAfkNotice(string msg) + { + var data = JsonConvert.DeserializeObject>(msg).arguments[0]; + if (data == null) return; + EventCenter.Instance.EventTrigger(ConstEventString.PlayerCancelAfkNotice, data); + } + + /// + /// 全局击杀公告 KillNotice。 + /// 真人击杀/机器人模拟,机器人模拟时额外带 fishName/multiplier/sceneName。 + /// + public static void KillNotice(string msg) + { + var data = JsonConvert.DeserializeObject>(msg).arguments[0]; + if (data == null) return; + EventCenter.Instance.EventTrigger(ConstEventString.KillNotice, data); + } + + /// 机器人退出房间通知 + public static void BotLeaveRoomNotice(string msg) + { + var data = JsonConvert.DeserializeObject>(msg).arguments[0]; + if (data == null) return; + WebSocketReponseList.BotLeaveRoom(data.userId); + } + + /// 房主断线 + public static void HostDisconnectNotice(string msg) + { + EventCenter.Instance.EventTrigger(ConstEventString.HostDisconnect); + WebSocketReponseList.HostDisconnect(); + } + + /// + /// 座位变更 OnSeatChange。 + /// 数据: { type, userId, position, showWaiting? } + /// + public static void OnSeatChange(string msg) + { + var data = JsonConvert.DeserializeObject>>(msg).arguments[0]; + if (data == null) return; + // JSON 字段可能是 long/double/string/bool 等任意类型,统一 ToString()+TryParse 容错 + int seatType = data.TryGetValue("type", out var t) && int.TryParse(t?.ToString(), out var st) ? st : 0; + string userId = data.TryGetValue("userId", out var u) ? u?.ToString() : ""; + int position = data.TryGetValue("position", out var p) && int.TryParse(p?.ToString(), out var pos) ? pos : -1; + bool showWaiting = data.TryGetValue("showWaiting", out var sw) && (sw is bool b ? b : (sw != null && bool.TryParse(sw.ToString(), out var bv) && bv)); + WebSocketReponseList.SeatChange(seatType, userId, position, showWaiting); + } + + /// + /// 房主控制权移交 ChangeControllerUser。 + /// 仅维护 SitPosData.roomOwnerSit,不再设置 fangzhu(鱼创建已由服务端统一出鱼)。 + /// + public static void ChangeControllerUser(string msg) + { + var userId = JsonConvert.DeserializeObject>(msg).arguments[0]; + if (userId == GameManager.Instance.user.id) + { + // 本地玩家成为房主:同步本地房主引用 + if (SitPosData.localPlayerSit != null) + SitPosData.roomOwnerSit = SitPosData.localPlayerSit; + } + else if (userId != null && userId.StartsWith("bot_")) + { + // 房主是人机:人机无法真正发包,由本地首个非人机玩家代行房主显示职责 + SitPosData.TransferRoomOwner(); + } + // 其他真人玩家成为房主时无需本地操作(roomOwnerSit 由 TransferRoomOwner/ShowPlayer 维护) + } public static void LeaveRoomResult(string msg) { //WebSocketManager.Instance.isGame = false; diff --git a/Assets/MyGame/Scripts/Net/WebSocketSendList_Fish.cs b/Assets/MyGame/Scripts/Net/WebSocketSendList_Fish.cs index a724c5ba2..715756d46 100644 --- a/Assets/MyGame/Scripts/Net/WebSocketSendList_Fish.cs +++ b/Assets/MyGame/Scripts/Net/WebSocketSendList_Fish.cs @@ -242,6 +242,26 @@ public partial class WebSocketSendList var sendObj = new WebSocketSendList("ChangeGunMultiple", roomId, userId, multiple); WebSocketManager.Instance.Send(SocketType.game, JsonConvert.SerializeObject(sendObj)); } + + /// + /// 玩家挂机(对应服务端 SetPlayerAfkNEW) + /// 服务端会向房间组广播 PlayerAfkNotice 事件 + /// + public static void SetPlayerAfk(string roomId, string userId) + { + var sendObj = new WebSocketSendList("SetPlayerAfkNEW", roomId, userId); + WebSocketManager.Instance.Send(SocketType.game, JsonConvert.SerializeObject(sendObj)); + } + + /// + /// 取消挂机(对应服务端 CancelPlayerAfkNEW) + /// 服务端会向房间组广播 PlayerCancelAfkNotice 事件,并通过 OnFishSpawn(type=sync) 同步当前鱼 + /// + public static void CancelPlayerAfk(string roomId, string userId) + { + var sendObj = new WebSocketSendList("CancelPlayerAfkNEW", roomId, userId); + WebSocketManager.Instance.Send(SocketType.game, JsonConvert.SerializeObject(sendObj)); + } /// /// 同步等级常量(秒) @@ -284,17 +304,14 @@ public partial class WebSocketSendList _syncLv1Append.Add(sgd); return; - case FrameDataType.createFish: + // createFish=3 已废弃:鱼创建改由服务端 OnFishSpawn 下发 case FrameDataType.changeTurretState: case FrameDataType.changeTurret: case FrameDataType.freezeFish: _syncLv2Append.Add(sgd); return; - case FrameDataType.requestSyncFish: - // ⭐ 新玩家加载完成的"请求同步鱼":立即发送,房主尽快响应广播 - SendImmediate(BuildJson(sgd)); - return; + // requestSyncFish=11 已废弃:鱼同步改由服务端 OnFishSpawn(type=sync) 下发 default: _syncLv3Append.Add(sgd); diff --git a/Assets/MyGame/Scripts/Turret/SitPosData.cs b/Assets/MyGame/Scripts/Turret/SitPosData.cs index 2cea840e2..63f2c34bb 100644 --- a/Assets/MyGame/Scripts/Turret/SitPosData.cs +++ b/Assets/MyGame/Scripts/Turret/SitPosData.cs @@ -153,6 +153,7 @@ public class SitPosData : MonoBehaviour } /// 房主退出后,将房主身份转移给第一个活跃的非人机玩家 + /// 鱼创建已由服务端 OnFishSpawn 统一下发,此处仅维护 roomOwnerSit 标识(用于 bot 控制/AFK 等场景)。 public static void TransferRoomOwner() { var list = FindObjectsOfType(); @@ -161,29 +162,18 @@ public class SitPosData : MonoBehaviour if (!sit.isBot && sit.gameObject.activeInHierarchy && sit != roomOwnerSit) { roomOwnerSit = sit; - // 同步出鱼权限:继承者是本地玩家时接管出鱼 - if (CreateFishManager.Instance != null) - { - CreateFishManager.Instance.fangzhu = - sit.user == GameManager.Instance?.user?.id; - } break; } } } /// 分配房主(首个非人机玩家) + /// 鱼创建已由服务端 OnFishSpawn 统一下发,此处仅维护 roomOwnerSit 标识(用于 bot 控制/AFK 等场景)。 public static void AssignRoomOwner(SitPosData sit) { if (roomOwnerSit == null && !sit.isBot) { roomOwnerSit = sit; - // 同步出鱼权限:本地玩家作为首个非人机玩家时接管出鱼 - if (CreateFishManager.Instance != null) - { - CreateFishManager.Instance.fangzhu = - sit.user == GameManager.Instance?.user?.id; - } } } diff --git a/Assets/MyGame/aa/UI/Game/GameMainPanel_FishBase.cs b/Assets/MyGame/aa/UI/Game/GameMainPanel_FishBase.cs index 4504fda83..eb5f2006b 100644 --- a/Assets/MyGame/aa/UI/Game/GameMainPanel_FishBase.cs +++ b/Assets/MyGame/aa/UI/Game/GameMainPanel_FishBase.cs @@ -148,7 +148,7 @@ public class GameMainPanel_FishBase : GameMainPanel_Base public Image img_BingFeng; public GameObject bingFengEffect; // 冰封特效预制体(首次Instantiate,之后激活/隐藏) private GameObject _bingFengInstance; // 克隆出来的实例 - float bingFengDuration = 5f; // 冰封状态持续时间 + float bingFengDuration = 15f; // 冰封状态持续时间(服务端 FishFrozen 延长 15 秒,客户端写死 15s 倒计时) public float currBingFengTime = 0f; private Dictionary _frozenAnimSpeeds = new Dictionary(); @@ -437,7 +437,8 @@ public class GameMainPanel_FishBase : GameMainPanel_Base EventCenter.Instance.AddEventListener(ConstEventString.SyncGameData_ChangeTurret, SyncGameData_ChangeTurret); EventCenter.Instance.AddEventListener(ConstEventString.SyncGameData_ChangeCannonMultiple, SyncGameData_ChangeCannonMultiple); EventCenter.Instance.AddEventListener(ConstEventString.SyncGameData_ChangeWing, SyncGameData_ChangeWing); - EventCenter.Instance.AddEventListener(ConstEventString.SyncGameData_FreezeFish, SyncGameData_FreezeFish); + // SyncGameData_FreezeFish 已废弃:冰冻改由服务端 FishFrozen 事件统一触发 + EventCenter.Instance.AddEventListener(ConstEventString.FishFrozen, OnFishFrozen); EventCenter.Instance.AddEventListener(ConstEventString.SyncGameData_BotFireAngle, SyncGameData_BotFireAngle); btn_TuJian.onClick.AddListener(() => { @@ -467,7 +468,6 @@ public class GameMainPanel_FishBase : GameMainPanel_Base { // 确认:直接退出房间 WebSocketSendList.LeaveRoom(GameManager.Instance.gameData.currRoomData.roomId, currSitData.user); - CreateFishManager.Instance.fangzhu = false; CancelLeaveCountdown(); }); } @@ -517,7 +517,8 @@ public class GameMainPanel_FishBase : GameMainPanel_Base EventCenter.Instance.RemoveEventListener(ConstEventString.SyncGameData_ChangeTurret, SyncGameData_ChangeTurret); EventCenter.Instance.RemoveEventListener(ConstEventString.SyncGameData_ChangeCannonMultiple, SyncGameData_ChangeCannonMultiple); EventCenter.Instance.RemoveEventListener(ConstEventString.SyncGameData_ChangeWing, SyncGameData_ChangeWing); - EventCenter.Instance.RemoveEventListener(ConstEventString.SyncGameData_FreezeFish, SyncGameData_FreezeFish); + // SyncGameData_FreezeFish 已废弃:冰冻改由服务端 FishFrozen 事件统一触发 + EventCenter.Instance.RemoveEventListener(ConstEventString.FishFrozen, OnFishFrozen); EventCenter.Instance.RemoveEventListener(ConstEventString.SyncGameData_BotFireAngle, SyncGameData_BotFireAngle); // ⭐ 离场清理:子弹池 + 特效池彻底清空,防止内存泄漏 @@ -718,25 +719,9 @@ public class GameMainPanel_FishBase : GameMainPanel_Base await Task.Delay(2000); - // ⭐ 进度条加载完毕:房主创建一批初始鱼(进度 0.2~0.8),屏幕开局就有鱼 - // 只在房主端调用,通过 SyncGameData 广播给所有客户端 - // 非房主(后加入的真人玩家):发"请求同步鱼"让房主广播场上活鱼 - // 这里只发请求,实际接收广播在 CreateFishManager.CreateFish(已做 guid 去重) - if (CreateFishManager.Instance.fangzhu) - { - CreateFishManager.Instance.CreateInitialFishes(Random.Range(10, 21)); - } - else - { - var req = new SyncGameData - { - sitPos = GameManager.Instance.gameData.roomJoinNotice.position, - frameType = FrameDataType.requestSyncFish, - time = BeijingTimeManager.Instance.CurrentTimestampMs, - frameData = null - }; - WebSocketSendList.SyncGameData(req); - } + // ⭐ 鱼创建由服务端 OnFishSpawn 事件统一下发,客户端无需房主出鱼/请求同步。 + // 玩家进入房间后服务端会主动推送 OnFishSpawn(type="init") 初始鱼, + // 重连/取消挂机时服务端会推送 OnFishSpawn(type="sync") 同步当前鱼。 JinDuTiao.SetProgress(1); await Task.Delay(300); @@ -1158,12 +1143,10 @@ public class GameMainPanel_FishBase : GameMainPanel_Base toggle_BingFeng.SetIsOnWithoutNotify(false); currBingFengTime = 0f; if (img_BingFeng != null) img_BingFeng.fillAmount = 1f; - // ⭐ 冰冻结束:本地解冻 + 广播给其他客户端解冻 - // 之前只设 toggle.isOn=false,但 onValueChanged 监听器 if(!on)return - // 导致解冻逻辑永远不执行,isFrozen 永远 true,之后永久不出鱼 + // ⭐ 冰冻结束:本地解冻(服务端不再下发解冻事件,15s 后客户端自动解冻) + // 之前 SyncFreeze(false) 客户端广播已废弃 CreateFishManager.Instance.UnfreezeAllFish(); ResumeAllFishAnimations(); - SyncFreeze(false); if (_bingFengInstance != null) _bingFengInstance.SetActive(false); } } @@ -2133,52 +2116,47 @@ public class GameMainPanel_FishBase : GameMainPanel_Base case "40003": // 冰封 { - // ⭐ 个人效果(数量/toggle/倒计时/广播):只有使用者自己执行 - // 服务器把 UsePropNotice 广播给房间内所有人,不判断 userId 会导致 - // 别人用冰封,本地数量也减了、toggle 也亮了、倒计时也走了 + // ⭐ 新协议:冰冻房间效果由服务端 FishFrozen 事件统一触发, + // UsePropNotice(40003) 只处理个人效果(数量/toggle) + // 房间效果(冻结鱼/暂停动画/冰冻特效)在 OnFishFrozen 中执行 bool isLocalSender = userId == GameManager.Instance.user.id; if (isLocalSender) { _localBingFengCount--; UpdateSkillPropsUI(); - currBingFengTime = bingFengDuration; - toggle_BingFeng.SetIsOnWithoutNotify(true); - // ⭐ 只有发起者广播,避免其他客户端重复广播 - SyncFreeze(true); + // 倒计时和 toggle 由 OnFishFrozen 统一启动,这里不重复设置 } - // ⭐ 房间效果(冻结鱼/暂停动画/冰冻特效):所有客户端都执行 - // FreezeAllFish/PauseAllFishAnimations 内部有 isFrozen 守卫, - // 重复调用安全(不会覆盖已记录的状态) - if (bingFengEffect != null) - { - if (_bingFengInstance == null) - _bingFengInstance = Instantiate(bingFengEffect); - _bingFengInstance.SetActive(true); - } - CreateFishManager.Instance.FreezeAllFish(); - PauseAllFishAnimations(); break; } } } - public void SyncGameData_FreezeFish(SyncGameData sync) + /// + /// 服务端 FishFrozen 事件回调:冰冻道具(40003)生效。 + /// 鱼存活时间延长 15 秒,客户端写死 15s 倒计时。 + /// 所有客户端都执行(房间效果)。 + /// + public void OnFishFrozen() { - if (sync.sitPos == currSitData.posID) return; // 自己发的不管 + // 启动 15s 倒计时 + currBingFengTime = bingFengDuration; + toggle_BingFeng.SetIsOnWithoutNotify(true); - var data = FrameDataParser.Parse(sync.frameData); - if (data.isFreeze) + // 冰冻特效 + if (bingFengEffect != null) { - CreateFishManager.Instance.FreezeAllFish(); - PauseAllFishAnimations(); - } - else - { - CreateFishManager.Instance.UnfreezeAllFish(); - ResumeAllFishAnimations(); + if (_bingFengInstance == null) + _bingFengInstance = Instantiate(bingFengEffect); + _bingFengInstance.SetActive(true); } + // 冻结所有鱼 + 暂停动画 + CreateFishManager.Instance.FreezeAllFish(); + PauseAllFishAnimations(); } + // SyncGameData_FreezeFish 已废弃:冰冻改由服务端 FishFrozen 事件统一触发 + // SyncFreeze 已废弃:不再需要客户端广播冰冻状态 + #endregion #region 冰冻动画暂停/恢复 @@ -2308,7 +2286,6 @@ public class GameMainPanel_FishBase : GameMainPanel_Base public void TuiChuHHWK() { WebSocketSendList.LeaveRoom(GameManager.Instance.gameData.currRoomData.roomId, currSitData.user); - CreateFishManager.Instance.fangzhu = false; } #endregion @@ -2707,27 +2684,16 @@ public class GameMainPanel_FishBase : GameMainPanel_Base botLastAngle.Remove(sit.user); botLastFireData.Remove(sit.user); - // ⭐ bot 退出后转移房主,否则房主是已退出的 bot 会导致出鱼停止 + // ⭐ bot 退出后转移房主,否则房主是已退出的 bot 会导致房主显示异常 // 1) 清除 bot 的房主引用(如果它是房主) // 2) TransferRoomOwner 会把房主交给第一个活跃的非 bot 玩家 + // 注:鱼创建已由服务端 OnFishSpawn 统一下发,不再需要 fangzhu 兜底 if (SitPosData.roomOwnerSit == sit) { SitPosData.roomOwnerSit = null; SitPosData.TransferRoomOwner(); } - // ⭐ 强制保证本地玩家能继续出鱼: - // bot 退出后如果本地玩家是新的房主(或房主为空但本地玩家在场),开启 fangzhu - // 这是对 ChangeControllerUser 网络消息的兜底,防止网络消息丢失或时序问题导致不出鱼 - if (CreateFishManager.Instance != null) - { - bool localIsOwner = SitPosData.roomOwnerSit != null && - SitPosData.roomOwnerSit.user == GameManager.Instance.user.id; - bool noOwner = SitPosData.roomOwnerSit == null; - if (localIsOwner || noOwner) - CreateFishManager.Instance.fangzhu = true; - } - if (cfg != null && cfg.exitCondition != null && cfg.exitCondition.showWaitingAfterExit) sit.ShowWaiting(); else diff --git a/Assets/MyGame/aa/UI/Home/Sub/UIHome_BY_SelectRoomLevelPanel.cs b/Assets/MyGame/aa/UI/Home/Sub/UIHome_BY_SelectRoomLevelPanel.cs index b1103c465..06c957899 100644 --- a/Assets/MyGame/aa/UI/Home/Sub/UIHome_BY_SelectRoomLevelPanel.cs +++ b/Assets/MyGame/aa/UI/Home/Sub/UIHome_BY_SelectRoomLevelPanel.cs @@ -122,6 +122,7 @@ public class UIHome_BY_SelectRoomLevelPanel : SubUIBase else { // ⭐ 快速加入:调 /api/Room/QuickJoin 拿 roomId + webSocketUrl → 连 socket → JoinRoom(-1) + // leave: 1=初级场 / 2=中级场 / 3=高级场 (-1=全场次快速进入,暂未实现) int gameType = (int)GameManager.Instance.gameData.currGameType; APIRequestList.RoomQuickJoin(gameType, leave, (resp) => {