This commit is contained in:
2026-09-29 18:09:08 +08:00
parent c91eaef60b
commit f4b5347fde
14 changed files with 866 additions and 762 deletions
@@ -179,9 +179,57 @@ public class MonsterMoveLine
return;
}
foreach (var text in handles.Result)
// ⭐ 主线程只做 Unity API 访问:把每个 TextAsset 的 name/text 读成纯托管字符串后立即释放句柄。
// .text 返回托管 string 拷贝,释放句柄后仍可安全在后台线程使用。
int fileCount = handles.Result.Count;
string[] fileNames = new string[fileCount];
string[] fileTexts = new string[fileCount];
for (int i = 0; i < fileCount; i++)
{
var lines = text.text.Split('\n');
fileNames[i] = handles.Result[i].name;
fileTexts[i] = handles.Result[i].text;
}
Addressables.Release(handles);
// ⭐ 座位镜像状态主线程读一次传入后台(后台线程不访问任何 Unity/游戏静态状态)
bool mirrored = SeatMirror.IsMirrored;
var warnings = new List<string>();
// ⭐ 重活挪到后台线程:全部路线文件的 Split + 逐坐标点 float 解析是纯托管计算,
// 文件大时主线程同步解析会卡死数秒(进度条停在20%冻结的根源)。
// await 完成后自动切回 Unity 主线程再写共享缓存。
var parsedCurves = await Task.Run(() => ParseBezierFiles(fileNames, fileTexts, mirrored, warnings));
// 后台收集的告警回主线程输出(Debug API 只能在主线程调用),只打前 20 条防刷屏
for (int i = 0; i < warnings.Count && i < 20; i++)
Debug.LogWarning(warnings[i]);
// ⭐ 回到主线程写共享缓存 + 构建点数组/节点向量缓存
// (同名直接覆盖,清场后理论上无重复,防御性覆盖确保永远用当前玩法数据)
foreach (var curve in parsedCurves)
{
curves.Add(curve);
dic_BezierCurveDatas[curve.name] = curve;
BuildCurvePointsArray(curve);
}
// ⭐ 座位镜像状态提示:镜像未初始化(-1)时按不镜像处理,排查进房时序问题用
if (SeatMirror.LocalServerPos < 0)
Debug.LogWarning("[MonsterMoveLine] 加载路线时 SeatMirror 未初始化(未收到 RoomJoinNotice),本次按不镜像处理");
Debug.Log($"曲线读取完成共{dic_BezierCurveDatas.Count}条 镜像={SeatMirror.IsMirrored}(本地服务端座位={SeatMirror.LocalServerPos})");
}
/// <summary>
/// ⭐ 后台线程解析全部路线文本(纯托管计算,禁止访问任何 Unity API):
/// 逐行 Split + InvariantCulture float 解析 + 曲线组装 + 镜像翻转(镜像标志由主线程传入)。
/// 解析告警收集到 warnings 列表,由调用方回主线程统一输出。
/// </summary>
private static List<BezierCurveData> ParseBezierFiles(string[] fileNames, string[] fileTexts, bool mirrored, List<string> warnings)
{
var result = new List<BezierCurveData>();
for (int f = 0; f < fileTexts.Length; f++)
{
var lines = fileTexts[f].Split('\n');
foreach (var line in lines)
{
if (string.IsNullOrWhiteSpace(line))
@@ -206,7 +254,7 @@ public class MonsterMoveLine
}
else
{
Debug.LogWarning($"[MonsterMoveLine] 坐标格式非法已跳过: '{parts[i]}'({text.name} 曲线 {curve.name})");
warnings.Add($"[MonsterMoveLine] 坐标格式非法已跳过: '{parts[i]}'({fileNames[f]} 曲线 {curve.name})");
}
}
}
@@ -221,23 +269,25 @@ public class MonsterMoveLine
for (int i = 1; i < points.Count - 1; i++)
curve.controlPoints.Add(points[i]);
// ⭐ 座位镜像:本端在上方座位时路线整条上下翻转(写入字典/建缓存前,只翻一次)
FlipCurveYForMirror(curve);
// ⭐ 座位镜像:本端在上方座位时路线整条上下翻转(与 FlipCurveYForMirror 同逻辑)。
// B样条求值(de Boor)是逐点线性插值,对控制点 y 取反后求样条 ≡ 原样条求值后 y 取反,
// 曲线形状严格镜像不形变。
if (mirrored)
{
curve.start.y = -curve.start.y;
curve.end.y = -curve.end.y;
for (int i = 0; i < curve.controlPoints.Count; i++)
{
Vector2 p = curve.controlPoints[i];
p.y = -p.y;
curve.controlPoints[i] = p;
}
}
curves.Add(curve);
// ⭐ 同名直接覆盖(清场后理论上无重复,防御性覆盖确保永远用当前玩法数据)
dic_BezierCurveDatas[curve.name] = curve;
// ⭐ 同步构建点数组缓存(批量加载路径)
BuildCurvePointsArray(curve);
result.Add(curve);
}
}
Addressables.Release(handles);
// ⭐ 座位镜像状态提示:镜像未初始化(-1)时按不镜像处理,排查进房时序问题用
if (SeatMirror.LocalServerPos < 0)
Debug.LogWarning("[MonsterMoveLine] 加载路线时 SeatMirror 未初始化(未收到 RoomJoinNotice),本次按不镜像处理");
Debug.Log($"曲线读取完成共{dic_BezierCurveDatas.Count}条 镜像={SeatMirror.IsMirrored}(本地服务端座位={SeatMirror.LocalServerPos})");
return result;
}
public static async Task UnLoadBezierTxtAsync(string path)
@@ -2,6 +2,7 @@ using Assets.MyGame.Scripts.Net.API;
using Assets.MyGame.Scripts.Net.WebResponses;
using Newtonsoft.Json;
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Text;
@@ -116,12 +117,10 @@ namespace Assets.MyGame.Scripts.Net
}
if (UIManager.Instance.Get<GameMainPanel_Base>(uIPanel)==null)
{
UIManager.Instance.ShowAsync(uIPanel, UIPanel.HomeMainPanel, msg);
// 卸载所有已加载的游戏地图场景(保证场景唯一性)
// ① 先卸载其他已加载的游戏地图场景(保证场景唯一性),不卸载即将加载的目标场景
string[] gameScenes = { "FLZY", "YYSC", "XJMH", "HHWK" };
foreach (var sn in gameScenes)
{
// 不卸载即将加载的目标场景
if (sn == sceneName) continue;
var s = SceneManager.GetSceneByName(sn);
if (s.IsValid() && s.isLoaded)
@@ -130,16 +129,15 @@ namespace Assets.MyGame.Scripts.Net
_loadedGameScenes.Remove(sn);
}
}
// 目标场景仅加载一次:标记已加载后不再重复 LoadSceneAsync(防止人机通知触发重复加载)
if (!string.IsNullOrEmpty(sceneName) && !_loadedGameScenes.Contains(sceneName))
{
_loadedGameScenes.Add(sceneName);
var targetScene = SceneManager.GetSceneByName(sceneName);
if (!targetScene.IsValid() || !targetScene.isLoaded)
SceneManager.LoadSceneAsync(sceneName, LoadSceneMode.Additive);
}
UIManager.Instance.Close(UIPanel.HomeMainPanel);
// ② 先显示游戏面板(加载页面,含进度条):ShowAsync 的 onShown 回调在面板实例化并显示后触发,
// 用它保证"面板先显示 → 再加载场景 → 再加载资源"的严格先后顺序。
// 全程为协程异步,不做 WaitForCompletion 之类的同步等待,不阻塞主线程。
UIManager.Instance.ShowAsync(uIPanel, UIPanel.HomeMainPanel, msg, (panel) =>
{
UIManager.Instance.StartCoroutine(LoadSceneThenResources(panel, sceneName));
});
WebSocketManager.Instance.isGame = true;
}
else
@@ -165,6 +163,30 @@ namespace Assets.MyGame.Scripts.Net
}
/// <summary>
/// ③ 加载游戏场景(异步等待完成)→ ④ 场景就绪后再让面板加载资源(鱼/音频/路线等)。
/// 全程逐帧 yield 等待,不做同步等待,不阻塞主线程。
/// </summary>
private static IEnumerator LoadSceneThenResources(UIBase panel, string sceneName)
{
// ③ 目标场景仅加载一次:标记已加载后不再重复 LoadSceneAsync(防止人机通知触发重复加载)
if (!string.IsNullOrEmpty(sceneName) && !_loadedGameScenes.Contains(sceneName))
{
_loadedGameScenes.Add(sceneName);
var targetScene = SceneManager.GetSceneByName(sceneName);
if (!targetScene.IsValid() || !targetScene.isLoaded)
{
var op = SceneManager.LoadSceneAsync(sceneName, LoadSceneMode.Additive);
while (op != null && !op.isDone) yield return null;
}
}
// ④ 场景就绪后再加载面板资源
if (panel is GameMainPanel_FishBase fishPanel)
fishPanel.BeginLoadResources();
}
/// <summary>
/// 把 RoomJoinNotice 玩家数据写入 currRoomData.players(存在则更新,不存在则添加)。
/// JoinGame 里 SetUserInfo 按 userId 从 currRoomData.players 查显示信息