Files
5.24BuYu/Assets/MyGame/Scripts/Manager/CreateFishManager.cs
T
2026-10-02 02:46:20 +08:00

2538 lines
114 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// ============================================================
// 文件: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 UnityEngine.Serialization;
using Random = UnityEngine.Random;
/// <summary>
/// 捕鱼游戏核心管理器:负责鱼的定时生成、网络同步、阵型调度、死亡特效。
/// 继承 MonoSingleton 保证全局唯一。
/// </summary>
public class CreateFishManager : MonoSingleton<CreateFishManager>
{
// ----------------------------------------------------------------
// 外部依赖
// ----------------------------------------------------------------
/// <summary>当前游戏全局配置(通过 GameManager 获取)</summary>
public GameConfig M_GameConfig => GameManager.Instance.gameConfig;
// ----------------------------------------------------------------
// 配置缓存(空间换时间,避免 List.Find O(n) 查找)
// ----------------------------------------------------------------
/// <summary>roomFishConfigs 按游戏类型索引</summary>
private Dictionary<GameTypeEnum, RoomFishConfig> roomFishConfig;
/// <summary>每个游戏类型下 fishNumber → RoomFishDetailConfig</summary>
private Dictionary<GameTypeEnum, Dictionary<string, RoomFishDetailConfig>> createDic;
/// <summary>fishNumber → FishTypeConfig(基础配置缓存)</summary>
private Dictionary<string, FishTypeConfig> fishDatas;
/// <summary>fishArrayNumber → FishArrayConfig(鱼阵配置缓存)</summary>
private Dictionary<string, FishArrayConfig> fishArrayConfigDic;
// ----------------------------------------------------------------
// UI 引用
// ----------------------------------------------------------------
/// <summary>当前游戏面板(用于坐标转换、金币飞行等)</summary>
public GameMainPanel_FishBase currGameType;
// ----------------------------------------------------------------
// 死亡特效 Prefab(Inspector 赋值)
// ----------------------------------------------------------------
public GameObject dieFXPrefab; // 基础死亡特效
public GameObject dieFXPrefabAward; // 普通鱼金币奖励特效
public GameObject dieFXPrefabAwardTmp; // Boss 分数浮动文本
public GameObject dieFXPrefabAwardTmp_AddScore;
// ----------------------------------------------------------------
// 生成系统运行时数据
// ----------------------------------------------------------------
/// <summary>鱼的父节点(保持场景层级整洁)</summary>
public Transform fishRoot;
/// <summary>鱼的父节点(保持场景层级整洁)</summary>
public Transform poolRoot;
/// <summary>
/// 非 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 节点)。
/// </summary>
[FormerlySerializedAs("bjEffectRoot")]
[SerializeField] private Transform _bjEffectRoot;
/// <summary>爆奖特效统一父节点:为 null(意外删除)时自动在场景根节点重建</summary>
public Transform bjEffectRoot
{
get
{
if (_bjEffectRoot == null) EnsureBJEffectRoot();
return _bjEffectRoot;
}
}
/// <summary>当前房间可生成鱼列表(与 NativeArray 下标对齐)</summary>
public List<RoomFishDetailConfig> spawnList;
/// <summary>当前房间 fishNumber → RoomFishDetailConfig</summary>
public Dictionary<string, RoomFishDetailConfig> spawnDic;
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<string, CharacterStateType> frozenFishStates = new Dictionary<string, CharacterStateType>();
private Dictionary<string, bool> frozenFishIsMoves = new Dictionary<string, bool>();
/// <summary>按鱼类型分类的生成配置(Boss / 普通 / 黄金…)</summary>
private Dictionary<FishTypeEnum, List<RoomFishDetailConfig>> fishTypeConfig;
// ------ 鱼阵路线相关 ------
/// <summary>arrayNumber → 该阵型所有可用生成配置</summary>
private Dictionary<string, List<RoomFishDetailConfig>> yz_FishConfig_Line;
/// <summary>列表下标 → arrayNumber(用于随机取阵型)</summary>
private Dictionary<int, string> yz_FishConfig_Line_Dic;
/// <summary>arrayNumber → 列表下标(反向索引)</summary>
private Dictionary<string, int> yz_FishConfig_Line_Dic1;
/// <summary>二维列表,与字典保持同步(供随机无重复取阵型用)</summary>
private List<List<RoomFishDetailConfig>> yz_FishConfig_Line_List;
// ------ Job System NativeArray ------
private NativeArray<float> timers; // 每条鱼的累计计时
private NativeArray<float> frequencies; // 每条鱼的生成间隔
private NativeArray<int> shouldSpawn; // 1=本帧应尝试生成,0=冷却中
// ----------------------------------------------------------------
// 运行时鱼实例字典(guid 为 key)
// ----------------------------------------------------------------
/// <summary>全部存活鱼(包含 Boss / 小鱼 / 黄金鱼)</summary>
public Dictionary<string, MonsterContral> fishDatas_hashCodekey = new Dictionary<string, MonsterContral>();
/// <summary>普通鱼</summary>
public Dictionary<string, MonsterContral> fishDatas_hashCodekey_Small = new Dictionary<string, MonsterContral>();
/// <summary>Boss 鱼</summary>
public Dictionary<string, MonsterContral> fishDatas_hashCodekey_Boss = new Dictionary<string, MonsterContral>();
/// <summary>黄金</summary>
public Dictionary<string, MonsterContral> fishDatas_hashCodekey_HJ = new Dictionary<string, MonsterContral>();
/// <summary>特殊鱼(非 Boss)</summary>
public Dictionary<string, MonsterContral> fishDatas_hashCodekey_TS = new Dictionary<string, MonsterContral>();
/// <summary>特殊 Boss</summary>
public Dictionary<string, MonsterContral> fishDatas_hashCodekey_TS_Boss = new Dictionary<string, MonsterContral>();
#if UNITY_EDITOR
// ⭐ 编辑器调试 List:与上方字典同步,方便在 Inspector 查看当前场上活鱼
public List<MonsterContral> fishList_All = new List<MonsterContral>();
public List<MonsterContral> fishList_Small = new List<MonsterContral>();
public List<MonsterContral> fishList_Boss = new List<MonsterContral>();
public List<MonsterContral> fishList_HJ = new List<MonsterContral>();
public List<MonsterContral> fishList_TS = new List<MonsterContral>();
public List<MonsterContral> fishList_TS_Boss = new List<MonsterContral>();
#endif
// ------ 各类计时器 ------
private RoomFishProbabilityConfig tsbossCreateData; // 特殊Boss 创建数据
private float tsbossTimer = 999f; // 特殊Boss 刷新倒计时(秒)
private int tsbossCount; // 本轮同时启用特殊Boss鱼数量
private int tsbossQuanZhongCount; // 本轮同时启用特殊Boss鱼数量
private List<string> tsbossRecentList = new List<string>(); // 最近出现过的特殊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<string> bossRecentList = new List<string>(); // 最近出现过的Boss(cool down)
private int bossRecentMax = 3; // N次内不重复出现
/// <summary>Boss鱼路线奇偶切换标记:true=下次选奇数路线,false=下次选偶数路线</summary>
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;
/// <summary>每条鱼阵路线最近一次被鱼阵占用的时刻(Time.time),用于同线撞车延迟</summary>
private readonly Dictionary<int, float> _lineBusyTime = new Dictionary<int, float>();
/// <summary>
/// 鱼阵实例全局计数器,每次 SpawnFishArray 递增。
/// 用于生成全局唯一 guid,防止 pathList 重复 pathId 或同 fishId 多次推送导致 guid 冲突。
/// guid 格式: baseGuid_arraySeq_i (如 16501_3_1),arraySeq 每次鱼阵递增,确保唯一。
/// </summary>
private int _fishArraySeq = 0;
// ================================================================
// 懒加载属性
// ================================================================
/// <summary>
/// 鱼基础配置字典(fishNumber → FishTypeConfig),首次访问时构建。
/// </summary>
public Dictionary<string, FishTypeConfig> FishDatas
{
get
{
if (fishDatas != null) return fishDatas;
fishDatas = new Dictionary<string, FishTypeConfig>(M_GameConfig.fishTypeConfigs.Count);
foreach (var item in M_GameConfig.fishTypeConfigs)
fishDatas[item.fishNumber] = item;
return fishDatas;
}
}
/// <summary>
/// 鱼阵配置字典(fishArrayNumber → FishArrayConfig),首次访问时构建。
/// </summary>
public Dictionary<string, FishArrayConfig> FishArrayDatas
{
get
{
if (fishArrayConfigDic != null) return fishArrayConfigDic;
fishArrayConfigDic = new Dictionary<string, FishArrayConfig>(M_GameConfig.fishArrayConfigs.Count);
foreach (var item in M_GameConfig.fishArrayConfigs)
fishArrayConfigDic[item.fishArrayNumber] = item;
return fishArrayConfigDic;
}
}
/// <summary>
/// 房间鱼配置(按游戏类型),首次访问时触发懒加载初始化。
/// </summary>
public Dictionary<GameTypeEnum, RoomFishConfig> RoomFishConfig
{
get { EnsureInit(); return roomFishConfig; }
}
/// <summary>
/// 游戏类型 → (fishNumber → RoomFishDetailConfig),首次访问时触发懒加载。
/// </summary>
public Dictionary<GameTypeEnum, Dictionary<string, RoomFishDetailConfig>> CreateDic
{
get { EnsureInit(); return createDic; }
}
#region 生命周期
// ================================================================
// Unity 生命周期
// ================================================================
protected override void Awake()
{
base.Awake();
yzCreateData = new RoomFishProbabilityConfig
{
countRange = new List<int> { 2, 3 },
firstFishCountRange = new List<int> { 2, 3 },
roomLevel = 3,
fishType = 0,
maxCountRange = new List<int> { 10, 20 },
timeRange = new List<int> { 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<OnFishSpawnData>(
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<OnFishSpawnData>(
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;
}
/// <summary>
/// 懒加载初始化:只执行一次,构建全部配置缓存字典。
/// </summary>
private void EnsureInit()
{
if (roomFishConfig != null) return;
FishYZ.Init();
roomFishConfig = new Dictionary<GameTypeEnum, RoomFishConfig>();
createDic = new Dictionary<GameTypeEnum, Dictionary<string, RoomFishDetailConfig>>();
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<string, RoomFishDetailConfig>();
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;
}
}
/// <summary>
/// 构建高频查询的字典缓存(fishArrayConfigs / fishTypeConfigs)。
/// </summary>
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 进入房间加载
/// <summary>进入房间:初始化生成系统并绑定游戏面板。</summary>
public void EnterRoom(GameTypeEnum gameType)
{
// ⭐ 复位残留冰冻状态:冰冻中切房时旧面板倒计时未走完就销毁,
// 不会触发 UnfreezeAllFish → isFrozen 残留 true → 新房间 OnFishSpawn 全部被拦,不出鱼
ResetFreezeState();
InitSpawnSystem(gameType);
BindCurrentGamePanel();
EnsureBJEffectRoot();
_lineBusyTime.Clear(); // 清掉上个房间的路线占用记录
// ⭐ 必须先把 isResources 置 true 再回放:
// 调用方( GameMainPanel_FishBase )是在 EnterRoom 之后才置 true 的,
// 而鱼阵协程(IeShanXing/IeQuan/YZ7007)里都有 `if (!isResources) yield break;`,
// 回放时它还是 false(退房残留)→ 服务端进房瞬间推的初始鱼阵(init/sync)
// 全部 ARRAY_ENTER 后一条鱼都不生成,进房看不到鱼阵
isResources = true;
// ⭐ 回放进房加载期间收到的出鱼事件(服务端进房瞬间推的 init/sync 初始鱼)
FlushPendingFishSpawns();
}
/// <summary>
/// 创建爆奖特效统一父节点(挂在场景根节点,不挂 UI 节点下)。
/// 已存在则不重复创建。所有爆奖相关 Instantiate 都应使用此节点作为父对象。
/// </summary>
private void EnsureBJEffectRoot()
{
if (_bjEffectRoot != null) return;
// ⭐ 挂场景根节点(不挂 UIManager.uiRoot):世界空间爆奖特效直接用世界坐标摆放
var go = new GameObject("bjEffectRoot");
_bjEffectRoot = go.transform;
}
/// <summary>根据当前 GameType 绑定对应 UI 面板到 currGameType。</summary>
private void BindCurrentGamePanel()
{
switch (GameManager.Instance.gameData.currGameType)
{
case GameType.FLZY:
currGameType = UIManager.Instance.Get<GameMainPanel_FLZY>(UIPanel.MainGamePanelFLZY);
break;
case GameType.YYSC:
currGameType = UIManager.Instance.Get<GameMainPanel_YYSC>(UIPanel.MainGamePanelYYSC);
break;
case GameType.XJMH:
currGameType = UIManager.Instance.Get<GameMainPanel_XJMH>(UIPanel.MainGamePanelXJMH);
break;
case GameType.HHWK:
currGameType = UIManager.Instance.Get<GameMainPanel_HHWK>(UIPanel.MainGamePanelHHWK);
break;
default:
break;
}
}
/// <summary>
/// 进入房间时初始化生成系统:加载当前房间配置、分配 NativeArray。
/// </summary>
public async void InitSpawnSystem(GameTypeEnum gameType)
{
EnsureInit();
// 初始化分类容器
fishTypeConfig = new Dictionary<FishTypeEnum, List<RoomFishDetailConfig>>();
yz_FishConfig_Line = new Dictionary<string, List<RoomFishDetailConfig>>();
yz_FishConfig_Line_Dic = new Dictionary<int, string>();
yz_FishConfig_Line_Dic1 = new Dictionary<string, int>();
yz_FishConfig_Line_List = new List<List<RoomFishDetailConfig>>();
spawnList = new List<RoomFishDetailConfig>();
spawnDic = new Dictionary<string, RoomFishDetailConfig>();
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<float>(count, Allocator.Persistent);
frequencies = new NativeArray<float>(count, Allocator.Persistent);
shouldSpawn = new NativeArray<int>(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();
}
/// <summary>
/// 将单体鱼配置按 FishTypeEnum 加入分类字典。
/// </summary>
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<RoomFishDetailConfig>();
fishTypeConfig[typeEnum].Add(item);
}
/// <summary>
/// 将鱼阵配置按 arrayNumber 加入路线字典(使用缓存替代 List.Find)。
/// </summary>
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<RoomFishDetailConfig>();
yz_FishConfig_Line_List.Add(new List<RoomFishDetailConfig>());
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" - 重连/取消挂机同步(把当前场上活鱼推送给回归玩家)
/// <summary>
/// 接收服务端 OnFishSpawn 事件,生成鱼实例。
/// 遍历 data.fish 列表,对每条鱼:
/// 1. guid 去重(已有此 guid 的鱼跳过,防 sync 模式重复 Spawn)
/// 2. 构造 FrameData_CreateFish 复用 SpawnFishMainThread 生成
/// 3. 启动 expireAt 过期自动移除协程
/// </summary>
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<OnFishSpawnData>();
_pendingFishSpawns.Add(data);
Debug.Log($"[CreateFishManager] 生成系统未就绪,出鱼事件入队等待回放: type={data.type}, count={data.fish.Count}, queued={_pendingFishSpawns.Count}");
return;
}
// ⭐ 分帧出鱼:普通鱼/黄金鱼分批生成(每帧5条),其他类型立即全出
// 避免一次出大量普通鱼导致单帧卡顿
var batchedQueue = new List<OnFishSpawnFishItem>(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}");
}
/// <summary>生成系统未就绪期间收到的出鱼事件队列(EnterRoom 完成后回放)</summary>
private List<OnFishSpawnData> _pendingFishSpawns;
/// <summary>
/// 回放进房加载期间收到的出鱼事件(服务端在进房瞬间推的 init/sync 初始鱼)。
/// 在 EnterRoom 末尾调用:此时配置/资源/面板都已就绪。
/// </summary>
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}"); }
}
}
/// <summary>
/// 判断该鱼是否需要分帧生成。
/// 普通鱼(1)和黄金鱼(3)且非鱼阵 → 分帧;其他(Boss/特殊鱼/特殊Boss/鱼阵) → 立即出。
/// </summary>
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;
}
/// <summary>
/// 分帧出鱼协程:每帧生成 batchSize 条,避免单帧卡顿。
/// </summary>
private IEnumerator SpawnFishBatchedCoroutine(List<OnFishSpawnFishItem> 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; // 等待下一帧
}
}
/// <summary>
/// 尝试生成单条鱼(含去重检查)。返回是否成功生成。
/// </summary>
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();
// 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;
SpawnFishMainThread(createFishData, spawnTime);
return true;
}
/// <summary>
/// 鱼过期自动移除协程。
/// remainingSeconds 秒后调用 Id_DieFish_NotFX 销毁鱼(无特效)。
/// </summary>
private IEnumerator ExpireFishCoroutine(string guid, int remainingSeconds)
{
yield return new WaitForSeconds(remainingSeconds);
Id_DieFish_NotFX(guid);
}
/// <summary>
/// 手动出鱼(测试/调试用,不走服务端协议)。
/// 用当前时间作为 spawnTime → moveTime≈0,鱼从曲线起点开始游。
/// </summary>
/// <param name="fishNumber">鱼编号(配置 fishNumber)</param>
/// <param name="routeId">路径编号,<=0 时随机取一条</param>
/// <param name="remainingSeconds">鱼存活秒数,<=0 时默认 30 秒</param>
/// <returns>生成成功返回 true,失败返回 false</returns>
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 调用)
// ================================================================
/// <summary>
/// 主线程实例化鱼对象(单体鱼 / 鱼阵分发)。
/// </summary>
private void SpawnFishMainThread(FrameData_CreateFish createFishData, long time)
{
// ⭐ spawnDic 可能在场景卸载时被置 null,这里再保护一次
if (spawnDic == null) return;
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);
}
}
// ================================================================
// 单体鱼生成
// ================================================================
/// <summary>
/// 生成一条普通/Boss/黄金鱼并初始化路径。
/// </summary>
/// <returns>成功返回 true,配置缺失返回 false。</returns>
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<MonsterContral>() ?? fish.AddComponent<MonsterContral>();
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;
// 按鱼类型放入对应字典
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;
}
/// <summary>将鱼实例注册到全局字典及对应分类字典。</summary>
private 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 前缀分发到对应阵型协程/方法)
// ================================================================
/// <summary>根据鱼阵配置分发到对应的生成逻辑。</summary>
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;
}
// ⭐ 日志:记录鱼阵入口,排查 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
// 防止 pathList 中 pathId 重复,或同 fishId 多次推送导致 guid 冲突
int arraySeq = ++_fishArraySeq;
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, arraySeq));
break;
case "701": // 环形阵:无延迟;同一鱼阵内重复同一条线(同一出生点)时延迟 1s 再发射
StartCoroutine(IeQuan(createFishData, yz, pos, prefab, pathList, time, yz.fishCount, arraySeq));
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;
}
}
// ================================================================
// 鱼阵生成协程 / 方法
// ================================================================
/// <summary>扇形阵:按路线列表逐条延迟 1s 生成一扇。</summary>
private IEnumerator IeShanXing(FrameData_CreateFish createFishData, FishArrayConfig yz,
Vector3 pos, GameObject prefab, List<int> pathList, long time, int count, int arraySeq)
{
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, arraySeq, lineIndex);
yield return new WaitForSeconds(1f);
}
}
/// <summary>
/// 同线撞车等待:某条鱼阵路线在 lineConflictCooldown 秒内刚被鱼阵占用过,
/// 就等到冷却结束再发射;返回后立刻把该线标记为占用。
/// ⭐ 进房时服务端会一口气推十几个鱼阵,很容易两个阵落在同一条线上,
/// 同帧同出生点出发必然重叠,靠这个把后出的推到 1 秒后。
/// </summary>
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;
}
/// <summary>扇形阵:一条线(一扇)的17条鱼一次性全部创建,不再逐条延迟。</summary>
private void ShanXing(FrameData_CreateFish createFishData, FishArrayConfig yz,
Vector3 pos, GameObject prefab, int pathId, int count, long time, int arraySeq, 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<MonsterContral>();
mc.monsterData.fishTypeConfig = FishDatas[yz.fishNumber];
// ⭐ guid 用 arraySeq + lineIndex 替代 pathId:arraySeq 每次鱼阵递增,lineIndex 为线列表下标,
// 两者组合全局唯一,防止 pathList 重复 pathId(如[1,1,1,1,1])导致 guid 冲突旧鱼被回收
mc.guid = createFishData.guid + "_" + arraySeq + "_" + 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")}");
}
}
/// <summary>
/// 环形阵:按路线列表遍历生成圈。
/// 正常无延迟(同帧出);⭐ 同一条线(lineConflictCooldown 秒内)刚被鱼阵用过时,
/// 等待冷却再发射,避免两圈叠在同一出生点(含鱼阵内重复线、不同鱼阵撞线)。
/// </summary>
private IEnumerator IeQuan(FrameData_CreateFish createFishData, FishArrayConfig yz,
Vector3 pos, GameObject prefab, List<int> pathList, long time, int count, int arraySeq)
{
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, arraySeq, lineIndex);
}
}
/// <summary>
/// 环形阵:在同一路径上生成 count 条鱼,角度均匀分布(360/count)。
/// </summary>
private void Quan(FrameData_CreateFish createFishData, FishArrayConfig yz,
Vector3 pos, GameObject prefab, int pathId, int count, long time, int arraySeq, 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<MonsterContral>();
mc.monsterData.fishTypeConfig = FishDatas[yz.fishNumber];
// ⭐ guid 用 arraySeq + lineIndex 替代 pathId:arraySeq 每次鱼阵递增,lineIndex 为线列表下标,
// 两者组合全局唯一,防止 pathList 重复 pathId(如[1,1,1,1,1])导致 guid 冲突旧鱼被回收
mc.guid = createFishData.guid + "_" + arraySeq + "_" + 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")}");
}
}
/// <summary>
/// 顺序阵:每隔 1s 生成一条鱼,共生成 count 条。
/// </summary>
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<MonsterContral>() ?? fish.AddComponent<MonsterContral>();
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;
// ⭐ 路线用 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 结束游戏退出房间
/// <summary>退出房间:关闭生成系统。</summary>
public void ExitRoom()
{
CloseSpawnSystem();
}
/// <summary>
/// 关闭生成系统:清理列表,销毁场景内所有鱼对象。
/// </summary>
public async void CloseSpawnSystem()
{
//await Task.Delay(2000);
// ⭐ 退房复位冰冻状态:冰冻中退房时旧面板倒计时未走完就销毁,
// 不会触发 UnfreezeAllFish → isFrozen 残留 true → 污染下一个房间不出鱼
ResetFreezeState();
spawnList = null;
spawnDic = null;
// ⭐ 退房清理排队中的出鱼事件,防止旧房间残留鱼泄进下一个房间
if (_pendingFishSpawns != null) _pendingFishSpawns.Clear();
yz_FishConfig_Line_Dic = null;
yz_FishConfig_Line = null;
yz_FishConfig_Line_Dic1 = null;
if (fishRoot == null) return;
// ⭐ 收集要销毁的对象列表,避免遍历时字典被修改或对象已销毁导致 MissingReferenceException
var fishToDestroy = new List<GameObject>();
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);
}
/// <summary>释放 NativeArray(避免 Unity 内存泄漏警告)。</summary>
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<int>
private static readonly List<int> _reusableIntResult = new List<int>(64);
/// <summary>
/// 从 [min, max) 区间随机取 count 个不重复整数。
/// 若 min==max 返回空列表。
/// ⭐ 优化:复用静态 Random + 预分配 List,零 GC 分配。
/// 注意:返回的 List 是内部复用对象,调用方用完前不要再次调用本方法。
/// </summary>
public static List<int> 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<int> _reusableIntSet = new HashSet<int>(64);
// ⭐ 预分配 string 结果 List,避免每次调用 new List<string>
private static readonly List<string> _reusableStrResult = new List<string>(64);
// ⭐ 预分配 key 缓冲,避免 keyCollection.ToList() 产生 GC
private static readonly List<string> _reusableKeyBuf = new List<string>(128);
// ⭐ 预分配索引 HashSet,避免每次 new HashSet
private static readonly HashSet<int> _reusableIdxSet = new HashSet<int>(64);
// ⭐ 缓存 uiRoot.Canvas,避免每次杀鱼特效都 GetComponent<Canvas>()(5处复用)
private static Canvas _cachedUiCanvas;
private static Canvas GetUiCanvas()
{
if (_cachedUiCanvas == null && UIManager.Instance != null && UIManager.Instance.uiRoot != null)
_cachedUiCanvas = UIManager.Instance.uiRoot.GetComponent<Canvas>();
return _cachedUiCanvas;
}
/// <summary>
/// 从 KeyCollection 中随机取 count 个不重复的 string key。
/// ⭐ 优化:复用静态 Random + 预分配缓冲,零 GC 分配。
/// 注意:返回的 List 是内部复用对象,调用方用完前不要再次调用本方法。
/// </summary>
public static List<string> RandomUniqueNumbers(int count, int min, int max,
Dictionary<string, List<RoomFishDetailConfig>>.KeyCollection keyCollection)
{
_reusableStrResult.Clear();
if (min >= max) return _reusableStrResult;
count = Mathf.Min(count, max - min);
// ⭐ 用 foreach 拷贝到复用缓冲,避免 keyCollection.ToList() 的 GC
_reusableKeyBuf.Clear();
foreach (var k in keyCollection) _reusableKeyBuf.Add(k);
_reusableIdxSet.Clear();
while (_reusableIdxSet.Count < count)
{
int idx = _sharedRand.Next(min, max);
if (_reusableIdxSet.Add(idx))
_reusableStrResult.Add(_reusableKeyBuf[idx]);
}
return _reusableStrResult;
}
#endregion
#region 杀鱼
// ================================================================
// 杀鱼接口
// ================================================================
/// <summary>
/// 批量杀死多条鱼(带特效 + 不足数量用纯金币补齐)。
/// </summary>
/// <param name="guids">实际场上能找到的鱼 guid 列表(可能少于 count)</param>
/// <param name="coinNum">红包拆分后的每份金额,长度应 >= count</param>
/// <param name="v3Point">金币最终飞回的世界坐标(一般是炮台位置)</param>
/// <param name="count">理论上要“死”的总数量(鱼 + 纯金币)</param>
public void Id_DieListFish_FX(List<string> guids, List<RedPacketRecord> coinNum, Vector3 v3Point, int count, bool isLocal = false)
{
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, isLocal);
}
// 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, isLocal);
}
}
/// <summary>
/// 随机选取 count 条小鱼/黄金鱼,通知服务器同步死亡,并返回 guid 列表。
/// </summary>
/// <param name="killerSitPos">击杀者座位 ID。Boss 爆奖连锁杀鱼时传 Boss 击杀者座位
/// (含人机);-1 表示用本地 currSitData.posID(普通玩家直接击杀场景).</param>
public List<string> SyncGameData_SendRendromDieFishID(
int count, List<RedPacketRecord> coinNum, Vector3 v3Point, int killerSitPos = -1)
{
var guids = new List<string>(count);
// ⭐ 优化:用 Fisher-Yates 洗牌索引代替 Keys.ToList()+RemoveAt,零 GC
// 将 keys 拷贝到复用缓冲
_reusableKeyBuf.Clear();
foreach (var k in fishDatas_hashCodekey_Small.Keys) _reusableKeyBuf.Add(k);
int keyCount = _reusableKeyBuf.Count;
if (keyCount <= count)
{
// 若存活鱼不足 count,全部选取
for (int i = 0; i < keyCount; i++) guids.Add(_reusableKeyBuf[i]);
}
else
{
// ⭐ Fisher-Yates 洗牌:只洗前 count 个位置,避免全量洗牌
for (int i = 0; i < count; i++)
{
int j = Random.Range(i, keyCount);
// 交换 i 和 j
var tmp = _reusableKeyBuf[i];
_reusableKeyBuf[i] = _reusableKeyBuf[j];
_reusableKeyBuf[j] = tmp;
guids.Add(_reusableKeyBuf[i]);
}
}
// ⭐ sitPos 用击杀者座位(含人机);房主代人机击杀 Boss 时,这里传人机座位,
// 否则所有客户端都会判定为房主击杀 → 房主自己看金色(错)
// ⭐ 座位镜像:网络帧里的 sitPos 必须是服务端座位号,显示座位转回服务端空间
int posID = SeatMirror.ToServer(killerSitPos >= 0 ? killerSitPos : 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;
}
/// <summary>
/// 发送"随机爆金币不死鱼"帧:guids 为空列表,所有客户端只播屏幕随机位置的金币特效,
/// 场上鱼不死亡(Id_DieListFish_FX 中 guids 为空 → 全部走 SpawnRandomCoinFX)。
/// </summary>
/// <param name="coinNum">红包分包记录(每条对应一个金币特效)</param>
/// <param name="v3Point">爆奖中心(金币特效的基准位置)</param>
/// <param name="killerSitPos">击杀者座位 ID(-1 = 本地座位)</param>
public void SyncGameData_SendRandomCoinFX(List<RedPacketRecord> coinNum, Vector3 v3Point, int killerSitPos = -1)
{
if (coinNum == null || coinNum.Count == 0) return;
// ⭐ 座位镜像:网络帧里的 sitPos 必须是服务端座位号,显示座位转回服务端空间
int posID = SeatMirror.ToServer(killerSitPos >= 0 ? killerSitPos : 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 = new List<string>(), // ⭐ 空 = 不关联场上鱼,金币全走 SpawnRandomCoinFX
coinNum = coinNum,
count = coinNum.Count
}
};
WebSocketSendList.SyncGameData(sgd);
}
/// <summary>
/// 清除所有座位对此鱼的锁定引用(鱼死亡/离场时调用,避免炮台继续对着死鱼/已回收鱼空转)。
/// </summary>
private void ClearLockOnFish(string fishId)
{
if (currGameType == null || currGameType.list_Sits == null) return;
foreach (var sit in currGameType.list_Sits)
{
if (sit.monCotl_Lock != null && sit.monCotl_Lock.guid == fishId)
{
// ⭐ 必须完整清锁(ClearLock 置 isLock=false/隐藏锁定圈/重置 isManualLock/炮台回Idle),
// 不能只清两个引用:isLock 残留 true 会让 GameMainPanel_FishBase.HandleAutoRotate
// 对空 monCotl_Lock_Position 取 .position 每帧抛 NRE,
// 排在其后的 CheckLockTargetValid/HandleAutoLock 永远执行不到 → 锁定系统卡死
sit.ClearLock();
}
}
}
/// <summary>
/// 鱼离场
/// </summary>
public void Id_DieFish_NotFX(string fishId)
{
if (!fishDatas_hashCodekey.TryGetValue(fishId, out var monster))
{
#if UNITY_EDITOR
// ⭐ 优化:调试日志用 #if UNITY_EDITOR 包裹,Release 包零 GC
// 原代码 Where().Take().ToList() + string.Join + $"" 每次 NotFound 都产生大量 GC
Debug.LogWarning($"[CreateFishManager] Id_DieFish_NotFX 找不到鱼: fishId={fishId}, 字典数量={fishDatas_hashCodekey.Count}");
string baseGuid = fishId.Contains("_") ? fishId.Split('_')[0] : fishId;
int matchCount = 0;
foreach (var k in fishDatas_hashCodekey.Keys)
{
if (k.StartsWith(baseGuid))
{
matchCount++;
if (matchCount <= 10) GameManager.LogFishFlow("NOT_FOUND", fishId, $"similarKey={k}");
}
}
if (matchCount == 0) GameManager.LogFishFlow("NOT_FOUND", fishId, "(无相似key)");
#else
GameManager.LogFishFlow("NOT_FOUND", fishId, fishDatas_hashCodekey.Count.ToString());
#endif
return;
}
// ⭐ 标记死亡:离场回收的鱼同样不可被命中/锁定(回池 OnEnable 会重置)
monster.isDead = true;
// ⭐ 清除座位锁定引用:炮台不再对着已离场的鱼
ClearLockOnFish(fishId);
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);
// ⭐ 记录成功回收日志
GameManager.LogFishFlow("REMOVE", fishId,
$"fishNumber={monster.monsterData?.fishTypeConfig?.fishNumber} isyz={monster.isyz} yzID={monster.yzID} dictCount={fishDatas_hashCodekey.Count}",
monster.GetInstanceID());
#if UNITY_EDITOR
// ⭐ 同步移除调试 List
fishList_All.Remove(monster);
switch (monster.fishenum)
{
case FishTypeEnum.普通鱼: fishList_Small.Remove(monster); break;
case FishTypeEnum.Boss鱼: fishList_Boss.Remove(monster); break;
case FishTypeEnum.黄金鱼: fishList_HJ.Remove(monster); break;
case FishTypeEnum.特殊鱼: fishList_TS.Remove(monster); break;
case FishTypeEnum.特殊Boss: fishList_TS_Boss.Remove(monster); break;
}
#endif
}
/// <summary>
/// 按 ID 杀死一条鱼(带特效 + 金币飞出动画)。
/// </summary>
public void Id_DieFish_FX(string fishId, RedPacketRecord coinNum, Vector3 v3Point, bool isLocal = false)
{
if (!fishDatas_hashCodekey.TryGetValue(fishId, out var monster))
{
// ⭐ 记录找不到鱼日志(带特效击杀路径)
GameManager.LogFishFlow("NOT_FOUND", fishId, $"路径=Id_DieFish_FX dictCount={fishDatas_hashCodekey.Count}");
return;
}
// ⭐ 记录带特效击杀日志
GameManager.LogFishFlow("KILL_FX", fishId,
$"fishNumber={monster.monsterData?.fishTypeConfig?.fishNumber} isyz={monster.isyz} yzID={monster.yzID} dictCount={fishDatas_hashCodekey.Count}",
monster.GetInstanceID());
// ⭐ 统一标记死亡:死亡后子弹不再命中、不再被锁定
monster.isDead = true;
// ⭐ 清除座位锁定引用:炮台不再对着死亡的鱼
ClearLockOnFish(fishId);
// 👁 调试:记录每架掉金币飞机的实际坐标,便于对比"其他位置掉金币"来源
Debug.Log($"[掉金币坐标] fishId={fishId} 飞机坐标={monster.transform.position} 金币飞向={v3Point} isLocal={isLocal} 调用栈={System.Environment.StackTrace}");
if (false)
{
AudioManager.Instance.PlayerFX(60004);
}
// 死亡粒子特效(池化,粒子播完自动回池)
EffectPool.Instance.Get(
monster.effect_Die != null ? monster.effect_Die : dieFXPrefab,
monster.transform.position, Quaternion.identity, transform.parent);
// 非 Boss 鱼:金币飞向炮台
if (monster.fishenum != FishTypeEnum.Boss鱼 && monster.fishenum != FishTypeEnum.特殊Boss)
{
// ⭐ 池化:动画事件(coinEvent/ScoreNumScrollEvent)播完手动回池
var award = EffectPool.Instance.Get(
monster.effect_DieAward != null ? monster.effect_DieAward : dieFXPrefabAward,
monster.transform.position, Quaternion.identity, currGameType.subPanelParent.parent,
autoReturn: false);
award.GetComponent<RectTransform>().anchoredPosition =
CoordinateTransformation.WorldPos_ToCanvas(
monster.transform.position,
GetUiCanvas());
var coinScript = award.GetComponent<BJBase>();
if (coinScript != null)
{
// 普通爆奖(BJBase 体系)
coinScript.Init((int)coinNum.Amount, v3Point);
coinScript.isBoss = true;
coinScript.isLocalCoin = isLocal;
coinScript.SetCoinCount();
}
else
{
// 特殊鱼爆奖(DieEffect_TSY 体系:小鱼/Boss 模式互通)
var tsy = award.GetComponent<DieEffect_TSY>();
if (tsy != null)
{
// ⭐ 闪电起点=鱼死亡位置(3D 世界坐标)。award 挂 UI 画布下,其 position 是画布坐标(≈屏幕中心)不可用
tsy.startPoint = monster.transform.position;
tsy.isLocalCoin = isLocal;
tsy.monster = monster;
tsy.coinCount = coinNum.Amount;
if (tsy.baoJiangMode == TSYBaoJiangMode.Boss爆奖)
{
var sit = currGameType.currSitData;
tsy.InitBossAward((int)coinNum.Amount,
sit != null ? sit.posID : 0,
sit != null ? (int)sit.consumeGoldNum : 1,
monster.monsterData.fishTypeConfig.fishScore, 1, null);
}
else
{
// ⭐ 小鱼模式也调 Init:注入参数 + 缓存 _targetSit(座位)+ JiaFen,
// 否则依赖 Init 的效果(ShanDian/ZuanTouXie 等查座位/结算)触发不到
var sit = currGameType.currSitData;
tsy.Init(coinNum.Amount,
sit != null ? sit.posID : 0,
sit != null ? sit.consumeGoldNum : 1,
monster.monsterData.fishTypeConfig.fishScore, 1, null);
tsy.startPoint = monster.transform.position; // Init 的 ApplySeatOffset 会挪位置,重注入
tsy.monster = monster;
tsy.coinCount = coinNum.Amount;
}
tsy.SetCoinCount();
}
}
}
//MonsterDieList.Instance.SmallHuangYiHuang(
// monster.transform.position,
// monster.monsterData.fishTypeConfig.fishNumber, 1000);
monster.DieFish();
fishDatas_hashCodekey.Remove(fishId);
#if UNITY_EDITOR
// ⭐ 同步移除调试 List
fishList_All.Remove(monster);
switch (monster.fishenum)
{
case FishTypeEnum.普通鱼: fishList_Small.Remove(monster); break;
case FishTypeEnum.Boss鱼: fishList_Boss.Remove(monster); break;
case FishTypeEnum.黄金鱼: fishList_HJ.Remove(monster); break;
case FishTypeEnum.特殊鱼: fishList_TS.Remove(monster); break;
case FishTypeEnum.特殊Boss: fishList_TS_Boss.Remove(monster); break;
}
#endif
}
/// <summary>
/// 不依赖鱼,只在屏幕内随机一个位置生成金币特效,然后飞回指定点。
/// </summary>
public void SpawnRandomCoinFX(RedPacketRecord coinNum, Vector3 targetWorldPos, bool isLocal = false)
{
// 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,
GetUiCanvas());
// 3. 实例化金币特效(池化,动画事件播完手动回池)
var award = EffectPool.Instance.Get(
dieFXPrefabAward,
targetWorldPos, Quaternion.identity, currGameType.subPanelParent.parent,
autoReturn: false);
var rect = award.GetComponent<RectTransform>();
rect.anchoredPosition = canvasPos;
// 4. 设置 CoinScript 的终点为 targetWorldPos
var coinScript = award.GetComponent<CoinScript>();
coinScript.Init((int)coinNum.Amount, targetWorldPos);
coinScript.isBoss = true;
coinScript.isLocalCoin = isLocal;
coinScript.SetCoinCount();
}
/// <summary>
/// 收到服务器 WebSocket 杀鱼通知,处理金币加分、特效、音效。
/// </summary>
public void Ws_DieFish(string msg)
{
var killFish = JsonConvert.DeserializeObject<WebSocketResponseBase<RoomKillNotice>>(msg)
.arguments[0];
NetLog.Log($"[CreateFishManager] 杀鱼延迟:{DateTimeOffset.Now.ToUnixTimeSeconds() - killFish.timespan}s");
SitPosData sitData = null; //击杀玩家座位数据
foreach (var item in currGameType.list_Sits)
{
if (item.user == killFish.userId)
{
//item.lockFX.gameObject.SetActive(false);
sitData = item;
}
}
// ⭐ 死鱼清除所有座位的锁定引用(原逻辑只清击杀者座位,其他座位锁同一条鱼时不清)
ClearLockOnFish(killFish.fishId);
if (!fishDatas_hashCodekey.TryGetValue(killFish.fishId, out var monster)) return;
// ⭐ 统一标记死亡:死亡后子弹不再命中、不再被锁定(含不调 base.DieFish 的 Boss 子类)
monster.isDead = true;
// 死亡粒子特效(池化,粒子播完自动回池)
EffectPool.Instance.Get(
monster.effect_Die != null ? monster.effect_Die : dieFXPrefab,
monster.transform.position, Quaternion.identity, transform.parent);
long goldToAdd = killFish.gold; // 立即复制需要的数据
string fishId = killFish.fishId; // 如果需要的话
Action<int> onScoreAdd = (audioID) =>
{
long oldGold = sitData.goldNum; // 记录加金币前的值
sitData.goldNum += goldToAdd; // 使用副本,不再依赖 killFish 对象
sitData.tMP_GoldNum.text = sitData.goldNum.ToString();
currGameType?.ShowGoldColumn(sitData, goldToAdd); // 座位金币柱(XJMH 覆写,其它房间空实现)
AudioManager.Instance.PlayerFX(audioID);
// ⭐ 记录金币流水:已加钱(杀鱼获得,本地已执行)
GameManager.LogGoldFlowAppliedGain("KillFish", $"击中鱼原有金币={oldGold} 增加金币=+{goldToAdd} 剩余金币={sitData.goldNum} 鱼ID={fishId} 座位={sitData.posID} 房间={GameManager.Instance.gameData?.currRoomData?.roomId}");
};
//// 加分回调(在金币/加分面板飞完后执行)
//Action<int> onScoreAdd = (audioID) =>
//{
// var sitList = GetCurrentPanelSitList();
// sitData.goldNum += killFish.gold;
// sitData.tMP_GoldNum.text = sitData.goldNum.ToString();
// AudioManager.Instance.PlayerFX(audioID);
//};
bool dieFishCalled = false; // Boss case 内部已调用 DieFish,底部跳过
switch (monster.fishenum)
{
case FishTypeEnum.普通鱼:
{
// 普通鱼:金币飞向炮台(池化,动画事件播完手动回池)
var award = EffectPool.Instance.Get(
monster.effect_DieAward != null ? monster.effect_DieAward : dieFXPrefabAward,
monster.transform.position, Quaternion.identity, currGameType.subPanelParent.parent,
autoReturn: false);
try
{
award.GetComponent<RectTransform>().anchoredPosition =
CoordinateTransformation.WorldPos_ToCanvas(
monster.transform.position,
GetUiCanvas());
}
catch
{
award.transform.position = monster.transform.position;
}
var coinScript = award.GetComponent<BJBase>();
coinScript.Init(killFish.gold, sitData.transform.position, killFish.userId, sitData.posID, killFish.param);
coinScript.isLocalCoin = sitData.user == GameManager.Instance.user.id;
coinScript.isBoss = false;
coinScript.JiaFen = onScoreAdd;
coinScript.SetCoinCount();
break;
}
case FishTypeEnum.Boss鱼:
{
// ⭐ 防御:配置了 Boss鱼 类型但预制体没挂 MonsterControl_BossBase(新鱼/配错)
// -> 按普通鱼金币流程处理,避免 InvalidCastException 崩掉杀鱼消息
var bossCtrl = monster as MonsterControl_BossBase;
if (bossCtrl == null)
{
Debug.LogWarning($"[Ws_DieFish] Boss鱼 未挂 MonsterControl_BossBase: fishId={killFish.fishId} 实际脚本={monster.GetType().Name},按普通鱼流程结算");
var award = EffectPool.Instance.Get(
monster.effect_DieAward != null ? monster.effect_DieAward : dieFXPrefabAward,
monster.transform.position, Quaternion.identity, currGameType.subPanelParent.parent,
autoReturn: false);
try
{
award.GetComponent<RectTransform>().anchoredPosition =
CoordinateTransformation.WorldPos_ToCanvas(monster.transform.position, GetUiCanvas());
}
catch
{
award.transform.position = monster.transform.position;
}
var coinScript = award.GetComponent<BJBase>();
coinScript.Init(killFish.gold, sitData.transform.position, killFish.userId, sitData.posID, killFish.param);
coinScript.isLocalCoin = sitData.user == GameManager.Instance.user.id;
coinScript.isBoss = false;
coinScript.JiaFen = onScoreAdd;
coinScript.SetCoinCount();
monster.DieFish(killFish);
dieFishCalled = true;
break;
}
// Boss 鱼:显示浮动分数文本
// ⭐ not-self 分流:非自己击杀(含机器人)且开启新模式 → 走 not-self 流程
// 不克隆 effect_DieAward,在 killer 座位显示 img_BaoJiangZhong,
// 等待帧同步 baoJiangEnd(机器人由房主延迟 5~10 秒代发)后显示最终 BJEndUI + 加分
bool isSelfKill_Boss = sitData.user == GameManager.Instance.user.id;
if (GameManager.Instance.useNotSelfBaoJiangMode
&& !isSelfKill_Boss
&& monster.effect_DieAward != null
&& BaoJiangNotSelfHandler.ShowInProgress(
sitData,
monster.effect_DieAward,
killFish.fishId,
killFish.userId,
killFish.beishu,
gold: killFish.gold,
onScoreAdd: onScoreAdd))
{
// Boss 死亡仍走 DieFish(处理死亡音效/震屏等),但不播爆奖特效
monster.DieFish(killFish);
dieFishCalled = true;
break;
}
bossCtrl.v3 = sitData.transform.position;
bossCtrl.JiaFen = WrapJiaFenWithBaoJiangEnd(onScoreAdd, killFish, sitData, isSelfKill_Boss);
bossCtrl.isLocalCoin = sitData.user == GameManager.Instance.user.id; // ⭐ 金/银区分
bossCtrl.killerSitPos = sitData.posID; // ⭐ 爆奖杀小鱼用击杀者座位(含人机)
monster.DieFish(killFish); // Boss 重写 action_effect_DieAward,捕获上面的 JiaFen
dieFishCalled = true;
if (bossCtrl.isToUI)
{
// ⭐ 池化:UI 空间 Boss 爆奖,动画事件播完手动回池
// ⭐ 不设置位置:位置/大小用爆奖预制体里配置好的(z 也用预制体自带的)
bossCtrl.currEffect_DieAward = EffectPool.Instance.Get(
monster.effect_DieAward != null ? monster.effect_DieAward : dieFXPrefabAwardTmp,
currGameType.subPanelParent.parent,
autoReturn: false);
}
else
{
// isToUI=false:世界空间特效,无 UI 父对象 → 挂 bjEffectRoot 统一管理(池化)
bossCtrl.currEffect_DieAward = EffectPool.Instance.Get(
monster.effect_DieAward != null ? monster.effect_DieAward : dieFXPrefabAwardTmp,
bjEffectRoot,
autoReturn: false);
}
bossCtrl.action_effect_DieAward?.Invoke(bossCtrl.currEffect_DieAward);
}
break;
case FishTypeEnum.黄金鱼:
{
// 黄金鱼:金币飞向炮台(池化,动画事件播完手动回池)
var award = EffectPool.Instance.Get(
monster.effect_DieAward != null ? monster.effect_DieAward : dieFXPrefabAward,
monster.transform.position, Quaternion.identity, currGameType.subPanelParent.parent,
autoReturn: false);
try
{
award.GetComponent<RectTransform>().anchoredPosition =
CoordinateTransformation.WorldPos_ToCanvas(
monster.transform.position,
GetUiCanvas());
}
catch
{
award.transform.position = monster.transform.position;
}
var coinScript = award.GetComponent<BJBase>();
coinScript.Init(killFish.gold, sitData.transform.position, killFish.userId, sitData.posID, killFish.param);
coinScript.isLocalCoin = sitData.user == GameManager.Instance.user.id;
coinScript.isBoss = false;
coinScript.JiaFen = onScoreAdd;
coinScript.SetCoinCount();
}
break;
case FishTypeEnum.特殊鱼:
{
// ⭐ not-self + Boss爆奖模式分流:不克隆 effect_DieAward,只读取预制体上的 BJEndUI
// 在 killer 座位 obj_BaoJiangZhongPanelPosition 显示 img_BaoJiangZhong(关 tMP_beishu)
// 等待帧同步 baoJiangEnd 后再走最终 BJEndUI 效果 + 加分
// 开关:GameManager.useNotSelfBaoJiangMode(false=旧模式,所有玩家走相同流程)
// ⚠️ 排除重写了 SetCoinCount 的子类(如炸弹蟹 HHWK_ZDX_DieEvent):
// 这些鱼有特殊爆炸/范围杀鱼玩法,必须走原流程,不能跳过 SetCoinCount
bool isSelfKill_TSY = sitData.user == GameManager.Instance.user.id;
if (GameManager.Instance.useNotSelfBaoJiangMode
&& !isSelfKill_TSY
&& monster.effect_DieAward != null)
{
var tsyPrefab = monster.effect_DieAward.GetComponent<DieEffect_TSY>();
if (tsyPrefab != null && tsyPrefab.baoJiangMode == TSYBaoJiangMode.Boss爆奖)
{
// 检查子类是否重写了 SetCoinCount(炸弹蟹等有特殊爆炸玩法的鱼不走 not-self 分流)
var setCoinMethod = tsyPrefab.GetType().GetMethod("SetCoinCount");
bool hasCustomSetCoinCount = setCoinMethod != null
&& setCoinMethod.DeclaringType != typeof(DieEffect_TSY);
if (!hasCustomSetCoinCount
&& BaoJiangNotSelfHandler.ShowInProgress(
sitData,
monster.effect_DieAward,
killFish.fishId,
killFish.userId,
killFish.beishu,
gold: killFish.gold,
onScoreAdd: onScoreAdd))
{
break;
}
}
}
// 特殊鱼:金币飞向炮台(池化,动画事件播完手动回池)
// ⭐ Get 不设置位置:DieEffect_TSY(ZDX 炸弹蟹等)爆奖完全用预制体里配置好的位置(含 y 轴);
// 只有 BJBase 金币特效按鱼死亡位置摆放(startPoint 依赖)
var award = EffectPool.Instance.Get(
monster.effect_DieAward != null ? monster.effect_DieAward : dieFXPrefabAward,
currGameType.subPanelParent.parent,
autoReturn: false);
var coinScript = award.GetComponent<BJBase>();
if (coinScript != null)
{
// 普通爆奖(BJBase 体系):摆到鱼死亡位置(画布坐标),startPoint 依赖
try
{
award.GetComponent<RectTransform>().anchoredPosition =
CoordinateTransformation.WorldPos_ToCanvas(
monster.transform.position,
GetUiCanvas());
}
catch
{
award.transform.position = monster.transform.position;
}
coinScript.startPoint = award.transform.position;
coinScript.Init(killFish.gold, sitData.transform.position, killFish.userId, sitData.posID, killFish.param);
coinScript.isLocalCoin = sitData.user == GameManager.Instance.user.id;
coinScript.isBoss = false;
coinScript.JiaFen = onScoreAdd;
coinScript.SetCoinCount();
}
else
{
// 特殊鱼爆奖(DieEffect_TSY 体系:小鱼/Boss 模式互通)
var tsy = award.GetComponent<DieEffect_TSY>();
if (tsy != null)
{
// ⭐ 闪电起点=鱼死亡位置(3D 世界坐标)。award 挂 UI 画布下,其 position 是画布坐标(≈屏幕中心)不可用
tsy.startPoint = monster.transform.position;
tsy.isLocalCoin = sitData.user == GameManager.Instance.user.id;
tsy.monster = monster;
tsy.coinCount = killFish.gold;
// 统一注入击杀者座位信息(posID/paoBei 供 GetSeatPosition/范围杀鱼金币计算使用)
tsy.posID = sitData.posID;
tsy.paoBei = (int)sitData.consumeGoldNum;
// 倍数:服务端返回的爆奖倍数(小鱼模式 BJEndUI 结算显示用;未下发时保持默认 1)
tsy.bJbeishu = killFish.beishu > 0 ? killFish.beishu : 1;
if (tsy.baoJiangMode == TSYBaoJiangMode.Boss爆奖)
{
tsy.InitBossAward(killFish.gold, sitData.posID,
(int)sitData.consumeGoldNum,
monster.monsterData.fishTypeConfig.fishScore, 1, onScoreAdd);
}
else
{
// ⭐ 小鱼模式也调 Init:注入参数 + 缓存 _targetSit(座位)+ JiaFen
tsy.Init(killFish.gold, sitData.posID,
(int)sitData.consumeGoldNum,
monster.monsterData.fishTypeConfig.fishScore,
killFish.beishu > 0 ? killFish.beishu : 1, onScoreAdd);
tsy.JiaFenEx = onScoreAdd;
// Init 的 ApplySeatOffset 会挪位置/清状态,重注入小鱼模式专用字段
tsy.startPoint = monster.transform.position;
tsy.isLocalCoin = sitData.user == GameManager.Instance.user.id;
tsy.monster = monster;
tsy.coinCount = killFish.gold;
tsy.posID = sitData.posID;
tsy.paoBei = (int)sitData.consumeGoldNum;
tsy.bJbeishu = killFish.beishu > 0 ? killFish.beishu : 1;
}
tsy.SetCoinCount();
}
}
// // Boss 鱼:显示浮动分数文本
// var bossCtrl = (MonsterControl_BossBase)monster;
// bossCtrl.v3 = sitData.transform.position;
// if (bossCtrl.isToUI)
// {
// bossCtrl.currEffect_DieAward = Instantiate(
//monster.effect_DieAward != null ? monster.effect_DieAward : dieFXPrefabAwardTmp,
//currGameType.subPanelParent.parent);
// }
// else
// {
// bossCtrl.currEffect_DieAward = Instantiate(
//monster.effect_DieAward != null ? monster.effect_DieAward : dieFXPrefabAwardTmp);
// }
// bossCtrl.action_effect_DieAward?.Invoke(bossCtrl.currEffect_DieAward);
// bossCtrl.JiaFen = onScoreAdd;
}
break;
case FishTypeEnum.特殊Boss:
{
// Boss 鱼:显示浮动分数文本
var bossCtrl = (MonsterControl_BossBase)monster;
// ⭐ not-self 分流:非自己击杀(含机器人)且开启新模式 → 走 not-self 流程
bool isSelfKill_TBoss = sitData.user == GameManager.Instance.user.id;
if (GameManager.Instance.useNotSelfBaoJiangMode
&& !isSelfKill_TBoss
&& monster.effect_DieAward != null
&& BaoJiangNotSelfHandler.ShowInProgress(
sitData,
monster.effect_DieAward,
killFish.fishId,
killFish.userId,
killFish.beishu,
gold: killFish.gold,
onScoreAdd: onScoreAdd))
{
// Boss 死亡仍走 DieFish(处理死亡音效/震屏等),但不播爆奖特效
monster.DieFish(killFish);
dieFishCalled = true;
break;
}
bossCtrl.v3 = sitData.transform.position;
bossCtrl.JiaFen = WrapJiaFenWithBaoJiangEnd(onScoreAdd, killFish, sitData, isSelfKill_TBoss);
bossCtrl.isLocalCoin = sitData.user == GameManager.Instance.user.id; // ⭐ 金/银区分
bossCtrl.killerSitPos = sitData.posID; // ⭐ 爆奖杀小鱼用击杀者座位(含人机)
monster.DieFish(killFish); // Boss 重写 action_effect_DieAward
dieFishCalled = true;
if (bossCtrl.isToUI)
{
// ⭐ 池化:UI 空间 Boss 爆奖,动画事件播完手动回池
// ⭐ 不设置位置:位置/大小用爆奖预制体里配置好的(z 也用预制体自带的)
bossCtrl.currEffect_DieAward = EffectPool.Instance.Get(
monster.effect_DieAward != null ? monster.effect_DieAward : dieFXPrefabAwardTmp,
currGameType.subPanelParent.parent,
autoReturn: false);
}
else
{
// isToUI=false:世界空间特效,无 UI 父对象 → 挂 bjEffectRoot 统一管理(池化)
bossCtrl.currEffect_DieAward = EffectPool.Instance.Get(
monster.effect_DieAward != null ? monster.effect_DieAward : dieFXPrefabAwardTmp,
bjEffectRoot,
autoReturn: false);
}
bossCtrl.action_effect_DieAward?.Invoke(bossCtrl.currEffect_DieAward);
}
break;
default:
break;
}
if (!dieFishCalled)
monster.DieFish(killFish);
MonsterDieList.Instance.SmallHuangYiHuang(
monster.transform.position,
monster.monsterData.fishTypeConfig.fishNumber, 1000);
}
#endregion
// ================================================================
// 工具:包装 JiaFen 回调,self 流程加分完成后发送 baoJiangEnd 帧同步
// ================================================================
/// <summary>
/// 包装 onScoreAdd:先执行原加分,再在 self+not-self 模式开启时发送 baoJiangEnd 帧同步。
/// 这样无论子类走 OnScoreEnd(雷龙)还是直接调 JiaFen(金龙),都能触发帧同步。
/// BJEventBase.Init 会把这个包装再包一层 `(p) => action?.Invoke(p)`,调用链不变。
/// </summary>
private System.Action<int> WrapJiaFenWithBaoJiangEnd(System.Action<int> onScoreAdd, RoomKillNotice killFish, SitPosData sitData, bool isSelfKill)
{
return (p) =>
{
// 1. 执行原加分逻辑
onScoreAdd?.Invoke(p);
// 2. self 流程:本地玩家击杀且开启 not-self 模式时,发送 baoJiangEnd 帧同步
// (机器人击杀由房主延迟代发,不在这里发)
if (isSelfKill
&& GameManager.Instance != null
&& GameManager.Instance.useNotSelfBaoJiangMode)
{
BaoJiangNotSelfHandler.SendBaoJiangEnd(
killFish.userId,
killFish.fishId,
killFish.beishu,
killFish.gold,
sitData.posID);
}
};
}
// ================================================================
// 工具:获取当前面板座位列表
// ================================================================
/// <summary>
/// 根据当前游戏类型返回对应 UI 面板的座位列表。
/// </summary>
private List<SitPosData> GetCurrentPanelSitList()
{
switch (GameManager.Instance.gameData.currGameType)
{
case GameType.defaultPanel:
return UIManager.Instance.Get<GameMainPanel_FishBase>(UIPanel.MainGamePanelDefault).list_Sits;
case GameType.FLZY:
return UIManager.Instance.Get<GameMainPanel_FishBase>(UIPanel.MainGamePanelFLZY).list_Sits;
case GameType.YYSC:
return UIManager.Instance.Get<GameMainPanel_FishBase>(UIPanel.MainGamePanelYYSC).list_Sits;
case GameType.XJMH:
return UIManager.Instance.Get<GameMainPanel_FishBase>(UIPanel.MainGamePanelXJMH).list_Sits;
case GameType.HHWK:
return UIManager.Instance.Get<GameMainPanel_FishBase>(UIPanel.MainGamePanelHHWK).list_Sits;
default:
return null;
}
}
#region 冰冻技能
/// <summary>
/// 复位冰冻状态(退房/进房时调用):
/// 清除 isFrozen 与冰冻记录,并停掉所有出鱼协程。
/// ⭐ 若不复位:冰冻中切换房间时,旧面板的 15s 解冻倒计时随面板一起销毁,
/// 永远不会调用 UnfreezeAllFish → isFrozen 残留 true →
/// 新房间 OnFishSpawn 第一行 `if (isFrozen) return;` 把服务端所有出鱼全部拦掉 → 新房间不出鱼。
/// ⭐ StopAllCoroutines:分帧出鱼协程里的 `while (isFrozen) yield return null;` 会一直挂起,
/// 解冻复位后会继续把旧房间排队的鱼生成到新房间,必须一并停掉。
/// </summary>
public void ResetFreezeState()
{
isFrozen = false;
frozenFishStates.Clear();
frozenFishIsMoves.Clear();
StopAllCoroutines();
}
/// <summary>
/// 冰冻:记录所有鱼当前状态并强制切到 Idle(暂停移动+动画)
/// </summary>
public void FreezeAllFish()
{
// ⭐ 守卫:已冻结时直接返回,避免重复调用覆盖已记录的状态
// (其他客户端会先后被 OnUsePropNotice 和 SyncGameData_FreezeFish 触发两次)
if (isFrozen) return;
isFrozen = true;
// ⭐ 两个字典必须同步清空,否则 frozenFishIsMoves 残留旧数据
// 会导致下次解冻时用旧值覆盖 isMove 但不恢复 currState
frozenFishStates.Clear();
frozenFishIsMoves.Clear();
// 只遍历全量字典 fishDatas_hashCodekey(已包含所有鱼),不重复遍历子字典
if (fishDatas_hashCodekey != null)
{
foreach (var kv in fishDatas_hashCodekey)
{
MonsterContral m = kv.Value;
if (m == null || !m.gameObject.activeInHierarchy) continue;
// 记录原状态
frozenFishStates[m.guid] = m.currState;
frozenFishIsMoves[m.guid] = m.isMove;
// 定格
m.isMove = false;
m.ChangeState(CharacterStateType.idle);
m.SetFreezeEffect(true);
}
}
}
/// <summary>
/// 解冻:恢复鱼到冰冻前状态
/// </summary>
public void UnfreezeAllFish()
{
isFrozen = false;
if (fishDatas_hashCodekey != null)
{
foreach (var kv in fishDatas_hashCodekey)
{
MonsterContral m = kv.Value;
if (m == null || !m.gameObject.activeInHierarchy) continue;
m.SetFreezeEffect(false);
if (frozenFishStates.TryGetValue(m.guid, out var prevState))
{
// 被本次冰冻冻结的鱼:恢复冰冻前的 isMove 和 currState
m.isMove = frozenFishIsMoves.TryGetValue(m.guid, out var prevIsMove)
? prevIsMove
: m.baseMmove;
m.ChangeState(prevState);
}
else
{
// ⭐ 不在冰冻记录里的鱼(冰冻期间新激活的,OnEnable 把 isMove 设成了 false)
// 用 baseMmove 恢复预制体默认状态,避免解冻后永远不动
m.isMove = m.baseMmove;
if (m.baseMmove)
m.ChangeState(CharacterStateType.move);
}
}
}
// ⭐ 两个字典同步清空
frozenFishStates.Clear();
frozenFishIsMoves.Clear();
}
#endregion
}
#if UNITY_EDITOR
[CustomEditor(typeof(CreateFishManager))]
public class CreateFishManagerEditor : Editor
{
private static readonly string[] FishNames = { "电鳗","吞噬鲲","玲珑宝塔","聚宝盆","金龙","神龙转盘","炸弹蟹","青龙","蟠桃树","激光蟹","钻头蟹","荧光水母","远古龙龟","深海灯笼鱼","深渊鳄鱼","禄存星君","苍穹之主","雷霆之主","人鱼公主","霸王蟹","哥斯拉" };
private static readonly string[] FishIds = { "11023","11024","11025","11026","11027","11028","11029","11030","11031","11032","11033","11034","11035","11036","11037","11038","11039","11040","11041","11042","11043" };
public override void OnInspectorGUI()
{
base.OnInspectorGUI();
var mgr = (CreateFishManager)target;
int index = Array.IndexOf(FishIds, mgr.debugSpawnFishNumber);
if (index < 0) index = 0;
EditorGUILayout.Space();
EditorGUI.BeginChangeCheck();
int newIndex = EditorGUILayout.Popup("调试出鱼(下拉选择)", index, FishNames);
if (EditorGUI.EndChangeCheck() && newIndex != index)
{
// 下拉框只负责给 debugSpawnFishNumber 赋值,出鱼逻辑只读这个字段
Undo.RecordObject(mgr, "Change Fish ID");
mgr.debugSpawnFishNumber = FishIds[newIndex];
EditorUtility.SetDirty(mgr);
}
}
}
#endif