This commit is contained in:
2026-07-25 23:05:24 +08:00
parent 0c58becf41
commit a5d9a06030
11 changed files with 405 additions and 806 deletions
+26 -3
View File
@@ -3,7 +3,7 @@ public class ConstEventString
{
public const string SyncGameData_Rotate = "SyncGameData_Rotate";
public const string SyncGameData_FireBullet = "SyncGameData_FireBullet";
public const string SyncGameData_CreateFish = "SyncGameData_CreateFish";
// SyncGameData_CreateFish 已废弃:鱼创建改由服务端 OnFishSpawn 事件下发
public const string SyncGameData_ChangeTargetMonster = "SyncGameData_ChangeTargetMonster";
public const string SyncGameData_RendromDieFish = "SyncGameData_RenderDieFish";
public const string SyncGameData_TurretState = "SyncGameData_TurretState";
@@ -12,6 +12,29 @@ public class ConstEventString
public const string SyncGameData_ChangeCannonMultiple = "SyncGameData_ChangeCannonMultiple";
public const string SyncGameData_ChangeWing = "SyncGameData_ChangeWing";
public const string SyncGameData_BotFireAngle = "SyncGameData_BotFireAngle";
public const string SyncGameData_RequestSyncFish = "SyncGameData_RequestSyncFish";
}
// SyncGameData_RequestSyncFish 已废弃:鱼同步改由服务端 OnFishSpawn(type=sync) 下发
// ================================================================
// 服务端 → 客户端事件 (新协议)
// ================================================================
/// <summary>服务端出鱼事件(破坏性替代 createFish 帧同步)</summary>
public const string OnFishSpawn = "OnFishSpawn";
/// <summary>鱼列表刷新</summary>
public const string OnFishList = "OnFishList";
/// <summary>冰冻道具生效(40003),鱼存活时间延长 15 秒,客户端写死 15s 倒计时</summary>
public const string FishFrozen = "FishFrozen";
/// <summary>玩家挂机通知</summary>
public const string PlayerAfkNotice = "PlayerAfkNotice";
/// <summary>玩家取消挂机通知</summary>
public const string PlayerCancelAfkNotice = "PlayerCancelAfkNotice";
/// <summary>全局击杀公告(真人击杀/机器人模拟)</summary>
public const string KillNotice = "KillNotice";
/// <summary>机器人退出房间</summary>
public const string BotLeaveRoomNotice = "BotLeaveRoomNotice";
/// <summary>房主断线,其他玩家可接受机器人控制权</summary>
public const string HostDisconnect = "HostDisconnect";
/// <summary>座位变更</summary>
public const string OnSeatChange = "OnSeatChange";
/// <summary>房主控制权移交</summary>
public const string ChangeControllerUser = "ChangeControllerUser";
}
@@ -189,8 +189,7 @@ public class CreateFishManager : MonoSingleton<CreateFishManager>
private int hjQuanZhongCount; // 本轮同时启用黄金鱼数量
/// <summary>是否开启放鱼(false 时 Update 不执行生成逻辑)</summary>
public bool fangzhu;
// fangzhu 字段已删除:鱼创建改由服务端 OnFishSpawn 统一下发,客户端无需房主出鱼
public bool isResources = false;
// ================================================================
// 懒加载属性
@@ -251,36 +250,28 @@ public class CreateFishManager : MonoSingleton<CreateFishManager>
fishRoot = new GameObject("FishRoot").transform;
if (poolRoot == null)
fishRoot = new GameObject("poolRoot").transform;
// 监听服务器下发的创鱼事件
EventCenter.Instance.AddEventListener<SyncGameData>(
ConstEventString.SyncGameData_CreateFish, CreateFish);
// 监听新玩家"请求同步鱼"事件:房主收到后广播场上活鱼
EventCenter.Instance.AddEventListener<SyncGameData>(
ConstEventString.SyncGameData_RequestSyncFish, OnRequestSyncFish);
// ⭐ 鱼创建改由服务端 OnFishSpawn 事件统一下发,客户端不再监听 createFish 帧同步
EventCenter.Instance.AddEventListener<OnFishSpawnData>(
ConstEventString.OnFishSpawn, OnFishSpawn);
}
// ================================================================
// Update:定时调度 + Job 驱动生成
// Update:冰冻时不做任何事(鱼创建由服务端 OnFishSpawn 驱动)
// ================================================================
private void Update()
{
if (!isFrozen)
isCreateFish = BroadCastCreateFish();
// 鱼创建由服务端 OnFishSpawn 事件驱动,客户端 Update 不再出鱼
// (保留 isFrozen 字段供 FreezeAllFish/UnfreezeAllFish 使用)
}
private void OnDestroy()
{
EventCenter.Instance.RemoveEventListener<SyncGameData>(
ConstEventString.SyncGameData_CreateFish, CreateFish);
EventCenter.Instance.RemoveEventListener<SyncGameData>(
ConstEventString.SyncGameData_RequestSyncFish, OnRequestSyncFish);
EventCenter.Instance.RemoveEventListener<OnFishSpawnData>(
ConstEventString.OnFishSpawn, OnFishSpawn);
// 释放 NativeArray,避免内存泄漏
DisposeNativeArrays();
@@ -594,656 +585,97 @@ public class CreateFishManager : MonoSingleton<CreateFishManager>
}
#endregion
#region 创建鱼 广播部分
#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>
/// <returns>此玩家是否可以创建鱼</returns>
private bool BroadCastCreateFish()
public void OnFishSpawn(OnFishSpawnData data)
{
if (!isResources)
if (data == null || data.fish == null || data.fish.Count == 0) return;
if (isFrozen) return; // 冰冻中不创建鱼
if (spawnDic == null) return; // 场景卸载后忽略
foreach (var item in data.fish)
{
return false;
}
// 未初始化 或 未开启放鱼 → 跳过
if (spawnList == null || !fangzhu) return false;
if (item == null) continue;
if (string.IsNullOrEmpty(item.fishNumber)) continue;
float dt = Time.deltaTime;
// guid 用服务端 fishId
string guid = item.fishId.ToString();
// ---- 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. 普通鱼定时批量激活 ----
TickPTFishSpawn(dt);
// ---- 5. 黄金鱼定时激活 ----
TickHJFishSpawn(dt);
// ---- 5. 特殊鱼定时激活 ----
TickTSFishSpawn(dt);
// ---- 6. 特殊boss定时激活 ----
TickTSBossSpawn(dt);
// ---- 7. 遍历 shouldSpawn,向服务器发送生成请求 ----
ProcessSpawnRequests();
return true;
}
// ================================================================
// 定时 Tick 方法(由 Update 调用)
// ================================================================
/// <summary>每 25 秒随机激活一条 Boss 鱼。</summary>
private void TickBossSpawn(float dt)
{
bossTimer -= dt;
if (bossTimer > 0f) return;
bossCount = Random.Range(bossCreateData.countRange[0], bossCreateData.countRange[1]);
bossTimer = Random.Range(bossCreateData.timeRange[0], bossCreateData.timeRange[1]);
//bossTimer = 3f;
if (!fishTypeConfig.TryGetValue(FishTypeEnum.Boss鱼, out var bossList) || bossList.Count == 0)
return;
foreach (var item in bossList)
{
//if (item.fishNumber == "11026")
// item.isCreate = 2;
//if (item.fishNumber == "11035")
// item.isCreate = 2;
// if (item.fishNumber == "11030")
// item.isCreate = 2;
}
StartCoroutine(buchongfu(bossList, bossCount, fishDatas_hashCodekey_Boss, bossRecentList, bossRecentMax));
}
/// <summary>每 25 秒随机激活一条 特殊Boss 鱼。</summary>
private void TickTSBossSpawn(float dt)
{
tsbossTimer -= dt;
if (tsbossTimer > 0f) return;
tsbossCount = Random.Range(tsbossCreateData.countRange[0], tsbossCreateData.countRange[1]);
tsbossTimer = Random.Range(tsbossCreateData.timeRange[0], tsbossCreateData.timeRange[1]);
if (!fishTypeConfig.TryGetValue(FishTypeEnum.特殊Boss, out var tsbossList) || tsbossList.Count == 0)
return;
//foreach (var item in tsbossList)
//{
// if (item.fishNumber == "11041")
// {
// item.isCreate = 2;
// }
//}
StartCoroutine(buchongfu(tsbossList, tsbossCount, fishDatas_hashCodekey_TS_Boss, tsbossRecentList, tsbossRecentMax));
}
private RoomFishDetailConfig GetRandomFishByWeight(List<RoomFishDetailConfig> configs)
{
if (configs == null || configs.Count == 0) return null;
int totalWeight = configs.Sum(c => c.fishWeight);
int randWeight = Random.Range(0, totalWeight + 1);
foreach (var cfg in configs)
{
randWeight -= cfg.fishWeight;
if (randWeight <= 0) // 注意这里是 <= 0
{
return cfg;
}
}
return configs[configs.Count - 1]; // 理论上不会到这里
}
/// <summary>
/// 从配置列表中按权重随机选择指定数量的鱼,不重复。
/// </summary>
private List<RoomFishDetailConfig> GetRandomFishByWeight(List<RoomFishDetailConfig> configs, int count)
{
var result = new List<RoomFishDetailConfig>();
if (configs == null || configs.Count == 0 || count <= 0) return result;
// 拷贝一份候选集合,避免修改原始列表
var candidates = new List<RoomFishDetailConfig>(configs);
count = Mathf.Min(count, candidates.Count); // 不超过候选数量
for (int k = 0; k < count; k++)
{
int totalWeight = candidates.Sum(c => c.fishWeight);
int randWeight = Random.Range(0, totalWeight);
RoomFishDetailConfig chosen = null;
foreach (var cfg in candidates)
{
randWeight -= cfg.fishWeight;
if (randWeight < 0) // 注意这里是 <= 0
{
chosen = cfg;
break;
}
}
if (chosen != null)
{
result.Add(chosen);
candidates.Remove(chosen); // 移除已选,保证不重复
}
}
return result;
}
IEnumerator buchongfu(List<RoomFishDetailConfig> bossList, int count, Dictionary<string, MonsterContral> targetDict, List<string> recentList = null, int recentMax = 0)
{
var batchPicked = new HashSet<string>(); // 本轮已选,防止同批重复
for (int j = 0; j < count; j++)
{
// 1. 先筛选出未重复的候选集合(排除场上已存在的同 fishNumber 的鱼 + 最近出现过的 + 本轮已选的)
var candidates = bossList
.Where(b => !targetDict.Values
.Any(existing => existing.fishTypeID.ToString() == b.fishNumber))
.Where(b => !batchPicked.Contains(b.fishNumber))
.ToList();
// 排除最近出现过的鱼
if (recentList != null && recentList.Count > 0)
candidates = candidates.Where(c => !recentList.Contains(c.fishNumber)).ToList();
if (candidates.Count == 0)
{
// 没有候选时清空最近列表 + 本轮记录重试
if (recentList != null) recentList.Clear();
batchPicked.Clear();
candidates = bossList
.Where(b => !targetDict.Values
.Any(existing => existing.fishTypeID.ToString() == b.fishNumber))
.ToList();
if (candidates.Count == 0)
yield break;
}
// 2. 在候选集合里做加权随机
var chosen = GetRandomFishByWeight(candidates);
batchPicked.Add(chosen.fishNumber);
// 3. 记录到最近列表
if (recentList != null && recentMax > 0)
{
recentList.Add(chosen.fishNumber);
while (recentList.Count > recentMax)
recentList.RemoveAt(0);
}
// 4. 标记激活
chosen.isCreate = 2;
yield return null; // 每帧只激活一条,避免同帧生成过多
}
}
//IEnumerator buchongfu(
//List<RoomFishDetailConfig> bossList,
//int count,
//Dictionary<string, MonsterContral> targetDict,
//List<string> recentList = null,
//int recentMax = 0)
//{
// Debug.Log($"===【调试开始】本次需要生成数量: {count},配置总数: {bossList.Count} ===");
// // 打印场上已有 Boss
// Debug.Log($"当前场上已有 Boss 数量: {targetDict.Count}");
// foreach (var kv in targetDict)
// Debug.Log($"场上已有 Boss fishNumber = {kv.Value.fishTypeID}");
// var batchPicked = new HashSet<string>(); // 本轮已选
// for (int j = 0; j < count; j++)
// {
// Debug.Log($"--- 第 {j + 1}/{count} 次抽取 ---");
// // 1. 先筛选候选池
// var candidates = bossList
// .Where(b => !targetDict.Values
// .Any(existing => existing.fishTypeID.ToString() == b.fishNumber))
// .Where(b => !batchPicked.Contains(b.fishNumber))
// .ToList();
// // 打印被排除的原因
// foreach (var b in bossList)
// {
// bool inTarget = targetDict.Values.Any(e => e.fishTypeID.ToString() == b.fishNumber);
// bool inBatch = batchPicked.Contains(b.fishNumber);
// bool inRecent = recentList != null && recentList.Contains(b.fishNumber);
// if (inTarget || inBatch || inRecent)
// {
// Debug.Log(
// $"排除 fishNumber={b.fishNumber} | " +
// $"场上存在={inTarget} | 本轮已选={inBatch} | 最近出现={inRecent}"
// );
// }
// }
// // 排除最近出现过的鱼
// if (recentList != null && recentList.Count > 0)
// candidates = candidates.Where(c => !recentList.Contains(c.fishNumber)).ToList();
// Debug.Log($"候选池数量: {candidates.Count}");
// // 2. 候选池为空 → 触发重置逻辑
// if (candidates.Count == 0)
// {
// Debug.LogWarning("候选池为空,触发 recentList + batchPicked 清空重置");
// recentList?.Clear();
// batchPicked.Clear();
// candidates = bossList
// .Where(b => !targetDict.Values
// .Any(existing => existing.fishTypeID.ToString() == b.fishNumber))
// .ToList();
// Debug.Log($"重置后候选池数量: {candidates.Count}");
// if (candidates.Count == 0)
// {
// Debug.LogError("重置后仍然没有候选鱼 → 说明 targetDict 永远占着某条鱼,导致永远少生成 1 条!");
// yield break;
// }
// }
// // 3. 加权随机
// var chosen = GetRandomFishByWeight(candidates);
// Debug.Log($"选中 fishNumber = {chosen.fishNumber}");
// batchPicked.Add(chosen.fishNumber);
// // 4. 记录最近出现
// if (recentList != null && recentMax > 0)
// {
// recentList.Add(chosen.fishNumber);
// while (recentList.Count > recentMax)
// recentList.RemoveAt(0);
// }
// // 5. 标记激活
// chosen.isCreate = 2;
// yield return null;
// }
// Debug.Log("===【调试结束】本轮生成完成 ===");
//}
/// <summary>每 1~3 秒随机激活若干组鱼阵。</summary>
private void TickFishArraySpawn(float dt)
{
yzTimer -= dt;
if (yzTimer > 0f) return;
yzCount = Random.Range(yzCreateData.countRange[0], yzCreateData.countRange[1]);
yzTimer = Random.Range(yzCreateData.timeRange[0], yzCreateData.timeRange[1]);
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)
// guid 去重:已有此 guid 的鱼跳过
if (!string.IsNullOrEmpty(guid) &&
fishDatas_hashCodekey != null &&
fishDatas_hashCodekey.ContainsKey(guid))
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 TickPTFishSpawn(float dt)
{
ptTimer -= dt;
if (ptTimer > 0f) return;
ptCount = Random.Range(ptCreateData.countRange[0], ptCreateData.countRange[1]);
ptTimer = Random.Range(ptCreateData.timeRange[0], ptCreateData.timeRange[1]);
if (!fishTypeConfig.TryGetValue(FishTypeEnum.普通鱼, out var normalList) || normalList.Count == 0)
return;
// 随机选 ptCount 个不重复下标
var indices = GetRandomFishByWeight(normalList, ptCount);
foreach (var fish in indices)
fish.isCreate = 2;
}
/// <summary>每 3 秒随机批量激活若干条黄金鱼。</summary>
private void TickHJFishSpawn(float dt)
{
hjTimer -= dt;
if (hjTimer > 0f) return;
hjCount = Random.Range(hjCreateData.countRange[0], hjCreateData.countRange[1]);
hjTimer = Random.Range(hjCreateData.timeRange[0], hjCreateData.timeRange[1]);
if (!fishTypeConfig.TryGetValue(FishTypeEnum.黄金鱼, out var hjlist) || hjlist.Count == 0)
return;
// 随机选 hjCount 个不重复下标
var indices = GetRandomFishByWeight(hjlist, hjCount);
foreach (var fish in indices)
fish.isCreate = 2;
}
/// <summary>每 3 秒随机批量激活若干条特殊鱼。</summary>
private void TickTSFishSpawn(float dt)
{
tsTimer -= dt;
if (tsTimer > 0f) return;
tsCount = Random.Range(tsCreateData.countRange[0], tsCreateData.countRange[1]);
tsTimer = Random.Range(tsCreateData.timeRange[0], tsCreateData.timeRange[1]);
if (!fishTypeConfig.TryGetValue(FishTypeEnum.特殊鱼, out var tslist) || tslist.Count == 0)
return;
// 随机选 tsCount 个不重复下标
var indices = GetRandomFishByWeight(tslist, tsCount);
foreach (var fish in indices)
fish.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)
// 构造 FrameData_CreateFish 复用 SpawnFishMainThread
var createFishData = new FrameData_CreateFish
{
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 ft = FishDatas[fish.fishNumber];
var pathList = ft.pathList;
if (pathList != null && pathList.Count > 0)
{
// Boss鱼(2)/特殊Boss(5):路线奇偶交替,避免同奇/同偶路径相互覆盖
if (ft != null && (ft.fishType == 2 || ft.fishType == 5))
{
var filtered = pathList.Where(p => (p % 2 != 0) == bossNextPathOdd).ToList();
if (filtered.Count == 0)
filtered = pathList;
pathId = filtered[Random.Range(0, filtered.Count)];
bossNextPathOdd = !bossNextPathOdd;
}
else
{
pathId = pathList[Random.Range(0, pathList.Count)];
}
}
}
else
{
if (!fishArrayConfigDic.TryGetValue(fish.fishNumber, out var yz))
{
Debug.LogError($"[CreateFishManager] 找不到 fishArrayConfig: {fish.fishNumber}");
return;
}
if (!FishYZ.dicfishYZ.TryGetValue(yz.arrayNumber, out var pathList) || pathList.Count == 0)
{
Debug.LogWarning($"[CreateFishManager] 鱼阵路线为空: {yz.arrayNumber}");
return;
}
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>
/// 进度条加载结束时房主调用,创建一批初始鱼(进度 0.2~0.8),让屏幕开局就有鱼不会显得空。
/// 直接复用 CreateFishManager 原有的出鱼机制:
/// - GetRandomFishByWeight 按权重选鱼(和 TickNormalFishSpawn 一致)
/// - WebSocketSendList.SyncGameData 帧同步广播(和 SendCreateFishRequest 一致)
/// - 通过 time 字段控制创建时间,让所有客户端收到后 progress 自然在 [0.2, 0.8]
/// mc.moveTime = (CurrentTimestampMs - time) / 1000f
/// progress = moveTime / times
/// → time = CurrentTimestampMs - progress * times * 1000
///
/// 只选普通鱼(fishTypeConfig[普通鱼]),Boss/特殊Boss/黄金鱼/特殊鱼由原有 Tick 逻辑
/// 按时间间隔触发,不在这里批量生成,避免开局一堆 Boss。
/// </summary>
/// <param name="count">要创建的鱼数量</param>
public void CreateInitialFishes(int count)
{
if (!fangzhu) return;
if (fishTypeConfig == null) return;
if (!fishTypeConfig.TryGetValue(FishTypeEnum.普通鱼, out var normalList) || normalList.Count == 0) return;
for (int i = 0; i < count; i++)
{
// ⭐ 用原有的 GetRandomFishByWeight 按权重选鱼
var fish = GetRandomFishByWeight(normalList);
if (fish == null) continue;
// 从该鱼的路径列表里随机选一条
if (!FishDatas.TryGetValue(fish.fishNumber, out var ft)) continue;
if (ft.pathList == null || ft.pathList.Count == 0) continue;
int pathId = ft.pathList[Random.Range(0, ft.pathList.Count)];
// ⭐ 通过 time 字段控制创建时间,让 progress ∈ [0.2, 0.8]
float progress = Random.Range(0.2f, 0.8f);
long timeOffsetMs = (long)(progress * fish.times * 1000f);
long sendTime = BeijingTimeManager.Instance.CurrentTimestampMs - timeOffsetMs;
var sgd = new SyncGameData
{
sitPos = GameManager.Instance.gameData.roomJoinNotice.position,
frameType = FrameDataType.createFish,
time = sendTime,
frameData = new FrameData_CreateFish
{
fishNumber = fish.fishNumber,
guid = Guid.NewGuid().ToString(),
moveLineId = pathId.ToString()
}
fishNumber = item.fishNumber,
guid = guid,
moveLineId = item.routeId.ToString()
};
// ⭐ 走原有的帧同步广播链路
WebSocketSendList.SyncGameData(sgd);
// 用 data.timestamp 作为生成时间,SpawnFishMainThread 内部会算 moveTime
SpawnFishMainThread(createFishData, data.timestamp);
// expireAt 到期自动移除(remainingSeconds 秒后销毁)
if (item.remainingSeconds > 0)
StartCoroutine(ExpireFishCoroutine(guid, item.remainingSeconds));
}
Debug.Log($"[CreateFishManager] 初始鱼已广播: {count} 条 (房主,走原有出鱼机制)");
Debug.Log($"[CreateFishManager] OnFishSpawn type={data.type}, count={data.fish.Count}");
}
/// <summary>
/// 房主把当前场上所有活鱼广播一遍,用于新玩家加入房间时同步场上鱼状态。
/// 直接遍历 fishDatas_hashCodekey(全部活鱼),反推 time 字段让新玩家收到后
/// 通过 (CurrentTimestampMs - time)/1000f 算出与房主一致的 moveTime,
/// 鱼会出现在路径的正确中段位置,与老玩家看到的一致。
/// guid 复用原 guid,保证后续杀鱼/锁定帧同步不会失同步。
/// 鱼过期自动移除协程。
/// remainingSeconds 秒后调用 Id_DieFish_NotFX 销毁鱼(无特效)。
/// </summary>
public void BroadcastExistingFishes()
private IEnumerator ExpireFishCoroutine(string guid, int remainingSeconds)
{
if (!fangzhu) return;
if (fishDatas_hashCodekey == null || fishDatas_hashCodekey.Count == 0) return;
// ⭐ 复制一份快照,避免遍历过程中鱼死亡修改字典导致 InvalidOperationException
var snapshot = new List<KeyValuePair<string, MonsterContral>>(fishDatas_hashCodekey);
long nowMs = BeijingTimeManager.Instance.CurrentTimestampMs;
int count = 0;
foreach (var kv in snapshot)
{
var mc = kv.Value;
if (mc == null) continue;
if (mc.monsterData == null || mc.monsterData.fishTypeConfig == null) continue;
if (mc.monsterData.moveLine == null) continue;
if (string.IsNullOrEmpty(mc.guid)) continue;
if (string.IsNullOrEmpty(mc.lastMoveLineId)) continue;
// ⭐ 跳过鱼阵鱼:鱼阵鱼 guid 是 原guid+i,单条广播会让新玩家收到后走
// SpawnFishArray 又生成 N 条,导致鱼阵指数爆炸(16×16=256)。
// 鱼阵鱼的同步需要鱼阵级别的广播(发鱼阵配置+原guid+数量),
// 这里不处理,鱼阵鱼不同步只影响视觉阵型,不影响游戏核心逻辑。
if (mc.isyz) continue;
// 总时长,用于过滤快死的鱼(避免新玩家刚进入就看到鱼死)
float totalTime = mc.monsterData.moveLine.moveTime;
if (totalTime <= 0f) continue;
// 跳过还没初始化的鱼(moveTime < 0)和快死的鱼(进度 >= 0.9)
if (mc.moveTime < 0f) continue;
if (mc.moveTime >= totalTime * 0.9f) continue;
// ⭐ 反推 sendTime:让新玩家收到后 (CurrentTimestampMs - sendTime)/1000f ≈ mc.moveTime
long sendTime = nowMs - (long)(mc.moveTime * 1000f);
var sgd = new SyncGameData
{
sitPos = GameManager.Instance.gameData.roomJoinNotice.position,
frameType = FrameDataType.createFish,
time = sendTime,
frameData = new FrameData_CreateFish
{
fishNumber = mc.monsterData.fishTypeConfig.fishNumber,
guid = mc.guid, // ⭐ 复用原 guid
moveLineId = mc.lastMoveLineId
}
};
WebSocketSendList.SyncGameData(sgd);
count++;
}
Debug.Log($"[CreateFishManager] 广播场上活鱼: {count} 条 (房主,新玩家加入同步)");
}
/// <summary>
/// 房主收到新玩家"请求同步鱼"事件后,广播场上活鱼。
/// 新玩家资源加载完成才会发请求,保证收到广播时 spawnDic 已初始化。
/// 非房主收到直接忽略。
/// </summary>
private void OnRequestSyncFish(SyncGameData syncGameData)
{
// 仅房主响应,且不响应自己发的请求(syncGameData.sitPos 是发送者座位)
if (!fangzhu) return;
BroadcastExistingFishes();
yield return new WaitForSeconds(remainingSeconds);
Id_DieFish_NotFX(guid);
}
#endregion
#region 创建鱼
/// <summary>
/// AI优化过的
/// </summary>
/// <param name="syncGameData"></param>
public void CreateFish(SyncGameData syncGameData)
{
if (isFrozen) return; // 冰冻中不创建鱼
// ⭐ 场景卸载/CloseSpawnSystem 后 spawnDic 会变 null,此时收到的广播直接忽略
if (spawnDic == null) return;
var rotateData = FrameDataParser.Parse<FrameData_CreateFish>(syncGameData.frameData);
if (rotateData == null) return;
#region 旧客户端出鱼链路(已删除)
// 以下方法已删除,鱼创建改由服务端 OnFishSpawn 事件下发:
// - BroadCastCreateFish / TickBossSpawn / TickTSBossSpawn / TickFishArraySpawn
// - TickPTFishSpawn / TickHJFishSpawn / TickTSFishSpawn
// - ProcessSpawnRequests / SendCreateFishRequest
// - CreateInitialFishes / BroadcastExistingFishes / OnRequestSyncFish
// - CreateFish(SyncGameData)
// 保留方法:SpawnFishMainThread / OnlyFish / SpawnFishArray 等(被 OnFishSpawn 调用)
#endregion
// ⭐ guid 去重:房主广播活鱼给新玩家时,服务器会广播给所有客户端(含房主自己)。
// 老玩家已存在该 guid 的鱼 → 跳过,避免重复 Spawn 同一条鱼。
// 仅本地无该 guid 的客户端(新加入玩家)才会真正 Spawn。
if (!string.IsNullOrEmpty(rotateData.guid) &&
fishDatas_hashCodekey != null &&
fishDatas_hashCodekey.ContainsKey(rotateData.guid))
return;
// ================================================================
// 旧定时 Tick 方法(已删除,鱼创建改由服务端 OnFishSpawn 事件下发)
// ================================================================
// TickBossSpawn / TickTSBossSpawn / TickFishArraySpawn / TickPTFishSpawn
// TickHJFishSpawn / TickTSFishSpawn / buchongfu / ProcessSpawnRequests
// SendCreateFishRequest / CreateInitialFishes / BroadcastExistingFishes
// OnRequestSyncFish / CreateFish(SyncGameData)
// —— 全部删除,改由 OnFishSpawn(OnFishSpawnData) 接收服务端出鱼事件 ——
// ================================================================
// 单体鱼 / 鱼阵生成(被 OnFishSpawn 调用)
// ================================================================
SpawnFishMainThread(rotateData, syncGameData.time);
}
/// <summary>
/// 主线程实例化鱼对象(单体鱼 / 鱼阵分发)。
/// </summary>
@@ -1524,8 +956,6 @@ public class CreateFishManager : MonoSingleton<CreateFishManager>
#endregion
#endregion
#region 结束游戏退出房间
/// <summary>退出房间:关闭生成系统。</summary>
public void ExitRoom()
+1 -1
View File
@@ -18,7 +18,7 @@ namespace Assets.MyGame.Scripts.Net
//public const string baseURL = "http://game5241.wzaxr.top";
#if UNITY_EDITOR
//public const string baseURL = "http://8.137.163.153";
public const string baseURL = "http://47.109.34.50";
public const string baseURL = "http://47.109.132.12";
#else
//public const string baseURL = "http://8.137.8.46";
@@ -44,7 +44,7 @@ namespace Assets.MyGame.Scripts.Net.WebResponses
none = 0,
rotateTurrect=1,
fireBullet=2,
createFish=3,
// createFish=3 已废弃:鱼创建改由服务端 OnFishSpawn 事件下发
changeAutoTarget=4,
randomDieFish=5,
changeTurretState=6,
@@ -52,8 +52,7 @@ namespace Assets.MyGame.Scripts.Net.WebResponses
freezeFish=8,
changeCannonMultiple=9,
changeWing=10,
/// <summary>新玩家资源加载完成,请求房主广播场上活鱼</summary>
requestSyncFish=11
// requestSyncFish=11 已废弃:鱼同步改由服务端 OnFishSpawn(type=sync) 事件下发
}
[Serializable]
public class FireCannonSendData
@@ -145,4 +144,93 @@ namespace Assets.MyGame.Scripts.Net.WebResponses
{
public int cannonMultiple;
}
// ================================================================
// OnFishSpawn 事件数据结构 (服务端出鱼,破坏性替代 createFish 帧同步)
// ================================================================
/// <summary>
/// 服务端出鱼事件 OnFishSpawn 的数据载体。
/// type:
/// "init" - 首次出鱼(玩家进入房间后服务端推送初始鱼)
/// "spawn" - 定时出鱼(游戏中持续刷怪)
/// "sync" - 重连/取消挂机同步(把当前场上活鱼推送给回归玩家)
/// </summary>
[Serializable]
public class OnFishSpawnData
{
/// <summary>"init" | "spawn" | "sync"</summary>
public string type;
/// <summary>服务器当前时间戳(毫秒)</summary>
public long timestamp;
/// <summary>本批次出鱼列表</summary>
public List<OnFishSpawnFishItem> fish;
}
/// <summary>
/// OnFishSpawn 单条鱼描述。
/// 客户端据此调用 SpawnFishMainThread 生成鱼实例。
/// </summary>
[Serializable]
public class OnFishSpawnFishItem
{
/// <summary>服务端分配的全局唯一鱼 ID(作为 guid 使用)</summary>
public long fishId;
/// <summary>鱼编号(配置 fishNumber)</summary>
public string fishNumber;
/// <summary>路径编号</summary>
public int routeId;
/// <summary>鱼阵编号(非鱼阵时为空字符串)</summary>
public string fishArrayNumber;
/// <summary>鱼阵的 Boss 鱼编号</summary>
public string bossFishNumber;
/// <summary>鱼阵普通鱼数量</summary>
public int fishCount;
/// <summary>鱼阵 Boss 数量</summary>
public int bossCount;
/// <summary>鱼过期时间戳(毫秒),到期自动移除</summary>
public long expireAt;
/// <summary>鱼剩余存活秒数(客户端做倒计时用)</summary>
public int remainingSeconds;
}
/// <summary>
/// 机器人退出房间通知数据 BotLeaveRoomNotice: { userId }
/// </summary>
[Serializable]
public class BotLeaveRoomNoticeData
{
public string userId;
}
/// <summary>
/// 玩家挂机/取消挂机通知数据: { userId }
/// </summary>
[Serializable]
public class PlayerAfkNoticeData
{
public string userId;
}
/// <summary>
/// 全局击杀公告 KillNotice 数据。
/// 真人击杀时只填基础字段;机器人模拟时额外填 fishName/multiplier/sceneName。
/// </summary>
[Serializable]
public class KillNoticeData
{
/// <summary>鱼编号</summary>
public string FishId;
/// <summary>玩家名</summary>
public string userName;
/// <summary>金币</summary>
public long gold;
// —— 机器人模拟时额外字段 ——
/// <summary>鱼名(机器人模拟时填充)</summary>
public string fishName;
/// <summary>倍数(机器人模拟时填充)</summary>
public int? multiplier;
/// <summary>场景名(机器人模拟时填充)</summary>
public string sceneName;
}
}
@@ -33,7 +33,7 @@ namespace Assets.MyGame.Scripts.Net
//private const string baseUrl = "ws://game5241.wzaxr.top";
#if UNITY_EDITOR
//private const string baseUrl = "ws://8.137.163.153";
private const string baseUrl = "ws://47.109.34.50";
private const string baseUrl = "ws://47.109.132.12";
#else
//private const string baseUrl = "ws://8.137.8.46";
@@ -453,31 +453,20 @@ namespace Assets.MyGame.Scripts.Net
break;
case "ChangeControllerUser":
{
// ⭐ 房主控制权移交:仅维护 roomOwnerSit,不再设置 fangzhu
// (鱼创建已由服务端 OnFishSpawn 统一下发,客户端无需房主出鱼)
var userId = JsonConvert.DeserializeObject<WebSocketResponseBase<string>>(msg).arguments[0];
if (userId == GameManager.Instance.user.id)
{
// 本地玩家成为房主:开启出鱼 + 同步本地房主引用
CreateFishManager.Instance.fangzhu = true;
if (SitPosData.localPlayerSit != null)
SitPosData.roomOwnerSit = SitPosData.localPlayerSit;
}
else if (userId != null && userId.StartsWith("bot_"))
{
// 房主是人机:人机无法真正发包,由本地首个非人机玩家代发
// 房主是人机:由本地首个非人机玩家代行房主显示职责
SitPosData.TransferRoomOwner();
CreateFishManager.Instance.fangzhu =
SitPosData.roomOwnerSit != null &&
SitPosData.roomOwnerSit.user == GameManager.Instance.user.id;
}
else
{
// ⭐ 只在 userId 非空且确实是真人玩家时才停止本地出鱼
// userId 为空(异常情况)时保持原状,避免误关出鱼导致 bot 退出后不出鱼
if (!string.IsNullOrEmpty(userId))
CreateFishManager.Instance.fangzhu = false;
}
}
break;
case "Exception":
{
@@ -590,18 +579,8 @@ namespace Assets.MyGame.Scripts.Net
break;
case "KillNotice":
{
var data = JsonConvert.DeserializeObject<WebSocketResponseBase<Dictionary<string, object>>>(msg).arguments[0];
if (data == null) break;
// { FishId, userName, gold }
string userName = data.TryGetValue("userName", out var un) ? un?.ToString() : "";
string fishId = data.TryGetValue("FishId", out var fi) ? fi?.ToString() : "";
long gold = data.TryGetValue("gold", out var g) ? Convert.ToInt64(g) : 0;
string roomName = GameManager.Instance.gameData.currRoomData?.roomName ?? "";
string roomLevel = GameManager.Instance.gameData.currRoomData?.roomLevel.ToString() ?? "";
string msgText = $"恭喜用户{userName} 在 {roomName} {roomLevel}场 捕获了{fishId}*{gold}倍";
//UIManager.Instance.ShowBoardGongGao(msgText);
}
// 全局击杀公告(真人击杀/机器人模拟)
WebSocketReponseList.KillNotice(msg);
break;
case "OnFishExpired":
@@ -612,49 +591,48 @@ namespace Assets.MyGame.Scripts.Net
break;
case "OnFishList":
{
var fishList = JsonConvert.DeserializeObject<WebSocketResponseBase<object[]>>(msg).arguments[0];
Debug.Log($"[FishGameHub] 鱼列表刷新, 数量={fishList?.Length}");
}
// 鱼列表刷新
WebSocketReponseList.OnFishList(msg);
break;
// ======== FishGameHub / FishRoomHub 补充事件 ========
case "BotLeaveRoomNotice":
{
// 机器人退出房间,数据 { userId }
var data = JsonConvert.DeserializeObject<WebSocketResponseBase<Dictionary<string, object>>>(msg).arguments[0];
string botUserId = data != null && data.TryGetValue("userId", out var buid) ? buid?.ToString() : "";
Debug.Log($"[FishGameHub] 机器人退出: {botUserId}");
WebSocketReponseList.BotLeaveRoom(botUserId);
}
// 机器人退出房间,数据 { userId }
WebSocketReponseList.BotLeaveRoomNotice(msg);
break;
case "HostDisconnect":
{
// 房主断线,其他玩家可接受机器人控制权
Debug.Log("[FishGameHub] 房主断线,可接受机器人控制权");
WebSocketReponseList.HostDisconnect();
}
// 房主断线,其他玩家可接受机器人控制权
WebSocketReponseList.HostDisconnectNotice(msg);
break;
case "OnSeatChange":
{
// 座位变更 { type, userId, position, showWaiting? }
var data = JsonConvert.DeserializeObject<WebSocketResponseBase<Dictionary<string, object>>>(msg).arguments[0];
if (data != null)
{
// 注意:JSON 字段可能是 long/double/string/bool 等任意类型,
// 直接 Convert.ToInt32(object) 对字符串会走 int.Parse 抛 FormatException。
// 这里统一通过 ToString() + TryParse 容错。
int seatType = data.TryGetValue("type", out var t) && int.TryParse(t?.ToString(), out var st) ? st : 0;
string userId = data.TryGetValue("userId", out var u) ? u?.ToString() : "";
int position = data.TryGetValue("position", out var p) && int.TryParse(p?.ToString(), out var pos) ? pos : -1;
bool showWaiting = data.TryGetValue("showWaiting", out var sw) && (sw is bool b ? b : (sw != null && bool.TryParse(sw.ToString(), out var bv) && bv));
Debug.Log($"[FishGameHub] 座位变更: type={seatType}, userId={userId}, position={position}, showWaiting={showWaiting}");
WebSocketReponseList.SeatChange(seatType, userId, position, showWaiting);
}
}
// 座位变更 { type, userId, position, showWaiting? }
WebSocketReponseList.OnSeatChange(msg);
break;
// ======== 新协议事件 ========
case "OnFishSpawn":
// ⭐ 服务端出鱼事件(破坏性替代 createFish 帧同步)
// type: "init"=首次出鱼 / "spawn"=定时出鱼 / "sync"=重连或取消挂机同步
WebSocketReponseList.OnFishSpawn(msg);
break;
case "FishFrozen":
// ⭐ 冰冻道具(40003)生效:鱼存活时间延长 15 秒,客户端写死 15s 倒计时
WebSocketReponseList.FishFrozen(msg);
break;
case "PlayerAfkNotice":
// 玩家挂机通知 { userId }
WebSocketReponseList.PlayerAfkNotice(msg);
break;
case "PlayerCancelAfkNotice":
// 玩家取消挂机通知 { userId }
WebSocketReponseList.PlayerCancelAfkNotice(msg);
break;
case "RoomChange":
@@ -233,9 +233,7 @@ namespace Assets.MyGame.Scripts.Net
// 与 RoomFireNotice(FireCannonSendData) 分开处理,仅用于存储角度到 botLastAngle
EventCenter.Instance.EventTrigger<SyncGameData>(ConstEventString.SyncGameData_BotFireAngle, syncGameData);
break;
case FrameDataType.createFish:
EventCenter.Instance.EventTrigger<SyncGameData>(ConstEventString.SyncGameData_CreateFish, syncGameData);
break;
// createFish=3 已废弃:鱼创建改由服务端 OnFishSpawn 事件下发
case FrameDataType.changeAutoTarget:
EventCenter.Instance.EventTrigger<SyncGameData>(ConstEventString.SyncGameData_ChangeTargetMonster, syncGameData);
break;
@@ -258,15 +256,123 @@ namespace Assets.MyGame.Scripts.Net
case FrameDataType.changeWing:
EventCenter.Instance.EventTrigger<SyncGameData>(ConstEventString.SyncGameData_ChangeWing, syncGameData);
break;
case FrameDataType.requestSyncFish:
EventCenter.Instance.EventTrigger<SyncGameData>(ConstEventString.SyncGameData_RequestSyncFish, syncGameData);
break;
// requestSyncFish=11 已废弃:鱼同步改由服务端 OnFishSpawn(type=sync) 下发
default:
break;
}
}
// ================================================================
// 新协议事件分发 (OnFishSpawn / PlayerAfk / FishFrozen / KillNotice 等)
// ================================================================
/// <summary>
/// 服务端出鱼事件 OnFishSpawn 处理。
/// 破坏性替代旧 createFish 帧同步:服务端统一出鱼,客户端只接收并生成。
/// </summary>
public static void OnFishSpawn(string msg)
{
var data = JsonConvert.DeserializeObject<WebSocketResponseBase<OnFishSpawnData>>(msg).arguments[0];
if (data == null) return;
EventCenter.Instance.EventTrigger<OnFishSpawnData>(ConstEventString.OnFishSpawn, data);
}
/// <summary>鱼列表刷新</summary>
public static void OnFishList(string msg)
{
var fishList = JsonConvert.DeserializeObject<WebSocketResponseBase<object[]>>(msg).arguments[0];
EventCenter.Instance.EventTrigger<object[]>(ConstEventString.OnFishList, fishList);
}
/// <summary>
/// 冰冻道具(40003)生效事件 FishFrozen。
/// 鱼存活时间延长 15 秒,客户端写死 15s 倒计时。
/// </summary>
public static void FishFrozen(string msg)
{
EventCenter.Instance.EventTrigger(ConstEventString.FishFrozen);
}
/// <summary>玩家挂机通知</summary>
public static void PlayerAfkNotice(string msg)
{
var data = JsonConvert.DeserializeObject<WebSocketResponseBase<PlayerAfkNoticeData>>(msg).arguments[0];
if (data == null) return;
EventCenter.Instance.EventTrigger<PlayerAfkNoticeData>(ConstEventString.PlayerAfkNotice, data);
}
/// <summary>玩家取消挂机通知</summary>
public static void PlayerCancelAfkNotice(string msg)
{
var data = JsonConvert.DeserializeObject<WebSocketResponseBase<PlayerAfkNoticeData>>(msg).arguments[0];
if (data == null) return;
EventCenter.Instance.EventTrigger<PlayerAfkNoticeData>(ConstEventString.PlayerCancelAfkNotice, data);
}
/// <summary>
/// 全局击杀公告 KillNotice。
/// 真人击杀/机器人模拟,机器人模拟时额外带 fishName/multiplier/sceneName。
/// </summary>
public static void KillNotice(string msg)
{
var data = JsonConvert.DeserializeObject<WebSocketResponseBase<KillNoticeData>>(msg).arguments[0];
if (data == null) return;
EventCenter.Instance.EventTrigger<KillNoticeData>(ConstEventString.KillNotice, data);
}
/// <summary>机器人退出房间通知</summary>
public static void BotLeaveRoomNotice(string msg)
{
var data = JsonConvert.DeserializeObject<WebSocketResponseBase<BotLeaveRoomNoticeData>>(msg).arguments[0];
if (data == null) return;
WebSocketReponseList.BotLeaveRoom(data.userId);
}
/// <summary>房主断线</summary>
public static void HostDisconnectNotice(string msg)
{
EventCenter.Instance.EventTrigger(ConstEventString.HostDisconnect);
WebSocketReponseList.HostDisconnect();
}
/// <summary>
/// 座位变更 OnSeatChange。
/// 数据: { type, userId, position, showWaiting? }
/// </summary>
public static void OnSeatChange(string msg)
{
var data = JsonConvert.DeserializeObject<WebSocketResponseBase<Dictionary<string, object>>>(msg).arguments[0];
if (data == null) return;
// JSON 字段可能是 long/double/string/bool 等任意类型,统一 ToString()+TryParse 容错
int seatType = data.TryGetValue("type", out var t) && int.TryParse(t?.ToString(), out var st) ? st : 0;
string userId = data.TryGetValue("userId", out var u) ? u?.ToString() : "";
int position = data.TryGetValue("position", out var p) && int.TryParse(p?.ToString(), out var pos) ? pos : -1;
bool showWaiting = data.TryGetValue("showWaiting", out var sw) && (sw is bool b ? b : (sw != null && bool.TryParse(sw.ToString(), out var bv) && bv));
WebSocketReponseList.SeatChange(seatType, userId, position, showWaiting);
}
/// <summary>
/// 房主控制权移交 ChangeControllerUser。
/// 仅维护 SitPosData.roomOwnerSit,不再设置 fangzhu(鱼创建已由服务端统一出鱼)。
/// </summary>
public static void ChangeControllerUser(string msg)
{
var userId = JsonConvert.DeserializeObject<WebSocketResponseBase<string>>(msg).arguments[0];
if (userId == GameManager.Instance.user.id)
{
// 本地玩家成为房主:同步本地房主引用
if (SitPosData.localPlayerSit != null)
SitPosData.roomOwnerSit = SitPosData.localPlayerSit;
}
else if (userId != null && userId.StartsWith("bot_"))
{
// 房主是人机:人机无法真正发包,由本地首个非人机玩家代行房主显示职责
SitPosData.TransferRoomOwner();
}
// 其他真人玩家成为房主时无需本地操作(roomOwnerSit 由 TransferRoomOwner/ShowPlayer 维护)
}
public static void LeaveRoomResult(string msg)
{
//WebSocketManager.Instance.isGame = false;
@@ -242,6 +242,26 @@ public partial class WebSocketSendList
var sendObj = new WebSocketSendList("ChangeGunMultiple", roomId, userId, multiple);
WebSocketManager.Instance.Send(SocketType.game, JsonConvert.SerializeObject(sendObj));
}
/// <summary>
/// 玩家挂机(对应服务端 SetPlayerAfkNEW)
/// 服务端会向房间组广播 PlayerAfkNotice 事件
/// </summary>
public static void SetPlayerAfk(string roomId, string userId)
{
var sendObj = new WebSocketSendList("SetPlayerAfkNEW", roomId, userId);
WebSocketManager.Instance.Send(SocketType.game, JsonConvert.SerializeObject(sendObj));
}
/// <summary>
/// 取消挂机(对应服务端 CancelPlayerAfkNEW)
/// 服务端会向房间组广播 PlayerCancelAfkNotice 事件,并通过 OnFishSpawn(type=sync) 同步当前鱼
/// </summary>
public static void CancelPlayerAfk(string roomId, string userId)
{
var sendObj = new WebSocketSendList("CancelPlayerAfkNEW", roomId, userId);
WebSocketManager.Instance.Send(SocketType.game, JsonConvert.SerializeObject(sendObj));
}
/// <summary>
/// 同步等级常量(秒)
@@ -284,17 +304,14 @@ public partial class WebSocketSendList
_syncLv1Append.Add(sgd);
return;
case FrameDataType.createFish:
// createFish=3 已废弃:鱼创建改由服务端 OnFishSpawn 下发
case FrameDataType.changeTurretState:
case FrameDataType.changeTurret:
case FrameDataType.freezeFish:
_syncLv2Append.Add(sgd);
return;
case FrameDataType.requestSyncFish:
// ⭐ 新玩家加载完成的"请求同步鱼":立即发送,房主尽快响应广播
SendImmediate(BuildJson(sgd));
return;
// requestSyncFish=11 已废弃:鱼同步改由服务端 OnFishSpawn(type=sync) 下发
default:
_syncLv3Append.Add(sgd);
+2 -12
View File
@@ -153,6 +153,7 @@ public class SitPosData : MonoBehaviour
}
/// <summary>房主退出后,将房主身份转移给第一个活跃的非人机玩家</summary>
/// <remarks>鱼创建已由服务端 OnFishSpawn 统一下发,此处仅维护 roomOwnerSit 标识(用于 bot 控制/AFK 等场景)。</remarks>
public static void TransferRoomOwner()
{
var list = FindObjectsOfType<SitPosData>();
@@ -161,29 +162,18 @@ public class SitPosData : MonoBehaviour
if (!sit.isBot && sit.gameObject.activeInHierarchy && sit != roomOwnerSit)
{
roomOwnerSit = sit;
// 同步出鱼权限:继承者是本地玩家时接管出鱼
if (CreateFishManager.Instance != null)
{
CreateFishManager.Instance.fangzhu =
sit.user == GameManager.Instance?.user?.id;
}
break;
}
}
}
/// <summary>分配房主(首个非人机玩家)</summary>
/// <remarks>鱼创建已由服务端 OnFishSpawn 统一下发,此处仅维护 roomOwnerSit 标识(用于 bot 控制/AFK 等场景)。</remarks>
public static void AssignRoomOwner(SitPosData sit)
{
if (roomOwnerSit == null && !sit.isBot)
{
roomOwnerSit = sit;
// 同步出鱼权限:本地玩家作为首个非人机玩家时接管出鱼
if (CreateFishManager.Instance != null)
{
CreateFishManager.Instance.fangzhu =
sit.user == GameManager.Instance?.user?.id;
}
}
}
@@ -148,7 +148,7 @@ public class GameMainPanel_FishBase : GameMainPanel_Base
public Image img_BingFeng;
public GameObject bingFengEffect; // 冰封特效预制体(首次Instantiate,之后激活/隐藏)
private GameObject _bingFengInstance; // 克隆出来的实例
float bingFengDuration = 5f; // 冰封状态持续时间
float bingFengDuration = 15f; // 冰封状态持续时间(服务端 FishFrozen 延长 15 秒,客户端写死 15s 倒计时)
public float currBingFengTime = 0f;
private Dictionary<Component, object> _frozenAnimSpeeds = new Dictionary<Component, object>();
@@ -437,7 +437,8 @@ public class GameMainPanel_FishBase : GameMainPanel_Base
EventCenter.Instance.AddEventListener<SyncGameData>(ConstEventString.SyncGameData_ChangeTurret, SyncGameData_ChangeTurret);
EventCenter.Instance.AddEventListener<SyncGameData>(ConstEventString.SyncGameData_ChangeCannonMultiple, SyncGameData_ChangeCannonMultiple);
EventCenter.Instance.AddEventListener<SyncGameData>(ConstEventString.SyncGameData_ChangeWing, SyncGameData_ChangeWing);
EventCenter.Instance.AddEventListener<SyncGameData>(ConstEventString.SyncGameData_FreezeFish, SyncGameData_FreezeFish);
// SyncGameData_FreezeFish 已废弃:冰冻改由服务端 FishFrozen 事件统一触发
EventCenter.Instance.AddEventListener(ConstEventString.FishFrozen, OnFishFrozen);
EventCenter.Instance.AddEventListener<SyncGameData>(ConstEventString.SyncGameData_BotFireAngle, SyncGameData_BotFireAngle);
btn_TuJian.onClick.AddListener(() =>
{
@@ -467,7 +468,6 @@ public class GameMainPanel_FishBase : GameMainPanel_Base
{
// 确认:直接退出房间
WebSocketSendList.LeaveRoom(GameManager.Instance.gameData.currRoomData.roomId, currSitData.user);
CreateFishManager.Instance.fangzhu = false;
CancelLeaveCountdown();
});
}
@@ -517,7 +517,8 @@ public class GameMainPanel_FishBase : GameMainPanel_Base
EventCenter.Instance.RemoveEventListener<SyncGameData>(ConstEventString.SyncGameData_ChangeTurret, SyncGameData_ChangeTurret);
EventCenter.Instance.RemoveEventListener<SyncGameData>(ConstEventString.SyncGameData_ChangeCannonMultiple, SyncGameData_ChangeCannonMultiple);
EventCenter.Instance.RemoveEventListener<SyncGameData>(ConstEventString.SyncGameData_ChangeWing, SyncGameData_ChangeWing);
EventCenter.Instance.RemoveEventListener<SyncGameData>(ConstEventString.SyncGameData_FreezeFish, SyncGameData_FreezeFish);
// SyncGameData_FreezeFish 已废弃:冰冻改由服务端 FishFrozen 事件统一触发
EventCenter.Instance.RemoveEventListener(ConstEventString.FishFrozen, OnFishFrozen);
EventCenter.Instance.RemoveEventListener<SyncGameData>(ConstEventString.SyncGameData_BotFireAngle, SyncGameData_BotFireAngle);
// ⭐ 离场清理:子弹池 + 特效池彻底清空,防止内存泄漏
@@ -718,25 +719,9 @@ public class GameMainPanel_FishBase : GameMainPanel_Base
await Task.Delay(2000);
// ⭐ 进度条加载完毕:房主创建一批初始鱼(进度 0.2~0.8),屏幕开局就有鱼
// 只在房主端调用,通过 SyncGameData 广播给所有客户端
// 非房主(后加入的真人玩家):发"请求同步鱼"让房主广播场上活鱼
// 这里只发请求,实际接收广播在 CreateFishManager.CreateFish(已做 guid 去重)
if (CreateFishManager.Instance.fangzhu)
{
CreateFishManager.Instance.CreateInitialFishes(Random.Range(10, 21));
}
else
{
var req = new SyncGameData
{
sitPos = GameManager.Instance.gameData.roomJoinNotice.position,
frameType = FrameDataType.requestSyncFish,
time = BeijingTimeManager.Instance.CurrentTimestampMs,
frameData = null
};
WebSocketSendList.SyncGameData(req);
}
// ⭐ 鱼创建由服务端 OnFishSpawn 事件统一下发,客户端无需房主出鱼/请求同步。
// 玩家进入房间后服务端会主动推送 OnFishSpawn(type="init") 初始鱼,
// 重连/取消挂机时服务端会推送 OnFishSpawn(type="sync") 同步当前鱼。
JinDuTiao.SetProgress(1);
await Task.Delay(300);
@@ -1158,12 +1143,10 @@ public class GameMainPanel_FishBase : GameMainPanel_Base
toggle_BingFeng.SetIsOnWithoutNotify(false);
currBingFengTime = 0f;
if (img_BingFeng != null) img_BingFeng.fillAmount = 1f;
// ⭐ 冰冻结束:本地解冻 + 广播给其他客户端解冻
// 之前只设 toggle.isOn=false,但 onValueChanged 监听器 if(!on)return
// 导致解冻逻辑永远不执行,isFrozen 永远 true,之后永久不出鱼
// ⭐ 冰冻结束:本地解冻(服务端不再下发解冻事件,15s 后客户端自动解冻)
// 之前 SyncFreeze(false) 客户端广播已废弃
CreateFishManager.Instance.UnfreezeAllFish();
ResumeAllFishAnimations();
SyncFreeze(false);
if (_bingFengInstance != null) _bingFengInstance.SetActive(false);
}
}
@@ -2133,52 +2116,47 @@ public class GameMainPanel_FishBase : GameMainPanel_Base
case "40003": // 冰封
{
// ⭐ 个人效果(数量/toggle/倒计时/广播):只有使用者自己执行
// 服务器把 UsePropNotice 广播给房间内所有人,不判断 userId 会导致
// 别人用冰封,本地数量也减了、toggle 也亮了、倒计时也走了
// ⭐ 新协议:冰冻房间效果由服务端 FishFrozen 事件统一触发,
// UsePropNotice(40003) 只处理个人效果(数量/toggle)
// 房间效果(冻结鱼/暂停动画/冰冻特效)在 OnFishFrozen 中执行
bool isLocalSender = userId == GameManager.Instance.user.id;
if (isLocalSender)
{
_localBingFengCount--;
UpdateSkillPropsUI();
currBingFengTime = bingFengDuration;
toggle_BingFeng.SetIsOnWithoutNotify(true);
// ⭐ 只有发起者广播,避免其他客户端重复广播
SyncFreeze(true);
// 倒计时和 toggle 由 OnFishFrozen 统一启动,这里不重复设置
}
// ⭐ 房间效果(冻结鱼/暂停动画/冰冻特效):所有客户端都执行
// FreezeAllFish/PauseAllFishAnimations 内部有 isFrozen 守卫,
// 重复调用安全(不会覆盖已记录的状态)
if (bingFengEffect != null)
{
if (_bingFengInstance == null)
_bingFengInstance = Instantiate(bingFengEffect);
_bingFengInstance.SetActive(true);
}
CreateFishManager.Instance.FreezeAllFish();
PauseAllFishAnimations();
break;
}
}
}
public void SyncGameData_FreezeFish(SyncGameData sync)
/// <summary>
/// 服务端 FishFrozen 事件回调:冰冻道具(40003)生效。
/// 鱼存活时间延长 15 秒,客户端写死 15s 倒计时。
/// 所有客户端都执行(房间效果)。
/// </summary>
public void OnFishFrozen()
{
if (sync.sitPos == currSitData.posID) return; // 自己发的不管
// 启动 15s 倒计时
currBingFengTime = bingFengDuration;
toggle_BingFeng.SetIsOnWithoutNotify(true);
var data = FrameDataParser.Parse<FrameData_FreezeFish>(sync.frameData);
if (data.isFreeze)
// 冰冻特效
if (bingFengEffect != null)
{
CreateFishManager.Instance.FreezeAllFish();
PauseAllFishAnimations();
}
else
{
CreateFishManager.Instance.UnfreezeAllFish();
ResumeAllFishAnimations();
if (_bingFengInstance == null)
_bingFengInstance = Instantiate(bingFengEffect);
_bingFengInstance.SetActive(true);
}
// 冻结所有鱼 + 暂停动画
CreateFishManager.Instance.FreezeAllFish();
PauseAllFishAnimations();
}
// SyncGameData_FreezeFish 已废弃:冰冻改由服务端 FishFrozen 事件统一触发
// SyncFreeze 已废弃:不再需要客户端广播冰冻状态
#endregion
#region 冰冻动画暂停/恢复
@@ -2308,7 +2286,6 @@ public class GameMainPanel_FishBase : GameMainPanel_Base
public void TuiChuHHWK()
{
WebSocketSendList.LeaveRoom(GameManager.Instance.gameData.currRoomData.roomId, currSitData.user);
CreateFishManager.Instance.fangzhu = false;
}
#endregion
@@ -2707,27 +2684,16 @@ public class GameMainPanel_FishBase : GameMainPanel_Base
botLastAngle.Remove(sit.user);
botLastFireData.Remove(sit.user);
// ⭐ bot 退出后转移房主,否则房主是已退出的 bot 会导致出鱼停止
// ⭐ bot 退出后转移房主,否则房主是已退出的 bot 会导致房主显示异常
// 1) 清除 bot 的房主引用(如果它是房主)
// 2) TransferRoomOwner 会把房主交给第一个活跃的非 bot 玩家
// 注:鱼创建已由服务端 OnFishSpawn 统一下发,不再需要 fangzhu 兜底
if (SitPosData.roomOwnerSit == sit)
{
SitPosData.roomOwnerSit = null;
SitPosData.TransferRoomOwner();
}
// ⭐ 强制保证本地玩家能继续出鱼:
// bot 退出后如果本地玩家是新的房主(或房主为空但本地玩家在场),开启 fangzhu
// 这是对 ChangeControllerUser 网络消息的兜底,防止网络消息丢失或时序问题导致不出鱼
if (CreateFishManager.Instance != null)
{
bool localIsOwner = SitPosData.roomOwnerSit != null &&
SitPosData.roomOwnerSit.user == GameManager.Instance.user.id;
bool noOwner = SitPosData.roomOwnerSit == null;
if (localIsOwner || noOwner)
CreateFishManager.Instance.fangzhu = true;
}
if (cfg != null && cfg.exitCondition != null && cfg.exitCondition.showWaitingAfterExit)
sit.ShowWaiting();
else
@@ -122,6 +122,7 @@ public class UIHome_BY_SelectRoomLevelPanel : SubUIBase
else
{
// ⭐ 快速加入:调 /api/Room/QuickJoin 拿 roomId + webSocketUrl → 连 socket → JoinRoom(-1)
// leave: 1=初级场 / 2=中级场 / 3=高级场 (-1=全场次快速进入,暂未实现)
int gameType = (int)GameManager.Instance.gameData.currGameType;
APIRequestList.RoomQuickJoin(gameType, leave, (resp) =>
{