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