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;
}
}
}