返回博客

Godot on XREAL AURA:原生空间开发实用指南

发布于 2026年11月3日
Godot on XREAL AURA:原生空间开发实用指南 借助 AI 生成

概要

了解在 XREAL AURA 上使用 Godot 进行原生空间开发的完整流程:从设备端编辑器安装、OpenXR Spatial Containers 场景构建、光学透视渲染适配,到锚点漂移规避与发布前最佳实践,涵盖容器丢失恢复与真机验证,并掌握桌面资源制作与眼镜端调优的迭代工作流。

你目前的 XR 构建流程意味着要连上桌面、导出 APK、通过 ADB 侧载、戴上头显、发现一个拼写错误,然后重新再来一遍。XREAL AURA 彻底移除了这个循环,而且 Godot 就在首发阵容里,带有一个能在眼镜本身上运行的完整编辑器。这改变了“迭代”在空间开发中的含义。

本指南逐步介绍实际工作流:让编辑器在 AURA 上运行,使用 OpenXR Spatial Containers 构建空间场景,处理让光学眼镜不同于 VR 头显的透传/透视模型,以及首次导出时必然会踩到的陷阱。

为什么光学透视会颠覆你的设计假设

XREAL AURA 是 Android XR 上第一款光学透视设备。这一句话就打破了许多 Godot XR 教程中固有的假设。

在 VR 头显中,头显摄像头渲染一切,用户只能看到你的虚拟环境。而在光学透视眼镜上,用户是透过镜片看真实世界的,你的渲染内容叠加在上面。这不同于 Quest 上的视频透视——视频透视是用摄像头捕捉现实再重新显示,会引入延迟和光线损失。光学透视对真实世界是零合成延迟的。

对渲染预算的实际影响:

  • 你无法让真实世界变暗。 在透明显示器上,黑色就是透明。你没有不透明背景,因此依赖深色面板对比度的 UI 在明亮房间里会失效。
  • 外观趋近叠加(additive)的内容胜出。 明亮的自发光内容清晰可见;暗色、低饱和内容会消失。调色板亮度请设为 VR 的 2-3 倍。
  • 视场角比 VR 更窄。 放在极端角度的内容会超出显示区域。主要交互目标应保持在正前方中心约 ±30 度以内。

如果你之前发布过 VR 项目,请做好重新调整每个材质和每个 UI 面板的准备。每个场景留出一整天的时间。

在眼镜上安装 Godot 编辑器

编辑器通过 Google Play 分发,而不是侧载 APK。这一点很重要,因为这意味着你可以像更新其他应用一样更新它。

设置步骤:

  1. 从 Play Store 在 XREAL AURA 上安装 Godot 编辑器。
  2. 安装配套的 Gradle 构建环境应用。它自带了 Android 导出工具链,因此你无需在设备上从零搭建 JDK 和 Android SDK。
  3. 如果你打算在设备上签名并发布构建,请登录 Android 开发者身份。
  4. 启动编辑器——它会打开一个多窗口空间工作区,而不是单个平面窗口。

第四点非常有意思。编辑器提供了桌面级功能集,但以空间中的浮动窗口排列。你可以将场景树放在一个空间区域里,将 2D 视口放在另一个空间区域。

设备端工作流到底能带来什么

老实说:在手感上的迭代更快,在资源上的迭代更慢。

在你目标设备上编辑着色器、调整节点变换、测试输入映射,可以省去整个导出-测试循环。算上构建时间、传输和重新启动,这个循环通常需要 3-8 分钟。十个迭代就是一个小时。

它不擅长的是:导入大型纹理图集、运行长时间光照贴图烘焙,或任何 CPU 密集型工作。这些请在工作站上完成。现实的分工是:桌面端做资源创建,眼镜上做场景组装和调优。

使用 OpenXR Spatial Containers 构建空间内容

Godot 的 Android XR 支持包括 OpenXR Spatial Containers——由系统管理的空间体,你的应用在其中放置空间内容。平台自带的类比基本准确:传统应用在桌面窗口内绘制 2D 内容;空间应用在房间内的容器中绘制 3D 内容。

在写代码之前,有两种容器类型值得了解:

容器类型 适用场景 权衡
Volume 具有真实深度的 3D 物体、世界锁定放置 系统可能限制体积边界
Panel UI 界面、文本、2D 游戏 在固定平面上渲染;深度线索是平的

大多数开发者会犯的错误是为 UI 选择 volume 容器。文本在 volume 容器中会被系统以你无法完全预测的方式缩放和移动,你精心定位的雷达小组件会漂移。把 UI 放在 panel 容器中,让系统来处理放置。

使用 Spatial Container API

Godot 通过 OpenXR 扩展类暴露空间容器。具体类名取决于你的 Godot 版本,因此在编写生产代码之前,请检查引擎当前空间容器绑定的功能列表。下面的模式反映了基于容器的 XR 内容通常是如何组织的。

extends Node3D

signal container_created(handle: int)

# Spatial container setup for Android XR.
# Class names are version-dependent; verify against the
# OpenXR extension bindings shipped with your Godot build.

var volume_container: SpatialContainer
var panel_container: SpatialContainer

func _ready() -> void:
    var xr_interface: XRInterface = XRServer.find_interface("OpenXR")
    if xr_interface == null or not xr_interface.is_initialized():
        push_error("OpenXR interface unavailable on this device.")
        return

    # Volume container for the 3D board.
    volume_container = SpatialContainer.new()
    volume_container.container_type = SpatialContainer.TYPE_VOLUME
    volume_container.extent = Vector3(0.6, 0.4, 0.6)  # 60cm x 40cm x 60cm
    if volume_container.request_container() != OK:
        push_error("Volume container request failed.")
        return

    # Panel container for the HUD.
    panel_container = SpatialContainer.new()
    panel_container.container_type = SpatialContainer.TYPE_PANEL
    panel_container.extent = Vector3(0.4, 0.4, 0.02)
    panel_container.request_container()

    add_child(volume_container)
    add_child(panel_container)

    container_created.emit(volume_container.get_container_handle())

func _process(_delta: float) -> void:
    # Containers can be rejected or resized by the system.
    if volume_container and not volume_container.is_active():
        _handle_container_loss()

func _handle_container_loss() -> void:
    push_warning("Volume container lost - repositioning.")
    # Never assume the container survives; re-request on loss.
    volume_container.request_container()

关键一行是 _handle_container_loss()。系统管理的容器,顾名思义,由系统管理。用户走开、系统助手重新定位内容、或设备回收空间,都可能发生容器丢失。如果你的输入处理在容器丢失后无法恢复,你的应用就会只剩一块隐形棋盘,玩家在那里疯狂按键。

几乎每个人都会踩的坑:锚点漂移

这就是那个具体问题,也是会耗费你最多调试时间的问题。

你在 volume 容器中放置了一块 3D 棋盘。你点击 Play,一切正常。你摘下眼镜,两分钟后再戴上,棋盘现在偏左半米,并且旋转了 20 度。

为什么?因为你把内容锚定到了世界原点,而不是容器上。空间设备上的世界原点并不是一个稳定的位置——它是相对于会话开始姿势定义的。当会话重新锚定时,世界原点会移动。

解决办法是让场景中的每个物件都作为容器变换的子节点,并且永远不要读取或写入空间内容的全局位置:

# WRONG: assumes world origin is stable
func place_piece(piece: Node3D, board_pos: Vector3) -> void:
    piece.global_position = board_pos

# RIGHT: position relative to the container transform
func place_piece(piece: Node3D, board_pos: Vector3) -> void:
    piece.position = board_pos   # local to container
    piece.rotation = Vector3.ZERO

在发布前,对项目全局执行一次 grep,查找 .global_position 和 .global_transform。每次在空间内容中出现,都是一个潜在的漂移 bug。这类 bug 在常规 3D 和多人项目中同样存在——复制变换上的锚定和所有权错误会产生完全相同的“某些客户端看到物体瞬移”的症状,因此把变换视为容器局部而非绝对坐标,是一个值得带入任何网络化 Godot 项目的习惯。

验证修复

要确认锚定是否正确:

  1. 启动你的场景并记下棋盘的位置。
  2. 摘掉设备,等待 60 秒,重新进入应用。
  3. 棋盘应出现在相对于房间的同一物理位置,而不是相对于你站的位置的同一偏移。
  4. 以不同朝向重复测试——先坐着,再站着。

如果漂移,说明你的内容树仍然有绝对引用。如果跳变但跳变后保持一致,说明容器正在被重新定位,而你的内容在正确跟随——这是预期行为,不是 bug。

处理玩家身份与持久化状态

Android XR 上的空间应用就是 Android 应用。它们会挂起、会恢复,用户可能在设备上登录了多个身份,而你的存档数据和其他所有东西一样,位于同一个账户生态系统中。

这个失败模式很隐蔽:玩家在眼镜上进行空间游戏,然后在手机上打开同一款游戏。如果你的存档状态是设备本地的,他们现在就有两个分歧的世界,且无法调和。修复这个问题不是渲染问题,而是账户身份问题。

要自己把它做对,意味着要实现登录流程、服务端存档存储,以及——每个人都低估的部分——两台设备写入同一个存档位时的冲突处理。只有采用具备修订感知的读写模型,让客户端比较版本并显式解决冲突,才能经得起真实用户的考验。从零构建这是数周的工作量;horizOn 正好为此提供了绑定账户的云存档,支持修订感知的读写,并附带邮箱、匿名、Google 和 Apple 认证,因此同一个玩家身份可以贯穿眼镜和手机。

无论你使用哪种后端,都要在发布前决定冲突策略。后写覆盖会悄悄摧毁进度。提示玩家总比丢失数据好。

在 XREAL AURA 上发布的最佳实践

  1. 为环境光设计。 在正午的户外和昏暗的房间里测试每个 UI 面板。如果文本在两者中任一环境变得不可读,提高对比度并添加微弱的亮色轮廓,而不是深色阴影——阴影在透明显示器上毫无作用。
  2. 让容器保持小巧且可重新请求。 60cm 的体积比 2m 更容易让系统可靠放置。假设每个容器随时可能丢失,优先编写恢复路径,而不是留到最后。
  3. 大力限制渲染预算。 这些是眼镜,不是桌面 GPU。目标帧时间低于 11ms(90Hz 级别),关闭 TAA,改用 MSAA,后者在空间分辨率下开销更低。
  4. UI 放面板,内容放体积。 不要为文本与系统的放置逻辑对抗。把 volume 保留给真正有深度、玩家会从多个角度观察的东西。
  5. 每个里程碑前在真机上验证。 眼镜上的编辑器让这件事成本很低。模拟器的结果无法揭示锚点漂移、亮度失效或容器丢失行为。

下一步

设备端编辑器才是这里真正的头条。它把空间迭代循环从几分钟压缩到几秒,这就是调优一个设计和猜测一个设计之间的区别。

首先在 AURA 上安装编辑器和配套的 Gradle 应用,然后把一个现有的小项目重建为空间场景,而不是从零开始——你会立即在已经熟悉的内容上暴露锚定和亮度问题。接下来,添加一个 3D volume 容器和一个 UI 面板,并让两者在挂起/恢复周期中保持稳定。一旦稳定下来,你就准备好为这个平台设计,而不是与它对抗了。

如果你的列表中有跨设备持久化玩家状态的需求,horizOn 可以处理绑定账户的云存档和客户端冲突解决,让你的空间会话与平面屏幕会话保持一致。


来源:为 XREAL AURA 做好准备