更完善的 XR 啟動腳本

設定 XR 中,我們介紹了一個初始化 XR 設定的啟動腳本,並將其作為主節點的腳本。該腳本執行了每個介面所需的最基本步驟。

當使用 OpenXR 時,這裡有許多可以改進的地方。為此,我們設計了一個更完整的啟動腳本,你可以在我們的範例專案中看到這些腳本的應用。

另外,如果你使用 XR Tools(請參閱 XR 工具簡介),其中包含了針對 XR 工具功能升級過的這個腳本版本。

以下我們會詳細說明範例專案中所使用的腳本,並解釋新增的部分。

腳本訊號

我們為腳本新增了三個訊號,讓遊戲可以根據這些事件添加額外邏輯:

  • 當玩家取下頭戴式裝置或進入頭戴式裝置的選單系統時,會發出 focus_lost 訊號。

  • 當玩家戴回頭戴式裝置或從選單系統返回遊戲時,會發出 focus_gained 訊號。

  • 當頭戴式裝置要求重設玩家的定位時,會發出 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 或 forward+ 繪圖器,會將視窗的 vrs_mode 設為 VRS_XR。在支援的平台上,這會啟用視網膜渲染(foveated rendering)。

如果使用 compatibility 繪圖器,會檢查 OpenXR 的視網膜渲染設定是否已正確設置,若否則會顯示警告。詳情請參閱 OpenXR 設定

我們連接數個來自 XRInterface 的訊號。接下來會詳細介紹這些訊號的處理方式。

如果無法成功初始化 OpenXR,則會直接結束應用程式。這可以視需求調整:若你的遊戲支援混合模式,可以在初始化成功時啟用 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 執行階段回報的可用更新率,以決定是否要調整成更高的更新率。

最後,我們會將物理運算更新率調整為與頭戴裝置的更新率一致。Godot 預設每秒進行 60 次物理更新,而頭戴裝置最低通常為 72Hz,現代裝置甚至可達 144Hz。如果兩者不同步,會導致畫面更新但物件未移動,造成卡頓現象。

...

# 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 中的定義較為特殊,通常代表遊戲剛啟動即將切換為焦點狀態、或使用者開啟了系統選單,或是剛取下頭戴裝置。

收到此訊號時,我們會更新焦點狀態,將本節點的運算模式設為停用,暫停此節點與其子節點的處理,並發出 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 會發出此訊號。這通常是在啟動過程結束時發出,但也可能在玩家離開系統選單或戴回頭戴裝置時發出。

請注意,如果遊戲啟動時使用者尚未戴上頭戴裝置,遊戲會維持在「可見」狀態,直到玩家戴上裝置才會取得焦點。

警告

因此,當遊戲處於可見模式時,務必保持遊戲暫停。否則,遊戲會在玩家未互動時持續運作。而當重新取得焦點時,所有控制器與手部追蹤會瞬間恢復,若未妥善處理可能造成遊戲異常。請務必在遊戲內測試這些行為!

處理該訊號時,我們會更新焦點狀態,解除節點暫停,並發出 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")

這樣我們的腳本就完成了。此腳本設計可在多個專案中重複使用。你只需將其作為主節點的腳本(有需要可擴充),或加在專用的子節點上即可。