diff --git a/Assets/AddressableAssetsData/Windows/addressables_content_state.bin b/Assets/AddressableAssetsData/Windows/addressables_content_state.bin index c4c6b185f..d08a3ff86 100644 Binary files a/Assets/AddressableAssetsData/Windows/addressables_content_state.bin and b/Assets/AddressableAssetsData/Windows/addressables_content_state.bin differ diff --git a/Assets/MyGame/Scripts/Characters/Monster/MonsterContral.cs b/Assets/MyGame/Scripts/Characters/Monster/MonsterContral.cs index 29579d61b..76b7c8482 100644 --- a/Assets/MyGame/Scripts/Characters/Monster/MonsterContral.cs +++ b/Assets/MyGame/Scripts/Characters/Monster/MonsterContral.cs @@ -22,9 +22,9 @@ using UnityEngine; using static MonsterMoveLine; /// -/// 鱼实体基类:封装状态机(移动/待机)、贝塞尔曲线路径跟随、动画播放、死亡逻辑。 -/// 具体鱼类型(Boss等)通过继承重写虚方法扩展行为。 -/// + /// 鱼实体基类:封装状态机(移动/待机)、贝塞尔曲线路径跟随、动画播放、死亡逻辑。 + /// 具体鱼类型(Boss等)通过继承重写虚方法扩展行为。 + /// public class MonsterContral : MonoBehaviour { // ---------------------------------------------------------------- @@ -103,9 +103,59 @@ public class MonsterContral : MonoBehaviour /// /// 前进方向轴,可选 "X" / "-X" / "Y" / "-Y" / "Z" / "-Z"。 /// 为空时默认使用 up(2D)或 forward(3D)。 + /// ⭐ 运行时不应修改;如需修改请调用 RefreshFacingAxis() 重新解析。 /// public string forwordXYZ; + /// + /// forwordXYZ 预解析后的朝向轴枚举(OnEnable 时解析)。 + /// 避免 ApplyFacingDirection 每帧每鱼做 string.IsNullOrEmpty + switch(string)。 + /// + private FacingAxis _facingAxis = FacingAxis.None; + + /// 朝向轴枚举:对应 forwordXYZ 的 7 种取值 + private enum FacingAxis + { + None, // "" / null + X, + NegX, // "-X" + Y, + NegY, // "-Y" + Z, + NegZ // "-Z" + } + + /// + /// 将 forwordXYZ 字符串解析为枚举。OnEnable 调用一次,运行时直接用枚举 switch。 + /// + private void RefreshFacingAxis() + { + if (string.IsNullOrEmpty(forwordXYZ)) + { + _facingAxis = FacingAxis.None; + return; + } + switch (forwordXYZ) + { + case "X": _facingAxis = FacingAxis.X; break; + case "-X": _facingAxis = FacingAxis.NegX; break; + case "Y": _facingAxis = FacingAxis.Y; break; + case "-Y": _facingAxis = FacingAxis.NegY; break; + case "Z": _facingAxis = FacingAxis.Z; break; + case "-Z": _facingAxis = FacingAxis.NegZ; break; + default: _facingAxis = FacingAxis.None; break; + } + } + + /// + /// 缓存 yzID 前3字符前缀。Init 时调用一次(普通鱼在 base.Init,Boss 在自己 Init)。 + /// 避免 UpdateYZMovement 每帧每鱼 yzID.Substring(0,3) 产生字符串 GC 分配。 + /// + protected void SetYzPrefix(string id) + { + _yzPrefix = (!string.IsNullOrEmpty(id) && id.Length >= 3) ? id.Substring(0, 3) : ""; + } + /// 鱼类型枚举(缓存,避免反复类型转换) public FishTypeEnum fishenum; @@ -118,6 +168,29 @@ public class MonsterContral : MonoBehaviour /// 所属鱼阵 ID(空字符串表示普通鱼) public string yzID = ""; + // ---------------------------------------------------------------- + // ⭐ 缓存优化字段 + // ---------------------------------------------------------------- + + /// 缓存的子物体(朝向控制用),避免每帧 transform.GetChild(0) + private Transform _cachedChild; + + // ---------------------------------------------------------------- + // ⭐ 鱼阵 700/701 直线移动缓存 + // 同一鱼阵起点相同,Init 时一次性算好世界坐标 + 方向系数, + // UpdateYZMovement 每帧直接用,避免每帧每鱼 CanvasToWorld + 条件判断。 + // ---------------------------------------------------------------- + private bool _isYzLineMove; // 是否为 700/701 直线阵 + private Vector3 _yzStartWorld; // 直线阵起点世界坐标 + private float _yzLineDir; // 直线阵方向系数 (-1 向上 / +1 向下) + private bool _yzLineInited; // 直线阵缓存是否已初始化 + + /// + /// yzID 前3字符前缀缓存(Init 时 Substring 一次)。 + /// 避免 UpdateYZMovement 每帧每鱼 yzID.Substring(0,3) 产生字符串 GC 分配。 + /// + private string _yzPrefix = ""; + // ---------------------------------------------------------------- // 私有字段 // ---------------------------------------------------------------- @@ -249,6 +322,25 @@ public class MonsterContral : MonoBehaviour _isPlayingCreateVideo = false; InitAnimList(); + // ⭐ 预解析 forwordXYZ 为枚举,避免 ApplyFacingDirection 每帧每鱼 string switch + RefreshFacingAxis(); + + // ⭐ 同步缓存到 moveLine,避免 GetBezierPointJob 内每帧每鱼重复查询 + // UIManager.Instance.uiRoot.GetComponent() 和 Camera.main + monsterData.moveLine.cachedCamera = mainCamera; + monsterData.moveLine.cachedUirootRect = uirootRect; + + // ⭐ 缓存子物体引用(朝向控制用),避免 ApplyFacingDirection 每帧 transform.GetChild(0) + // 池回收的鱼 transform 可能更换子层级,每次 OnEnable 重新缓存 + if (transform.childCount > 0) + { + _cachedChild = transform.GetChild(0); + } + else + { + _cachedChild = null; + } + // ⭐ 池回收的鱼(isInitialized=false)不立即 ChangeState(move), // 避免 OnMoveUpdata 用旧 moveTime/moveLine 误触发 Id_DieFish_NotFX // Init 调用后会设置 isInitialized=true 并 ChangeState(move) @@ -322,8 +414,11 @@ public class MonsterContral : MonoBehaviour isyz = isyuzhen; this.yzID = yzID; + // ⭐ 缓存 yzID 前3字符前缀,避免 UpdateYZMovement 每帧每鱼 Substring 产生 GC + SetYzPrefix(yzID); + // 鱼阵特殊处理:700 系扇形阵根据起点 y 值翻转朝向 - if (isyuzhen && yzID.Length >= 3 && yzID.Substring(0, 3) == "700") + if (isyuzhen && _yzPrefix == "700") { if (bezierData.start.y < 0) { @@ -333,6 +428,10 @@ public class MonsterContral : MonoBehaviour } } + // ⭐ 预计算 700/701 直线阵的世界起点 + 方向系数 + // 原逻辑每帧每鱼 CanvasToWorld(start) + 判断 goUp,现 Init 一次性算好 + CacheYzLineMoveData(bezierData); + fishenum = (FishTypeEnum)monsterData.fishTypeConfig.fishType; // isMove=false 的鱼不需要移动路线,直接归位到 (0,0,0) @@ -413,6 +512,12 @@ public class MonsterContral : MonoBehaviour /// /// 每帧更新鱼的位置(贝塞尔曲线插值)和朝向。 /// 鱼阵(isyz==true)按阵型 ID 前缀分支,普通鱼走统一逻辑。 + /// ⭐ 性能优化(零 GC + 缓存): + /// - 贝塞尔求值:静态 Vector2[] 缓存 + ThreadStatic 临时缓冲,零 GC 分配 + /// - 朝向:forwordXYZ 预解析为枚举,避免每帧 string switch + /// - 子物体:OnEnable 缓存 _cachedChild,避免每帧 transform.GetChild(0) + /// - 鱼阵 700/701:Init 预计算起点世界坐标 + 方向系数 + /// - yzID 前缀:Init 缓存 _yzPrefix,避免每帧 Substring /// public virtual void OnMoveUpdata() { @@ -429,6 +534,18 @@ public class MonsterContral : MonoBehaviour moveTime += Time.deltaTime; + // ⭐ 终点检测:每帧执行(轻量) + if (moveTime >= monsterData.moveLine.moveTime) + { + // ⭐ 路径走完后立即停止移动,防止 Id_DieFish_NotFX 每帧重复触发 + // (野鱼 guid 已被覆盖时,Id_DieFish_NotFX 会 NOT_FOUND,但 moveTime 一直 >= moveTime, + // 不停止的话每帧都会调用,产生海量 NOT_FOUND 日志) + isMove = false; + CreateFishManager.Instance.Id_DieFish_NotFX(guid); + return; + } + + // ⭐ 每帧执行重计算(已无 GC 压力,分帧会导致鱼体一卡一卡) if (isyz) { UpdateYZMovement(); @@ -437,25 +554,53 @@ public class MonsterContral : MonoBehaviour { UpdateNormalMovement(); } - if (moveTime>=monsterData.moveLine.moveTime) - { - // ⭐ 路径走完后立即停止移动,防止 Id_DieFish_NotFX 每帧重复触发 - // (野鱼 guid 已被覆盖时,Id_DieFish_NotFX 会 NOT_FOUND,但 moveTime 一直 >= moveTime, - // 不停止的话每帧都会调用,产生海量 NOT_FOUND 日志) - isMove = false; - CreateFishManager.Instance.Id_DieFish_NotFX(guid); - } + } + + /// + /// 预计算 700/701 直线阵的世界起点 + 方向系数。 + /// ⭐ 优化:原 UpdateYZMovement 每帧每鱼 CanvasToWorld(start) + 判断 goUp, + /// 现在 Init 一次性算好,运行时直接用缓存值。 + /// 非直线阵(702/704/710 等)标记 _isYzLineMove=false 跳过。 + /// + private void CacheYzLineMoveData(MonsterMoveLine.BezierCurveData bezierData) + { + _isYzLineMove = false; + _yzLineInited = false; + + if (!isyz || string.IsNullOrEmpty(_yzPrefix)) return; + + if (_yzPrefix != "700" && _yzPrefix != "701") return; + + // 世界起点(CanvasToWorld 已在 OnEnable 缓存 uirootRect/mainCamera) + _yzStartWorld = CoordinateTransformation.CanvasToWorld( + bezierData.start, uirootRect, mainCamera); + + // 700:根据起点 y 值决定正方向;701:固定向上(dir=-1) + bool goUp = (_yzPrefix == "701") || (bezierData.start.y > 0); + _yzLineDir = goUp ? -1f : 1f; + + _isYzLineMove = true; + _yzLineInited = true; } /// 鱼阵移动:按 yzID 前缀选择运动方程。 private void UpdateYZMovement() { - if (yzID.Length < 3) return; - string prefix = yzID.Substring(0, 3); - - switch (prefix) + // ⭐ 700/701 直线阵:使用预计算缓存,零 CanvasToWorld 调用 + if (_isYzLineMove && _yzLineInited) { - // 700 / 701:直线向前移动(沿 up 方向) + Vector2 pos = _yzStartWorld + (Vector3)(_yzLineDir * transform.up * moveTime + * monsterData.moveLine.moveTime); + transform.position = pos; + return; + } + + // ⭐ 用 Init 时缓存的 _yzPrefix,避免每帧每鱼 yzID.Substring(0,3) GC 分配 + if (string.IsNullOrEmpty(_yzPrefix)) return; + + switch (_yzPrefix) + { + // 700 / 701:直线向前移动(沿 up 方向)—— 兜底分支,正常走缓存路径 case "700": case "701": { @@ -463,7 +608,7 @@ public class MonsterContral : MonoBehaviour monsterData.moveLine.bezierCurveData.start, uirootRect, mainCamera); // 700:根据起点 y 值决定正方向;701:固定向上 - bool goUp = (prefix == "701") || (monsterData.moveLine.bezierCurveData.start.y > 0); + bool goUp = (_yzPrefix == "701") || (monsterData.moveLine.bezierCurveData.start.y > 0); float dir = goUp ? -1f : 1f; Vector2 pos = startWorld + (Vector3)(dir * transform.up * moveTime @@ -509,7 +654,16 @@ public class MonsterContral : MonoBehaviour /// 下一帧目标位置 private void ApplyFacingDirection(Vector2 nextPos) { - Transform child = transform.GetChild(0); + // ⭐ 使用 OnEnable 缓存的 _cachedChild,避免每帧 transform.GetChild(0) 调用开销 + // 兜底:缓存丢失时重新获取一次(理论上不会发生) + Transform child = _cachedChild; + if (child == null) + { + if (transform.childCount == 0) return; + child = transform.GetChild(0); + _cachedChild = child; + } + Vector3 localPos = Vector3.zero; localPos.z = depthOffset; child.localPosition = localPos; @@ -518,8 +672,11 @@ public class MonsterContral : MonoBehaviour Vector2 toNext = nextPos - (Vector2)transform.position; if (toNext.sqrMagnitude < 0.0001f) return; + Vector2 dirNorm = toNext.normalized; // ⭐ 缓存归一化向量,避免分支内重复计算 + + // ⭐ 用预解析的枚举 switch,避免每帧每鱼 string 比较 // 处理未指定朝向轴的情况(默认行为) - if (string.IsNullOrEmpty(forwordXYZ)) + if (_facingAxis == FacingAxis.None) { if (!is3D) { @@ -528,7 +685,7 @@ public class MonsterContral : MonoBehaviour } else { - transform.rotation = Quaternion.LookRotation(toNext.normalized, Vector3.up); + transform.rotation = Quaternion.LookRotation(dirNorm, Vector3.up); } return; } @@ -536,99 +693,77 @@ public class MonsterContral : MonoBehaviour // 是否为子物体单独控制朝向和深度 if (ischild0) { - - - //// 父物体朝向 - switch (forwordXYZ) + // ⭐ 全部使用缓存 child,不再 transform.GetChild(0) + switch (_facingAxis) { - case "X": + case FacingAxis.X: { - transform.GetChild(0).right = toNext.normalized; - var vv = transform.GetChild(0).eulerAngles; - + child.right = dirNorm; + var vv = child.eulerAngles; vv.x = rotationPY; - transform.GetChild(0).eulerAngles = vv; + child.eulerAngles = vv; } break; - case "-X": + case FacingAxis.NegX: { - transform.GetChild(0).right = -toNext.normalized; - var vv = transform.GetChild(0).eulerAngles; + child.right = -dirNorm; + var vv = child.eulerAngles; vv.x = -rotationPY; - transform.GetChild(0).eulerAngles = vv; + child.eulerAngles = vv; } - break; - case "Y": + case FacingAxis.Y: { - - transform.GetChild(0).up = toNext.normalized; - var vv = transform.GetChild(0).eulerAngles; + child.up = dirNorm; + var vv = child.eulerAngles; vv.y = rotationPY; - transform.GetChild(0).eulerAngles = vv; + child.eulerAngles = vv; } break; - case "-Y": + case FacingAxis.NegY: { - transform.GetChild(0).up = -toNext.normalized; - var vv = transform.GetChild(0).eulerAngles; + child.up = -dirNorm; + var vv = child.eulerAngles; vv.y = -rotationPY; - transform.GetChild(0).eulerAngles = vv; + child.eulerAngles = vv; } - break; - case "Z": + case FacingAxis.Z: { - transform.GetChild(0).forward = toNext.normalized; - var vv = transform.GetChild(0).eulerAngles; + child.forward = dirNorm; + var vv = child.eulerAngles; if (isNot1) { vv.z = rotationPY; - } else { vv.z = rotationPY * (vv.y < 180 ? -1 : 1); - } - transform.GetChild(0).eulerAngles = vv; + child.eulerAngles = vv; } - break; - case "-Z": + case FacingAxis.NegZ: { - transform.GetChild(0).forward = -toNext.normalized; - var vv = transform.GetChild(0).eulerAngles; + child.forward = -dirNorm; + var vv = child.eulerAngles; vv.z = -rotationPY * (vv.y < 180 ? -1 : 1); - transform.GetChild(0).eulerAngles = vv; + child.eulerAngles = vv; } - break; } - - //// 子物体深度偏移:方向正 → 偏移正,方向负 → 偏移负 - //switch (forwordXYZ) - //{ - // case "X": localPos.z = depthOffset; break; - // case "-X": localPos.z = -depthOffset; break; - // case "Y": localPos.x = depthOffset; break; - // case "-Y": localPos.x = -depthOffset; break; - // case "Z": localPos.x = depthOffset; break; - // case "-Z": localPos.x = -depthOffset; break; - //} - } else { // 控制父物体自身朝向 - switch (forwordXYZ) + switch (_facingAxis) { - case "X": transform.right = toNext.normalized; break; - case "-X": transform.right = -toNext.normalized; break; - case "Y": transform.up = toNext.normalized; break; - case "-Y": transform.up = -toNext.normalized; break; - case "Z": transform.forward = toNext.normalized; break; - case "-Z": transform.forward = -toNext.normalized; break; + case FacingAxis.X: transform.right = dirNorm; break; + case FacingAxis.NegX: transform.right = -dirNorm; break; + case FacingAxis.Y: transform.up = dirNorm; break; + case FacingAxis.NegY: transform.up = -dirNorm; break; + case FacingAxis.Z: transform.forward = dirNorm; break; + case FacingAxis.NegZ: transform.forward = -dirNorm; break; } } } diff --git a/Assets/MyGame/Scripts/Characters/Monster/MonsterControl_BossBase.cs b/Assets/MyGame/Scripts/Characters/Monster/MonsterControl_BossBase.cs index e984d1e00..71035c2e3 100644 --- a/Assets/MyGame/Scripts/Characters/Monster/MonsterControl_BossBase.cs +++ b/Assets/MyGame/Scripts/Characters/Monster/MonsterControl_BossBase.cs @@ -115,6 +115,10 @@ public class MonsterControl_BossBase : MonsterContral isyz = isyuzhen; this.yzID = yzID; + // ⭐ 缓存 yzID 前缀(Boss 未调 base.Init,需单独缓存) + // 避免 UpdateYZMovement 里 _yzPrefix 为空导致鱼阵 Boss 不移动 + SetYzPrefix(yzID); + // ⭐ 创建视频处理:配置了 createVideoClip 时通过 GameMainPanel 的播放器播放 // 播放期间 OnMoveUpdata 会跳过移动,视频结束后才开始走路线 _createVideoDuration = 0f; diff --git a/Assets/MyGame/Scripts/Characters/Monster/MonsterMoveLine.cs b/Assets/MyGame/Scripts/Characters/Monster/MonsterMoveLine.cs index 3fab45788..b6648bfe2 100644 --- a/Assets/MyGame/Scripts/Characters/Monster/MonsterMoveLine.cs +++ b/Assets/MyGame/Scripts/Characters/Monster/MonsterMoveLine.cs @@ -6,15 +6,21 @@ using System.Threading.Tasks; using UnityEngine; using UnityEngine.AddressableAssets; using UnityEngine.ResourceManagement.AsyncOperations; -using Unity.Collections; -using Unity.Jobs; -using Unity.Burst; using System; [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(); [Serializable] public class BezierCurveData { @@ -62,9 +68,29 @@ public class MonsterMoveLine curves.Add(curve); dic_BezierCurveDatas.Add(curve.name, curve); + // ⭐ 同步构建点数组缓存(单条加载路径) + BuildCurvePointsArray(curve); } } + /// + /// ⭐ 将曲线的 [start, controlPoints..., end] 组装成 Vector2[] 并缓存。 + /// 所有引用此曲线的鱼共享同一份数组,避免每帧每鱼 new List + ToArray 的 GC。 + /// + private static void BuildCurvePointsArray(BezierCurveData curve) + { + if (_curvePointsArrays.ContainsKey(curve.name)) return; + + 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; + } + public static async Task LoadBezierTxtAsync(string path) { List keys = new List(); @@ -119,7 +145,11 @@ public class MonsterMoveLine // 不覆盖旧数据 if (!dic_BezierCurveDatas.ContainsKey(curve.name)) + { dic_BezierCurveDatas.Add(curve.name, curve); + // ⭐ 同步构建点数组缓存(批量加载路径) + BuildCurvePointsArray(curve); + } } } @@ -167,6 +197,9 @@ public class MonsterMoveLine dic_BezierCurveDatas.Remove(curveName); } + // ⭐ 同步清理点数组缓存 + _curvePointsArrays.Remove(curveName); + // 列表移除所有同名曲线 curves.RemoveAll(c => c.name == curveName); } @@ -187,6 +220,12 @@ public class MonsterMoveLine 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; @@ -195,7 +234,14 @@ public class MonsterMoveLine } /// - /// 使用 Job 计算 B样条点 + /// 主线程零分配计算贝塞尔曲线点(De Casteljau 算法)。 + /// ⭐ 优化说明(原 Job 方案的负优化问题): + /// 1. 原 GetBezierPointJob 每次调用 new List + 2 个 NativeArray(TempJob) + ToArray, + /// 每条鱼每帧产生 3 次堆分配,N 条鱼 = 3N 次 GC 分配/帧 + /// 2. 原 Job.Schedule() 后立即 Complete(),Job 实际单线程执行, + /// 调度开销 > 计算收益(4-7 个控制点的计算量极小) + /// 3. 现方案:曲线点数组静态缓存(共享),主线程直接 Lerp 计算,零 GC 分配 + /// 4. Camera/RectTransform 由调用方缓存,避免每帧每鱼重复获取 /// public Vector2 GetBezierPointJob(float t) { @@ -203,65 +249,46 @@ public class MonsterMoveLine if (moveTime <= 0f) return bezierCurveData.start; - if (t>moveTime) - { - t=moveTime; - } + if (t > moveTime) + t = moveTime; t = t / moveTime; - List pts = new List(); - pts.Add(bezierCurveData.start); - pts.AddRange(bezierCurveData.controlPoints); - pts.Add(bezierCurveData.end); - var controlPoints = new NativeArray(pts.ToArray(), Allocator.TempJob); - var result = new NativeArray(1, Allocator.TempJob); - - var job = new BSplineJob + // ⭐ 取静态缓存的曲线点数组(所有鱼共享,零分配) + if (!_curvePointsArrays.TryGetValue(bezierCurveData.name, out var pts)) { - controlPoints = controlPoints, - t = t, - result = result - }; - - JobHandle handle = job.Schedule(); - handle.Complete(); - - Vector2 worldPos = CoordinateTransformation.CanvasToWorld(result[0], - UIManager.Instance.uiRoot.GetComponent(), Camera.main); - - controlPoints.Dispose(); - result.Dispose(); - - return worldPos; - } - - // Job 结构体 - [BurstCompile] - public struct BSplineJob : IJob - { - [ReadOnly] public NativeArray controlPoints; - public float t; - public NativeArray result; - - public void Execute() - { - var temp = new NativeArray(controlPoints.Length, Allocator.Temp); - controlPoints.CopyTo(temp); - - int count = temp.Length; - while (count > 1) - { - for (int i = 0; i < count - 1; i++) - { - temp[i] = Vector2.Lerp(temp[i], temp[i + 1], t); - } - count--; - } - - result[0] = temp[0]; - temp.Dispose(); + // 兜底:缓存未命中时实时构建(理论上不应发生,加载时已构建) + BuildCurvePointsArray(bezierCurveData); + pts = _curvePointsArrays[bezierCurveData.name]; } + + // ⭐ 复用线程静态缓冲区(主线程单线程执行,安全复用) + if (_tempPoints == null || _tempPoints.Length < pts.Length) + _tempPoints = new Vector2[pts.Length]; + Array.Copy(pts, _tempPoints, pts.Length); + + // De Casteljau 算法:逐层 Lerp 直到剩一个点 + int count = pts.Length; + while (count > 1) + { + for (int i = 0; i < count - 1; i++) + _tempPoints[i] = Vector2.Lerp(_tempPoints[i], _tempPoints[i + 1], t); + count--; + } + + Vector2 canvasPos = _tempPoints[0]; + + // ⭐ 用缓存的 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 diff --git a/Assets/MyGame/Scripts/Net/APIRequest/Params/SupplementParams.cs b/Assets/MyGame/Scripts/Net/APIRequest/Params/SupplementParams.cs index e65aded28..457fd827d 100644 --- a/Assets/MyGame/Scripts/Net/APIRequest/Params/SupplementParams.cs +++ b/Assets/MyGame/Scripts/Net/APIRequest/Params/SupplementParams.cs @@ -13,7 +13,7 @@ namespace Assets.MyGame.Scripts.Net.APIRequest.Params public class LoginByPasswordParam : RequestParamBase { /// 账号(手机号/用户名) - public string account; + public string mobile; /// 密码 public string password; } diff --git a/Assets/MyGame/aa/UI/Game/GameMainPanel_FishBase.cs b/Assets/MyGame/aa/UI/Game/GameMainPanel_FishBase.cs index d6d75a76d..381c833ac 100644 --- a/Assets/MyGame/aa/UI/Game/GameMainPanel_FishBase.cs +++ b/Assets/MyGame/aa/UI/Game/GameMainPanel_FishBase.cs @@ -805,6 +805,9 @@ public class GameMainPanel_FishBase : GameMainPanel_Base // 先创建等待信号(在发送请求前创建,避免响应太快丢失) _leaveRoomResponseTcs = new TaskCompletionSource(); + // ⭐ 立即记录当前面板类型,不依赖 socket 返回(超时强制退出时也能正确关闭) + _leavingGamePanelType = this.PanelType; + // 发送离房请求 WebSocketSendList.LeaveRoom( GameManager.Instance.gameData.currRoomData.roomId, diff --git a/Assets/MyGame/aa/UI/Login/SubPanel/LoginEnterPanel.cs b/Assets/MyGame/aa/UI/Login/SubPanel/LoginEnterPanel.cs index 5dd35f6c0..abc3a22c3 100644 --- a/Assets/MyGame/aa/UI/Login/SubPanel/LoginEnterPanel.cs +++ b/Assets/MyGame/aa/UI/Login/SubPanel/LoginEnterPanel.cs @@ -1,7 +1,5 @@ -using Assets.MyGame.Scripts.Net; +using Assets.MyGame.Scripts.Net; using Assets.MyGame.Scripts.Net.APIRequest.Params; -using System.Collections; -using System.Collections.Generic; using TMPro; using UnityEngine; using UnityEngine.UI; @@ -19,9 +17,6 @@ public class LoginEnterPanel : SubUIBase private const string KEY_USER = "login_user"; private const string KEY_PASS = "login_pass"; - private string _realPassword = ""; - private Coroutine _maskCoroutine; - public override void Awake() { base.Awake(); @@ -30,17 +25,20 @@ public class LoginEnterPanel : SubUIBase tMPInput_UserName.characterLimit = 11; tMPInput_UserName.contentType = TMP_InputField.ContentType.IntegerNumber; - // 密码:Standard 模式 + 延迟遮罩 - tMPInput_Password.contentType = TMP_InputField.ContentType.Standard; - tMPInput_Password.onValueChanged.AddListener(OnPwdChanged); + // 密码:使用 TMP 内置 Password 模式,自动遮罩且无光标错位问题 + tMPInput_Password.contentType = TMP_InputField.ContentType.Password; + // 清理 prefab 中 Text 子组件可能残留的零宽空格(\u200B),避免被 InputField 拾取为首字符 + tMPInput_Password.text = ""; // 加载本地保存的账号密码 if (PlayerPrefs.HasKey(KEY_USER)) tMPInput_UserName.text = PlayerPrefs.GetString(KEY_USER); if (PlayerPrefs.HasKey(KEY_PASS)) { - _realPassword = PlayerPrefs.GetString(KEY_PASS); - tMPInput_Password.SetTextWithoutNotify(new string('*', _realPassword.Length)); + string saved = PlayerPrefs.GetString(KEY_PASS); + // 过滤掉可能被旧版遮罩逻辑污染的非法字符 + saved = saved.Replace("\u200B", "").Replace("*", ""); + tMPInput_Password.text = saved; } btn_Register.onClick.AddListener(() => @@ -55,14 +53,13 @@ public class LoginEnterPanel : SubUIBase { // 保存账号密码到本地 PlayerPrefs.SetString(KEY_USER, tMPInput_UserName.text); - PlayerPrefs.SetString(KEY_PASS, _realPassword); + PlayerPrefs.SetString(KEY_PASS, tMPInput_Password.text); PlayerPrefs.Save(); - //APIRequestList.Login(); - APIRequestList.LoginPhone(new LoginParam + APIRequestList.LoginByPassword(new LoginByPasswordParam { - mobile=tMPInput_UserName.text, - code=_realPassword, + mobile = tMPInput_UserName.text, + password=tMPInput_Password.text, }, token => { WebSocketManager.Instance.Connect(SocketType.login); @@ -85,60 +82,4 @@ public class LoginEnterPanel : SubUIBase // }); }); } - // Start is called before the first frame update - void Start() - { - - } - - // Update is called once per frame - void Update() - { - - } - - private void OnPwdChanged(string display) - { - bool isAdd = _realPassword.Length < display.Length; - - if (isAdd) - { - for (int i = _realPassword.Length; i < display.Length; i++) - _realPassword += display[i]; - } - else if (_realPassword.Length > display.Length) - { - _realPassword = _realPassword.Substring(0, display.Length); - } - - int len = _realPassword.Length; - if (isAdd && len > 0) - { - // 新增:只显示最后一个字符明文,其余为 * - string masked = new string('*', len - 1) + _realPassword[len - 1]; - tMPInput_Password.SetTextWithoutNotify(masked); - tMPInput_Password.caretPosition = len; - MaskAfterDelay(); - } - else - { - // 删除或无变化:全星 - tMPInput_Password.SetTextWithoutNotify(new string('*', len)); - tMPInput_Password.caretPosition = len; - } - } - - private void MaskAfterDelay() - { - if (_maskCoroutine != null) StopCoroutine(_maskCoroutine); - _maskCoroutine = StartCoroutine(MaskRoutine()); - } - - private IEnumerator MaskRoutine() - { - yield return new WaitForSeconds(0.5f); - if (string.IsNullOrEmpty(_realPassword)) yield break; - tMPInput_Password.SetTextWithoutNotify(new string('*', _realPassword.Length)); - tMPInput_Password.caretPosition = _realPassword.Length; - } } diff --git a/Assets/MyGame/aa/UI/Login/SubPanel/LoginEnterPanel.prefab b/Assets/MyGame/aa/UI/Login/SubPanel/LoginEnterPanel.prefab index 9b27b1e46..79bba9322 100644 --- a/Assets/MyGame/aa/UI/Login/SubPanel/LoginEnterPanel.prefab +++ b/Assets/MyGame/aa/UI/Login/SubPanel/LoginEnterPanel.prefab @@ -766,7 +766,7 @@ MonoBehaviour: m_OnCullStateChanged: m_PersistentCalls: m_Calls: [] - m_text: "\u200B" + m_text: "" m_isRightToLeft: 0 m_fontAsset: {fileID: 11400000, guid: 5d4a713fdd931db4c9ce49f068b54e0a, type: 2} m_sharedMaterial: {fileID: 3593458263369068374, guid: 5d4a713fdd931db4c9ce49f068b54e0a, @@ -1327,7 +1327,7 @@ MonoBehaviour: m_OnCullStateChanged: m_PersistentCalls: m_Calls: [] - m_text: "\u200B" + m_text: "" m_isRightToLeft: 0 m_fontAsset: {fileID: 11400000, guid: 5d4a713fdd931db4c9ce49f068b54e0a, type: 2} m_sharedMaterial: {fileID: 3593458263369068374, guid: 5d4a713fdd931db4c9ce49f068b54e0a,