9.5 KiB
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 ~5929–6274 |
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 ~6391–6456 |
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):
-
Booth (
m_iBoothState == 2+ booth model AABB):
vPos = m_aabb.Center + Y * boothCHAABB.Extents.y * 1.15f -
Chariot (
IsInChariot()):
vPos = m_aabb.Center + Y * dummyModelAABB.Extents.y * 2.f -
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. -
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. -
Riding pet (
IsRidingOnPet() && m_pPetModel):
Replace with pick AABB top:
vPos = pickAABB.Center + (0, pickAABB.Extents.y, 0)
(GetPlayerPickAABB()— skin model AABB when available, elsem_aabb.) -
Sitting + race
RACE_GHOSTorRACE_OBORO:
ExtravPos.y += m_aabb.Extents.y * scaleRatio[race][gender](table in source). -
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.xiBaseY = (int)vScrPos.y - 10iCurY = iBaseYz = 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)thenRegisterRender(..., 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.cs—LateUpdatesetscanvasRoot.positionevery frame viaNameplateWorldAnchor.TryGetWorldPosition.Assets/Scripts/PlayerVisual.cs—TryGetNamePlateAnchorWorld— combinedSkinnedMeshRendererbounds top.
Important: readssharedMesh.bounds(mesh local space) +TransformPoint+lossyScaleto build world bounds manually — NOTrenderer.boundswhich may be stale right afterSetActive(true).
Anchor priority for Player (world space)
- Skeleton hook:
CECPlayer.GetHook(headHookName)(default"HH_Head") +headHookWorldOffset. - Else if
preferRendererBoundsandPlayerVisual.TryGetNamePlateAnchorWorldsucceeds: top of combined SMR bounds(center.x, bounds.max.y, center.z). - Else: logical AABB top —
m_aabb.Center+m_aabb.Extents.yon Y (same structural idea as PC defaultvPos). - If AABB branch +
applyMaleHeadWorldOffsetForAabbOnly+ male:y += maleHeadWorldOffset(default0.1f, aligned with PC's +0.1 for male). - Always add
extraWorldYOffsetfor 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 +LateUpdateper frame (after coroutine resolves).Assets/PerfectWorld/Scripts/NPC/NPCVisual.cs—TryGetNamePlateAnchorWorld(SMR merged bounds) +CacheTopBone/TryGetTopBoneWorld(highest skeleton bone).
Anchor priority for NPC/Monster (world space)
- Skeleton hook (
useNpcHeadHook = true):hostNpc.GetHook("HH_Head")+headHookWorldOffset— follows animation every frame viaLateUpdate. Best for models with a head hook. - Cached top bone:
NPCVisual.TryGetTopBoneWorld()— bone with highest world-Y found when model is ready, cached byCacheTopBone(). Follows animation per-frame. Solves floating/flying monsters (e.g. 小星星) that have noHH_Head. - SMR merged bounds:
NPCVisual.TryGetNamePlateAnchorWorld()— static rest-pose bounds. - 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 fromMeshFilter.sharedMeshSkinnedMeshRenderer→smr.sharedMesh- Same
sharedMesh.bounds + TransformPoint + lossyScaleapproach asPlayerVisual/NPCVisual
Anchor: (combinedBounds.center.x, combinedBounds.max.y + 0.05f, combinedBounds.center.z) → InverseTransformPoint → localPosition.
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::FillPateContent — GetCHAABB 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
FillPateContentwould need explicit ports. - For NPC/Monster with no
HH_Headhook, ensureQueueLoadNPCModeltriggersRefreshWorldNameplatePosition()so the top-bone scan runs after the first animation frame.
References (code)
- PC:
CElementClient/EC_Player.cpp—CECPlayer::RenderName(~5929),CECPlayer::FillPateContent(~6391). - PC:
CElementClient/EC_NPC.cpp—CECNpc::RenderName,CECNpc::FillPateContent. - PC:
CElementClient/EC_Matter.cpp—CECMatter::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.