Attention: Here be dragons

This is the latest (unstable) version of this documentation, which may document features not available in or compatible with released stable versions of Godot.

更好的 XR 启动脚本

设置 XR 中,我们介绍了一个用于初始化配置的启动脚本,并将其作为主节点脚本使用,以执行任何接口部署所需的最小步骤。

使用 OpenXR 时,这个脚本最好进行一些改进。为此,我们重新编写了一个更为详尽的启动脚本。你可以在演示项目中找到它。

除此以外,如果你使用 XR 工具(见 XR 工具简介),它也包含了另一个版本的启动脚本,那个版本在源代码基础上添加了一些与 XR 工具相关联的功能。

下面将详细介绍演示中使用的脚本,并解释添加的部分。

脚本的信号

我们在脚本中引入了 3 个信号以方便在游戏中添加更多逻辑:

  • focus_lost 作为检测玩家摘下头戴设备或进入头戴设备的菜单系统时的触发器。

  • 当玩家重新戴上头显,或者退出系统菜单并返回游戏时,就会触发(emit) focus_gained 信号。

  • 当头戴设备(头显)请求重置玩家的位置时,就会触发(emit) pose_recentered 这个信号。

我们的游戏将根据这些信号作出相应的反应。

extends Node3D

signal focus_lost
signal focus_gained
signal pose_recentered

...

脚本的变量

我们还向脚本引入了几个新变量:

  • maximum_refresh_rate 将控制头显设备的刷新率——如果头显设备支持控制的话。

  • xr_interface 保存了对我们的 XR 接口的引用,这个变量其实已经存在,但现在我们将其类型化,以便更好地访问 XRInterface API。

  • xr_is_focussed 将在我们的游戏获得焦点时设置为 true。

...

@export var maximum_refresh_rate : int = 90

var xr_interface : OpenXRInterface
var xr_is_focussed = false

...

更新后的 _ready 函数

我们在 ready 函数中新加了一些东西。

如果我们使用的是移动端渲染器(Mobile Renderer)或者前向加渲染器(Forward+ Renderer),我们会将视口(viewport)的 vrs_mode 设置为 VRS_XR 。在支持该功能的平台上,这样做就能开启注视点渲染。

如果我们使用的是兼容性渲染器(Compatibility Renderer),我们会检查 OpenXR 的注视点渲染设置是否已经配置好了。如果没有配置,系统就会输出一条警告信息。详情请参见: OpenXR Settings

这些信号将由 XRInterface 触发。随着实现的深入,后续将提供更多关于这些信号的详细信息。

如果我们无法顺利启动 OpenXR ,我们也会选择退出应用。对于混合现实游戏的开发来说,你可以在成功初始化后进入 VR 模式,若失败再切换至非 VR 模式。不过,在一个独立的 VR 设备上运行仅支持 VR 的应用,启动失败时直接退出程序会比让系统挂着更合适。

...

# Called when the node enters the scene tree for the first time.
func _ready():
    xr_interface = XRServer.find_interface("OpenXR")
    if xr_interface and xr_interface.is_initialized():
        print("OpenXR instantiated successfully.")
        var vp : Viewport = get_viewport()

        # Enable XR on our viewport
        vp.use_xr = true

        # Make sure v-sync is off, v-sync is handled by OpenXR
        DisplayServer.window_set_vsync_mode(DisplayServer.VSYNC_DISABLED)

        # Enable VRS
        if RenderingServer.get_rendering_device():
            vp.vrs_mode = Viewport.VRS_XR
        elif int(ProjectSettings.get_setting("xr/openxr/foveation_level")) == 0:
            push_warning("OpenXR: Recommend setting Foveation level to High in Project Settings")

        # Connect the OpenXR events
        xr_interface.session_begun.connect(_on_openxr_session_begun)
        xr_interface.session_visible.connect(_on_openxr_visible_state)
        xr_interface.session_focussed.connect(_on_openxr_focused_state)
        xr_interface.session_stopping.connect(_on_openxr_stopping)
        xr_interface.pose_recentered.connect(_on_openxr_pose_recentered)
    else:
        # We couldn't start OpenXR.
        print("OpenXR not instantiated!")
        get_tree().quit()

...

会话开始

该信号由 OpenXR 在我们设置会话时发出。意味着头戴设备已经完成了所有设置,并准备好开始接收程序内容。只有此时,各种信息才能正确地获取到。

我们在这里主要做的事情,就是检测头显当前的刷新率。同时,我们还会检查 XR 运行环境(XR runtime)所汇报的可用刷新率列表,以此来决定是否需要将头显切换到一个更高的刷新率。

最后,我们将物理更新速率与头戴设备的更新速率相匹配。Godot 默认物理帧刷新率为每秒 60 帧,而头戴设备通常至少以每秒 72 帧运行,较为先进的型号能高达 144 帧 / 秒。如果不将物理帧刷新率相匹配,将导致设备在对象尚未移动前过早开始渲染,导致画面出现卡顿。

...

# Handle OpenXR session ready
func _on_openxr_session_begun() -> void:
    # Get the reported refresh rate
    var current_refresh_rate = xr_interface.get_display_refresh_rate()
    if current_refresh_rate > 0:
        print("OpenXR: Refresh rate reported as ", str(current_refresh_rate))
    else:
        print("OpenXR: No refresh rate given by XR runtime")

    # See if we have a better refresh rate available
    var new_rate = current_refresh_rate
    var available_rates : Array = xr_interface.get_available_display_refresh_rates()
    if available_rates.size() == 0:
        print("OpenXR: Target does not support refresh rate extension")
    elif available_rates.size() == 1:
        # Only one available, so use it
        new_rate = available_rates[0]
    else:
        for rate in available_rates:
            if rate > new_rate and rate <= maximum_refresh_rate:
                new_rate = rate

    # Did we find a better rate?
    if current_refresh_rate != new_rate:
        print("OpenXR: Setting refresh rate to ", str(new_rate))
        xr_interface.set_display_refresh_rate(new_rate)
        current_refresh_rate = new_rate

    # Now match our physics rate
    Engine.physics_ticks_per_second = current_refresh_rate

...

进入可见状态

当我们的游戏变为可见状态,但并没有获得焦点时,OpenXR 就会触发这个信号。OpenXR 对这个状态的描述听起来可能有点绕,但它基本上就代表了以下三种情况之一:我们的游戏刚刚启动,正准备切换到‘聚焦(focused)’状态;用户刚刚打开了系统菜单;或者是用户刚刚把头戴式设备摘了下来。

在接收到这个信号时,我们会更新焦点的状态,将当前节点的进程模式(process mode)改为禁用(disabled),这会让该节点及其所有子节点暂停处理逻辑,并触发(emit)我们的 focus_lost 信号。

如果你将此脚本添加到根节点,这意味着你的游戏将在需要时自动暂停。如果没有,你可以将方法连接到该信号,以执行额外的更改。

备注

如果游戏是因当用户打开系统菜单而处于可见状态,Godot 会继续渲染帧并保持头部跟踪活跃,因此游戏会在后台保持可见。然而,控制器和手部跟踪将被禁用,直到用户退出系统菜单为止。

...

# Handle OpenXR visible state
func _on_openxr_visible_state() -> void:
    # We always pass this state at startup,
    # but the second time we get this it means our player took off their headset
    if xr_is_focussed:
        print("OpenXR lost focus")

        xr_is_focussed = false

        # pause our game
        get_tree().paused = true

        emit_signal("focus_lost")

...

进入聚焦状态

OpenXR 会在游戏获得聚焦时发出这个信号。这会在启动完成时触发,但也可能在用户退出系统菜单或重新戴上头戴设备时触发。

同时注意,当游戏在用户未佩戴头戴设备时启动,游戏会保持在可见状态,直到用户戴上头戴设备。

警告

因此,在你的游戏处于‘可见模式(visible mode)’时,保持游戏暂停是非常关键的。如果你不这么做,游戏就会在用户没有实际交互的情况下继续运行。此外,当游戏重新回到‘聚焦模式(focused mode)’时,所有的控制器和手部追踪功能会突然重新启用。如果你没有对此做出相应的处理,可能会导致游戏出现严重的 Bug 甚至崩溃。请务必在你的游戏中测试一下这种表现!

在处理我们的信号时,我们会更新焦点(focus)的状态,取消当前节点的暂停状态,并触发(emit)我们的 focus_gained 信号。

...

# Handle OpenXR focused state
func _on_openxr_focused_state() -> void:
    print("OpenXR gained focus")
    xr_is_focussed = true

    # unpause our game
    get_tree().paused = false

    emit_signal("focus_gained")

...

进入停止状态

OpenXR 会在进入停止状态时发出这个信号。不同平台在该情况下的表现会有所不同。一部分平台只会在游戏关闭时发出此信号,另一部分在玩家摘下头戴设备时也会发出。

目前为止,该方法只充当一个占位符。

...

# Handle OpenXR stopping state
func _on_openxr_stopping() -> void:
    # Our session is being stopped.
    print("OpenXR is stopping")

...

姿势重新居中

当用户请求重新定位视角时,OpenXR 会发出此信号。该信号主要用于告诉你的游戏:用户现在面朝前方,你应该重新定位玩家,使其在虚拟世界中面朝前方。

由于重新定位视角依赖于游戏设计,因此你的游戏需要被设计能正确地做出反应。

下面这段代码里,我们只是发出 pose_recentered 信号,并未提供用户重新定位的代码实现。你可以连接到这个信号并自行实现它。通常调用 center_on_hmd() 就足够了。

...

# Handle OpenXR pose recentered signal
func _on_openxr_pose_recentered() -> void:
    # User recentered view, we have to react to this by recentering the view.
    # This is game implementation dependent.
    emit_signal("pose_recentered")

这样就完成了我们的脚本。它被设计为能够重复利用。只需将它添加为主节点的脚本(如有需要还可以进行扩展),或者添加到专门用于此脚本的子节点上。