快速查找表
| 需求场景 | 使用方法 | 关键API | 注意事项 |
|---|---|---|---|
| 3D物体→屏幕位置 | WorldToScreenPoint | Camera.WorldToScreenPoint() |
检查Z值是否>0 |
| 屏幕点击→3D位置 | 射线检测(推荐) | Camera.ScreenPointToRay() + Physics.Raycast() |
最精确的方法 |
| 屏幕坐标→3D坐标 | ScreenToWorldPoint | Camera.ScreenToWorldPoint() |
必须指定Z深度 |
| UI跟随3D物体 | WorldToScreen + ScreenToLocal | WorldToScreenPoint() + ScreenPointToLocalPointInRectangle() |
使用LateUpdate |
| 3D物体跟随UI | UI坐标→屏幕→3D | WorldToScreenPoint() + ScreenToWorldPoint() |
指定深度值 |
| 判断物体是否可见 | ViewportPoint检查 | Camera.WorldToViewportPoint() |
x,y在0-1且z>0 |
| UI点击检测 | EventSystem | EventSystem.current.IsPointerOverGameObject() |
防止穿透到3D |
坐标系统概述
Unity中存在多个坐标系统:
世界坐标 (World Space) → 3D场景中的绝对坐标
局部坐标 (Local Space) → 相对于父物体的坐标
屏幕坐标 (Screen Space) → 像素坐标,左下角(0,0),右上角(Screen.width, Screen.height)
视口坐标 (Viewport Space) → 归一化坐标,左下角(0,0),右上角(1,1)
UI坐标 (UI Space) → Canvas下的RectTransform本地坐标
坐标系统详解:
-
世界坐标 (World Space)
Transform.position- 物体在场景中的绝对位置- 不受父物体影响
- 用于物理计算、射线检测
-
局部坐标 (Local Space)
Transform.localPosition- 相对于父物体的位置- 受父物体变换影响(位置、旋转、缩放)
- UI元素通常使用局部坐标
-
屏幕坐标 (Screen Space)
- 以像素为单位,原点在左下角
Input.mousePosition返回屏幕坐标- 不同分辨率下数值不同
-
视口坐标 (Viewport Space)
- 归一化坐标系,范围0-1
- (0,0)=左下角,(1,1)=右上角
- 与分辨率无关,便于判断可见性
-
RectTransform坐标
anchoredPosition- 相对于锚点的位置localPosition- 相对于父节点的位置pivot- 自身旋转缩放中心点anchor- 相对父节点的锚定位置
坐标系统转换关系图
World Space
Vector3] S[屏幕坐标
Screen Space
像素 0,0 到 width,height] V[视口坐标
Viewport Space
归一化 0,0 到 1,1] U[UI坐标
Canvas/UI Space
RectTransform] R[射线
Ray] W -->|WorldToScreenPoint| S S -->|ScreenToWorldPoint
⚠️需要Z深度| W W -->|WorldToViewportPoint| V V -->|ViewportToWorldPoint
⚠️需要Z深度| W S -->|ScreenPointToRay| R R -->|Raycast| W S -->|ScreenPointToLocalPointInRectangle
需传入Canvas| U U -->|RectTransformUtility
WorldToScreenPoint| S style W fill:#e1f5e1 style S fill:#e1e5f5 style V fill:#f5e1f5 style U fill:#f5f5e1 style R fill:#ffe1e1 end
转换要点:
- 🔴 红色警告:屏幕坐标↔世界坐标转换时必须指定Z深度
- 🟢 最佳实践:使用射线检测(Ray)获取精确3D坐标
- 🔵 Canvas相关:不同RenderMode需要传入不同的Camera参数
RectTransform坐标详解
RectTransform是UI元素的核心组件,理解其坐标系统对UI开发至关重要。
关键属性
csharp
// 1. anchoredPosition - 相对于锚点的位置
rectTransform.anchoredPosition = new Vector2(100, 50);
// 2. localPosition - 相对于父节点的位置(考虑pivot)
rectTransform.localPosition = new Vector3(100, 50, 0);
// 3. position - 世界坐标位置
rectTransform.position = new Vector3(500, 300, 0);
// 4. anchorMin/anchorMax - 锚点(相对于父节点的比例)
rectTransform.anchorMin = new Vector2(0, 0); // 左下角
rectTransform.anchorMax = new Vector2(1, 1); // 右上角
// 5. pivot - 自身旋转缩放中心(0-1)
rectTransform.pivot = new Vector2(0.5f, 0.5f); // 中心点
// 6. sizeDelta - 相对于锚点的尺寸
rectTransform.sizeDelta = new Vector2(200, 100);
常用锚点预设
csharp
// 中心锚点
anchorMin = anchorMax = new Vector2(0.5f, 0.5f);
// 左上角
anchorMin = anchorMax = new Vector2(0, 1);
// 右下角
anchorMin = anchorMax = new Vector2(1, 0);
// 拉伸填充父节点
anchorMin = new Vector2(0, 0);
anchorMax = new Vector2(1, 1);
sizeDelta = Vector2.zero;
坐标转换示例
csharp
using UnityEngine;
public class RectTransformHelper : MonoBehaviour
{
// 世界坐标转RectTransform本地坐标
public static Vector2 WorldToRectTransformLocal(RectTransform rectTransform, Vector3 worldPos, Camera camera)
{
Vector2 screenPos = camera.WorldToScreenPoint(worldPos);
Vector2 localPos;
RectTransformUtility.ScreenPointToLocalPointInRectangle(
rectTransform,
screenPos,
camera,
out localPos
);
return localPos;
}
// RectTransform本地坐标转世界坐标
public static Vector3 RectTransformLocalToWorld(RectTransform rectTransform, Vector2 localPos)
{
// 使用TransformPoint将本地坐标转换为世界坐标
return rectTransform.TransformPoint(localPos);
}
}
一、3D对象 → 屏幕坐标
1.1 WorldToScreenPoint
将世界坐标转换为屏幕像素坐标。
csharp
public class WorldToScreenExample : MonoBehaviour
{
public Transform target3D;
public Camera mainCamera;
void Update()
{
// 获取3D对象的屏幕坐标
Vector3 screenPos = mainCamera.WorldToScreenPoint(target3D.position);
Debug.Log($"屏幕坐标: X={screenPos.x}, Y={screenPos.y}, Z={screenPos.z}");
// Z值表示距离相机的深度,负值表示在相机后方
}
}
关键点:
Z值 > 0:对象在相机前方Z值 < 0:对象在相机后方(屏幕外)- 坐标系原点在屏幕左下角
1.2 WorldToViewportPoint
转换为归一化视口坐标(0-1范围)。
csharp
Vector3 viewportPos = mainCamera.WorldToViewportPoint(target3D.position);
// 判断是否在屏幕内
bool isVisible = viewportPos.x >= 0 && viewportPos.x <= 1 &&
viewportPos.y >= 0 && viewportPos.y <= 1 &&
viewportPos.z > 0;
二、屏幕坐标 → 3D世界坐标
2.1 ScreenToWorldPoint
将屏幕坐标转换为世界坐标,必须指定Z深度。
csharp
public class ScreenToWorldExample : MonoBehaviour
{
public Camera mainCamera;
void Update()
{
if (Input.GetMouseButtonDown(0))
{
Vector3 mousePos = Input.mousePosition;
// 方法1:指定距离相机的深度
mousePos.z = 10f; // 距离相机10单位
Vector3 worldPos = mainCamera.ScreenToWorldPoint(mousePos);
// 方法2:基于某个3D对象的深度
Vector3 targetScreenPos = mainCamera.WorldToScreenPoint(transform.position);
mousePos.z = targetScreenPos.z;
Vector3 worldPosAtTargetDepth = mainCamera.ScreenToWorldPoint(mousePos);
Debug.Log($"世界坐标: {worldPos}");
}
}
}
2.2 射线检测法(推荐)
使用射线获取精确的3D位置。
csharp
void Update()
{
if (Input.GetMouseButtonDown(0))
{
Ray ray = mainCamera.ScreenPointToRay(Input.mousePosition);
// 检测射线碰撞
if (Physics.Raycast(ray, out RaycastHit hit))
{
Vector3 hitPoint = hit.point;
Debug.Log($"点击位置: {hitPoint}");
// 在点击位置生成物体
Instantiate(prefab, hitPoint, Quaternion.identity);
}
// 或者指定平面检测
Plane groundPlane = new Plane(Vector3.up, Vector3.zero);
if (groundPlane.Raycast(ray, out float distance))
{
Vector3 hitPoint = ray.GetPoint(distance);
Debug.Log($"地面点击位置: {hitPoint}");
}
}
}
三、UI坐标 ↔ 世界坐标
3.1 UI跟随3D对象
让UI元素(血条、名称)跟随3D角色移动。
csharp
public class UIFollowWorld : MonoBehaviour
{
public Transform target3D; // 3D角色
public RectTransform uiElement; // UI元素
public Canvas canvas;
public Camera mainCamera;
public Vector3 offset = Vector3.up * 2f; // 偏移量
void LateUpdate()
{
// 计算3D对象的屏幕坐标
Vector3 screenPos = mainCamera.WorldToScreenPoint(target3D.position + offset);
// 判断是否在相机前方
if (screenPos.z > 0)
{
// 转换为Canvas坐标
Vector2 canvasPos;
RectTransformUtility.ScreenPointToLocalPointInRectangle(
canvas.transform as RectTransform,
new Vector2(screenPos.x, screenPos.y),
canvas.worldCamera,
out canvasPos
);
uiElement.anchoredPosition = canvasPos;
uiElement.gameObject.SetActive(true);
}
else
{
// 在相机后方,隐藏UI
uiElement.gameObject.SetActive(false);
}
}
}
3.2 3D对象跟随UI
让3D对象(预览模型)跟随UI拖拽。
csharp
public class WorldFollowUI : MonoBehaviour
{
public RectTransform uiElement;
public Transform object3D;
public Canvas canvas;
public Camera mainCamera;
public float depth = 5f; // 3D对象距离相机的深度
void Update()
{
// 获取UI元素的屏幕坐标
Vector2 screenPos = RectTransformUtility.WorldToScreenPoint(
canvas.worldCamera,
uiElement.position
);
// 转换为世界坐标
Vector3 worldPos = mainCamera.ScreenToWorldPoint(
new Vector3(screenPos.x, screenPos.y, depth)
);
object3D.position = worldPos;
}
}
3.3 UI元素点击检测转3D
在UI上点击,在3D场景中执行操作。
csharp
using UnityEngine;
using UnityEngine.EventSystems;
public class UIClick3DAction : MonoBehaviour, IPointerClickHandler
{
public Camera mainCamera;
public float rayDistance = 100f;
public void OnPointerClick(PointerEventData eventData)
{
// 从UI点击位置发射射线
Ray ray = mainCamera.ScreenPointToRay(eventData.position);
if (Physics.Raycast(ray, out RaycastHit hit, rayDistance))
{
// 选中3D对象
Debug.Log($"选中: {hit.collider.gameObject.name}");
// 执行操作
var selectable = hit.collider.GetComponent<ISelectable>();
selectable?.OnSelected();
}
}
}
3.4 多相机场景处理
在复杂项目中,可能存在多个相机(主相机、UI相机、小地图相机等),需要正确处理坐标转换。
csharp
using UnityEngine;
using System.Linq;
public class MultiCameraCoordinate : MonoBehaviour
{
public Camera mainCamera; // 主相机
public Camera uiCamera; // UI相机
public Camera minimapCamera; // 小地图相机
// 获取鼠标位置对应的相机
public Camera GetCameraUnderMouse(Vector2 mousePos)
{
// 按深度从大到小排序(深度越大越后渲染,优先级越高)
Camera[] cameras = Camera.allCameras
.OrderByDescending(c => c.depth)
.ToArray();
foreach (var cam in cameras)
{
// 检查相机是否启用
if (!cam.enabled) continue;
// 检查鼠标是否在相机视口内
Rect pixelRect = cam.pixelRect;
if (pixelRect.Contains(mousePos))
{
return cam;
}
}
return Camera.main; // 默认返回主相机
}
// 示例:在正确的相机中进行射线检测
void HandleClick()
{
if (Input.GetMouseButtonDown(0))
{
Vector2 mousePos = Input.mousePosition;
Camera targetCamera = GetCameraUnderMouse(mousePos);
Ray ray = targetCamera.ScreenPointToRay(mousePos);
if (Physics.Raycast(ray, out RaycastHit hit))
{
Debug.Log($"使用相机: {targetCamera.name}, 击中: {hit.collider.name}");
}
}
}
}
多相机UI坐标转换:
csharp
public class MultiCameraUIFollow : MonoBehaviour
{
public Transform target3D;
public RectTransform uiElement;
public Canvas canvas;
public Camera worldCamera; // 3D世界相机
public Camera uiCamera; // UI相机(如果Canvas使用Camera模式)
void LateUpdate()
{
// Step 1: 使用世界相机将3D坐标转为屏幕坐标
Vector3 screenPos = worldCamera.WorldToScreenPoint(target3D.position);
if (screenPos.z > 0)
{
// Step 2: 使用UI相机将屏幕坐标转为Canvas本地坐标
Vector2 canvasPos;
RectTransformUtility.ScreenPointToLocalPointInRectangle(
canvas.transform as RectTransform,
new Vector2(screenPos.x, screenPos.y),
canvas.renderMode == RenderMode.ScreenSpaceOverlay ? null : uiCamera,
out canvasPos
);
uiElement.anchoredPosition = canvasPos;
uiElement.gameObject.SetActive(true);
}
else
{
uiElement.gameObject.SetActive(false);
}
}
}
3.5 移动端触摸处理
移动设备需要使用触摸输入,并适配不同屏幕比例。
csharp
using UnityEngine;
public class MobileTouchHandler : MonoBehaviour
{
public Camera mainCamera;
void Update()
{
Vector2 inputPosition = GetInputPosition();
if (inputPosition != Vector2.zero)
{
HandleInput(inputPosition);
}
}
// 兼容PC和移动端的输入获取
Vector2 GetInputPosition()
{
#if UNITY_EDITOR || UNITY_STANDALONE
// PC端:使用鼠标
if (Input.GetMouseButton(0))
return Input.mousePosition;
#elif UNITY_IOS || UNITY_ANDROID
// 移动端:使用触摸
if (Input.touchCount > 0)
{
Touch touch = Input.GetTouch(0);
return touch.position;
}
#endif
return Vector2.zero;
}
// 处理输入
void HandleInput(Vector2 screenPos)
{
Ray ray = mainCamera.ScreenPointToRay(screenPos);
if (Physics.Raycast(ray, out RaycastHit hit))
{
Debug.Log($"点击: {hit.collider.name}");
// 执行相应操作
}
}
}
多点触摸处理:
csharp
public class MultiTouchHandler : MonoBehaviour
{
public Camera mainCamera;
void Update()
{
// 处理所有触摸点
for (int i = 0; i < Input.touchCount; i++)
{
Touch touch = Input.GetTouch(i);
switch (touch.phase)
{
case TouchPhase.Began:
OnTouchBegan(touch.position);
break;
case TouchPhase.Moved:
OnTouchMoved(touch.position, touch.deltaPosition);
break;
case TouchPhase.Ended:
OnTouchEnded(touch.position);
break;
}
}
}
void OnTouchBegan(Vector2 screenPos)
{
Ray ray = mainCamera.ScreenPointToRay(screenPos);
if (Physics.Raycast(ray, out RaycastHit hit))
{
Debug.Log($"触摸开始: {hit.collider.name}");
}
}
void OnTouchMoved(Vector2 screenPos, Vector2 deltaPosition)
{
// 处理拖拽
Debug.Log($"拖拽偏移: {deltaPosition}");
}
void OnTouchEnded(Vector2 screenPos)
{
Debug.Log("触摸结束");
}
}
3.6 UI边界限制
防止UI元素超出屏幕边界,保持在可视区域内。
csharp
public class UIBoundaryClamp : MonoBehaviour
{
public RectTransform uiElement;
public Canvas canvas;
public float padding = 10f; // 边距(像素)
void LateUpdate()
{
ClampToScreen();
}
void ClampToScreen()
{
Vector3 pos = uiElement.localPosition;
RectTransform canvasRect = canvas.transform as RectTransform;
// 获取UI元素和Canvas的尺寸
Vector2 elementSize = uiElement.sizeDelta;
Vector2 canvasSize = canvasRect.sizeDelta;
// 考虑Canvas缩放
float canvasScale = canvas.transform.localScale.x;
// 计算边界(本地坐标)
float minX = -canvasSize.x / 2 + elementSize.x / 2 + padding / canvasScale;
float maxX = canvasSize.x / 2 - elementSize.x / 2 - padding / canvasScale;
float minY = -canvasSize.y / 2 + elementSize.y / 2 + padding / canvasScale;
float maxY = canvasSize.y / 2 - elementSize.y / 2 - padding / canvasScale;
// 限制在边界内
pos.x = Mathf.Clamp(pos.x, minX, maxX);
pos.y = Mathf.Clamp(pos.y, minY, maxY);
uiElement.localPosition = pos;
}
// 检查UI是否在屏幕内
public bool IsUIInScreen()
{
Vector3[] corners = new Vector3[4];
uiElement.GetWorldCorners(corners);
Rect screenRect = new Rect(0, 0, Screen.width, Screen.height);
foreach (Vector3 corner in corners)
{
if (!screenRect.Contains(corner))
return false;
}
return true;
}
}
智能位置调整(避免遮挡):
csharp
public class SmartUIPositioning : MonoBehaviour
{
public RectTransform tooltip;
public Canvas canvas;
public Vector2 offset = new Vector2(10, 10);
public void ShowTooltip(Vector2 screenPos)
{
Vector2 canvasPos;
RectTransformUtility.ScreenPointToLocalPointInRectangle(
canvas.transform as RectTransform,
screenPos,
canvas.worldCamera,
out canvasPos
);
// 智能调整位置避免超出屏幕
canvasPos = AdjustPositionToFitScreen(canvasPos);
tooltip.localPosition = canvasPos;
tooltip.gameObject.SetActive(true);
}
Vector2 AdjustPositionToFitScreen(Vector2 position)
{
RectTransform canvasRect = canvas.transform as RectTransform;
Vector2 tooltipSize = tooltip.sizeDelta;
Vector2 canvasSize = canvasRect.sizeDelta;
Vector2 adjustedPos = position + offset;
// 右边界检查
if (adjustedPos.x + tooltipSize.x / 2 > canvasSize.x / 2)
{
adjustedPos.x = position.x - offset.x - tooltipSize.x;
}
// 上边界检查
if (adjustedPos.y + tooltipSize.y / 2 > canvasSize.y / 2)
{
adjustedPos.y = position.y - offset.y - tooltipSize.y;
}
// 左边界检查
if (adjustedPos.x - tooltipSize.x / 2 < -canvasSize.x / 2)
{
adjustedPos.x = -canvasSize.x / 2 + tooltipSize.x / 2;
}
// 下边界检查
if (adjustedPos.y - tooltipSize.y / 2 < -canvasSize.y / 2)
{
adjustedPos.y = -canvasSize.y / 2 + tooltipSize.y / 2;
}
return adjustedPos;
}
}
四、Canvas坐标系统详解
4.1 三种渲染模式
csharp
// Screen Space - Overlay: 直接覆盖在屏幕上
canvas.renderMode = RenderMode.ScreenSpaceOverlay;
// camera参数传null
// Screen Space - Camera: 固定距离相机
canvas.renderMode = RenderMode.ScreenSpaceCamera;
canvas.worldCamera = mainCamera;
// camera参数传worldCamera
// World Space: 3D世界中的Canvas
canvas.renderMode = RenderMode.WorldSpace;
// camera参数传场景相机
五、调试与可视化工具
5.1 坐标调试器
实时显示各种坐标信息,方便调试。
csharp
using UnityEngine;
public class CoordinateDebugger : MonoBehaviour
{
public Camera mainCamera;
public Transform target;
public bool showDebugInfo = true;
public bool drawGizmos = true;
private GUIStyle labelStyle;
void Start()
{
// 设置GUI样式
labelStyle = new GUIStyle();
labelStyle.fontSize = 14;
labelStyle.normal.textColor = Color.white;
labelStyle.alignment = TextAnchor.UpperLeft;
}
void OnDrawGizmos()
{
if (!drawGizmos || !target || !mainCamera) return;
// 绘制世界坐标点
Gizmos.color = Color.red;
Gizmos.DrawSphere(target.position, 0.2f);
// 绘制从相机到物体的射线
Gizmos.color = Color.yellow;
Gizmos.DrawLine(mainCamera.transform.position, target.position);
// 绘制坐标轴
Gizmos.color = Color.red;
Gizmos.DrawRay(target.position, Vector3.right);
Gizmos.color = Color.green;
Gizmos.DrawRay(target.position, Vector3.up);
Gizmos.color = Color.blue;
Gizmos.DrawRay(target.position, Vector3.forward);
}
void OnGUI()
{
if (!showDebugInfo || !target || !mainCamera) return;
// 计算各种坐标
Vector3 worldPos = target.position;
Vector3 screenPos = mainCamera.WorldToScreenPoint(worldPos);
Vector3 viewportPos = mainCamera.WorldToViewportPoint(worldPos);
// 判断可见性
bool isVisible = viewportPos.z > 0 &&
viewportPos.x >= 0 && viewportPos.x <= 1 &&
viewportPos.y >= 0 && viewportPos.y <= 1;
// 显示信息
GUILayout.BeginArea(new Rect(10, 10, 400, 300));
GUILayout.BeginVertical("box");
GUILayout.Label("=== 坐标调试信息 ===", labelStyle);
GUILayout.Space(10);
GUILayout.Label($"世界坐标: {worldPos}", labelStyle);
GUILayout.Label($"屏幕坐标: X={screenPos.x:F1}, Y={screenPos.y:F1}, Z={screenPos.z:F1}", labelStyle);
GUILayout.Label($"视口坐标: X={viewportPos.x:F3}, Y={viewportPos.y:F3}, Z={viewportPos.z:F3}", labelStyle);
GUILayout.Space(5);
GUILayout.Label($"是否可见: {(isVisible ? "✓ 是" : "✗ 否")}", labelStyle);
GUILayout.Label($"距相机距离: {Vector3.Distance(mainCamera.transform.position, worldPos):F2}m", labelStyle);
// 鼠标信息
GUILayout.Space(10);
Vector3 mousePos = Input.mousePosition;
GUILayout.Label($"鼠标屏幕坐标: ({mousePos.x:F0}, {mousePos.y:F0})", labelStyle);
Vector3 mouseViewport = mainCamera.ScreenToViewportPoint(mousePos);
GUILayout.Label($"鼠标视口坐标: ({mouseViewport.x:F3}, {mouseViewport.y:F3})", labelStyle);
GUILayout.EndVertical();
GUILayout.EndArea();
}
}
5.2 射线可视化工具
可视化射线检测过程。
csharp
using UnityEngine;
public class RaycastVisualizer : MonoBehaviour
{
public Camera mainCamera;
public float rayLength = 100f;
public Color hitColor = Color.green;
public Color missColor = Color.red;
public float hitPointSize = 0.2f;
private Vector3? lastHitPoint;
private bool lastRaycastHit;
void Update()
{
if (Input.GetMouseButton(0))
{
Ray ray = mainCamera.ScreenPointToRay(Input.mousePosition);
if (Physics.Raycast(ray, out RaycastHit hit, rayLength))
{
lastHitPoint = hit.point;
lastRaycastHit = true;
Debug.DrawRay(ray.origin, ray.direction * hit.distance, hitColor, 0.1f);
}
else
{
lastRaycastHit = false;
lastHitPoint = null;
Debug.DrawRay(ray.origin, ray.direction * rayLength, missColor, 0.1f);
}
}
}
void OnDrawGizmos()
{
if (lastHitPoint.HasValue && lastRaycastHit)
{
Gizmos.color = hitColor;
Gizmos.DrawSphere(lastHitPoint.Value, hitPointSize);
// 绘制法线
Gizmos.color = Color.blue;
Gizmos.DrawRay(lastHitPoint.Value, Vector3.up * 0.5f);
}
}
}
5.3 UI范围可视化
显示UI元素的边界和Canvas范围。
csharp
using UnityEngine;
using UnityEngine.UI;
[ExecuteInEditMode]
public class UIBoundsVisualizer : MonoBehaviour
{
public RectTransform uiElement;
public Canvas canvas;
public Color boundsColor = Color.cyan;
public Color canvasColor = Color.yellow;
void OnDrawGizmos()
{
if (!uiElement || !canvas) return;
// 绘制Canvas边界
DrawRectBounds(canvas.transform as RectTransform, canvasColor);
// 绘制UI元素边界
DrawRectBounds(uiElement, boundsColor);
}
void DrawRectBounds(RectTransform rectTransform, Color color)
{
if (rectTransform == null) return;
Vector3[] corners = new Vector3[4];
rectTransform.GetWorldCorners(corners);
Gizmos.color = color;
// 绘制边框
for (int i = 0; i < 4; i++)
{
Gizmos.DrawLine(corners[i], corners[(i + 1) % 4]);
}
// 绘制对角线
Gizmos.color = new Color(color.r, color.g, color.b, 0.3f);
Gizmos.DrawLine(corners[0], corners[2]);
Gizmos.DrawLine(corners[1], corners[3]);
}
}
5.4 性能分析工具
六、常见陷阱与错误
在使用Unity坐标转换时,开发者经常会遇到一些隐蔽的陷阱。本章列举最常见的错误及其正确做法。
6.1 Canvas RenderMode参数错误 ⚠️
❌ 错误做法:
csharp
// 在Overlay模式下传入相机
canvas.renderMode = RenderMode.ScreenSpaceOverlay;
RectTransformUtility.ScreenPointToLocalPointInRectangle(
rect,
screenPos,
mainCamera, // ❌ 错误!Overlay模式应该传null
out localPos
);
✅ 正确做法:
csharp
// 根据Canvas模式传入正确的相机参数
Camera cam = canvas.renderMode == RenderMode.ScreenSpaceOverlay
? null
: canvas.worldCamera;
RectTransformUtility.ScreenPointToLocalPointInRectangle(
rect,
screenPos,
cam, // ✓ 正确
out localPos
);
6.2 忘记检查Z值 ⚠️
❌ 错误做法:
csharp
Vector3 screenPos = camera.WorldToScreenPoint(worldPos);
// 没检查Z值,对象在背后也会显示UI
uiElement.position = screenPos;
✅ 正确做法:
csharp
Vector3 screenPos = camera.WorldToScreenPoint(worldPos);
// 必须检查Z值
if (screenPos.z > 0)
{
// 在相机前方,显示UI
UpdateUIPosition(screenPos);
}
else
{
// 在相机后方,隐藏UI
uiElement.gameObject.SetActive(false);
}
6.3 每帧查找Camera.main ⚠️
❌ 错误做法:
csharp
void Update()
{
// Camera.main每次调用都会查找,性能很差!
var pos = Camera.main.WorldToScreenPoint(transform.position);
}
✅ 正确做法:
csharp
private Camera mainCamera;
void Start()
{
// 缓存引用
mainCamera = Camera.main;
}
void Update()
{
var pos = mainCamera.WorldToScreenPoint(transform.position);
}
6.4 ScreenToWorldPoint缺少Z深度 ⚠️
❌ 错误做法:
csharp
Vector3 mousePos = Input.mousePosition;
// 没有设置Z值,会使用0,导致结果错误
Vector3 worldPos = camera.ScreenToWorldPoint(mousePos);
✅ 正确做法:
csharp
Vector3 mousePos = Input.mousePosition;
mousePos.z = 10f; // 必须指定Z深度
Vector3 worldPos = camera.ScreenToWorldPoint(mousePos);
// 或使用射线检测(更好的方法)
Ray ray = camera.ScreenPointToRay(Input.mousePosition);
if (Physics.Raycast(ray, out RaycastHit hit))
{
Vector3 worldPos = hit.point;
}
6.5 混淆本地坐标和世界坐标 ⚠️
❌ 错误做法:
csharp
// uiElement.position 是世界坐标
// 但ScreenPointToLocalPointInRectangle返回的是本地坐标
Vector2 localPos;
RectTransformUtility.ScreenPointToLocalPointInRectangle(
canvas.transform as RectTransform,
screenPos,
camera,
out localPos
);
uiElement.position = localPos; // ❌ 类型不匹配!
✅ 正确做法:
csharp
Vector2 localPos;
RectTransformUtility.ScreenPointToLocalPointInRectangle(
canvas.transform as RectTransform,
screenPos,
camera,
out localPos
);
// 使用anchoredPosition而不是localPosition(最佳实践)
uiElement.anchoredPosition = localPos; // ✓ 正确
6.6 Update vs LateUpdate ⚠️
❌ 错误做法:
csharp
// 在Update中更新UI位置
void Update()
{
UpdateUIPosition();
}
// 可能出现抖动,因为3D对象可能在LateUpdate中移动
✅ 正确做法:
csharp
// 使用LateUpdate确保在所有Update之后执行
void LateUpdate()
{
UpdateUIPosition();
}
6.7 忽略Canvas Scaler影响 ⚠️
❌ 错误做法:
csharp
// 直接使用屏幕像素
uiElement.anchoredPosition = new Vector2(100, 100);
// 在不同分辨率下位置会错乱
✅ 正确做法:
csharp
// 使用RectTransformUtility考虑Canvas Scaler
Vector2 localPos;
RectTransformUtility.ScreenPointToLocalPointInRectangle(
canvas.transform as RectTransform,
new Vector2(100, 100),
canvas.worldCamera,
out localPos
);
uiElement.localPosition = localPos;
6.8 射线检测忽略UI层 ⚠️
❌ 错误做法:
csharp
Ray ray = camera.ScreenPointToRay(Input.mousePosition);
if (Physics.Raycast(ray, out RaycastHit hit))
{
// 点击UI时也会触发3D物体检测
SelectObject(hit.collider.gameObject);
}
✅ 正确做法:
csharp
using UnityEngine.EventSystems;
// 检查是否点击在UI上
if (EventSystem.current.IsPointerOverGameObject())
{
return; // 点击在UI上,不处理3D对象
}
Ray ray = camera.ScreenPointToRay(Input.mousePosition);
if (Physics.Raycast(ray, out RaycastHit hit))
{
SelectObject(hit.collider.gameObject);
}
6.9 Vector3和Vector2混用 ⚠️
❌ 错误做法:
csharp
Vector3 screenPos = camera.WorldToScreenPoint(worldPos);
// 隐式转换丢失了Z信息
Vector2 pos2D = screenPos;
// 后续使用pos2D会丢失深度信息
✅ 正确做法:
csharp
Vector3 screenPos = camera.WorldToScreenPoint(worldPos);
// 先检查Z值
if (screenPos.z > 0)
{
// 明确转换,了解丢失了什么
Vector2 pos2D = new Vector2(screenPos.x, screenPos.y);
// 使用pos2D
}
6.10 多相机场景使用错误的相机 ⚠️
❌ 错误做法:
csharp
// 场景中有多个相机,使用了错误的相机进行转换
Vector3 screenPos = Camera.main.WorldToScreenPoint(worldPos);
// 如果worldPos是被另一个相机渲染的,结果会错误
✅ 正确做法:
csharp
// 使用渲染该对象的相机
public Camera renderingCamera; // 在Inspector中指定正确的相机
Vector3 screenPos = renderingCamera.WorldToScreenPoint(worldPos);
6.11 坐标转换时机错误 ⚠️
❌ 错误做法:
csharp
void Awake()
{
// UI可能还未初始化完成
UpdateUIPosition();
}
✅ 正确做法:
csharp
void Start()
{
// 确保所有组件都已初始化
UpdateUIPosition();
}
// 或者使用协程等待一帧
IEnumerator Start()
{
yield return null; // 等待下一帧
UpdateUIPosition();
}
6.12 忘记处理屏幕旋转 ⚠️
❌ 错误做法:
csharp
// 移动设备屏幕旋转后坐标系统变化,但代码未处理
void Update()
{
Vector3 screenPos = camera.WorldToScreenPoint(worldPos);
// 旋转后可能出现问题
}
✅ 正确做法:
csharp
using UnityEngine;
private ScreenOrientation lastOrientation;
void Update()
{
// 检测屏幕旋转
if (Screen.orientation != lastOrientation)
{
lastOrientation = Screen.orientation;
OnOrientationChanged();
}
UpdateCoordinates();
}
void OnOrientationChanged()
{
// 重新计算Canvas Scaler等
Debug.Log($"屏幕旋转: {Screen.orientation}");
}
6.13 常见错误检查清单
使用此清单避免常见陷阱:
csharp
public class CoordinateConversionValidator : MonoBehaviour
{
public static bool ValidateWorldToScreen(Camera camera, Vector3 worldPos, out Vector3 screenPos)
{
// ✓ 检查1: 相机是否存在
if (camera == null)
{
Debug.LogError("相机为null!");
screenPos = Vector3.zero;
return false;
}
// ✓ 检查2: 相机是否启用
if (!camera.enabled)
{
Debug.LogWarning("相机未启用!");
}
// 执行转换
screenPos = camera.WorldToScreenPoint(worldPos);
// ✓ 检查3: Z值验证
if (screenPos.z <= 0)
{
Debug.LogWarning($"对象在相机后方!Z={screenPos.z}");
return false;
}
// ✓ 检查4: 屏幕边界验证
if (screenPos.x < 0 || screenPos.x > Screen.width ||
screenPos.y < 0 || screenPos.y > Screen.height)
{
Debug.LogWarning($"坐标超出屏幕范围!({screenPos.x}, {screenPos.y})");
return false;
}
return true;
}
public static bool ValidateUIConversion(Canvas canvas, Vector2 screenPos, out Vector2 localPos)
{
localPos = Vector2.zero;
// ✓ 检查1: Canvas是否存在
if (canvas == null)
{
Debug.LogError("Canvas为null!");
return false;
}
// ✓ 检查2: RenderMode验证
Camera cam = canvas.renderMode == RenderMode.ScreenSpaceOverlay
? null
: canvas.worldCamera;
if (canvas.renderMode != RenderMode.ScreenSpaceOverlay && cam == null)
{
Debug.LogError("Canvas设置了Camera模式但worldCamera为null!");
return false;
}
// 执行转换
RectTransform canvasRect = canvas.transform as RectTransform;
return RectTransformUtility.ScreenPointToLocalPointInRectangle(
canvasRect,
screenPos,
cam,
out localPos
);
}
}
使用验证器:
csharp
void UpdateUIPosition()
{
if (CoordinateConversionValidator.ValidateWorldToScreen(
mainCamera,
target.position,
out Vector3 screenPos))
{
if (CoordinateConversionValidator.ValidateUIConversion(
canvas,
screenPos,
out Vector2 localPos))
{
uiElement.localPosition = localPos;
}
}
}
csharp
using UnityEngine;
using System.Diagnostics;
public class CoordinatePerformanceProfiler : MonoBehaviour
{
private Stopwatch stopwatch = new Stopwatch();
private int frameCount = 0;
private double totalTime = 0;
void Update()
{
ProfileCoordinateConversions();
}
void ProfileCoordinateConversions()
{
Camera cam = Camera.main;
Vector3 worldPos = transform.position;
stopwatch.Restart();
// 测试WorldToScreenPoint性能
for (int i = 0; i < 100; i++)
{
Vector3 screenPos = cam.WorldToScreenPoint(worldPos);
}
stopwatch.Stop();
totalTime += stopwatch.Elapsed.TotalMilliseconds;
frameCount++;
if (frameCount >= 60)
{
double avgTime = totalTime / frameCount;
UnityEngine.Debug.Log($"平均每帧100次WorldToScreenPoint: {avgTime:F3}ms");
frameCount = 0;
totalTime = 0;
}
}
}
5.5 交互式调试面板
csharp
using UnityEngine;
using UnityEngine.UI;
public class CoordinateDebugPanel : MonoBehaviour
{
public Camera mainCamera;
public Transform target;
[Header("UI References")]
public Text worldPosText;
public Text screenPosText;
public Text viewportPosText;
public Text distanceText;
public Toggle visibilityToggle;
void Update()
{
if (!target || !mainCamera) return;
UpdateDebugInfo();
}
void UpdateDebugInfo()
{
Vector3 worldPos = target.position;
Vector3 screenPos = mainCamera.WorldToScreenPoint(worldPos);
Vector3 viewportPos = mainCamera.WorldToViewportPoint(worldPos);
// 更新文本
if (worldPosText)
worldPosText.text = $"世界: {worldPos}";
if (screenPosText)
screenPosText.text = $"屏幕: ({screenPos.x:F0}, {screenPos.y:F0}, {screenPos.z:F2})";
if (viewportPosText)
viewportPosText.text = $"视口: ({viewportPos.x:F3}, {viewportPos.y:F3})";
if (distanceText)
{
float distance = Vector3.Distance(mainCamera.transform.position, worldPos);
distanceText.text = $"距离: {distance:F2}m";
}
// 更新可见性
if (visibilityToggle)
{
bool isVisible = viewportPos.z > 0 &&
viewportPos.x >= 0 && viewportPos.x <= 1 &&
viewportPos.y >= 0 && viewportPos.y <= 1;
visibilityToggle.isOn = isVisible;
}
}
}
4.2 Canvas Scaler详细配置
Canvas Scaler组件用于处理不同分辨率和屏幕比例的UI缩放。
csharp
using UnityEngine;
using UnityEngine.UI;
public class CanvasScalerSetup : MonoBehaviour
{
public Canvas canvas;
public CanvasScaler canvasScaler;
void Start()
{
SetupCanvasScaler();
}
void SetupCanvasScaler()
{
if (!canvasScaler)
{
canvasScaler = canvas.GetComponent<CanvasScaler>();
if (!canvasScaler)
canvasScaler = canvas.gameObject.AddComponent<CanvasScaler>();
}
// 设置缩放模式
canvasScaler.uiScaleMode = CanvasScaler.ScaleMode.ScaleWithScreenSize;
// 设置参考分辨率
canvasScaler.referenceResolution = new Vector2(1920, 1080);
// 根据屏幕比例智能调整
float aspectRatio = (float)Screen.width / Screen.height;
if (aspectRatio > 1.7f) // 宽屏 (如21:9, 2560x1080)
{
canvasScaler.matchWidthOrHeight = 1f; // 匹配高度
Debug.Log("检测到宽屏,匹配高度");
}
else if (aspectRatio < 1.5f) // 窄屏或方形 (如4:3, iPad)
{
canvasScaler.matchWidthOrHeight = 0f; // 匹配宽度
Debug.Log("检测到窄屏,匹配宽度");
}
else // 标准16:9
{
canvasScaler.matchWidthOrHeight = 0.5f; // 居中权重
Debug.Log("标准16:9比例");
}
// 设置物理单位模式(可选)
canvasScaler.physicalUnit = CanvasScaler.Unit.Points;
canvasScaler.fallbackScreenDPI = 96f;
canvasScaler.defaultSpriteDPI = 96f;
}
// 运行时动态调整
void OnRectTransformDimensionsChange()
{
SetupCanvasScaler();
}
}
三种缩放模式详解:
- Constant Pixel Size(恒定像素大小)
csharp
canvasScaler.uiScaleMode = CanvasScaler.ScaleMode.ConstantPixelSize;
canvasScaler.scaleFactor = 1f; // 全局缩放因子
canvasScaler.referencePixelsPerUnit = 100f; // Sprite像素比
- 适用场景:像素完美的游戏(如像素风格游戏)
- 优点:UI大小固定,不会模糊
- 缺点:在不同分辨率下显示大小不一致
- Scale With Screen Size(随屏幕大小缩放) ⭐ 推荐
csharp
canvasScaler.uiScaleMode = CanvasScaler.ScaleMode.ScaleWithScreenSize;
canvasScaler.referenceResolution = new Vector2(1920, 1080);
canvasScaler.screenMatchMode = CanvasScaler.ScreenMatchMode.MatchWidthOrHeight;
canvasScaler.matchWidthOrHeight = 0.5f; // 0=宽度, 1=高度, 0.5=平衡
- 适用场景:大多数移动和PC游戏
- 优点:自适应不同分辨率
- 缺点:可能产生轻微缩放模糊
- Constant Physical Size(恒定物理大小)
csharp
canvasScaler.uiScaleMode = CanvasScaler.ScaleMode.ConstantPhysicalSize;
canvasScaler.physicalUnit = CanvasScaler.Unit.Centimeters;
canvasScaler.fallbackScreenDPI = 96f;
- 适用场景:需要真实物理尺寸的应用
- 优点:跨设备物理大小一致
- 缺点:依赖设备DPI信息
不同设备的最佳实践:
csharp
public class AdaptiveCanvasScaler : MonoBehaviour
{
public CanvasScaler canvasScaler;
void Start()
{
SetupForCurrentDevice();
}
void SetupForCurrentDevice()
{
canvasScaler.uiScaleMode = CanvasScaler.ScaleMode.ScaleWithScreenSize;
#if UNITY_ANDROID || UNITY_IOS
// 移动设备设置
SetupForMobile();
#elif UNITY_STANDALONE
// PC设置
SetupForPC();
#elif UNITY_WEBGL
// WebGL设置
SetupForWeb();
#endif
}
void SetupForMobile()
{
// 移动设备通常是竖屏或横屏固定
bool isPortrait = Screen.height > Screen.width;
if (isPortrait)
{
canvasScaler.referenceResolution = new Vector2(1080, 1920);
canvasScaler.matchWidthOrHeight = 0f; // 匹配宽度
}
else
{
canvasScaler.referenceResolution = new Vector2(1920, 1080);
canvasScaler.matchWidthOrHeight = 1f; // 匹配高度
}
Debug.Log($"移动设备配置: {(isPortrait ? "竖屏" : "横屏")}");
}
void SetupForPC()
{
// PC通常是横屏,支持多种分辨率
canvasScaler.referenceResolution = new Vector2(1920, 1080);
float aspect = (float)Screen.width / Screen.height;
// 超宽屏
if (aspect >= 2.0f)
{
canvasScaler.matchWidthOrHeight = 1f;
}
// 标准宽屏
else if (aspect >= 1.5f)
{
canvasScaler.matchWidthOrHeight = 0.5f;
}
// 接近方形
else
{
canvasScaler.matchWidthOrHeight = 0f;
}
}
void SetupForWeb()
{
// WebGL可能运行在各种浏览器窗口中
canvasScaler.referenceResolution = new Vector2(1920, 1080);
canvasScaler.matchWidthOrHeight = 0.5f; // 平衡模式
}
}
常见分辨率适配表:
| 设备类型 | 分辨率 | 参考分辨率 | matchWidthOrHeight |
|---|---|---|---|
| iPhone (竖屏) | 1080x1920 | 1080x1920 | 0 (宽度) |
| iPhone (横屏) | 1920x1080 | 1920x1080 | 1 (高度) |
| iPad | 2048x2732 | 1536x2048 | 0.5 (平衡) |
| Android 手机 | 1080x2340 | 1080x1920 | 0 (宽度) |
| PC 1080p | 1920x1080 | 1920x1080 | 0.5 (平衡) |
| PC 1440p | 2560x1440 | 1920x1080 | 0.5 (平衡) |
| PC 4K | 3840x2160 | 1920x1080 | 0.5 (平衡) |
| 超宽屏 21:9 | 2560x1080 | 1920x1080 | 1 (高度) |
4.3 ScreenPointToLocalPointInRectangle
将屏幕坐标转换为RectTransform的本地坐标。
csharp
using UnityEngine;
using UnityEngine.EventSystems;
public class DragUI : MonoBehaviour, IDragHandler
{
public Canvas canvas;
private RectTransform rectTransform;
void Start()
{
rectTransform = GetComponent<RectTransform>();
}
public void OnDrag(PointerEventData eventData)
{
Vector2 localPoint;
RectTransformUtility.ScreenPointToLocalPointInRectangle(
canvas.transform as RectTransform,
eventData.position,
canvas.worldCamera,
out localPoint
);
rectTransform.localPosition = localPoint;
}
}
七、实战应用场景
6.1 小地图系统
csharp
public class Minimap : MonoBehaviour
{
public RectTransform minimapRect;
public Transform player;
public RectTransform playerIcon;
public float mapScale = 0.1f; // 世界单位到UI像素的比例
void Update()
{
// 玩家世界坐标转小地图坐标
Vector2 mapPos = new Vector2(
player.position.x * mapScale,
player.position.z * mapScale
);
playerIcon.anchoredPosition = mapPos;
}
}
6.2 3D物体提示框
csharp
public class ObjectTooltip : MonoBehaviour
{
public RectTransform tooltip;
public Canvas canvas;
public Camera mainCamera;
void OnMouseEnter()
{
// 显示提示框
tooltip.gameObject.SetActive(true);
UpdateTooltipPosition();
}
void OnMouseExit()
{
tooltip.gameObject.SetActive(false);
}
void Update()
{
if (tooltip.gameObject.activeSelf)
{
UpdateTooltipPosition();
}
}
void UpdateTooltipPosition()
{
Vector3 screenPos = mainCamera.WorldToScreenPoint(transform.position);
Vector2 canvasPos;
RectTransformUtility.ScreenPointToLocalPointInRectangle(
canvas.transform as RectTransform,
new Vector2(screenPos.x, screenPos.y),
canvas.worldCamera,
out canvasPos
);
// 添加偏移避免遮挡
tooltip.localPosition = canvasPos + new Vector2(50, 50);
}
}
6.3 拖拽生成3D物体
csharp
using UnityEngine;
using UnityEngine.EventSystems;
public class DragSpawn3D : MonoBehaviour, IBeginDragHandler, IDragHandler, IEndDragHandler
{
public GameObject prefab3D;
public Camera mainCamera;
public float spawnHeight = 0f;
private GameObject previewObject;
public void OnBeginDrag(PointerEventData eventData)
{
// 创建预览对象
previewObject = Instantiate(prefab3D);
UpdatePreviewPosition(eventData.position);
}
public void OnDrag(PointerEventData eventData)
{
UpdatePreviewPosition(eventData.position);
}
public void OnEndDrag(PointerEventData eventData)
{
// 确认放置位置
Ray ray = mainCamera.ScreenPointToRay(eventData.position);
Plane groundPlane = new Plane(Vector3.up, new Vector3(0, spawnHeight, 0));
if (groundPlane.Raycast(ray, out float distance))
{
Vector3 spawnPos = ray.GetPoint(distance);
previewObject.transform.position = spawnPos;
// 最终确认生成
}
else
{
Destroy(previewObject);
}
}
void UpdatePreviewPosition(Vector2 screenPos)
{
Ray ray = mainCamera.ScreenPointToRay(screenPos);
Plane groundPlane = new Plane(Vector3.up, new Vector3(0, spawnHeight, 0));
if (groundPlane.Raycast(ray, out float distance))
{
previewObject.transform.position = ray.GetPoint(distance);
}
}
}
八、常见问题与解决方案
7.1 UI坐标转换不准确
原因: Canvas设置不正确。
csharp
// 确保正确设置
if (canvas.renderMode == RenderMode.ScreenSpaceOverlay)
{
// Overlay模式,camera传null
RectTransformUtility.ScreenPointToLocalPointInRectangle(
parentRect, screenPos, null, out localPos);
}
else
{
// Camera模式,传入worldCamera
RectTransformUtility.ScreenPointToLocalPointInRectangle(
parentRect, screenPos, canvas.worldCamera, out localPos);
}
7.2 物体在相机后方仍显示UI
解决: 检查Z值。
csharp
Vector3 screenPos = camera.WorldToScreenPoint(worldPos);
if (screenPos.z > 0) // 必须检查Z值
{
// 在相机前方,显示UI
}
7.3 高分辨率屏幕UI错位
解决: 使用Canvas Scaler。
csharp
// Canvas Scaler设置
canvasScaler.uiScaleMode = CanvasScaler.ScaleMode.ScaleWithScreenSize;
canvasScaler.referenceResolution = new Vector2(1920, 1080);
canvasScaler.matchWidthOrHeight = 0.5f;
7.4 3D物体跟随UI有延迟
解决: 使用LateUpdate()。
csharp
void LateUpdate() // 而非Update()
{
// 在所有Update之后执行,保证同步
UpdateUIPosition();
}
九、性能优化建议
8.1 基础优化技巧
csharp
public class OptimizedUIFollow : MonoBehaviour
{
private Camera mainCamera;
private RectTransform canvasRect;
private Vector3 cachedScreenPos;
private Vector2 cachedCanvasPos;
void Start()
{
// 缓存引用,避免每帧查找
mainCamera = Camera.main;
canvasRect = canvas.transform as RectTransform;
}
void LateUpdate()
{
// 仅在可见时更新
if (!renderer.isVisible) return;
// 减少GC:复用Vector3
cachedScreenPos = mainCamera.WorldToScreenPoint(target.position);
// 距离剔除:太远不显示UI
if (cachedScreenPos.z > maxDistance) return;
// 转换坐标
UpdateUIPosition(cachedScreenPos);
}
void UpdateUIPosition(Vector3 screenPos)
{
RectTransformUtility.ScreenPointToLocalPointInRectangle(
canvasRect,
new Vector2(screenPos.x, screenPos.y),
canvas.worldCamera,
out cachedCanvasPos
);
uiElement.localPosition = cachedCanvasPos;
}
}
8.2 对象池优化
当需要频繁创建和销毁UI元素时,使用对象池可以显著提升性能。
通用对象池实现:
csharp
using UnityEngine;
using System.Collections.Generic;
public class ObjectPool<T> where T : Component
{
private Queue<T> pool = new Queue<T>();
private T prefab;
private Transform parent;
private int initialSize;
public ObjectPool(T prefab, Transform parent, int initialSize = 10)
{
this.prefab = prefab;
this.parent = parent;
this.initialSize = initialSize;
// 预创建对象
for (int i = 0; i < initialSize; i++)
{
CreateNewObject();
}
}
private T CreateNewObject()
{
T obj = Object.Instantiate(prefab, parent);
obj.gameObject.SetActive(false);
pool.Enqueue(obj);
return obj;
}
public T Get()
{
if (pool.Count == 0)
{
CreateNewObject();
}
T obj = pool.Dequeue();
obj.gameObject.SetActive(true);
return obj;
}
public void Release(T obj)
{
obj.gameObject.SetActive(false);
pool.Enqueue(obj);
}
public void Clear()
{
while (pool.Count > 0)
{
T obj = pool.Dequeue();
if (obj != null)
Object.Destroy(obj.gameObject);
}
}
}
UI跟随3D对象的对象池版本:
csharp
using UnityEngine;
using System.Collections.Generic;
public class UIFollowPoolManager : MonoBehaviour
{
[Header("References")]
public RectTransform uiPrefab;
public Canvas canvas;
public Camera mainCamera;
public Transform uiContainer;
[Header("Settings")]
public int poolSize = 20;
private ObjectPool<RectTransform> uiPool;
private Dictionary<Transform, RectTransform> activeUIs = new Dictionary<Transform, RectTransform>();
void Start()
{
// 初始化对象池
uiPool = new ObjectPool<RectTransform>(uiPrefab, uiContainer, poolSize);
}
public void ShowUI(Transform target3D)
{
// 如果已经存在,直接返回
if (activeUIs.ContainsKey(target3D))
return;
// 从对象池获取UI
RectTransform ui = uiPool.Get();
activeUIs[target3D] = ui;
// 设置初始位置
UpdateUIPosition(target3D, ui);
}
public void HideUI(Transform target3D)
{
if (activeUIs.TryGetValue(target3D, out RectTransform ui))
{
// 释放回对象池
uiPool.Release(ui);
activeUIs.Remove(target3D);
}
}
void LateUpdate()
{
// 更新所有活动的UI位置
foreach (var kvp in activeUIs)
{
UpdateUIPosition(kvp.Key, kvp.Value);
}
}
void UpdateUIPosition(Transform target3D, RectTransform ui)
{
Vector3 screenPos = mainCamera.WorldToScreenPoint(target3D.position);
if (screenPos.z > 0)
{
Vector2 canvasPos;
RectTransformUtility.ScreenPointToLocalPointInRectangle(
canvas.transform as RectTransform,
new Vector2(screenPos.x, screenPos.y),
canvas.worldCamera,
out canvasPos
);
ui.localPosition = canvasPos;
}
}
void OnDestroy()
{
// 清理对象池
uiPool?.Clear();
}
}
使用示例:
csharp
public class Enemy : MonoBehaviour
{
private UIFollowPoolManager uiManager;
void Start()
{
uiManager = FindObjectOfType<UIFollowPoolManager>();
}
void OnBecameVisible()
{
// 显示血条
uiManager.ShowUI(transform);
}
void OnBecameInvisible()
{
// 隐藏血条
uiManager.HideUI(transform);
}
void OnDestroy()
{
// 确保释放UI
uiManager?.HideUI(transform);
}
}
8.3 批量更新优化
当有大量UI需要跟随3D对象时,批量处理可以提升效率。
csharp
using UnityEngine;
using System.Collections.Generic;
public class BatchUIUpdateManager : MonoBehaviour
{
[System.Serializable]
public class UIFollowPair
{
public Transform target3D;
public RectTransform uiElement;
[HideInInspector] public Vector3 cachedScreenPos;
[HideInInspector] public Vector2 cachedCanvasPos;
}
public Canvas canvas;
public Camera mainCamera;
public List<UIFollowPair> pairs = new List<UIFollowPair>();
private RectTransform canvasRect;
void Start()
{
canvasRect = canvas.transform as RectTransform;
}
void LateUpdate()
{
// 批量处理所有UI
int count = pairs.Count;
for (int i = 0; i < count; i++)
{
UpdateSingleUI(pairs[i]);
}
}
void UpdateSingleUI(UIFollowPair pair)
{
// 计算屏幕坐标
pair.cachedScreenPos = mainCamera.WorldToScreenPoint(pair.target3D.position);
// 检查可见性
if (pair.cachedScreenPos.z <= 0)
{
pair.uiElement.gameObject.SetActive(false);
return;
}
// 转换为Canvas坐标
RectTransformUtility.ScreenPointToLocalPointInRectangle(
canvasRect,
new Vector2(pair.cachedScreenPos.x, pair.cachedScreenPos.y),
canvas.worldCamera,
out pair.cachedCanvasPos
);
pair.uiElement.localPosition = pair.cachedCanvasPos;
pair.uiElement.gameObject.SetActive(true);
}
// 动态添加
public void AddPair(Transform target, RectTransform ui)
{
pairs.Add(new UIFollowPair { target3D = target, uiElement = ui });
}
// 动态移除
public void RemovePair(Transform target)
{
pairs.RemoveAll(p => p.target3D == target);
}
}
8.4 LOD(细节层次)优化
根据距离调整UI更新频率。
csharp
public class LODUIFollow : MonoBehaviour
{
public Transform target3D;
public RectTransform uiElement;
public Camera mainCamera;
[Header("LOD Settings")]
public float nearDistance = 10f; // 近距离
public float farDistance = 50f; // 远距离
private float updateInterval;
private float timer;
void LateUpdate()
{
float distance = Vector3.Distance(mainCamera.transform.position, target3D.position);
// 根据距离设置更新频率
if (distance < nearDistance)
{
updateInterval = 0f; // 每帧更新
}
else if (distance < farDistance)
{
updateInterval = 0.1f; // 10帧更新一次
}
else
{
updateInterval = 0.5f; // 30帧更新一次
}
// 计时器控制
timer += Time.deltaTime;
if (timer >= updateInterval)
{
timer = 0f;
UpdateUIPosition();
}
}
void UpdateUIPosition()
{
Vector3 screenPos = mainCamera.WorldToScreenPoint(target3D.position);
if (screenPos.z > 0)
{
Vector2 canvasPos;
RectTransformUtility.ScreenPointToLocalPointInRectangle(
canvas.transform as RectTransform,
new Vector2(screenPos.x, screenPos.y),
canvas.worldCamera,
out canvasPos
);
uiElement.anchoredPosition = canvasPos;
}
}
}
8.5 性能监控
csharp
using UnityEngine;
using System.Diagnostics;
public class CoordinatePerformanceMonitor : MonoBehaviour
{
private Stopwatch stopwatch = new Stopwatch();
[Header("统计信息")]
public int conversionsPerFrame = 0;
public float averageTimeMs = 0f;
public int totalConversions = 0;
void Update()
{
conversionsPerFrame = 0;
}
public void MeasureConversion(System.Action conversionAction)
{
stopwatch.Restart();
conversionAction.Invoke();
stopwatch.Stop();
conversionsPerFrame++;
totalConversions++;
// 计算平均时间
averageTimeMs = (averageTimeMs * (totalConversions - 1) +
(float)stopwatch.Elapsed.TotalMilliseconds) / totalConversions;
}
void OnGUI()
{
GUILayout.BeginArea(new Rect(10, 100, 300, 100));
GUILayout.Label($"每帧转换次数: {conversionsPerFrame}");
GUILayout.Label($"平均耗时: {averageTimeMs:F4}ms");
GUILayout.Label($"总转换次数: {totalConversions}");
GUILayout.EndArea();
}
}
总结
核心API速查:
| 转换方向 | 方法 | 注意事项 |
|---|---|---|
| 世界→屏幕 | Camera.WorldToScreenPoint() |
Z值表示深度 |
| 屏幕→世界 | Camera.ScreenToWorldPoint() |
必须指定Z深度 |
| 世界→视口 | Camera.WorldToViewportPoint() |
归一化0-1 |
| 视口→世界 | Camera.ViewportToWorldPoint() |
必须指定Z深度 |
| 屏幕→射线 | Camera.ScreenPointToRay() |
用于射线检测 |
| 屏幕→UI | ScreenPointToLocalPointInRectangle() |
需传入Canvas |
| UI→屏幕 | WorldToScreenPoint() |
RectTransformUtility |
最佳实践:
- 使用射线检测获取精确3D坐标
- UI跟随3D时检查Z值和可见性
- 缓存Camera和RectTransform引用
- 在LateUpdate()中处理跟随逻辑
- 根据Canvas模式选择正确的camera参数