// ============================================================
// 文件: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 Newtonsoft.Json.Linq;
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 UnityEngine.Serialization;
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;
///
/// 非 UI 爆奖特效统一父节点(挂在场景根节点,不挂 UI 节点下)。
/// 仅装载那些"原本挂在 UIManager.uiRoot 下(不会随房间退出自动销毁)"
/// 或"原本无父对象(世界空间特效)"的爆奖特效:
/// - BJEndUI 系列(BJEventBase/GSL/FLXJ 的 EndBJ)
/// - DieEffect_TSY / EffectZuanTouDie 的 baoJiangEffectPrefab
/// - HHWK_SDY_DieEvent 的 explosionPrefab
/// - HHWK_ZDX_DieEvent 的 bombPrefab / explosionPrefab
/// - Boss鱼/特殊Boss isToUI=false 分支的 currEffect_DieAward
/// UI 爆奖(挂在 subPanelParent/subPanelParent.parent 下的金币飞、分数版等)
/// 不挂这里,由 UIManager 卸载游戏主面板时自动清理。
/// ⭐ 本节点不能删除:ExitRoom 只清空子物体;
/// 意外被删除/为 null 时取值会自动在场景根节点重建(不建到 UI 节点)。
///
[FormerlySerializedAs("bjEffectRoot")]
[SerializeField] private Transform _bjEffectRoot;
/// 爆奖特效统一父节点:为 null(意外删除)时自动在场景根节点重建
public Transform bjEffectRoot
{
get
{
if (_bjEffectRoot == null) EnsureBJEffectRoot();
return _bjEffectRoot;
}
}
/// 当前房间可生成鱼列表(与 NativeArray 下标对齐)
public List spawnList;
/// 当前房间 fishNumber → RoomFishDetailConfig
public Dictionary spawnDic;
/// 击杀广播:某条鱼被击杀时触发(monster=被击杀的鱼,sit=击杀者座位)。
/// XJMH 免费模式等"按鱼配置"的特性用它,监听方自行过滤需要的鱼号,不改动既有击杀流程。
public static event System.Action OnFishKilled;
public bool isCreateFish; //是否可以创建鱼
public bool isFrozen = false; //是否冰冻中(冻结出鱼+所有鱼定格)
[Header("调试:手动出鱼")]
[Tooltip("按 M 键生成的鱼编号(配置 fishNumber)")]
public string debugSpawnFishNumber = "11043";
public bool isleft = true;
[Tooltip("按M键批量模式:激活=本地在所有普通鱼路线上各生成一条(不走服务端);不激活=走服务端出一条")]
public bool debugSpawnAllLines = false;
private Dictionary frozenFishStates = new Dictionary();
private Dictionary frozenFishIsMoves = new Dictionary();
/// 按鱼类型分类的生成配置(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();
// ----------------------------------------------------------------
// 击杀消息幂等去重(fishId|killtime → 处理时刻)
// 服务端可能对同一次击杀下发多条 RoomKillNotice(掉落推送之外的重复/重发),
// 同 fishId+killtime 只处理第一条,防止重复克隆爆奖特效/重复出 bjEndUI;
// MultiDeath(星际巡航等多次死亡玩法)每段 killtime 不同,不受影响。
// ----------------------------------------------------------------
private readonly Dictionary _handledKillKeys = new Dictionary();
private readonly List _expiredKillKeys = new List();
private const float KillDedupKeepSeconds = 60f; // 键保鲜期(秒),超时清理防字典增长
#if UNITY_EDITOR
// ⭐ 编辑器调试 List:与上方字典同步,方便在 Inspector 查看当前场上活鱼
public List fishList_All = new List();
public List fishList_Small = new List();
public List fishList_Boss = new List();
public List fishList_HJ = new List();
public List fishList_TS = new List();
public List fishList_TS_Boss = new List();
#endif
// ------ 各类计时器 ------
private RoomFishProbabilityConfig tsbossCreateData; // 特殊Boss 创建数据
private float tsbossTimer = 999f; // 特殊Boss 刷新倒计时(秒)
private int tsbossCount; // 本轮同时启用特殊Boss鱼数量
private int tsbossQuanZhongCount; // 本轮同时启用特殊Boss鱼数量
private List tsbossRecentList = new List(); // 最近出现过的特殊Boss(cool down)
private int tsbossRecentMax = 3; // N次内不重复出现
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 List bossRecentList = new List(); // 最近出现过的Boss(cool down)
private int bossRecentMax = 3; // N次内不重复出现
/// Boss鱼路线奇偶切换标记:true=下次选奇数路线,false=下次选偶数路线
private bool bossNextPathOdd = true;
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; // 本轮同时启用黄金鱼数量
// fangzhu 字段已删除:鱼创建改由服务端 OnFishSpawn 统一下发,客户端无需房主出鱼
public bool isResources = false;
[Header("鱼阵路线撞车")]
[Tooltip("同一条路线被鱼阵占用后,其他鱼阵再用到同一条线需要等待的秒数(避免同一出生点同帧重叠)")]
public float lineConflictCooldown = 1f;
/// 每条鱼阵路线最近一次被鱼阵占用的时刻(Time.time),用于同线撞车延迟
private readonly Dictionary _lineBusyTime = new Dictionary();
// ================================================================
// 懒加载属性
// ================================================================
///
/// 鱼基础配置字典(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;
}
}
///
/// 鱼阵配置字典(fishArrayNumber → FishArrayConfig),首次访问时构建。
///
public Dictionary FishArrayDatas
{
get
{
if (fishArrayConfigDic != null) return fishArrayConfigDic;
fishArrayConfigDic = new Dictionary(M_GameConfig.fishArrayConfigs.Count);
foreach (var item in M_GameConfig.fishArrayConfigs)
fishArrayConfigDic[item.fishArrayNumber] = item;
return fishArrayConfigDic;
}
}
///
/// 房间鱼配置(按游戏类型),首次访问时触发懒加载初始化。
///
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)
poolRoot = new GameObject("poolRoot").transform;
// ⭐ 鱼创建改由服务端 OnFishSpawn 事件统一下发,客户端不再监听 createFish 帧同步
EventCenter.Instance.AddEventListener(
ConstEventString.OnFishSpawn, OnFishSpawn);
}
// ================================================================
// Update:冰冻时不做任何事(鱼创建由服务端 OnFishSpawn 驱动)
// ================================================================
private void Update()
{
// 鱼创建由服务端 OnFishSpawn 事件驱动,客户端 Update 不再出鱼
// (保留 isFrozen 字段供 FreezeAllFish/UnfreezeAllFish 使用)
// ⭐ 调试用:按 M 键发送 HTTP 请求到服务端 AdminApi 出鱼
// roomId 从 gameData.currRoomData 读取,fishNumber 从 Inspector 配置读取
if (UnityEngine.InputSystem.Keyboard.current != null &&
UnityEngine.InputSystem.Keyboard.current.mKey.wasPressedThisFrame)
{
if (string.IsNullOrEmpty(debugSpawnFishNumber))
{
Debug.LogWarning("[CreateFishManager] 未配置 debugSpawnFishNumber,请在 Inspector 设置鱼编号");
return;
}
string roomId = GameManager.Instance.gameData?.currRoomData?.roomId;
if (string.IsNullOrEmpty(roomId))
{
Debug.LogWarning("[CreateFishManager] 当前未在房间中(roomId 为空),无法出鱼");
return;
}
// ⭐ debugSpawnAllLines 激活:本地在所有普通鱼路线("曲线"+id)上各生成一条(不走服务端);
// 不激活:走服务端 AdminApi 出一条(现状)
if (debugSpawnAllLines)
{
int spawned = 0;
foreach (var key in MonsterMoveLine.dic_BezierCurveDatas.Keys)
{
if (key == null || !key.StartsWith("曲线")) continue;
if (!int.TryParse(key.Substring(2), out int rid)) continue;
if (ManualSpawn(debugSpawnFishNumber, rid, 30)) spawned++;
}
Debug.Log($"[CreateFishManager] 调试批量出鱼(所有路线): fishNumber={debugSpawnFishNumber}, 共生成{spawned}条");
}
else
{
APIManager.TempSpawnFish(roomId, debugSpawnFishNumber,1,30, isleft?1:2);
}
}
}
private void OnDestroy()
{
EventCenter.Instance.RemoveEventListener(
ConstEventString.OnFishSpawn, OnFishSpawn);
// 释放 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)
{
// ⭐ 复位残留冰冻状态:冰冻中切房时旧面板倒计时未走完就销毁,
// 不会触发 UnfreezeAllFish → isFrozen 残留 true → 新房间 OnFishSpawn 全部被拦,不出鱼
ResetFreezeState();
InitSpawnSystem(gameType);
BindCurrentGamePanel();
EnsureBJEffectRoot();
_lineBusyTime.Clear(); // 清掉上个房间的路线占用记录
_handledFishIds.Clear(); // 清掉上个房间的出鱼去重记录(flush 回放时会重新登记)
// ⭐ 必须先把 isResources 置 true 再回放:
// 调用方( GameMainPanel_FishBase )是在 EnterRoom 之后才置 true 的,
// 而鱼阵协程(IeShanXing/IeQuan/YZ7007)里都有 `if (!isResources) yield break;`,
// 回放时它还是 false(退房残留)→ 服务端进房瞬间推的初始鱼阵(init/sync)
// 全部 ARRAY_ENTER 后一条鱼都不生成,进房看不到鱼阵
isResources = true;
// ⭐ 回放进房加载期间收到的出鱼事件(服务端进房瞬间推的 init/sync 初始鱼)
FlushPendingFishSpawns();
}
///
/// 创建爆奖特效统一父节点(挂在场景根节点,不挂 UI 节点下)。
/// 已存在则不重复创建。所有爆奖相关 Instantiate 都应使用此节点作为父对象。
///
private void EnsureBJEffectRoot()
{
if (_bjEffectRoot != null) return;
// ⭐ 挂场景根节点(不挂 UIManager.uiRoot):世界空间爆奖特效直接用世界坐标摆放
var go = new GameObject("bjEffectRoot");
_bjEffectRoot = go.transform;
}
/// 根据当前 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 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 过期自动移除协程
///
public void OnFishSpawn(OnFishSpawnData data)
{
if (data == null || data.fish == null || data.fish.Count == 0) return;
if (isFrozen) return; // 冰冻中不创建鱼
// ⭐ 修复:生成系统未就绪(进房加载中/退房清理后 spawnDic==null)时不再丢弃事件,
// 先入队,EnterRoom 完成后回放。服务端在进房瞬间就会推 OnFishSpawn(init/sync)初始鱼,
// 而此时客户端还在 LoadResources 加载(数秒),直接 return 会把初始鱼全部丢掉 → 进房不出鱼。
if (spawnDic == null)
{
if (_pendingFishSpawns == null) _pendingFishSpawns = new List();
_pendingFishSpawns.Add(data);
Debug.Log($"[CreateFishManager] 生成系统未就绪,出鱼事件入队等待回放: type={data.type}, count={data.fish.Count}, queued={_pendingFishSpawns.Count}");
return;
}
// ⭐ 分帧出鱼:普通鱼/黄金鱼分批生成(每帧5条),其他类型立即全出
// 避免一次出大量普通鱼导致单帧卡顿
var batchedQueue = new List(data.fish.Count);
bool isSync = data.type == "sync";
foreach (var item in data.fish)
{
if (item == null) continue;
// 判断是否需要分帧:普通鱼(1)和黄金鱼(3)且非鱼阵 → 分帧
bool shouldBatch = ShouldBatchSpawn(item);
if (shouldBatch)
{
batchedQueue.Add(item);
}
else
{
// Boss/特殊鱼/鱼阵等:立即生成
TrySpawnSingleFish(item, isSync);
}
}
// 分帧组:启动协程逐帧生成
if (batchedQueue.Count > 0)
{
StartCoroutine(SpawnFishBatchedCoroutine(batchedQueue, isSync));
}
NetLog.Log($"[CreateFishManager] OnFishSpawn type={data.type}, count={data.fish.Count}, batched={batchedQueue.Count}");
}
/// 生成系统未就绪期间收到的出鱼事件队列(EnterRoom 完成后回放)
private List _pendingFishSpawns;
///
/// 已受理的服务端 fishId → 受理时刻(Time.time),用于事件级去重。
/// ⭐ 服务端会把同一批鱼推 2~3 遍(RoomInfo.activeFishes + 约300ms 后补发 OnFishSpawn(init)),
/// 且重复推送带的是同一个 fishId。鱼阵是协程延迟创建的(WaitLineFree/每条线间隔 1s),
/// 等实例注册到字典再去重来不及 → 普通鱼/Boss/鱼阵都可能被创建两遍、叠在一起。
/// 所以在"事件受理"这一刻就登记 fishId。
/// ⭐ 只拦窗口期内的重复推送:窗口外交给字典去重(第二道),
/// 鱼已死/已离场时允许按 sync 重新创建,避免"该出的鱼被永久挡住"。
/// 房间切换时清空。
///
private readonly Dictionary _handledFishIds = new Dictionary();
/// 事件级去重的保护窗口(秒):需覆盖鱼阵协程把所有鱼建完的时间(5条线约10秒)
private const float FishIdDedupWindow = 15f;
///
/// 回放进房加载期间收到的出鱼事件(服务端在进房瞬间推的 init/sync 初始鱼)。
/// 在 EnterRoom 末尾调用:此时配置/资源/面板都已就绪。
///
private void FlushPendingFishSpawns()
{
if (_pendingFishSpawns == null || _pendingFishSpawns.Count == 0) return;
var queue = _pendingFishSpawns;
_pendingFishSpawns = null; // 先置空,回放期间新到事件走正常路径
Debug.Log($"[CreateFishManager] 回放加载期间出鱼事件: {queue.Count} 批");
foreach (var data in queue)
{
try { OnFishSpawn(data); }
catch (System.Exception e) { Debug.LogWarning($"[CreateFishManager] 回放出鱼事件失败: {e.Message}"); }
}
}
///
/// 判断该鱼是否需要分帧生成。
/// 普通鱼(1)和黄金鱼(3)且非鱼阵 → 分帧;其他(Boss/特殊鱼/特殊Boss/鱼阵) → 立即出。
///
private bool ShouldBatchSpawn(OnFishSpawnFishItem item)
{
// 鱼阵一次性全出(fishArrayNumber 非空表示鱼阵)
if (!string.IsNullOrEmpty(item.fishArrayNumber)) return false;
// 通过 fishNumber 查 FishTypeConfig 判断鱼类型
if (string.IsNullOrEmpty(item.fishNumber)) return false;
if (FishDatas != null && FishDatas.TryGetValue(item.fishNumber, out var cfg))
{
int ft = cfg.fishType;
// 普通鱼=1, 黄金鱼=3 → 分帧
return ft == (int)FishTypeEnum.普通鱼 || ft == (int)FishTypeEnum.黄金鱼;
}
// 查不到配置,默认分帧(保守处理,避免未知类型一次大量生成)
return true;
}
///
/// 分帧出鱼协程:每帧生成 batchSize 条,避免单帧卡顿。
///
private IEnumerator SpawnFishBatchedCoroutine(List queue, bool isSync)
{
const int batchSize = 5; // 每帧生成5条
int index = 0;
while (index < queue.Count)
{
// 冰冻中持续等待解冻,避免冰冻期间继续生成鱼
while (isFrozen) yield return null;
int end = Mathf.Min(index + batchSize, queue.Count);
for (int i = index; i < end; i++)
{
TrySpawnSingleFish(queue[i], isSync);
}
index = end;
yield return null; // 等待下一帧
}
}
///
/// 尝试生成单条鱼(含去重检查)。返回是否成功生成。
///
private bool TrySpawnSingleFish(OnFishSpawnFishItem item, bool isSync = false)
{
if (item == null) return false;
NetLog.Log("差值:" + (BeijingTimeManager.Instance.CurrentTimestampMs - item.createdAt) / 1000);
NetLog.Log("差值:" + (BeijingTimeManager.Instance.CurrentTimestampMs - item.createdAt));
NetLog.Log("差值:" + BeijingTimeManager.Instance.CurrentTimestampMs + "创建\n" + item.createdAt);
// ⭐ 鱼阵时 fishNumber 为空,用 fishArrayNumber 作为配置 key;
// 单体鱼时 fishNumber 非空,直接用 fishNumber
string cfgKey = string.IsNullOrEmpty(item.fishNumber) ? item.fishArrayNumber : item.fishNumber;
if (string.IsNullOrEmpty(cfgKey)) return false;
// guid 用服务端 fishId
string guid = item.fishId.ToString();
// ⭐ 事件级去重(第一道):服务端会把同一批鱼推 2~3 遍(activeFishes + 300ms 后补发 init),
// 鱼阵/普通鱼分帧都是延迟创建的,等实例注册到字典再去重来不及 → 会被创建两遍叠一起。
// 受理瞬间就登记 fishId,窗口期内的重复推送直接跳过。
if (!string.IsNullOrEmpty(guid))
{
if (_handledFishIds.TryGetValue(guid, out float handledAt)
&& Time.time - handledAt < FishIdDedupWindow)
{
NetLog.Log($"[CreateFishManager] 重复出鱼已跳过(事件级去重): fishId={guid} cfgKey={cfgKey}");
return false;
}
_handledFishIds[guid] = Time.time; // 登记 / 刷新受理时刻
}
// guid 去重(第二道):已有此 guid 的鱼跳过
// ⭐ 鱼阵鱼的 guid 是 baseGuid_pathId_i 格式,字典里没有 baseGuid 本身,
// 所以要额外检查是否有以 baseGuid+"_" 开头的 key,防止 sync 模式重复出鱼
// 导致 guid 冲突覆盖(旧鱼变野鱼无法销毁)
// ⭐ 优化:用 foreach 代替 LINQ Any(StartsWith),消除 Lambda 闭包和迭代器 GC
if (!string.IsNullOrEmpty(guid) && fishDatas_hashCodekey != null)
{
if (fishDatas_hashCodekey.ContainsKey(guid)) return false;
string guidPrefix = guid + "_";
bool hasPrefixMatch = false;
foreach (var key in fishDatas_hashCodekey.Keys)
{
if (key != null && key.StartsWith(guidPrefix))
{
hasPrefixMatch = true;
break;
}
}
if (hasPrefixMatch) return false;
}
// 构造 FrameData_CreateFish 复用 SpawnFishMainThread
var createFishData = new FrameData_CreateFish
{
fishNumber = cfgKey,
guid = guid,
moveLineId = item.routeId.ToString()
};
// ⭐ 根据 type 决定 moveTime:
// - spawn/init:新鱼,传当前时间 → moveTime≈0,鱼从曲线起点开始游
// (避免服务端/客户端时间差或事件排队回放延迟导致 moveTime>0,
// 鱼凭空出现在曲线中间甚至已过路径末端)
// - sync:同步已有鱼,传 createdAt → moveTime=时间差,鱼出现在路径正确位置
long spawnTime = isSync ? item.createdAt : BeijingTimeManager.Instance.CurrentTimestampMs;
// ⭐ 受理失败(配置/路线缺失等)时回滚去重登记,否则这条鱼会被永久挡住(后续补发也进不来)
if (!SpawnFishMainThread(createFishData, spawnTime))
{
_handledFishIds.Remove(guid);
return false;
}
return true;
}
///
/// 鱼过期自动移除协程。
/// remainingSeconds 秒后调用 Id_DieFish_NotFX 销毁鱼(无特效)。
///
private IEnumerator ExpireFishCoroutine(string guid, int remainingSeconds)
{
yield return new WaitForSeconds(remainingSeconds);
Id_DieFish_NotFX(guid);
}
///
/// 手动出鱼(测试/调试用,不走服务端协议)。
/// 用当前时间作为 spawnTime → moveTime≈0,鱼从曲线起点开始游。
///
/// 鱼编号(配置 fishNumber)
/// 路径编号,<=0 时随机取一条
/// 鱼存活秒数,<=0 时默认 30 秒
/// 生成成功返回 true,失败返回 false
public bool ManualSpawn(string fishNumber, int routeId = 0, int remainingSeconds = 30)
{
if (isFrozen) { Debug.LogWarning("[CreateFishManager] 冰冻中,手动出鱼失败"); return false; }
if (spawnDic == null) { Debug.LogWarning("[CreateFishManager] spawnDic 未初始化,手动出鱼失败"); return false; }
if (string.IsNullOrEmpty(fishNumber)) { Debug.LogWarning("[CreateFishManager] fishNumber 为空,手动出鱼失败"); return false; }
if (!spawnDic.ContainsKey(fishNumber)) { Debug.LogWarning($"[CreateFishManager] 找不到鱼配置: {fishNumber},手动出鱼失败"); return false; }
// routeId<=0 时随机取一条路线(1~4 范围,按现有路线命名规则)
if (routeId <= 0)
routeId = Random.Range(1, 5);
// 生成唯一 guid(用手动出鱼前缀 + 时间戳,避免与服务端 fishId 冲突)
string guid = "manual_" + BeijingTimeManager.Instance.CurrentTimestampMs + "_" + Random.Range(1000, 9999);
var createFishData = new FrameData_CreateFish
{
fishNumber = fishNumber,
guid = guid,
moveLineId = routeId.ToString()
};
// 用当前时间 → moveTime≈0,鱼从曲线起点开始游
SpawnFishMainThread(createFishData, BeijingTimeManager.Instance.CurrentTimestampMs);
//if (remainingSeconds > 0)
// StartCoroutine(ExpireFishCoroutine(guid, remainingSeconds));
Debug.Log($"[CreateFishManager] 手动出鱼: fishNumber={fishNumber}, routeId={routeId}, guid={guid}");
return true;
}
#endregion
#region 旧客户端出鱼链路(已删除)
// 以下方法已删除,鱼创建改由服务端 OnFishSpawn 事件下发:
// - BroadCastCreateFish / TickBossSpawn / TickTSBossSpawn / TickFishArraySpawn
// - TickPTFishSpawn / TickHJFishSpawn / TickTSFishSpawn
// - ProcessSpawnRequests / SendCreateFishRequest
// - CreateInitialFishes / BroadcastExistingFishes / OnRequestSyncFish
// - CreateFish(SyncGameData)
// 保留方法:SpawnFishMainThread / OnlyFish / SpawnFishArray 等(被 OnFishSpawn 调用)
#endregion
#region 鱼生成核心逻辑
// ================================================================
// 旧定时 Tick 方法(已删除,鱼创建改由服务端 OnFishSpawn 事件下发)
// ================================================================
// TickBossSpawn / TickTSBossSpawn / TickFishArraySpawn / TickPTFishSpawn
// TickHJFishSpawn / TickTSFishSpawn / buchongfu / ProcessSpawnRequests
// SendCreateFishRequest / CreateInitialFishes / BroadcastExistingFishes
// OnRequestSyncFish / CreateFish(SyncGameData)
// —— 全部删除,改由 OnFishSpawn(OnFishSpawnData) 接收服务端出鱼事件 ——
// ================================================================
// 单体鱼 / 鱼阵生成(被 OnFishSpawn 调用)
// ================================================================
///
/// 主线程实例化鱼对象(单体鱼 / 鱼阵分发)。
///
private bool SpawnFishMainThread(FrameData_CreateFish createFishData, long time)
{
// ⭐ spawnDic 可能在场景卸载时被置 null,这里再保护一次
if (spawnDic == null) return false;
if (!spawnDic.TryGetValue(createFishData.fishNumber, out var cfg))
{
Debug.LogWarning($"[CreateFishManager] 找不到 spawnDic 配置: {createFishData.fishNumber}");
return false;
}
if (cfg.type == 1)
{
// 单体鱼
return OnlyFish(createFishData, time);
}
if (cfg.type == 2)
{
// 鱼阵(内部校验失败时返回 false,由调用方回滚事件级去重登记)
return SpawnFishArray(createFishData, cfg.fishNumber, time);
}
return false;
}
// ================================================================
// 单体鱼生成
// ================================================================
///
/// 生成一条普通/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);
if (fish == null)
{
Debug.LogError($"[CreateFishManager] Pool.Get 返回 null,跳过此鱼: {createFishData.fishNumber}");
return false;
}
var mc = fish.GetComponent() ?? fish.AddComponent();
mc.monsterData.fishTypeConfig = FishDatas[createFishData.fishNumber];
mc.guid = createFishData.guid;
// ⭐ moveTime 计算:当前时间 - 创建时间(秒)
float elapsedSec = (BeijingTimeManager.Instance.CurrentTimestampMs - time) / 1000f;
// ⭐ 创建视频时长抵扣:
// - 配置了 createVideoClip 且 elapsedSec < 视频时长:视频还没播完
// → moveTime=0(视频继续播),OnMoveUpdata 会跳过移动直到视频结束
// - 配置了 createVideoClip 且 elapsedSec >= 视频时长:视频已播完
// → moveTime = elapsedSec - 视频时长(视频结束后才开始累计路线时间)
// - 未配置 createVideoClip:moveTime = elapsedSec(原逻辑)
if (mc.createVideoClip != null)
{
float videoDuration = (float)mc.createVideoClip.length;
if (elapsedSec < videoDuration)
{
// 视频还没播完,继续播放(Init 里会启动 createVideoPlayer)
mc.moveTime = 0f;
}
else
{
// 视频已播完,扣除视频时长,moveTime 为视频结束后已走的路线时间
mc.moveTime = elapsedSec - videoDuration;
}
}
else
{
mc.moveTime = elapsedSec;
}
mc.fishenum = (FishTypeEnum)fishType.fishType;
// ⭐ 潜行Boss(DeferFishRegister=true)跳过注册:等子类 ActivateBoss() 激活时再入列表。
// 激活前不在鱼列表 → 自动锁定枚举/技能选鱼/服务端击杀通知全部找不到此Boss
if (!mc.DeferFishRegister)
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;
}
///
/// 将鱼实例注册到全局字典及对应分类字典。
/// ⭐ public:潜行Boss(DeferFishRegister=true)由子类 ActivateBoss() 激活时显式调用补注册。
///
public void RegisterFishInstance(MonsterContral mc, int fishTypeId)
{
// ⭐ 检测 guid 冲突:如果 guid 已存在,说明有重复出鱼(可能是 sync 模式重发),
// 旧鱼会被覆盖变成野鱼(无法通过 Id_DieFish_NotFX 销毁)
if (fishDatas_hashCodekey.TryGetValue(mc.guid, out var existing) && existing != null && existing != mc)
{
Debug.LogWarning($"[CreateFishManager] guid 冲突! guid={mc.guid}, 旧鱼={existing.gameObject.name} 被新鱼={mc.gameObject.name} 覆盖");
GameManager.LogFishFlow("COLLISION", mc.guid,
$"旧鱼={existing.gameObject.name}(instId={existing.GetInstanceID()}) 被新鱼={mc.gameObject.name}(instId={mc.GetInstanceID()}) 覆盖,旧鱼强制回收",
mc.GetInstanceID());
// ⭐ 旧鱼强制回收:被覆盖的旧鱼 GameObject 还活着但字典里已找不到,
// 会变成野鱼永远停在屏幕上。这里主动回收旧鱼,避免泄漏
existing.isMove = false; // 停止移动,防止 OnMoveUpdata 继续跑
Pool.Instance.Return(existing.gameObject);
}
fishDatas_hashCodekey[mc.guid] = mc;
// ⭐ 记录创建日志:包含 fishNumber/isyz/yzID/instId/dictCount,用于排查鱼阵鱼无法销毁
GameManager.LogFishFlow("CREATE", mc.guid,
$"fishNumber={mc.monsterData?.fishTypeConfig?.fishNumber} type={fishTypeId} isyz={mc.isyz} yzID={mc.yzID} dictCount={fishDatas_hashCodekey.Count}",
mc.GetInstanceID());
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;
}
#if UNITY_EDITOR
// ⭐ 同步到调试 List,方便 Inspector 查看
if (!fishList_All.Contains(mc)) fishList_All.Add(mc);
switch (fishTypeId)
{
case 1: if (!fishList_Small.Contains(mc)) fishList_Small.Add(mc); break;
case 2: if (!fishList_Boss.Contains(mc)) fishList_Boss.Add(mc); break;
case 3: if (!fishList_HJ.Contains(mc)) fishList_HJ.Add(mc); break;
case 4: if (!fishList_TS.Contains(mc)) fishList_TS.Add(mc); break;
case 5: if (!fishList_TS_Boss.Contains(mc)) fishList_TS_Boss.Add(mc); break;
}
#endif
}
#endregion
#region 鱼阵
// ================================================================
// 鱼阵生成(按 arrayNumber 前缀分发到对应阵型协程/方法)
// ================================================================
/// 根据鱼阵配置分发到对应的生成逻辑。返回是否成功受理(失败时调用方回滚去重登记)。
private bool SpawnFishArray(FrameData_CreateFish createFishData, string fishNumber, long time)
{
if (fishArrayConfigDic == null || !fishArrayConfigDic.TryGetValue(fishNumber, out var yz))
{
Debug.LogError($"[CreateFishManager] 找不到 fishArrayConfig: {fishNumber}");
return false;
}
if (fishDatas == null || !fishDatas.TryGetValue(yz.fishNumber, out var fishType))
{
Debug.LogError($"[CreateFishManager] 鱼阵 fishType 缺失: {yz.fishNumber}");
return false;
}
if (FishYZ.dicfishYZ == null
|| !FishYZ.dicfishYZ.TryGetValue(yz.arrayNumber, out var pathList)
|| pathList.Count == 0)
{
Debug.LogWarning($"[CreateFishManager] 鱼阵路线为空: {yz.arrayNumber}");
return false;
}
// ⭐ 日志:记录鱼阵入口,排查 pathList 重复导致 guid 冲突
GameManager.LogFishFlow("ARRAY_ENTER", createFishData.guid,
$"arrayNumber={yz.arrayNumber} fishNumber={yz.fishNumber} fishCount={yz.fishCount} pathList=[{string.Join(",", pathList)}] dictCount={fishDatas_hashCodekey.Count}");
// ⭐ guid 只能用服务端下发的数据(base=鱼阵 fishId + 线下标 + 第几条):
// 原来还拼了本地自增的 _fishArraySeq,但它是"每个客户端各自的计数器",
// 同一架飞机在各客户端 guid 不同 → 击杀上报的 guid 别的客户端找不到 → 多人不同步。
// 去重已由 TrySpawnSingleFish 的事件级去重负责,不再需要本地序号。
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));
return true;
case "701": // 环形阵:无延迟;同一鱼阵内重复同一条线(同一出生点)时延迟 1s 再发射
StartCoroutine(IeQuan(createFishData, yz, pos, prefab, pathList, time, yz.fishCount));
return true;
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));
//}
return true;
default:
Debug.LogWarning($"[CreateFishManager] 未知鱼阵前缀: {prefix}");
return false;
}
}
// ================================================================
// 鱼阵生成协程 / 方法
// ================================================================
/// 扇形阵:按路线列表逐条延迟 1s 生成一扇。
private IEnumerator IeShanXing(FrameData_CreateFish createFishData, FishArrayConfig yz,
Vector3 pos, GameObject prefab, List pathList, long time, int count)
{
for (int lineIndex = 0; lineIndex < pathList.Count; lineIndex++)
{
if (!isResources)
{
yield break;
}
// ⭐ 同线撞车:这条线刚被别的鱼阵用过时先等一会,避免同一出生点同帧叠一起
yield return WaitLineFree(pathList[lineIndex]);
// ⭐ 一扇内部一次性全部创建(不再逐条延迟);组与组之间靠 1s 等待 + 创建时间偏移拉开
ShanXing(createFishData, yz, pos, prefab, pathList[lineIndex], count, time, lineIndex);
yield return new WaitForSeconds(1f);
}
}
///
/// 同线撞车等待:某条鱼阵路线在 lineConflictCooldown 秒内刚被鱼阵占用过,
/// 就等到冷却结束再发射;返回后立刻把该线标记为占用。
/// ⭐ 进房时服务端会一口气推十几个鱼阵,很容易两个阵落在同一条线上,
/// 同帧同出生点出发必然重叠,靠这个把后出的推到 1 秒后。
///
private IEnumerator WaitLineFree(int pathId)
{
if (lineConflictCooldown > 0f && _lineBusyTime.TryGetValue(pathId, out float last))
{
float wait = lineConflictCooldown - (Time.time - last);
if (wait > 0f && isResources)
yield return new WaitForSeconds(wait);
}
_lineBusyTime[pathId] = Time.time;
}
/// 扇形阵:一条线(一扇)的17条鱼一次性全部创建,不再逐条延迟。
private void ShanXing(FrameData_CreateFish createFishData, FishArrayConfig yz,
Vector3 pos, GameObject prefab, int pathId, int count, long time, int lineIndex)
{
var cfg = spawnDic[createFishData.fishNumber];
// ⭐ 上下基准角(与原始方向判断相反取正):起点 y>0→+90°/y<0→-90°
// (与MonsterContral.Init同key规则"鱼阵"+pathId查曲线)
float baseAngle = -90f;
if (MonsterMoveLine.dic_BezierCurveDatas.TryGetValue("鱼阵" + pathId, out var bd700))
baseAngle = bd700.start.y > 0f ? 90f : -90f;
// ⭐ 扇形:17 条(单数)以基准角为中心左右对称展开,总张角 150°;
// 顺序从中间往两边:第1条在正中间(0°),之后依次 +step、-step 交替(保证左右对称)
const int fanFishCount = 17;
const float fanTotalAngle = 150f;
float fanStep = fanTotalAngle / (fanFishCount - 1); // 150 / 16 = 9.375°
for (int i = 1; i <= fanFishCount; i++)
{
// i=1→0°, 2→+9.375°, 3→-9.375°, 4→+18.75°, 5→-18.75° … 16→+75°, 17→-75°
float mag = (i / 2) * fanStep;
float angle = i <= 1 ? 0f : (i % 2 == 0 ? mag : -mag);
// ⭐ 方向判断与原始flipY700一致:路线起点start.y<0的阵绕鱼头轴翻面180°
// (鱼头方向不变,肚背朝向翻转);原始写法为 transform.Rotate(Z,180,Space.Self)
float flipZ = (bd700 != null && bd700.start.y < 0f) ? 180f : 0f;
// ⭐ 分布角度=基准角+偏移,绕世界Z轴左乘叠加(屏幕平面内展开);
// Y=-90固定摆平,Z是朝前的轴(翻面阵+180)
var rotation = Quaternion.AngleAxis(baseAngle + angle, Vector3.forward)
* Quaternion.Euler(prefab.transform.eulerAngles.x, -90f, prefab.transform.eulerAngles.z + flipZ);
//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];
// ⭐ guid = 鱼阵fishId_线下标_第几条(只用服务端数据,全端一致):
// 线下标区分 pathList 里重复的同一条线(如[1,1,1,1,1]不会重号),
// 不能拼本地自增序号,否则同一架飞机各客户端 guid 不同 → 击杀广播对不上 → 多人不同步
mc.guid = createFishData.guid + "_" + lineIndex + "_" + i;
RegisterFishInstance(mc, FishDatas[yz.fishNumber].fishType);
// ⭐ 位置只按"本条鱼自己的创建时间"推算:
// moveTime 从 0 开始累计(自己出生到现在) → 先创建的组已经游远、后创建的刚出生,
// 顺序天然是"先出的在前、后出的在后"。
// yzGroupTimeOffset 固定 0:给后创建的组加偏移会把它往前顶,顺序就反了(且会顶出屏)
mc.moveTime = 0f;
mc.yzGroupTimeOffset = 0f;
mc.Init(pathId.ToString(), cfg.times, true, yz.arrayNumber);
// ⭐ 鱼阵调试日志:输出每条子鱼生成后的旋转与缩放(排查"有的正有的反")
//Debug.Log($"[鱼阵调试] 阵={yz.arrayNumber}(扇形700) pathId={pathId} 第{i}/{fanFishCount}条 guid={mc.guid} " +
// $"rotation={fish.transform.eulerAngles.ToString("F1")} localScale={fish.transform.localScale.ToString("F3")}");
}
}
///
/// 环形阵:按路线列表遍历生成圈。
/// 正常无延迟(同帧出);⭐ 同一条线(lineConflictCooldown 秒内)刚被鱼阵用过时,
/// 等待冷却再发射,避免两圈叠在同一出生点(含鱼阵内重复线、不同鱼阵撞线)。
///
private IEnumerator IeQuan(FrameData_CreateFish createFishData, FishArrayConfig yz,
Vector3 pos, GameObject prefab, List pathList, long time, int count)
{
for (int lineIndex = 0; lineIndex < pathList.Count; lineIndex++)
{
if (!isResources)
{
yield break;
}
int pathId = pathList[lineIndex];
yield return WaitLineFree(pathId);
Quan(createFishData, yz, pos, prefab, pathId, count, time, lineIndex);
}
}
///
/// 环形阵:在同一路径上生成 count 条鱼,角度均匀分布(360/count)。
///
private void Quan(FrameData_CreateFish createFishData, FishArrayConfig yz,
Vector3 pos, GameObject prefab, int pathId, int count, long time, int lineIndex)
{
if (count <= 0) return;
var cfg = spawnDic[createFishData.fishNumber];
for (int i = 0; i < count; i++)
{
float angle = (360f / count) * i;
// ⭐ 分布角度=基准90°+偏移(上下的鱼阵,中间点从90°起算,沿圈360°均分);
// 绕世界Z轴左乘叠加(屏幕平面内展开);Y=-90固定摆平,Z是朝前的轴不动
var rotation = Quaternion.AngleAxis(90f + angle, Vector3.forward)
* Quaternion.Euler(prefab.transform.eulerAngles.x, -90f, prefab.transform.eulerAngles.z);
//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];
// ⭐ guid = 鱼阵fishId_线下标_第几条(只用服务端数据,全端一致):
// 线下标区分 pathList 里重复的同一条线(如[1,1,1,1,1]不会重号),
// 不能拼本地自增序号,否则同一架飞机各客户端 guid 不同 → 击杀广播对不上 → 多人不同步
mc.guid = createFishData.guid + "_" + lineIndex + "_" + i;
RegisterFishInstance(mc, FishDatas[yz.fishNumber].fishType);
// ⭐ 位置只按"本条鱼自己的创建时间"推算(moveTime 从 0 累计),说明同扇形
// 圈的不同线本来就在不同出生点,同线重复的靠同线冷闸 1s 错开
mc.moveTime = 0f;
mc.yzGroupTimeOffset = 0f;
mc.Init(pathId.ToString(), cfg.times, true, yz.arrayNumber);
// ⭐ 鱼阵调试日志:输出每条子鱼生成后的旋转与缩放(排查"有的正有的反")
Debug.Log($"[鱼阵调试] 阵={yz.arrayNumber}(环形701) pathId={pathId} 第{i + 1}/{count}条 guid={mc.guid} " +
$"rotation={fish.transform.eulerAngles.ToString("F1")} localScale={fish.transform.localScale.ToString("F3")}");
}
}
///
/// 顺序阵:每隔 1s 生成一条鱼,共生成 count 条。
/// guid 与 700/701 保持同一格式:base_线下标_第几条(顺序阵只用第0条线,线下标固定0)。
///
private IEnumerator YZ7007(FrameData_CreateFish createFishData, FishArrayConfig yz,
Vector3 pos, GameObject prefab, long time)
{
if (!spawnDic.TryGetValue(createFishData.fishNumber, out var cfg))
{
Debug.LogWarning($"[CreateFishManager] YZ7007 找不到 spawnDic 配置: {createFishData.fishNumber}");
yield break;
}
// ⭐ 7007 鱼阵:路线从 FishYZ.dicfishYZ[arrayNumber] 取第 0 个
// (数量由后端控制,客户端只读路线列表第 0 个)
if (!FishYZ.dicfishYZ.TryGetValue(yz.arrayNumber, out var yzPathList) || yzPathList.Count == 0)
{
Debug.LogWarning($"[CreateFishManager] YZ7007 找不到鱼阵路线: {yz.arrayNumber}");
yield break;
}
int yzPathId = yzPathList[0];
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);
if (fish == null)
{
Debug.LogWarning($"[CreateFishManager] YZ7007 Pool.Get 返回 null,跳过: {yz.fishNumber}");
yield return new WaitForSeconds(1f);
continue;
}
var mc = fish.GetComponent() ?? fish.AddComponent();
mc.monsterData.fishTypeConfig = FishDatas[yz.fishNumber];
// ⭐ guid 统一成 base_线下标_第几条(只用服务端数据,全端一致):
// 原来用 base+i(如 113950851)没有下划线,重复推送时去重扫不到 → 旧鱼被回收重建;
// 拼本地自增序号又会让各客户端 guid 不同 → 击杀广播对不上,故两者都不能用
mc.guid = createFishData.guid + "_0_" + i;
RegisterFishInstance(mc, FishDatas[yz.fishNumber].fishType);
// 每条鱼额外减去已等待秒数,使路径起点与帧数同步
mc.moveTime = ((BeijingTimeManager.Instance.CurrentTimestampMs - time) / 1000f) - i;
// ⭐ 路线用 FishYZ 取到的第 0 个,yzID 用 arrayNumber
mc.Init(yzPathId.ToString(), cfg.times, true, yz.arrayNumber);
// ⭐ 鱼阵调试日志:输出每条子鱼生成后的旋转与缩放(排查"有的正有的反")
//Debug.Log($"[鱼阵调试] 阵={yz.arrayNumber}(顺序阵) pathId={yzPathId} 第{i + 1}/{yz.fishCount}条 guid={mc.guid} " +
// $"rotation={fish.transform.eulerAngles.ToString("F1")} localScale={fish.transform.localScale.ToString("F3")}");
yield return new WaitForSeconds(1f);
}
}
#endregion
#region 结束游戏退出房间
/// 退出房间:关闭生成系统。
public void ExitRoom()
{
CloseSpawnSystem();
}
///
/// 关闭生成系统:清理列表,销毁场景内所有鱼对象。
///
public async void CloseSpawnSystem()
{
//await Task.Delay(2000);
// ⭐ 退房复位冰冻状态:冰冻中退房时旧面板倒计时未走完就销毁,
// 不会触发 UnfreezeAllFish → isFrozen 残留 true → 污染下一个房间不出鱼
ResetFreezeState();
spawnList = null;
spawnDic = null;
// ⭐ 退房清理排队中的出鱼事件,防止旧房间残留鱼泄进下一个房间
if (_pendingFishSpawns != null) _pendingFishSpawns.Clear();
// ⭐ 退房清空出鱼去重记录,避免旧房间 fishId 影响下一个房间(新房间 EnterRoom 也会清)
_handledFishIds.Clear();
yz_FishConfig_Line_Dic = null;
yz_FishConfig_Line = null;
yz_FishConfig_Line_Dic1 = null;
if (fishRoot == null) return;
// ⭐ 收集要销毁的对象列表,避免遍历时字典被修改或对象已销毁导致 MissingReferenceException
var fishToDestroy = new List();
foreach (var item in fishDatas_hashCodekey)
{
if (item.Value != null && item.Value.gameObject != null)
{
fishToDestroy.Add(item.Value.gameObject);
}
}
foreach (var go in fishToDestroy)
{
if (go != null) Destroy(go);
}
fishDatas_hashCodekey.Clear();
fishDatas_hashCodekey_Boss.Clear();
fishDatas_hashCodekey_Small.Clear();
#if UNITY_EDITOR
// ⭐ 清理调试 List
fishList_All.Clear();
fishList_Small.Clear();
fishList_Boss.Clear();
fishList_HJ.Clear();
fishList_TS.Clear();
fishList_TS_Boss.Clear();
#endif
foreach (Transform item in fishRoot)
{
Destroy(item.gameObject);
}
// ⭐ 不销毁 bjEffectRoot 本身:只清空其下的爆奖特效子物体
// (Boss 分数版/加分弹出/BJEndUI/特殊爆奖等),
// 防止退出房间时残留未结束的爆奖协程/动画造成的内存泄漏
if (_bjEffectRoot != null)
{
for (int i = _bjEffectRoot.childCount - 1; i >= 0; i--)
Destroy(_bjEffectRoot.GetChild(i).gameObject);
}
// ⭐ 清理特效池:bjEffectRoot 下的特效子物体销毁后池内对象失效,清空避免复用已销毁对象
EffectPool.Instance.ClearAll();
//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
// ================================================================
// 随机工具方法
// ================================================================
// ⭐ 静态复用 Random 实例,避免每次调用 new System.Random() 产生 GC
private static readonly System.Random _sharedRand = new System.Random();
// ⭐ 预分配结果 List,避免调用方反复 new List
private static readonly List _reusableIntResult = new List(64);
///
/// 从 [min, max) 区间随机取 count 个不重复整数。
/// 若 min==max 返回空列表。
/// ⭐ 优化:复用静态 Random + 预分配 List,零 GC 分配。
/// 注意:返回的 List 是内部复用对象,调用方用完前不要再次调用本方法。
///
public static List RandomUniqueNumbers(int count, int min, int max)
{
_reusableIntResult.Clear();
if (min >= max)
{
Debug.LogWarning("[CreateFishManager] RandomUniqueNumbers: min >= max,返回空列表");
return _reusableIntResult;
}
count = Mathf.Min(count, max - min); // 不能超过范围
// ⭐ 用 HashSet 去重,但复用静态实例避免每次 new
_reusableIntSet.Clear();
while (_reusableIntSet.Count < count)
_reusableIntSet.Add(_sharedRand.Next(min, max));
foreach (var v in _reusableIntSet)
_reusableIntResult.Add(v);
return _reusableIntResult;
}
private static readonly HashSet _reusableIntSet = new HashSet(64);
// ⭐ 预分配 string 结果 List,避免每次调用 new List
private static readonly List _reusableStrResult = new List(64);
// ⭐ 预分配 key 缓冲,避免 keyCollection.ToList() 产生 GC
private static readonly List _reusableKeyBuf = new List(128);
// ⭐ 预分配索引 HashSet,避免每次 new HashSet
private static readonly HashSet _reusableIdxSet = new HashSet(64);
// ⭐ 缓存 uiRoot.Canvas,避免每次杀鱼特效都 GetComponent