add code height name

This commit is contained in:
CuongNV
2026-04-15 18:38:20 +07:00
parent 8e9360180f
commit 8c72c3349c
5 changed files with 254 additions and 9 deletions
+68 -1
View File
@@ -11,6 +11,7 @@ public class NPCVisual : MonoBehaviour
protected CECNPC.INFO m_NPCInfo;
[SerializeField] private Queue<string> _animationQueue = new Queue<string>();
[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<NamedAnimancerComponent>();
}
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"}");
}
}
/// <summary>
/// Resolve world anchor from merged bounds of all SkinnedMeshRenderers.
/// 从所有SkinnedMeshRenderer合并后的包围盒解析世界锚点。
/// </summary>
public bool TryGetNamePlateAnchorWorld(out Vector3 worldPos)
{
worldPos = default;
var skinnedMeshRenderers = GetComponentsInChildren<SkinnedMeshRenderer>(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;
}
}
@@ -0,0 +1,125 @@
using CSNetwork.GPDataType;
using UnityEngine;
namespace BrewMonster
{
/// <summary>
/// World-space nameplate height / anchor: player (hook → SMR → AABB) and NPC (SMR → root offset). No CHAABB on NPC.
/// 世界坐标名牌高度锚点:玩家(挂点→蒙皮包围盒→AABB)与NPC(蒙皮包围盒→根节点抬高);NPC不走CHAABB。
/// </summary>
[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();
/// <summary>
/// Refresh host and visual references (call after hierarchy changes if needed).
/// 刷新宿主与视觉引用(层级变动后可再调)。
/// </summary>
public void CacheRefs()
{
if (hostPlayer == null && hostNpc == null)
{
hostPlayer = GetComponentInParent<CECPlayer>();
if (hostPlayer == null)
hostNpc = GetComponentInParent<CECNPC>();
}
if (playerVisual == null && hostPlayer != null)
playerVisual = hostPlayer.GetComponent<PlayerVisual>();
if (npcVisual == null && hostNpc != null)
npcVisual = hostNpc.GetComponent<NPCVisual>();
}
/// <summary>
/// Resolve world position for the nameplate canvas root.
/// 解析名牌Canvas根的世界坐标。
/// </summary>
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;
}
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: de86c6e56ad8e224fbd7d825ef43197b
@@ -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<Sprite> _listIconTask;
[Header("World Nameplate")]
[SerializeField] private Transform _canvasRoot;
private NameplateWorldAnchor _nameplateAnchor;
private Image _cachedIconImage;
private void Awake()
{
CacheRefs();
_nameplateAnchor = GetComponent<NameplateWorldAnchor>();
if (_iconTaskMain != null)
{
_cachedIconImage = _iconTaskMain.GetComponent<Image>();
@@ -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;
}
}
}
}
+30 -6
View File
@@ -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<cmd_self_info_00>(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<CECPlayer>();
if (nameplateAnchor == null)
nameplateAnchor = GetComponent<NameplateWorldAnchor>();
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;
}
}
}
}