// ============================================================ // 文件:CreateFishManager.cs // 职责:捕鱼游戏 —— 鱼的生成 / 销毁 / 鱼阵管理核心管理器 // 优化点: // 1. 消除 Update 中重复创建 List、字典查找性能问题 // 2. 统一鱼阵路线选取逻辑,减少冗余代码 // 3. FishSpawnTimerJob shouldSpawn 逻辑修正(原逻辑只在 ==0 时计时,现保持一致) // 4. 增加必要的空检查与防御性编程 // 5. 清理无用注释与废弃代码块 // 6. 方法职责单一化拆分 // ============================================================ using Assets.MyGame.Scripts.Characters.Monster; using Assets.MyGame.Scripts.Net; using Assets.MyGame.Scripts.Net.API; using Assets.MyGame.Scripts.Net.API.GameConfig_SubClass; using Assets.MyGame.Scripts.Net.APIResponses.Enum; using Assets.MyGame.Scripts.Net.APIResponses.GameConfig_SubClass; using Assets.MyGame.Scripts.Net.WebResponses; using cfg; using Newtonsoft.Json; using System; using System.Collections; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using TMPro; using Unity.Collections; using Unity.Jobs; using UnityEditor; using UnityEngine; using Random = UnityEngine.Random; /// /// 捕鱼游戏核心管理器:负责鱼的定时生成、网络同步、阵型调度、死亡特效。 /// 继承 MonoSingleton 保证全局唯一。 /// public class CreateFishManager : MonoSingleton { // ---------------------------------------------------------------- // 外部依赖 // ---------------------------------------------------------------- /// 当前游戏全局配置(通过 GameManager 获取) public GameConfig M_GameConfig => GameManager.Instance.gameConfig; // ---------------------------------------------------------------- // 配置缓存(空间换时间,避免 List.Find O(n) 查找) // ---------------------------------------------------------------- /// roomFishConfigs 按游戏类型索引 private Dictionary roomFishConfig; /// 每个游戏类型下 fishNumber → RoomFishDetailConfig private Dictionary> createDic; /// fishNumber → FishTypeConfig(基础配置缓存) private Dictionary fishDatas; /// fishArrayNumber → FishArrayConfig(鱼阵配置缓存) private Dictionary fishArrayConfigDic; // ---------------------------------------------------------------- // UI 引用 // ---------------------------------------------------------------- /// 当前游戏面板(用于坐标转换、金币飞行等) public GameMainPanel_FishBase currGameType; // ---------------------------------------------------------------- // 死亡特效 Prefab(Inspector 赋值) // ---------------------------------------------------------------- public GameObject dieFXPrefab; // 基础死亡特效 public GameObject dieFXPrefabAward; // 普通鱼金币奖励特效 public GameObject dieFXPrefabAwardTmp; // Boss 分数浮动文本 public GameObject dieFXPrefabAwardTmp_AddScore; // ---------------------------------------------------------------- // 生成系统运行时数据 // ---------------------------------------------------------------- /// 鱼的父节点(保持场景层级整洁) public Transform fishRoot; /// 鱼的父节点(保持场景层级整洁) public Transform poolRoot; /// 当前房间可生成鱼列表(与 NativeArray 下标对齐) public List spawnList; /// 当前房间 fishNumber → RoomFishDetailConfig public Dictionary spawnDic; public bool isCreateFish; //是否可以创建鱼 /// 按鱼类型分类的生成配置(Boss / 普通 / 黄金…) private Dictionary> fishTypeConfig; // ------ 鱼阵路线相关 ------ /// arrayNumber → 该阵型所有可用生成配置 private Dictionary> yz_FishConfig_Line; /// 列表下标 → arrayNumber(用于随机取阵型) private Dictionary yz_FishConfig_Line_Dic; /// arrayNumber → 列表下标(反向索引) private Dictionary yz_FishConfig_Line_Dic1; /// 二维列表,与字典保持同步(供随机无重复取阵型用) private List> yz_FishConfig_Line_List; // ------ Job System NativeArray ------ private NativeArray timers; // 每条鱼的累计计时 private NativeArray frequencies; // 每条鱼的生成间隔 private NativeArray shouldSpawn; // 1=本帧应尝试生成,0=冷却中 // ---------------------------------------------------------------- // 运行时鱼实例字典(guid 为 key) // ---------------------------------------------------------------- /// 全部存活鱼(包含 Boss / 小鱼 / 黄金鱼) public Dictionary fishDatas_hashCodekey = new Dictionary(); /// 普通鱼 public Dictionary fishDatas_hashCodekey_Small = new Dictionary(); /// Boss 鱼 public Dictionary fishDatas_hashCodekey_Boss = new Dictionary(); /// 黄金 public Dictionary fishDatas_hashCodekey_HJ = new Dictionary(); /// 特殊鱼(非 Boss) public Dictionary fishDatas_hashCodekey_TS = new Dictionary(); /// 特殊 Boss public Dictionary fishDatas_hashCodekey_TS_Boss = new Dictionary(); // ------ 各类计时器 ------ private RoomFishProbabilityConfig tsbossCreateData; // 特殊Boss 创建数据 private float tsbossTimer = 999f; // 特殊Boss 刷新倒计时(秒) private int tsbossCount; // 本轮同时启用特殊Boss鱼数量 private int tsbossQuanZhongCount; // 本轮同时启用特殊Boss鱼数量 private RoomFishProbabilityConfig tsCreateData; // 特殊鱼 创建数据 private float tsTimer = 999f; // 特殊鱼 刷新倒计时(秒) private int tsCount; // 本轮同时启用特殊鱼数量 private int tsQuanZhongCount; // 本轮同时启用特殊鱼数量 private RoomFishProbabilityConfig bossCreateData; // Boss 创建数据 private float bossTimer = 999f; // Boss 刷新倒计时(秒) private int bossCount; // 本轮同时启用Boss鱼数量 private int bossQuanZhongCount; // 本轮同时启用Boss鱼数量 private RoomFishProbabilityConfig yzCreateData; // 鱼阵创建数据 private float yzTimer = 999f; // 鱼阵刷新倒计时(秒) private int yzCount; // 本轮同时激活阵型数量 private int yz_LineCount = 0; // 场景内鱼阵路线总数 private int yzQuanZhongCount = 0; // 场景内鱼阵路线总数 private RoomFishProbabilityConfig ptCreateData; // 普通鱼创建数据 private float ptTimer = 999f; // 普通鱼批量启用倒计时(秒) private int ptCount; // 本轮同时启用普通鱼数量 private int ptQuanZhongCount; // 本轮同时启用普通鱼数量 private RoomFishProbabilityConfig hjCreateData; // 普通鱼创建数据 private float hjTimer = 999f; // 黄金鱼批量启用倒计时(秒) private int hjCount; // 本轮同时启用黄金鱼数量 private int hjQuanZhongCount; // 本轮同时启用黄金鱼数量 /// 是否开启放鱼(false 时 Update 不执行生成逻辑) public bool fangzhu; public bool isResources = false; // ================================================================ // 懒加载属性 // ================================================================ /// /// 鱼基础配置字典(fishNumber → FishTypeConfig),首次访问时构建。 /// public Dictionary FishDatas { get { if (fishDatas != null) return fishDatas; fishDatas = new Dictionary(M_GameConfig.fishTypeConfigs.Count); foreach (var item in M_GameConfig.fishTypeConfigs) fishDatas[item.fishNumber] = item; return fishDatas; } } /// /// 房间鱼配置(按游戏类型),首次访问时触发懒加载初始化。 /// public Dictionary RoomFishConfig { get { EnsureInit(); return roomFishConfig; } } /// /// 游戏类型 → (fishNumber → RoomFishDetailConfig),首次访问时触发懒加载。 /// public Dictionary> CreateDic { get { EnsureInit(); return createDic; } } #region 生命周期 // ================================================================ // Unity 生命周期 // ================================================================ protected override void Awake() { base.Awake(); yzCreateData = new RoomFishProbabilityConfig { countRange = new List { 2, 3 }, firstFishCountRange = new List { 2, 3 }, roomLevel = 3, fishType = 0, maxCountRange = new List { 10, 20 }, timeRange = new List { 3, 4 }, type = 2 }; // 确保 FishRoot 存在 if (fishRoot == null) fishRoot = new GameObject("FishRoot").transform; if (poolRoot == null) fishRoot = new GameObject("poolRoot").transform; // 监听服务器下发的创鱼事件 EventCenter.Instance.AddEventListener( ConstEventString.SyncGameData_CreateFish, CreateFish); } // ================================================================ // Update:定时调度 + Job 驱动生成 // ================================================================ private void Update() { isCreateFish = BroadCastCreateFish(); } private void OnDestroy() { EventCenter.Instance.RemoveEventListener( ConstEventString.SyncGameData_CreateFish, CreateFish); // 释放 NativeArray,避免内存泄漏 DisposeNativeArrays(); } #endregion #region 初始化 public void InitChuYu() { // 随机初始化计时器,避免所有类型同帧触发 yzCount = Random.Range(yzCreateData.countRange[0], yzCreateData.countRange[1]); ptCount = Random.Range(ptCreateData.countRange[0], ptCreateData.countRange[1]); hjCount = Random.Range(hjCreateData.countRange[0], hjCreateData.countRange[1]); bossCount = Random.Range(bossCreateData.countRange[0], bossCreateData.countRange[1]); tsbossCount = Random.Range(tsbossCreateData.countRange[0], tsbossCreateData.countRange[1]); tsCount = Random.Range(tsCreateData.countRange[0], tsCreateData.countRange[1]); yzTimer = 0.5f; ptTimer = 0.5f; hjTimer = 0.5f; tsTimer = 1f; bossTimer = 5f; tsbossTimer = 5f; } /// /// 懒加载初始化:只执行一次,构建全部配置缓存字典。 /// private void EnsureInit() { if (roomFishConfig != null) return; FishYZ.Init(); roomFishConfig = new Dictionary(); createDic = new Dictionary>(); var list = GameManager.Instance.gameConfig.roomFishConfigs; if (list == null || list.Count == 0) return; // 构建 fishArray / fishType 字典缓存 BuildConfigCache(); foreach (var item in list) { var gameType = (GameTypeEnum)item.gameType; roomFishConfig[gameType] = item; var fishDic = new Dictionary(); createDic[gameType] = fishDic; // 普通鱼列表 if (item.normalFishList != null) foreach (var fish in item.normalFishList) fishDic[fish.fishNumber] = fish; // 等级鱼列表(同 fishNumber 会覆盖普通鱼,实现换皮) if (item.roomFishLevelList != null) foreach (var fish in item.roomFishLevelList) fishDic[fish.fishNumber] = fish; } } /// /// 构建高频查询的字典缓存(fishArrayConfigs / fishTypeConfigs)。 /// private void BuildConfigCache() { fishArrayConfigDic = GameManager.Instance.gameConfig.fishArrayConfigs .ToDictionary(a => a.fishArrayNumber, a => a); fishDatas = GameManager.Instance.gameConfig.fishTypeConfigs .ToDictionary(f => f.fishNumber, f => f); } #endregion #region 进入房间加载 /// 进入房间:初始化生成系统并绑定游戏面板。 public void EnterRoom(GameTypeEnum gameType) { InitSpawnSystem(gameType); BindCurrentGamePanel(); } /// 根据当前 GameType 绑定对应 UI 面板到 currGameType。 private void BindCurrentGamePanel() { switch (GameManager.Instance.gameData.currGameType) { case GameType.FLZY: currGameType = UIManager.Instance.Get(UIPanel.MainGamePanelFLZY); break; case GameType.YYSC: currGameType = UIManager.Instance.Get(UIPanel.MainGamePanelYYSC); break; case GameType.XJMH: currGameType = UIManager.Instance.Get(UIPanel.MainGamePanelXJMH); break; case GameType.HHWK: currGameType = UIManager.Instance.Get(UIPanel.MainGamePanelHHWK); break; default: break; } } /// /// 进入房间时初始化生成系统:加载当前房间配置、分配 NativeArray。 /// public async void InitSpawnSystem(GameTypeEnum gameType) { EnsureInit(); // 初始化分类容器 fishTypeConfig = new Dictionary>(); yz_FishConfig_Line = new Dictionary>(); yz_FishConfig_Line_Dic = new Dictionary(); yz_FishConfig_Line_Dic1 = new Dictionary(); yz_FishConfig_Line_List = new List>(); spawnList = new List(); spawnDic = new Dictionary(); var config = RoomFishConfig[gameType]; // ---- 处理普通鱼列表 ---- if (config.normalFishList != null) { foreach (var item in config.normalFishList) { if (!spawnDic.ContainsKey(item.fishNumber)) { spawnDic[item.fishNumber] = item; spawnList.Add(item); } if (item.type == 1) { // 单体鱼:按鱼类型分组 AddToFishTypeConfig(item); } else if (item.type == 2) { // 鱼阵:按 arrayNumber 分组 TryAddToArrayLine(item); } } } // ---- 处理等级鱼列表(仅当前房间等级)---- int currRoomLevel = GameManager.Instance.gameData.currRoomData.roomLevel; if (config.roomFishLevelList != null) { foreach (var item in config.roomFishProbabilityList) { if (item.roomLevel != currRoomLevel) continue; switch (item.type) { case 1: switch ((FishTypeEnum)item.fishType) { case FishTypeEnum.普通鱼: ptCreateData = item; break; case FishTypeEnum.Boss鱼: bossCreateData = item; break; case FishTypeEnum.黄金鱼: hjCreateData = item; break; case FishTypeEnum.特殊鱼: tsCreateData = item; break; case FishTypeEnum.特殊Boss: tsbossCreateData = item; break; default: break; } break; case 2: //yzTimer=item. yzCreateData = item; break; default: break; } } foreach (var item in config.roomFishLevelList) { if (item.roomLevel != currRoomLevel) continue; if (item.type == 2) { spawnDic[item.fishNumber] = item; spawnList.Add(item); yzQuanZhongCount += item.fishWeight; TryAddToArrayLine(item); } else { AddToFishTypeConfig(item); // 等级鱼也加入 spawnDic / spawnList spawnDic[item.fishNumber] = item; spawnList.Add(item); fishDatas.TryGetValue(item.fishNumber, out var ft); switch ((FishTypeEnum)ft.fishType) { case FishTypeEnum.普通鱼: ptQuanZhongCount += item.fishWeight; break; case FishTypeEnum.Boss鱼: bossQuanZhongCount += item.fishWeight; break; case FishTypeEnum.黄金鱼: hjQuanZhongCount += item.fishWeight; break; case FishTypeEnum.特殊鱼: tsQuanZhongCount += item.fishWeight; break; case FishTypeEnum.特殊Boss: tsbossQuanZhongCount += item.fishWeight; break; default: break; } } } } // ---- 分配 Job System NativeArray ---- int count = spawnList.Count; DisposeNativeArrays(); // 防止重复进房泄漏 timers = new NativeArray(count, Allocator.Persistent); frequencies = new NativeArray(count, Allocator.Persistent); shouldSpawn = new NativeArray(count, Allocator.Persistent); for (int i = 0; i < count; i++) { timers[i] = 0f; frequencies[i] = spawnList[i].fishFrequency; shouldSpawn[i] = 1; // 初始全部允许生成 } // ---- 统计场景内鱼阵路线总数 ---- yz_LineCount = 0; foreach (var key in MonsterMoveLine.dic_BezierCurveDatas.Keys) { if (key.StartsWith("鱼阵")) yz_LineCount++; } InitChuYu(); } /// /// 将单体鱼配置按 FishTypeEnum 加入分类字典。 /// private void AddToFishTypeConfig(RoomFishDetailConfig item) { if (!FishDatas.TryGetValue(item.fishNumber, out var ft)) return; var typeEnum = (FishTypeEnum)ft.fishType; if (!fishTypeConfig.ContainsKey(typeEnum)) fishTypeConfig[typeEnum] = new List(); fishTypeConfig[typeEnum].Add(item); } /// /// 将鱼阵配置按 arrayNumber 加入路线字典(使用缓存替代 List.Find)。 /// private void TryAddToArrayLine(RoomFishDetailConfig item) { if (!fishArrayConfigDic.TryGetValue(item.fishNumber, out var yz)) return; if (!yz_FishConfig_Line.ContainsKey(yz.arrayNumber)) { yz_FishConfig_Line[yz.arrayNumber] = new List(); yz_FishConfig_Line_List.Add(new List()); int newIdx = yz_FishConfig_Line_List.Count - 1; yz_FishConfig_Line_Dic[newIdx] = yz.arrayNumber; yz_FishConfig_Line_Dic1[yz.arrayNumber] = newIdx; } yz_FishConfig_Line[yz.arrayNumber].Add(item); yz_FishConfig_Line_List[yz_FishConfig_Line_Dic1[yz.arrayNumber]].Add(item); } #endregion #region 创建鱼 广播部分 /// /// 广播创建鱼 /// /// 此玩家是否可以创建鱼 private bool BroadCastCreateFish() { if (!isResources) { return false; } // 未初始化 或 未开启放鱼 → 跳过 if (spawnList == null || !fangzhu) return false; float dt = Time.deltaTime; // ---- 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; StartCoroutine(buchongfu(bossList, bossCount)); } /// 每 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; StartCoroutine(buchongfu(tsbossList, tsbossCount)); } 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); 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) { for (int j = 0; j < count; j++) { // 1. 先筛选出未重复的候选集合 var candidates = bossList .Where(b => !fishDatas_hashCodekey_Boss.Values .Any(existing => existing.fishTypeID.ToString() == b.fishNumber)) .ToList(); if (candidates.Count == 0) { yield break; // 没有可用候选,直接退出 } // 2. 在候选集合里做加权随机 var chosen = GetRandomFishByWeight(candidates); // 3. 标记激活 chosen.isCreate = 2; } } /// 每 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) 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) { 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 pathList = FishDatas[fish.fishNumber].pathList; if (pathList != null && pathList.Count > 0) 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); } #endregion #region 创建鱼 /// /// AI优化过的 /// /// public void CreateFish(SyncGameData syncGameData) { var rotateData = FrameDataParser.Parse(syncGameData.frameData); if (rotateData != null) SpawnFishMainThread(rotateData, syncGameData.time); } /// /// 主线程实例化鱼对象(单体鱼 / 鱼阵分发)。 /// private void SpawnFishMainThread(FrameData_CreateFish createFishData, long time) { if (!spawnDic.TryGetValue(createFishData.fishNumber, out var cfg)) { Debug.LogWarning($"[CreateFishManager] 找不到 spawnDic 配置: {createFishData.fishNumber}"); return; } if (cfg.type == 1) { // 单体鱼 OnlyFish(createFishData, time); } else if (cfg.type == 2) { // 鱼阵 SpawnFishArray(createFishData, cfg.fishNumber, time); } } // ================================================================ // 单体鱼生成 // ================================================================ /// /// 生成一条普通/Boss/黄金鱼并初始化路径。 /// /// 成功返回 true,配置缺失返回 false。 private bool OnlyFish(FrameData_CreateFish createFishData, long time) { if (!fishDatas.TryGetValue(createFishData.fishNumber, out var fishType)) { Debug.LogError($"[CreateFishManager] 找不到 fishTypeConfig: {createFishData.fishNumber}"); return false; } //var prefab = FishResourcesData.GetFishPrefab(int.Parse(fishType.fishNumber)); //var fish = Instantiate(prefab, new Vector3(9999, 9999, 0), prefab.transform.rotation, fishRoot); try { var fish = Pool.Instance.Get(int.Parse(createFishData.fishNumber), new Vector3(9999, 9999, 0), Quaternion.identity, fishRoot); var mc = fish.GetComponent() ?? fish.AddComponent(); mc.monsterData.fishTypeConfig = FishDatas[createFishData.fishNumber]; mc.guid = createFishData.guid; mc.moveTime = (BeijingTimeManager.Instance.CurrentTimestampMs - time) / 1000f; mc.fishenum = (FishTypeEnum)fishType.fishType; // 按鱼类型放入对应字典 RegisterFishInstance(mc, fishType.fishType); mc.Init(createFishData.moveLineId, spawnDic[createFishData.fishNumber].times); Debug.Log($"[CreateFishManager] 生成鱼:{fishType.fishName}"); } catch (Exception ex) { Debug.LogError(createFishData.fishNumber); Debug.LogError(ex.Message); } return true; } /// 将鱼实例注册到全局字典及对应分类字典。 private void RegisterFishInstance(MonsterContral mc, int fishTypeId) { fishDatas_hashCodekey[mc.guid] = mc; switch (fishTypeId) { case 1: // 普通鱼 fishDatas_hashCodekey_Small[mc.guid] = mc; break; case 2: // Boss fishDatas_hashCodekey_Boss[mc.guid] = mc; break; case 3: // 特殊鱼 / 黄金鱼 fishDatas_hashCodekey_HJ[mc.guid] = mc; break; case 4: fishDatas_hashCodekey_TS[mc.guid] = mc; break; case 5: fishDatas_hashCodekey_TS_Boss[mc.guid] = mc; break; } } #region 鱼阵 // ================================================================ // 鱼阵生成(按 arrayNumber 前缀分发到对应阵型协程/方法) // ================================================================ /// 根据鱼阵配置分发到对应的生成逻辑。 private void SpawnFishArray(FrameData_CreateFish createFishData, string fishNumber, long time) { if (!fishArrayConfigDic.TryGetValue(fishNumber, out var yz)) { Debug.LogError($"[CreateFishManager] 找不到 fishArrayConfig: {fishNumber}"); return; } if (!fishDatas.TryGetValue(yz.fishNumber, out var fishType)) { Debug.LogError($"[CreateFishManager] 鱼阵 fishType 缺失: {yz.fishNumber}"); return; } if (!FishYZ.dicfishYZ.TryGetValue(yz.arrayNumber, out var pathList) || pathList.Count == 0) { Debug.LogWarning($"[CreateFishManager] 鱼阵路线为空: {yz.arrayNumber}"); return; } var prefab = FishResourcesData.GetFishPrefab(int.Parse(fishType.fishNumber)); var pos = new Vector3(9999, 9999, 0); string prefix = yz.arrayNumber.Length >= 3 ? yz.arrayNumber.Substring(0, 3) : yz.arrayNumber; switch (prefix) { case "700": // 扇形阵:逐条延迟生成 StartCoroutine(IeShanXing(createFishData, yz, pos, prefab, pathList, time, yz.fishCount)); break; case "701": // 环形阵:同帧同路径全部生成 foreach (int pathId in pathList) Quan(createFishData, yz, pos, prefab, pathId, yz.fishCount, time); break; case "702": case "7007": case "710": // 顺序阵:每条路径逐一延迟生成 case "704": //int chosen710 = (prefix == "710") // ? pathList[Random.Range(0, pathList.Count)] // : -1; StartCoroutine(YZ7007(createFishData, yz, pos, prefab, time)); //foreach (int pathId in pathList) //{ // int actualPath = (prefix == "710") ? chosen710 : pathId; // StartCoroutine(YZ7007(createFishData, yz, pos, prefab, actualPath, time, yz.fishCount)); //} break; default: Debug.LogWarning($"[CreateFishManager] 未知鱼阵前缀: {prefix}"); break; } } // ================================================================ // 鱼阵生成协程 / 方法 // ================================================================ /// 扇形阵:按路线列表逐条延迟 0.8s 生成一扇。 private IEnumerator IeShanXing(FrameData_CreateFish createFishData, FishArrayConfig yz, Vector3 pos, GameObject prefab, List pathList, long time, int count) { foreach (int pathId in pathList) { if (!isResources) { yield break; } ShanXing(createFishData, yz, pos, prefab, pathId, count, time); yield return new WaitForSeconds(0.8f); } } /// /// 扇形阵单扇:生成 16 条鱼,每条旋转角度递增(±5°交替),模拟扇形展开。 /// private void ShanXing(FrameData_CreateFish createFishData, FishArrayConfig yz, Vector3 pos, GameObject prefab, int pathId, int count, long time) { var cfg = spawnDic[createFishData.fishNumber]; for (int i = 1; i <= 16; i++) { // 交替正负角度,形成扇形分布 float angle = i * 5f * (i % 2 == 0 ? -1f : 1f); var rotation = Quaternion.Euler( prefab.transform.eulerAngles + Vector3.forward * angle); //var fish = Instantiate(prefab, pos, rotation, fishRoot); var fish = Pool.Instance.Get(int.Parse(yz.fishNumber), pos, rotation, fishRoot); var mc = fish.GetComponent(); mc.monsterData.fishTypeConfig = FishDatas[yz.fishNumber]; mc.guid = createFishData.guid + i; RegisterFishInstance(mc, FishDatas[yz.fishNumber].fishType); mc.moveTime = (BeijingTimeManager.Instance.CurrentTimestampMs - time) / 1000f; mc.Init(pathId.ToString(), cfg.times, true, yz.arrayNumber); } } /// /// 环形阵:在同一路径上生成 count 条鱼,角度均匀分布(360/count)。 /// private void Quan(FrameData_CreateFish createFishData, FishArrayConfig yz, Vector3 pos, GameObject prefab, int pathId, int count, long time) { if (count <= 0) return; var cfg = spawnDic[createFishData.fishNumber]; for (int i = 0; i < count; i++) { float angle = (360f / count) * i; var rotation = Quaternion.Euler(prefab.transform.eulerAngles + Vector3.forward * angle); //var fish = Instantiate(prefab, pos, rotation, fishRoot); var fish = Pool.Instance.Get(int.Parse(yz.fishNumber), pos, rotation, fishRoot); var mc = fish.GetComponent(); mc.monsterData.fishTypeConfig = FishDatas[yz.fishNumber]; mc.guid = createFishData.guid + i; RegisterFishInstance(mc, FishDatas[yz.fishNumber].fishType); mc.moveTime = (BeijingTimeManager.Instance.CurrentTimestampMs - time) / 1000f; mc.Init(pathId.ToString(), cfg.times, true, yz.arrayNumber); } } /// /// 顺序阵:每隔 1s 生成一条鱼,共生成 count 条。 /// private IEnumerator YZ7007(FrameData_CreateFish createFishData, FishArrayConfig yz, Vector3 pos, GameObject prefab, long time) { var cfg = spawnDic[createFishData.fishNumber]; for (int i = 0; i < yz.fishCount; i++) { if (!isResources) { yield break; } //var fish = Instantiate(prefab, pos, prefab.transform.rotation, fishRoot); var fish = Pool.Instance.Get(int.Parse(yz.fishNumber), pos, Quaternion.identity, fishRoot); var mc = fish.GetComponent() ?? fish.AddComponent(); mc.monsterData.fishTypeConfig = FishDatas[yz.fishNumber]; mc.guid = createFishData.guid + i; RegisterFishInstance(mc, FishDatas[yz.fishNumber].fishType); // 每条鱼额外减去已等待秒数,使路径起点与帧数同步 mc.moveTime = ((BeijingTimeManager.Instance.CurrentTimestampMs - time) / 1000f) - i; mc.Init(createFishData.moveLineId, cfg.times, true, yz.arrayNumber); yield return new WaitForSeconds(1f); } } #endregion #endregion #region 结束游戏退出房间 /// 退出房间:关闭生成系统。 public void ExitRoom() { CloseSpawnSystem(); } /// /// 关闭生成系统:清理列表,销毁场景内所有鱼对象。 /// public async void CloseSpawnSystem() { //await Task.Delay(2000); spawnList = null; spawnDic = null; yz_FishConfig_Line_Dic = null; yz_FishConfig_Line = null; yz_FishConfig_Line_Dic1 = null; if (fishRoot == null) return; foreach (var item in fishDatas_hashCodekey) { if (item.Value.gameObject != null) { Destroy(item.Value.gameObject); } } fishDatas_hashCodekey.Clear(); fishDatas_hashCodekey_Boss.Clear(); fishDatas_hashCodekey_Small.Clear(); foreach (Transform item in fishRoot) { Destroy(item.gameObject); } //for (int i = fishDatas_hashCodekey.childCount - 1; i >= 0; i--) // Destroy(fishRoot.GetChild(i).gameObject); } /// 释放 NativeArray(避免 Unity 内存泄漏警告)。 private void DisposeNativeArrays() { if (timers.IsCreated) timers.Dispose(); if (frequencies.IsCreated) frequencies.Dispose(); if (shouldSpawn.IsCreated) shouldSpawn.Dispose(); } #endregion #region Tool // ================================================================ // 随机工具方法 // ================================================================ /// /// 从 [min, max) 区间随机取 count 个不重复整数。 /// 若 min==max 返回空列表。 /// public static List RandomUniqueNumbers(int count, int min, int max) { if (min >= max) { Debug.LogWarning("[CreateFishManager] RandomUniqueNumbers: min >= max,返回空列表"); return new List(); } count = Mathf.Min(count, max - min); // 不能超过范围 var result = new HashSet(); var rand = new System.Random(); while (result.Count < count) result.Add(rand.Next(min, max)); return result.ToList(); } /// /// 从 KeyCollection 中随机取 count 个不重复的 string key。 /// public static List RandomUniqueNumbers(int count, int min, int max, Dictionary>.KeyCollection keyCollection) { if (min >= max) return new List(); count = Mathf.Min(count, max - min); var keyList = keyCollection.ToList(); var indices = new HashSet(); var rand = new System.Random(); var result = new List(count); while (indices.Count < count) { int idx = rand.Next(min, max); if (indices.Add(idx)) result.Add(keyList[idx]); } return result; } #endregion #region 杀鱼 // ================================================================ // 杀鱼接口 // ================================================================ /// /// 批量杀死多条鱼(带特效 + 不足数量用纯金币补齐)。 /// /// 实际场上能找到的鱼 guid 列表(可能少于 count) /// 红包拆分后的每份金额,长度应 >= count /// 金币最终飞回的世界坐标(一般是炮台位置) /// 理论上要“死”的总数量(鱼 + 纯金币) public void Id_DieListFish_FX(List guids, List coinNum, Vector3 v3Point, int count) { if (coinNum == null || coinNum.Count == 0) return; // 1. 先处理“真的有鱼”的部分 int realCount = Mathf.Min(guids.Count, coinNum.Count, count); for (int i = 0; i < realCount; i++) { // 用你原来的单条逻辑,不动 Id_DieFish_FX(guids[i], coinNum[i], v3Point); } // 2. 不足的部分,用“屏幕随机位置的金币特效”补齐 int needExtra = count - realCount; if (needExtra <= 0) return; // 防御:最多用 coinNum 里剩下的 int maxExtra = Mathf.Min(needExtra, coinNum.Count - realCount); for (int i = 0; i < maxExtra; i++) { var red = coinNum[realCount + i]; SpawnRandomCoinFX(red, v3Point); } } /// /// 随机选取 count 条小鱼/黄金鱼,通知服务器同步死亡,并返回 guid 列表。 /// public List SyncGameData_SendRendromDieFishID( int count, List coinNum, Vector3 v3Point) { var guids = new List(count); var tempKeys = fishDatas_hashCodekey_Small.Keys.ToList(); // 若存活鱼不足 count,全部选取 if (tempKeys.Count <= count) { guids.AddRange(tempKeys); } else { for (int i = 0; i < count; i++) { int idx = Random.Range(0, tempKeys.Count); guids.Add(tempKeys[idx]); tempKeys.RemoveAt(idx); } } int posID = currGameType.currSitData.posID; var sgd = new SyncGameData { sitPos = posID, frameType = FrameDataType.randomDieFish, time = BeijingTimeManager.Instance.CurrentTimestampMs, frameData = new FrameData_RandomDieFish { x = v3Point.x, y = v3Point.y, z = v3Point.z, guids = guids, coinNum = coinNum, count = count } }; WebSocketSendList.SyncGameData(sgd); return guids; } /// /// 鱼离场 /// public void Id_DieFish_NotFX(string fishId) { if (!fishDatas_hashCodekey.TryGetValue(fishId, out var monster)) return; Pool.Instance.Return(monster.gameObject); switch (monster.fishenum) { case FishTypeEnum.普通鱼: fishDatas_hashCodekey_Small.Remove(fishId); break; case FishTypeEnum.Boss鱼: fishDatas_hashCodekey_Boss.Remove(fishId); break; case FishTypeEnum.黄金鱼: fishDatas_hashCodekey_HJ.Remove(fishId); break; case FishTypeEnum.特殊鱼: fishDatas_hashCodekey_TS.Remove(fishId); break; case FishTypeEnum.特殊Boss: fishDatas_hashCodekey_TS_Boss.Remove(fishId); break; default: break; } fishDatas_hashCodekey.Remove(fishId); } /// /// 按 ID 杀死一条鱼(带特效 + 金币飞出动画)。 /// public void Id_DieFish_FX(string fishId, RedPacketRecord coinNum, Vector3 v3Point) { if (!fishDatas_hashCodekey.TryGetValue(fishId, out var monster)) return; AudioManager.Instance.PlayerFX(60004); // 死亡粒子特效 Instantiate( monster.effect_Die != null ? monster.effect_Die : dieFXPrefab, monster.transform.position, Quaternion.identity, transform.parent); // 非 Boss 鱼:金币飞向炮台 if (monster.fishenum != FishTypeEnum.Boss鱼) { var award = Instantiate( monster.effect_DieAward != null ? monster.effect_DieAward : dieFXPrefabAward, currGameType.subPanelParent.parent); award.GetComponent().anchoredPosition = CoordinateTransformation.WorldPos_ToCanvas( monster.transform.position, UIManager.Instance.uiRoot.GetComponent()); var coinScript = award.GetComponent(); coinScript.Init(coinNum.Amount, v3Point); coinScript.endPoint = v3Point; coinScript.isBoss = true; coinScript.SetCoinCount(); } //MonsterDieList.Instance.SmallHuangYiHuang( // monster.transform.position, // monster.monsterData.fishTypeConfig.fishNumber, 1000); monster.DieFish(); fishDatas_hashCodekey.Remove(fishId); } /// /// 不依赖鱼,只在屏幕内随机一个位置生成金币特效,然后飞回指定点。 /// private void SpawnRandomCoinFX(RedPacketRecord coinNum, Vector3 targetWorldPos) { // 1. 随机一个屏幕坐标 Vector2 screenPos = new Vector2( UnityEngine.Random.Range(0f, Screen.width), UnityEngine.Random.Range(0f, Screen.height)); // 2. 屏幕坐标 → Canvas 坐标(跟你原来金币飞行逻辑保持一致) Vector2 canvasPos = CoordinateTransformation.ScreenPoint_ToCanvas( screenPos, UIManager.Instance.uiRoot.GetComponent()); // 3. 实例化金币特效(沿用你原来的 award 逻辑) var award = Instantiate( dieFXPrefabAward, // 或者用某个专门的“掉金币” prefab currGameType.subPanelParent.parent); var rect = award.GetComponent(); rect.anchoredPosition = canvasPos; // 4. 设置 CoinScript 的终点为 targetWorldPos var coinScript = award.GetComponent(); coinScript.Init(coinNum.Amount, targetWorldPos); coinScript.isBoss = true; coinScript.SetCoinCount(); } /// /// 收到服务器 WebSocket 杀鱼通知,处理金币加分、特效、音效。 /// public void Ws_DieFish(string msg) { var killFish = JsonConvert.DeserializeObject>(msg) .arguments[0]; Debug.Log($"[CreateFishManager] 杀鱼延迟:{DateTimeOffset.Now.ToUnixTimeSeconds() - killFish.timespan}s"); SitPosData sitData = null; //击杀玩家座位数据 foreach (var item in currGameType.list_Sits) { if (item.user == killFish.userId) { item.monCotl_Lock = null; //item.lockFX.gameObject.SetActive(false); sitData = item; } } if (!fishDatas_hashCodekey.TryGetValue(killFish.fishId, out var monster)) return; monster.DieFish(killFish); // 死亡粒子特效 Instantiate( monster.effect_Die != null ? monster.effect_Die : dieFXPrefab, monster.transform.position, Quaternion.identity, transform.parent); // 加分回调(在金币/加分面板飞完后执行) Action onScoreAdd = audioID => { var sitList = GetCurrentPanelSitList(); sitData.goldNum += killFish.gold; sitData.tMP_GoldNum.text = sitData.goldNum.ToString(); AudioManager.Instance.PlayerFX(audioID); }; switch (monster.fishenum) { case FishTypeEnum.普通鱼: { // 普通鱼:金币飞向炮台 var award = Instantiate( monster.effect_DieAward != null ? monster.effect_DieAward : dieFXPrefabAward, currGameType.subPanelParent.parent); try { award.GetComponent().anchoredPosition = CoordinateTransformation.WorldPos_ToCanvas( monster.transform.position, UIManager.Instance.uiRoot.GetComponent()); } catch { award.transform.position = monster.transform.position; } var coinScript = award.GetComponent(); coinScript.Init(killFish.gold, sitData.transform.position, killFish.userId, sitData.posID, killFish.param); coinScript.isBoss = false; coinScript.SetCoinCount(); coinScript.JiaFen = onScoreAdd; break; } case FishTypeEnum.Boss鱼: { // Boss 鱼:显示浮动分数文本 var bossCtrl = (MonsterControl_BossBase)monster; bossCtrl.v3 = sitData.transform.position; bossCtrl.currEffect_DieAward = Instantiate( monster.effect_DieAward != null ? monster.effect_DieAward : dieFXPrefabAwardTmp, currGameType.subPanelParent.parent); bossCtrl.action_effect_DieAward?.Invoke(bossCtrl.currEffect_DieAward); bossCtrl.JiaFen = onScoreAdd; } break; case FishTypeEnum.黄金鱼: { // 普通鱼:金币飞向炮台 var award = Instantiate( monster.effect_DieAward != null ? monster.effect_DieAward : dieFXPrefabAward, currGameType.subPanelParent.parent); try { award.GetComponent().anchoredPosition = CoordinateTransformation.WorldPos_ToCanvas( monster.transform.position, UIManager.Instance.uiRoot.GetComponent()); } catch { award.transform.position = monster.transform.position; } var coinScript = award.GetComponent(); coinScript.Init(killFish.gold, sitData.transform.position, killFish.userId, sitData.posID, killFish.param); coinScript.isBoss = false; coinScript.SetCoinCount(); coinScript.JiaFen = onScoreAdd; } break; case FishTypeEnum.特殊鱼: { // 普通鱼:金币飞向炮台 var award = Instantiate( monster.effect_DieAward != null ? monster.effect_DieAward : dieFXPrefabAward, currGameType.subPanelParent.parent); try { award.GetComponent().anchoredPosition = CoordinateTransformation.WorldPos_ToCanvas( monster.transform.position, UIManager.Instance.uiRoot.GetComponent()); } catch { award.transform.position = monster.transform.position; } var coinScript = award.GetComponent(); coinScript.Init(killFish.gold, sitData.transform.position, killFish.userId, sitData.posID, killFish.param); coinScript.isBoss = false; coinScript.SetCoinCount(); coinScript.JiaFen = onScoreAdd; } break; case FishTypeEnum.特殊Boss: { // Boss 鱼:显示浮动分数文本 var bossCtrl = (MonsterControl_BossBase)monster; bossCtrl.v3 = sitData.transform.position; bossCtrl.currEffect_DieAward = Instantiate( monster.effect_DieAward != null ? monster.effect_DieAward : dieFXPrefabAwardTmp, currGameType.subPanelParent.parent); bossCtrl.action_effect_DieAward?.Invoke(bossCtrl.currEffect_DieAward); bossCtrl.JiaFen = onScoreAdd; } break; default: break; } MonsterDieList.Instance.SmallHuangYiHuang( monster.transform.position, monster.monsterData.fishTypeConfig.fishNumber, 1000); } #endregion // ================================================================ // 工具:获取当前面板座位列表 // ================================================================ /// /// 根据当前游戏类型返回对应 UI 面板的座位列表。 /// private List GetCurrentPanelSitList() { switch (GameManager.Instance.gameData.currGameType) { case GameType.defaultPanel: return UIManager.Instance.Get(UIPanel.MainGamePanelDefault).list_Sits; case GameType.FLZY: return UIManager.Instance.Get(UIPanel.MainGamePanelFLZY).list_Sits; case GameType.YYSC: return UIManager.Instance.Get(UIPanel.MainGamePanelYYSC).list_Sits; case GameType.XJMH: return UIManager.Instance.Get(UIPanel.MainGamePanelXJMH).list_Sits; case GameType.HHWK: return UIManager.Instance.Get(UIPanel.MainGamePanelHHWK).list_Sits; default: return null; } } }