update prefab and logic draw name

This commit is contained in:
CuongNV
2026-04-16 14:29:08 +07:00
parent 042fdeeba4
commit 329f62a050
6 changed files with 103 additions and 12 deletions
@@ -1019,6 +1019,10 @@ public class CECNPC : CECObject
var npcVisual = GetComponent<NPCVisual>();
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; }
@@ -211,4 +211,5 @@ public class NPCVisual : MonoBehaviour
}
return true;
}
}
@@ -61,11 +61,19 @@ namespace BrewMonster
/// </summary>
public bool TryGetWorldPosition(out Vector3 worldPos)
{
return TryGetWorldPosition(out worldPos, out _, logNpcRootFallback: true);
}
/// <param name="npcUsedSkinnedMeshMergedBounds">True when NPC anchor came from merged SMR bounds (not root fallback). NPC以外恒为false。</param>
/// <param name="logNpcRootFallback">When false, NPC root fallback does not LogError (for retries before model load). 为假时NPC根回退不打LogError(模型未加载前的重试)。</param>
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;
}
}
@@ -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));
}
/// <summary>
/// World position for the nameplate root is applied once at startup (no per-frame follow).
/// 名牌根节点世界坐标仅在启动时设置一次(不做每帧跟随)
/// Call after NPC model async load (e.g. <see cref="CECNPC.QueueLoadNPCModel"/>) so SMR bounds exist before anchoring.
/// 在NPC模型异步加载完成后调用(如 <see cref="CECNPC.QueueLoadNPCModel"/>),以便SMR包围盒已存在再定位名牌
/// </summary>
private void ApplyInitialCanvasRootPosition()
public void RefreshWorldNameplatePosition()
{
if (_initialNameplateRoutine != null)
{
StopCoroutine(_initialNameplateRoutine);
_initialNameplateRoutine = null;
}
_initialNameplateRoutine = StartCoroutine(CoApplyInitialNameplateDeferred(maxFrames: 45));
}
/// <summary>
/// World position for the nameplate root is applied once SMR bounds exist, or after timeout with root fallback (then may log).
/// <see cref="NameplateWorldAnchor.TryGetWorldPosition"/> with <c>logNpcRootFallback: false</c> while retrying so early frames do not spam errors.
/// 名牌根节点世界坐标:优先等合并SMR包围盒就绪后再设一次;超时则用根回退(此时才打LogError)。
/// </summary>
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)
@@ -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
+3 -3
View File
@@ -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