diff --git a/Assets/PerfectWorld/Scripts/NPC/CECNPC.cs b/Assets/PerfectWorld/Scripts/NPC/CECNPC.cs index 51d33888db..399a09ace2 100644 --- a/Assets/PerfectWorld/Scripts/NPC/CECNPC.cs +++ b/Assets/PerfectWorld/Scripts/NPC/CECNPC.cs @@ -1019,6 +1019,10 @@ public class CECNPC : CECObject var npcVisual = GetComponent(); npcVisual?.InitNPCEventDoneHandler(m_NPCInfo); + // UINPC.Start can run before async model instantiate; refresh anchor once SMR hierarchy exists. + // UINPC.Start可能在异步模型实例化之前执行;SMR层次就绪后刷新名牌锚点。 + m_npcUI?.RefreshWorldNameplatePosition(); + //QueueECModelForLoad(MTL_ECM_NPC, GetNPCInfo().nid, GetBornStamp(), GetServerPos(), szModelFile, tid); } public ROLEBASICPROP GetBasicProps() { return m_BasicProps; } diff --git a/Assets/PerfectWorld/Scripts/NPC/NPCVisual.cs b/Assets/PerfectWorld/Scripts/NPC/NPCVisual.cs index d435950d63..eca6c7a647 100644 --- a/Assets/PerfectWorld/Scripts/NPC/NPCVisual.cs +++ b/Assets/PerfectWorld/Scripts/NPC/NPCVisual.cs @@ -211,4 +211,5 @@ public class NPCVisual : MonoBehaviour } return true; } + } diff --git a/Assets/PerfectWorld/Scripts/UI/GamePlay/NameplateWorldAnchor.cs b/Assets/PerfectWorld/Scripts/UI/GamePlay/NameplateWorldAnchor.cs index e4238a8e24..5d17eb9c58 100644 --- a/Assets/PerfectWorld/Scripts/UI/GamePlay/NameplateWorldAnchor.cs +++ b/Assets/PerfectWorld/Scripts/UI/GamePlay/NameplateWorldAnchor.cs @@ -61,11 +61,19 @@ namespace BrewMonster /// public bool TryGetWorldPosition(out Vector3 worldPos) { + return TryGetWorldPosition(out worldPos, out _, logNpcRootFallback: true); + } + + /// True when NPC anchor came from merged SMR bounds (not root fallback). NPC以外恒为false。 + /// When false, NPC root fallback does not LogError (for retries before model load). 为假时NPC根回退不打LogError(模型未加载前的重试)。 + public bool TryGetWorldPosition(out Vector3 worldPos, out bool npcUsedSkinnedMeshMergedBounds, bool logNpcRootFallback = true) + { + npcUsedSkinnedMeshMergedBounds = false; CacheRefs(); if (hostPlayer != null) return TryGetForPlayer(out worldPos); if (hostNpc != null) - return TryGetForNpc(out worldPos); + return TryGetForNpc(out worldPos, out npcUsedSkinnedMeshMergedBounds, logNpcRootFallback); worldPos = default; return false; } @@ -104,9 +112,10 @@ namespace BrewMonster return true; } - private bool TryGetForNpc(out Vector3 worldPos) + private bool TryGetForNpc(out Vector3 worldPos, out bool usedSkinnedMeshMergedBounds, bool logNpcRootFallback) { worldPos = default; + usedSkinnedMeshMergedBounds = false; if (hostNpc == null) return false; @@ -114,11 +123,25 @@ namespace BrewMonster && npcVisual != null && npcVisual.TryGetNamePlateAnchorWorld(out worldPos)) { + usedSkinnedMeshMergedBounds = true; worldPos.y += extraWorldYOffset; return true; } worldPos = hostNpc.transform.position + Vector3.up * (fallbackHeightAboveRoot + extraWorldYOffset); + if (logNpcRootFallback) + { + string reason = !preferVisualBoundsFallback + ? "preferVisualBoundsFallback is false (bounds path skipped)" + : npcVisual == null + ? "npcVisual is null" + : "TryGetNamePlateAnchorWorld failed (no usable SkinnedMeshRenderer/sharedMesh)"; + BMLogger.LogError( + "[Cuong] [NameplateWorldAnchor] NPC: cannot use merged SMR bounds; using root fallback. " + + $"Reason: {reason}. hostNpc={hostNpc.name} fallbackHeightAboveRoot={fallbackHeightAboveRoot} extraWorldYOffset={extraWorldYOffset}. " + + "Formula: worldPos = hostNpc.transform.position + Vector3.up * (fallbackHeightAboveRoot + extraWorldYOffset). " + + "When bounds OK: worldPos.x/z = combinedBounds.center.x/z, worldPos.y = combinedBounds.max.y + extraWorldYOffset."); + } return true; } } diff --git a/Assets/PerfectWorld/Scripts/UI/GamePlay/UINPC.cs b/Assets/PerfectWorld/Scripts/UI/GamePlay/UINPC.cs index cec231fbc0..eb6b7165b7 100644 --- a/Assets/PerfectWorld/Scripts/UI/GamePlay/UINPC.cs +++ b/Assets/PerfectWorld/Scripts/UI/GamePlay/UINPC.cs @@ -1,3 +1,4 @@ +using System.Collections; using System.Collections.Generic; using TMPro; using UnityEngine; @@ -41,6 +42,7 @@ namespace BrewMonster private NameplateWorldAnchor _nameplateAnchor; private Image _cachedIconImage; + private Coroutine _initialNameplateRoutine; private void Awake() { @@ -58,23 +60,84 @@ namespace BrewMonster private void Start() { - ApplyInitialCanvasRootPosition(); + _initialNameplateRoutine = StartCoroutine(CoApplyInitialNameplateDeferred(maxFrames: 120)); } /// - /// World position for the nameplate root is applied once at startup (no per-frame follow). - /// 名牌根节点世界坐标仅在启动时设置一次(不做每帧跟随)。 + /// Call after NPC model async load (e.g. ) so SMR bounds exist before anchoring. + /// 在NPC模型异步加载完成后调用(如 ),以便SMR包围盒已存在再定位名牌。 /// - private void ApplyInitialCanvasRootPosition() + public void RefreshWorldNameplatePosition() + { + if (_initialNameplateRoutine != null) + { + StopCoroutine(_initialNameplateRoutine); + _initialNameplateRoutine = null; + } + _initialNameplateRoutine = StartCoroutine(CoApplyInitialNameplateDeferred(maxFrames: 45)); + } + + /// + /// World position for the nameplate root is applied once SMR bounds exist, or after timeout with root fallback (then may log). + /// with logNpcRootFallback: false while retrying so early frames do not spam errors. + /// 名牌根节点世界坐标:优先等合并SMR包围盒就绪后再设一次;超时则用根回退(此时才打LogError)。 + /// + private IEnumerator CoApplyInitialNameplateDeferred(int maxFrames) { if (_canvasRoot == null) { - return; + CacheRefs(); } - if (_nameplateAnchor != null && _nameplateAnchor.TryGetWorldPosition(out var worldPos)) + if (_canvasRoot == null) + { + yield break; + } + if (_nameplateAnchor == null) + { + BMLogger.LogError( + "[Cuong] [UINPC] ApplyInitialCanvasRootPosition: NameplateWorldAnchor missing. " + + "Expected formula: _canvasRoot.position = anchor.TryGetWorldPosition(out worldPos) ? worldPos : unchanged."); + _initialNameplateRoutine = null; + yield break; + } + + for (var i = 0; i < maxFrames; i++) + { + if (_nameplateAnchor.TryGetWorldPosition(out var worldPos, out var fromSmr, logNpcRootFallback: false) + && fromSmr) + { + ApplyCanvasRootLocalPosition(worldPos); + _initialNameplateRoutine = null; + yield break; + } + yield return null; + } + + if (_nameplateAnchor.TryGetWorldPosition(out var finalPos, out _, logNpcRootFallback: true)) + { + ApplyCanvasRootLocalPosition(finalPos); + } + else + { + BMLogger.LogError( + "[Cuong] [UINPC] ApplyInitialCanvasRootPosition: TryGetWorldPosition returned false (no CECPlayer/CECNPC parent?). " + + "Formula when OK: _canvasRoot.position = worldPos. " + + "NPC with SMR bounds: worldPos = (combinedBounds.center.x, combinedBounds.max.y, combinedBounds.center.z) + Vector3.up * extraWorldYOffset. " + + "NPC fallback: worldPos = hostNpc.transform.position + Vector3.up * (fallbackHeightAboveRoot + extraWorldYOffset)."); + } + _initialNameplateRoutine = null; + } + + private void ApplyCanvasRootLocalPosition(Vector3 worldPos) + { + var parent = _canvasRoot.parent; + if (parent == null) { _canvasRoot.position = worldPos; + return; } + + _canvasRoot.localPosition = parent.InverseTransformPoint(worldPos); } public void SetName(string name) diff --git a/Assets/Resources/Monster/MonsterPrefab.prefab b/Assets/Resources/Monster/MonsterPrefab.prefab index 9c3aa1b9b2..f9a8cfefc6 100644 --- a/Assets/Resources/Monster/MonsterPrefab.prefab +++ b/Assets/Resources/Monster/MonsterPrefab.prefab @@ -502,7 +502,7 @@ MonoBehaviour: _healthImage: {fileID: 3715353156977051930} _iconTaskMain: {fileID: 0} _listIconTask: [] - _canvasRoot: {fileID: 0} + _canvasRoot: {fileID: 7528353842011023148} --- !u!114 &9001000111222333445 MonoBehaviour: m_ObjectHideFlags: 0 diff --git a/Assets/Resources/NPC/NPCServer.prefab b/Assets/Resources/NPC/NPCServer.prefab index 2033443c8c..08cb538df5 100644 --- a/Assets/Resources/NPC/NPCServer.prefab +++ b/Assets/Resources/NPC/NPCServer.prefab @@ -147,7 +147,7 @@ RectTransform: m_PrefabInstance: {fileID: 0} m_PrefabAsset: {fileID: 0} m_GameObject: {fileID: 3297168817873124018} - m_LocalRotation: {x: -3.7702125e-16, y: 0.97228837, z: 0.23378475, w: 1.5679955e-15} + m_LocalRotation: {x: 0.0260765, y: 0.57263446, z: -0.018228054, w: 0.8191932} m_LocalPosition: {x: 0, y: 0, z: 0} m_LocalScale: {x: 1, y: 1, z: 1} m_ConstrainProportionsScale: 0 @@ -272,7 +272,7 @@ RectTransform: m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} m_AnchorMin: {x: 0.5, y: 0.5} m_AnchorMax: {x: 0.5, y: 0.5} - m_AnchoredPosition: {x: 0, y: 1} + m_AnchoredPosition: {x: 0, y: 0.5} m_SizeDelta: {x: 0.5, y: 0.5} m_Pivot: {x: 0.5, y: 0.5} --- !u!222 &397635185778217656 @@ -378,7 +378,7 @@ MonoBehaviour: - {fileID: 536497386, guid: 75fb3bbb7c9421e4d8bf0728cf2b59b1, type: 3} - {fileID: -440769636, guid: 75fb3bbb7c9421e4d8bf0728cf2b59b1, type: 3} - {fileID: 844877792, guid: 75fb3bbb7c9421e4d8bf0728cf2b59b1, type: 3} - _canvasRoot: {fileID: 8006159455096186264} + _canvasRoot: {fileID: 8745273338113588215} --- !u!114 &9001000111222333446 MonoBehaviour: m_ObjectHideFlags: 0