diff --git a/Assets/PerfectWorld/Scripts/Objet/CECMatter.cs b/Assets/PerfectWorld/Scripts/Objet/CECMatter.cs index 5776c5bd88..1a12a48c1a 100644 --- a/Assets/PerfectWorld/Scripts/Objet/CECMatter.cs +++ b/Assets/PerfectWorld/Scripts/Objet/CECMatter.cs @@ -37,6 +37,10 @@ namespace PerfectWorld.Scripts public const uint MATTER_MONEY = 3; public const uint MATTER_TYPEMASK = 0xff; + private const float MatterNameExtraWorldYOffset = 0.05f; + private const float MatterNameFallbackLocalY = 0.6f; + private const string ItemNameTextChildName = "ItemNameText"; + // Constructor / Constructor public CECMatter() { @@ -179,11 +183,24 @@ namespace PerfectWorld.Scripts var collider = matterObject.AddComponent(); //this is a workaround to fix the collider size issue when load prefab go wrong at some point //TODO: remove this workaround after the prefab load issue is fixed - Vector3 size = matterObject.GetComponentInChildren().bounds.size; - if (size.x < 0.5f) size.x = 0.5f; - if (size.y < 0.5f) size.y = 0.5f; - if (size.z < 0.5f) size.z = 0.5f; - collider.size = size; + if (TryGetCombinedRendererBounds(matterObject.transform, null, out var combinedBounds)) + { + Vector3 size = combinedBounds.size; + if (size.x < 0.5f) size.x = 0.5f; + if (size.y < 0.5f) size.y = 0.5f; + if (size.z < 0.5f) size.z = 0.5f; + collider.size = size; + collider.center = matterObject.transform.InverseTransformPoint(combinedBounds.center); + } + else + { + var firstRenderer = matterObject.GetComponentInChildren(); + Vector3 size = firstRenderer != null ? firstRenderer.bounds.size : Vector3.one; + if (size.x < 0.5f) size.x = 0.5f; + if (size.y < 0.5f) size.y = 0.5f; + if (size.z < 0.5f) size.z = 0.5f; + collider.size = size; + } } // Create text object to display item name above the cube CreateItemNameText(matterObject, Info.tid); @@ -244,18 +261,109 @@ namespace PerfectWorld.Scripts return null; } + /// + /// Merge world-space bounds of all child Renderers (MeshRenderer + SkinnedMeshRenderer). + /// Reads sharedMesh.bounds (mesh local space) and manually converts to world space — + /// same approach as PlayerVisual/NPCVisual — to avoid stale renderer.bounds after SetActive. + /// 合并所有子 Renderer 的世界包围盒(MeshRenderer + SkinnedMeshRenderer)。 + /// 直接读 sharedMesh.bounds(网格本地空间)再手动转为世界坐标,避免 SetActive 后同帧 renderer.bounds 未刷新的问题。 + /// + private static bool TryGetCombinedRendererBounds(Transform matterRoot, Transform excludeSubtree, out Bounds combinedBounds) + { + combinedBounds = default; + if (matterRoot == null) + return false; + + var renderers = matterRoot.GetComponentsInChildren(true); + bool hasAny = false; + for (int i = 0; i < renderers.Length; i++) + { + var renderer = renderers[i]; + if (renderer == null) + continue; + if (excludeSubtree != null && renderer.transform.IsChildOf(excludeSubtree)) + continue; + + Mesh mesh = null; + if (renderer is SkinnedMeshRenderer smr) + { + mesh = smr.sharedMesh; + } + else if (renderer is MeshRenderer) + { + var mf = renderer.GetComponent(); + if (mf != null) + mesh = mf.sharedMesh; + } + + if (mesh == null) + continue; + + // Manually build world-space bounds from mesh-local bounds + transform, + // identical to PlayerVisual/NPCVisual — reliable even right after SetActive(true). + // 与 PlayerVisual/NPCVisual 相同:从网格本地包围盒手动计算世界包围盒,SetActive 后同帧可靠。 + var meshBounds = mesh.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); + + Debug.Log($"[Cuong] [CECMatter] renderer={renderer.name} meshLocalCenter={meshBounds.center} meshLocalSize={meshBounds.size} worldCenter={worldCenter} worldSize={worldSize} worldMaxY={currentBounds.max.y}"); + + if (!hasAny) + { + combinedBounds = currentBounds; + hasAny = true; + } + else + { + combinedBounds.Encapsulate(currentBounds); + } + } + + return hasAny; + } + + /// + /// Name anchor at top of tallest mesh: combinedBounds.max.y in world, converted to local. + /// 名牌锚点位于最高 mesh 顶部:世界坐标 combinedBounds.max.y,再转为本地坐标。 + /// + private static bool TryGetItemNameAnchorLocal(Transform matterRoot, out Vector3 localAnchor) + { + if (!TryGetCombinedRendererBounds(matterRoot, null, out var combinedBounds)) + { + localAnchor = new Vector3(0f, MatterNameFallbackLocalY, 0f); + return false; + } + + var worldAnchor = new Vector3( + combinedBounds.center.x, + combinedBounds.max.y + MatterNameExtraWorldYOffset, + combinedBounds.center.z); + localAnchor = matterRoot.InverseTransformPoint(worldAnchor); + return true; + } + private static void CreateItemNameText(GameObject matterObject, int tid) { if (matterObject == null) return; // Avoid duplicating if prefab already contains it (or Init called twice). - if (matterObject.transform.Find("ItemNameText") != null) + if (matterObject.transform.Find(ItemNameTextChildName) != null) return; - var textObject = new GameObject("ItemNameText"); + var textObject = new GameObject(ItemNameTextChildName); textObject.transform.SetParent(matterObject.transform, false); - textObject.transform.localPosition = new Vector3(0f, 0.6f, 0f); + if (!TryGetItemNameAnchorLocal(matterObject.transform, out var localAnchor)) + { + Debug.LogWarning( + $"[Cuong] [CECMatter] No renderer bounds for '{matterObject.name}'; using fallback Y={MatterNameFallbackLocalY}"); + } + textObject.transform.localPosition = localAnchor; var textMesh = textObject.AddComponent();