149 lines
6.5 KiB
C#
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;
|
|
}
|
|
}
|
|
}
|