1. 项目概述与核心思路在Unity开发中尤其是制作交互式应用、游戏或者工具时让用户直观地感知到“这个物体可以被点击或交互”是提升用户体验的关键一环。鼠标悬停高亮就是实现这一目标最经典、最有效的手段之一。它不仅仅是让物体“亮”一下那么简单背后涉及到射线检测、材质管理、着色器交互等一系列核心知识点。很多新手开发者包括几年前的我在实现这个看似简单的功能时常常会遇到高亮闪烁、性能开销大、多材质对象处理不当等问题。今天我就结合自己踩过的坑和优化经验来详细拆解一下在Unity中实现一个稳定、高效、可复用的鼠标悬停高亮方案。这个方案的核心目标很明确当鼠标移动到一个带有碰撞体的3D或2D物体上时该物体能以某种视觉方式如改变颜色、增加发光、描边等进行反馈当鼠标移开时物体恢复原状。我们将从最基础的物理射线检测讲起逐步深入到使用Shader Graph实现更复杂、更美观的高亮效果并最终封装成一个健壮的组件。无论你是刚接触Unity的新手还是想优化现有交互逻辑的老手这篇文章都能给你提供清晰的路径和实用的代码。2. 核心原理与方案选型实现鼠标悬停高亮本质上是一个“检测-响应”的过程。检测鼠标是否在某个物体上Unity提供了几种主流方案每种都有其适用场景和优缺点。2.1 方案一基于物理射线检测Physics Raycast这是最经典、最通用的方法适用于绝大多数3D场景。其原理是从摄像机发出一条穿过鼠标屏幕位置的射线与场景中的碰撞体Collider进行相交测试。为什么选择它精准度高直接与物体的物理形状进行交互符合直觉。灵活性好可以轻松区分不同层Layer的物体设置检测距离甚至获取碰撞点的具体信息如法线、UV坐标。Unity原生支持Physics.RaycastAPI成熟稳定文档丰富。核心代码逻辑框架void Update() { // 将鼠标屏幕坐标转换为一条射线 Ray ray Camera.main.ScreenPointToRay(Input.mousePosition); RaycastHit hit; // 进行射线检测这里可以指定检测距离和层级掩码以优化性能 if (Physics.Raycast(ray, out hit, maxDistance, layerMask)) { // 如果击中了物体并且这个物体是我们想要高亮的例如通过Tag判断 if (hit.collider.CompareTag(Highlightable)) { // 触发高亮逻辑 OnMouseEnterObject(hit.collider.gameObject); } } else { // 如果没有击中任何物体则触发取消高亮逻辑 OnMouseExitObject(); } }注意这里使用Camera.main在Update中每帧调用在简单场景中没问题但对于复杂场景或移动端需要考虑性能优化比如将检测逻辑放在FixedUpdate中或者使用事件驱动的方式减少不必要的调用。2.2 方案二基于图形射线检测Graphic Raycaster - 仅限UI如果你的高亮对象是UI元素如Image、Button那么使用Graphic Raycaster是更合适的选择。它是Unity UI系统的专用检测器效率更高且能正确处理UI的层级、遮挡关系。实现方式通常不需要自己写完整的检测逻辑可以直接使用UI组件自带的IPointerEnterHandler和IPointerExitHandler接口。using UnityEngine.EventSystems; public class UIHighlight : MonoBehaviour, IPointerEnterHandler, IPointerExitHandler { public void OnPointerEnter(PointerEventData eventData) { // 鼠标进入UI区域触发高亮 GetComponentImage().color Color.yellow; } public void OnPointerExit(PointerEventData eventData) { // 鼠标离开UI区域恢复原状 GetComponentImage().color Color.white; } }2.3 方案三基于碰撞体触发器Collider Trigger这种方法较少用于纯粹的鼠标悬停检测更常用于物体之间的进入/退出检测。其原理是为物体添加一个略大于实际模型的碰撞体并设置为Is Trigger然后通过OnTriggerEnter和OnTriggerExit方法来响应。这种方法需要有一个代表“鼠标”的碰撞体持续在场景中移动实现起来较为繁琐一般不作为首选。方案选型总结对于3D/2D场景物体无脑选择方案一Physics Raycast它是基石。对于UI元素选择方案二Graphic Raycaster。在接下来的深度实现中我们将主要聚焦于方案一因为它最具挑战性和扩展性。3. 基础实现简单的颜色变换高亮我们先从最简单的实现开始通过代码动态修改物体的材质颜色。这是理解整个流程的最佳起点。3.1 创建可高亮对象在场景中创建一个Cube或任意3D模型。确保其拥有一个Collider如Box Collider。这是射线检测能够命中的前提。为其创建一个新的材质Material或者使用默认材质。3.2 编写高亮控制脚本我们创建一个名为SimpleHighlight的脚本。using UnityEngine; public class SimpleHighlight : MonoBehaviour { // 存储物体原本的材质颜色 private Color originalColor; // 高亮时的目标颜色 public Color highlightColor Color.yellow; // 指向该物体渲染器Renderer的引用 private Renderer objectRenderer; void Start() { // 获取物体上的Renderer组件 objectRenderer GetComponentRenderer(); if (objectRenderer ! null objectRenderer.material ! null) { // 保存初始颜色 originalColor objectRenderer.material.color; } else { Debug.LogWarning(SimpleHighlight: No Renderer or Material found on gameObject.name); } } // 提供给外部调用的高亮开启方法 public void EnableHighlight() { if (objectRenderer ! null) { objectRenderer.material.color highlightColor; } } // 提供给外部调用的高亮关闭方法 public void DisableHighlight() { if (objectRenderer ! null) { objectRenderer.material.color originalColor; } } }3.3 编写全局鼠标检测管理器我们需要一个“管理器”来统一处理鼠标射线检测并通知对应的SimpleHighlight组件。using UnityEngine; public class MouseHighlightManager : MonoBehaviour { // 射线检测的最大距离 public float maxDetectionDistance 100f; // 可高亮物体所在的层级用于优化检测 public LayerMask highlightableLayer; // 当前被高亮的物体 private GameObject currentHighlightedObject; // 当前物体上的SimpleHighlight组件缓存 private SimpleHighlight currentHighlightScript; void Update() { HandleMouseHover(); } void HandleMouseHover() { Ray ray Camera.main.ScreenPointToRay(Input.mousePosition); RaycastHit hit; // 执行射线检测使用指定的层级掩码 bool hasHit Physics.Raycast(ray, out hit, maxDetectionDistance, highlightableLayer); if (hasHit) { GameObject hitObject hit.collider.gameObject; // 如果当前命中的物体不是上一个被高亮的物体 if (currentHighlightedObject ! hitObject) { // 先取消上一个物体的高亮 if (currentHighlightScript ! null) { currentHighlightScript.DisableHighlight(); } // 设置新的当前物体 currentHighlightedObject hitObject; // 尝试获取新物体上的高亮脚本 currentHighlightScript currentHighlightedObject.GetComponentSimpleHighlight(); // 如果新物体有高亮脚本则启用高亮 if (currentHighlightScript ! null) { currentHighlightScript.EnableHighlight(); } else { // 如果没有找到脚本清空引用 currentHighlightedObject null; } } // 如果命中的还是同一个物体则什么也不做 } else { // 射线没有命中任何物体取消当前的高亮 if (currentHighlightScript ! null) { currentHighlightScript.DisableHighlight(); currentHighlightScript null; currentHighlightedObject null; } } } }3.4 场景设置与测试将SimpleHighlight脚本挂载到需要高亮的Cube上。在场景中创建一个空物体如命名为“HighlightManager”将MouseHighlightManager脚本挂载上去。在MouseHighlightManager的Inspector面板中设置Highlightable Layer。例如你可以为所有可高亮物体创建一个名为“Highlightable”的Layer并在管理器中选择它。确保你的可高亮Cube的Layer被设置为“Highlightable”。运行游戏将鼠标移动到Cube上它应该会变成你设置的highlightColor默认为黄色。实操心得这种直接修改material.color的方式有一个大坑它修改的是共享材质的实例。如果你在Start中直接使用renderer.materialUnity会为这个物体创建一个该材质的独立副本Instance。这会导致两个问题一是内存开销增加每个高亮物体都有一个材质副本二是如果你有多个相同材质的物体它们的高亮颜色会互相干扰。更优的做法是使用MaterialPropertyBlock我们会在高级优化部分详细讲解。4. 进阶实现使用Shader实现高级高亮效果直接改颜色效果比较生硬。在现代游戏开发中更流行使用着色器Shader来实现发光、外描边、内发光等更炫酷的效果。这里我们介绍两种主流方式通过代码修改Shader参数以及使用Shader Graph可视化编辑。4.1 通过代码控制Standard Shader的“自发光”EmissionUnity的标准着色器Standard Shader有一个“自发光”_EmissionColor属性。我们可以通过修改它来实现发光效果并且这个属性不受场景灯光影响非常适合做高亮提示。第一步准备材质为你的高亮物体创建一个新材质。在材质的Shader下拉菜单中选择“Standard”。勾选“Emission”属性并为其指定一个基础纹理或颜色可以是黑色。关键一步需要将材质的“Global Illumination”模式从“Realtime”改为“Baked”或“None”否则实时修改Emission可能不生效或性能较差。第二步修改高亮脚本我们创建一个新的脚本EmissionHighlight。using UnityEngine; public class EmissionHighlight : MonoBehaviour { public Color emissionHighlightColor Color.cyan; // 发光高亮色 private Color originalEmissionColor; // 原始发光色 private Renderer objectRenderer; private Material objectMaterial; // 保存材质实例 private static int emissionColorID Shader.PropertyToID(_EmissionColor); // 性能优化缓存属性ID void Start() { objectRenderer GetComponentRenderer(); if (objectRenderer ! null) { // 获取材质实例 objectMaterial objectRenderer.material; if (objectMaterial ! null) { // 保存初始的自发光颜色 originalEmissionColor objectMaterial.GetColor(emissionColorID); } } } public void EnableEmissionHighlight() { if (objectMaterial ! null) { // 设置新的自发光颜色并启用关键字某些Shader需要 objectMaterial.SetColor(emissionColorID, emissionHighlightColor); // 为了让自发光在场景中可见需要将强度乘以一个系数或者启用全局光照关键字 // 动态为材质启用EMISSION关键字 objectMaterial.EnableKeyword(_EMISSION); // 如果使用HDRP/URP可能需要不同的处理方式 } } public void DisableEmissionHighlight() { if (objectMaterial ! null) { // 恢复原始自发光颜色 objectMaterial.SetColor(emissionColorID, originalEmissionColor); // 如果原始颜色是黑色可以禁用EMISSION关键字以节省性能 if (originalEmissionColor Color.black) { objectMaterial.DisableKeyword(_EMISSION); } } } // 当物体被禁用或销毁时确保清理如果是实例化材质可能需要销毁 void OnDestroy() { if (objectMaterial ! null) { // 如果是运行时创建的材质实例最好销毁以防内存泄漏 // Destroy(objectMaterial); } } }第三步更新管理器你需要修改MouseHighlightManager让它去获取并调用EmissionHighlight组件逻辑与之前类似。注意事项使用自发光做高亮时需要确保场景的后期处理Post Processing中启用了泛光Bloom效果否则发光感会非常弱。同时频繁修改材质属性每帧对性能有影响尤其是在移动端。4.2 使用Shader Graph制作自定义高亮着色器URP/HDRP对于URP通用渲染管线或HDRP高清渲染管线使用Shader Graph是更灵活、更强大的选择。我们可以创建一个专门用于高亮的着色器。目标创建一个Shader它有一个“高亮强度”_HighlightIntensity参数。当强度为0时显示原始颜色当强度增加时物体表面叠加一层可自定义颜色的发光效果。操作步骤在Project窗口中右键 - Create - Shader Graph - URP - Lit Shader Graph根据你的管线选择。将其命名为“HighlightShader”。双击打开Shader Graph编辑器。关键节点设置添加一个Color类型的属性命名为_HighlightColor用于控制高光颜色。添加一个Float类型的属性命名为_HighlightIntensity范围0-1用于控制高光强度。主节点PBR Master的Base Color输入原本连接着原始颜色。我们需要将其修改为Lerp(Original Color, _HighlightColor, _HighlightIntensity)。这样当强度为0时输出原色为1时输出高亮色中间值则是混合。为了有发光效果可以将混合后的颜色也连接到Emission输入并乘以_HighlightIntensity来控制发光亮度。保存Shader Graph并基于它创建一个新的材质。编写一个控制脚本动态修改材质的_HighlightIntensity属性即可。Shader Graph控制脚本示例public class ShaderGraphHighlight : MonoBehaviour { private Renderer rend; private Material mat; private float targetIntensity 0f; private float currentIntensity 0f; public float fadeSpeed 5f; // 淡入淡出速度 void Start() { rend GetComponentRenderer(); mat rend.material; // 注意材质实例化问题 } void Update() { // 使用Mathf.Lerp平滑过渡高亮强度避免突兀 currentIntensity Mathf.Lerp(currentIntensity, targetIntensity, Time.deltaTime * fadeSpeed); mat.SetFloat(_HighlightIntensity, currentIntensity); } public void EnableHighlight() { targetIntensity 1.0f; } public void DisableHighlight() { targetIntensity 0.0f; } }这种方法的好处是效果完全自定义可以实现描边、溶解、扫描线等各种高亮特效且通过参数控制过渡可以非常平滑。5. 性能优化与工程化实践基础功能实现后我们需要关注性能、内存和代码结构使其能胜任真实项目。5.1 使用MaterialPropertyBlock替代修改Material直接修改material.property会导致材质实例化Instantiate每个高亮物体都会产生一个独立的材质副本造成Draw Call增加和内存浪费。MaterialPropertyBlock允许我们直接向Renderer传递属性值而无需创建材质副本。优化后的高亮脚本using UnityEngine; public class OptimizedHighlight : MonoBehaviour { public Color highlightColor Color.green; private Renderer objectRenderer; private MaterialPropertyBlock propertyBlock; private static int colorID Shader.PropertyToID(_BaseColor); // 假设Shader中颜色属性名为_BaseColor void Start() { objectRenderer GetComponentRenderer(); propertyBlock new MaterialPropertyBlock(); // 初始时获取当前渲染器的属性块并保存原始颜色 objectRenderer.GetPropertyBlock(propertyBlock); // 注意这里我们假设初始属性块里已经有_BaseColor如果没有可能需要从material里读取 // Color original objectRenderer.material.GetColor(colorID); // 但为了通用性我们只在开启高亮时设置高亮色关闭时清空属性块即可。 } public void EnableHighlight() { if (propertyBlock null) propertyBlock new MaterialPropertyBlock(); // 先获取现有的属性块避免覆盖其他属性 objectRenderer.GetPropertyBlock(propertyBlock); // 设置高亮颜色属性 propertyBlock.SetColor(colorID, highlightColor); // 将属性块应用回渲染器 objectRenderer.SetPropertyBlock(propertyBlock); } public void DisableHighlight() { if (propertyBlock null) propertyBlock new MaterialPropertyBlock(); // 清空属性块物体将使用其原始材质的所有属性 propertyBlock.Clear(); objectRenderer.SetPropertyBlock(propertyBlock); // 或者更精确的做法是只清除我们修改过的属性 // objectRenderer.GetPropertyBlock(propertyBlock); // propertyBlock.SetColor(colorID, Color.clear); // 或一个表示“无”的值 // objectRenderer.SetPropertyBlock(propertyBlock); } }核心要点MaterialPropertyBlock只覆盖指定的属性其他属性仍使用原始材质的值。多个物体可以共享同一个材质但通过不同的PropertyBlock显示不同的颜色这极大地提升了性能。5.2 对象池与事件驱动管理当场景中有成百上千个可交互物体时每帧对每个物体进行射线检测是不可行的。我们需要一个中心化的、高效的管理器。思路注册机制所有可高亮物体在启用时向管理器注册自己禁用时注销。管理器维护一个可高亮物体的列表。智能检测不需要每帧检测所有物体。管理器每帧只执行一次从摄像机到鼠标的射线检测。事件通知当射线检测结果发生变化时命中新物体/离开旧物体管理器通过事件C#的event或UnityEvent通知对应的物体而不是让物体自己每帧查询。高效管理器示例框架using System.Collections.Generic; using UnityEngine; using UnityEngine.Events; public class AdvancedHighlightManager : MonoBehaviour { public UnityEventGameObject OnHighlightEnter; // 高亮进入事件 public UnityEventGameObject OnHighlightExit; // 高亮离开事件 private RaycastHit currentHit; private GameObject lastHighlightedObject; private HashSetCollider highlightableColliders new HashSetCollider(); void Update() { PerformRaycast(); } void PerformRaycast() { Ray ray Camera.main.ScreenPointToRay(Input.mousePosition); RaycastHit hit; bool hasHit Physics.Raycast(ray, out hit, 100f); if (hasHit) { // 检查命中的碰撞体是否在可高亮集合中 if (highlightableColliders.Contains(hit.collider)) { // 如果命中了新的物体 if (hit.collider.gameObject ! lastHighlightedObject) { // 触发上一个物体的离开事件 if (lastHighlightedObject ! null) { OnHighlightExit?.Invoke(lastHighlightedObject); } // 更新当前物体并触发进入事件 lastHighlightedObject hit.collider.gameObject; OnHighlightEnter?.Invoke(lastHighlightedObject); } // 如果命中的是同一个物体不做任何事 } else { // 命中了不可高亮的物体触发离开事件 if (lastHighlightedObject ! null) { OnHighlightExit?.Invoke(lastHighlightedObject); lastHighlightedObject null; } } } else { // 什么都没命中触发离开事件 if (lastHighlightedObject ! null) { OnHighlightExit?.Invoke(lastHighlightedObject); lastHighlightedObject null; } } } // 供可高亮物体调用的注册/注销方法 public void RegisterHighlightable(Collider col) highlightableColliders.Add(col); public void UnregisterHighlightable(Collider col) highlightableColliders.Remove(col); }可高亮物体脚本只需在OnEnable时注册在OnDisable时注销并监听管理器的事件即可。这样检测逻辑集中在管理器效率极高。5.3 多材质对象与子物体处理一个复杂的模型可能由多个子网格Mesh和材质组成。我们的高亮需要能覆盖整个模型。解决方案遍历所有Renderer在Start函数中使用GetComponentsInChildrenRenderer(true)获取物体及其所有子物体上的渲染器并存储在一个数组里。批量处理在EnableHighlight和DisableHighlight方法中遍历这个渲染器数组对每一个渲染器应用MaterialPropertyBlock或修改材质属性。处理材质数组如果一个渲染器有多个材质materials数组则需要遍历每一个材质进行处理。示例代码片段private Renderer[] allRenderers; private ListMaterialPropertyBlock propertyBlocks; void Start() { allRenderers GetComponentsInChildrenRenderer(true); propertyBlocks new ListMaterialPropertyBlock(); for (int i 0; i allRenderers.Length; i) { propertyBlocks.Add(new MaterialPropertyBlock()); // 可以初始化保存原始属性... } } public void EnableHighlightForAll() { for (int i 0; i allRenderers.Length; i) { var rb allRenderers[i]; var pb propertyBlocks[i]; rb.GetPropertyBlock(pb); pb.SetColor(_BaseColor, highlightColor); rb.SetPropertyBlock(pb); } }6. 常见问题排查与实战技巧在实际开发中你肯定会遇到各种各样的问题。这里我总结了一份“避坑指南”。6.1 问题排查速查表问题现象可能原因解决方案鼠标悬停无反应1. 物体没有Collider。2. Collider被禁用或设置为Trigger但未正确检测。3. 物体Layer不在射线检测的LayerMask中。4. 摄像机不是Camera.main或射线起点错误。5. 脚本执行顺序问题管理器未初始化。1. 为物体添加Collider。2. 检查Collider组件勾选状态若是Trigger需在射线检测中使用Physics.Overlap或调整代码逻辑。3. 检查物体Layer和管理器LayerMask设置。4. 使用Camera.current或指定摄像机引用。5. 确保管理器脚本在场景中且已启用。高亮闪烁On/Off快速切换1. 物体由多个子碰撞体组成射线在它们之间切换。2. 鼠标指针轻微抖动导致射线在物体边缘反复进出。3. 每帧检测逻辑中currentHighlightedObject的比较逻辑有误。1. 为父物体添加一个包裹整个模型的单一碰撞体。2. 添加一个短暂的延迟计时器Cooldown例如离开后0.1秒才真正取消高亮。3. 仔细检查if (currentHighlightedObject ! hitObject)这行代码的逻辑。高亮后颜色无法恢复1. 直接修改了renderer.material.color导致材质实例化且原始颜色未保存或保存错误。2.DisableHighlight方法未被调用。3. 使用MaterialPropertyBlock后清空逻辑有误。1. 使用MaterialPropertyBlock或确保在Start中正确保存原始材质/颜色。2. 检查射线检测逻辑确保鼠标移开时能触发取消事件。3. 使用propertyBlock.Clear()或重新设置回原始值。UI物体遮挡导致3D物体无法高亮UI的Graphic Raycaster默认会阻挡物理射线。1. 为UI Canvas设置一个不同的Layer并在物理射线检测的LayerMask中排除该层。2. 使用EventSystem.current.IsPointerOverGameObject()判断鼠标是否在UI上如果是则跳过3D物体检测。移动端触摸高亮不准确直接用Input.mousePosition在移动端可能不适用。使用Input.touches数组获取触摸位置并转换为射线。例如Ray ray Camera.main.ScreenPointToRay(Input.GetTouch(0).position);性能低下帧率下降1. 每帧对大量物体进行GetComponent调用。2. 每帧实例化新的MaterialPropertyBlock或Material。3. 射线检测距离maxDistance过长或未使用LayerMask过滤。1. 在Start或Awake中缓存组件引用。2. 复用MaterialPropertyBlock避免在循环中new。3. 合理设置检测距离和层级使用Physics.RaycastNonAlloc进行无GC分配检测。6.2 实战技巧与心得分层管理LayerMask是利器永远不要对所有层进行射线检测。为你需要高亮的物体如“Interactable”、“Enemy”、“Item”设置专用层并在管理器的LayerMask中精确指定。这能大幅提升检测效率和准确性。考虑使用Physics.SphereCast或Physics.BoxCast如果你的物体很小或者希望高亮区域更“宽松”即鼠标在物体附近就能触发可以使用Physics.SphereCast它从起点发射一个球体射线检测范围更大。为高亮添加平滑过渡直接切换颜色很生硬。可以在脚本中使用Mathf.Lerp或Color.Lerp结合Time.deltaTime让高亮颜色和强度有一个淡入淡出的动画效果体验会好很多。上文Shader Graph示例中的Lerp和fadeSpeed就是为此设计。高亮反馈的多样性不要局限于改颜色。可以结合动画缩放、旋转、粒子特效、播放音效等多种方式给玩家更丰富的反馈。例如高亮时物体轻微脉动或者发出“嗡”的一声。在编辑器中调试在MouseHighlightManager的Update里可以使用Debug.DrawRay(ray.origin, ray.direction * maxDistance, Color.red);来可视化射线非常有助于调试检测范围。处理屏幕空间UI与世界空间物体的混合在复杂的UI界面如背包、菜单覆盖下你可能希望鼠标在UI上时背后的3D物体不高亮。一定要在物理射线检测前先判断EventSystem.current.IsPointerOverGameObject()。鼠标悬停高亮是一个“小功能大世界”的典型。从简单的颜色变换到基于Shader Graph的炫酷特效从直接修改材质到使用MaterialPropertyBlock进行性能优化再到构建一个事件驱动的中心化管理器每一步都蕴含着对Unity引擎更深层次的理解。希望这篇长文能帮你彻底掌握这个功能并应用到你的下一个精彩项目中。记住最好的学习就是动手实践不妨现在就创建一个新场景把上面的代码敲一遍看看会有什么效果。