#region AI优化 using System.Collections; using System.Collections.Generic; using System.IO; using System.Threading.Tasks; using UnityEngine; using UnityEngine.AddressableAssets; using UnityEngine.ResourceManagement.AsyncOperations; using System; using System.Globalization; [Serializable] public class MonsterMoveLine { public static Dictionary dic_BezierCurveDatas = new Dictionary(); public static List curves = new List(); // ---------------------------------------------------------------- // ⭐ 性能优化:曲线点数组静态缓存 // 每条曲线的 [start, controlPoints..., end] 预组装成 Vector2[], // 所有鱼共享同一份数组,避免每帧每鱼 new List + ToArray 的 GC 分配。 // 加载时构建,卸载时清理。 // ---------------------------------------------------------------- private static readonly Dictionary _curvePointsArrays = new Dictionary(); /// ⭐ B样条节点向量缓存(曲线名 → 节点数组,与点数组同步构建/清理) private static readonly Dictionary _curveKnotArrays = new Dictionary(); [Serializable] public class BezierCurveData { public string name; public Vector2 start; public List controlPoints = new List(); public Vector2 end; } /// 加载当前玩法路线。onProgress:0~1 进度回调(供进房进度条;路线文本可能和鱼资源在同一个大包里, /// 包的解压耗时会体现在这里,避免大包场景进度条失真) public static async Task LoadBezier(Action onProgress = null) { await LoadBezierTxtAsync("MoveLine", onProgress); } public static async Task UnLoadBezier() { await UnLoadBezierTxtAsync("MoveLine"); } public static async Task LoadSingleBezierTxtAsync(string path) { AsyncOperationHandle handle = Addressables.LoadAssetAsync(path); await handle.Task; var lines = handle.Result.text.Split('\n'); foreach (var line in lines) { if (string.IsNullOrWhiteSpace(line)) continue; var parts = line.Split(';'); BezierCurveData curve = new BezierCurveData(); curve.name = parts[0]; List points = new List(); for (int i = 1; i < parts.Length; i++) { if (string.IsNullOrWhiteSpace(parts[i])) continue; // ⭐ 单点格式错误只跳过该点并告警,不能让整条路线(乃至进房加载流程)中断 if (TryParseVector(parts[i], out var p)) points.Add(p); else Debug.LogWarning($"[MonsterMoveLine] 坐标格式非法已跳过: '{parts[i]}'({handle.Result.name} 曲线 {curve.name})"); } if (points.Count == 0) continue; curve.start = points[0]; if (points.Count > 1) curve.end = points[points.Count - 1]; for (int i = 1; i < points.Count - 1; i++) curve.controlPoints.Add(points[i]); // ⭐ 座位镜像:本端在上方座位时路线整条上下翻转(写入字典/建缓存前,只翻一次) FlipCurveYForMirror(curve); curves.Add(curve); dic_BezierCurveDatas.Add(curve.name, curve); // ⭐ 同步构建点数组缓存(单条加载路径) BuildCurvePointsArray(curve); } } /// /// ⭐ 座位镜像:路线整条上下翻转(所有点 y 关于画布中心取反,画布坐标以中心为原点)。 /// B样条求值(de Boor)是逐点线性插值,对控制点 y 取反后求样条 ≡ 原样条求值后 y 取反, /// 曲线形状严格镜像不形变。必须在写入 dic_BezierCurveDatas / BuildCurvePointsArray /// 之前调用(curves 列表与字典引用同一实例,只翻一次)。 /// 下游自动适应:取点/移动/朝向切线/鱼阵 700 起点/翻面/分布角全部基于翻转后数据; /// 仅 701 环形阵写死的"固定向上"需在 MonsterContral 里随镜像取反。 /// private static void FlipCurveYForMirror(BezierCurveData curve) { if (!SeatMirror.IsMirrored) return; 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; } } /// /// ⭐ 将曲线的 [start, controlPoints..., end] 组装成 Vector2[] 并缓存。 /// 所有引用此曲线的鱼共享同一份数组,避免每帧每鱼 new List + ToArray 的 GC。 /// private static void BuildCurvePointsArray(BezierCurveData curve) { // ⭐ 无条件覆盖重建:同名曲线被覆盖时,点数组/节点缓存必须同步重建, // 否则字典指向新曲线、缓存还是旧曲线的点 → 路线错乱 int total = 2 + curve.controlPoints.Count; // start + controls + end Vector2[] arr = new Vector2[total]; arr[0] = curve.start; for (int i = 0; i < curve.controlPoints.Count; i++) arr[1 + i] = curve.controlPoints[i]; arr[total - 1] = curve.end; _curvePointsArrays[curve.name] = arr; // ⭐ 同步构建 B样条节点向量缓存 _curveKnotArrays[curve.name] = BuildKnotVector(total); } /// /// ⭐ 构建钳位(clamped)均匀 B样条节点向量。 /// 首尾各重复 p+1 个 0/1 → 曲线严格过首末控制点(start/end),鱼的出入屏位置与路线数据一致; /// 内部节点在 (0,1) 均匀分布。度数 p = min(3, 控制点数-1),点不足时自动降阶。 /// private static float[] BuildKnotVector(int controlPointCount) { int p = Mathf.Min(3, controlPointCount - 1); int knotCount = controlPointCount + p + 1; float[] knots = new float[knotCount]; // 首尾钳位:前 p+1 个 0,后 p+1 个 1 for (int i = 0; i <= p; i++) knots[i] = 0f; for (int i = controlPointCount; i < knotCount; i++) knots[i] = 1f; // 内部节点:i = p+1 .. N-1,均匀分布在 (0,1) float denom = controlPointCount - p; for (int i = p + 1; i < controlPointCount; i++) knots[i] = (i - p) / denom; return knots; } /// /// ⭐ 路线加载句柄(保留到离房才释放): /// 路线文本常和鱼资源打在同一个大包里。旧实现"读完文本立即 Release", /// 单一大包场景下引用归零 → 大包立即卸载 → LoadFishAssetsData 又重新加载同一个包 /// (同一个 LZMA 大包解压两次 + 内存暴涨→回落→再暴涨的过山车)。 /// 现在句柄保留,与鱼包句柄共存,离房 UnLoadBezierTxtAsync 统一释放。 /// private static AsyncOperationHandle> _bezierHandles; public static async Task LoadBezierTxtAsync(string path, Action onProgress = null) { // ⭐ 资源加载超过 0.1 秒才显示"加载中"界面(引用计数,快速加载不显示) UIManager.BeginLoading(); try { await LoadBezierTxtAsyncCore(path, onProgress); } finally { UIManager.EndLoading(); } } /// LoadBezierTxtAsync 的实际逻辑(外层负责"加载中"界面) private static async Task LoadBezierTxtAsyncCore(string path, Action onProgress) { // ⭐ 加载前先全量清空上一场的路线缓存: // 各玩法普通路线/鱼阵曲线名完全重名(曲线1~N、鱼阵N)但坐标不同, // 不清场会把上一玩法的同名坐标带进本场 → 路线错乱(如 HHWK 残留导致 FLZY 鱼按旧路线游); // 旧玩法独有的 Boss 路线也会残留越积越多。进游戏只需要当前玩法的路线。 ClearAllBezierCache(); List keys = new List(); keys.Add(path); keys.Add(GameManager.Instance.gameData.currGameType.ToString()); // ⭐ 防御:上一轮句柄未释放(如离房流程被跳过)时先释放,避免引用计数累加 if (_bezierHandles.IsValid()) Addressables.Release(_bezierHandles); _bezierHandles = Addressables.LoadAssetsAsync( keys, null, Addressables.MergeMode.Intersection); // ⭐ 轮询上报进度(0~85% 为加载段):路线文本常和鱼资源打在同一个大包里, // 大包的解压耗时发生在这里,必须反映到进度条(否则单一大包场景进度失真: // 卡在路线段不动,后面的"鱼包段"反而秒过)。无回调时直接等完成。 if (onProgress != null) { while (!_bezierHandles.IsDone) { onProgress(0.85f * _bezierHandles.PercentComplete); await Task.Delay(50); } } else { await _bezierHandles.Task; } // ⭐ 没有资源 → 直接跳出,不报错(缓存已清空,保持干净状态) // 句柄保留(离房统一释放),防止路线与鱼同包时提前卸掉大包 if (_bezierHandles.Result == null || _bezierHandles.Result.Count == 0) { Debug.LogWarning($"Bezier 曲线加载跳过:未找到标签 {path} + {GameManager.Instance.gameData.currGameType}"); onProgress?.Invoke(1f); return; } // ⭐ 主线程只做 Unity API 访问:把每个 TextAsset 的 name/text 读成纯托管字符串。 // .text 返回托管 string 拷贝,句柄保留期间数据稳定,后台线程可安全使用。 // (句柄不再立即 Release —— 路线与鱼同包时提前释放会把整个大包卸掉) int fileCount = _bezierHandles.Result.Count; string[] fileNames = new string[fileCount]; string[] fileTexts = new string[fileCount]; for (int i = 0; i < fileCount; i++) { fileNames[i] = _bezierHandles.Result[i].name; fileTexts[i] = _bezierHandles.Result[i].text; } onProgress?.Invoke(0.88f); // ⭐ 座位镜像状态主线程读一次传入后台(后台线程不访问任何 Unity/游戏静态状态) bool mirrored = SeatMirror.IsMirrored; var warnings = new List(); // ⭐ 重活挪到后台线程:全部路线文件的 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); } onProgress?.Invoke(1f); // ⭐ 座位镜像状态提示:镜像未初始化(-1)时按不镜像处理,排查进房时序问题用 if (SeatMirror.LocalServerPos < 0) Debug.LogWarning("[MonsterMoveLine] 加载路线时 SeatMirror 未初始化(未收到 RoomJoinNotice),本次按不镜像处理"); Debug.Log($"曲线读取完成共{dic_BezierCurveDatas.Count}条 镜像={SeatMirror.IsMirrored}(本地服务端座位={SeatMirror.LocalServerPos})"); } /// /// ⭐ 后台线程解析全部路线文本(纯托管计算,禁止访问任何 Unity API): /// 逐行 Split + InvariantCulture float 解析 + 曲线组装 + 镜像翻转(镜像标志由主线程传入)。 /// 解析告警收集到 warnings 列表,由调用方回主线程统一输出。 /// private static List ParseBezierFiles(string[] fileNames, string[] fileTexts, bool mirrored, List warnings) { var result = new List(); for (int f = 0; f < fileTexts.Length; f++) { var lines = fileTexts[f].Split('\n'); foreach (var line in lines) { if (string.IsNullOrWhiteSpace(line)) continue; var parts = line.Split(';'); if (parts.Length < 2) continue; BezierCurveData curve = new BezierCurveData(); curve.name = parts[0]; List points = new List(); for (int i = 1; i < parts.Length; i++) { if (!string.IsNullOrWhiteSpace(parts[i])) { // ⭐ 单点格式错误只跳过该点并告警,不能让整条路线(乃至进房加载流程)中断 if (TryParseVector(parts[i], out var p)) { points.Add(p); } else { warnings.Add($"[MonsterMoveLine] 坐标格式非法已跳过: '{parts[i]}'({fileNames[f]} 曲线 {curve.name})"); } } } if (points.Count == 0) continue; curve.start = points[0]; if (points.Count > 1) curve.end = points[points.Count - 1]; for (int i = 1; i < points.Count - 1; i++) curve.controlPoints.Add(points[i]); // ⭐ 座位镜像:本端在上方座位时路线整条上下翻转(与 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; } } result.Add(curve); } } return result; } public static async Task UnLoadBezierTxtAsync(string path) { // ⭐ 释放路线加载句柄(与鱼包句柄共同归零后,路线/鱼共用的大包才真正卸载) if (_bezierHandles.IsValid()) { Addressables.Release(_bezierHandles); } // ⭐ 全量同步清空,不再按 currGameType 重查 Addressables 删除: // 1. 卸载依赖 currGameType 有时序风险 —— 选房面板在玩家点选新玩法时就改了全局类型, // 延迟执行的卸载(OnHide 补卸载/异常重跑)会按新玩法 txt 去删 → 旧玩法 Boss 路线全部残留; // 甚至可能把新玩法刚加载的路线误删。 // 2. 原实现标签查询为空时静默 return,什么都清不掉。 // 卸载只发生在离房时,场上只有当前玩法的路线,直接全清最干净且无竞态。 ClearAllBezierCache(); Debug.Log("曲线卸载完成,剩余曲线数量:" + dic_BezierCurveDatas.Count); await Task.CompletedTask; } /// 全量清空路线缓存(曲线字典/列表 + 点数组缓存 + B样条节点缓存) private static void ClearAllBezierCache() { dic_BezierCurveDatas.Clear(); curves.Clear(); _curvePointsArrays.Clear(); _curveKnotArrays.Clear(); } private static bool TryParseVector(string s, out Vector2 v) { v = default; var xy = s.Split(','); if (xy.Length < 2) return false; // ⭐ 固定用 InvariantCulture:曲线文件是 "x,y" 机器格式,不能受设备区域的小数点符号影响 if (!float.TryParse(xy[0], NumberStyles.Float, CultureInfo.InvariantCulture, out float x)) return false; if (!float.TryParse(xy[1], NumberStyles.Float, CultureInfo.InvariantCulture, out float y)) return false; v = new Vector2(x, y); return true; } public BezierCurveData bezierCurveData; public float moveTime; public float moveSpeed; // ⭐ 缓存相机和 UI 根 RectTransform,避免每帧每鱼调用 // UIManager.Instance.uiRoot.GetComponent() 和 Camera.main // 由 MonsterContral.OnEnable 同步赋值 public Camera cachedCamera; public RectTransform cachedUirootRect; public void Init(BezierCurveData bezierCurveData, float Time) { this.bezierCurveData = bezierCurveData; this.moveTime = Time; Debug.Log($"初始化曲线 {this.bezierCurveData.name} ,移动时间 {moveTime}"); } /// /// 主线程零分配计算曲线点(⭐ B样条 de Boor 算法;原贝塞尔 De Casteljau 已注释保留备查,方法名保留兼容调用处)。 /// ⭐ 钳位(clamped)均匀三次 B样条: /// - 节点向量首尾各重复 p+1 个 0/1 → 曲线严格过首控制点(start)和末控制点(end), /// 鱼的出入屏位置与路线数据一致 /// - 中间控制点只被逼近不再穿过(与贝塞尔的本质区别) /// - 控制点总数 ≤ 4 时钳位B样条数学上退化为同阶贝塞尔,短路线行为天然兼容 /// - 度数 p = min(3, N-1):仅 start+end 两点时退化为直线段 /// ⭐ 优化说明(原 Job 方案的负优化问题): /// 1. 原 GetBezierPointJob 每次调用 new List + 2 个 NativeArray(TempJob) + ToArray, /// 每条鱼每帧产生 3 次堆分配,N 条鱼 = 3N 次 GC 分配/帧 /// 2. 原 Job.Schedule() 后立即 Complete(),Job 实际单线程执行, /// 调度开销 > 计算收益(4-7 个控制点的计算量极小) /// 3. 现方案:曲线点数组+节点向量静态缓存(共享),主线程直接计算,零 GC 分配 /// 4. Camera/RectTransform 由调用方缓存,避免每帧每鱼重复获取 /// public Vector2 GetBezierPointJob(float t) { // ⭐ 防止除零:moveTime<=0 时直接返回起点,避免 NaN if (moveTime <= 0f) return bezierCurveData.start; if (t > moveTime) t = moveTime; t = t / moveTime; // ⭐ 取静态缓存的曲线点数组(所有鱼共享,零分配) if (!_curvePointsArrays.TryGetValue(bezierCurveData.name, out var pts)) { // 兜底:缓存未命中时实时构建(理论上不应发生,加载时已构建) BuildCurvePointsArray(bezierCurveData); pts = _curvePointsArrays[bezierCurveData.name]; } // ⭐ 取静态缓存的 B样条节点向量(加载时与点数组同步构建) if (!_curveKnotArrays.TryGetValue(bezierCurveData.name, out var knots)) { // 兜底:缓存未命中时实时构建 knots = BuildKnotVector(pts.Length); _curveKnotArrays[bezierCurveData.name] = knots; } // ⭐ 复用线程静态缓冲区(主线程单线程执行,安全复用) if (_tempPoints == null || _tempPoints.Length < pts.Length) _tempPoints = new Vector2[pts.Length]; Array.Copy(pts, _tempPoints, pts.Length); int count = pts.Length; // 控制点总数 N = start + 控制点 + end int p = Mathf.Min(3, count - 1); // ⭐ B样条度数:默认三次,控制点不足时自动降阶 Vector2 canvasPos; // ⭐ 钳位B样条端点:曲线严格过首末控制点;de Boor 在端点节点区间退化,直接取端点等价且更稳 if (t <= 0f) { canvasPos = pts[0]; } else if (t >= 1f) { canvasPos = pts[count - 1]; } else { // ================= 贝塞尔(De Casteljau)已停用,注释保留备查 ================= // int c = pts.Length; // while (c > 1) // { // for (int i = 0; i < c - 1; i++) // _tempPoints[i] = Vector2.Lerp(_tempPoints[i], _tempPoints[i + 1], t); // c--; // } // canvasPos = _tempPoints[0]; // ========================================================================== // ================= ⭐ B样条求值(de Boor 递推) ================= // 找节点区间 k: knots[k] <= u < knots[k+1],k ∈ [p, N-1] int k = p; while (k < count - 1 && t >= knots[k + 1]) k++; // d[j] = pts[j + k - p],j = 0..p(取当前区间涉及的 p+1 个控制点) for (int j = 0; j <= p; j++) _tempPoints[j] = pts[j + k - p]; // de Boor 逐层递推(结构同 De Casteljau,alpha 改由节点向量决定) for (int r = 1; r <= p; r++) { for (int j = p; j >= r; j--) { // 分母区间 [j+k-p, j+1+k-r] ⊇ 当前节点区间 [k, k+1],恒非退化,无除零风险 float alpha = (t - knots[j + k - p]) / (knots[j + 1 + k - r] - knots[j + k - p]); _tempPoints[j] = Vector2.Lerp(_tempPoints[j - 1], _tempPoints[j], alpha); } } canvasPos = _tempPoints[p]; // ==================================================================== } // ⭐ 用缓存的 Camera/RectTransform,避免每帧每鱼查询 var cam = cachedCamera != null ? cachedCamera : Camera.main; var rect = cachedUirootRect != null ? cachedUirootRect : UIManager.Instance.uiRoot.GetComponent(); return CoordinateTransformation.CanvasToWorld(canvasPos, rect, cam); } /// 主线程复用的临时计算缓冲(避免每帧每鱼 new 数组) [ThreadStatic] private static Vector2[] _tempPoints; } #endregion #region zijixie //using System.Collections; //using System.Collections.Generic; //using System.IO; //using System.Threading.Tasks; //using UnityEngine; //using UnityEngine.AddressableAssets; //using UnityEngine.ResourceManagement.AsyncOperations; //public class MonsterMoveLine //{ // public static Dictionary dic_BezierCurveDatas = new Dictionary(); // public static List curves = new List(); // public class BezierCurveData // { // public string name; // public Vector2 start; // public List controlPoints = new List(); // public Vector2 end; // } // public static async void LoadBezier() // { // await LoadBezierTxtAsync("MoveLine"); // } // /// // /// 读取曲线数据 读取单个 // /// // /// // /// // public static async Task LoadSingleBezierTxtAsync(string path) // { // AsyncOperationHandle handle = Addressables.LoadAssetAsync(path); // await handle.Task; // var lines = handle.Result.text.Split('\n'); // foreach (var line in lines) // { // if (string.IsNullOrWhiteSpace(line)) continue; // var parts = line.Split(';'); // BezierCurveData curve = new BezierCurveData(); // // 第一个是曲线名 // curve.name = parts[0]; // // 后面所有都是点 // List points = new List(); // for (int i = 1; i < parts.Length; i++) // { // points.Add(ParseVector(parts[i])); // } // // 第一个点是 start // curve.start = points[0]; // if (points.Count > 1) // { // // 最后一个点是 end // curve.end = points[points.Count - 1]; // } // // 中间的都是 controlPoints // for (int i = 1; i < points.Count - 1; i++) // { // curve.controlPoints.Add(points[i]); // } // curves.Add(curve); // dic_BezierCurveDatas.Add(curve.name, curve); // } // } // /// // /// 读取曲线数据 游戏开始时在awake调用aa包 // /// // /// // /// // public static async Task LoadBezierTxtAsync(string path) // { // List keys = new List(); // keys.Add(path); // keys.Add(GameManager.Instance.gameData.currGameType.ToString()); // AsyncOperationHandle> handles = Addressables.LoadAssetsAsync(keys, null, Addressables.MergeMode.Intersection); // await handles.Task; // foreach (var text in handles.Result) // { // var lines = text.text.Split('\n'); // foreach (var line in lines) // { // if (string.IsNullOrWhiteSpace(line)) continue; // var parts = line.Split(';'); // BezierCurveData curve = new BezierCurveData(); // // 第一个是曲线名 // curve.name = parts[0]; // // 后面所有都是点 // List points = new List(); // for (int i = 1; i < parts.Length; i++) // { // points.Add(ParseVector(parts[i])); // } // // 第一个点是 start // curve.start = points[0]; // if (points.Count > 1) // { // // 最后一个点是 end // curve.end = points[points.Count - 1]; // } // // 中间的都是 controlPoints // for (int i = 1; i < points.Count - 1; i++) // { // curve.controlPoints.Add(points[i]); // } // curves.Add(curve); // dic_BezierCurveDatas.Add(curve.name, curve); // } // } // Debug.Log("曲线读取完成共" + dic_BezierCurveDatas.Count + "条"); // } // private static Vector2 ParseVector(string s) // { // var xy = s.Split(','); // return new Vector2(float.Parse(xy[0]), float.Parse(xy[1])); // } // public BezierCurveData bezierCurveData; // public float moveTime; // public float moveSpeed; // public void Init(BezierCurveData bezierCurveData, float Time) // { // this.bezierCurveData = bezierCurveData; // this.moveTime = time; // Debug.Log($"初始化曲线 {this.bezierCurveData.name} ,移动时间 {moveTime}"); // } // /// // /// 获取曲线上的点 // /// // /// // /// // public Vector2 GetBezierPoint(float t) // { // t = t / moveTime; // BezierCurveData curve = bezierCurveData; // List points = new List(); // points.Add(curve.start); // points.AddRange(curve.controlPoints); // points.Add(curve.end); // return DeCasteljau_BY(points, t % 1); // } // /// // /// 贝塞尔 // /// // /// // /// // /// // private Vector2 DeCasteljau_BSE(List points, float t) // { // while (points.Count > 1) // { // List next = new List(); // for (int i = 0; i < points.Count - 1; i++) // { // next.Add(Vector2.Lerp(points[i], points[i + 1], t)); // } // points = next; // } // return CoordinateTransformation.CanvasToWorld(points[0], UIManager.Instance.uiRoot.GetComponent(), Camera.main); // } // /// // /// B样 // /// // /// // /// // /// // private Vector2 DeCasteljau_BY(List points, float t) // { // // 方法A:保持德卡斯特里奥算法(推荐,数值更稳定) // while (points.Count > 1) // { // List next = new List(); // for (int i = 0; i < points.Count - 1; i++) // { // next.Add(Vector2.Lerp(points[i], points[i + 1], t)); // } // points = next; // } // return CoordinateTransformation.CanvasToWorld(points[0], // UIManager.Instance.uiRoot.GetComponent(), Camera.main); // /* 方法B:伯恩斯坦多项式形式(标准贝塞尔公式)- 如果要用这个替换上面的循环 // int n = points.Count - 1; // Vector2 result = Vector2.zero; // for (int i = 0; i <= n; i++) // { // float bernstein = BinomialCoefficient(n, i) * Mathf.Pow(t, i) * Mathf.Pow(1 - t, n - i); // result += points[i] * bernstein; // } // return CoordinateTransformation.CanvasToWorld(result, // UIManager.Instance.uiRoot.GetComponent(), Camera.main); // */ // } // // 计算二项式系数 // private float BinomialCoefficient(int n, int k) // { // float result = 1f; // for (int i = 1; i <= k; i++) // { // result *= (float)(n - k + i) / i; // } // return result; // } //} #endregion