From 439941d15e3f5ccb190cba73dce91277ce900360 Mon Sep 17 00:00:00 2001 From: HungDK <> Date: Sat, 18 Apr 2026 15:10:10 +0700 Subject: [PATCH] Fix unstable router, water route --- .../Scripts/Managers/EC_HPWorkMove.cs | 127 ++--------- .../Move/AutoPFImp/AutoMove/CMoveAgent.cs | 7 + .../Scripts/Move/CECIntelligentRoute.cs | 209 +++++++++++++++--- 3 files changed, 199 insertions(+), 144 deletions(-) diff --git a/Assets/PerfectWorld/Scripts/Managers/EC_HPWorkMove.cs b/Assets/PerfectWorld/Scripts/Managers/EC_HPWorkMove.cs index a090a4b0e1..ae9155a839 100644 --- a/Assets/PerfectWorld/Scripts/Managers/EC_HPWorkMove.cs +++ b/Assets/PerfectWorld/Scripts/Managers/EC_HPWorkMove.cs @@ -178,15 +178,20 @@ namespace BrewMonster.Scripts #if ENABLE_CEC_INTELLIGENT_ROUTE if (IsAutoPF()) - { + { if (global::BrewMonster.Scripts.CECIntelligentRoute.Instance().IsIdle()) - { + { // C++: AutoPF not ready yet, wait next tick for SwitchTo2D (EC_HPWorkMove.cpp). // C++:智能寻路模式未成功时,等待下个 Tick 切换到 DEST_2D 模式。 - return true; - } - // Unity extension: do not clear AutoPF when flying — Tick_FlySwim follows waypoints in air/water. - // 原版 C++ 此处会 Reset+DEST_2D;空中/水中目标需要保留路径并在 Tick_FlySwim 沿路径移动。 + return true; + } + // EC_HPWorkMove.cpp: mid-air AutoPF → reset route and switch to DEST_2D next tick. + if (m_pHost.IsFlying()) + { + global::BrewMonster.Scripts.CECIntelligentRoute.Instance().ResetSearch(); + m_bSwitchTo2D = true; + return true; + } } #endif @@ -1323,111 +1328,10 @@ namespace BrewMonster.Scripts } else if (IsAutoPF()) { + // EC_HPWorkMove.cpp Tick_FlySwim: AutoPF in air/water does not walk waypoints — drop path and use DEST_2D. #if ENABLE_CEC_INTELLIGENT_ROUTE - var route = global::BrewMonster.Scripts.CECIntelligentRoute.Instance(); - // Unity: ground AutoPF is in Tick_Walk; swim/fly use Tick_FlySwim. C++ resets here and forces DEST_2D, - // which breaks routes when the path or target is in water/air — follow path nodes like DEST_2D swim/fly. - bool envAirOrWater = m_pHost.m_iMoveEnv == (int)MoveEnvironment.MOVEENV_WATER - || m_pHost.m_iMoveEnv == (int)MoveEnvironment.MOVEENV_AIR; - if (envAirOrWater && route.IsUsageMove()) - { - if (route.IsPathFinished()) - { - if (TryContinueAutoPFToWorldDestDirect()) - return true; - Finish(); - m_pHost.m_MoveCtrl.SendStopMoveCmd(EC_Utility.ToVector3(vCurPos), fMaxSpeed, - iMoveMode | (int)GPMoveMode.GP_MOVE_RUN); - return true; - } - - if (!route.IsMoveOn()) - { - route.ResetSearch(); - m_bSwitchTo2D = true; - return true; - } - - Vector3 vPushDir = Vector3.zero; - m_pHost.GetPushDir(ref vPushDir, (uint)CECHostPlayer.MOVE_DIR.MD_ALL, 0f); - vPushDir.x = vPushDir.z = 0.0f; - - float fSpeed1H = m_pHost.m_vVelocity.MagnitudeH(); - float fSpeed1V = m_pHost.m_vVelocity.y; - - A3DVECTOR3 vCurDest = route.GetCurDest(); - A3DVECTOR3 vMoveDirH = vCurDest - vCurPos; - vMoveDirH.y = 0.0f; - float fDistH = vMoveDirH.Normalize(); - - if (fDistH < 1e-4f) - { - route.OnPlayerPosChange(vCurPos); - if (route.IsPathFinished()) - { - if (TryContinueAutoPFToWorldDestDirect()) - return true; - Finish(); - m_pHost.m_MoveCtrl.SendStopMoveCmd(EC_Utility.ToVector3(vCurPos), fMaxSpeed, - iMoveMode | (int)GPMoveMode.GP_MOVE_RUN); - } - - return true; - } - - float pa = 0.0f; - float s = -0.5f * fSpeed1H * fSpeed1H / na; - if (fDistH > s - 0.01f) - pa = CECHostMove.EC_PUSH_ACCE; - - float fSpeed2H = fSpeed1H + (pa + na) * fDeltaTime; - if (Math.Abs(pa - 0f) < float.Epsilon && fSpeed2H < 0.0f) - fSpeed2H = 0.0f; - else if (fSpeed2H > fMaxSpeed) - fSpeed2H = fMaxSpeed; - - Glide(fDistH / Mathf.Max(fMaxSpeed, 0.01f), vMoveDirH, fDeltaTime, bInAir); - - vMoveDirH = m_pHost.GetModelMoveDir(); - vMoveDirH.y = 0; - vMoveDirH.Normalize(); - - float fSpeed2V = CalcFlySwimVertSpeed(fSpeed1V, vPushDir.y, CECHostMove.EC_PUSH_ACCE, fDeltaTime); - A3DVECTOR3 vVel2 = vMoveDirH * fSpeed2H + GPDataTypeHelper.g_vAxisY * fSpeed2V; - - vCurPos = m_pHost.m_MoveCtrl.AirWaterMove(vVel2, fDeltaTime, bInAir); - - if (m_pHost.m_MoveCtrl.MoveBlocked() >= 3) - { - vVel2.Clear(); - Finish(); - m_pHost.m_MoveCtrl.SendStopMoveCmd(EC_Utility.ToVector3(vCurPos), fMaxSpeed, - iMoveMode | (int)GPMoveMode.GP_MOVE_RUN); - } - else - { - m_pHost.SetPos(EC_Utility.ToVector3(vCurPos)); - m_pHost.m_vVelocity = vVel2; - route.OnPlayerPosChange(vCurPos); - if (route.IsPathFinished()) - { - if (TryContinueAutoPFToWorldDestDirect()) - return true; - Finish(); - m_pHost.m_MoveCtrl.SendStopMoveCmd(EC_Utility.ToVector3(vCurPos), vVel2.Magnitude(), - iMoveMode | (int)GPMoveMode.GP_MOVE_RUN); - } - else - { - m_pHost.m_MoveCtrl.SendMoveCmd(vCurPos, 0, m_vMoveDest, vVel2, - iMoveMode | (int)GPMoveMode.GP_MOVE_RUN); - } - } - - return true; - } + global::BrewMonster.Scripts.CECIntelligentRoute.Instance().ResetSearch(); #endif - CECIntelligentRoute.Instance().ResetSearch(); m_bSwitchTo2D = true; } return true; @@ -1508,8 +1412,9 @@ namespace BrewMonster.Scripts while (true) { - // C++ skips Search while flying (forces DEST_2D). Unity: allow Search so aerial targets get a 2D path. - // 原版飞行时不做 Search;此处允许寻路,使空中起点/空中目标也能得到 XZ 路径。 + // EC_HPWorkMove.cpp UpdateResetUseAutoPF: skip Search while flying (bSwitchTo2D stays true). + if (m_pHost.IsFlying()) + break; // Brush test is not fully ported yet; pass null for now (static movemap only). // BrushTest 暂未完整移植;当前先传 null(仅静态 movemap)。 diff --git a/Assets/PerfectWorld/Scripts/Move/AutoPFImp/AutoMove/CMoveAgent.cs b/Assets/PerfectWorld/Scripts/Move/AutoPFImp/AutoMove/CMoveAgent.cs index 98f49f0362..5199de3cb9 100644 --- a/Assets/PerfectWorld/Scripts/Move/AutoPFImp/AutoMove/CMoveAgent.cs +++ b/Assets/PerfectWorld/Scripts/Move/AutoPFImp/AutoMove/CMoveAgent.cs @@ -118,6 +118,12 @@ namespace AutoMove public int GetPathCount() => m_path3D.Count; + /// + /// Matches C++ CMoveAgent::GetOptimizeCatchCount — lookahead window for path following. + /// 对应 C++:无 PathOptimizer 时为 0,FindNearest/Farthest 仅在单步索引上工作。 + /// + public int GetOptimizeCatchCount() => 0; + public Vector3 Get3DPathNode(int index) { if (index < 0 || index >= m_path3D.Count) return Vector3.zero; @@ -351,6 +357,7 @@ namespace AutoMove //m_pBrushTest = null; //m_iStat = PF_STATE_UNKNOWN; + m_path3D.Clear(); } /// diff --git a/Assets/PerfectWorld/Scripts/Move/CECIntelligentRoute.cs b/Assets/PerfectWorld/Scripts/Move/CECIntelligentRoute.cs index 780f94f1f5..e9d1e10c32 100644 --- a/Assets/PerfectWorld/Scripts/Move/CECIntelligentRoute.cs +++ b/Assets/PerfectWorld/Scripts/Move/CECIntelligentRoute.cs @@ -20,6 +20,8 @@ namespace BrewMonster.Scripts public sealed class CECIntelligentRoute { private const bool DEBUG_AUTOPF = true; + /// 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 { enumSearchSuccess, // 寻路成功 // Search success @@ -395,26 +397,32 @@ namespace BrewMonster.Scripts return SearchResult.enumSearchNoPath; } + // EC_IntelligentRoute.cpp: single-node 2D path → start/goal coincide. + if (agent.GetPathCount() == 1) + { + agent.ResetSearch(); + if (DEBUG_AUTOPF) + BMLogger.Log("[CECIntelligentRoute] Search: path size 1 (start/end coincide)."); + return SearchResult.enumSearchStartEndCoincide; + } + m_state = RouteState.enumRouteMoving; - m_iCurDest = 0; + m_lastPos = m_start; + m_lastMove = 0.0f; + m_iCurDest = FindNextNode(start, 0); + m_dist2CurDest = (GetCurDest() - m_start).MagnitudeH(); if (DEBUG_AUTOPF) { - BMLogger.Log($"[CECIntelligentRoute] Search: success pathCount={agent.GetPathCount()} start=({start.x:F2},{start.z:F2}) end=({end.x:F2},{end.z:F2})"); + BMLogger.Log($"[CECIntelligentRoute] Search: success pathCount={agent.GetPathCount()} start=({start.x:F2},{start.z:F2}) end=({end.x:F2},{end.z:F2}) curDestIdx={m_iCurDest}"); } return SearchResult.enumSearchSuccess; } public A3DVECTOR3 GetCurDest() { - if (m_state == RouteState.enumRouteIdle || m_iCurMoveAgent < 0) return m_end; - var agent = m_moveAgents[m_iCurMoveAgent].agent; - if (agent == null) return m_end; - - int cnt = agent.GetPathCount(); - if (cnt <= 0) return m_end; - int idx = Mathf.Clamp(m_iCurDest, 0, cnt - 1); - Vector3 v = agent.Get3DPathNode(idx); - return new A3DVECTOR3(v.x, v.y, v.z); + if (IsIdle() || m_iCurMoveAgent < 0) + return m_end; + return GetNodePos(m_iCurDest); } /// @@ -430,40 +438,175 @@ namespace BrewMonster.Scripts return agent.GetFullPath(); } + /// Ported from C++ CECIntelligentRoute::OnPlayerPosChange (EC_IntelligentRoute.cpp). public void OnPlayerPosChange(A3DVECTOR3 pos) { - if (m_state != RouteState.enumRouteMoving) return; - if (m_iCurMoveAgent < 0) return; - var agent = m_moveAgents[m_iCurMoveAgent].agent; - if (agent == null) return; + if (!IsMoveOn()) + return; - int cnt = agent.GetPathCount(); - if (cnt <= 0) + if (CanFinishPath(pos)) { m_state = RouteState.enumRoutePathFinished; return; } - // Advance nodes when close to current dest. - // 接近当前节点时推进到下一节点。 - float reach = 1.0f; - while (m_iCurDest < cnt) - { - Vector3 d = agent.Get3DPathNode(m_iCurDest); - Vector3 delta = d - new Vector3(pos.x, pos.y, pos.z); - delta.y = 0.0f; - if (delta.sqrMagnitude <= reach * reach) - { - m_iCurDest++; - continue; - } - break; - } + if (!CanMoveToNext(pos)) + return; - if (m_iCurDest >= cnt) + RangedMoveAgent? pCur = GetCurAgent(); + if (pCur == null || pCur.Value.agent == null) + return; + CMoveAgent agent = pCur.Value.agent; + int pathCount = agent.GetPathCount(); + if (m_iCurDest == pathCount - 1) { m_state = RouteState.enumRoutePathFinished; + return; } + + m_iCurDest = FindNextNode(pos, m_iCurDest + 1); + m_dist2CurDest = (GetCurDest() - pos).MagnitudeH(); + // C++ calls agent->Optimize(m_iCurDest) here; Unity CMoveAgent has no path optimizer yet. + } + + A3DVECTOR3 GetNodePos(int iNode) + { + RangedMoveAgent? pCurAgent = GetCurAgent(); + if (pCurAgent == null || pCurAgent.Value.agent == null) + return new A3DVECTOR3(0f, 0f, 0f); + CMoveAgent ag = pCurAgent.Value.agent; + int nPathCount = ag.GetPathCount(); + if (iNode >= 0 && iNode < nPathCount) + return GetNodePosNoCheck(iNode); + return new A3DVECTOR3(0f, 0f, 0f); + } + + A3DVECTOR3 GetNodePosNoCheck(int iNode) + { + if (m_iCurMoveAgent < 0 || m_iCurMoveAgent >= m_moveAgents.Count) + return new A3DVECTOR3(0f, 0f, 0f); + CMoveAgent agent = m_moveAgents[m_iCurMoveAgent].agent; + 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); + } + + A3DVECTOR3 GetNodePosXZ(int iNode) + { + if (IsIdle()) + return new A3DVECTOR3(0f, 0f, 0f); + A3DVECTOR3 p = GetNodePosNoCheck(iNode); + p.y = 0f; + return p; + } + + int FindNearestNode(A3DVECTOR3 curPos, int iNodeFrom) + { + if (!IsMoveOn()) + return -1; + RangedMoveAgent? pCur = GetCurAgent(); + if (pCur == null || pCur.Value.agent == null) + return -1; + CMoveAgent agent = pCur.Value.agent; + int pathCount = agent.GetPathCount(); + if (iNodeFrom < 0 || iNodeFrom >= pathCount) + return -1; + if (iNodeFrom == pathCount - 1) + return iNodeFrom; + + int maxCheckIndex = iNodeFrom + agent.GetOptimizeCatchCount(); + maxCheckIndex = Mathf.Min(maxCheckIndex, pathCount - 1); + int bestIndex = -1; + double bestDist2 = double.MaxValue; + for (int i = iNodeFrom; i <= maxCheckIndex; ++i) + { + A3DVECTOR3 testPos = GetNodePosXZ(i); + testPos -= curPos; + testPos.y = 0f; + double dist2 = testPos.x * testPos.x + testPos.z * testPos.z; + if (dist2 < bestDist2) + { + bestIndex = i; + bestDist2 = dist2; + } + } + + return bestIndex >= 0 ? bestIndex : -1; + } + + int FindFarthestNode(A3DVECTOR3 curPos, int iNodeFrom) + { + if (iNodeFrom < 0) + return iNodeFrom; + RangedMoveAgent? pCur = GetCurAgent(); + if (pCur == null || pCur.Value.agent == null) + return iNodeFrom; + CMoveAgent agent = pCur.Value.agent; + int pathCount = agent.GetPathCount(); + if (iNodeFrom == pathCount - 1) + return iNodeFrom; + + A3DVECTOR3 moveDir = GetNodePosXZ(iNodeFrom); + moveDir -= curPos; + moveDir.y = 0f; + if (moveDir.Normalize() < 1e-4f) + return iNodeFrom; + + int maxCheckIndex = iNodeFrom + agent.GetOptimizeCatchCount(); + maxCheckIndex = Mathf.Min(maxCheckIndex, pathCount - 1); + for (int i = iNodeFrom + 1; i <= maxCheckIndex; ++i) + { + A3DVECTOR3 testDir = GetNodePosXZ(i); + testDir -= curPos; + testDir.y = 0f; + if (testDir.Normalize() < 1e-4f) + break; + float dtp = A3DVECTOR3.DotProduct(moveDir, testDir); + if (dtp < s_cos5Deg) + break; + iNodeFrom = i; + } + + return iNodeFrom; + } + + int FindNextNode(A3DVECTOR3 curPos, int iNodeFrom) + { + int iCandidate = FindNearestNode(curPos, iNodeFrom); + if (iCandidate < 0) + return Mathf.Max(0, iNodeFrom); + return FindFarthestNode(curPos, iCandidate); + } + + bool CanFinishPath(A3DVECTOR3 pos) + { + if (!IsMoveOn()) + return false; + A3DVECTOR3 delta = m_end - pos; + return delta.Magnitude() <= 0.5f; + } + + bool CanMoveToNext(A3DVECTOR3 pos) + { + float fMove = (pos - m_lastPos).MagnitudeH(); + m_lastPos = pos; + + float lastMove = m_lastMove; + m_lastMove = fMove; + + float dist2CurDest = m_dist2CurDest; + A3DVECTOR3 vCurDest = GetCurDest(); + m_dist2CurDest = (vCurDest - pos).MagnitudeH(); + + if (fMove >= dist2CurDest) + return true; + if (fMove + 0.1f >= dist2CurDest) + return true; + if (lastMove * 0.5f >= m_dist2CurDest) + return true; + return false; } } }