diff --git a/Assets/MyGame/Scripts/Characters/Monster/MonsterContral.cs b/Assets/MyGame/Scripts/Characters/Monster/MonsterContral.cs index 65639f5ab..35ea47fc7 100644 --- a/Assets/MyGame/Scripts/Characters/Monster/MonsterContral.cs +++ b/Assets/MyGame/Scripts/Characters/Monster/MonsterContral.cs @@ -194,6 +194,15 @@ public class MonsterContral : MonoBehaviour /// 路径已行进的时间(秒),用于B样曲线插值;可由外部预设偏移实现同步追帧。 public float moveTime = 0f; + /// + /// 阵鱼(700/701)创建时刻偏移(秒):由生成端填入"该鱼创建时刻 - 本次鱼阵开始时刻"。 + /// 位移公式里用 (moveTime + yzGroupTimeOffset) 取代 moveTime, + /// 使先创建的那一组已经沿分布方向游出去、后创建的从出生点出发, + /// 避免多个 组/鱼 全部重合在同一个出生点上。 + /// ⭐ 与 moveTime 分开:偏移只影响初始位置,不影响存活时长(moveTime 仍从 0 计时)。 + /// + public float yzGroupTimeOffset = 0f; + /// 是否为鱼阵成员 public bool isyz = false; @@ -395,6 +404,7 @@ public class MonsterContral : MonoBehaviour uirootRect = UIManager.Instance.uiRoot.GetComponent(); mainCamera = Camera.main; moveTime = 0f; + yzGroupTimeOffset = 0f; // 池复用清掉上次的阵鱼创建偏移 // ⭐ 重置创建视频状态:池回收的鱼 _isPlayingCreateVideo 可能残留 true, // 重新生成时重置,由 Init 决定是否重新播放 _createVideoDuration = 0f; @@ -718,7 +728,8 @@ public class MonsterContral : MonoBehaviour // 扇形/圈的阵型靠生成端分布角实现,移动方向必须与之一致; // _yzMoveDir 在 CacheYzLineMoveData 里按轴读生成姿态前方,投影过短回退(0,±1) Vector2 lineDir = _yzMoveDir; - Vector2 pos = (Vector2)_yzStartWorld + lineDir * moveTime * monsterData.moveLine.moveTime; + // ⭐ 加上创建时刻偏移:先创建的组已经游出去,后创建的从出生点出发,避免重合在同一点 + Vector2 pos = (Vector2)_yzStartWorld + lineDir * (moveTime + yzGroupTimeOffset) * monsterData.moveLine.moveTime; // ⭐ 朝向保持生成姿态(分布角)不转——不调 ApplyFacingDirection: // 它会覆盖根的分布角,把扇形/圈抹成一列竖队 @@ -746,7 +757,8 @@ public class MonsterContral : MonoBehaviour // ⭐ 同缓存分支:沿分布方向移动(缓存值优先,未初始化回退固定垂直),朝向不转 Vector2 lineDir = _yzMoveDir != Vector2.zero ? _yzMoveDir : new Vector2(0f, dir); - Vector2 pos = (Vector2)startWorld + lineDir * moveTime * monsterData.moveLine.moveTime; + // ⭐ 兜底分支同样加上创建时刻偏移(与缓存分支一致) + Vector2 pos = (Vector2)startWorld + lineDir * (moveTime + yzGroupTimeOffset) * monsterData.moveLine.moveTime; // ⭐ 保留根节点当前 z(2D 小鱼 z 一直为 0,行为不变) transform.position = new Vector3(pos.x, pos.y, transform.position.z); diff --git a/Assets/MyGame/Scripts/Manager/CreateFishManager.cs b/Assets/MyGame/Scripts/Manager/CreateFishManager.cs index 1c16d321f..baabf3f2c 100644 --- a/Assets/MyGame/Scripts/Manager/CreateFishManager.cs +++ b/Assets/MyGame/Scripts/Manager/CreateFishManager.cs @@ -222,6 +222,17 @@ public class CreateFishManager : MonoSingleton // fangzhu 字段已删除:鱼创建改由服务端 OnFishSpawn 统一下发,客户端无需房主出鱼 public bool isResources = false; + [Header("扇形阵(700)")] + [Tooltip("同一扇内相邻两条鱼之间的生成间隔(秒),避免17条鱼同帧出生叠在一起")] + public float shanXingFishInterval = 0.05f; + + [Header("鱼阵路线撞车")] + [Tooltip("同一条路线被鱼阵占用后,其他鱼阵再用到同一条线需要等待的秒数(避免同一出生点同帧重叠)")] + public float lineConflictCooldown = 1f; + + /// 每条鱼阵路线最近一次被鱼阵占用的时刻(Time.time),用于同线撞车延迟 + private readonly Dictionary _lineBusyTime = new Dictionary(); + /// /// 鱼阵实例全局计数器,每次 SpawnFishArray 递增。 /// 用于生成全局唯一 guid,防止 pathList 重复 pathId 或同 fishId 多次推送导致 guid 冲突。 @@ -449,6 +460,13 @@ public class CreateFishManager : MonoSingleton InitSpawnSystem(gameType); BindCurrentGamePanel(); EnsureBJEffectRoot(); + _lineBusyTime.Clear(); // 清掉上个房间的路线占用记录 + // ⭐ 必须先把 isResources 置 true 再回放: + // 调用方( GameMainPanel_FishBase )是在 EnterRoom 之后才置 true 的, + // 而鱼阵协程(IeShanXing/IeQuan/YZ7007)里都有 `if (!isResources) yield break;`, + // 回放时它还是 false(退房残留)→ 服务端进房瞬间推的初始鱼阵(init/sync) + // 全部 ARRAY_ENTER 后一条鱼都不生成,进房看不到鱼阵 + isResources = true; // ⭐ 回放进房加载期间收到的出鱼事件(服务端进房瞬间推的 init/sync 初始鱼) FlushPendingFishSpawns(); } @@ -1188,12 +1206,32 @@ public class CreateFishManager : MonoSingleton { yield break; } - ShanXing(createFishData, yz, pos, prefab, pathList[lineIndex], count, time, arraySeq, lineIndex); + // ⭐ 同线撞车:这条线刚被别的鱼阵用过时先等一会,避免同一出生点同帧叠一起 + yield return WaitLineFree(pathList[lineIndex]); + yield return ShanXing(createFishData, yz, pos, prefab, pathList[lineIndex], count, time, arraySeq, lineIndex); yield return new WaitForSeconds(1f); } } - private void ShanXing(FrameData_CreateFish createFishData, FishArrayConfig yz, + /// + /// 同线撞车等待:某条鱼阵路线在 lineConflictCooldown 秒内刚被鱼阵占用过, + /// 就等到冷却结束再发射;返回后立刻把该线标记为占用。 + /// ⭐ 进房时服务端会一口气推十几个鱼阵,很容易两个阵落在同一条线上, + /// 同帧同出生点出发必然重叠,靠这个把后出的推到 1 秒后。 + /// + private IEnumerator WaitLineFree(int pathId) + { + if (lineConflictCooldown > 0f && _lineBusyTime.TryGetValue(pathId, out float last)) + { + float wait = lineConflictCooldown - (Time.time - last); + if (wait > 0f && isResources) + yield return new WaitForSeconds(wait); + } + _lineBusyTime[pathId] = Time.time; + } + + /// 扇形阵:一条线的17条鱼按 shanXingFishInterval 逐条延迟生成(避免同帧出生叠在一起)。 + private IEnumerator ShanXing(FrameData_CreateFish createFishData, FishArrayConfig yz, Vector3 pos, GameObject prefab, int pathId, int count, long time, int arraySeq, int lineIndex) { var cfg = spawnDic[createFishData.fishNumber]; @@ -1227,25 +1265,31 @@ public class CreateFishManager : MonoSingleton mc.guid = createFishData.guid + "_" + arraySeq + "_" + lineIndex + "_" + i; RegisterFishInstance(mc, FishDatas[yz.fishNumber].fishType); - mc.moveTime = (BeijingTimeManager.Instance.CurrentTimestampMs - time) / 1000f; + // ⭐ moveTime 归零(存活时长从本条鱼自己出生算起), + // "创建时刻偏移"单独放进 yzGroupTimeOffset:它只进位移公式, + // 让先出的那一组已经沿分布方向游出去、后出的从出生点出发,避免整组重叠在同一点 + mc.moveTime = 0f; + mc.yzGroupTimeOffset = (BeijingTimeManager.Instance.CurrentTimestampMs - time) / 1000f; mc.Init(pathId.ToString(), cfg.times, true, yz.arrayNumber); // ⭐ 鱼阵调试日志:输出每条子鱼生成后的旋转与缩放(排查"有的正有的反") Debug.Log($"[鱼阵调试] 阵={yz.arrayNumber}(扇形700) pathId={pathId} 第{i}/{16}条 guid={mc.guid} " + $"rotation={fish.transform.eulerAngles.ToString("F1")} localScale={fish.transform.localScale.ToString("F3")}"); + + // ⭐ 同一扇内逐条延迟:避免17条鱼同帧出生全叠在出生点 + yield return new WaitForSeconds(shanXingFishInterval); } } /// /// 环形阵:按路线列表遍历生成圈。 - /// 正常无延迟(同帧出);⭐ 同一鱼阵内出现重复的同一条线(同一出生点)时, - /// 延迟 1s 再发射,避免两圈叠在出生点。 + /// 正常无延迟(同帧出);⭐ 同一条线(lineConflictCooldown 秒内)刚被鱼阵用过时, + /// 等待冷却再发射,避免两圈叠在同一出生点(含鱼阵内重复线、不同鱼阵撞线)。 /// private IEnumerator IeQuan(FrameData_CreateFish createFishData, FishArrayConfig yz, Vector3 pos, GameObject prefab, List pathList, long time, int count, int arraySeq) { - var usedLines = new HashSet(); for (int lineIndex = 0; lineIndex < pathList.Count; lineIndex++) { if (!isResources) @@ -1254,10 +1298,7 @@ public class CreateFishManager : MonoSingleton } int pathId = pathList[lineIndex]; - // ⭐ 该线本次鱼阵已出现过(同一出生点):延迟 1s 再发射 - if (!usedLines.Add(pathId)) - yield return new WaitForSeconds(1f); - + yield return WaitLineFree(pathId); Quan(createFishData, yz, pos, prefab, pathId, count, time, arraySeq, lineIndex); } } @@ -1290,7 +1331,9 @@ public class CreateFishManager : MonoSingleton mc.guid = createFishData.guid + "_" + arraySeq + "_" + lineIndex + "_" + i; RegisterFishInstance(mc, FishDatas[yz.fishNumber].fishType); - mc.moveTime = (BeijingTimeManager.Instance.CurrentTimestampMs - time) / 1000f; + // ⭐ moveTime 归零 + 创建时刻偏移进 yzGroupTimeOffset(说明同扇形) + mc.moveTime = 0f; + mc.yzGroupTimeOffset = (BeijingTimeManager.Instance.CurrentTimestampMs - time) / 1000f; mc.Init(pathId.ToString(), cfg.times, true, yz.arrayNumber);