Files
test/Docs/name-plate-height/Unity_WorldNameplate_Summary.md
T
2026-05-25 10:33:40 +07:00

8.3 KiB

Tóm tắt nameplate world-space — các file liên quan

Tài liệu tóm tắt toàn bộ công việc cho neo tên / HUD trên đầu (world-space Canvas), tập trung vào NameplateWorldAnchor (MonoBehaviour) làm một chỗ chỉnh độ cao / neo 3D.


Danh sách file chính

# Đường dẫn Vai trò
1 Assets/PerfectWorld/Scripts/UI/GamePlay/NameplateWorldAnchor.cs MonoBehaviour: toàn bộ logic neo 3D (player + NPC/Monster); TryGetWorldPosition, CacheRefs, RefreshNpcTopBone.
2 Assets/PerfectWorld/Scripts/UI/UIPlayer.cs Player UI: LateUpdate gán canvasRoot.position mỗi frame; gọi NameplateWorldAnchor.TryGetWorldPosition. [RequireComponent(typeof(NameplateWorldAnchor))].
3 Assets/PerfectWorld/Scripts/UI/GamePlay/UINPC.cs NPC/Monster UI: coroutine retry ở Start + LateUpdate mỗi frame sau khi coroutine hoàn tất; gọi NameplateWorldAnchor.TryGetWorldPosition. [RequireComponent(typeof(NameplateWorldAnchor))].
4 Assets/Scripts/PlayerVisual.cs Player: TryGetNamePlateAnchorWorld — gộp bounds SkinnedMeshRenderer (dùng sharedMesh.bounds).
5 Assets/PerfectWorld/Scripts/NPC/NPCVisual.cs NPC/Monster: TryGetNamePlateAnchorWorld (SMR gộp) + CacheTopBone / TryGetTopBoneWorld (bone cao nhất).
6 Assets/PerfectWorld/Scripts/Objet/CECMatter.cs Matter (item/mine): neo tên bằng TryGetCombinedRendererBounds (gộp mọi Renderer). Không dùng NameplateWorldAnchor.

Prefab: GameObject "UIPlayer" bên trong PlayerPrefab, MonsterPrefab, NPCServer.prefab đã có cả UIPlayer.cs / UINPC.cs lẫn NameplateWorldAnchor được serialize sẵn.


1. NameplateWorldAnchor.cs (MonoBehaviour)

Gắn trên object chứa UIPlayer hoặc UINPC (cùng transform hoặc cha).

  • CacheRefs: tự tìm CECPlayer / CECNPC / PlayerVisual / NPCVisual từ hierarchy nếu chưa gán.
  • RefreshNpcTopBone: gọi npcVisual.CacheTopBone() — trigger scan xương sau khi model sẵn sàng.
  • TryGetWorldPosition(out Vector3 worldPos):
    • CECPlayer → nhánh player:
      1. Hook xương HH_Head (useHeadSkeletonHook, headHookName, headHookWorldOffset).
      2. PlayerVisual.TryGetNamePlateAnchorWorld (SMR bounds).
      3. Fallback đỉnh m_aabb + male offset + extraWorldYOffset.
    • CECNPC → nhánh NPC/Monster (ưu tiên theo thứ tự):
      1. Hook xương (useNpcHeadHook): hostNpc.GetHook("HH_Head") — theo animation mỗi frame.
      2. Bone cao nhất (NPCVisual.TryGetTopBoneWorld): bone đã cache từ CacheTopBone. Dùng khi không có HH_Head, giải quyết NPC/Monster bay/nổi (vd: 小星星).
      3. SMR merged bounds (NPCVisual.TryGetNamePlateAnchorWorld): rest-pose bounds.
      4. Root fallback: hostNpc.transform.position + up * fallbackHeightAboveRoot.
      • Luôn cộng extraWorldYOffset.

→ Một class duy nhất chỉnh độ cao / neo; UI chỉ đọc điểm world và đặt Canvas.


2. UIPlayer.cs

  • Giữ reference NameplateWorldAnchor (auto-find nếu null).
  • LateUpdate mỗi frame: nameplateAnchor.TryGetWorldPositioncanvasRoot.position.
  • Các field hook / SMR / AABB / offset đã chuyển sang NameplateWorldAnchor trên cùng prefab.

3. UINPC.cs

  • Giữ _canvasRoot cho world HUD; mọi offset / fallback NPC nằm trên NameplateWorldAnchor.
  • LateUpdate mỗi frame (thêm mới): chỉ chạy sau khi _initialNameplateRoutine == null (coroutine kết thúc); gọi TryGetWorldPositionApplyCanvasRootLocalPosition.
    → Cho phép nameplate theo animation (vd: xương đầu di chuyển khi nhân vật bay/nổi).
  • Start() dùng coroutine defer/retry để chờ model/SMR load xong rồi set vị trí lần đầu.
  • Khi model load xong từ CECNPC.QueueLoadNPCModel, gọi RefreshWorldNameplatePosition() để restart coroutine.
  • Khi coroutine xác nhận model sẵn sàng (SMR/hook/top-bone ready), gọi thêm _nameplateAnchor.RefreshNpcTopBone() để cache xương cao nhất.
  • Set theo local position (convert từ world bằng parent.InverseTransformPoint).

4. PlayerVisual.cs

  • TryGetNamePlateAnchorWorld: gộp bounds tất cả SkinnedMeshRenderer con.
  • Đọc sharedMesh.bounds (mesh local space) + TransformPoint(center) + lossyScale → world bounds thủ công.
    Không dùng renderer.bounds (có thể stale ngay sau SetActive(true) trong cùng frame).
  • Kết quả: worldPos = (combinedBounds.center.x, combinedBounds.max.y, combinedBounds.center.z).
  • debugNamePlateBounds log tiền tố [Cuong].

5. NPCVisual.cs

  • TryGetNamePlateAnchorWorld: cùng thuật toán SMR gộp như PlayerVisual (dùng sharedMesh.bounds).
  • CacheTopBone() (thêm mới):
    • Lấy tất cả SkinnedMeshRenderer con → duyệt smr.bones (dedup bằng HashSet<Transform>).
    • Tìm bone có world.y cao nhất → lưu vào _cachedTopBone.
    • Log [Cuong] [NPCVisual] CacheTopBone khi debugNamePlateBounds = true.
  • TryGetTopBoneWorld(out Vector3) (thêm mới): trả _cachedTopBone.position; false nếu chưa cache.
  • debugNamePlateBounds log tiền tố [Cuong].

6. CECMatter.cs

Nameplate cho item/mine drop. Không dùng NameplateWorldAnchor (Matter là mesh tĩnh, không có skeleton).

  • TryGetCombinedRendererBounds(matterRoot, excludeSubtree):
    • Duyệt tất cả Renderer con (gồm cả MeshRendererSkinnedMeshRenderer).
    • Với MeshRenderer: lấy mesh từ MeshFilter.sharedMesh.
    • Với SkinnedMeshRenderer: lấy smr.sharedMesh.
    • Cùng cách tính world bounds như PlayerVisual / NPCVisual.
  • TryGetItemNameAnchorLocal: gọi TryGetCombinedRendererBounds → anchor tại (center.x, max.y + 0.05, center.z)InverseTransformPointlocalPosition.
  • Fallback nếu không có renderer: y = 0.6f + LogWarning [Cuong].
  • BoxCollider khi spawn cũng dùng combinedBounds.size + combinedBounds.center (thay vì chỉ lấy renderer đầu tiên).

Cập nhật phiên hiện tại (2026-05)

CECMatter — fix multi-mesh nameplate height

  • Vấn đề: offset cố định 0.6f sai với model nhiều mesh (vd: 噬人花 có mesh _0 cao + _1 thấp → tên đè lên cây thấp).
  • Nguyên nhân sâu hơn: renderer.bounds stale sau SetActive(true) cùng frame.
  • Fix: TryGetCombinedRendererBounds đọc sharedMesh.bounds + transform thủ công, gộp mọi Renderer (cả MeshRenderer). Anchor tại combinedBounds.max.y.

NameplateWorldAnchor — thêm NPC hook + cached top bone

  • Thêm Priority 1 NPC: hostNpc.GetHook("HH_Head") — dùng chung headHookName với player.
  • Thêm Priority 2 NPC: NPCVisual.TryGetTopBoneWorld() (bone cao nhất đã cache).
  • Thêm RefreshNpcTopBone() để trigger scan từ UINPC.
  • Field mới: useNpcHeadHook (default true).

NPCVisual — thêm CacheTopBone

  • CacheTopBone(): scan tất cả SMR bones, dedup HashSet, lưu bone world.y cao nhất.
  • TryGetTopBoneWorld(): trả vị trí bone đã cache.

UINPC — thêm LateUpdate

  • Vấn đề: 小星星 và các monster có animation nổi/bay bị đè tên vì canvas set vị trí một lần từ rest-pose.
  • Fix: LateUpdate re-read TryGetWorldPosition mỗi frame sau khi coroutine kết thúc → nameplate theo bone animation.
  • Coroutine gọi RefreshNpcTopBone() ở cả early-exit lẫn timeout để đảm bảo top bone được cache.

Đối chiếu nhanh với PC (tham khảo)

  • PC: FillPateContent quyết định điểm 3D; RenderName chỉ layout pixel.
  • Unity world Canvas: bắt chước bước neo 3D; player có hook + SMR + AABB; NPC/Monster có hook + top bone + SMR + root fallback.

Chi tiết player & matter PC vs Unity: EC_Player_RenderName_vs_UIPlayer.md.


PC gốc (tham khảo)

  • CElementClient/EC_Player.cppFillPateContent, RenderName
  • CElementClient/EC_NPC.cppFillPateContent, RenderName
  • CElementClient/EC_Matter.cppRenderName (~889): vPos = modelAABB.Center + Y * (Extents.y * 1.3f)