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 中,我們介紹了一個初始化 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")

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