Files
5.24BuYu/Assets/MyGame/Scripts/Manager/CreateFishManager.cs
T

2362 lines
103 KiB
C#
Raw Normal View History

2026-04-29 01:18:39 +08:00
// ============================================================
// 文件:CreateFishManager.cs
// 职责:捕鱼游戏 —— 鱼的生成 / 销毁 / 鱼阵管理核心管理器
// 优化点:
// 1. 消除 Update 中重复创建 List、字典查找性能问题
// 2. 统一鱼阵路线选取逻辑,减少冗余代码
// 3. FishSpawnTimerJob shouldSpawn 逻辑修正(原逻辑只在 ==0 时计时,现保持一致)
// 4. 增加必要的空检查与防御性编程
// 5. 清理无用注释与废弃代码块
// 6. 方法职责单一化拆分
// ============================================================
using Assets.MyGame.Scripts.Characters.Monster;
2026-03-27 15:18:35 +08:00
using Assets.MyGame.Scripts.Net;
2026-02-03 11:35:19 +08:00
using Assets.MyGame.Scripts.Net.API;
using Assets.MyGame.Scripts.Net.API.GameConfig_SubClass;
using Assets.MyGame.Scripts.Net.APIResponses.Enum;
2026-05-01 06:26:50 +08:00
using Assets.MyGame.Scripts.Net.APIResponses.GameConfig_SubClass;
2026-02-03 11:35:19 +08:00
using Assets.MyGame.Scripts.Net.WebResponses;
2026-02-09 10:44:38 +08:00
using cfg;
2026-02-03 11:35:19 +08:00
using Newtonsoft.Json;
using System;
2026-02-09 10:44:38 +08:00
using System.Collections;
2026-02-03 11:35:19 +08:00
using System.Collections.Generic;
2026-03-27 15:18:35 +08:00
using System.Linq;
2026-05-01 06:26:50 +08:00
using System.Threading.Tasks;
2026-04-25 00:12:55 +08:00
using TMPro;
2026-02-03 11:35:19 +08:00
using Unity.Collections;
using Unity.Jobs;
2026-05-04 17:21:52 +08:00
using UnityEditor;
2026-02-03 11:35:19 +08:00
using UnityEngine;
using Random = UnityEngine.Random;
2026-04-29 01:18:39 +08:00
/// <summary>
/// 捕鱼游戏核心管理器:负责鱼的定时生成、网络同步、阵型调度、死亡特效。
/// 继承 MonoSingleton 保证全局唯一。
/// </summary>
2026-02-03 11:35:19 +08:00
public class CreateFishManager : MonoSingleton<CreateFishManager>
{
2026-04-29 01:18:39 +08:00
// ----------------------------------------------------------------
// 外部依赖
// ----------------------------------------------------------------
2026-02-03 11:35:19 +08:00
2026-04-29 01:18:39 +08:00
/// <summary>当前游戏全局配置(通过 GameManager 获取)</summary>
2026-02-03 11:35:19 +08:00
public GameConfig M_GameConfig => GameManager.Instance.gameConfig;
2026-03-27 15:18:35 +08:00
2026-04-29 01:18:39 +08:00
// ----------------------------------------------------------------
// 配置缓存(空间换时间,避免 List.Find O(n) 查找)
// ----------------------------------------------------------------
/// <summary>roomFishConfigs 按游戏类型索引</summary>
2026-02-03 11:35:19 +08:00
private Dictionary<GameTypeEnum, RoomFishConfig> roomFishConfig;
2026-04-29 01:18:39 +08:00
/// <summary>每个游戏类型下 fishNumber → RoomFishDetailConfig</summary>
2026-02-03 11:35:19 +08:00
private Dictionary<GameTypeEnum, Dictionary<string, RoomFishDetailConfig>> createDic;
2026-04-29 01:18:39 +08:00
/// <summary>fishNumber → FishTypeConfig(基础配置缓存)</summary>
2026-02-03 11:35:19 +08:00
private Dictionary<string, FishTypeConfig> fishDatas;
2026-04-29 01:18:39 +08:00
/// <summary>fishArrayNumber → FishArrayConfig(鱼阵配置缓存)</summary>
2026-04-03 18:32:19 +08:00
private Dictionary<string, FishArrayConfig> fishArrayConfigDic;
2026-04-29 01:18:39 +08:00
// ----------------------------------------------------------------
// UI 引用
// ----------------------------------------------------------------
/// <summary>当前游戏面板(用于坐标转换、金币飞行等)</summary>
2026-04-24 22:51:34 +08:00
public GameMainPanel_FishBase currGameType;
2026-04-03 18:32:19 +08:00
2026-04-29 01:18:39 +08:00
// ----------------------------------------------------------------
// 死亡特效 Prefab(Inspector 赋值)
// ----------------------------------------------------------------
public GameObject dieFXPrefab; // 基础死亡特效
public GameObject dieFXPrefabAward; // 普通鱼金币奖励特效
public GameObject dieFXPrefabAwardTmp; // Boss 分数浮动文本
public GameObject dieFXPrefabAwardTmp_AddScore;
// ----------------------------------------------------------------
// 生成系统运行时数据
// ----------------------------------------------------------------
/// <summary>鱼的父节点(保持场景层级整洁)</summary>
public Transform fishRoot;
2026-05-01 06:26:50 +08:00
/// <summary>鱼的父节点(保持场景层级整洁)</summary>
public Transform poolRoot;
2026-04-29 01:18:39 +08:00
2026-07-21 05:13:43 +08:00
/// <summary>
2026-07-25 21:04:58 +08:00
/// 非 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 卸载游戏主面板时自动清理。
/// EnterRoom 时创建挂在 UIManager.uiRoot 下,ExitRoom 时整节点销毁。
2026-07-21 05:13:43 +08:00
/// </summary>
public Transform bjEffectRoot;
2026-04-29 01:18:39 +08:00
/// <summary>当前房间可生成鱼列表(与 NativeArray 下标对齐)</summary>
public List<RoomFishDetailConfig> spawnList;
/// <summary>当前房间 fishNumber → RoomFishDetailConfig</summary>
public Dictionary<string, RoomFishDetailConfig> spawnDic;
2026-05-18 18:39:11 +08:00
public bool isCreateFish; //是否可以创建鱼
2026-05-28 18:16:45 +08:00
public bool isFrozen = false; //是否冰冻中(冻结出鱼+所有鱼定格)
2026-07-27 16:59:22 +08:00
[Header("调试:手动出鱼")]
[Tooltip("按 M 键生成的鱼编号(配置 fishNumber)")]
public string debugSpawnFishNumber = "11043";
2026-05-28 18:16:45 +08:00
private Dictionary<string, CharacterStateType> frozenFishStates = new Dictionary<string, CharacterStateType>();
2026-06-21 00:07:26 +08:00
private Dictionary<string, bool> frozenFishIsMoves = new Dictionary<string, bool>();
2026-05-18 18:39:11 +08:00
2026-04-29 01:18:39 +08:00
/// <summary>按鱼类型分类的生成配置(Boss / 普通 / 黄金…)</summary>
private Dictionary<FishTypeEnum, List<RoomFishDetailConfig>> fishTypeConfig;
// ------ 鱼阵路线相关 ------
/// <summary>arrayNumber → 该阵型所有可用生成配置</summary>
private Dictionary<string, List<RoomFishDetailConfig>> yz_FishConfig_Line;
/// <summary>列表下标 → arrayNumber(用于随机取阵型)</summary>
2026-05-01 06:26:50 +08:00
private Dictionary<int, string> yz_FishConfig_Line_Dic;
2026-04-29 01:18:39 +08:00
/// <summary>arrayNumber → 列表下标(反向索引)</summary>
2026-05-01 06:26:50 +08:00
private Dictionary<string, int> yz_FishConfig_Line_Dic1;
2026-04-29 01:18:39 +08:00
/// <summary>二维列表,与字典保持同步(供随机无重复取阵型用)</summary>
2026-05-01 06:26:50 +08:00
private List<List<RoomFishDetailConfig>> yz_FishConfig_Line_List;
2026-04-29 01:18:39 +08:00
// ------ Job System NativeArray ------
private NativeArray<float> timers; // 每条鱼的累计计时
private NativeArray<float> frequencies; // 每条鱼的生成间隔
2026-05-01 06:26:50 +08:00
private NativeArray<int> shouldSpawn; // 1=本帧应尝试生成,0=冷却中
2026-05-22 19:30:02 +08:00
// ----------------------------------------------------------------
// 运行时鱼实例字典(guid 为 key)
// ----------------------------------------------------------------
2026-04-29 01:18:39 +08:00
2026-05-22 19:30:02 +08:00
/// <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>();
2026-07-28 11:35:36 +08:00
#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
2026-04-29 01:18:39 +08:00
// ------ 各类计时器 ------
2026-05-18 18:39:11 +08:00
private RoomFishProbabilityConfig tsbossCreateData; // 特殊Boss 创建数据
private float tsbossTimer = 999f; // 特殊Boss 刷新倒计时(秒)
2026-05-22 19:30:02 +08:00
private int tsbossCount; // 本轮同时启用特殊Boss鱼数量
private int tsbossQuanZhongCount; // 本轮同时启用特殊Boss鱼数量
2026-06-16 15:41:22 +08:00
private List<string> tsbossRecentList = new List<string>(); // 最近出现过的特殊Boss(cool down)
private int tsbossRecentMax = 3; // N次内不重复出现
2026-05-22 19:30:02 +08:00
2026-05-18 18:39:11 +08:00
private RoomFishProbabilityConfig tsCreateData; // 特殊鱼 创建数据
private float tsTimer = 999f; // 特殊鱼 刷新倒计时(秒)
2026-05-22 19:30:02 +08:00
private int tsCount; // 本轮同时启用特殊鱼数量
private int tsQuanZhongCount; // 本轮同时启用特殊鱼数量
2026-05-01 06:26:50 +08:00
private RoomFishProbabilityConfig bossCreateData; // Boss 创建数据
private float bossTimer = 999f; // Boss 刷新倒计时(秒)
2026-05-22 19:30:02 +08:00
private int bossCount; // 本轮同时启用Boss鱼数量
private int bossQuanZhongCount; // 本轮同时启用Boss鱼数量
2026-06-16 15:41:22 +08:00
private List<string> bossRecentList = new List<string>(); // 最近出现过的Boss(cool down)
private int bossRecentMax = 3; // N次内不重复出现
2026-05-22 19:30:02 +08:00
2026-07-05 17:39:30 +08:00
/// <summary>Boss鱼路线奇偶切换标记:true=下次选奇数路线,false=下次选偶数路线</summary>
private bool bossNextPathOdd = true;
2026-05-01 06:26:50 +08:00
private RoomFishProbabilityConfig yzCreateData; // 鱼阵创建数据
2026-05-01 21:59:16 +08:00
private float yzTimer = 999f; // 鱼阵刷新倒计时(秒)
2026-05-22 19:30:02 +08:00
private int yzCount; // 本轮同时激活阵型数量
private int yz_LineCount = 0; // 场景内鱼阵路线总数
private int yzQuanZhongCount = 0; // 场景内鱼阵路线总数
2026-05-01 06:26:50 +08:00
private RoomFishProbabilityConfig ptCreateData; // 普通鱼创建数据
private float ptTimer = 999f; // 普通鱼批量启用倒计时(秒)
2026-05-22 19:30:02 +08:00
private int ptCount; // 本轮同时启用普通鱼数量
private int ptQuanZhongCount; // 本轮同时启用普通鱼数量
2026-05-18 18:39:11 +08:00
private RoomFishProbabilityConfig hjCreateData; // 普通鱼创建数据
private float hjTimer = 999f; // 黄金鱼批量启用倒计时(秒)
private int hjCount; // 本轮同时启用黄金鱼数量
2026-05-22 19:30:02 +08:00
private int hjQuanZhongCount; // 本轮同时启用黄金鱼数量
2026-07-25 23:05:24 +08:00
// fangzhu 字段已删除:鱼创建改由服务端 OnFishSpawn 统一下发,客户端无需房主出鱼
2026-05-01 21:59:16 +08:00
public bool isResources = false;
2026-08-01 00:49:49 +08:00
/// <summary>
/// 鱼阵实例全局计数器,每次 SpawnFishArray 递增。
/// 用于生成全局唯一 guid,防止 pathList 重复 pathId 或同 fishId 多次推送导致 guid 冲突。
/// guid 格式: baseGuid_arraySeq_i (如 16501_3_1),arraySeq 每次鱼阵递增,确保唯一。
/// </summary>
private int _fishArraySeq = 0;
2026-04-29 01:18:39 +08:00
// ================================================================
// 懒加载属性
// ================================================================
/// <summary>
/// 鱼基础配置字典(fishNumber → FishTypeConfig),首次访问时构建。
/// </summary>
public Dictionary<string, FishTypeConfig> FishDatas
2026-04-03 18:32:19 +08:00
{
2026-04-29 01:18:39 +08:00
get
{
if (fishDatas != null) return fishDatas;
2026-04-03 18:32:19 +08:00
2026-04-29 01:18:39 +08:00
fishDatas = new Dictionary<string, FishTypeConfig>(M_GameConfig.fishTypeConfigs.Count);
foreach (var item in M_GameConfig.fishTypeConfigs)
fishDatas[item.fishNumber] = item;
return fishDatas;
}
2026-04-03 18:32:19 +08:00
}
2026-09-09 01:49:49 +08:00
/// <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;
}
}
2026-04-29 01:18:39 +08:00
/// <summary>
/// 房间鱼配置(按游戏类型),首次访问时触发懒加载初始化。
/// </summary>
public Dictionary<GameTypeEnum, RoomFishConfig> RoomFishConfig
{
get { EnsureInit(); return roomFishConfig; }
}
2026-04-03 18:32:19 +08:00
2026-04-29 01:18:39 +08:00
/// <summary>
/// 游戏类型 → (fishNumber → RoomFishDetailConfig),首次访问时触发懒加载。
/// </summary>
public Dictionary<GameTypeEnum, Dictionary<string, RoomFishDetailConfig>> CreateDic
{
get { EnsureInit(); return createDic; }
}
2026-05-18 18:39:11 +08:00
#region 生命周期
2026-04-29 01:18:39 +08:00
// ================================================================
// Unity 生命周期
// ================================================================
2026-04-03 18:32:19 +08:00
2026-04-29 01:18:39 +08:00
protected override void Awake()
{
base.Awake();
2026-05-08 06:49:53 +08:00
yzCreateData = new RoomFishProbabilityConfig
{
2026-05-22 19:30:02 +08:00
countRange = new List<int> { 2, 3 },
2026-05-08 06:49:53 +08:00
firstFishCountRange = new List<int> { 2, 3 },
roomLevel = 3,
fishType = 0,
maxCountRange = new List<int> { 10, 20 },
timeRange = new List<int> { 3, 4 },
type = 2
};
2026-04-29 01:18:39 +08:00
// 确保 FishRoot 存在
if (fishRoot == null)
fishRoot = new GameObject("FishRoot").transform;
2026-05-01 06:26:50 +08:00
if (poolRoot == null)
2026-07-27 16:59:22 +08:00
poolRoot = new GameObject("poolRoot").transform;
2026-04-03 18:32:19 +08:00
2026-07-25 23:05:24 +08:00
// ⭐ 鱼创建改由服务端 OnFishSpawn 事件统一下发,客户端不再监听 createFish 帧同步
EventCenter.Instance.AddEventListener<OnFishSpawnData>(
ConstEventString.OnFishSpawn, OnFishSpawn);
2026-05-01 06:26:50 +08:00
}
2026-05-18 18:39:11 +08:00
// ================================================================
2026-07-25 23:05:24 +08:00
// Update:冰冻时不做任何事(鱼创建由服务端 OnFishSpawn 驱动)
2026-05-18 18:39:11 +08:00
// ================================================================
private void Update()
2026-05-01 06:26:50 +08:00
{
2026-07-25 23:05:24 +08:00
// 鱼创建由服务端 OnFishSpawn 事件驱动,客户端 Update 不再出鱼
// (保留 isFrozen 字段供 FreezeAllFish/UnfreezeAllFish 使用)
2026-07-27 16:59:22 +08:00
2026-08-09 17:57:46 +08:00
// ⭐ 调试用:按 M 键发送 HTTP 请求到服务端 AdminApi 出鱼
// roomId 从 gameData.currRoomData 读取,fishNumber 从 Inspector 配置读取
2026-07-27 16:59:22 +08:00
if (UnityEngine.InputSystem.Keyboard.current != null &&
UnityEngine.InputSystem.Keyboard.current.mKey.wasPressedThisFrame)
{
if (string.IsNullOrEmpty(debugSpawnFishNumber))
{
Debug.LogWarning("[CreateFishManager] 未配置 debugSpawnFishNumber,请在 Inspector 设置鱼编号");
return;
}
2026-08-09 17:57:46 +08:00
string roomId = GameManager.Instance.gameData?.currRoomData?.roomId;
if (string.IsNullOrEmpty(roomId))
{
Debug.LogWarning("[CreateFishManager] 当前未在房间中(roomId 为空),无法出鱼");
return;
}
APIManager.TempSpawnFish(roomId, debugSpawnFishNumber);
2026-07-27 16:59:22 +08:00
}
2026-05-01 06:26:50 +08:00
}
2026-05-18 18:39:11 +08:00
2026-05-22 19:30:02 +08:00
2026-05-18 18:39:11 +08:00
2026-04-29 01:18:39 +08:00
private void OnDestroy()
{
2026-07-25 23:05:24 +08:00
EventCenter.Instance.RemoveEventListener<OnFishSpawnData>(
ConstEventString.OnFishSpawn, OnFishSpawn);
2026-04-29 01:18:39 +08:00
// 释放 NativeArray,避免内存泄漏
DisposeNativeArrays();
}
2026-05-18 18:39:11 +08:00
#endregion
2026-04-29 01:18:39 +08:00
2026-05-18 18:39:11 +08:00
#region 初始化
public void InitChuYu()
2026-04-29 01:18:39 +08:00
{
2026-05-18 18:39:11 +08:00
// 随机初始化计时器,避免所有类型同帧触发
2026-05-01 06:26:50 +08:00
yzCount = Random.Range(yzCreateData.countRange[0], yzCreateData.countRange[1]);
ptCount = Random.Range(ptCreateData.countRange[0], ptCreateData.countRange[1]);
2026-05-18 18:39:11 +08:00
hjCount = Random.Range(hjCreateData.countRange[0], hjCreateData.countRange[1]);
bossCount = Random.Range(bossCreateData.countRange[0], bossCreateData.countRange[1]);
2026-05-22 19:30:02 +08:00
tsbossCount = Random.Range(tsbossCreateData.countRange[0], tsbossCreateData.countRange[1]);
tsCount = Random.Range(tsCreateData.countRange[0], tsCreateData.countRange[1]);
2026-05-18 18:39:11 +08:00
yzTimer = 0.5f;
ptTimer = 0.5f;
hjTimer = 0.5f;
2026-05-22 19:30:02 +08:00
tsTimer = 1f;
2026-05-18 18:39:11 +08:00
bossTimer = 5f;
2026-05-22 19:30:02 +08:00
tsbossTimer = 5f;
2026-04-29 01:18:39 +08:00
}
/// <summary>
/// 懒加载初始化:只执行一次,构建全部配置缓存字典。
/// </summary>
2026-04-03 18:32:19 +08:00
private void EnsureInit()
{
2026-04-29 01:18:39 +08:00
if (roomFishConfig != null) return;
2026-04-03 18:32:19 +08:00
FishYZ.Init();
roomFishConfig = new Dictionary<GameTypeEnum, RoomFishConfig>();
2026-05-01 06:26:50 +08:00
createDic = new Dictionary<GameTypeEnum, Dictionary<string, RoomFishDetailConfig>>();
2026-04-03 18:32:19 +08:00
var list = GameManager.Instance.gameConfig.roomFishConfigs;
2026-04-29 01:18:39 +08:00
if (list == null || list.Count == 0) return;
2026-04-03 18:32:19 +08:00
2026-04-29 01:18:39 +08:00
// 构建 fishArray / fishType 字典缓存
2026-04-03 18:32:19 +08:00
BuildConfigCache();
foreach (var item in list)
{
2026-04-29 01:18:39 +08:00
var gameType = (GameTypeEnum)item.gameType;
2026-04-03 18:32:19 +08:00
roomFishConfig[gameType] = item;
var fishDic = new Dictionary<string, RoomFishDetailConfig>();
createDic[gameType] = fishDic;
2026-04-29 01:18:39 +08:00
// 普通鱼列表
2026-04-03 18:32:19 +08:00
if (item.normalFishList != null)
foreach (var fish in item.normalFishList)
fishDic[fish.fishNumber] = fish;
2026-04-29 01:18:39 +08:00
// 等级鱼列表(同 fishNumber 会覆盖普通鱼,实现换皮)
2026-04-03 18:32:19 +08:00
if (item.roomFishLevelList != null)
foreach (var fish in item.roomFishLevelList)
fishDic[fish.fishNumber] = fish;
2026-02-03 11:35:19 +08:00
}
}
/// <summary>
2026-04-29 01:18:39 +08:00
/// 构建高频查询的字典缓存(fishArrayConfigs / fishTypeConfigs)。
2026-02-03 11:35:19 +08:00
/// </summary>
2026-04-29 01:18:39 +08:00
private void BuildConfigCache()
2026-02-03 11:35:19 +08:00
{
2026-04-29 01:18:39 +08:00
fishArrayConfigDic = GameManager.Instance.gameConfig.fishArrayConfigs
.ToDictionary(a => a.fishArrayNumber, a => a);
2026-05-22 19:30:02 +08:00
fishDatas = GameManager.Instance.gameConfig.fishTypeConfigs
2026-04-29 01:18:39 +08:00
.ToDictionary(f => f.fishNumber, f => f);
2026-02-03 11:35:19 +08:00
}
2026-05-18 18:39:11 +08:00
#endregion
#region 进入房间加载
/// <summary>进入房间:初始化生成系统并绑定游戏面板。</summary>
public void EnterRoom(GameTypeEnum gameType)
{
InitSpawnSystem(gameType);
BindCurrentGamePanel();
2026-07-21 05:13:43 +08:00
EnsureBJEffectRoot();
}
/// <summary>
/// 创建爆奖特效统一父节点(挂在 UIManager.uiRoot 下)。
/// 已存在则不重复创建。所有爆奖相关 Instantiate 都应使用此节点作为父对象。
/// </summary>
private void EnsureBJEffectRoot()
{
if (bjEffectRoot != null) return;
var go = new GameObject("BJEffectRoot");
go.transform.SetParent(UIManager.Instance.uiRoot, false);
var rect = go.AddComponent<RectTransform>();
rect.anchorMin = Vector2.zero;
rect.anchorMax = Vector2.one;
rect.offsetMin = Vector2.zero;
rect.offsetMax = Vector2.zero;
bjEffectRoot = go.transform;
2026-05-18 18:39:11 +08:00
}
/// <summary>根据当前 GameType 绑定对应 UI 面板到 currGameType。</summary>
private void BindCurrentGamePanel()
{
switch (GameManager.Instance.gameData.currGameType)
{
case GameType.FLZY:
2026-05-22 19:30:02 +08:00
currGameType = UIManager.Instance.Get<GameMainPanel_FLZY>(UIPanel.MainGamePanelFLZY);
2026-05-18 18:39:11 +08:00
break;
case GameType.YYSC:
currGameType = UIManager.Instance.Get<GameMainPanel_YYSC>(UIPanel.MainGamePanelYYSC);
break;
case GameType.XJMH:
2026-05-22 19:30:02 +08:00
currGameType = UIManager.Instance.Get<GameMainPanel_XJMH>(UIPanel.MainGamePanelXJMH);
2026-05-18 18:39:11 +08:00
break;
case GameType.HHWK:
2026-05-22 19:30:02 +08:00
currGameType = UIManager.Instance.Get<GameMainPanel_HHWK>(UIPanel.MainGamePanelHHWK);
2026-05-18 18:39:11 +08:00
break;
default:
break;
}
}
2026-04-29 01:18:39 +08:00
/// <summary>
/// 进入房间时初始化生成系统:加载当前房间配置、分配 NativeArray。
/// </summary>
2026-05-01 06:26:50 +08:00
public async void InitSpawnSystem(GameTypeEnum gameType)
2026-02-03 11:35:19 +08:00
{
EnsureInit();
2026-04-29 01:18:39 +08:00
// 初始化分类容器
2026-05-01 06:26:50 +08:00
fishTypeConfig = new Dictionary<FishTypeEnum, List<RoomFishDetailConfig>>();
yz_FishConfig_Line = new Dictionary<string, List<RoomFishDetailConfig>>();
2026-04-29 01:18:39 +08:00
yz_FishConfig_Line_Dic = new Dictionary<int, string>();
2026-05-01 06:26:50 +08:00
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>();
2026-04-29 01:18:39 +08:00
2026-02-03 11:35:19 +08:00
var config = RoomFishConfig[gameType];
2026-04-29 01:18:39 +08:00
// ---- 处理普通鱼列表 ----
if (config.normalFishList != null)
2026-03-18 17:26:14 +08:00
{
2026-04-29 01:18:39 +08:00
foreach (var item in config.normalFishList)
2026-03-27 15:18:35 +08:00
{
2026-05-01 21:59:16 +08:00
if (!spawnDic.ContainsKey(item.fishNumber))
{
spawnDic[item.fishNumber] = item;
spawnList.Add(item);
}
2026-04-29 01:18:39 +08:00
if (item.type == 1)
2026-03-27 15:18:35 +08:00
{
2026-04-29 01:18:39 +08:00
// 单体鱼:按鱼类型分组
AddToFishTypeConfig(item);
2026-03-27 15:18:35 +08:00
}
2026-04-29 01:18:39 +08:00
else if (item.type == 2)
2026-03-27 15:18:35 +08:00
{
2026-04-29 01:18:39 +08:00
// 鱼阵:按 arrayNumber 分组
TryAddToArrayLine(item);
2026-03-27 15:18:35 +08:00
}
}
2026-03-18 17:26:14 +08:00
}
2026-04-29 01:18:39 +08:00
// ---- 处理等级鱼列表(仅当前房间等级)----
2026-05-18 18:39:11 +08:00
int currRoomLevel = GameManager.Instance.gameData.currRoomData.roomLevel;
2026-04-29 01:18:39 +08:00
if (config.roomFishLevelList != null)
2026-03-18 17:26:14 +08:00
{
2026-05-01 06:26:50 +08:00
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.黄金鱼:
2026-05-18 18:39:11 +08:00
hjCreateData = item;
2026-05-01 06:26:50 +08:00
break;
case FishTypeEnum.特殊鱼:
2026-05-22 19:30:02 +08:00
tsCreateData = item;
break;
case FishTypeEnum.特殊Boss:
2026-05-18 18:39:11 +08:00
tsbossCreateData = item;
2026-05-01 06:26:50 +08:00
break;
default:
break;
}
break;
2026-05-01 21:59:16 +08:00
case 2:
2026-05-01 06:26:50 +08:00
//yzTimer=item.
yzCreateData = item;
break;
default:
break;
}
}
2026-04-29 01:18:39 +08:00
foreach (var item in config.roomFishLevelList)
2026-03-18 17:26:14 +08:00
{
2026-04-29 01:18:39 +08:00
if (item.roomLevel != currRoomLevel) continue;
2026-05-01 21:59:16 +08:00
2026-03-27 15:18:35 +08:00
if (item.type == 2)
2026-03-18 17:26:14 +08:00
{
2026-04-29 01:18:39 +08:00
spawnDic[item.fishNumber] = item;
2026-03-27 15:18:35 +08:00
spawnList.Add(item);
2026-05-22 19:30:02 +08:00
yzQuanZhongCount += item.fishWeight;
2026-04-29 01:18:39 +08:00
TryAddToArrayLine(item);
2026-03-27 15:18:35 +08:00
}
else
{
2026-04-29 01:18:39 +08:00
AddToFishTypeConfig(item);
// 等级鱼也加入 spawnDic / spawnList
spawnDic[item.fishNumber] = item;
spawnList.Add(item);
2026-05-22 19:30:02 +08:00
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;
}
2026-03-27 15:18:35 +08:00
}
}
2026-03-18 17:26:14 +08:00
}
2026-02-03 11:35:19 +08:00
2026-04-29 01:18:39 +08:00
// ---- 分配 Job System NativeArray ----
2026-02-03 11:35:19 +08:00
int count = spawnList.Count;
2026-04-29 01:18:39 +08:00
DisposeNativeArrays(); // 防止重复进房泄漏
2026-05-01 06:26:50 +08:00
timers = new NativeArray<float>(count, Allocator.Persistent);
2026-02-03 11:35:19 +08:00
frequencies = new NativeArray<float>(count, Allocator.Persistent);
2026-05-01 06:26:50 +08:00
shouldSpawn = new NativeArray<int>(count, Allocator.Persistent);
2026-02-03 11:35:19 +08:00
for (int i = 0; i < count; i++)
{
2026-05-01 06:26:50 +08:00
timers[i] = 0f;
2026-02-03 11:35:19 +08:00
frequencies[i] = spawnList[i].fishFrequency;
2026-04-29 01:18:39 +08:00
shouldSpawn[i] = 1; // 初始全部允许生成
2026-03-27 15:18:35 +08:00
}
2026-04-29 01:18:39 +08:00
// ---- 统计场景内鱼阵路线总数 ----
yz_LineCount = 0;
foreach (var key in MonsterMoveLine.dic_BezierCurveDatas.Keys)
{
if (key.StartsWith("鱼阵"))
yz_LineCount++;
}
2026-05-01 06:26:50 +08:00
InitChuYu();
2026-02-03 11:35:19 +08:00
}
2026-04-29 01:18:39 +08:00
/// <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;
2026-05-01 06:26:50 +08:00
yz_FishConfig_Line_Dic[newIdx] = yz.arrayNumber;
2026-04-29 01:18:39 +08:00
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);
}
2026-05-18 18:39:11 +08:00
#endregion
2026-04-29 01:18:39 +08:00
2026-07-25 23:05:24 +08:00
#region OnFishSpawn 服务端出鱼接收处理
// ⭐ 鱼创建改由服务端 OnFishSpawn 事件统一下发
// 客户端 BroadCastCreateFish / TickXxxSpawn / SendCreateFishRequest /
// CreateInitialFishes / BroadcastExistingFishes / OnRequestSyncFish / CreateFish
// 全部删除,改为接收 OnFishSpawnData 后调用 SpawnFishMainThread 生成鱼实例
//
// type:
// "init" - 首次出鱼(玩家进入房间后服务端推送初始鱼)
// "spawn" - 定时出鱼(游戏中持续刷怪)
// "sync" - 重连/取消挂机同步(把当前场上活鱼推送给回归玩家)
2026-05-18 18:39:11 +08:00
/// <summary>
2026-07-25 23:05:24 +08:00
/// 接收服务端 OnFishSpawn 事件,生成鱼实例。
/// 遍历 data.fish 列表,对每条鱼:
/// 1. guid 去重(已有此 guid 的鱼跳过,防 sync 模式重复 Spawn)
/// 2. 构造 FrameData_CreateFish 复用 SpawnFishMainThread 生成
/// 3. 启动 expireAt 过期自动移除协程
2026-05-18 18:39:11 +08:00
/// </summary>
2026-07-25 23:05:24 +08:00
public void OnFishSpawn(OnFishSpawnData data)
2026-05-18 18:39:11 +08:00
{
2026-07-25 23:05:24 +08:00
if (data == null || data.fish == null || data.fish.Count == 0) return;
if (isFrozen) return; // 冰冻中不创建鱼
if (spawnDic == null) return; // 场景卸载后忽略
2026-08-04 21:20:56 +08:00
// ⭐ 分帧出鱼:普通鱼/黄金鱼分批生成(每帧5条),其他类型立即全出
// 避免一次出大量普通鱼导致单帧卡顿
var batchedQueue = new List<OnFishSpawnFishItem>(data.fish.Count);
2026-07-25 23:05:24 +08:00
foreach (var item in data.fish)
2026-05-18 18:39:11 +08:00
{
2026-07-25 23:05:24 +08:00
if (item == null) continue;
2026-05-18 18:39:11 +08:00
2026-08-04 21:20:56 +08:00
// 判断是否需要分帧:普通鱼(1)和黄金鱼(3)且非鱼阵 → 分帧
bool shouldBatch = ShouldBatchSpawn(item);
2026-05-18 18:39:11 +08:00
2026-08-04 21:20:56 +08:00
if (shouldBatch)
2026-05-18 18:39:11 +08:00
{
2026-08-04 21:20:56 +08:00
batchedQueue.Add(item);
}
else
{
// Boss/特殊鱼/鱼阵等:立即生成
TrySpawnSingleFish(item);
}
2026-07-19 15:04:15 +08:00
}
2026-08-04 21:20:56 +08:00
// 分帧组:启动协程逐帧生成
if (batchedQueue.Count > 0)
{
StartCoroutine(SpawnFishBatchedCoroutine(batchedQueue));
}
NetLog.Log($"[CreateFishManager] OnFishSpawn type={data.type}, count={data.fish.Count}, batched={batchedQueue.Count}");
}
/// <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)
{
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]);
}
index = end;
yield return null; // 等待下一帧
}
}
/// <summary>
/// 尝试生成单条鱼(含去重检查)。返回是否成功生成。
/// </summary>
private bool TrySpawnSingleFish(OnFishSpawnFishItem item)
{
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 = item.createdAt;
SpawnFishMainThread(createFishData, spawnTime);
return true;
2026-07-19 15:04:15 +08:00
}
2026-07-20 16:30:28 +08:00
/// <summary>
2026-07-25 23:05:24 +08:00
/// 鱼过期自动移除协程。
/// remainingSeconds 秒后调用 Id_DieFish_NotFX 销毁鱼(无特效)。
2026-07-20 16:30:28 +08:00
/// </summary>
2026-07-25 23:05:24 +08:00
private IEnumerator ExpireFishCoroutine(string guid, int remainingSeconds)
2026-07-20 16:30:28 +08:00
{
2026-07-25 23:05:24 +08:00
yield return new WaitForSeconds(remainingSeconds);
Id_DieFish_NotFX(guid);
2026-07-20 22:32:52 +08:00
}
2026-07-27 16:59:22 +08:00
/// <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;
}
2026-05-18 18:39:11 +08:00
#endregion
2026-07-25 23:05:24 +08:00
#region 旧客户端出鱼链路(已删除)
// 以下方法已删除,鱼创建改由服务端 OnFishSpawn 事件下发:
// - BroadCastCreateFish / TickBossSpawn / TickTSBossSpawn / TickFishArraySpawn
// - TickPTFishSpawn / TickHJFishSpawn / TickTSFishSpawn
// - ProcessSpawnRequests / SendCreateFishRequest
// - CreateInitialFishes / BroadcastExistingFishes / OnRequestSyncFish
// - CreateFish(SyncGameData)
// 保留方法:SpawnFishMainThread / OnlyFish / SpawnFishArray 等(被 OnFishSpawn 调用)
#endregion
2026-07-20 22:32:52 +08:00
2026-07-26 22:00:38 +08:00
#region 鱼生成核心逻辑
2026-07-25 23:05:24 +08:00
// ================================================================
// 旧定时 Tick 方法(已删除,鱼创建改由服务端 OnFishSpawn 事件下发)
// ================================================================
// TickBossSpawn / TickTSBossSpawn / TickFishArraySpawn / TickPTFishSpawn
// TickHJFishSpawn / TickTSFishSpawn / buchongfu / ProcessSpawnRequests
// SendCreateFishRequest / CreateInitialFishes / BroadcastExistingFishes
// OnRequestSyncFish / CreateFish(SyncGameData)
// —— 全部删除,改由 OnFishSpawn(OnFishSpawnData) 接收服务端出鱼事件 ——
// ================================================================
// 单体鱼 / 鱼阵生成(被 OnFishSpawn 调用)
// ================================================================
2026-07-20 22:32:52 +08:00
2026-04-29 01:18:39 +08:00
/// <summary>
/// 主线程实例化鱼对象(单体鱼 / 鱼阵分发)。
/// </summary>
private void SpawnFishMainThread(FrameData_CreateFish createFishData, long time)
{
2026-07-16 20:16:05 +08:00
// ⭐ spawnDic 可能在场景卸载时被置 null,这里再保护一次
if (spawnDic == null) return;
2026-04-29 01:18:39 +08:00
if (!spawnDic.TryGetValue(createFishData.fishNumber, out var cfg))
2026-04-23 17:24:31 +08:00
{
2026-04-29 01:18:39 +08:00
Debug.LogWarning($"[CreateFishManager] 找不到 spawnDic 配置: {createFishData.fishNumber}");
return;
}
if (cfg.type == 1)
{
// 单体鱼
OnlyFish(createFishData, time);
}
else if (cfg.type == 2)
{
// 鱼阵
2026-05-22 19:30:02 +08:00
SpawnFishArray(createFishData, cfg.fishNumber, time);
2026-04-29 01:18:39 +08:00
}
}
// ================================================================
// 单体鱼生成
// ================================================================
2026-05-19 17:59:23 +08:00
2026-04-29 01:18:39 +08:00
/// <summary>
/// 生成一条普通/Boss/黄金鱼并初始化路径。
/// </summary>
/// <returns>成功返回 true,配置缺失返回 false。</returns>
private bool OnlyFish(FrameData_CreateFish createFishData, long time)
{
2026-05-22 19:30:02 +08:00
if (!fishDatas.TryGetValue(createFishData.fishNumber, out var fishType))
2026-04-29 01:18:39 +08:00
{
Debug.LogError($"[CreateFishManager] 找不到 fishTypeConfig: {createFishData.fishNumber}");
return false;
}
2026-04-29 23:53:55 +08:00
//var prefab = FishResourcesData.GetFishPrefab(int.Parse(fishType.fishNumber));
//var fish = Instantiate(prefab, new Vector3(9999, 9999, 0), prefab.transform.rotation, fishRoot);
2026-05-22 19:30:02 +08:00
2026-05-01 06:26:50 +08:00
try
{
var fish = Pool.Instance.Get(int.Parse(createFishData.fishNumber), new Vector3(9999, 9999, 0), Quaternion.identity, fishRoot);
2026-07-13 04:48:18 +08:00
if (fish == null)
{
Debug.LogError($"[CreateFishManager] Pool.Get 返回 null,跳过此鱼: {createFishData.fishNumber}");
return false;
}
2026-05-01 06:26:50 +08:00
var mc = fish.GetComponent<MonsterContral>() ?? fish.AddComponent<MonsterContral>();
mc.monsterData.fishTypeConfig = FishDatas[createFishData.fishNumber];
mc.guid = createFishData.guid;
2026-07-27 23:30:25 +08:00
// ⭐ 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;
}
2026-05-01 21:59:16 +08:00
mc.fishenum = (FishTypeEnum)fishType.fishType;
2026-05-01 06:26:50 +08:00
// 按鱼类型放入对应字典
RegisterFishInstance(mc, fishType.fishType);
2026-04-29 01:18:39 +08:00
2026-05-01 06:26:50 +08:00
mc.Init(createFishData.moveLineId, spawnDic[createFishData.fishNumber].times);
2026-04-29 01:18:39 +08:00
2026-05-01 06:26:50 +08:00
Debug.Log($"[CreateFishManager] 生成鱼:{fishType.fishName}");
}
catch (Exception ex)
{
Debug.LogError(createFishData.fishNumber);
Debug.LogError(ex.Message);
}
2026-04-29 01:18:39 +08:00
return true;
}
2026-05-22 19:30:02 +08:00
/// <summary>将鱼实例注册到全局字典及对应分类字典。</summary>
private void RegisterFishInstance(MonsterContral mc, int fishTypeId)
{
2026-08-01 00:49:49 +08:00
// ⭐ 检测 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);
}
2026-05-22 19:30:02 +08:00
fishDatas_hashCodekey[mc.guid] = mc;
2026-08-01 00:49:49 +08:00
// ⭐ 记录创建日志:包含 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());
2026-05-22 19:30:02 +08:00
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;
}
2026-07-28 11:35:36 +08:00
#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
2026-05-22 19:30:02 +08:00
}
2026-07-26 22:00:38 +08:00
#endregion
2026-05-22 19:30:02 +08:00
#region 鱼阵
2026-04-29 01:18:39 +08:00
// ================================================================
// 鱼阵生成(按 arrayNumber 前缀分发到对应阵型协程/方法)
// ================================================================
/// <summary>根据鱼阵配置分发到对应的生成逻辑。</summary>
2026-05-22 19:30:02 +08:00
private void SpawnFishArray(FrameData_CreateFish createFishData, string fishNumber, long time)
2026-04-29 01:18:39 +08:00
{
2026-05-22 19:30:02 +08:00
if (!fishArrayConfigDic.TryGetValue(fishNumber, out var yz))
2026-04-29 01:18:39 +08:00
{
2026-05-22 19:30:02 +08:00
Debug.LogError($"[CreateFishManager] 找不到 fishArrayConfig: {fishNumber}");
2026-04-29 01:18:39 +08:00
return;
}
2026-05-22 19:30:02 +08:00
if (!fishDatas.TryGetValue(yz.fishNumber, out var fishType))
2026-04-29 01:18:39 +08:00
{
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;
}
2026-08-01 00:49:49 +08:00
// ⭐ 日志:记录鱼阵入口,排查 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;
2026-05-22 19:30:02 +08:00
var prefab = FishResourcesData.GetFishPrefab(int.Parse(fishType.fishNumber));
var pos = new Vector3(9999, 9999, 0);
2026-04-29 01:18:39 +08:00
string prefix = yz.arrayNumber.Length >= 3
? yz.arrayNumber.Substring(0, 3)
: yz.arrayNumber;
switch (prefix)
{
case "700": // 扇形阵:逐条延迟生成
2026-08-01 00:49:49 +08:00
StartCoroutine(IeShanXing(createFishData, yz, pos, prefab, pathList, time, yz.fishCount, arraySeq));
2026-04-29 01:18:39 +08:00
break;
case "701": // 环形阵:同帧同路径全部生成
foreach (int pathId in pathList)
2026-08-01 00:49:49 +08:00
Quan(createFishData, yz, pos, prefab, pathId, yz.fishCount, time, arraySeq);
2026-04-29 01:18:39 +08:00
break;
case "702":
case "7007":
case "710": // 顺序阵:每条路径逐一延迟生成
2026-05-18 18:39:11 +08:00
case "704":
2026-05-07 03:46:03 +08:00
//int chosen710 = (prefix == "710")
// ? pathList[Random.Range(0, pathList.Count)]
// : -1;
2026-05-22 19:30:02 +08:00
StartCoroutine(YZ7007(createFishData, yz, pos, prefab, time));
2026-05-01 21:59:16 +08:00
//foreach (int pathId in pathList)
//{
// int actualPath = (prefix == "710") ? chosen710 : pathId;
// StartCoroutine(YZ7007(createFishData, yz, pos, prefab, actualPath, time, yz.fishCount));
//}
2026-04-29 01:18:39 +08:00
break;
default:
Debug.LogWarning($"[CreateFishManager] 未知鱼阵前缀: {prefix}");
break;
}
}
// ================================================================
// 鱼阵生成协程 / 方法
// ================================================================
/// <summary>扇形阵:按路线列表逐条延迟 0.8s 生成一扇。</summary>
private IEnumerator IeShanXing(FrameData_CreateFish createFishData, FishArrayConfig yz,
2026-08-01 00:49:49 +08:00
Vector3 pos, GameObject prefab, List<int> pathList, long time, int count, int arraySeq)
2026-04-29 01:18:39 +08:00
{
foreach (int pathId in pathList)
{
2026-05-01 06:26:50 +08:00
if (!isResources)
{
yield break;
}
2026-08-01 00:49:49 +08:00
ShanXing(createFishData, yz, pos, prefab, pathId, count, time, arraySeq);
2026-04-29 01:18:39 +08:00
yield return new WaitForSeconds(0.8f);
}
}
private void ShanXing(FrameData_CreateFish createFishData, FishArrayConfig yz,
2026-08-01 00:49:49 +08:00
Vector3 pos, GameObject prefab, int pathId, int count, long time, int arraySeq)
2026-04-29 01:18:39 +08:00
{
var cfg = spawnDic[createFishData.fishNumber];
for (int i = 1; i <= 16; i++)
{
// 交替正负角度,形成扇形分布
2026-05-01 06:26:50 +08:00
float angle = i * 5f * (i % 2 == 0 ? -1f : 1f);
2026-04-29 01:18:39 +08:00
var rotation = Quaternion.Euler(
prefab.transform.eulerAngles + Vector3.forward * angle);
2026-04-29 23:53:55 +08:00
//var fish = Instantiate(prefab, pos, rotation, fishRoot);
var fish = Pool.Instance.Get(int.Parse(yz.fishNumber), pos, rotation, fishRoot);
2026-05-01 21:59:16 +08:00
2026-05-01 06:26:50 +08:00
var mc = fish.GetComponent<MonsterContral>();
2026-04-29 01:18:39 +08:00
mc.monsterData.fishTypeConfig = FishDatas[yz.fishNumber];
2026-08-01 00:49:49 +08:00
// ⭐ guid 用 arraySeq 替代 pathId:arraySeq 每次鱼阵递增,全局唯一,
// 防止 pathList 重复 pathId 或同 fishId 多次推送导致 guid 冲突
mc.guid = createFishData.guid + "_" + arraySeq + "_" + i;
2026-05-22 19:30:02 +08:00
RegisterFishInstance(mc, FishDatas[yz.fishNumber].fishType);
2026-04-29 01:18:39 +08:00
mc.moveTime = (BeijingTimeManager.Instance.CurrentTimestampMs - time) / 1000f;
mc.Init(pathId.ToString(), cfg.times, true, yz.arrayNumber);
2026-09-10 17:23:20 +08:00
// ⭐ 鱼阵调试日志:输出每条子鱼生成后的旋转与缩放(排查"有的正有的反")
Debug.Log($"[鱼阵调试] 阵={yz.arrayNumber}(扇形700) pathId={pathId} 第{i}/{16}条 guid={mc.guid} " +
$"rotation={fish.transform.eulerAngles.ToString("F1")} localScale={fish.transform.localScale.ToString("F3")}");
2026-04-29 01:18:39 +08:00
}
}
/// <summary>
/// 环形阵:在同一路径上生成 count 条鱼,角度均匀分布(360/count)。
/// </summary>
private void Quan(FrameData_CreateFish createFishData, FishArrayConfig yz,
2026-08-01 00:49:49 +08:00
Vector3 pos, GameObject prefab, int pathId, int count, long time, int arraySeq)
2026-04-29 01:18:39 +08:00
{
if (count <= 0) return;
var cfg = spawnDic[createFishData.fishNumber];
for (int i = 0; i < count; i++)
{
2026-05-01 06:26:50 +08:00
float angle = (360f / count) * i;
2026-05-18 18:39:11 +08:00
var rotation = Quaternion.Euler(prefab.transform.eulerAngles + Vector3.forward * angle);
2026-04-29 01:18:39 +08:00
2026-04-29 23:53:55 +08:00
//var fish = Instantiate(prefab, pos, rotation, fishRoot);
var fish = Pool.Instance.Get(int.Parse(yz.fishNumber), pos, rotation, fishRoot);
2026-05-01 06:26:50 +08:00
var mc = fish.GetComponent<MonsterContral>();
2026-04-29 01:18:39 +08:00
mc.monsterData.fishTypeConfig = FishDatas[yz.fishNumber];
2026-08-01 00:49:49 +08:00
// ⭐ guid 用 arraySeq 替代 pathId:arraySeq 每次鱼阵递增,全局唯一,
// 防止 pathList 重复 pathId 或同 fishId 多次推送导致 guid 冲突
mc.guid = createFishData.guid + "_" + arraySeq + "_" + i;
2026-05-22 19:30:02 +08:00
RegisterFishInstance(mc, FishDatas[yz.fishNumber].fishType);
2026-04-29 01:18:39 +08:00
2026-05-22 19:30:02 +08:00
mc.moveTime = (BeijingTimeManager.Instance.CurrentTimestampMs - time) / 1000f;
2026-04-29 01:18:39 +08:00
mc.Init(pathId.ToString(), cfg.times, true, yz.arrayNumber);
2026-09-10 17:23:20 +08:00
// ⭐ 鱼阵调试日志:输出每条子鱼生成后的旋转与缩放(排查"有的正有的反")
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")}");
2026-04-29 01:18:39 +08:00
}
}
/// <summary>
/// 顺序阵:每隔 1s 生成一条鱼,共生成 count 条。
/// </summary>
private IEnumerator YZ7007(FrameData_CreateFish createFishData, FishArrayConfig yz,
2026-05-22 19:30:02 +08:00
Vector3 pos, GameObject prefab, long time)
2026-04-29 01:18:39 +08:00
{
2026-07-28 10:59:08 +08:00
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];
2026-05-22 19:30:02 +08:00
for (int i = 0; i < yz.fishCount; i++)
2026-04-29 01:18:39 +08:00
{
2026-05-01 06:26:50 +08:00
if (!isResources)
{
yield break;
}
2026-04-29 23:53:55 +08:00
//var fish = Instantiate(prefab, pos, prefab.transform.rotation, fishRoot);
var fish = Pool.Instance.Get(int.Parse(yz.fishNumber), pos, Quaternion.identity, fishRoot);
2026-07-28 10:59:08 +08:00
if (fish == null)
{
Debug.LogWarning($"[CreateFishManager] YZ7007 Pool.Get 返回 null,跳过: {yz.fishNumber}");
yield return new WaitForSeconds(1f);
continue;
}
2026-04-29 23:53:55 +08:00
2026-05-01 06:26:50 +08:00
var mc = fish.GetComponent<MonsterContral>() ?? fish.AddComponent<MonsterContral>();
2026-05-22 19:30:02 +08:00
2026-04-29 01:18:39 +08:00
mc.monsterData.fishTypeConfig = FishDatas[yz.fishNumber];
2026-05-01 06:26:50 +08:00
mc.guid = createFishData.guid + i;
2026-05-22 19:30:02 +08:00
RegisterFishInstance(mc, FishDatas[yz.fishNumber].fishType);
2026-04-29 01:18:39 +08:00
// 每条鱼额外减去已等待秒数,使路径起点与帧数同步
mc.moveTime = ((BeijingTimeManager.Instance.CurrentTimestampMs - time) / 1000f) - i;
2026-07-28 10:59:08 +08:00
// ⭐ 路线用 FishYZ 取到的第 0 个,yzID 用 arrayNumber
mc.Init(yzPathId.ToString(), cfg.times, true, yz.arrayNumber);
2026-04-29 01:18:39 +08:00
2026-09-10 17:23:20 +08:00
// ⭐ 鱼阵调试日志:输出每条子鱼生成后的旋转与缩放(排查"有的正有的反")
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")}");
2026-04-29 01:18:39 +08:00
yield return new WaitForSeconds(1f);
}
}
2026-05-22 19:30:02 +08:00
#endregion
2026-04-29 01:18:39 +08:00
2026-05-19 17:59:23 +08:00
#region 结束游戏退出房间
/// <summary>退出房间:关闭生成系统。</summary>
public void ExitRoom()
{
CloseSpawnSystem();
}
/// <summary>
/// 关闭生成系统:清理列表,销毁场景内所有鱼对象。
/// </summary>
public async void CloseSpawnSystem()
{
//await Task.Delay(2000);
spawnList = null;
spawnDic = null;
yz_FishConfig_Line_Dic = null;
yz_FishConfig_Line = null;
yz_FishConfig_Line_Dic1 = null;
if (fishRoot == null) return;
2026-08-09 17:57:46 +08:00
// ⭐ 收集要销毁的对象列表,避免遍历时字典被修改或对象已销毁导致 MissingReferenceException
var fishToDestroy = new List<GameObject>();
2026-05-19 17:59:23 +08:00
foreach (var item in fishDatas_hashCodekey)
{
2026-08-09 17:57:46 +08:00
if (item.Value != null && item.Value.gameObject != null)
2026-05-19 17:59:23 +08:00
{
2026-08-09 17:57:46 +08:00
fishToDestroy.Add(item.Value.gameObject);
2026-05-19 17:59:23 +08:00
}
}
2026-08-09 17:57:46 +08:00
foreach (var go in fishToDestroy)
{
if (go != null) Destroy(go);
}
2026-05-19 17:59:23 +08:00
fishDatas_hashCodekey.Clear();
fishDatas_hashCodekey_Boss.Clear();
fishDatas_hashCodekey_Small.Clear();
2026-07-28 11:35:36 +08:00
#if UNITY_EDITOR
// ⭐ 清理调试 List
fishList_All.Clear();
fishList_Small.Clear();
fishList_Boss.Clear();
fishList_HJ.Clear();
fishList_TS.Clear();
fishList_TS_Boss.Clear();
#endif
2026-05-19 17:59:23 +08:00
foreach (Transform item in fishRoot)
{
Destroy(item.gameObject);
}
2026-07-21 05:13:43 +08:00
// ⭐ 销毁爆奖特效父节点:所有挂在 bjEffectRoot 下的爆奖特效
// (Boss 分数版/加分弹出/BJEndUI/特殊爆奖等) 会一并被销毁,
// 防止退出房间时残留未结束的爆奖协程/动画造成的内存泄漏
if (bjEffectRoot != null)
{
Destroy(bjEffectRoot.gameObject);
bjEffectRoot = null;
}
2026-08-04 21:20:56 +08:00
// ⭐ 清理特效池:bjEffectRoot 销毁后池内对象失效,清空避免复用已销毁对象
EffectPool.Instance.ClearAll();
2026-05-19 17:59:23 +08:00
//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
2026-04-29 01:18:39 +08:00
// ================================================================
// 随机工具方法
// ================================================================
2026-08-04 21:20:56 +08:00
// ⭐ 静态复用 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);
2026-04-29 01:18:39 +08:00
/// <summary>
/// 从 [min, max) 区间随机取 count 个不重复整数。
/// 若 min==max 返回空列表。
2026-08-04 21:20:56 +08:00
/// ⭐ 优化:复用静态 Random + 预分配 List,零 GC 分配。
/// 注意:返回的 List 是内部复用对象,调用方用完前不要再次调用本方法。
2026-04-29 01:18:39 +08:00
/// </summary>
public static List<int> RandomUniqueNumbers(int count, int min, int max)
{
2026-08-04 21:20:56 +08:00
_reusableIntResult.Clear();
2026-04-29 01:18:39 +08:00
if (min >= max)
{
Debug.LogWarning("[CreateFishManager] RandomUniqueNumbers: min >= max,返回空列表");
2026-08-04 21:20:56 +08:00
return _reusableIntResult;
2026-04-29 01:18:39 +08:00
}
count = Mathf.Min(count, max - min); // 不能超过范围
2026-08-04 21:20:56 +08:00
// ⭐ 用 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);
2026-04-29 01:18:39 +08:00
2026-08-04 21:20:56 +08:00
// ⭐ 预分配 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;
2026-04-29 01:18:39 +08:00
}
/// <summary>
/// 从 KeyCollection 中随机取 count 个不重复的 string key。
2026-08-04 21:20:56 +08:00
/// ⭐ 优化:复用静态 Random + 预分配缓冲,零 GC 分配。
/// 注意:返回的 List 是内部复用对象,调用方用完前不要再次调用本方法。
2026-04-29 01:18:39 +08:00
/// </summary>
public static List<string> RandomUniqueNumbers(int count, int min, int max,
Dictionary<string, List<RoomFishDetailConfig>>.KeyCollection keyCollection)
{
2026-08-04 21:20:56 +08:00
_reusableStrResult.Clear();
if (min >= max) return _reusableStrResult;
2026-04-29 01:18:39 +08:00
count = Mathf.Min(count, max - min);
2026-08-04 21:20:56 +08:00
// ⭐ 用 foreach 拷贝到复用缓冲,避免 keyCollection.ToList() 的 GC
_reusableKeyBuf.Clear();
foreach (var k in keyCollection) _reusableKeyBuf.Add(k);
_reusableIdxSet.Clear();
2026-04-29 01:18:39 +08:00
2026-08-04 21:20:56 +08:00
while (_reusableIdxSet.Count < count)
2026-04-29 01:18:39 +08:00
{
2026-08-04 21:20:56 +08:00
int idx = _sharedRand.Next(min, max);
if (_reusableIdxSet.Add(idx))
_reusableStrResult.Add(_reusableKeyBuf[idx]);
2026-04-29 01:18:39 +08:00
}
2026-08-04 21:20:56 +08:00
return _reusableStrResult;
2026-04-29 01:18:39 +08:00
}
2026-05-19 17:59:23 +08:00
#endregion
2026-04-29 01:18:39 +08:00
2026-05-19 17:59:23 +08:00
#region 杀鱼
2026-04-29 01:18:39 +08:00
// ================================================================
// 杀鱼接口
// ================================================================
2026-05-19 17:59:23 +08:00
/// <summary>
/// 批量杀死多条鱼(带特效 + 不足数量用纯金币补齐)。
/// </summary>
/// <param name="guids">实际场上能找到的鱼 guid 列表(可能少于 count)</param>
/// <param name="coinNum">红包拆分后的每份金额,长度应 >= count</param>
/// <param name="v3Point">金币最终飞回的世界坐标(一般是炮台位置)</param>
/// <param name="count">理论上要“死”的总数量(鱼 + 纯金币)</param>
2026-07-20 01:44:28 +08:00
public void Id_DieListFish_FX(List<string> guids, List<RedPacketRecord> coinNum, Vector3 v3Point, int count, bool isLocal = false)
2026-05-19 17:59:23 +08:00
{
if (coinNum == null || coinNum.Count == 0) return;
2026-07-20 01:44:28 +08:00
// 1. 先处理"真的有鱼"的部分
2026-05-19 17:59:23 +08:00
int realCount = Mathf.Min(guids.Count, coinNum.Count, count);
for (int i = 0; i < realCount; i++)
{
2026-07-20 01:44:28 +08:00
Id_DieFish_FX(guids[i], coinNum[i], v3Point, isLocal);
2026-05-19 17:59:23 +08:00
}
2026-07-20 01:44:28 +08:00
// 2. 不足的部分,用"屏幕随机位置的金币特效"补齐
2026-05-19 17:59:23 +08:00
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];
2026-07-20 01:44:28 +08:00
SpawnRandomCoinFX(red, v3Point, isLocal);
2026-05-19 17:59:23 +08:00
}
}
2026-04-29 01:18:39 +08:00
/// <summary>
/// 随机选取 count 条小鱼/黄金鱼,通知服务器同步死亡,并返回 guid 列表。
/// </summary>
2026-07-23 01:51:07 +08:00
/// <param name="killerSitPos">击杀者座位 ID。Boss 爆奖连锁杀鱼时传 Boss 击杀者座位
/// (含人机);-1 表示用本地 currSitData.posID(普通玩家直接击杀场景).</param>
2026-04-29 01:18:39 +08:00
public List<string> SyncGameData_SendRendromDieFishID(
2026-07-23 01:51:07 +08:00
int count, List<RedPacketRecord> coinNum, Vector3 v3Point, int killerSitPos = -1)
2026-04-29 01:18:39 +08:00
{
2026-05-01 06:26:50 +08:00
var guids = new List<string>(count);
2026-08-04 21:20:56 +08:00
// ⭐ 优化:用 Fisher-Yates 洗牌索引代替 Keys.ToList()+RemoveAt,零 GC
// 将 keys 拷贝到复用缓冲
_reusableKeyBuf.Clear();
foreach (var k in fishDatas_hashCodekey_Small.Keys) _reusableKeyBuf.Add(k);
2026-04-29 01:18:39 +08:00
2026-08-04 21:20:56 +08:00
int keyCount = _reusableKeyBuf.Count;
if (keyCount <= count)
2026-04-29 01:18:39 +08:00
{
2026-08-04 21:20:56 +08:00
// 若存活鱼不足 count,全部选取
for (int i = 0; i < keyCount; i++) guids.Add(_reusableKeyBuf[i]);
2026-04-23 17:24:31 +08:00
}
else
{
2026-08-04 21:20:56 +08:00
// ⭐ Fisher-Yates 洗牌:只洗前 count 个位置,避免全量洗牌
2026-04-23 17:24:31 +08:00
for (int i = 0; i < count; i++)
{
2026-08-04 21:20:56 +08:00
int j = Random.Range(i, keyCount);
// 交换 i 和 j
var tmp = _reusableKeyBuf[i];
_reusableKeyBuf[i] = _reusableKeyBuf[j];
_reusableKeyBuf[j] = tmp;
guids.Add(_reusableKeyBuf[i]);
2026-04-23 17:24:31 +08:00
}
}
2026-04-29 01:18:39 +08:00
2026-07-23 01:51:07 +08:00
// ⭐ sitPos 用击杀者座位(含人机);房主代人机击杀 Boss 时,这里传人机座位,
// 否则所有客户端都会判定为房主击杀 → 房主自己看金色(错)
int posID = killerSitPos >= 0 ? killerSitPos : currGameType.currSitData.posID;
2026-04-29 01:18:39 +08:00
var sgd = new SyncGameData
2026-04-23 17:24:31 +08:00
{
2026-05-01 06:26:50 +08:00
sitPos = posID,
2026-04-29 01:18:39 +08:00
frameType = FrameDataType.randomDieFish,
2026-05-01 06:26:50 +08:00
time = BeijingTimeManager.Instance.CurrentTimestampMs,
2026-04-29 01:18:39 +08:00
frameData = new FrameData_RandomDieFish
2026-04-23 17:24:31 +08:00
{
2026-05-01 06:26:50 +08:00
x = v3Point.x,
y = v3Point.y,
z = v3Point.z,
guids = guids,
2026-04-29 01:18:39 +08:00
coinNum = coinNum,
2026-05-01 06:26:50 +08:00
count = count
2026-04-29 01:18:39 +08:00
}
};
2026-04-23 17:24:31 +08:00
2026-04-29 01:18:39 +08:00
WebSocketSendList.SyncGameData(sgd);
return guids;
2026-04-23 17:24:31 +08:00
}
2026-04-29 01:18:39 +08:00
2026-08-22 17:23:07 +08:00
/// <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;
int posID = 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);
}
2026-08-27 15:44:36 +08:00
/// <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)
{
2026-08-29 01:02:09 +08:00
// ⭐ 必须完整清锁(ClearLock 置 isLock=false/隐藏锁定圈/重置 isManualLock/炮台回Idle),
// 不能只清两个引用:isLock 残留 true 会让 GameMainPanel_FishBase.HandleAutoRotate
// 对空 monCotl_Lock_Position 取 .position 每帧抛 NRE,
// 排在其后的 CheckLockTargetValid/HandleAutoLock 永远执行不到 → 锁定系统卡死
sit.ClearLock();
2026-08-27 15:44:36 +08:00
}
}
}
2026-04-29 01:18:39 +08:00
/// <summary>
2026-05-01 06:26:50 +08:00
/// 鱼离场
2026-04-29 01:18:39 +08:00
/// </summary>
2026-04-22 16:22:28 +08:00
public void Id_DieFish_NotFX(string fishId)
2026-03-31 17:00:05 +08:00
{
2026-08-01 00:49:49 +08:00
if (!fishDatas_hashCodekey.TryGetValue(fishId, out var monster))
{
2026-08-04 21:20:56 +08:00
#if UNITY_EDITOR
// ⭐ 优化:调试日志用 #if UNITY_EDITOR 包裹,Release 包零 GC
// 原代码 Where().Take().ToList() + string.Join + $"" 每次 NotFound 都产生大量 GC
2026-08-01 00:49:49 +08:00
Debug.LogWarning($"[CreateFishManager] Id_DieFish_NotFX 找不到鱼: fishId={fishId}, 字典数量={fishDatas_hashCodekey.Count}");
string baseGuid = fishId.Contains("_") ? fishId.Split('_')[0] : fishId;
2026-08-04 21:20:56 +08:00
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
2026-08-01 00:49:49 +08:00
return;
}
2026-08-27 15:44:36 +08:00
// ⭐ 标记死亡:离场回收的鱼同样不可被命中/锁定(回池 OnEnable 会重置)
monster.isDead = true;
// ⭐ 清除座位锁定引用:炮台不再对着已离场的鱼
ClearLockOnFish(fishId);
2026-05-01 06:26:50 +08:00
Pool.Instance.Return(monster.gameObject);
2026-05-01 21:59:16 +08:00
switch (monster.fishenum)
{
case FishTypeEnum.普通鱼:
fishDatas_hashCodekey_Small.Remove(fishId);
2026-04-29 01:18:39 +08:00
2026-05-01 21:59:16 +08:00
break;
case FishTypeEnum.Boss鱼:
fishDatas_hashCodekey_Boss.Remove(fishId);
break;
case FishTypeEnum.黄金鱼:
2026-05-22 19:30:02 +08:00
fishDatas_hashCodekey_HJ.Remove(fishId);
2026-05-01 21:59:16 +08:00
break;
case FishTypeEnum.特殊鱼:
2026-05-22 19:30:02 +08:00
fishDatas_hashCodekey_TS.Remove(fishId);
2026-05-01 21:59:16 +08:00
2026-05-22 19:30:02 +08:00
break;
case FishTypeEnum.特殊Boss:
fishDatas_hashCodekey_TS_Boss.Remove(fishId);
2026-05-01 21:59:16 +08:00
break;
default:
break;
}
2026-04-29 01:18:39 +08:00
fishDatas_hashCodekey.Remove(fishId);
2026-07-28 11:35:36 +08:00
2026-08-01 00:49:49 +08:00
// ⭐ 记录成功回收日志
GameManager.LogFishFlow("REMOVE", fishId,
$"fishNumber={monster.monsterData?.fishTypeConfig?.fishNumber} isyz={monster.isyz} yzID={monster.yzID} dictCount={fishDatas_hashCodekey.Count}",
monster.GetInstanceID());
2026-07-28 11:35:36 +08:00
#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
2026-04-03 18:32:19 +08:00
}
2026-04-29 01:18:39 +08:00
/// <summary>
/// 按 ID 杀死一条鱼(带特效 + 金币飞出动画)。
/// </summary>
2026-07-20 01:44:28 +08:00
public void Id_DieFish_FX(string fishId, RedPacketRecord coinNum, Vector3 v3Point, bool isLocal = false)
2026-04-22 18:15:22 +08:00
{
2026-08-01 00:49:49 +08:00
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());
2026-08-27 15:44:36 +08:00
// ⭐ 统一标记死亡:死亡后子弹不再命中、不再被锁定
monster.isDead = true;
// ⭐ 清除座位锁定引用:炮台不再对着死亡的鱼
ClearLockOnFish(fishId);
2026-08-15 21:53:24 +08:00
// 👁 调试:记录每架掉金币飞机的实际坐标,便于对比"其他位置掉金币"来源
Debug.Log($"[掉金币坐标] fishId={fishId} 飞机坐标={monster.transform.position} 金币飞向={v3Point} isLocal={isLocal} 调用栈={System.Environment.StackTrace}");
2026-06-14 02:18:37 +08:00
if (false)
{
AudioManager.Instance.PlayerFX(60004);
2026-04-29 01:18:39 +08:00
2026-06-14 02:18:37 +08:00
}
2026-04-29 01:18:39 +08:00
2026-08-04 21:20:56 +08:00
// 死亡粒子特效(池化,粒子播完自动回池)
EffectPool.Instance.Get(
2026-04-29 01:18:39 +08:00
monster.effect_Die != null ? monster.effect_Die : dieFXPrefab,
monster.transform.position, Quaternion.identity, transform.parent);
// 非 Boss 鱼:金币飞向炮台
2026-06-21 00:07:26 +08:00
if (monster.fishenum != FishTypeEnum.Boss鱼 && monster.fishenum != FishTypeEnum.特殊Boss)
2026-04-22 18:15:22 +08:00
{
2026-08-04 21:20:56 +08:00
// ⭐ 池化:动画事件(coinEvent/ScoreNumScrollEvent)播完手动回池
var award = EffectPool.Instance.Get(
2026-04-29 01:18:39 +08:00
monster.effect_DieAward != null ? monster.effect_DieAward : dieFXPrefabAward,
2026-08-04 21:20:56 +08:00
monster.transform.position, Quaternion.identity, currGameType.subPanelParent.parent,
autoReturn: false);
2026-04-23 17:24:31 +08:00
2026-04-29 01:18:39 +08:00
award.GetComponent<RectTransform>().anchoredPosition =
CoordinateTransformation.WorldPos_ToCanvas(
monster.transform.position,
2026-08-04 21:20:56 +08:00
GetUiCanvas());
2026-04-22 18:15:22 +08:00
2026-05-22 19:30:02 +08:00
var coinScript = award.GetComponent<BJBase>();
2026-07-29 22:59:26 +08:00
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)
{
2026-08-29 22:11:10 +08:00
// ⭐ 闪电起点=鱼死亡位置(3D 世界坐标)。award 挂 UI 画布下,其 position 是画布坐标(≈屏幕中心)不可用
tsy.startPoint = monster.transform.position;
2026-07-29 22:59:26 +08:00
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);
}
2026-08-29 22:11:10 +08:00
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;
}
2026-07-29 22:59:26 +08:00
tsy.SetCoinCount();
}
}
2026-04-22 18:15:22 +08:00
}
2026-04-29 01:18:39 +08:00
2026-05-01 06:26:50 +08:00
//MonsterDieList.Instance.SmallHuangYiHuang(
// monster.transform.position,
// monster.monsterData.fishTypeConfig.fishNumber, 1000);
2026-04-29 01:18:39 +08:00
monster.DieFish();
fishDatas_hashCodekey.Remove(fishId);
2026-07-28 11:35:36 +08:00
#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
2026-04-22 18:15:22 +08:00
}
2026-05-04 17:21:52 +08:00
/// <summary>
/// 不依赖鱼,只在屏幕内随机一个位置生成金币特效,然后飞回指定点。
/// </summary>
2026-08-01 00:49:49 +08:00
public void SpawnRandomCoinFX(RedPacketRecord coinNum, Vector3 targetWorldPos, bool isLocal = false)
2026-05-04 17:21:52 +08:00
{
// 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,
2026-08-04 21:20:56 +08:00
GetUiCanvas());
2026-05-04 17:21:52 +08:00
2026-08-04 21:20:56 +08:00
// 3. 实例化金币特效(池化,动画事件播完手动回池)
var award = EffectPool.Instance.Get(
dieFXPrefabAward,
targetWorldPos, Quaternion.identity, currGameType.subPanelParent.parent,
autoReturn: false);
2026-05-04 17:21:52 +08:00
var rect = award.GetComponent<RectTransform>();
rect.anchoredPosition = canvasPos;
// 4. 设置 CoinScript 的终点为 targetWorldPos
var coinScript = award.GetComponent<CoinScript>();
2026-07-29 22:59:26 +08:00
coinScript.Init((int)coinNum.Amount, targetWorldPos);
2026-05-19 17:59:23 +08:00
coinScript.isBoss = true;
2026-07-20 01:44:28 +08:00
coinScript.isLocalCoin = isLocal;
2026-05-22 19:30:02 +08:00
coinScript.SetCoinCount();
2026-05-04 17:21:52 +08:00
}
2026-04-22 23:08:46 +08:00
2026-05-22 19:30:02 +08:00
2026-05-04 17:21:52 +08:00
2026-04-29 01:18:39 +08:00
/// <summary>
/// 收到服务器 WebSocket 杀鱼通知,处理金币加分、特效、音效。
/// </summary>
2026-03-31 17:00:05 +08:00
public void Ws_DieFish(string msg)
2026-02-03 11:35:19 +08:00
{
2026-04-29 01:18:39 +08:00
var killFish = JsonConvert.DeserializeObject<WebSocketResponseBase<RoomKillNotice>>(msg)
.arguments[0];
2026-04-25 18:53:04 +08:00
2026-08-04 21:20:56 +08:00
NetLog.Log($"[CreateFishManager] 杀鱼延迟:{DateTimeOffset.Now.ToUnixTimeSeconds() - killFish.timespan}s");
2026-05-22 19:30:02 +08:00
SitPosData sitData = null; //击杀玩家座位数据
2026-05-01 06:26:50 +08:00
foreach (var item in currGameType.list_Sits)
{
2026-05-01 21:59:16 +08:00
if (item.user == killFish.userId)
2026-05-01 06:26:50 +08:00
{
//item.lockFX.gameObject.SetActive(false);
2026-05-22 19:30:02 +08:00
sitData = item;
2026-05-01 06:26:50 +08:00
}
}
2026-08-27 15:44:36 +08:00
// ⭐ 死鱼清除所有座位的锁定引用(原逻辑只清击杀者座位,其他座位锁同一条鱼时不清)
ClearLockOnFish(killFish.fishId);
2026-05-01 21:59:16 +08:00
2026-05-22 19:30:02 +08:00
2026-04-29 01:18:39 +08:00
if (!fishDatas_hashCodekey.TryGetValue(killFish.fishId, out var monster)) return;
2026-05-18 18:39:11 +08:00
2026-08-27 15:44:36 +08:00
// ⭐ 统一标记死亡:死亡后子弹不再命中、不再被锁定(含不调 base.DieFish 的 Boss 子类)
monster.isDead = true;
2026-05-19 17:59:23 +08:00
2026-08-04 21:20:56 +08:00
// 死亡粒子特效(池化,粒子播完自动回池)
EffectPool.Instance.Get(
2026-04-29 01:18:39 +08:00
monster.effect_Die != null ? monster.effect_Die : dieFXPrefab,
monster.transform.position, Quaternion.identity, transform.parent);
2026-08-09 17:57:46 +08:00
long goldToAdd = killFish.gold; // 立即复制需要的数据
2026-06-15 18:37:06 +08:00
string fishId = killFish.fishId; // 如果需要的话
Action<int> onScoreAdd = (audioID) =>
2026-04-29 01:18:39 +08:00
{
2026-07-29 17:17:14 +08:00
long oldGold = sitData.goldNum; // 记录加金币前的值
2026-06-15 18:37:06 +08:00
sitData.goldNum += goldToAdd; // 使用副本,不再依赖 killFish 对象
2026-05-22 19:30:02 +08:00
sitData.tMP_GoldNum.text = sitData.goldNum.ToString();
AudioManager.Instance.PlayerFX(audioID);
2026-07-29 22:59:26 +08:00
// ⭐ 记录金币流水:已加钱(杀鱼获得,本地已执行)
GameManager.LogGoldFlowAppliedGain("KillFish", $"击中鱼原有金币={oldGold} 增加金币=+{goldToAdd} 剩余金币={sitData.goldNum} 鱼ID={fishId} 座位={sitData.posID} 房间={GameManager.Instance.gameData?.currRoomData?.roomId}");
2026-05-22 19:30:02 +08:00
};
2026-06-15 18:37:06 +08:00
//// 加分回调(在金币/加分面板飞完后执行)
//Action<int> onScoreAdd = (audioID) =>
//{
// var sitList = GetCurrentPanelSitList();
// sitData.goldNum += killFish.gold;
// sitData.tMP_GoldNum.text = sitData.goldNum.ToString();
// AudioManager.Instance.PlayerFX(audioID);
//};
2026-07-20 22:32:52 +08:00
bool dieFishCalled = false; // Boss case 内部已调用 DieFish,底部跳过
2026-06-15 18:37:06 +08:00
2026-05-22 19:30:02 +08:00
switch (monster.fishenum)
2026-04-29 01:18:39 +08:00
{
2026-05-22 19:30:02 +08:00
case FishTypeEnum.普通鱼:
2026-05-01 06:26:50 +08:00
{
2026-08-04 21:20:56 +08:00
// 普通鱼:金币飞向炮台(池化,动画事件播完手动回池)
var award = EffectPool.Instance.Get(
2026-05-22 19:30:02 +08:00
monster.effect_DieAward != null ? monster.effect_DieAward : dieFXPrefabAward,
2026-08-04 21:20:56 +08:00
monster.transform.position, Quaternion.identity, currGameType.subPanelParent.parent,
autoReturn: false);
2026-05-22 19:30:02 +08:00
try
{
award.GetComponent<RectTransform>().anchoredPosition =
CoordinateTransformation.WorldPos_ToCanvas(
monster.transform.position,
2026-08-04 21:20:56 +08:00
GetUiCanvas());
2026-05-22 19:30:02 +08:00
}
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);
2026-07-20 01:44:28 +08:00
coinScript.isLocalCoin = sitData.user == GameManager.Instance.user.id;
2026-05-22 19:30:02 +08:00
coinScript.isBoss = false;
coinScript.JiaFen = onScoreAdd;
2026-06-16 23:45:31 +08:00
coinScript.SetCoinCount();
2026-05-22 19:30:02 +08:00
break;
2026-05-01 06:26:50 +08:00
}
2026-05-01 21:59:16 +08:00
2026-05-22 19:30:02 +08:00
case FishTypeEnum.Boss鱼:
{
2026-08-29 22:11:10 +08:00
// ⭐ 防御:配置了 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;
}
2026-05-22 19:30:02 +08:00
// Boss 鱼:显示浮动分数文本
2026-07-30 22:09:55 +08:00
// ⭐ 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;
}
2026-05-22 19:30:02 +08:00
bossCtrl.v3 = sitData.transform.position;
2026-08-03 17:00:34 +08:00
bossCtrl.JiaFen = WrapJiaFenWithBaoJiangEnd(onScoreAdd, killFish, sitData, isSelfKill_Boss);
2026-07-23 01:51:07 +08:00
bossCtrl.isLocalCoin = sitData.user == GameManager.Instance.user.id; // ⭐ 金/银区分
bossCtrl.killerSitPos = sitData.posID; // ⭐ 爆奖杀小鱼用击杀者座位(含人机)
2026-07-20 22:32:52 +08:00
monster.DieFish(killFish); // Boss 重写 action_effect_DieAward,捕获上面的 JiaFen
dieFishCalled = true;
2026-06-02 14:57:23 +08:00
if (bossCtrl.isToUI)
{
2026-08-04 21:20:56 +08:00
// ⭐ 池化:UI 空间 Boss 爆奖,动画事件播完手动回池
bossCtrl.currEffect_DieAward = EffectPool.Instance.Get(
2026-06-02 14:57:23 +08:00
monster.effect_DieAward != null ? monster.effect_DieAward : dieFXPrefabAwardTmp,
2026-08-04 21:20:56 +08:00
monster.transform.position, Quaternion.identity, currGameType.subPanelParent.parent,
autoReturn: false);
2026-06-02 14:57:23 +08:00
}
else
{
2026-08-04 21:20:56 +08:00
// isToUI=false:世界空间特效,无 UI 父对象 → 挂 bjEffectRoot 统一管理(池化)
bossCtrl.currEffect_DieAward = EffectPool.Instance.Get(
2026-07-25 21:04:58 +08:00
monster.effect_DieAward != null ? monster.effect_DieAward : dieFXPrefabAwardTmp,
2026-08-04 21:20:56 +08:00
monster.transform.position, Quaternion.identity, bjEffectRoot,
autoReturn: false);
2026-06-02 14:57:23 +08:00
}
2026-05-22 19:30:02 +08:00
bossCtrl.action_effect_DieAward?.Invoke(bossCtrl.currEffect_DieAward);
}
break;
case FishTypeEnum.黄金鱼:
{
2026-08-04 21:20:56 +08:00
// 黄金鱼:金币飞向炮台(池化,动画事件播完手动回池)
var award = EffectPool.Instance.Get(
2026-05-22 19:30:02 +08:00
monster.effect_DieAward != null ? monster.effect_DieAward : dieFXPrefabAward,
2026-08-04 21:20:56 +08:00
monster.transform.position, Quaternion.identity, currGameType.subPanelParent.parent,
autoReturn: false);
2026-05-22 19:30:02 +08:00
try
{
award.GetComponent<RectTransform>().anchoredPosition =
CoordinateTransformation.WorldPos_ToCanvas(
monster.transform.position,
2026-08-04 21:20:56 +08:00
GetUiCanvas());
2026-05-22 19:30:02 +08:00
}
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);
2026-07-20 01:44:28 +08:00
coinScript.isLocalCoin = sitData.user == GameManager.Instance.user.id;
2026-05-22 19:30:02 +08:00
coinScript.isBoss = false;
coinScript.JiaFen = onScoreAdd;
2026-06-16 23:45:31 +08:00
coinScript.SetCoinCount();
2026-05-22 19:30:02 +08:00
}
break;
case FishTypeEnum.特殊鱼:
{
2026-07-30 22:09:55 +08:00
// ⭐ 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;
}
}
}
2026-08-04 21:20:56 +08:00
// 特殊鱼:金币飞向炮台(池化,动画事件播完手动回池)
var award = EffectPool.Instance.Get(
monster.effect_DieAward != null ? monster.effect_DieAward : dieFXPrefabAward,
monster.transform.position, Quaternion.identity, currGameType.subPanelParent.parent,
autoReturn: false);
2026-05-22 19:30:02 +08:00
try
{
award.GetComponent<RectTransform>().anchoredPosition =
CoordinateTransformation.WorldPos_ToCanvas(
monster.transform.position,
2026-08-04 21:20:56 +08:00
GetUiCanvas());
2026-05-22 19:30:02 +08:00
}
catch
{
award.transform.position = monster.transform.position;
}
var coinScript = award.GetComponent<BJBase>();
2026-07-29 22:59:26 +08:00
if (coinScript != null)
{
// 普通爆奖(BJBase 体系)
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)
{
2026-08-29 22:11:10 +08:00
// ⭐ 闪电起点=鱼死亡位置(3D 世界坐标)。award 挂 UI 画布下,其 position 是画布坐标(≈屏幕中心)不可用
tsy.startPoint = monster.transform.position;
2026-07-29 22:59:26 +08:00
tsy.isLocalCoin = sitData.user == GameManager.Instance.user.id;
tsy.monster = monster;
tsy.coinCount = killFish.gold;
2026-07-30 22:09:55 +08:00
// 统一注入击杀者座位信息(posID/paoBei 供 GetSeatPosition/范围杀鱼金币计算使用)
tsy.posID = sitData.posID;
tsy.paoBei = (int)sitData.consumeGoldNum;
2026-08-28 18:57:36 +08:00
// 倍数:服务端返回的爆奖倍数(小鱼模式 BJEndUI 结算显示用;未下发时保持默认 1)
tsy.bJbeishu = killFish.beishu > 0 ? killFish.beishu : 1;
2026-07-29 22:59:26 +08:00
if (tsy.baoJiangMode == TSYBaoJiangMode.Boss爆奖)
{
tsy.InitBossAward(killFish.gold, sitData.posID,
(int)sitData.consumeGoldNum,
monster.monsterData.fishTypeConfig.fishScore, 1, onScoreAdd);
}
else
{
2026-08-29 22:11:10 +08:00
// ⭐ 小鱼模式也调 Init:注入参数 + 缓存 _targetSit(座位)+ JiaFen
tsy.Init(killFish.gold, sitData.posID,
(int)sitData.consumeGoldNum,
monster.monsterData.fishTypeConfig.fishScore,
killFish.beishu > 0 ? killFish.beishu : 1, onScoreAdd);
2026-07-29 22:59:26 +08:00
tsy.JiaFenEx = onScoreAdd;
2026-08-29 22:11:10 +08:00
// 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;
2026-07-29 22:59:26 +08:00
}
tsy.SetCoinCount();
}
}
2026-05-22 19:30:02 +08:00
2026-06-16 23:45:31 +08:00
2026-06-21 00:07:26 +08:00
// // Boss 鱼:显示浮动分数文本
// var bossCtrl = (MonsterControl_BossBase)monster;
2026-06-16 23:45:31 +08:00
2026-06-21 00:07:26 +08:00
// 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;
2026-05-22 19:30:02 +08:00
}
break;
case FishTypeEnum.特殊Boss:
{
// Boss 鱼:显示浮动分数文本
var bossCtrl = (MonsterControl_BossBase)monster;
2026-07-30 22:09:55 +08:00
// ⭐ 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;
}
2026-05-22 19:30:02 +08:00
bossCtrl.v3 = sitData.transform.position;
2026-08-03 17:00:34 +08:00
bossCtrl.JiaFen = WrapJiaFenWithBaoJiangEnd(onScoreAdd, killFish, sitData, isSelfKill_TBoss);
2026-07-23 01:51:07 +08:00
bossCtrl.isLocalCoin = sitData.user == GameManager.Instance.user.id; // ⭐ 金/银区分
bossCtrl.killerSitPos = sitData.posID; // ⭐ 爆奖杀小鱼用击杀者座位(含人机)
2026-07-20 22:32:52 +08:00
monster.DieFish(killFish); // Boss 重写 action_effect_DieAward
dieFishCalled = true;
2026-05-22 19:30:02 +08:00
2026-06-02 14:57:23 +08:00
if (bossCtrl.isToUI)
{
2026-08-04 21:20:56 +08:00
// ⭐ 池化:UI 空间 Boss 爆奖,动画事件播完手动回池
bossCtrl.currEffect_DieAward = EffectPool.Instance.Get(
2026-06-02 14:57:23 +08:00
monster.effect_DieAward != null ? monster.effect_DieAward : dieFXPrefabAwardTmp,
2026-08-04 21:20:56 +08:00
monster.transform.position, Quaternion.identity, currGameType.subPanelParent.parent,
autoReturn: false);
2026-06-02 14:57:23 +08:00
}
else
{
2026-08-04 21:20:56 +08:00
// isToUI=false:世界空间特效,无 UI 父对象 → 挂 bjEffectRoot 统一管理(池化)
bossCtrl.currEffect_DieAward = EffectPool.Instance.Get(
2026-07-25 21:04:58 +08:00
monster.effect_DieAward != null ? monster.effect_DieAward : dieFXPrefabAwardTmp,
2026-08-04 21:20:56 +08:00
monster.transform.position, Quaternion.identity, bjEffectRoot,
autoReturn: false);
2026-06-02 14:57:23 +08:00
}
2026-05-22 19:30:02 +08:00
bossCtrl.action_effect_DieAward?.Invoke(bossCtrl.currEffect_DieAward);
}
break;
default:
break;
2026-04-29 01:18:39 +08:00
}
2026-07-20 22:32:52 +08:00
if (!dieFishCalled)
monster.DieFish(killFish);
2026-07-20 16:30:28 +08:00
2026-04-29 01:18:39 +08:00
MonsterDieList.Instance.SmallHuangYiHuang(
monster.transform.position,
monster.monsterData.fishTypeConfig.fishNumber, 1000);
}
2026-05-19 17:59:23 +08:00
#endregion
2026-04-29 01:18:39 +08:00
2026-08-03 17:00:34 +08:00
// ================================================================
// 工具:包装 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);
}
};
}
2026-04-29 01:18:39 +08:00
// ================================================================
// 工具:获取当前面板座位列表
// ================================================================
/// <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;
2026-05-07 03:46:03 +08:00
case GameType.YYSC:
return UIManager.Instance.Get<GameMainPanel_FishBase>(UIPanel.MainGamePanelYYSC).list_Sits;
2026-04-29 01:18:39 +08:00
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;
2026-02-03 11:35:19 +08:00
}
}
2026-05-28 18:16:45 +08:00
#region 冰冻技能
/// <summary>
/// 冰冻:记录所有鱼当前状态并强制切到 Idle(暂停移动+动画)
/// </summary>
public void FreezeAllFish()
{
2026-07-25 21:04:58 +08:00
// ⭐ 守卫:已冻结时直接返回,避免重复调用覆盖已记录的状态
// (其他客户端会先后被 OnUsePropNotice 和 SyncGameData_FreezeFish 触发两次)
if (isFrozen) return;
2026-05-28 18:16:45 +08:00
isFrozen = true;
2026-07-24 23:47:50 +08:00
// ⭐ 两个字典必须同步清空,否则 frozenFishIsMoves 残留旧数据
// 会导致下次解冻时用旧值覆盖 isMove 但不恢复 currState
2026-05-28 18:16:45 +08:00
frozenFishStates.Clear();
2026-07-24 23:47:50 +08:00
frozenFishIsMoves.Clear();
2026-05-28 18:16:45 +08:00
// 只遍历全量字典 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;
2026-06-21 00:07:26 +08:00
frozenFishIsMoves[m.guid] = m.isMove;
2026-05-28 18:16:45 +08:00
// 定格
m.isMove = false;
m.ChangeState(CharacterStateType.idle);
2026-06-16 19:12:37 +08:00
m.SetFreezeEffect(true);
2026-05-28 18:16:45 +08:00
}
}
}
/// <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;
2026-06-16 19:12:37 +08:00
m.SetFreezeEffect(false);
2026-07-25 21:04:58 +08:00
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);
}
2026-05-28 18:16:45 +08:00
}
}
2026-07-24 23:47:50 +08:00
// ⭐ 两个字典同步清空
2026-05-28 18:16:45 +08:00
frozenFishStates.Clear();
2026-07-24 23:47:50 +08:00
frozenFishIsMoves.Clear();
2026-05-28 18:16:45 +08:00
}
#endregion
2026-02-03 11:35:19 +08:00
}
#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