Files
test/Assets/PerfectWorld/Scripts/UI/GamePlay/NameplateWorldAnchor.cs
T
vuong dinh hoang 284a77e4d2 remove debug
2026-04-21 11:43:07 +07:00

149 lines
6.5 KiB
C#

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. Call once at init or every frame if the plate should follow the host.
/// 解析名牌Canvas根的世界坐标。仅在初始化时调用一次,或若名牌需跟随宿主则每帧调用。
/// </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, out npcUsedSkinnedMeshMergedBounds, logNpcRootFallback);
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, out bool usedSkinnedMeshMergedBounds, bool logNpcRootFallback)
{
worldPos = default;
usedSkinnedMeshMergedBounds = false;
if (hostNpc == null)
return false;
if (preferVisualBoundsFallback
&& 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;
}
}
}