Merge pull request 'feature/chat_01' (#348) from feature/chat_01 into develop

Reviewed-on: https://git.pthub.vn/Unity/perfect-world-unity/pulls/348
This commit is contained in:
cuongnv
2026-04-16 03:13:15 +00:00
11 changed files with 493 additions and 34 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. Call once at init or every frame if the plate should follow the host.
/// 解析名牌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,7 +56,27 @@ namespace BrewMonster
}
}
// Start is called once before the first execution of Update after the MonoBehaviour is created
private void Start()
{
ApplyInitialCanvasRootPosition();
}
/// <summary>
/// World position for the nameplate root is applied once at startup (no per-frame follow).
/// 名牌根节点世界坐标仅在启动时设置一次(不做每帧跟随)。
/// </summary>
private void ApplyInitialCanvasRootPosition()
{
if (_canvasRoot == null)
{
return;
}
if (_nameplateAnchor != null && _nameplateAnchor.TryGetWorldPosition(out var worldPos))
{
_canvasRoot.position = worldPos;
}
}
public void SetName(string name)
{
_nameText.text = name;
@@ -90,6 +116,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;
}
}
}
}
+31 -4
View File
@@ -273,7 +273,7 @@ RectTransform:
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 0.5, y: 0.5}
m_AnchorMax: {x: 0.5, y: 0.5}
m_AnchoredPosition: {x: -0, y: 1.76}
m_AnchoredPosition: {x: -0, y: 0}
m_SizeDelta: {x: 1, y: 1}
m_Pivot: {x: 0.5, y: 0.5}
--- !u!222 &2934704790758419263
@@ -460,6 +460,7 @@ GameObject:
m_Component:
- component: {fileID: 7528353842011023148}
- component: {fileID: 15783571296627952}
- component: {fileID: 9001000111222333445}
- component: {fileID: 775994425462537197}
m_Layer: 0
m_Name: UIPlayer
@@ -476,8 +477,8 @@ Transform:
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 6780952369966265306}
serializedVersion: 2
m_LocalRotation: {x: 0.026024496, y: 0.5726356, z: -0.0181917, w: 0.8191949}
m_LocalPosition: {x: 0, y: 1, z: 0}
m_LocalRotation: {x: 0.02654451, y: 0.57262397, z: -0.018555203, w: 0.81917816}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children:
@@ -501,6 +502,32 @@ MonoBehaviour:
_healthImage: {fileID: 3715353156977051930}
_iconTaskMain: {fileID: 0}
_listIconTask: []
_canvasRoot: {fileID: 0}
--- !u!114 &9001000111222333445
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 6780952369966265306}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: de86c6e56ad8e224fbd7d825ef43197b, type: 3}
m_Name:
m_EditorClassIdentifier:
hostPlayer: {fileID: 0}
hostNpc: {fileID: 2542060226037108388}
playerVisual: {fileID: 0}
npcVisual: {fileID: -3520322077839857420}
useHeadSkeletonHook: 1
headHookName: HH_Head
headHookWorldOffset: 0
preferRendererBounds: 1
applyMaleHeadWorldOffsetForAabbOnly: 1
maleHeadWorldOffset: 0.1
preferVisualBoundsFallback: 1
fallbackHeightAboveRoot: 2
extraWorldYOffset: 0.3
--- !u!114 &775994425462537197
MonoBehaviour:
m_ObjectHideFlags: 0
@@ -671,7 +698,7 @@ RectTransform:
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 0.5, y: 0.5}
m_AnchorMax: {x: 0.5, y: 0.5}
m_AnchoredPosition: {x: -0, y: 1.51}
m_AnchoredPosition: {x: -0, y: 0}
m_SizeDelta: {x: 1, y: 0.2}
m_Pivot: {x: 0.5, y: 0.5}
--- !u!222 &7187889842481329847
+36 -9
View File
@@ -237,7 +237,7 @@ MonoBehaviour:
m_Name:
m_EditorClassIdentifier:
targetCamera: {fileID: 0}
mode: 1
mode: 0
--- !u!1 &5100466107490290627
GameObject:
m_ObjectHideFlags: 0
@@ -263,16 +263,16 @@ RectTransform:
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 5100466107490290627}
m_LocalRotation: {x: 3.7702125e-16, y: -0.97228837, z: -0.23378475, w: 1.5679955e-15}
m_LocalPosition: {x: 0, y: 0, z: 1.2}
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children: []
m_Father: {fileID: 8006159455096186264}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 0, y: 0}
m_AnchorMax: {x: 1, y: 1}
m_AnchoredPosition: {x: 0, y: 2.36}
m_AnchorMin: {x: 0.5, y: 0.5}
m_AnchorMax: {x: 0.5, y: 0.5}
m_AnchoredPosition: {x: 0, y: 1}
m_SizeDelta: {x: 0.5, y: 0.5}
m_Pivot: {x: 0.5, y: 0.5}
--- !u!222 &397635185778217656
@@ -323,6 +323,7 @@ GameObject:
m_Component:
- component: {fileID: 8745273338113588215}
- component: {fileID: 7336483376496286557}
- component: {fileID: 9001000111222333446}
m_Layer: 0
m_Name: UIPlayer
m_TagString: Untagged
@@ -377,6 +378,32 @@ MonoBehaviour:
- {fileID: 536497386, guid: 75fb3bbb7c9421e4d8bf0728cf2b59b1, type: 3}
- {fileID: -440769636, guid: 75fb3bbb7c9421e4d8bf0728cf2b59b1, type: 3}
- {fileID: 844877792, guid: 75fb3bbb7c9421e4d8bf0728cf2b59b1, type: 3}
_canvasRoot: {fileID: 8006159455096186264}
--- !u!114 &9001000111222333446
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 5709911674741944065}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: de86c6e56ad8e224fbd7d825ef43197b, type: 3}
m_Name:
m_EditorClassIdentifier:
hostPlayer: {fileID: 0}
hostNpc: {fileID: -5899287755522118344}
playerVisual: {fileID: 0}
npcVisual: {fileID: 1882963580244400679}
useHeadSkeletonHook: 1
headHookName: HH_Head
headHookWorldOffset: 0
preferRendererBounds: 1
applyMaleHeadWorldOffsetForAabbOnly: 1
maleHeadWorldOffset: 0.1
preferVisualBoundsFallback: 1
fallbackHeightAboveRoot: 2
extraWorldYOffset: 0.3
--- !u!1 &6510845919681767284
GameObject:
m_ObjectHideFlags: 0
@@ -403,7 +430,7 @@ RectTransform:
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 6510845919681767284}
m_LocalRotation: {x: 0.15080568, y: -0.8027092, z: -0.16719593, w: 0.5522329}
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
@@ -412,7 +439,7 @@ RectTransform:
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 0.5, y: 0.5}
m_AnchorMax: {x: 0.5, y: 0.5}
m_AnchoredPosition: {x: 0, y: 2.11}
m_AnchoredPosition: {x: 0, y: 0}
m_SizeDelta: {x: 1, y: 1}
m_Pivot: {x: 0.5, y: 0.5}
--- !u!222 &4151558405913130980
@@ -521,7 +548,7 @@ MonoBehaviour:
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 6510845919681767284}
m_Enabled: 1
m_Enabled: 0
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 1a05efb08a5fbef42b3e8414040b6c33, type: 3}
m_Name:
@@ -531,6 +531,7 @@ GameObject:
m_Component:
- component: {fileID: 4817336479570982655}
- component: {fileID: 623488520321375200}
- component: {fileID: 9001000111222333448}
- component: {fileID: 7635475614995666769}
m_Layer: 0
m_Name: UIPlayer
@@ -570,6 +571,31 @@ MonoBehaviour:
_nameText: {fileID: 2285106042110813528}
_healthText: {fileID: 7174288820249738816}
_healthImage: {fileID: 5673963502904499965}
--- !u!114 &9001000111222333448
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 7411326708969747318}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: de86c6e56ad8e224fbd7d825ef43197b, type: 3}
m_Name:
m_EditorClassIdentifier:
hostPlayer: {fileID: 0}
hostNpc: {fileID: 0}
playerVisual: {fileID: 0}
npcVisual: {fileID: 0}
useHeadSkeletonHook: 1
headHookName: HH_Head
headHookWorldOffset: 0
preferRendererBounds: 1
applyMaleHeadWorldOffsetForAabbOnly: 1
maleHeadWorldOffset: 0.1
preferVisualBoundsFallback: 1
fallbackHeightAboveRoot: 2
extraWorldYOffset: 0.3
--- !u!114 &7635475614995666769
MonoBehaviour:
m_ObjectHideFlags: 0
+29
View File
@@ -582,6 +582,7 @@ GameObject:
m_Component:
- component: {fileID: 7933090353384311527}
- component: {fileID: 2684574450150702093}
- component: {fileID: 9001000111222333447}
- component: {fileID: 8928742731626340957}
m_Layer: 0
m_Name: UIPlayer
@@ -621,6 +622,34 @@ MonoBehaviour:
_nameText: {fileID: 6491069708694599127}
_healthText: {fileID: 8939523771274757837}
_healthImage: {fileID: 3651088183696393374}
_iconTaskMain: {fileID: 0}
_listIconTask: []
_canvasRoot: {fileID: 0}
--- !u!114 &9001000111222333447
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 6209053004261175930}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: de86c6e56ad8e224fbd7d825ef43197b, type: 3}
m_Name:
m_EditorClassIdentifier:
hostPlayer: {fileID: 0}
hostNpc: {fileID: 4017591853686837057}
playerVisual: {fileID: 0}
npcVisual: {fileID: 1776217013714687322}
useHeadSkeletonHook: 1
headHookName: HH_Head
headHookWorldOffset: 0
preferRendererBounds: 1
applyMaleHeadWorldOffsetForAabbOnly: 1
maleHeadWorldOffset: 0.1
preferVisualBoundsFallback: 1
fallbackHeightAboveRoot: 2
extraWorldYOffset: 0.3
--- !u!114 &8928742731626340957
MonoBehaviour:
m_ObjectHideFlags: 0
+39 -11
View File
@@ -103,7 +103,7 @@ RectTransform:
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 3356314488807061436}
m_LocalRotation: {x: -3.7702125e-16, y: 0.97228837, z: 0.23378475, w: 1.5679955e-15}
m_LocalRotation: {x: 0.02634692, y: 0.5726284, z: -0.018417083, w: 0.81918466}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
@@ -194,7 +194,7 @@ MonoBehaviour:
m_Name:
m_EditorClassIdentifier:
targetCamera: {fileID: 0}
mode: 1
mode: 0
--- !u!1 &3780435110686298972
GameObject:
m_ObjectHideFlags: 0
@@ -236,6 +236,7 @@ GameObject:
m_Component:
- component: {fileID: 8793437051475293945}
- component: {fileID: 2274946704047842688}
- component: {fileID: 9001000111222333444}
m_Layer: 0
m_Name: UIPlayer
m_TagString: Untagged
@@ -273,10 +274,36 @@ MonoBehaviour:
m_EditorClassIdentifier:
healthImage: {fileID: 6923483925198672938}
nameText: {fileID: 0}
canvasRoot: {fileID: 0}
canvasRoot: {fileID: 7804302986034915478}
chatText: {fileID: 5163939573699579047}
hostplayer: {fileID: 0}
nameplateAnchor: {fileID: 9001000111222333444}
chatDisplayDuration: 5
--- !u!114 &9001000111222333444
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 5505736060713067023}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: de86c6e56ad8e224fbd7d825ef43197b, type: 3}
m_Name:
m_EditorClassIdentifier:
hostPlayer: {fileID: 0}
hostNpc: {fileID: 0}
playerVisual: {fileID: 6892195500068497589}
npcVisual: {fileID: 0}
useHeadSkeletonHook: 1
headHookName: HH_Head
headHookWorldOffset: 0
preferRendererBounds: 1
applyMaleHeadWorldOffsetForAabbOnly: 1
maleHeadWorldOffset: 0.1
preferVisualBoundsFallback: 1
fallbackHeightAboveRoot: 2
extraWorldYOffset: 0.3
--- !u!1 &5826062684364525110
GameObject:
m_ObjectHideFlags: 0
@@ -334,8 +361,8 @@ RectTransform:
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 6417286913550183034}
m_LocalRotation: {x: 0.15073968, y: -0.8028954, z: -0.1672268, w: 0.5519708}
m_LocalPosition: {x: 0, y: 0, z: -0.263}
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children: []
@@ -343,7 +370,7 @@ RectTransform:
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 0.5, y: 0.5}
m_AnchorMax: {x: 0.5, y: 0.5}
m_AnchoredPosition: {x: 0.01, y: 2.799}
m_AnchoredPosition: {x: 0, y: 0}
m_SizeDelta: {x: 1, y: 1}
m_Pivot: {x: 0.5, y: 0.5}
--- !u!222 &4235828909678541546
@@ -452,7 +479,7 @@ MonoBehaviour:
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 6417286913550183034}
m_Enabled: 1
m_Enabled: 0
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 1a05efb08a5fbef42b3e8414040b6c33, type: 3}
m_Name:
@@ -541,6 +568,7 @@ MonoBehaviour:
isHit: 0
id: 0
isDebug: 0
debugNamePlateBounds: 0
--- !u!1 &7348274655157863130
GameObject:
m_ObjectHideFlags: 0
@@ -567,8 +595,8 @@ RectTransform:
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 7348274655157863130}
m_LocalRotation: {x: 0.1502115, y: -0.80294836, z: -0.16792805, w: 0.55182487}
m_LocalPosition: {x: 0, y: 0, z: -0.19}
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children: []
@@ -576,7 +604,7 @@ RectTransform:
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 0.5, y: 0.5}
m_AnchorMax: {x: 0.5, y: 0.5}
m_AnchoredPosition: {x: -0, y: 4.205}
m_AnchoredPosition: {x: -0, y: 0}
m_SizeDelta: {x: 1.5, y: 1.5}
m_Pivot: {x: 0.5, y: 0.5}
--- !u!222 &6415805438999997156
@@ -685,7 +713,7 @@ MonoBehaviour:
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 7348274655157863130}
m_Enabled: 1
m_Enabled: 0
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 1a05efb08a5fbef42b3e8414040b6c33, type: 3}
m_Name:
+69
View File
@@ -24,6 +24,7 @@ namespace BrewMonster
[SerializeField] private bool isHit;
[SerializeField] private int id;
[SerializeField] private bool isDebug;
[SerializeField] private bool debugNamePlateBounds;
private bool _eventBusSubscribed;
private const float FadeTime = 100;
@@ -311,5 +312,73 @@ namespace BrewMonster
BMLogger.Log($"PlayerVisual: RefreshNamedAnimancer - Successfully refreshed namedAnimancer from model: {modelRoot?.name ?? "default"}");
}
}
/// <summary>
/// Resolve nameplate 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] [PlayerVisual] 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] [PlayerVisual] TryGetNamePlateAnchorWorld: no valid SkinnedMeshRenderer/sharedMesh.");
}
return false;
}
// Use merged bounds center for X/Z and merged top for Y.
// 使用合并包围盒中心作为X/Z,使用合并包围盒顶部作为Y。
worldPos = new Vector3(combinedBounds.center.x, combinedBounds.max.y, combinedBounds.center.z);
if (debugNamePlateBounds)
{
Debug.Log($"[Cuong] [PlayerVisual] combinedCenter={combinedBounds.center} combinedSize={combinedBounds.size} anchorWorld={worldPos}");
}
return true;
}
}
}