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

9.5 KiB
Raw Permalink Blame History

Name plate height: EC_Player::RenderName (PC) vs Unity UIPlayer

This note explains how the vertical anchor for the player name / pate ("名牌") was derived from the original C++ client and what the Unity C# port does instead.

Source locations (PC)

Piece File Approx. lines Role
Screen-space draw & layout CElementClient/EC_Player.cpp RenderName ~59296274 Uses m_PateContent (2D base X/Y, depth Z), scales with fScale, stacks title/name/icons in pixels.
World-space anchor → screen Same file FillPateContent ~63916456 Computes 3D point vPos above the character, projects to screen, fills m_PateContent.

So: height above the character is decided in FillPateContent, not inside the name-drawing math at line 5937. RenderName starts with y = m_PateContent.iCurY - 20*fScale and horizontal centering from iBaseX.


C++: FillPateContent — 3D anchor vPos

Logic summary (in order):

  1. Booth (m_iBoothState == 2 + booth model AABB):
    vPos = m_aabb.Center + Y * boothCHAABB.Extents.y * 1.15f

  2. Chariot (IsInChariot()):
    vPos = m_aabb.Center + Y * dummyModelAABB.Extents.y * 2.f

  3. Default (most cases):
    vPos = m_aabb.Center + g_vAxisY * m_aabb.Extents.y
    → top of the logical player AABB (m_aabb), same idea as "center + full height half-extent" on Y.

  4. Male (GetGender() == GENDER_MALE):
    vPos.y += 0.1f
    Comment in source: 男模型比较高,拉近时名字容易嵌到身体里面 — male model reads taller; when zooming in the name tends to clip into the body, so lift slightly.

  5. Riding pet (IsRidingOnPet() && m_pPetModel):
    Replace with pick AABB top:
    vPos = pickAABB.Center + (0, pickAABB.Extents.y, 0)
    (GetPlayerPickAABB() — skin model AABB when available, else m_aabb.)

  6. Sitting + race RACE_GHOST or RACE_OBORO:
    Extra vPos.y += m_aabb.Extents.y * scaleRatio[race][gender] (table in source).

  7. Flying + RACE_OBORO:
    vPos.y += m_aabb.Extents.y * 0.5f

Then Transform(vPos → vScrPos) (world to screen). If depth out of range, pate is hidden. Otherwise:

  • iBaseX = (int)vScrPos.x
  • iBaseY = (int)vScrPos.y - 10
  • iCurY = iBaseY
  • z = vScrPos.z (used as depth for draw order)

C++: RenderName — 2D layout

  • Uses m_PateContent.iBaseX, iCurY, z.
  • Example: y = int(m_PateContent.iCurY - 20*fScale) then RegisterRender(..., y+2, ..., z).
  • Name/title/team icons are laid out in screen space (centered on iBaseX), not in world space.

Unity C#: UIPlayer + PlayerVisual (Player)

Files

  • Assets/PerfectWorld/Scripts/UI/UIPlayer.csLateUpdate sets canvasRoot.position every frame via NameplateWorldAnchor.TryGetWorldPosition.
  • Assets/Scripts/PlayerVisual.csTryGetNamePlateAnchorWorld — combined SkinnedMeshRenderer bounds top.
    Important: reads sharedMesh.bounds (mesh local space) + TransformPoint + lossyScale to build world bounds manually — NOT renderer.bounds which may be stale right after SetActive(true).

Anchor priority for Player (world space)

  1. Skeleton hook: CECPlayer.GetHook(headHookName) (default "HH_Head") + headHookWorldOffset.
  2. Else if preferRendererBounds and PlayerVisual.TryGetNamePlateAnchorWorld succeeds: top of combined SMR bounds (center.x, bounds.max.y, center.z).
  3. Else: logical AABB topm_aabb.Center + m_aabb.Extents.y on Y (same structural idea as PC default vPos).
  4. If AABB branch + applyMaleHeadWorldOffsetForAabbOnly + male: y += maleHeadWorldOffset (default 0.1f, aligned with PC's +0.1 for male).
  5. Always add extraWorldYOffset for tuning in the Editor.

Billboard / facing camera is intentionally not in UIPlayer; use LookAtCamera on the Canvas / parent, matching the split on PC (world point → then screen/UI drawing).


Unity C#: UINPC + NPCVisual (NPC / Monster)

Files

  • Assets/PerfectWorld/Scripts/UI/GamePlay/UINPC.cs — coroutine retry at init + LateUpdate per frame (after coroutine resolves).
  • Assets/PerfectWorld/Scripts/NPC/NPCVisual.csTryGetNamePlateAnchorWorld (SMR merged bounds) + CacheTopBone / TryGetTopBoneWorld (highest skeleton bone).

Anchor priority for NPC/Monster (world space)

  1. Skeleton hook (useNpcHeadHook = true): hostNpc.GetHook("HH_Head") + headHookWorldOffset — follows animation every frame via LateUpdate. Best for models with a head hook.
  2. Cached top bone: NPCVisual.TryGetTopBoneWorld() — bone with highest world-Y found when model is ready, cached by CacheTopBone(). Follows animation per-frame. Solves floating/flying monsters (e.g. 小星星) that have no HH_Head.
  3. SMR merged bounds: NPCVisual.TryGetNamePlateAnchorWorld() — static rest-pose bounds.
  4. Root fallback: hostNpc.transform.position + up * fallbackHeightAboveRoot.

CacheTopBone flow

UINPC coroutine → SMR/hook ready → _nameplateAnchor.RefreshNpcTopBone()
  → NPCVisual.CacheTopBone()
  → scan all SkinnedMeshRenderer.bones (HashSet dedup)
  → find bone with highest world.y → store as _cachedTopBone
LateUpdate every frame → TryGetTopBoneWorld() → _cachedTopBone.position + extraWorldYOffset

Why LateUpdate was added to UINPC
Without it, canvas position is set once at init (rest-pose height). When animation lifts the body (e.g. 小星星 flying +1.5 units above pivot), the nameplate stays at the init position → overlap. LateUpdate re-reads the cached top bone's current world position every frame → nameplate follows animation.


Unity C#: CECMatter (Item / Mine drop)

File: Assets/PerfectWorld/Scripts/Objet/CECMatter.cs

Matter objects use a self-contained bounds helper — no NameplateWorldAnchor — because they use MeshRenderer (static mesh), not SkinnedMeshRenderer.

Root cause of old bug: textObject.localPosition = new Vector3(0, 0.6f, 0) — fixed offset, wrong for multi-mesh models. Example: 噬人花 has mesh _0 (tall) + mesh _1 (short) → text appeared at small-mesh height, clipped by the tall mesh.

Fix: TryGetCombinedRendererBounds gathers all child Renderer components:

  • MeshRenderer → mesh from MeshFilter.sharedMesh
  • SkinnedMeshRenderersmr.sharedMesh
  • Same sharedMesh.bounds + TransformPoint + lossyScale approach as PlayerVisual / NPCVisual

Anchor: (combinedBounds.center.x, combinedBounds.max.y + 0.05f, combinedBounds.center.z)InverseTransformPointlocalPosition.
Fallback (no renderer): y = 0.6f + Debug.LogWarning [Cuong].

PC equivalent (EC_Matter::RenderName ~908): vPos = aabb.Center + Y * (aabb.Extents.y * 1.3f) — full model AABB, same intent as combined bounds.


Mapping: PC vs Unity

Topic PC (FillPateContent / RenderName) Unity
Coordinate space World point → screen (2D UI pate) World position on canvasRoot (World Space Canvas)
Player default height m_aabb.Center + Y * m_aabb.Extents.y (+ male +0.1) Same AABB top when hook + mesh fail; male +0.1 only on AABB fallback
Player skin / mesh Used indirectly via GetPlayerPickAABB() when riding pet Optional SMR bounds path (PlayerVisual) before falling back to m_aabb
Player skeleton head Not used in FillPateContent HH_Head hook preferred when available
NPC/Monster height EC_NPC::FillPateContentGetCHAABB or AABB Priority: hook → cached top bone → SMR bounds → root offset
NPC follows animation N/A (PC projected each frame) LateUpdate reads top bone per frame
Matter height EC_Matter::RenderName — full model AABB * 1.3 Combined Renderer.bounds (all meshes), max.y + 0.05
Booth / chariot Special vPos branches Not ported
Sitting / Oboro flying Extra Y from race/gender tables Not ported
Male offset Always after default vPos Only on AABB fallback
Visibility Depth test via projected z SetVisible, camera culling, layer
Fine vertical nudge iBaseY = vScrPos.y - 10, y = iCurY - 20*fScale extraWorldYOffset, headHookWorldOffset

Practical notes for designers / programmers

  • Unity places the whole world Canvas at one world point; name/chat/HP offsets are local under canvasRoot (prefab layout).
  • PC stacks multiple pate rows in pixels from iCurY; that is replaced by RectTransform hierarchy + LookAtCamera.
  • If behaviour should match PC exactly for booth/chariot/mount/sitting/flying races, those branches from FillPateContent would need explicit ports.
  • For NPC/Monster with no HH_Head hook, ensure QueueLoadNPCModel triggers RefreshWorldNameplatePosition() so the top-bone scan runs after the first animation frame.

References (code)

  • PC: CElementClient/EC_Player.cppCECPlayer::RenderName (~5929), CECPlayer::FillPateContent (~6391).
  • PC: CElementClient/EC_NPC.cppCECNpc::RenderName, CECNpc::FillPateContent.
  • PC: CElementClient/EC_Matter.cppCECMatter::RenderName (~889).
  • Unity Player: Assets/PerfectWorld/Scripts/UI/UIPlayer.cs, Assets/Scripts/PlayerVisual.cs.
  • Unity NPC/Monster: Assets/PerfectWorld/Scripts/UI/GamePlay/UINPC.cs, Assets/PerfectWorld/Scripts/NPC/NPCVisual.cs, Assets/PerfectWorld/Scripts/UI/GamePlay/NameplateWorldAnchor.cs.
  • Unity Matter: Assets/PerfectWorld/Scripts/Objet/CECMatter.cs.