Files
5.24BuYu/Assets/MyGame/Scripts/Manager/CreateFishManager.cs
T
2026-04-29 23:53:55 +08:00

1116 lines
41 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.WebResponses;
using cfg;
using Newtonsoft.Json;
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using TMPro;
using Unity.Collections;
using Unity.Jobs;
using UnityEngine;
using static UnityEditor.FilePathAttribute;
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;
/// <summary>fishNumber → FishTypeConfig(与 fishDatas 相同来源,供内部高频使用)</summary>
private Dictionary<string, FishTypeConfig> fishTypeConfigDic;
// ----------------------------------------------------------------
// 运行时鱼实例字典(guid 为 key)
// ----------------------------------------------------------------
/// <summary>全部存活鱼(包含 Boss / 小鱼 / 黄金鱼)</summary>
public Dictionary<string, MonsterContral> fishDatas_hashCodekey = new Dictionary<string, MonsterContral>();
/// <summary>Boss 鱼</summary>
public Dictionary<string, MonsterContral> fishDatas_hashCodekey_Boss = new Dictionary<string, MonsterContral>();
/// <summary>普通 / 黄金 / 特殊鱼(非 Boss)</summary>
public Dictionary<string, MonsterContral> fishDatas_hashCodekey_Small = new Dictionary<string, MonsterContral>();
// ----------------------------------------------------------------
// 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>当前房间可生成鱼列表(与 NativeArray 下标对齐)</summary>
public List<RoomFishDetailConfig> spawnList;
/// <summary>当前房间 fishNumber → RoomFishDetailConfig</summary>
public Dictionary<string, RoomFishDetailConfig> spawnDic;
/// <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=冷却中
// ------ 各类计时器 ------
private float bossTimer = 1f; // Boss 刷新倒计时(秒)
private float yzTimer; // 鱼阵刷新倒计时(秒)
private float ptTimer = 3f; // 普通鱼批量启用倒计时(秒)
private int yzCount; // 本轮同时激活阵型数量
private int ptCount; // 本轮同时启用普通鱼数量
private int yz_LineCount = 0; // 场景内鱼阵路线总数
/// <summary>是否开启放鱼(false 时 Update 不执行生成逻辑)</summary>
public bool fangzhu;
// ================================================================
// 懒加载属性
// ================================================================
/// <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; }
}
// ================================================================
// Unity 生命周期
// ================================================================
protected override void Awake()
{
base.Awake();
// 确保 FishRoot 存在
if (fishRoot == null)
fishRoot = new GameObject("FishRoot").transform;
// 监听服务器下发的创鱼事件
EventCenter.Instance.AddEventListener<SyncGameData>(
ConstEventString.SyncGameData_CreateFish, CreateFish);
// 随机初始化计时器,避免所有类型同帧触发
yzTimer = Random.Range(2f, 6f);
yzCount = Random.Range(2, 5);
ptCount = Random.Range(1, 4);
}
private void OnDestroy()
{
EventCenter.Instance.RemoveEventListener<SyncGameData>(
ConstEventString.SyncGameData_CreateFish, CreateFish);
// 释放 NativeArray,避免内存泄漏
DisposeNativeArrays();
}
// ================================================================
// Update:定时调度 + Job 驱动生成
// ================================================================
private void Update()
{
// 未初始化 或 未开启放鱼 → 跳过
if (spawnList == null || !fangzhu) return;
float dt = Time.deltaTime;
// ---- 1. 用 Job System 并行更新所有鱼的计时器 ----
var job = new FishSpawnTimerJob
{
deltaTime = dt,
timers = timers,
frequencies = frequencies,
shouldSpawn = shouldSpawn,
};
job.Schedule().Complete();
// ---- 2. Boss 定时激活 ----
TickBossSpawn(dt);
// ---- 3. 鱼阵定时激活 ----
TickFishArraySpawn(dt);
// ---- 4. 普通鱼定时批量激活 ----
TickNormalFishSpawn(dt);
// ---- 5. 遍历 shouldSpawn,向服务器发送生成请求 ----
ProcessSpawnRequests();
}
// ================================================================
// 定时 Tick 方法(由 Update 调用)
// ================================================================
/// <summary>每 25 秒随机激活一条 Boss 鱼。</summary>
private void TickBossSpawn(float dt)
{
bossTimer -= dt;
if (bossTimer > 0f) return;
bossTimer = 25f;
if (!fishTypeConfig.TryGetValue(FishTypeEnum.Boss鱼, out var bossList) || bossList.Count == 0)
return;
bossList[Random.Range(0, bossList.Count)].isCreate = 2;
}
/// <summary>每 1~3 秒随机激活若干组鱼阵。</summary>
private void TickFishArraySpawn(float dt)
{
yzTimer -= dt;
if (yzTimer > 0f) return;
yzTimer = Random.Range(1f, 3f);
yzCount = Random.Range(2, 5);
int lineCount = yz_FishConfig_Line_List.Count;
if (lineCount == 0) return;
// 随机选 yzCount 个不重复的阵型下标
var selectedIndices = RandomUniqueNumbers(
Mathf.Min(yzCount, lineCount), 0, lineCount);
foreach (int idx in selectedIndices)
{
string arrayNumber = yz_FishConfig_Line_Dic[idx];
if (!yz_FishConfig_Line.TryGetValue(arrayNumber, out var lineList) || lineList.Count == 0)
continue;
// 随机选一条配置并标记为待生成
var chosen = lineList[Random.Range(0, lineList.Count)];
chosen.isCreate = 2;
// 同步更新 spawnDic 中的状态
if (spawnDic.TryGetValue(chosen.fishNumber, out var spawnEntry))
spawnEntry.isCreate = 2;
}
}
/// <summary>每 3 秒随机批量激活若干条普通鱼。</summary>
private void TickNormalFishSpawn(float dt)
{
ptTimer -= dt;
if (ptTimer > 0f) return;
ptTimer = 3f;
ptCount = Random.Range(1, 4);
if (!fishTypeConfig.TryGetValue(FishTypeEnum.普通鱼, out var normalList) || normalList.Count == 0)
return;
// 随机选 ptCount 个不重复下标
var indices = RandomUniqueNumbers(
Mathf.Min(ptCount, normalList.Count), 0, normalList.Count);
foreach (int idx in indices)
normalList[idx].isCreate = 2;
}
/// <summary>
/// 遍历 shouldSpawn 数组,对本帧到期且被标记 isCreate==2 的鱼
/// 向服务器发送 WebSocket 生成同步包。
/// </summary>
private void ProcessSpawnRequests()
{
for (int i = 0; i < spawnList.Count; i++)
{
if (shouldSpawn[i] != 1) continue;
var fish = spawnList[i];
// isCreate==0:刚到期,重置为冷却中,等待外部计时器激活
if (fish.isCreate == 0)
{
fish.isCreate = 1; // 标记为"已处理但未激活"
continue;
}
// isCreate==2:已被激活,向服务器同步一次生成请求
if (fish.isCreate == 2)
{
SendCreateFishRequest(fish);
fish.isCreate = 0;
}
}
}
/// <summary>
/// 构建并发送生成鱼的 WebSocket 同步数据包。
/// </summary>
private void SendCreateFishRequest(RoomFishDetailConfig fish)
{
int pathId = 0;
// 普通鱼(type==1)才从 pathList 随机选路径;鱼阵(type==2)由后续逻辑决定
if (fish.type == 1)
{
var pathList = FishDatas[fish.fishNumber].pathList;
if (pathList != null && pathList.Count > 0)
pathId = pathList[Random.Range(0, pathList.Count)];
}
var sgd = new SyncGameData
{
sitPos = GameManager.Instance.gameData.roomJoinNotice.position,
frameType = FrameDataType.createFish,
time = BeijingTimeManager.Instance.CurrentTimestampMs,
frameData = new FrameData_CreateFish
{
fishNumber = fish.fishNumber,
guid = Guid.NewGuid().ToString(),
moveLineId = pathId.ToString()
}
};
WebSocketSendList.SyncGameData(sgd);
}
// ================================================================
// 初始化 / 清理
// ================================================================
/// <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);
fishTypeConfigDic = GameManager.Instance.gameConfig.fishTypeConfigs
.ToDictionary(f => f.fishNumber, f => f);
}
/// <summary>
/// 进入房间时初始化生成系统:加载当前房间配置、分配 NativeArray。
/// </summary>
public 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)
{
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.roomFishLevelList)
{
if (item.roomLevel != currRoomLevel) continue;
if (item.type == 2)
{
spawnDic[item.fishNumber] = item;
spawnList.Add(item);
TryAddToArrayLine(item);
}
else
{
AddToFishTypeConfig(item);
// 等级鱼也加入 spawnDic / spawnList
spawnDic[item.fishNumber] = item;
spawnList.Add(item);
}
}
}
// ---- 分配 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++;
}
}
/// <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);
}
/// <summary>
/// 关闭生成系统:清理列表,销毁场景内所有鱼对象。
/// </summary>
public void CloseSpawnSystem()
{
spawnList = null;
spawnDic = null;
yz_FishConfig_Line_Dic = null;
yz_FishConfig_Line = null;
yz_FishConfig_Line_Dic1 = null;
if (fishRoot == null) return;
for (int i = fishRoot.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();
}
// ================================================================
// 进退房间接口
// ================================================================
/// <summary>进入房间:初始化生成系统并绑定游戏面板。</summary>
public void EnterRoom(GameTypeEnum gameType)
{
InitSpawnSystem(gameType);
BindCurrentGamePanel();
}
/// <summary>退出房间:关闭生成系统。</summary>
public void ExitRoom()
{
CloseSpawnSystem();
}
/// <summary>根据当前 GameType 绑定对应 UI 面板到 currGameType。</summary>
private void BindCurrentGamePanel()
{
switch (GameManager.Instance.gameData.currGameType)
{
case GameType.FLZY:
currGameType = UIManager.Instance.Get<GameMainPanel_FishBase>(UIPanel.MainGamePanelFLZY);
break;
case GameType.TGXX:
currGameType = UIManager.Instance.Get<GameMainPanel_FishBase>(UIPanel.MainGamePanelTGXX);
break;
case GameType.XJMH:
currGameType = UIManager.Instance.Get<GameMainPanel_FishBase>(UIPanel.MainGamePanelXJMH);
break;
case GameType.HHWK:
currGameType = UIManager.Instance.Get<GameMainPanel_FishBase>(UIPanel.MainGamePanelHHWK);
break;
default:
break;
}
}
// ================================================================
// 网络回调:创建鱼
// ================================================================
/// <summary>
/// 收到服务器 SyncGameData 创鱼事件,反序列化并执行实例化。
/// </summary>
public void CreateFish(SyncGameData syncGameData)
{
var rotateData = JsonConvert.DeserializeObject<FrameData_CreateFish>(
syncGameData.frameData.ToString());
SpawnFishMainThread(rotateData, syncGameData.time);
}
/// <summary>
/// 主线程实例化鱼对象(单体鱼 / 鱼阵分发)。
/// </summary>
private void SpawnFishMainThread(FrameData_CreateFish createFishData, long time)
{
if (!spawnDic.TryGetValue(createFishData.fishNumber, out var cfg))
{
Debug.LogWarning($"[CreateFishManager] 找不到 spawnDic 配置: {createFishData.fishNumber}");
return;
}
if (cfg.type == 1)
{
// 单体鱼
OnlyFish(createFishData, time);
}
else if (cfg.type == 2)
{
// 鱼阵
SpawnFishArray(createFishData, cfg, time);
}
}
// ================================================================
// 单体鱼生成
// ================================================================
/// <summary>
/// 生成一条普通/Boss/黄金鱼并初始化路径。
/// </summary>
/// <returns>成功返回 true,配置缺失返回 false。</returns>
private bool OnlyFish(FrameData_CreateFish createFishData, long time)
{
if (!fishTypeConfigDic.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);
var fish = Pool.Instance.Get(int.Parse(createFishData.fishNumber), new Vector3(9999, 9999, 0), Quaternion.identity,fishRoot);
var mc = fish.GetComponent<MonsterContral>() ?? fish.AddComponent<MonsterContral>();
mc.monsterData.fishTypeConfig = FishDatas[createFishData.fishNumber];
mc.guid = createFishData.guid;
mc.moveTime = (BeijingTimeManager.Instance.CurrentTimestampMs - time) / 1000f;
// 按鱼类型放入对应字典
RegisterFishInstance(mc, fishType.fishType);
mc.Init(createFishData.moveLineId, spawnDic[createFishData.fishNumber].times);
Debug.Log($"[CreateFishManager] 生成鱼:{fishType.fishName}");
return true;
}
/// <summary>将鱼实例注册到全局字典及对应分类字典。</summary>
private void RegisterFishInstance(MonsterContral mc, int fishTypeId)
{
fishDatas_hashCodekey[mc.guid] = mc;
switch (fishTypeId)
{
case 1: // 普通鱼
case 3: // 特殊鱼 / 黄金鱼
fishDatas_hashCodekey_Small[mc.guid] = mc;
break;
case 2: // Boss
fishDatas_hashCodekey_Boss[mc.guid] = mc;
break;
}
}
// ================================================================
// 鱼阵生成(按 arrayNumber 前缀分发到对应阵型协程/方法)
// ================================================================
/// <summary>根据鱼阵配置分发到对应的生成逻辑。</summary>
private void SpawnFishArray(FrameData_CreateFish createFishData, RoomFishDetailConfig cfg, long time)
{
if (!fishArrayConfigDic.TryGetValue(cfg.fishNumber, out var yz))
{
Debug.LogError($"[CreateFishManager] 找不到 fishArrayConfig: {cfg.fishNumber}");
return;
}
if (!fishTypeConfigDic.TryGetValue(yz.fishNumber, out var fishType))
{
Debug.LogError($"[CreateFishManager] 鱼阵 fishType 缺失: {yz.fishNumber}");
return;
}
var prefab = FishResourcesData.GetFishPrefab(int.Parse(fishType.fishNumber));
var pos = new Vector3(9999, 9999, 0);
if (!FishYZ.dicfishYZ.TryGetValue(yz.arrayNumber, out var pathList) || pathList.Count == 0)
{
Debug.LogWarning($"[CreateFishManager] 鱼阵路线为空: {yz.arrayNumber}");
return;
}
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": // 顺序阵:每条路径逐一延迟生成
int chosen710 = (prefix == "710")
? pathList[Random.Range(0, pathList.Count)]
: -1;
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)
{
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;
mc.moveTime = (BeijingTimeManager.Instance.CurrentTimestampMs - time) / 1000f;
fishDatas_hashCodekey[mc.guid] = mc;
fishDatas_hashCodekey_Small[mc.guid] = mc;
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;
mc.moveTime = (BeijingTimeManager.Instance.CurrentTimestampMs - time) / 1000f;
fishDatas_hashCodekey[mc.guid] = mc;
fishDatas_hashCodekey_Small[mc.guid] = mc;
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, int pathId, long time, int count)
{
var cfg = spawnDic[createFishData.fishNumber];
for (int i = 0; i < count; i++)
{
//var fish = Instantiate(prefab, pos, prefab.transform.rotation, fishRoot);
var fish = Pool.Instance.Get(int.Parse(yz.fishNumber), pos, Quaternion.identity, fishRoot);
var mc = fish.GetComponent<MonsterContral>() ?? fish.AddComponent<MonsterContral>();
mc.monsterData.fishTypeConfig = FishDatas[yz.fishNumber];
mc.guid = createFishData.guid + i;
// 每条鱼额外减去已等待秒数,使路径起点与帧数同步
mc.moveTime = ((BeijingTimeManager.Instance.CurrentTimestampMs - time) / 1000f) - i;
fishDatas_hashCodekey[mc.guid] = mc;
fishDatas_hashCodekey_Small[mc.guid] = mc;
mc.Init(pathId.ToString(), cfg.times, true, yz.arrayNumber);
yield return new WaitForSeconds(1f);
}
}
// ================================================================
// 随机工具方法
// ================================================================
/// <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;
}
// ================================================================
// 杀鱼接口
// ================================================================
/// <summary>
/// 随机选取 count 条小鱼/黄金鱼,通知服务器同步死亡,并返回 guid 列表。
/// </summary>
public List<string> SyncGameData_SendRendromDieFishID(
int count, List<RedPacketRecord> coinNum, Vector3 v3Point)
{
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);
}
}
int posID = currGameType.currSitData.posID;
var sgd = new SyncGameData
{
sitPos = posID,
frameType = FrameDataType.randomDieFish,
time = BeijingTimeManager.Instance.CurrentTimestampMs,
frameData = new FrameData_RandomDieFish
{
x = v3Point.x,
y = v3Point.y,
z = v3Point.z,
guids = guids,
coinNum = coinNum,
count = count
}
};
WebSocketSendList.SyncGameData(sgd);
return guids;
}
/// <summary>
/// 按 ID 杀死一条鱼(无特效版)。
/// </summary>
public void Id_DieFish_NotFX(string fishId)
{
if (!fishDatas_hashCodekey.TryGetValue(fishId, out var monster)) return;
monster.DieFish();
fishDatas_hashCodekey.Remove(fishId);
}
/// <summary>
/// 按 ID 杀死一条鱼(带特效 + 金币飞出动画)。
/// </summary>
public void Id_DieFish_FX(string fishId, RedPacketRecord coinNum, Vector3 v3Point)
{
if (!fishDatas_hashCodekey.TryGetValue(fishId, out var monster)) return;
AudioManager.Instance.PlayerFX(60004);
// 死亡粒子特效
Instantiate(
monster.effect_Die != null ? monster.effect_Die : dieFXPrefab,
monster.transform.position, Quaternion.identity, transform.parent);
// 非 Boss 鱼:金币飞向炮台
if (monster.fishenum != FishTypeEnum.Boss鱼)
{
var award = Instantiate(
monster.effect_DieAward != null ? monster.effect_DieAward : dieFXPrefabAward,
currGameType.subPanelParent.parent);
award.GetComponent<RectTransform>().anchoredPosition =
CoordinateTransformation.WorldPos_ToCanvas(
monster.transform.position,
UIManager.Instance.uiRoot.GetComponent<Canvas>());
var coinScript = award.GetComponent<CoinScript>();
var endTra = new GameObject("CoinEndPoint").transform;
endTra.position = v3Point;
endTra.parent = coinScript.transform;
coinScript.endPoint = endTra;
coinScript.isBoss = true;
coinScript.SetCoinCount(coinNum.Amount);
}
MonsterDieList.Instance.SmallHuangYiHuang(
monster.transform.position,
monster.monsterData.fishTypeConfig.fishNumber, 1000);
monster.DieFish();
fishDatas_hashCodekey.Remove(fishId);
}
/// <summary>
/// 批量杀死多条鱼(带特效)。
/// </summary>
public void Id_DieListFish_FX(List<string> ids, List<RedPacketRecord> coinNum, Vector3 v3Point)
{
for (int i = 0; i < ids.Count; i++)
Id_DieFish_FX(ids[i], coinNum[i], v3Point);
}
/// <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");
if (!fishDatas_hashCodekey.TryGetValue(killFish.fishId, out var monster)) return;
// 加分回调(在金币飞完后执行)
Action<int> onScoreAdd = audioID =>
{
var sitList = GetCurrentPanelSitList();
if (sitList != null)
{
foreach (var sit in sitList)
{
if (sit.user != null && sit.user == killFish.userId)
sit.goldNum += killFish.gold;
}
}
AudioManager.Instance.PlayerFX(audioID);
};
// 死亡粒子特效
Instantiate(
monster.effect_Die != null ? monster.effect_Die : dieFXPrefab,
monster.transform.position, Quaternion.identity, transform.parent);
if (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<CoinScript>();
coinScript.endPoint = currGameType.currSitData.transform;
coinScript.isBoss = false;
coinScript.SetCoinCount(killFish.gold);
coinScript.JiaFen = onScoreAdd;
}
else
{
// Boss 鱼:显示浮动分数文本
var bossCtrl = (MonsterControl_BossBase)monster;
bossCtrl.tMP_CurrBossScore = Instantiate(
monster.effect_DieAward != null ? monster.effect_DieAward : dieFXPrefabAwardTmp,
currGameType.subPanelParent.parent)
.transform.GetChild(0).GetComponent<TextMeshProUGUI>();
bossCtrl.JiaFen = onScoreAdd;
}
MonsterDieList.Instance.SmallHuangYiHuang(
monster.transform.position,
monster.monsterData.fishTypeConfig.fishNumber, 1000);
monster.DieFish(killFish);
fishDatas_hashCodekey.Remove(killFish.fishId);
}
// ================================================================
// 工具:获取当前面板座位列表
// ================================================================
/// <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.TGXX:
return UIManager.Instance.Get<GameMainPanel_FishBase>(UIPanel.MainGamePanelTGXX).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;
}
}
}