mcpField
H
hz-unity-meta-quest-ui

hz-unity-meta-quest-ui

github.com/meta-quest设计与 UI241239 次安装5 周前更新
代码仓库

关于 hz-unity-meta-quest-ui

面向 Unity Meta Quest 项目的 VR UI 制作指南,涵盖 Canvas 配置、TextMesh Pro、布局验证与交互设置。通过明确的尺寸规则和常见问题排查,帮助开发者构建可读且可操作的头显界面。

主要功能

  1. 01配置 World Space Canvas
  2. 02设置 TextMesh Pro 与 UI 尺寸
  3. 03验证画布和子元素布局
  4. 04添加射线、触碰或注视交互
  5. 05排查 VR UI 常见问题

适用场景

  1. 01创建 Quest 菜单和交互面板
  2. 02修复文字颜色或清晰度问题
  3. 03排查头显中按钮无法点击
  4. 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 10025cm x 10cm
按钮(标准)300 x 12030cm x 12cm
按钮(大)400 x 15040cm x 15cm
下拉菜单500 x 14050cm x 14cm
滑块600 x 8060cm x 8cm
切换开关120 x 12012cm x 12cm
输入字段800 x 14080cm x 14cm
面板/菜单1200-2000 x 800-14001.2-2m x 0.8-1.4m
按钮间距

交互元素之间至少间隔 50 个单位(5cm),以避免误触。

字体大小(画布缩放比例为 0.001 时)
距离最小值舒适大号/标题
1.5m32pt40-48pt60-72pt
2.0m36pt48-56pt72-84pt
2.5m40pt52-64pt84-96pt
3.0m48pt64-72pt96-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)
按钮在编辑器中可用,在设备上不可用缺少交互 RigOVRQuickActionsAPI.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、从安全模式恢复。

评论

支持 Markdown