From 8c72c3349ce96f1a090e7d2e79961fd7a9fff71e Mon Sep 17 00:00:00 2001 From: CuongNV <> Date: Wed, 15 Apr 2026 18:38:20 +0700 Subject: [PATCH] add code height name --- Assets/PerfectWorld/Scripts/NPC/NPCVisual.cs | 69 +++++++++- .../UI/GamePlay/NameplateWorldAnchor.cs | 125 ++++++++++++++++++ .../UI/GamePlay/NameplateWorldAnchor.cs.meta | 2 + .../PerfectWorld/Scripts/UI/GamePlay/UINPC.cs | 31 ++++- Assets/PerfectWorld/Scripts/UI/UIPlayer.cs | 36 ++++- 5 files changed, 254 insertions(+), 9 deletions(-) create mode 100644 Assets/PerfectWorld/Scripts/UI/GamePlay/NameplateWorldAnchor.cs create mode 100644 Assets/PerfectWorld/Scripts/UI/GamePlay/NameplateWorldAnchor.cs.meta diff --git a/Assets/PerfectWorld/Scripts/NPC/NPCVisual.cs b/Assets/PerfectWorld/Scripts/NPC/NPCVisual.cs index 973daa245c..d435950d63 100644 --- a/Assets/PerfectWorld/Scripts/NPC/NPCVisual.cs +++ b/Assets/PerfectWorld/Scripts/NPC/NPCVisual.cs @@ -11,6 +11,7 @@ public class NPCVisual : MonoBehaviour protected CECNPC.INFO m_NPCInfo; [SerializeField] private Queue _animationQueue = new Queue(); [SerializeField] private AnimancerState _currentState; + private bool debugNamePlateBounds = true; private const float fadeTime = .2f; private const FadeMode FadeMode = Animancer.FadeMode.FixedDuration; @@ -134,7 +135,7 @@ public class NPCVisual : MonoBehaviour // 回退到从此组件的层次结构搜索 namedAnimancer = GetComponentInChildren(); } - + if (namedAnimancer == null) { BMLogger.LogWarning($"NPCVisual: RefreshNamedAnimancer - namedAnimancer == null after refresh (modelRoot: {modelRoot?.name ?? "null"})"); @@ -144,4 +145,70 @@ public class NPCVisual : MonoBehaviour BMLogger.LogMono(this, $"NPCVisual: RefreshNamedAnimancer - Successfully refreshed namedAnimancer from model: {modelRoot?.name ?? "default"}"); } } + + /// + /// Resolve world anchor from merged bounds of all SkinnedMeshRenderers. + /// 从所有SkinnedMeshRenderer合并后的包围盒解析世界锚点。 + /// + public bool TryGetNamePlateAnchorWorld(out Vector3 worldPos) + { + worldPos = default; + + var skinnedMeshRenderers = GetComponentsInChildren(true); + if (skinnedMeshRenderers == null || skinnedMeshRenderers.Length == 0) + { + return false; + } + + Bounds combinedBounds = default; + bool hasAnySkinnedMesh = false; + for (int i = 0; i < skinnedMeshRenderers.Length; i++) + { + var renderer = skinnedMeshRenderers[i]; + if (renderer == null || renderer.sharedMesh == null) + { + continue; + } + + var meshBounds = renderer.sharedMesh.bounds; + var scale = renderer.transform.lossyScale; + var worldCenter = renderer.transform.TransformPoint(meshBounds.center); + var worldSize = new Vector3( + Mathf.Abs(meshBounds.size.x * scale.x), + Mathf.Abs(meshBounds.size.y * scale.y), + Mathf.Abs(meshBounds.size.z * scale.z) + ); + var currentBounds = new Bounds(worldCenter, worldSize); + if (debugNamePlateBounds) + { + Debug.Log($"[Cuong] [NPCVisual] smr={renderer.name} localCenter={meshBounds.center} localSize={meshBounds.size} worldCenter={worldCenter} worldSize={worldSize} worldMaxY={currentBounds.max.y}"); + } + + if (!hasAnySkinnedMesh) + { + combinedBounds = currentBounds; + hasAnySkinnedMesh = true; + } + else + { + combinedBounds.Encapsulate(currentBounds); + } + } + + if (!hasAnySkinnedMesh) + { + if (debugNamePlateBounds) + { + Debug.LogWarning("[Cuong] [NPCVisual] TryGetNamePlateAnchorWorld: no valid SkinnedMeshRenderer/sharedMesh."); + } + return false; + } + + worldPos = new Vector3(combinedBounds.center.x, combinedBounds.max.y, combinedBounds.center.z); + if (debugNamePlateBounds) + { + Debug.Log($"[Cuong] [NPCVisual] combinedCenter={combinedBounds.center} combinedSize={combinedBounds.size} anchorWorld={worldPos}"); + } + return true; + } } diff --git a/Assets/PerfectWorld/Scripts/UI/GamePlay/NameplateWorldAnchor.cs b/Assets/PerfectWorld/Scripts/UI/GamePlay/NameplateWorldAnchor.cs new file mode 100644 index 0000000000..16bb2ffc68 --- /dev/null +++ b/Assets/PerfectWorld/Scripts/UI/GamePlay/NameplateWorldAnchor.cs @@ -0,0 +1,125 @@ +using CSNetwork.GPDataType; +using UnityEngine; + +namespace BrewMonster +{ + /// + /// World-space nameplate height / anchor: player (hook → SMR → AABB) and NPC (SMR → root offset). No CHAABB on NPC. + /// 世界坐标名牌高度锚点:玩家(挂点→蒙皮包围盒→AABB)与NPC(蒙皮包围盒→根节点抬高);NPC不走CHAABB。 + /// + [DisallowMultipleComponent] + public class NameplateWorldAnchor : MonoBehaviour + { + [Header("Hosts (optional — filled from parents if empty)")] + [SerializeField] private CECPlayer hostPlayer; + [SerializeField] private CECNPC hostNpc; + [SerializeField] private PlayerVisual playerVisual; + [SerializeField] private NPCVisual npcVisual; + + [Header("Player — skeleton hook")] + [SerializeField] private bool useHeadSkeletonHook = true; + [SerializeField] private string headHookName = "HH_Head"; + [SerializeField] private float headHookWorldOffset; + + [Header("Player — SMR / AABB fallback")] + [SerializeField] private bool preferRendererBounds = true; + [SerializeField] private bool applyMaleHeadWorldOffsetForAabbOnly = true; + [SerializeField] private float maleHeadWorldOffset = 0.1f; + + [Header("NPC — SMR / root fallback")] + [SerializeField] private bool preferVisualBoundsFallback = true; + [SerializeField] private float fallbackHeightAboveRoot = 2f; + + [Header("Shared")] + [SerializeField] private float extraWorldYOffset; + + private void Awake() => CacheRefs(); + + /// + /// Refresh host and visual references (call after hierarchy changes if needed). + /// 刷新宿主与视觉引用(层级变动后可再调)。 + /// + public void CacheRefs() + { + if (hostPlayer == null && hostNpc == null) + { + hostPlayer = GetComponentInParent(); + if (hostPlayer == null) + hostNpc = GetComponentInParent(); + } + + if (playerVisual == null && hostPlayer != null) + playerVisual = hostPlayer.GetComponent(); + + if (npcVisual == null && hostNpc != null) + npcVisual = hostNpc.GetComponent(); + } + + /// + /// Resolve world position for the nameplate canvas root. + /// 解析名牌Canvas根的世界坐标。 + /// + public bool TryGetWorldPosition(out Vector3 worldPos) + { + CacheRefs(); + if (hostPlayer != null) + return TryGetForPlayer(out worldPos); + if (hostNpc != null) + return TryGetForNpc(out worldPos); + worldPos = default; + return false; + } + + private bool TryGetForPlayer(out Vector3 worldPos) + { + worldPos = default; + if (hostPlayer == null) + return false; + + if (useHeadSkeletonHook) + { + var headHook = hostPlayer.GetHook(headHookName); + if (headHook != null) + { + worldPos = headHook.position + Vector3.up * (headHookWorldOffset + extraWorldYOffset); + return true; + } + } + + if (preferRendererBounds && playerVisual != null && playerVisual.TryGetNamePlateAnchorWorld(out worldPos)) + { + worldPos.y += extraWorldYOffset; + return true; + } + + worldPos = new Vector3( + hostPlayer.m_aabb.Center.x, + hostPlayer.m_aabb.Center.y + hostPlayer.m_aabb.Extents.y, + hostPlayer.m_aabb.Center.z); + + if (applyMaleHeadWorldOffsetForAabbOnly && hostPlayer.GetGender() == (int)GENDER.GENDER_MALE) + worldPos.y += maleHeadWorldOffset; + + worldPos.y += extraWorldYOffset; + return true; + } + + private bool TryGetForNpc(out Vector3 worldPos) + { + worldPos = default; + if (hostNpc == null) + return false; + + if (preferVisualBoundsFallback + && npcVisual != null + && npcVisual.TryGetNamePlateAnchorWorld(out worldPos)) + { + worldPos.y += extraWorldYOffset; + return true; + } + + worldPos = hostNpc.transform.position + Vector3.up * (fallbackHeightAboveRoot + extraWorldYOffset); + return true; + } + } +} diff --git a/Assets/PerfectWorld/Scripts/UI/GamePlay/NameplateWorldAnchor.cs.meta b/Assets/PerfectWorld/Scripts/UI/GamePlay/NameplateWorldAnchor.cs.meta new file mode 100644 index 0000000000..494791c2b8 --- /dev/null +++ b/Assets/PerfectWorld/Scripts/UI/GamePlay/NameplateWorldAnchor.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: de86c6e56ad8e224fbd7d825ef43197b \ No newline at end of file diff --git a/Assets/PerfectWorld/Scripts/UI/GamePlay/UINPC.cs b/Assets/PerfectWorld/Scripts/UI/GamePlay/UINPC.cs index 5b68423b5a..42a9785531 100644 --- a/Assets/PerfectWorld/Scripts/UI/GamePlay/UINPC.cs +++ b/Assets/PerfectWorld/Scripts/UI/GamePlay/UINPC.cs @@ -5,7 +5,6 @@ using UnityEngine.UI; namespace BrewMonster { - public enum IconTaskType { QI_NONE = -1, @@ -22,10 +21,11 @@ namespace BrewMonster QI_IN_TYPE2 = 9, // chua biet QI_OUT_TYPE3 = 10, // task daily (nhan) QI_IN_TYPE3 = 11, // task daily (hoan thanh) - QI_OUT_TYPE4 = 12, // task chinh (nhan) + QI_OUT_TYPE4 = 12, // task chinh (nhan) QI_IN_TYPE4 = 13, // task chinh (hoan thanh) } + [RequireComponent(typeof(NameplateWorldAnchor))] public class UINPC : MonoBehaviour { [SerializeField] private TextMeshProUGUI _nameText; @@ -36,10 +36,16 @@ namespace BrewMonster [SerializeField] private GameObject _iconTaskMain; [SerializeField] private List _listIconTask; + [Header("World Nameplate")] + [SerializeField] private Transform _canvasRoot; + + private NameplateWorldAnchor _nameplateAnchor; private Image _cachedIconImage; private void Awake() { + CacheRefs(); + _nameplateAnchor = GetComponent(); if (_iconTaskMain != null) { _cachedIconImage = _iconTaskMain.GetComponent(); @@ -50,6 +56,18 @@ namespace BrewMonster } } + private void LateUpdate() + { + if (_canvasRoot == null) + { + return; + } + if (_nameplateAnchor != null && _nameplateAnchor.TryGetWorldPosition(out var worldPos)) + { + _canvasRoot.position = worldPos; + } + } + // Start is called once before the first execution of Update after the MonoBehaviour is created public void SetName(string name) { @@ -90,6 +108,15 @@ namespace BrewMonster if (_iconTaskMain != null) _iconTaskMain.SetActive(isShow); } + + private void CacheRefs() + { + if (_canvasRoot == null) + { + _canvasRoot = transform; + } + + } } } diff --git a/Assets/PerfectWorld/Scripts/UI/UIPlayer.cs b/Assets/PerfectWorld/Scripts/UI/UIPlayer.cs index 155352d451..1679f4b094 100644 --- a/Assets/PerfectWorld/Scripts/UI/UIPlayer.cs +++ b/Assets/PerfectWorld/Scripts/UI/UIPlayer.cs @@ -1,5 +1,6 @@ using System; using System.Threading; +using BrewMonster; using BrewMonster.Scripts.ChatUI; using CSNetwork.GPDataType; using Cysharp.Threading.Tasks; @@ -9,6 +10,7 @@ using UnityEngine.UI; namespace BrewMonster.PerfectWorld.Scripts.UI { + [RequireComponent(typeof(NameplateWorldAnchor))] public class UIPlayer : MonoBehaviour { [Header("World HUD (optional)")] @@ -19,7 +21,8 @@ namespace BrewMonster.PerfectWorld.Scripts.UI [Header("References")] [SerializeField] private CECPlayer hostplayer; - + [SerializeField] private NameplateWorldAnchor nameplateAnchor; + [SerializeField] private float chatDisplayDuration = 5f; private CancellationTokenSource _chatCts; @@ -33,7 +36,7 @@ namespace BrewMonster.PerfectWorld.Scripts.UI } private void Start() - { + { CacheRefs(); if (hostplayer == null) @@ -49,6 +52,19 @@ namespace BrewMonster.PerfectWorld.Scripts.UI EventBus.SubscribeChannel(hostplayer.m_PlayerInfo.cid, UpdateHostPlayerInfoUI); } + private void LateUpdate() + { + if (!_isVisible || canvasRoot == null) + { + return; + } + + if (nameplateAnchor != null && nameplateAnchor.TryGetWorldPosition(out var worldPos)) + { + canvasRoot.position = worldPos; + } + } + private void OnDestroy() { _chatCts?.Cancel(); @@ -105,6 +121,14 @@ namespace BrewMonster.PerfectWorld.Scripts.UI if (hostplayer == null) hostplayer = GetComponentInParent(); + if (nameplateAnchor == null) + nameplateAnchor = GetComponent(); + + if (nameplateAnchor != null) + { + nameplateAnchor.CacheRefs(); + } + if (canvasRoot == null) { var t = transform.Find("Canvas"); @@ -133,7 +157,7 @@ namespace BrewMonster.PerfectWorld.Scripts.UI for (int i = 0; i < t.childCount; i++) SetLayerRecursively(t.GetChild(i).gameObject, layer); } - + private void SetChatMessage(EventChatMessageOnTopPlayer cxt) { if (chatText == null) @@ -145,7 +169,7 @@ namespace BrewMonster.PerfectWorld.Scripts.UI SetLastSaidWords(cxt.context); }); } - + public void SetLastSaidWords(string message, float duration = -1f) { if (chatText == null || string.IsNullOrEmpty(message)) @@ -162,7 +186,7 @@ namespace BrewMonster.PerfectWorld.Scripts.UI HideChatAsync(time, _chatCts.Token).Forget(); } - + private async UniTaskVoid HideChatAsync(float time, CancellationToken token) { try @@ -192,4 +216,4 @@ namespace BrewMonster.PerfectWorld.Scripts.UI this.context = context; } } -} \ No newline at end of file +}