关于 hz-unity-meta-quest-ui
面向 Unity Meta Quest 项目的 VR UI 制作指南,涵盖 Canvas 配置、TextMesh Pro、布局验证与交互设置。通过明确的尺寸规则和常见问题排查,帮助开发者构建可读且可操作的头显界面。
主要功能
- 01配置 World Space Canvas
- 02设置 TextMesh Pro 与 UI 尺寸
- 03验证画布和子元素布局
- 04添加射线、触碰或注视交互
- 05排查 VR UI 常见问题
适用场景
- 01创建 Quest 菜单和交互面板
- 02修复文字颜色或清晰度问题
- 03排查头显中按钮无法点击
- 04调整画布比例与元素位置
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ meta-quest/ agentic-tools --skill hz-unity-meta-quest-ui安装后在 Claude Code 中输入/hz-unity-meta-quest-ui确认已加载。
附带文件references/unity-mcp-fallback.md
详细介绍
Meta VR UI 设置
何时使用此技能
在以下情况下自动使用此技能:
- 为 VR 设置 Canvas
- 在 VR 项目中使用 TextMesh Pro 创建 UI 文本
- 在 VR 中添加按钮、滑块或其他交互式 UI
- 用户报告 VR 中出现粉色/洋红色文本、按钮无法点击或 UI 尺寸问题
- 在 Canvas 上配置 VR 交互(射线或戳碰)
操作 Editor
使用 unity-cli 在已打开的 Editor 中构建 UI,而不是手动编辑场景 YAML——Editor 会将更改应用到当前活动场景。
unity status --format json # look for state "ready"
unity command run_script --file AgentScripts/BuildUI.cs --entry BuildUI.Run --format json
Canvas 的构建涉及多个组件,因此应放在 run_script .cs 文件中(保存在 Assets/ 之外,路径相对于项目根目录),而不是串联执行单条命令。添加 --dry_run true,可在脚本修改场景之前检查编译结果——但请读取 data.result.success 的结果:即使编译失败,外层仍会返回 success: true 和退出代码 0,错误信息位于 data.result.diagnostics 中。连接、--project-path 和 Safe Mode 恢复请参阅 unity-cli 技能;MCP 等效方法请参阅
references/unity-mcp-fallback.md。
读取和写入单个属性时,直接执行命令比使用脚本更省资源:
unity command get_component_properties --target MenuUI --type Canvas --format json
unity command set_transform --target MenuUI --position "[0,1.5,2]" --format json
unity command find_gameobjects --name MenuUI --format json
数组参数必须在一个参数中传入 JSON 数组(--position "[0,1.5,2]")。以空格或逗号分隔的组件会被拒绝,并返回 INVALID_COMMAND_ARGS。注意,set_transform 写入的是局部位置——对于有父级的 Canvas,请参阅 unity-placement 技能。
前提条件:TMP Essential Resources
创建任何 VR UI 之前,请确认已导入 TMP 资源:
unity command find_assets --name "LiberationSans SDF" --format json
data.result.count 为 0 表示资源缺失——请先使用 tmp-resources 技能,再继续操作。
第 1 步:创建 World Space Canvas
// AgentScripts/BuildUI.cs
using UnityEditor;
using UnityEditor.SceneManagement;
using UnityEngine;
using UnityEngine.UI;
public static class BuildUI
{
public static string Run()
{
// Adapt the name to match your canvas (e.g., "MainMenu", "SettingsUI")
var go = new GameObject("MenuUI");
var canvas = go.AddComponent<Canvas>();
canvas.renderMode = RenderMode.WorldSpace;
go.AddComponent<GraphicRaycaster>();
// Remove CanvasScaler — not appropriate for VR
var scaler = go.GetComponent<CanvasScaler>();
if (scaler != null)
Object.DestroyImmediate(scaler);
var rt = go.GetComponent<RectTransform>();
rt.localScale = new Vector3(0.001f, 0.001f, 0.001f);
rt.sizeDelta = new Vector2(1920f, 1080f);
rt.position = new Vector3(0f, 1.5f, 2f);
Undo.RegisterCreatedObjectUndo(go, "Create VR Canvas");
EditorSceneManager.MarkSceneDirty(EditorSceneManager.GetActiveScene());
return $"Created VR Canvas '{go.name}'. Scale: {rt.localScale}, Size: {rt.sizeDelta}, Position: {rt.position}";
}
}
确认布局合适后,将其持久化:
unity command save_scene --format json
Canvas 规则
- Render Mode:始终使用 World Space。Screen Space 模式会破坏立体渲染。
- Scale:所有轴均设为 0.001(Canvas 中 1 个单位 = 世界中的 1 毫米)。
- CanvasScaler:移除它。物理尺寸由世界缩放控制,而非屏幕适配。
- 距离:放置在距离用户 1.5-3m 处。不得近于 0.5m。为确保文本可读,最远不超过 5m。
- 物理尺寸公式:
Canvas sizeDelta * scale = meters。例如:1920 * 0.001 = 1.92m 宽。
第 2 步:创建子 UI 元素
所有子元素(面板、按钮、文本)都必须遵循以下规则:
- localScale:始终设为
[1, 1, 1]。不得通过缩放子元素来补偿 Canvas 的缩放。 - localPosition.z:始终设为
0。子元素必须位于 Canvas 平面上。 - 尺寸控制:使用
RectTransform.sizeDelta和锚点,绝不使用缩放。
// AgentScripts/BuildPanel.cs
using TMPro;
using UnityEditor;
using UnityEditor.SceneManagement;
using UnityEngine;
using UnityEngine.UI;
public static class BuildPanel
{
public static string Run()
{
// Replace "MenuUI" with the actual canvas name used in Step 1
var canvas = GameObject.Find("MenuUI");
if (canvas == null) return "ERROR: Canvas 'MenuUI' not found.";
// Panel
var panel = new GameObject("ButtonPanel");
panel.transform.SetParent(canvas.transform, false);
var panelRT = panel.AddComponent<RectTransform>();
panelRT.localScale = Vector3.one;
panelRT.sizeDelta = new Vector2(800f, 600f);
var panelImg = panel.AddComponent<Image>();
panelImg.color = new Color(0.1f, 0.1f, 0.1f, 0.95f);
panelImg.raycastTarget = false;
var layout = panel.AddComponent<VerticalLayoutGroup>();
layout.spacing = 50f;
layout.padding = new RectOffset(80, 80, 100, 100);
layout.childAlignment = TextAnchor.MiddleCenter;
// Button
var btnGO = new GameObject("StartButton");
btnGO.transform.SetParent(panel.transform, false);
var btnRT = btnGO.AddComponent<RectTransform>();
btnRT.localScale = Vector3.one;
btnRT.sizeDelta = new Vector2(400f, 120f);
var btnImg = btnGO.AddComponent<Image>();
btnImg.color = new Color(0.2f, 0.6f, 1f, 1f);
btnGO.AddComponent<Button>();
// Button text
var textGO = new GameObject("Text");
textGO.transform.SetParent(btnGO.transform, false);
var textRT = textGO.AddComponent<RectTransform>();
textRT.localScale = Vector3.one;
textRT.anchorMin = Vector2.zero;
textRT.anchorMax = Vector2.one;
textRT.sizeDelta = Vector2.zero;
var tmp = textGO.AddComponent<TextMeshProUGUI>();
tmp.text = "Start";
tmp.fontSize = 48f;
tmp.color = new Color(0.95f, 0.95f, 0.95f, 1f);
tmp.alignment = TextAlignmentOptions.Center;
tmp.raycastTarget = false;
Undo.RegisterCreatedObjectUndo(panel, "Create VR Panel");
EditorSceneManager.MarkSceneDirty(EditorSceneManager.GetActiveScene());
return $"Created panel with button. Panel scale: {panelRT.localScale}, Button scale: {btnRT.localScale}";
}
}
第 3 步:验证已创建的 UI
使用直接命令对 Canvas 本身进行抽查:
unity command get_component_properties --target MenuUI --type Canvas --format json
unity command get_scene_hierarchy --format json # whole active scene, incl. children
get_scene_hierarchy --path 接受已打开的场景路径,而不是 GameObject——使用 --path MenuUI 会失败,并显示 "Scene 'MenuUI' is not open."。省略该参数,然后从活动场景中读取 MenuUI 子树。
Editor 检查只能做到这一步——请部署构建版本,并从头显中提取 metavr capture screenshot,查看用户实际看到的 UI。
要遍历所有子元素并自动修复,请使用脚本:
// AgentScripts/ValidateUI.cs
using System.Text;
using UnityEngine;
public static class ValidateUI
{
public static string Run()
{
// Replace "MenuUI" with the actual canvas name
var canvas = GameObject.Find("MenuUI");
if (canvas == null) return "ERROR: Canvas not found.";
var crt = canvas.GetComponent<RectTransform>();
var c = canvas.GetComponent<Canvas>();
var sb = new StringBuilder();
if (c.renderMode != RenderMode.WorldSpace)
sb.AppendLine("FAIL: Canvas renderMode is not World Space.");
if (Mathf.Abs(crt.localScale.x - 0.001f) > 0.0001f)
sb.AppendLine($"FAIL: Canvas scale is {crt.localScale}, expected 0.001.");
foreach (Transform child in canvas.GetComponentsInChildren<Transform>(true))
{
if (child == canvas.transform) continue;
var rt = child.GetComponent<RectTransform>();
if (rt == null) continue;
if (rt.localScale != Vector3.one)
{
sb.AppendLine($"FIXED: '{child.name}' had localScale {rt.localScale}, expected (1,1,1).");
rt.localScale = Vector3.one;
}
if (Mathf.Abs(rt.localPosition.z) > 0.01f)
{
sb.AppendLine($"FIXED: '{child.name}' had localPosition.z={rt.localPosition.z}, expected 0.");
rt.localPosition = new Vector3(rt.localPosition.x, rt.localPosition.y, 0f);
}
}
return sb.Length == 0 ? "Validation PASSED. All properties correct." : sb.ToString();
}
}
此操作可捕获的常见问题:
- Canvas 缩放仍为 1,1,1,而不是 0.001
- 子元素被自动缩放以补偿父级(例如,缩放为 1000x)
- 子元素在 Z 轴上发生偏移(例如,localPosition.z = -2500)
第 4 步:添加 VR 交互(仅限交互式 UI)
仅当 Canvas 包含交互元素(按钮、滑块、下拉菜单、开关、输入字段)时,才添加交互。对于仅用于显示的 UI(HUD、分数、标签),请跳过此步骤。
决策树
Does your Canvas have buttons, dropdowns, sliders, toggles, or input fields?
YES -> Add VR interaction. Without it, interactive elements won't work in VR.
NO -> Skip. Display-only UI doesn't need interaction overhead.
接入 — ISDK 快捷操作
支持的入口是 ISDK 快捷操作 API,通过 run_script 调用。签名以及这些调用内置的行为,请参阅
hz-unity-meta-interaction-sdk 技能。
// AgentScripts/AddCanvasInteraction.cs
using Oculus.Interaction.Editor.QuickActions; // Editor-only assembly
using UnityEngine;
public static class AddCanvasInteraction
{
public static string Run()
{
var target = GameObject.Find("MenuUI");
QuickActionsAPI.AddRayCanvasInteraction(target); // or AddPokeCanvasInteraction
return "ok";
}
}
这些调用接收画布的 GameObject,而不是 Canvas 组件——传入 Canvas 会导致编译错误(cannot convert from 'UnityEngine.Canvas' to 'UnityEngine.GameObject')。已验证的签名:
| 交互方式 | 签名 | 适用场景 |
|---|---|---|
| 射线(默认) | AddRayCanvasInteraction(GameObject target, bool fixPointableCanvasModule = true) | 菜单、设置面板,以及任何超出手臂可及范围的 UI |
| 戳碰(近距离) | AddPokeCanvasInteraction(GameObject target, bool fixPointableCanvasModule = true) | 表面上的控制面板、虚拟键盘、道具上的场景化 UI(< 0.8m) |
| 注视 | AddGazeCanvasInteraction(GameObject target, bool enableRayFallback = true, bool addToChildCanvases = true, bool fixPointableCanvasModule = true) | 眼动追踪选择 |
| 两者(高级) | 先射线,再戳碰 | 用户可以指向或触摸的混合式 UI |
射线交互会添加 RayInteractable、PointableCanvas、一个 ISDK_RayCanvasInteraction 子对象,以及一个场景级的 Pointable Canvas Module。戳碰交互会添加 PokeInteractable、PointableCanvas 和近距离碰撞检测。
如果
PointableCanvasModule来自预制体或尚未加载的附加场景,请传入fixPointableCanvasModule: false,否则会在当前打开的场景中再添加一个。
如果你使用的是带有 Meta Unity 扩展的 Unity MCP 服务,则对应的工具为 meta_add_canvas_interaction_ray / meta_add_canvas_interaction_poke —
请参阅 references/unity-mcp-fallback.md。
验证是否已添加交互
unity command get_component_properties --target MenuUI --type PointableCanvas --format json
unity command find_gameobjects --name "Pointable Canvas Module" --format json
当 --target 是 GameObject 时,get_component_properties 需要提供 --type(否则调用会失败:"'type' is required when 'target' is a GameObject."),并且它只针对一个组件,而不是列出所有组件——因此请指定要断言的组件名称。成功表示该组件存在;如果失败信息中列出了类型,则表示该组件不存在。
画布上有 PointableCanvas,且场景中有 Pointable Canvas Module,这才算达标。请注意,PointableElement 的连线会在 Awake() 中赋值,因此在预制体资源上它始终读取为 null——请检查序列化字段:
unity command get_serialized_fields --target <interactable> --field _pointableElement --format json
如果按钮在 Editor 中有效,但在 Meta VR 设备上无效
场景需要在运行时包含一个交互 Rig:
using Oculus.Interaction.OVR.Editor.QuickActions;
OVRQuickActionsAPI.AddOVRInteractionRig(); // also adds an OVRCameraRig if the scene has none
这会提供所有手部/控制器交互器。Rig 的构建方式以及“在不存在 Rig 的情况下创建了可交互对象”的注意事项,请参阅 hz-unity-meta-interaction-sdk。
VR UI 尺寸参考
所有尺寸均假设画布缩放比例为 0.001(1 个单位 = 1mm)。
元素尺寸(最小值)
| 元素 | sizeDelta(单位) | 实际尺寸 |
|---|---|---|
| 按钮(小) | 250 x 100 | 25cm x 10cm |
| 按钮(标准) | 300 x 120 | 30cm x 12cm |
| 按钮(大) | 400 x 150 | 40cm x 15cm |
| 下拉菜单 | 500 x 140 | 50cm x 14cm |
| 滑块 | 600 x 80 | 60cm x 8cm |
| 切换开关 | 120 x 120 | 12cm x 12cm |
| 输入字段 | 800 x 140 | 80cm x 14cm |
| 面板/菜单 | 1200-2000 x 800-1400 | 1.2-2m x 0.8-1.4m |
按钮间距
交互元素之间至少间隔 50 个单位(5cm),以避免误触。
字体大小(画布缩放比例为 0.001 时)
| 距离 | 最小值 | 舒适 | 大号/标题 |
|---|---|---|---|
| 1.5m | 32pt | 40-48pt | 60-72pt |
| 2.0m | 36pt | 48-56pt | 72-84pt |
| 2.5m | 40pt | 52-64pt | 84-96pt |
| 3.0m | 48pt | 64-72pt | 96-120pt |
VR 中切勿使用自动调整大小。切勿使用旧版 Text 组件——始终使用 TextMeshPro。
颜色
- 文本:米白色(0.9, 0.9, 0.9)或深灰色(0.1, 0.1, 0.1)。避免使用纯白色/纯黑色。
- 背景:深色(0.1, 0.1, 0.1, 0.95)或浅色(0.9, 0.9, 0.9, 0.95)。
- 对比度:至少 4.5:1,优先选择 7:1 以上。
- 优先使用不透明背景,而非透明背景(渲染成本更低)。
性能提示
- 对非交互元素(标签、背景、装饰性图像)禁用
raycastTarget。 - 将静态内容和动态内容拆分到不同的画布上,以尽量减少重建。
- 画布重建耗时应小于 1-2ms,以维持 72/90Hz。
- 所有文本元素共享每种字体系列对应的一个 SDF 字体资源。
- 不可见时禁用画布或 GameObject。
故障排除
| 现象 | 原因 | 解决方法 |
|---|---|---|
| 文本呈粉色/洋红色 | 未导入 TMP 资源 | 使用 tmp-resources 技能 |
| 按钮在 VR 中无法点击 | 画布缺少 VR 交互 | QuickActionsAPI.AddRayCanvasInteraction(go) |
| 按钮在编辑器中可用,在设备上不可用 | 缺少交互 Rig | OVRQuickActionsAPI.AddOVRInteractionRig() |
| UI 太小/太大 | 画布缩放比例错误 | 设置为 0.001,并使用验证脚本检查 |
| 子对象在 Z 轴上偏移 | 自动定位错误 | 将所有子对象的 localPosition.z 设置为 0 |
| 子对象缩放为 1000 倍 | 自动缩放补偿 | 将所有子对象的 localScale 设置为 Vector3.one |
| 文本模糊 | 图集分辨率低或字体过小 | 使用 2048x2048+ 的 SDF,字号设为 48+ |
| 帧率下降 | 画布重建过于频繁 | 拆分为静态/动态画布 |
unity command 无法连接 | 编译错误导致编辑器处于安全模式 | 执行 unity pipeline list,修复错误,然后重启 Unity |
检查清单
交互式 UI(按钮、滑块等)
[ ] TMP Essential Resources imported
[ ] Canvas: renderMode = World Space
[ ] Canvas: localScale = [0.001, 0.001, 0.001]
[ ] Canvas: positioned 1.5-3m from user
[ ] All children: localScale = [1, 1, 1]
[ ] All children: localPosition.z = 0
[ ] Sizes via sizeDelta, not scale
[ ] AddRayCanvasInteraction called on Canvas
[ ] PointableCanvas component verified on Canvas
[ ] Pointable Canvas Module exists in scene
[ ] Interaction rig present (AddOVRInteractionRig)
[ ] Buttons >= 250x100 units
[ ] Button spacing >= 50 units
[ ] Text >= 48pt, raycastTarget = false on non-interactive text
[ ] Validation script run and passed
[ ] Scene saved (unity command save_scene)
仅用于显示的 UI(HUD、分数、标签)
[ ] TMP Essential Resources imported
[ ] Canvas: renderMode = World Space
[ ] Canvas: localScale = [0.001, 0.001, 0.001]
[ ] All children: localScale = [1, 1, 1]
[ ] All children: localPosition.z = 0
[ ] Text >= 48pt
[ ] raycastTarget = false on all elements
[ ] NO interaction components needed
[ ] Scene saved (unity command save_scene)
与其他技能集成
- tmp-resources:先使用此技能导入 TMP Essential Resources,然后再创建任何 UI。
- hz-unity-meta-interaction-sdk:负责步骤 4 中使用的 Quick Actions API。
- unity-placement:用于相对于其他场景对象定位画布。
- unity-cli:连接到编辑器、使用
--project-path、从安全模式恢复。