更完善的 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
...
using Godot;
public partial class MyNode3D : Node3D
{
[Signal]
public delegate void FocusLostEventHandler();
[Signal]
public delegate void FocusGainedEventHandler();
[Signal]
public delegate void PoseRecenteredEventHandler();
...
腳本變數
我們也為腳本新增了一些變數:
如果裝置支援,
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
...
...
[Export]
public int MaximumRefreshRate { get; set; } = 90;
private OpenXRInterface _xrInterface;
private bool _xrIsFocused;
...
已更新的 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()
...
...
/// <summary>
/// Called when the node enters the scene tree for the first time.
/// </summary>
public override void _Ready()
{
_xrInterface = (OpenXRInterface)XRServer.FindInterface("OpenXR");
if (_xrInterface != null && _xrInterface.IsInitialized())
{
GD.Print("OpenXR instantiated successfully.");
var vp = GetViewport();
// Enable XR on our viewport
vp.UseXR = true;
// Make sure v-sync is off, v-sync is handled by OpenXR
DisplayServer.WindowSetVsyncMode(DisplayServer.VSyncMode.Disabled);
// Enable VRS
if (RenderingServer.GetRenderingDevice() != null)
vp.VrsMode = Viewport.VrsModeEnum.XR;
else if ((int)ProjectSettings.GetSetting("xr/openxr/foveation_level") == 0)
GD.PushWarning("OpenXR: Recommend setting Foveation level to High in Project Settings");
// Connect the OpenXR events
_xrInterface.SessionBegun += OnOpenXRSessionBegun;
_xrInterface.SessionVisible += OnOpenXRVisibleState;
_xrInterface.SessionFocussed += OnOpenXRFocusedState;
_xrInterface.SessionStopping += OnOpenXRStopping;
_xrInterface.PoseRecentered += OnOpenXRPoseRecentered;
}
else
{
// We couldn't start OpenXR.
GD.Print("OpenXR not instantiated!");
GetTree().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
...
...
/// <summary>
/// Handle OpenXR session ready
/// </summary>
private void OnOpenXRSessionBegun()
{
// Get the reported refresh rate
var currentRefreshRate = _xrInterface.DisplayRefreshRate;
GD.Print(currentRefreshRate > 0.0F
? $"OpenXR: Refresh rate reported as {currentRefreshRate}"
: "OpenXR: No refresh rate given by XR runtime");
// See if we have a better refresh rate available
var newRate = currentRefreshRate;
var availableRates = _xrInterface.GetAvailableDisplayRefreshRates();
if (availableRates.Count == 0)
{
GD.Print("OpenXR: Target does not support refresh rate extension");
}
else if (availableRates.Count == 1)
{
// Only one available, so use it
newRate = (float)availableRates[0];
}
else
{
GD.Print("OpenXR: Available refresh rates: ", availableRates);
foreach (float rate in availableRates)
if (rate > newRate && rate <= MaximumRefreshRate)
newRate = rate;
}
// Did we find a better rate?
if (currentRefreshRate != newRate)
{
GD.Print($"OpenXR: Setting refresh rate to {newRate}");
_xrInterface.DisplayRefreshRate = newRate;
currentRefreshRate = newRate;
}
// Now match our physics rate
Engine.PhysicsTicksPerSecond = (int)currentRefreshRate;
}
...
進入可見狀態
當遊戲變為可見但未取得焦點時,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")
...
...
/// <summary>
/// Handle OpenXR visible state
/// </summary>
private void OnOpenXRVisibleState()
{
// We always pass this state at startup,
// but the second time we get this it means our player took off their headset
if (_xrIsFocused)
{
GD.Print("OpenXR lost focus");
_xrIsFocused = false;
// Pause our game
GetTree().Paused = true;
EmitSignal(SignalName.FocusLost);
}
}
...
取得焦點狀態
當遊戲取得焦點時,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")
...
...
/// <summary>
/// Handle OpenXR focused state
/// </summary>
private void OnOpenXRFocusedState()
{
GD.Print("OpenXR gained focus");
_xrIsFocused = true;
// Un-pause our game
GetTree().Paused = false;
EmitSignal(SignalName.FocusGained);
}
...
停止狀態
當進入停止狀態時,OpenXR 會發出此訊號。各平台觸發時機略有不同,有些平台僅在遊戲關閉時發出,有些則在玩家每次取下頭戴裝置時也會發出。
目前這個方法僅作為預留位置。
...
# Handle OpenXR stopping state
func _on_openxr_stopping() -> void:
# Our session is being stopped.
print("OpenXR is stopping")
...
...
/// <summary>
/// Handle OpenXR stopping state
/// </summary>
private void OnOpenXRStopping()
{
// Our session is being stopped.
GD.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")
...
/// <summary>
/// Handle OpenXR pose recentered signal
/// </summary>
private void OnOpenXRPoseRecentered()
{
// User recentered view, we have to react to this by recentering the view.
// This is game implementation dependent.
EmitSignal(SignalName.PoseRecentered);
}
}
這樣我們的腳本就完成了。此腳本設計可在多個專案中重複使用。你只需將其作為主節點的腳本(有需要可擴充),或加在專用的子節點上即可。