Merge pull request 'fixbug/npc-dialog' (#360) from fixbug/npc-dialog into develop

Reviewed-on: https://git.pthub.vn/Unity/perfect-world-unity/pulls/360
This commit is contained in:
hungdk
2026-04-18 08:10:54 +00:00
3 changed files with 199 additions and 144 deletions
@@ -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)。
@@ -118,6 +118,12 @@ namespace AutoMove
public int GetPathCount() => m_path3D.Count;
/// <summary>
/// Matches C++ CMoveAgent::GetOptimizeCatchCount — lookahead window for path following.
/// 对应 C++:无 PathOptimizer 时为 0FindNearest/Farthest 仅在单步索引上工作。
/// </summary>
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();
}
/// <summary>
@@ -20,6 +20,8 @@ namespace BrewMonster.Scripts
public sealed class CECIntelligentRoute
{
private const bool DEBUG_AUTOPF = true;
/// <summary>cos(5°) for FindFarthestNode — same as C++ EC_IntelligentRoute.cpp.</summary>
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);
}
/// <summary>
@@ -430,40 +438,175 @@ namespace BrewMonster.Scripts
return agent.GetFullPath();
}
/// <summary>Ported from C++ CECIntelligentRoute::OnPlayerPosChange (EC_IntelligentRoute.cpp).</summary>
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;
}
}
}