From f8523004141d062baeda8ba895b54da0d5674cb0 Mon Sep 17 00:00:00 2001 From: HungDK <> Date: Tue, 19 May 2026 18:01:53 +0700 Subject: [PATCH 1/3] Fix route map smoothness --- .../Scripts/Managers/EC_HPWorkMove.cs | 6 +- .../Move/AutoPFImp/AutoMove/CMoveAgent.cs | 154 +++++++++++++++++- .../Scripts/Move/CECIntelligentRoute.cs | 41 ++++- Assets/PerfectWorld/Scripts/World/CECWorld.cs | 18 +- 4 files changed, 198 insertions(+), 21 deletions(-) diff --git a/Assets/PerfectWorld/Scripts/Managers/EC_HPWorkMove.cs b/Assets/PerfectWorld/Scripts/Managers/EC_HPWorkMove.cs index 7672d2d72a..000fb86a8b 100644 --- a/Assets/PerfectWorld/Scripts/Managers/EC_HPWorkMove.cs +++ b/Assets/PerfectWorld/Scripts/Managers/EC_HPWorkMove.cs @@ -875,11 +875,7 @@ namespace BrewMonster.Scripts vCurPos = m_pHost.m_MoveCtrl.GroundMove(m_vCurDir, fSpeed, fDeltaTime, m_pHost.m_fVertSpeed); if (!m_vCurDir.IsZero()) - { - //m_pHost.StartModelMove(m_vCurDir, GPDataTypeHelper.g_vAxisY, 100); - //m_pHost.ChangeModelTargetDirAndUp(m_vCurDir, GPDataTypeHelper.g_vAxisY); - UpdateFacingFromDelta(vCurPos); - } + OrientHostHorizontal(m_vCurDir); if (m_pHost.m_MoveCtrl.MoveBlocked() >= 3) { diff --git a/Assets/PerfectWorld/Scripts/Move/AutoPFImp/AutoMove/CMoveAgent.cs b/Assets/PerfectWorld/Scripts/Move/AutoPFImp/AutoMove/CMoveAgent.cs index 5199de3cb9..e4e37f05c1 100644 --- a/Assets/PerfectWorld/Scripts/Move/AutoPFImp/AutoMove/CMoveAgent.cs +++ b/Assets/PerfectWorld/Scripts/Move/AutoPFImp/AutoMove/CMoveAgent.cs @@ -35,6 +35,9 @@ namespace AutoMove private int m_iLayerGoal; private readonly List m_path3D = new List(1024); + private readonly List m_pathMap = new List(1024); + private int m_optimizeCurIndex; + private const int DefaultOptimizeCatchCount = 10; public bool Load(string basePathNoExt, Func resolver, Vector3? originOverride) { @@ -119,10 +122,16 @@ namespace AutoMove public int GetPathCount() => m_path3D.Count; /// - /// Matches C++ CMoveAgent::GetOptimizeCatchCount — lookahead window for path following. - /// 对应 C++:无 PathOptimizer 时为 0,FindNearest/Farthest 仅在单步索引上工作。 + /// Matches C++ CMoveAgent::GetOptimizeCatchCount (COptimizePath::m_CatchCount, default 10). /// - public int GetOptimizeCatchCount() => 0; + public int GetOptimizeCatchCount() => DefaultOptimizeCatchCount; + + public Vector3 Get2DPathNode(int index) + { + if (index < 0 || index >= m_path3D.Count) return Vector3.zero; + Vector3 v = m_path3D[index]; + return new Vector3(v.x, 0f, v.z); + } public Vector3 Get3DPathNode(int index) { @@ -130,6 +139,40 @@ namespace AutoMove return m_path3D[index]; } + /// Ported from C++ CMoveAgent::Optimize — incremental path smoothing while walking. + public bool Optimize(int moveIndex) + { + if (moveIndex < 0 || moveIndex >= m_pathMap.Count - 1) + return false; + + var layer0 = m_pMoveMap.GetLayer(0); + var rmap = layer0?.GetRMap(); + if (rmap == null) + return false; + + int from = Mathf.Max(moveIndex, m_optimizeCurIndex); + if (from >= m_pathMap.Count - 1) + return false; + + int best = from + 1; + for (int j = m_pathMap.Count - 1; j > from; j--) + { + if (IsLinePassable(rmap, m_pathMap[from], m_pathMap[j])) + { + best = j; + break; + } + } + + if (best <= from + 1) + return false; + + m_pathMap.RemoveRange(from + 1, best - from - 1); + m_path3D.RemoveRange(from + 1, best - from - 1); + m_optimizeCurIndex = from + 1; + return true; + } + public System.Collections.Generic.List GetFullPath() { return new System.Collections.Generic.List(m_path3D); @@ -293,14 +336,109 @@ namespace AutoMove } rev.Reverse(); - // Convert to world positions (y will be resolved by host terrain in movement). - // 转为世界坐标(y 由移动逻辑/地形解析)。 - for (int i = 0; i < rev.Count; i++) + var layer0 = m_pMoveMap.GetLayer(0); + var rmap = layer0?.GetRMap(); + List smoothed = rmap != null ? SmoothPathMap(rev, rmap) : rev; + + m_pathMap.Clear(); + m_path3D.Clear(); + m_optimizeCurIndex = 0; + for (int i = 0; i < smoothed.Count; i++) { - m_path3D.Add(Map2Wld(rev[i])); + m_pathMap.Add(smoothed[i]); + m_path3D.Add(Map2Wld(smoothed[i])); } } + /// String-pull on passable grid (same goal as C++ COptimizePath::SetupOptimize). + static List SmoothPathMap(List raw, CBitImage rmap) + { + if (raw == null || raw.Count <= 2) + return raw; + + var result = new List(raw.Count) { raw[0] }; + int i = 0; + while (i < raw.Count - 1) + { + int furthest = i + 1; + for (int j = raw.Count - 1; j > i; j--) + { + if (IsLinePassable(rmap, raw[i], raw[j])) + { + furthest = j; + break; + } + } + + if (furthest != i) + { + if (result[result.Count - 1] != raw[furthest]) + result.Add(raw[furthest]); + i = furthest; + } + else + { + i++; + if (result[result.Count - 1] != raw[i]) + result.Add(raw[i]); + } + } + + return result; + } + + /// Matches C++ COptimizePath::_LineTo strict diagonal checks on rmap. + static bool IsLinePassable(CBitImage rmap, Vector2Int from, Vector2Int to) + { + if (from == to) + return true; + + rmap.GetImageSize(out int w, out int h); + int x0 = from.x, y0 = from.y; + int x1 = to.x, y1 = to.y; + int dx = Mathf.Abs(x1 - x0); + int dy = Mathf.Abs(y1 - y0); + int sx = x0 < x1 ? 1 : -1; + int sy = y0 < y1 ? 1 : -1; + int err = dx - dy; + int lastX = x0, lastY = y0; + + while (true) + { + if (x0 < 0 || x0 >= w || y0 < 0 || y0 >= h || !rmap.GetPixel(x0, y0)) + return false; + + if (x0 == x1 && y0 == y1) + break; + + int e2 = err << 1; + int nextX = x0, nextY = y0; + if (e2 > -dy) + { + err -= dy; + nextX += sx; + } + if (e2 < dx) + { + err += dx; + nextY += sy; + } + + if (nextX != lastX && nextY != lastY) + { + if (!rmap.GetPixel(lastX, nextY) || !rmap.GetPixel(nextX, lastY)) + return false; + } + + x0 = nextX; + y0 = nextY; + lastX = x0; + lastY = y0; + } + + return true; + } + private static bool InBounds(Vector2Int p, int w, int h) => p.x >= 0 && p.x < w && p.y >= 0 && p.y < h; private static int Heuristic(Vector2Int a, Vector2Int b) @@ -358,6 +496,8 @@ namespace AutoMove //m_iStat = PF_STATE_UNKNOWN; m_path3D.Clear(); + m_pathMap.Clear(); + m_optimizeCurIndex = 0; } /// diff --git a/Assets/PerfectWorld/Scripts/Move/CECIntelligentRoute.cs b/Assets/PerfectWorld/Scripts/Move/CECIntelligentRoute.cs index e9d1e10c32..a9a0619740 100644 --- a/Assets/PerfectWorld/Scripts/Move/CECIntelligentRoute.cs +++ b/Assets/PerfectWorld/Scripts/Move/CECIntelligentRoute.cs @@ -376,9 +376,22 @@ namespace BrewMonster.Scripts // Determine layers (minimal: layer0 or invalid). // 确定层(最小实现:层0或无效)。 + float startDh = 0f; + float endDh = 0f; + var world = EC_Game.GetGameRun()?.GetWorld(); + if (world != null) + { + A3DVECTOR3 n = GPDataTypeHelper.g_vAxisY; + if (world.TryGetTerrainHeight(start, ref n, out float startTer)) + startDh = start.y - startTer; + n = GPDataTypeHelper.g_vAxisY; + if (world.TryGetTerrainHeight(end, ref n, out float endTer)) + endDh = end.y - endTer; + } + float dist; - int startLayer = agent.WhichLayer(new Vector3(start.x, start.y, start.z), 0.0f, out dist); - int endLayer = agent.WhichLayer(new Vector3(end.x, end.y, end.z), 0.0f, out dist); + int startLayer = agent.WhichLayer(new Vector3(start.x, start.y, start.z), startDh, out dist); + int endLayer = agent.WhichLayer(new Vector3(end.x, end.y, end.z), endDh, out dist); if (startLayer < 0) startLayer = 0; if (endLayer < 0) endLayer = 0; @@ -465,8 +478,11 @@ namespace BrewMonster.Scripts } m_iCurDest = FindNextNode(pos, m_iCurDest + 1); + agent.Optimize(m_iCurDest); + int newPathCount = agent.GetPathCount(); + if (m_iCurDest >= newPathCount) + m_iCurDest = Mathf.Max(0, newPathCount - 1); m_dist2CurDest = (GetCurDest() - pos).MagnitudeH(); - // C++ calls agent->Optimize(m_iCurDest) here; Unity CMoveAgent has no path optimizer yet. } A3DVECTOR3 GetNodePos(int iNode) @@ -489,17 +505,26 @@ namespace BrewMonster.Scripts if (agent == null) return new A3DVECTOR3(0f, 0f, 0f); Vector3 v = agent.Get3DPathNode(iNode); - // C++ adds GetTerrainHeight to Y; Unity path nodes use movemap/world height from loader. - return new A3DVECTOR3(v.x, v.y, v.z); + var pos = new A3DVECTOR3(v.x, v.y, v.z); + var world = EC_Game.GetGameRun()?.GetWorld(); + if (world != null) + { + A3DVECTOR3 n = GPDataTypeHelper.g_vAxisY; + if (world.TryGetTerrainHeight(pos, ref n, out float terY)) + pos.y = terY; + } + return pos; } A3DVECTOR3 GetNodePosXZ(int iNode) { if (IsIdle()) return new A3DVECTOR3(0f, 0f, 0f); - A3DVECTOR3 p = GetNodePosNoCheck(iNode); - p.y = 0f; - return p; + RangedMoveAgent? pCurAgent = GetCurAgent(); + if (pCurAgent == null || pCurAgent.Value.agent == null) + return new A3DVECTOR3(0f, 0f, 0f); + Vector3 v = pCurAgent.Value.agent.Get2DPathNode(iNode); + return new A3DVECTOR3(v.x, v.y, v.z); } int FindNearestNode(A3DVECTOR3 curPos, int iNodeFrom) diff --git a/Assets/PerfectWorld/Scripts/World/CECWorld.cs b/Assets/PerfectWorld/Scripts/World/CECWorld.cs index a929005c48..a7293d0eb6 100644 --- a/Assets/PerfectWorld/Scripts/World/CECWorld.cs +++ b/Assets/PerfectWorld/Scripts/World/CECWorld.cs @@ -32,9 +32,25 @@ namespace BrewMonster.Scripts.World return m_pOnmtMan; } public float GetTerrainHeight(A3DVECTOR3 vPos, ref A3DVECTOR3 pvNormal /* NULL */) + { + A3DVECTOR3 normal = pvNormal; + if (!TryGetTerrainHeight(vPos, ref normal, out float height)) + return vPos.y; + pvNormal = normal; + return height; + } + + /// False when terrain is not loaded yet (e.g. during early AutoPF Search). + public bool TryGetTerrainHeight(A3DVECTOR3 vPos, ref A3DVECTOR3 pvNormal, out float height) { A3DTerrain2 pTerrain = GetTerrain(); - return pTerrain.GetPosHeight(vPos, ref pvNormal); + if (pTerrain == null) + { + height = vPos.y; + return false; + } + height = pTerrain.GetPosHeight(vPos, ref pvNormal); + return true; } public float GetWaterHeight(A3DVECTOR3 vPos) From 13bf8214a7b0602ec94ae89d6ac1bdfd4436bd37 Mon Sep 17 00:00:00 2001 From: Tran Hai Nam Date: Tue, 19 May 2026 18:03:25 +0700 Subject: [PATCH 2/3] fix bug tien nu trieu hoan --- Assets/PerfectWorld/Scene/AnimationTest.unity | 4 ++-- Assets/PerfectWorld/Scripts/NPC/CECModel.cs | 9 ++++++++- .../Scripts/Players/CECHostNavigatePlayer.cs | 11 +++++++---- 3 files changed, 17 insertions(+), 7 deletions(-) diff --git a/Assets/PerfectWorld/Scene/AnimationTest.unity b/Assets/PerfectWorld/Scene/AnimationTest.unity index 2d16aa6a66..ce2ed17328 100644 --- a/Assets/PerfectWorld/Scene/AnimationTest.unity +++ b/Assets/PerfectWorld/Scene/AnimationTest.unity @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2daf9dffc5bed73510959aebf1ee1b4b2fe326380d6417e1a262c919b651095e -size 70182 +oid sha256:11f16ce7ddd16e5c3de941b56358deb495efb2d5d4783c8d59c75aab0dad1975 +size 92966 diff --git a/Assets/PerfectWorld/Scripts/NPC/CECModel.cs b/Assets/PerfectWorld/Scripts/NPC/CECModel.cs index 1d715caad5..e797a8c2ca 100644 --- a/Assets/PerfectWorld/Scripts/NPC/CECModel.cs +++ b/Assets/PerfectWorld/Scripts/NPC/CECModel.cs @@ -1366,7 +1366,14 @@ public class CECModel { return false; } - + if(m_skeletonBuilder == null ) + { + return false; + } + if(pChild.m_skeletonBuilder == null) + { + return false; + } Transform hook = m_skeletonBuilder.GetHook(szHookName, true); Transform hangger = pChild.m_skeletonBuilder.GetHook(szCCName, true); if (hook == null || hangger == null) diff --git a/Assets/PerfectWorld/Scripts/Players/CECHostNavigatePlayer.cs b/Assets/PerfectWorld/Scripts/Players/CECHostNavigatePlayer.cs index 09b7c9f058..c6748bff39 100644 --- a/Assets/PerfectWorld/Scripts/Players/CECHostNavigatePlayer.cs +++ b/Assets/PerfectWorld/Scripts/Players/CECHostNavigatePlayer.cs @@ -193,7 +193,7 @@ namespace BrewMonster.Scripts m_pNavigateModel = new CECModel(); m_pNavigateModel.m_pPlayerModel = instance; SkeletonBuilder skeletonBuilder = instance.GetComponentInChildren(true); - NamedAnimancerComponent animancer = instance.GetComponent(); + NamedAnimancerComponent animancer = instance.GetComponentInChildren(); if (skeletonBuilder != null && animancer != null) { @@ -214,7 +214,6 @@ namespace BrewMonster.Scripts CameraController.Instance.UpdateFollowObject(m_pNavigateModel.transform); TickInvoker.Instance.RegisterTickable(this); // Clone host player model/equipment // 克隆宿主玩家模型与装备 - BMLogger.Log($"CECHostNavigatePlayer::InitAsync, debugStep: {debugStep++}"); bool loaded = await LoadFrom(m_pHostPlayer, false); if (loaded) SetupNavigatePlayerVisual(); @@ -278,6 +277,7 @@ namespace BrewMonster.Scripts base.Tick(dwDeltatime); if (!m_bNavigateModelApplied /*&& IsAllResReady()*/) { + //TOdo: implement IsAllresReady ApplyNavigateModel(); // if(!IsShapeChanged()) // ApplyNavigateModel(); @@ -365,8 +365,11 @@ namespace BrewMonster.Scripts private bool ApplyNavigateModel() { - // if ( !GetMajorModel() || m_pNavigateModel == null || m_bNavigateModelApplied) - // return false; + if ( !GetMajorModel() || m_pNavigateModel == null || m_bNavigateModelApplied) + { + BMLogger.Log($"GetMajorModel(){GetMajorModel() == true } m_NavigateMode {m_pNavigateModel == null} m_bNavigateModelApplied {m_bNavigateModelApplied} "); + return false; + } A3DVECTOR3 vCurPos = GetPos(); From e1ca2dd8e3d4c97787da5605e865af107128430a Mon Sep 17 00:00:00 2001 From: HungDK <> Date: Wed, 20 May 2026 10:09:16 +0700 Subject: [PATCH 3/3] Add optimize path --- .../Move/AutoPFImp/AutoMove/CMoveAgent.cs | 294 ++++------------- .../Move/AutoPFImp/AutoMove/CMoveMap.cs | 9 +- .../Move/AutoPFImp/AutoMove/COptimizePath.cs | 298 ++++++++++++++++++ .../AutoPFImp/AutoMove/COptimizePath.cs.meta | 2 + .../Move/AutoPFImp/AutoMove/PathNode.cs | 22 ++ .../Move/AutoPFImp/AutoMove/PathNode.cs.meta | 2 + .../Scripts/Move/CECIntelligentRoute.cs | 6 +- 7 files changed, 402 insertions(+), 231 deletions(-) create mode 100644 Assets/PerfectWorld/Scripts/Move/AutoPFImp/AutoMove/COptimizePath.cs create mode 100644 Assets/PerfectWorld/Scripts/Move/AutoPFImp/AutoMove/COptimizePath.cs.meta create mode 100644 Assets/PerfectWorld/Scripts/Move/AutoPFImp/AutoMove/PathNode.cs create mode 100644 Assets/PerfectWorld/Scripts/Move/AutoPFImp/AutoMove/PathNode.cs.meta diff --git a/Assets/PerfectWorld/Scripts/Move/AutoPFImp/AutoMove/CMoveAgent.cs b/Assets/PerfectWorld/Scripts/Move/AutoPFImp/AutoMove/CMoveAgent.cs index e4e37f05c1..20fa085849 100644 --- a/Assets/PerfectWorld/Scripts/Move/AutoPFImp/AutoMove/CMoveAgent.cs +++ b/Assets/PerfectWorld/Scripts/Move/AutoPFImp/AutoMove/CMoveAgent.cs @@ -3,79 +3,59 @@ using System.Collections.Generic; using UnityEngine; // Filename : CMoveAgent.cs -// Creator : ported/simplified from C++ (AutoPFImp/AutoMoveImp/MoveAgent.*) +// Creator : ported from C++ (AutoPFImp/AutoMoveImp/MoveAgent.*) // Date : 2026/01/09 namespace AutoMove { /// - /// Minimal MoveAgent: loads movemap and provides A* path on layer0 rmap. - /// 最小 MoveAgent:加载 movemap,并在第0层 rmap 上执行 A* 路径搜索。 + /// MoveAgent: A* on layer0 rmap + COptimizePath (matches C++ CMoveAgent flow). /// public class CMoveAgent { - // Debug switch (enable temporarily while validating routes). - // 调试开关(验证寻路时可临时开启)。 - // NOTE: must not be const, otherwise Unity compiler warns about unreachable code. - // 注意:不要用 const,否则 Unity 编译会报“不可达代码”警告。 private static bool DEBUG_AUTOPF = false; + public abstract class BrushTest { - // from.y/to.y store DH in original engine. - // 原版中 from.y/to.y 存储 DH(相对地形高度差)。 public abstract bool Collide(Vector3 from, Vector3 to); } private readonly CMoveMap m_pMoveMap = new CMoveMap(); - private Vector3 m_vOriginOverride; + private COptimizePath m_pPathOptimizer; private Vector2Int m_ptStart; private Vector2Int m_ptGoal; private int m_iLayerStart; private int m_iLayerGoal; - private readonly List m_path3D = new List(1024); - private readonly List m_pathMap = new List(1024); - private int m_optimizeCurIndex; - private const int DefaultOptimizeCatchCount = 10; - public bool Load(string basePathNoExt, Func resolver, Vector3? originOverride) { - // basePathNoExt corresponds to "maps\\\\movemap\\r.._..-c.._..-l0" in C++. - // basePathNoExt 对应 C++ 的 "maps\\\\movemap\\r.._..-c.._..-l0"。 string cfgName = basePathNoExt; if (!cfgName.EndsWith(".cfg", StringComparison.OrdinalIgnoreCase)) - { cfgName += ".cfg"; - } byte[] cfgBytes = resolver(cfgName); if (cfgBytes == null) - { return false; - } - // Resolver for referenced files: in cfg, references are baseName + ".prmap" etc. - // cfg 内引用:baseName + ".prmap" 等。 bool ok = m_pMoveMap.Load(cfgBytes, resolver, basePathNoExt); if (!ok) - { return false; - } if (originOverride.HasValue) - { - m_vOriginOverride = originOverride.Value; - m_pMoveMap.SetOrigin(m_vOriginOverride); - } + m_pMoveMap.SetOrigin(originOverride.Value); + CreateOptimizer(); return true; } + void CreateOptimizer() + { + m_pPathOptimizer = new COptimizePath(); + } + public bool IsReady() { - // Original C++ requires MultiCluGraph; here we only require layer0 RMap. - // 原版 C++ 依赖 MultiCluGraph;这里最小实现只要求第0层 RMap。 var layer0 = m_pMoveMap.GetLayer(0); return layer0 != null && layer0.GetRMap() != null; } @@ -101,8 +81,6 @@ namespace AutoMove public int WhichLayer(Vector3 vWld, float dH, out float layerDist) { - // Minimal: single layer 0 if passable. - // 最小实现:如果可通行则使用单层0。 layerDist = 0.0f; var pt = TransWld2Map(vWld); var layer0 = m_pMoveMap.GetLayer(0); @@ -119,71 +97,72 @@ namespace AutoMove return true; } - public int GetPathCount() => m_path3D.Count; + public List Get2DPath() + { + return m_pPathOptimizer?.GetPath(); + } - /// - /// Matches C++ CMoveAgent::GetOptimizeCatchCount (COptimizePath::m_CatchCount, default 10). - /// - public int GetOptimizeCatchCount() => DefaultOptimizeCatchCount; + public int GetPathCount() + { + if (m_pPathOptimizer != null) + { + var path = m_pPathOptimizer.GetPath(); + if (path != null) + return path.Count; + } + return 0; + } + + public int GetOptimizeCatchCount() + { + return m_pPathOptimizer != null ? m_pPathOptimizer.GetCatchCount() : 0; + } public Vector3 Get2DPathNode(int index) { - if (index < 0 || index >= m_path3D.Count) return Vector3.zero; - Vector3 v = m_path3D[index]; - return new Vector3(v.x, 0f, v.z); + var path2d = Get2DPath(); + if (path2d == null || index < 0 || index >= path2d.Count) + return Vector3.zero; + PathNode pathNode = path2d[index]; + Vector2 ptWld = m_pMoveMap.TransMap2Wld(pathNode.ptMap.x, pathNode.ptMap.y); + return new Vector3(ptWld.x, 0f, ptWld.y); } public Vector3 Get3DPathNode(int index) { - if (index < 0 || index >= m_path3D.Count) return Vector3.zero; - return m_path3D[index]; + var path2d = Get2DPath(); + if (path2d == null || index < 0 || index >= path2d.Count) + return Vector3.zero; + PathNode pathNode = path2d[index]; + Vector2 ptWld = m_pMoveMap.TransMap2Wld(pathNode.ptMap.x, pathNode.ptMap.y); + float dh = m_pMoveMap.GetDH(pathNode.layer, (int)pathNode.ptMap.x, (int)pathNode.ptMap.y); + return new Vector3(ptWld.x, dh, ptWld.y); } - /// Ported from C++ CMoveAgent::Optimize — incremental path smoothing while walking. + /// Ported from C++ CMoveAgent::Optimize. public bool Optimize(int moveIndex) { - if (moveIndex < 0 || moveIndex >= m_pathMap.Count - 1) + if (m_pPathOptimizer == null) return false; - - var layer0 = m_pMoveMap.GetLayer(0); - var rmap = layer0?.GetRMap(); - if (rmap == null) + if (!m_pPathOptimizer.NeedOptimize(moveIndex)) return false; - - int from = Mathf.Max(moveIndex, m_optimizeCurIndex); - if (from >= m_pathMap.Count - 1) - return false; - - int best = from + 1; - for (int j = m_pathMap.Count - 1; j > from; j--) - { - if (IsLinePassable(rmap, m_pathMap[from], m_pathMap[j])) - { - best = j; - break; - } - } - - if (best <= from + 1) - return false; - - m_pathMap.RemoveRange(from + 1, best - from - 1); - m_path3D.RemoveRange(from + 1, best - from - 1); - m_optimizeCurIndex = from + 1; + m_pPathOptimizer.StepOptimize(); return true; } - public System.Collections.Generic.List GetFullPath() + public List GetFullPath() { - return new System.Collections.Generic.List(m_path3D); + var path = new List(); + var path2d = Get2DPath(); + if (path2d == null) + return path; + for (int i = 0; i < path2d.Count; i++) + path.Add(Get2DPathNode(i)); + return path; } public bool Search(int nMaxExpand = -1) { - // A* on layer0 rmap - // 在第0层 rmap 上执行 A* - m_path3D.Clear(); - var layer0 = m_pMoveMap.GetLayer(0); var rmap = layer0?.GetRMap(); if (rmap == null) return false; @@ -192,30 +171,18 @@ namespace AutoMove if (w <= 0 || h <= 0) return false; if (!InBounds(m_ptStart, w, h) || !InBounds(m_ptGoal, w, h)) - { return false; - } - // If goal or start is not passable, try to find nearest passable (original MoveAgent does this). - // 如果起点或终点不可通行,尝试寻找最近可通行点(原版 MoveAgent 会这样做)。 if (!rmap.GetPixel(m_ptStart.x, m_ptStart.y)) { if (!TryFindNearestPassable(rmap, m_ptStart, w, h, 64, out var newStart)) - { - if (DEBUG_AUTOPF) Debug.LogWarning($"[CMoveAgent] start not passable and no nearest passable found. start={m_ptStart}"); return false; - } - if (DEBUG_AUTOPF) Debug.Log($"[CMoveAgent] Adjust start {m_ptStart} -> {newStart}"); m_ptStart = newStart; } if (!rmap.GetPixel(m_ptGoal.x, m_ptGoal.y)) { if (!TryFindNearestPassable(rmap, m_ptGoal, w, h, 64, out var newGoal)) - { - if (DEBUG_AUTOPF) Debug.LogWarning($"[CMoveAgent] goal not passable and no nearest passable found. goal={m_ptGoal}"); return false; - } - if (DEBUG_AUTOPF) Debug.Log($"[CMoveAgent] Adjust goal {m_ptGoal} -> {newGoal}"); m_ptGoal = newGoal; } @@ -227,8 +194,6 @@ namespace AutoMove open.Push(m_ptStart, Heuristic(m_ptStart, m_ptGoal)); int expands = 0; - //ToDo: need use another method to caculate the value of maxExpand - //800000 is a magic number, need to be optimized int maxExpand = nMaxExpand > 0 ? nMaxExpand : 800000; while (open.Count > 0 && expands < maxExpand) @@ -271,16 +236,12 @@ namespace AutoMove if (rmap == null) return false; - // Check origin first - // 先检查原点 if (origin.x >= 0 && origin.x < w && origin.y >= 0 && origin.y < h && rmap.GetPixel(origin.x, origin.y)) { best = origin; return true; } - // Expand square rings - // 按方形“圈”扩展搜索 for (int r = 1; r <= maxRadius; r++) { int minX = Math.Max(0, origin.x - r); @@ -288,14 +249,11 @@ namespace AutoMove int minY = Math.Max(0, origin.y - r); int maxY = Math.Min(h - 1, origin.y + r); - // Top/bottom edges for (int x = minX; x <= maxX; x++) { TryConsider(x, minY); TryConsider(x, maxY); } - - // Left/right edges (excluding corners already checked) for (int y = minY + 1; y <= maxY - 1; y++) { TryConsider(minX, y); @@ -328,123 +286,35 @@ namespace AutoMove private void ReconstructPath(Dictionary cameFrom, Vector2Int cur) { - List rev = new List(1024) { cur }; + var rev = new List(1024) { cur }; while (cameFrom.TryGetValue(cur, out var prev)) { cur = prev; rev.Add(cur); } - rev.Reverse(); - var layer0 = m_pMoveMap.GetLayer(0); - var rmap = layer0?.GetRMap(); - List smoothed = rmap != null ? SmoothPathMap(rev, rmap) : rev; - m_pathMap.Clear(); - m_path3D.Clear(); - m_optimizeCurIndex = 0; - for (int i = 0; i < smoothed.Count; i++) + var initPath = new List(rev.Count); + for (int i = 0; i < rev.Count; i++) { - m_pathMap.Add(smoothed[i]); - m_path3D.Add(Map2Wld(smoothed[i])); - } - } - - /// String-pull on passable grid (same goal as C++ COptimizePath::SetupOptimize). - static List SmoothPathMap(List raw, CBitImage rmap) - { - if (raw == null || raw.Count <= 2) - return raw; - - var result = new List(raw.Count) { raw[0] }; - int i = 0; - while (i < raw.Count - 1) - { - int furthest = i + 1; - for (int j = raw.Count - 1; j > i; j--) + initPath.Add(new PathNode { - if (IsLinePassable(rmap, raw[i], raw[j])) - { - furthest = j; - break; - } - } - - if (furthest != i) - { - if (result[result.Count - 1] != raw[furthest]) - result.Add(raw[furthest]); - i = furthest; - } - else - { - i++; - if (result[result.Count - 1] != raw[i]) - result.Add(raw[i]); - } + ptMap = new Vector2(rev[i].x, rev[i].y), + layer = m_iLayerStart, + }); } - return result; - } + if (m_pPathOptimizer == null) + CreateOptimizer(); - /// Matches C++ COptimizePath::_LineTo strict diagonal checks on rmap. - static bool IsLinePassable(CBitImage rmap, Vector2Int from, Vector2Int to) - { - if (from == to) - return true; - - rmap.GetImageSize(out int w, out int h); - int x0 = from.x, y0 = from.y; - int x1 = to.x, y1 = to.y; - int dx = Mathf.Abs(x1 - x0); - int dy = Mathf.Abs(y1 - y0); - int sx = x0 < x1 ? 1 : -1; - int sy = y0 < y1 ? 1 : -1; - int err = dx - dy; - int lastX = x0, lastY = y0; - - while (true) - { - if (x0 < 0 || x0 >= w || y0 < 0 || y0 >= h || !rmap.GetPixel(x0, y0)) - return false; - - if (x0 == x1 && y0 == y1) - break; - - int e2 = err << 1; - int nextX = x0, nextY = y0; - if (e2 > -dy) - { - err -= dy; - nextX += sx; - } - if (e2 < dx) - { - err += dx; - nextY += sy; - } - - if (nextX != lastX && nextY != lastY) - { - if (!rmap.GetPixel(lastX, nextY) || !rmap.GetPixel(nextX, lastY)) - return false; - } - - x0 = nextX; - y0 = nextY; - lastX = x0; - lastY = y0; - } - - return true; + // C++: m_pPathOptimizer->SetupOptimize(GetMoveMap(), path); + m_pPathOptimizer.SetupOptimize(m_pMoveMap, initPath); } private static bool InBounds(Vector2Int p, int w, int h) => p.x >= 0 && p.x < w && p.y >= 0 && p.y < h; private static int Heuristic(Vector2Int a, Vector2Int b) { - // Octile distance * 10 - // 八方向启发式(octile)*10 int dx = Mathf.Abs(a.x - b.x); int dy = Mathf.Abs(a.y - b.y); int min = Math.Min(dx, dy); @@ -454,8 +324,6 @@ namespace AutoMove private static int Cost(Vector2Int a, Vector2Int b) { - // Diagonal=14, straight=10 - // 斜向=14,直向=10 int dx = Mathf.Abs(a.x - b.x); int dy = Mathf.Abs(a.y - b.y); return (dx + dy == 2) ? 14 : 10; @@ -475,35 +343,13 @@ namespace AutoMove public void ResetSearch() { - // Çå³ýµ±Ç°ËÑË÷״̬ - //if (m_iStat == PF_STATE_UNKNOWN) - //{ - // return; - //} - //if (m_pPfAlg) - //{ - // m_pPfAlg->Reset(); - //} - //if (m_pPathOptimizer) - //{ - // m_pPathOptimizer.Reset(); - //} - m_ptStart.x = m_ptStart.y = 0; + m_ptStart = Vector2Int.zero; m_iLayerStart = -1; - m_ptGoal.x = m_ptGoal.y = 0; + m_ptGoal = Vector2Int.zero; m_iLayerGoal = -1; - //m_pBrushTest = null; - - //m_iStat = PF_STATE_UNKNOWN; - m_path3D.Clear(); - m_pathMap.Clear(); - m_optimizeCurIndex = 0; + m_pPathOptimizer?.Reset(); } - /// - /// Very small min-heap for A*. - /// A* 用的小型最小堆。 - /// private sealed class MinHeap { private struct Node @@ -559,5 +405,3 @@ namespace AutoMove } } } - - diff --git a/Assets/PerfectWorld/Scripts/Move/AutoPFImp/AutoMove/CMoveMap.cs b/Assets/PerfectWorld/Scripts/Move/AutoPFImp/AutoMove/CMoveMap.cs index 70c4441a47..d4a8aa0139 100644 --- a/Assets/PerfectWorld/Scripts/Move/AutoPFImp/AutoMove/CMoveMap.cs +++ b/Assets/PerfectWorld/Scripts/Move/AutoPFImp/AutoMove/CMoveMap.cs @@ -56,8 +56,13 @@ namespace AutoMove public Vector2 TransMap2Wld(int x, int y) { - float wx = m_vOrigin.x + (x + 0.5f) * m_fPixelSize; - float wz = m_vOrigin.z + (y + 0.5f) * m_fPixelSize; + return TransMap2Wld((float)x, (float)y); + } + + public Vector2 TransMap2Wld(float mapX, float mapY) + { + float wx = m_vOrigin.x + (mapX + 0.5f) * m_fPixelSize; + float wz = m_vOrigin.z + (mapY + 0.5f) * m_fPixelSize; return new Vector2(wx, wz); } diff --git a/Assets/PerfectWorld/Scripts/Move/AutoPFImp/AutoMove/COptimizePath.cs b/Assets/PerfectWorld/Scripts/Move/AutoPFImp/AutoMove/COptimizePath.cs new file mode 100644 index 0000000000..e75975f261 --- /dev/null +++ b/Assets/PerfectWorld/Scripts/Move/AutoPFImp/AutoMove/COptimizePath.cs @@ -0,0 +1,298 @@ +using System.Collections.Generic; +using UnityEngine; + +// Ported from C++ OptimizePath.h / OptimizePath.cpp (Kui Wu, 2007). + +namespace AutoMove +{ + /// Line sampler for map-space path optimization (C++ CLine). + sealed class CLine + { + Vector2 m_from; + Vector2 m_dir; + int m_count; + + public void Init(Vector2 from, Vector2 dir) + { + m_from = from; + m_dir = dir; + float len = m_dir.magnitude; + if (len > 1e-8f) + { + m_dir.x /= len; + m_dir.y /= len; + } + m_count = 0; + } + + public void Init(Vector2 from, int dirX, int dirY) + { + Init(from, new Vector2(dirX, dirY)); + } + + public Vector2 Next() + { + m_count++; + return new Vector2(m_from.x + m_dir.x * m_count, m_from.y + m_dir.y * m_count); + } + + public int GetCount() => m_count; + public Vector2 GetFrom() => m_from; + public void Reset() => m_count = 0; + } + + /// Path optimizer — port of C++ COptimizePath. + public sealed class COptimizePath + { + const int LookAhead = 60; + const int LookStep = 3; + + CMoveMap m_pMoveMap; + int m_mapWidth; + int m_mapHeight; + readonly Dictionary m_lookUp = new Dictionary(4096); + readonly List m_path = new List(1024); + int m_curIndex = -1; + int m_catchCount = 10; + int m_curLayer = -1; + + public void Reset() + { + m_pMoveMap = null; + m_mapWidth = 0; + m_mapHeight = 0; + m_lookUp.Clear(); + m_path.Clear(); + m_curIndex = -1; + m_curLayer = -1; + } + + public int GetCatchCount() => m_catchCount; + + public List GetPath() => m_path; + + public bool NeedOptimize(int moveIndex) + { + if (m_curIndex < m_path.Count + && moveIndex < m_path.Count + && m_curIndex - moveIndex < m_catchCount) + { + if (moveIndex > m_curIndex) + { + SetFootprintRange(m_curIndex, moveIndex - 1, 0); + m_curIndex = moveIndex; + } + return true; + } + return false; + } + + public void SetupOptimize(CMoveMap pMoveMap, List initPath, int catchCount = 10) + { + m_path.Clear(); + if (initPath != null && initPath.Count > 0) + m_path.AddRange(initPath); + + m_pMoveMap = pMoveMap; + m_mapWidth = pMoveMap != null ? pMoveMap.GetMapWidth() : 0; + m_mapHeight = pMoveMap != null ? pMoveMap.GetMapLength() : 0; + m_curIndex = 0; + m_catchCount = catchCount; + m_curLayer = -1; + + if (m_path.Count == 0) + return; + + CheckLayer(); + } + + public void StepOptimize() + { + CheckLayer(); + int step = m_catchCount * 2; + if (step <= 0) + return; + + int i = 0; + while (i < step && m_curIndex < m_path.Count) + { + LocalOptimize(); + i++; + m_curIndex++; + } + } + + void CheckLayer() + { + if (m_path.Count == 0 || m_curIndex < 0 || m_curIndex >= m_path.Count) + return; + + if (m_path[m_curIndex].layer == m_curLayer) + return; + + m_curLayer = m_path[m_curIndex].layer; + m_lookUp.Clear(); + + int i = m_curIndex; + while (i < m_path.Count && m_path[i].layer == m_curLayer) + { + SetFootprint(m_path[i].ptMap, (short)(i + 1)); + i++; + } + } + + void LocalOptimize() + { + int toIndex = Mathf.Min(m_curIndex + LookAhead, m_path.Count - 1); + var line = new CLine(); + Vector2 dir; + int newCount = -1; + + while (toIndex - m_curIndex > LookStep) + { + if (GetFootprint(m_path[toIndex].ptMap) == 0) + { + toIndex -= LookStep; + continue; + } + if (m_path[toIndex].layer != m_curLayer) + { + toIndex -= LookStep; + continue; + } + + dir = m_path[toIndex].ptMap - m_path[m_curIndex].ptMap; + if ((int)dir.x == 0 && (int)dir.y == 0) + { + PathIntersect(toIndex); + return; + } + + line.Init(m_path[m_curIndex].ptMap, dir); + if (LineTo(line, m_path[toIndex].ptMap)) + { + newCount = line.GetCount(); + break; + } + toIndex -= LookStep; + } + + if (newCount > 0) + { + line.Reset(); + AddPathPortion(line, toIndex - m_curIndex, newCount); + } + } + + CBitImage GetRMap(int layer) + { + if (m_pMoveMap == null) + return null; + var layerMap = m_pMoveMap.GetLayer(layer); + return layerMap?.GetRMap(); + } + + bool LineTo(CLine line, Vector2 to) + { + int toX = (int)to.x; + int toY = (int)to.y; + int curX = (int)line.GetFrom().x; + int curY = (int)line.GetFrom().y; + + CBitImage pRMap = GetRMap(m_curLayer); + if (pRMap == null) + return false; + + int lastX = curX; + int lastY = curY; + + while (curX != toX || curY != toY) + { + Vector2 cur = line.Next(); + curX = (int)cur.x; + curY = (int)cur.y; + + if (!pRMap.GetPixel(curX, curY)) + return false; + + bool bNeighbor1 = pRMap.GetPixel(lastX, curY); + bool bNeighbor2 = pRMap.GetPixel(curX, lastY); + + if (toX == lastX && toY == curY) + break; + if (toX == curX && toY == lastY) + break; + + if (curX != lastX && curY != lastY && (!bNeighbor1 || !bNeighbor2)) + return false; + + lastX = curX; + lastY = curY; + } + + return true; + } + + void PathIntersect(int toIndex) + { + SetFootprintRange(m_curIndex + 1, toIndex, 0); + int removeCount = toIndex - m_curIndex; + if (removeCount > 0 && m_curIndex + 1 < m_path.Count) + m_path.RemoveRange(m_curIndex + 1, removeCount); + } + + void AddPathPortion(CLine line, int oldCount, int newCount) + { + SetFootprintRange(m_curIndex + 1, m_curIndex + oldCount, 0); + + if (oldCount > newCount) + { + m_path.RemoveRange(m_curIndex + 1, oldCount - newCount); + } + else if (oldCount < newCount) + { + int insertCount = newCount - oldCount; + for (int k = 0; k < insertCount; k++) + m_path.Insert(m_curIndex + 1, new PathNode()); + } + + int index = m_curIndex + 1; + while (line.GetCount() < newCount && index < m_path.Count) + { + var node = m_path[index]; + node.ptMap = line.Next(); + node.layer = m_curLayer; + m_path[index] = node; + index++; + } + + SetFootprintRange(m_curIndex + 1, m_curIndex + newCount, 1); + } + + int GetLookUpKey(int x, int y) => y * m_mapWidth + x; + + int GetFootprint(int x, int y) + { + return m_lookUp.TryGetValue(GetLookUpKey(x, y), out short v) ? v : 0; + } + + int GetFootprint(Vector2 pt) => GetFootprint((int)pt.x, (int)pt.y); + + void SetFootprint(int x, int y, int val) + { + int key = GetLookUpKey(x, y); + if (val == 0) + m_lookUp.Remove(key); + else + m_lookUp[key] = (short)val; + } + + void SetFootprint(Vector2 pt, int val) => SetFootprint((int)pt.x, (int)pt.y, val); + + void SetFootprintRange(int fromIndex, int toIndex, int val) + { + for (int i = fromIndex; i <= toIndex && i < m_path.Count; ++i) + SetFootprint(m_path[i].ptMap, val); + } + } +} diff --git a/Assets/PerfectWorld/Scripts/Move/AutoPFImp/AutoMove/COptimizePath.cs.meta b/Assets/PerfectWorld/Scripts/Move/AutoPFImp/AutoMove/COptimizePath.cs.meta new file mode 100644 index 0000000000..86d0edecc0 --- /dev/null +++ b/Assets/PerfectWorld/Scripts/Move/AutoPFImp/AutoMove/COptimizePath.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 96e45cded56a0bc4188f329f2cf3b676 \ No newline at end of file diff --git a/Assets/PerfectWorld/Scripts/Move/AutoPFImp/AutoMove/PathNode.cs b/Assets/PerfectWorld/Scripts/Move/AutoPFImp/AutoMove/PathNode.cs new file mode 100644 index 0000000000..885435e2d2 --- /dev/null +++ b/Assets/PerfectWorld/Scripts/Move/AutoPFImp/AutoMove/PathNode.cs @@ -0,0 +1,22 @@ +using UnityEngine; + +namespace AutoMove +{ + /// Matches C++ AutoMove::PathNode (PfConstant.h). + public struct PathNode + { + public Vector2 ptMap; + public int layer; + + public Vector2Int GetPtI() + { + return new Vector2Int(Mathf.FloorToInt(ptMap.x), Mathf.FloorToInt(ptMap.y)); + } + + public void SetI(Vector2Int pt, int iLayer) + { + ptMap = new Vector2(pt.x, pt.y); + layer = iLayer; + } + } +} diff --git a/Assets/PerfectWorld/Scripts/Move/AutoPFImp/AutoMove/PathNode.cs.meta b/Assets/PerfectWorld/Scripts/Move/AutoPFImp/AutoMove/PathNode.cs.meta new file mode 100644 index 0000000000..0cf163da7d --- /dev/null +++ b/Assets/PerfectWorld/Scripts/Move/AutoPFImp/AutoMove/PathNode.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 98c1e0dc325b8e64bbda287a0128aeed \ No newline at end of file diff --git a/Assets/PerfectWorld/Scripts/Move/CECIntelligentRoute.cs b/Assets/PerfectWorld/Scripts/Move/CECIntelligentRoute.cs index a9a0619740..a772cd9a43 100644 --- a/Assets/PerfectWorld/Scripts/Move/CECIntelligentRoute.cs +++ b/Assets/PerfectWorld/Scripts/Move/CECIntelligentRoute.cs @@ -19,7 +19,7 @@ namespace BrewMonster.Scripts /// public sealed class CECIntelligentRoute { - private const bool DEBUG_AUTOPF = true; + private const bool DEBUG_AUTOPF = false; /// cos(5°) for FindFarthestNode — same as C++ EC_IntelligentRoute.cpp. private static readonly float s_cos5Deg = Mathf.Cos(5f * Mathf.PI / 180f); public enum SearchResult @@ -478,10 +478,8 @@ namespace BrewMonster.Scripts } m_iCurDest = FindNextNode(pos, m_iCurDest + 1); + m_dist2CurDest = (GetCurDest() - pos).MagnitudeH(); agent.Optimize(m_iCurDest); - int newPathCount = agent.GetPathCount(); - if (m_iCurDest >= newPathCount) - m_iCurDest = Mathf.Max(0, newPathCount - 1); m_dist2CurDest = (GetCurDest() - pos).MagnitudeH(); }