1. 悬浮 3D 物体时鼠标光标消失,问题到底出在哪
在 Unity 里做 UI 悬浮交互时,很多开发者会遇到一个很典型的现象:鼠标移到 3D 物体上,本来应该切换成自定义光标图案,结果光标要么直接消失,要么还是系统默认箭头,代码里明明写了Cursor.SetCursor却毫无反应。这个问题的核心检索词就是 Unity、UI、3D、Cursor、鼠标光标,它本质上不是「代码写错了」这么简单,而是纹理导入设置、Raycast 遮挡、Canvas 层级三件事叠加在一起的结果。
我先把结论摆出来:绝大多数情况下,光标图案无法显示,第一嫌疑是光标贴图的 Texture Type 没有改成 Cursor。Unity 对光标纹理有专门的导入类型,如果它还是默认的 Sprite 或 Default,Cursor.SetCursor拿到的是一张不满足光标格式要求的纹理,运行时就会静默失败,既不报错也不显示。第二嫌疑是 Raycast 被 UI 挡住,你以为鼠标悬停在 3D 物体上,其实 Physics Raycast 根本没打到它。第三嫌疑是 Canvas 的 Sort Order 或渲染模式让 UI 盖住了交互判定区域。
这篇文章适合正在做 Unity UI 与 3D 混合交互的开发者,尤其是刚接触Cursor.SetCursor、Physics Raycaster、EventSystem 这套组合的人。我会从 Cursor 配置讲起,给出可直接复制的代码片段,再补上 TaoToken 统一 Key 通道下的config.toml骨架,方便你在排查过程中顺手把模型调用通道也理顺。整个流程你可以跟着一步步做,不需要额外猜测。
需要先说明一点:光标显示问题属于纯客户端渲染与输入逻辑,和网络通道无关。但为什么还要提 TaoToken?因为很多团队在开发期会用统一 Key 通道去调用模型做代码辅助、日志分析,把配置集中管理能减少环境切换的干扰。下面第 2 节会讲清楚它在整个工作流里的位置,你按需接入即可。
2. TaoToken 统一 Key 通道:把配置集中管理
在排查 Unity 光标问题的过程中,你可能会让模型帮你分析报错日志、生成 Raycast 调试代码,或者解释Cursor.SetCursor的参数含义。如果每个项目、每台机器都各自维护一套 Key 和地址,切换环境时很容易乱。TaoToken 提供的是一个统一 Key 通道,官网地址是 https://taotoken.net/?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content= ,API 入口是 https://taotoken.net/api ,它本身不替代你的编辑器,也不参与 Unity 运行时光标逻辑,只是把模型调用的凭据和地址收敛到一处。
它的定位可以这样理解:你在本地写一个config.toml,把 base_url 和 api_key 填好,之后无论是命令行工具、IDE 插件还是自己写的脚本,都读同一份配置。这样做的直接好处是,当你从一台机器换到另一台,或者从测试 Key 换到正式 Key,只改一个文件,不用满项目搜api_key。对于 Unity 项目来说,这个配置文件通常放在项目根目录之外的用户目录,避免被误提交进版本库。
如果你只是想让模型帮你解释一段 Cursor 代码,用模型对话入口就够了,地址是 https://taotoken.net/api?utm_source=taotoken_aicg_blog_end&utm_content=model_chat&utm_campaign=rewrite 。如果你在做长期的编码辅助、想让 Agent 持续参与项目,那更适合用 Coding Plan,入口是 https://taotoken.net/api?utm_source=taotoken_aicg_blog_end&utm_content=coding_plan&utm_campaign=rewrite 。需要生成或管理 Key 的时候走控制台和 API Keys 页面,分别是 https://taotoken.net/api?utm_source=taotoken_aicg_blog_end&utm_content=console&utm_campaign=rewrite 和 https://taotoken.net/api?utm_source=taotoken_aicg_blog_end&utm_content=api_keys&utm_campaign=rewrite 。文档在 https://taotoken.net/api?utm_source=taotoken_aicg_blog_end&utm_content=doc&utm_campaign=rewrite ,Claude Code 相关的接入说明在 https://taotoken.net/api?utm_source=taotoken_aicg_blog_end&utm_content=claudecode&utm_campaign=rewrite 。
注意:TaoToken 是模型调用的统一通道,和 Unity 光标渲染没有直接关系。把它放在这里,是为了让你在排查问题时有一个稳定的辅助工具链,而不是让你把光标逻辑接到网络上。
3. 可复制配置:Cursor 设置代码与 config.toml 骨架
3.1 光标贴图的导入设置
先把最容易被忽略的一步做掉。选中你用作光标的 PNG 贴图,在 Inspector 里找到 Texture Type,把它从默认值改成 Cursor。这一步不做,后面代码写得再对也没用。改完之后点 Apply,Unity 会按光标格式重新导入这张纹理。
改完导入类型后,还要确认几个参数:Texture Shape 选 2D,Alpha Is Transparency 勾上(如果你的光标有透明边缘),Wrap Mode 选 Clamp,Filter Mode 选 Point 可以避免缩放模糊。这些不是必须全改,但透明边缘发黑、光标模糊这类问题,基本都出在这里。
3.2 悬浮检测与光标切换代码
下面这段代码可以直接挂在一个带 Collider 的 3D 物体上,或者挂在一个管理器上统一处理。它做三件事:用 Camera 发射射线检测鼠标是否悬停在目标物体上,悬停时切换自定义光标,离开时恢复默认光标。
using UnityEngine; public class HoverCursorChanger : MonoBehaviour { [SerializeField] private Texture2D hoverCursor; [SerializeField] private Vector2 hotspot = Vector2.zero; [SerializeField] private CursorMode cursorMode = CursorMode.Auto; [SerializeField] private float rayDistance = 100f; private Camera mainCamera; private bool isHovering = false; private void Awake() { mainCamera = Camera.main; if (hoverCursor == null) { Debug.LogError("hoverCursor 未赋值,请在 Inspector 中指定光标贴图"); } } private void Update() { if (mainCamera == null || hoverCursor == null) return; Ray ray = mainCamera.ScreenPointToRay(Input.mousePosition); bool hitThisFrame = false; if (Physics.Raycast(ray, out RaycastHit hit, rayDistance)) { if (hit.collider.gameObject == gameObject) { hitThisFrame = true; } } if (hitThisFrame && !isHovering) { isHovering = true; Cursor.SetCursor(hoverCursor, hotspot, cursorMode); } else if (!hitThisFrame && isHovering) { isHovering = false; Cursor.SetCursor(null, Vector2.zero, cursorMode); } } }这段代码的关键点是Cursor.SetCursor的第一个参数必须是导入类型为 Cursor 的 Texture2D,hotspot 是光标的热点坐标,通常取图片的左上角或中心。CursorMode.Auto会让 Unity 在支持硬件光标时用硬件光标,不支持时回退到软件光标。
3.3 排除 UI 遮挡的 Raycast 写法
如果你的场景里有 Canvas,鼠标可能先被 UI 接走了。这时候 Physics.Raycast 依然能打到 3D 物体,但 EventSystem 的判定会优先给 UI。要判断当前是否真的悬停在 3D 物体上,可以加一层 EventSystem 检查:
using UnityEngine; using UnityEngine.EventSystems; public class HoverCursorChanger : MonoBehaviour { [SerializeField] private Texture2D hoverCursor; [SerializeField] private Vector2 hotspot = Vector2.zero; [SerializeField] private CursorMode cursorMode = CursorMode.Auto; [SerializeField] private float rayDistance = 100f; private Camera mainCamera; private bool isHovering = false; private void Awake() { mainCamera = Camera.main; } private void Update() { if (mainCamera == null || hoverCursor == null) return; bool pointerOverUI = EventSystem.current != null && EventSystem.current.IsPointerOverGameObject(); Ray ray = mainCamera.ScreenPointToRay(Input.mousePosition); bool hitThisFrame = false; if (!pointerOverUI && Physics.Raycast(ray, out RaycastHit hit, rayDistance)) { if (hit.collider.gameObject == gameObject) { hitThisFrame = true; } } if (hitThisFrame && !isHovering) { isHovering = true; Cursor.SetCursor(hoverCursor, hotspot, cursorMode); } else if (!hitThisFrame && isHovering) { isHovering = false; Cursor.SetCursor(null, Vector2.zero, cursorMode); } } }IsPointerOverGameObject会告诉你鼠标当前是否在 UI 元素上。如果返回 true,就跳过 3D 悬停判定,避免光标在 UI 和 3D 物体之间来回闪烁。
3.4 config.toml 骨架
如果你用 TaoToken 做辅助开发,配置文件可以这样写。把 api_key 换成你在控制台生成的 Key,base_url 用 API 入口地址。
# TaoToken 统一 Key 通道配置骨架 # 文档:https://taotoken.net/api?utm_source=taotoken_aicg_blog_end&utm_content=doc&utm_campaign=rewrite [default] base_url = "https://taotoken.net/api" api_key = "sk-你的Key" timeout = 60 [model] name = "claude-sonnet" max_tokens = 4096 temperature = 0.2 [logging] level = "info"这个骨架只保留必要字段,你可以按文档补充其他参数。注意不要把真实 Key 提交到 Git,建议用环境变量或本地忽略文件。
4. 验证请求与成功结果
4.1 验证光标切换
把上面的脚本挂到一个 Cube 上,给 Cube 加 Box Collider,把光标贴图拖到 hoverCursor 字段。运行场景,鼠标移到 Cube 上,光标应该变成你的自定义图案;移开,恢复默认箭头。如果没变化,先看 Console 有没有报错,再检查贴图的 Texture Type 是不是 Cursor。
一个快速自检方法:在Cursor.SetCursor调用前后各加一行日志,确认代码确实执行到了。
Debug.Log("准备切换光标,贴图:" + hoverCursor.name); Cursor.SetCursor(hoverCursor, hotspot, cursorMode); Debug.Log("光标切换调用完成");如果日志打印了但光标没变,问题一定在贴图导入设置或平台支持上,而不是逻辑。
4.2 验证 TaoToken 通道
配置文件写好后,可以用一个最简单的请求确认通道可用。如果你用命令行工具,按文档里的方式发起一次对话请求,能拿到正常返回就说明 Key 和地址没问题。这一步的意义是排除「辅助工具本身不通」的干扰,让你在排查 Unity 问题时不用怀疑工具链。
提示:验证通道时不要贴真实 Key 到公开渠道,控制台里可以随时吊销和重建 Key。
5. 本篇常见错排查
5.1 光标贴图 Texture Type 没改
这是最高频的原因。表现是代码执行了、日志也打了,但光标纹丝不动。解决方式就是选中贴图,把 Texture Type 改成 Cursor,Apply 后重试。很多人以为只要是一张 PNG 就能当光标,实际上 Unity 需要专门的导入类型来生成光标所需的内部格式。
5.2 Raycast 被 UI 挡住
表现是鼠标在 3D 物体上,但光标不切换,或者 UI 和 3D 之间来回跳。解决方式是加上EventSystem.current.IsPointerOverGameObject()判断,或者调整 Canvas 的 Sort Order 和 Raycast Target 设置。如果你的 UI 不需要接收点击,把 Image 的 Raycast Target 取消勾选,能减少很多误判。
5.3 Canvas 渲染模式与层级问题
Screen Space - Overlay 的 Canvas 永远盖在 3D 物体上面,如果它铺满全屏且 Raycast Target 开着,鼠标事件会被它接走。可以改成 Screen Space - Camera,或者把不需要交互的 UI 元素的 Raycast Target 关掉。另外检查 Canvas 的 Sort Order,数值大的会盖住数值小的。
5.4 热点坐标设置错误
hotspot 如果设得离图片太远,光标可能显示在鼠标位置之外,看起来像「消失了」。一般取图片中心或左上角,具体看你的光标设计。可以先设成 Vector2.zero 测试,确认能显示后再微调。
5.5 平台差异
不同平台对硬件光标和软件光标支持不一样。CursorMode.Auto在大多数桌面平台没问题,但如果遇到异常,可以显式指定CursorMode.ForceSoftware测试。WebGL 平台对自定义光标支持有限,需要单独验证。
6. 把工具链和交互逻辑分开管理
排查完这一轮,你会发现 Unity 光标问题几乎都落在三个点上:贴图导入类型、Raycast 遮挡、Canvas 层级。把这三件事按顺序过一遍,基本能覆盖九成以上的场景。代码层面,Cursor.SetCursor本身很稳定,出问题往往是它拿到的纹理不合法,或者悬停判定根本没触发。
工具链这边,TaoToken 的统一 Key 通道适合把模型调用配置集中起来,需要生成 Key 就去 https://taotoken.net/api?utm_source=taotoken_aicg_blog_end&utm_content=api_keys&utm_campaign=rewrite ,接入细节看 https://taotoken.net/api?utm_source=taotoken_aicg_blog_end&utm_content=doc&utm_campaign=rewrite ,长期编码辅助用 https://taotoken.net/api?utm_source=taotoken_aicg_blog_end&utm_content=coding_plan&utm_campaign=rewrite 。它不参与你的光标渲染,但能让你在调试过程中少折腾环境配置。
最后留一个实用习惯:每次改完光标贴图导入设置,先在一个空场景里用最简单的 Cube 验证,确认光标能切换,再回到复杂场景排查 UI 遮挡。这样能把变量控制到最少,定位速度会快很多。