Files
5.24BuYu/Assets/MyGame/Scripts/Manager/CreateFishManager.cs
T
2026-07-28 10:59:08 +08:00

1742 lines
70 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 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 爆奖特效统一父节点。
/// 仅装载那些"原本挂在 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 时整节点销毁。
/// </summary>
public Transform 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";
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>();
// ------ 各类计时器 ------
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;
// ================================================================
// 懒加载属性
// ================================================================
/// <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>
/// 房间鱼配置(按游戏类型),首次访问时触发懒加载初始化。
/// </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 键手动出鱼(出 Inspector 配置的 debugSpawnFishNumber)
// 使用新版输入系统(Input System):Keyboard.current.wasPressedThisFrame
if (UnityEngine.InputSystem.Keyboard.current != null &&
UnityEngine.InputSystem.Keyboard.current.mKey.wasPressedThisFrame)
{
if (string.IsNullOrEmpty(debugSpawnFishNumber))
{
Debug.LogWarning("[CreateFishManager] 未配置 debugSpawnFishNumber,请在 Inspector 设置鱼编号");
return;
}
ManualSpawn(debugSpawnFishNumber);
}
}
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)
{
InitSpawnSystem(gameType);
BindCurrentGamePanel();
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;
}
/// <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; // 冰冻中不创建鱼
if (spawnDic == null) return; // 场景卸载后忽略
foreach (var item in data.fish)
{
if (item == null) continue;
//BeijingTimeManager.Instance.SyncBeijingTime(() =>
//{
// Debug.Log("差值:" + (BeijingTimeManager.Instance.CurrentTimestampMs - item.createdAt) / 1000);
// Debug.Log("差值:" + (BeijingTimeManager.Instance.CurrentTimestampMs - item.createdAt));
// Debug.Log("差值:" + BeijingTimeManager.Instance.CurrentTimestampMs + "创建\n" + item.createdAt);
//});
Debug.Log("差值:" + (BeijingTimeManager.Instance.CurrentTimestampMs - item.createdAt) / 1000);
Debug.Log("差值:" + (BeijingTimeManager.Instance.CurrentTimestampMs - item.createdAt));
Debug.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)) continue;
// guid 用服务端 fishId
string guid = item.fishId.ToString();
// guid 去重:已有此 guid 的鱼跳过
if (!string.IsNullOrEmpty(guid) &&
fishDatas_hashCodekey != null &&
fishDatas_hashCodekey.ContainsKey(guid))
continue;
// 构造 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;
//if (data.type == "sync")
spawnTime = item.createdAt;
//else
// spawnTime = BeijingTimeManager.Instance.CurrentTimestampMs;
SpawnFishMainThread(createFishData, spawnTime);
// ⭐ expireAt 过期自动移除暂不启用:
// - 走路线的鱼:OnMoveUpdata 里 moveTime>=moveLine.moveTime 时自动 Id_DieFish_NotFX
// - 原点不动的鱼(Boss):正常播放结束动画后销毁
// 后续若需要服务端强制过期销毁,再启用此协程
// if (item.remainingSeconds > 0)
// StartCoroutine(ExpireFishCoroutine(guid, item.remainingSeconds));
}
Debug.Log($"[CreateFishManager] OnFishSpawn type={data.type}, count={data.fish.Count}");
}
/// <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)
{
fishDatas_hashCodekey[mc.guid] = mc;
switch (fishTypeId)
{
case 1: // 普通鱼
fishDatas_hashCodekey_Small[mc.guid] = mc;
break;
case 2: // Boss
fishDatas_hashCodekey_Boss[mc.guid] = mc;
break;
case 3: // 特殊鱼 / 黄金鱼
fishDatas_hashCodekey_HJ[mc.guid] = mc;
break;
case 4:
fishDatas_hashCodekey_TS[mc.guid] = mc;
break;
case 5:
fishDatas_hashCodekey_TS_Boss[mc.guid] = mc;
break;
}
}
#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;
}
var prefab = FishResourcesData.GetFishPrefab(int.Parse(fishType.fishNumber));
var pos = new Vector3(9999, 9999, 0);
string prefix = yz.arrayNumber.Length >= 3
? yz.arrayNumber.Substring(0, 3)
: yz.arrayNumber;
switch (prefix)
{
case "700": // 扇形阵:逐条延迟生成
StartCoroutine(IeShanXing(createFishData, yz, pos, prefab, pathList, time, yz.fishCount));
break;
case "701": // 环形阵:同帧同路径全部生成
foreach (int pathId in pathList)
Quan(createFishData, yz, pos, prefab, pathId, yz.fishCount, time);
break;
case "702":
case "7007":
case "710": // 顺序阵:每条路径逐一延迟生成
case "704":
//int chosen710 = (prefix == "710")
// ? pathList[Random.Range(0, pathList.Count)]
// : -1;
StartCoroutine(YZ7007(createFishData, yz, pos, prefab, time));
//foreach (int pathId in pathList)
//{
// int actualPath = (prefix == "710") ? chosen710 : pathId;
// StartCoroutine(YZ7007(createFishData, yz, pos, prefab, actualPath, time, yz.fishCount));
//}
break;
default:
Debug.LogWarning($"[CreateFishManager] 未知鱼阵前缀: {prefix}");
break;
}
}
// ================================================================
// 鱼阵生成协程 / 方法
// ================================================================
/// <summary>扇形阵:按路线列表逐条延迟 0.8s 生成一扇。</summary>
private IEnumerator IeShanXing(FrameData_CreateFish createFishData, FishArrayConfig yz,
Vector3 pos, GameObject prefab, List<int> pathList, long time, int count)
{
foreach (int pathId in pathList)
{
if (!isResources)
{
yield break;
}
ShanXing(createFishData, yz, pos, prefab, pathId, count, time);
yield return new WaitForSeconds(0.8f);
}
}
/// <summary>
/// 扇形阵单扇:生成 16 条鱼,每条旋转角度递增(±5°交替),模拟扇形展开。
/// </summary>
private void ShanXing(FrameData_CreateFish createFishData, FishArrayConfig yz,
Vector3 pos, GameObject prefab, int pathId, int count, long time)
{
var cfg = spawnDic[createFishData.fishNumber];
for (int i = 1; i <= 16; i++)
{
// 交替正负角度,形成扇形分布
float angle = i * 5f * (i % 2 == 0 ? -1f : 1f);
var rotation = Quaternion.Euler(
prefab.transform.eulerAngles + Vector3.forward * angle);
//var fish = Instantiate(prefab, pos, rotation, fishRoot);
var fish = Pool.Instance.Get(int.Parse(yz.fishNumber), pos, rotation, fishRoot);
var mc = fish.GetComponent<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;
mc.Init(pathId.ToString(), cfg.times, true, yz.arrayNumber);
}
}
/// <summary>
/// 环形阵:在同一路径上生成 count 条鱼,角度均匀分布(360/count)。
/// </summary>
private void Quan(FrameData_CreateFish createFishData, FishArrayConfig yz,
Vector3 pos, GameObject prefab, int pathId, int count, long time)
{
if (count <= 0) return;
var cfg = spawnDic[createFishData.fishNumber];
for (int i = 0; i < count; i++)
{
float angle = (360f / count) * i;
var rotation = Quaternion.Euler(prefab.transform.eulerAngles + Vector3.forward * angle);
//var fish = Instantiate(prefab, pos, rotation, fishRoot);
var fish = Pool.Instance.Get(int.Parse(yz.fishNumber), pos, rotation, fishRoot);
var mc = fish.GetComponent<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;
mc.Init(pathId.ToString(), cfg.times, true, yz.arrayNumber);
}
}
/// <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);
yield return new WaitForSeconds(1f);
}
}
#endregion
#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;
foreach (var item in fishDatas_hashCodekey)
{
if (item.Value.gameObject != null)
{
Destroy(item.Value.gameObject);
}
}
fishDatas_hashCodekey.Clear();
fishDatas_hashCodekey_Boss.Clear();
fishDatas_hashCodekey_Small.Clear();
foreach (Transform item in fishRoot)
{
Destroy(item.gameObject);
}
// ⭐ 销毁爆奖特效父节点:所有挂在 bjEffectRoot 下的爆奖特效
// (Boss 分数版/加分弹出/BJEndUI/特殊爆奖等) 会一并被销毁,
// 防止退出房间时残留未结束的爆奖协程/动画造成的内存泄漏
if (bjEffectRoot != null)
{
Destroy(bjEffectRoot.gameObject);
bjEffectRoot = null;
}
//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
// ================================================================
// 随机工具方法
// ================================================================
/// <summary>
/// 从 [min, max) 区间随机取 count 个不重复整数。
/// 若 min==max 返回空列表。
/// </summary>
public static List<int> RandomUniqueNumbers(int count, int min, int max)
{
if (min >= max)
{
Debug.LogWarning("[CreateFishManager] RandomUniqueNumbers: min >= max,返回空列表");
return new List<int>();
}
count = Mathf.Min(count, max - min); // 不能超过范围
var result = new HashSet<int>();
var rand = new System.Random();
while (result.Count < count)
result.Add(rand.Next(min, max));
return result.ToList();
}
/// <summary>
/// 从 KeyCollection 中随机取 count 个不重复的 string key。
/// </summary>
public static List<string> RandomUniqueNumbers(int count, int min, int max,
Dictionary<string, List<RoomFishDetailConfig>>.KeyCollection keyCollection)
{
if (min >= max) return new List<string>();
count = Mathf.Min(count, max - min);
var keyList = keyCollection.ToList();
var indices = new HashSet<int>();
var rand = new System.Random();
var result = new List<string>(count);
while (indices.Count < count)
{
int idx = rand.Next(min, max);
if (indices.Add(idx))
result.Add(keyList[idx]);
}
return result;
}
#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);
var tempKeys = fishDatas_hashCodekey_Small.Keys.ToList();
// 若存活鱼不足 count,全部选取
if (tempKeys.Count <= count)
{
guids.AddRange(tempKeys);
}
else
{
for (int i = 0; i < count; i++)
{
int idx = Random.Range(0, tempKeys.Count);
guids.Add(tempKeys[idx]);
tempKeys.RemoveAt(idx);
}
}
// ⭐ sitPos 用击杀者座位(含人机);房主代人机击杀 Boss 时,这里传人机座位,
// 否则所有客户端都会判定为房主击杀 → 房主自己看金色(错)
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 = guids,
coinNum = coinNum,
count = count
}
};
WebSocketSendList.SyncGameData(sgd);
return guids;
}
/// <summary>
/// 鱼离场
/// </summary>
public void Id_DieFish_NotFX(string fishId)
{
if (!fishDatas_hashCodekey.TryGetValue(fishId, out var monster)) return;
Pool.Instance.Return(monster.gameObject);
switch (monster.fishenum)
{
case FishTypeEnum.普通鱼:
fishDatas_hashCodekey_Small.Remove(fishId);
break;
case FishTypeEnum.Boss鱼:
fishDatas_hashCodekey_Boss.Remove(fishId);
break;
case FishTypeEnum.黄金鱼:
fishDatas_hashCodekey_HJ.Remove(fishId);
break;
case FishTypeEnum.特殊鱼:
fishDatas_hashCodekey_TS.Remove(fishId);
break;
case FishTypeEnum.特殊Boss:
fishDatas_hashCodekey_TS_Boss.Remove(fishId);
break;
default:
break;
}
fishDatas_hashCodekey.Remove(fishId);
}
/// <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)) return;
if (false)
{
AudioManager.Instance.PlayerFX(60004);
}
// 死亡粒子特效
Instantiate(
monster.effect_Die != null ? monster.effect_Die : dieFXPrefab,
monster.transform.position, Quaternion.identity, transform.parent);
// 非 Boss 鱼:金币飞向炮台
if (monster.fishenum != FishTypeEnum.Boss鱼 && monster.fishenum != FishTypeEnum.特殊Boss)
{
var award = Instantiate(
monster.effect_DieAward != null ? monster.effect_DieAward : dieFXPrefabAward,
currGameType.subPanelParent.parent);
award.GetComponent<RectTransform>().anchoredPosition =
CoordinateTransformation.WorldPos_ToCanvas(
monster.transform.position,
UIManager.Instance.uiRoot.GetComponent<Canvas>());
var coinScript = award.GetComponent<BJBase>();
coinScript.Init(coinNum.Amount, v3Point);
coinScript.isBoss = true;
coinScript.isLocalCoin = isLocal;
coinScript.SetCoinCount();
}
//MonsterDieList.Instance.SmallHuangYiHuang(
// monster.transform.position,
// monster.monsterData.fishTypeConfig.fishNumber, 1000);
monster.DieFish();
fishDatas_hashCodekey.Remove(fishId);
}
/// <summary>
/// 不依赖鱼,只在屏幕内随机一个位置生成金币特效,然后飞回指定点。
/// </summary>
private 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,
UIManager.Instance.uiRoot.GetComponent<Canvas>());
// 3. 实例化金币特效(沿用你原来的 award 逻辑)
var award = Instantiate(
dieFXPrefabAward, // 或者用某个专门的"掉金币" prefab
currGameType.subPanelParent.parent);
var rect = award.GetComponent<RectTransform>();
rect.anchoredPosition = canvasPos;
// 4. 设置 CoinScript 的终点为 targetWorldPos
var coinScript = award.GetComponent<CoinScript>();
coinScript.Init(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];
Debug.Log($"[CreateFishManager] 杀鱼延迟:{DateTimeOffset.Now.ToUnixTimeSeconds() - killFish.timespan}s");
SitPosData sitData = null; //击杀玩家座位数据
foreach (var item in currGameType.list_Sits)
{
if (item.user == killFish.userId)
{
// 只有死的鱼正好是当前锁定目标时才清除
if (item.monCotl_Lock != null && item.monCotl_Lock.guid == killFish.fishId)
item.monCotl_Lock = null;
//item.lockFX.gameObject.SetActive(false);
sitData = item;
}
}
if (!fishDatas_hashCodekey.TryGetValue(killFish.fishId, out var monster)) return;
// 死亡粒子特效
Instantiate(
monster.effect_Die != null ? monster.effect_Die : dieFXPrefab,
monster.transform.position, Quaternion.identity, transform.parent);
int goldToAdd = killFish.gold; // 立即复制需要的数据
string fishId = killFish.fishId; // 如果需要的话
Action<int> onScoreAdd = (audioID) =>
{
sitData.goldNum += goldToAdd; // 使用副本,不再依赖 killFish 对象
sitData.tMP_GoldNum.text = sitData.goldNum.ToString();
AudioManager.Instance.PlayerFX(audioID);
};
//// 加分回调(在金币/加分面板飞完后执行)
//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 = Instantiate(
monster.effect_DieAward != null ? monster.effect_DieAward : dieFXPrefabAward,
currGameType.subPanelParent.parent);
try
{
award.GetComponent<RectTransform>().anchoredPosition =
CoordinateTransformation.WorldPos_ToCanvas(
monster.transform.position,
UIManager.Instance.uiRoot.GetComponent<Canvas>());
}
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 鱼:显示浮动分数文本
var bossCtrl = (MonsterControl_BossBase)monster;
bossCtrl.v3 = sitData.transform.position;
bossCtrl.JiaFen = onScoreAdd; // 先设置 JiaFen,DieFish 的 lambda 才能捕获到
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)
{
bossCtrl.currEffect_DieAward = Instantiate(
monster.effect_DieAward != null ? monster.effect_DieAward : dieFXPrefabAwardTmp,
currGameType.subPanelParent.parent);
}
else
{
// isToUI=false:世界空间特效,无 UI 父对象 → 挂 bjEffectRoot 统一管理
bossCtrl.currEffect_DieAward = Instantiate(
monster.effect_DieAward != null ? monster.effect_DieAward : dieFXPrefabAwardTmp,
bjEffectRoot);
}
bossCtrl.action_effect_DieAward?.Invoke(bossCtrl.currEffect_DieAward);
}
break;
case FishTypeEnum.黄金鱼:
{
// 普通鱼:金币飞向炮台
var award = Instantiate(
monster.effect_DieAward != null ? monster.effect_DieAward : dieFXPrefabAward,
currGameType.subPanelParent.parent);
try
{
award.GetComponent<RectTransform>().anchoredPosition =
CoordinateTransformation.WorldPos_ToCanvas(
monster.transform.position,
UIManager.Instance.uiRoot.GetComponent<Canvas>());
}
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.特殊鱼:
{
// 普通鱼:金币飞向炮台
var award = Instantiate(
monster.effect_DieAward != null ? monster.effect_DieAward : dieFXPrefabAward);
try
{
award.GetComponent<RectTransform>().anchoredPosition =
CoordinateTransformation.WorldPos_ToCanvas(
monster.transform.position,
UIManager.Instance.uiRoot.GetComponent<Canvas>());
award.transform.SetParent(currGameType.subPanelParent.parent);
}
catch
{
award.transform.position = monster.transform.position;
}
var coinScript = award.GetComponent<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.startPoint = award.transform.position;
coinScript.isBoss = false;
coinScript.JiaFen = onScoreAdd;
coinScript.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;
bossCtrl.v3 = sitData.transform.position;
bossCtrl.JiaFen = onScoreAdd; // 先设置 JiaFen
bossCtrl.isLocalCoin = sitData.user == GameManager.Instance.user.id; // ⭐ 金/银区分
bossCtrl.killerSitPos = sitData.posID; // ⭐ 爆奖杀小鱼用击杀者座位(含人机)
monster.DieFish(killFish); // Boss 重写 action_effect_DieAward
dieFishCalled = true;
if (bossCtrl.isToUI)
{
bossCtrl.currEffect_DieAward = Instantiate(
monster.effect_DieAward != null ? monster.effect_DieAward : dieFXPrefabAwardTmp,
currGameType.subPanelParent.parent);
}
else
{
// isToUI=false:世界空间特效,无 UI 父对象 → 挂 bjEffectRoot 统一管理
bossCtrl.currEffect_DieAward = Instantiate(
monster.effect_DieAward != null ? monster.effect_DieAward : dieFXPrefabAwardTmp,
bjEffectRoot);
}
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
// ================================================================
// 工具:获取当前面板座位列表
// ================================================================
/// <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>
/// 冰冻:记录所有鱼当前状态并强制切到 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
}