add code height name
This commit is contained in:
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user