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")

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