mirror of
https://github.com/godotengine/godot-docs.git
synced 2026-01-05 22:09:56 +03:00
Merge pull request #8759 from BastiaanOlij/a_better_xr_start_script
Add a chapter to the XR manual that improves the startup script.
This commit is contained in:
482
tutorials/xr/a_better_xr_start_script.rst
Normal file
482
tutorials/xr/a_better_xr_start_script.rst
Normal file
@@ -0,0 +1,482 @@
|
||||
.. _doc_a_better_xr_start_script:
|
||||
|
||||
A better XR start script
|
||||
========================
|
||||
|
||||
In :ref:`doc_setting_up_xr` we introduced a startup script that initialises our setup which we used as our script on our main node.
|
||||
This script performs the minimum steps required for any given interface.
|
||||
|
||||
When using OpenXR there are a number of improvements we should do here.
|
||||
For this we've created a more elaborate starting script.
|
||||
You will find these used in our demo projects.
|
||||
|
||||
Alternatively, if you are using XR Tools (see :ref:`doc_introducing_xr_tools`) it contains a version of this script updated with some features related to XR tools.
|
||||
|
||||
Below we will detail out the script used in our demos and explain the parts that are added.
|
||||
|
||||
Signals for our script
|
||||
----------------------
|
||||
|
||||
We are introducing 3 signals to our script so that our game can add further logic:
|
||||
|
||||
- ``focus_lost`` is emitted when the player takes off their headset or when the player enters the menu system of the headset.
|
||||
- ``focus_gained`` is emitted when the player puts their headset back on or exists the menu system and returns to the game.
|
||||
- ``pose_recentered`` is emitted when the headset requests the players position to be reset.
|
||||
|
||||
Our game should react accordingly to these signals.
|
||||
|
||||
.. tabs::
|
||||
.. code-tab:: gdscript GDScript
|
||||
|
||||
extends Node3D
|
||||
|
||||
signal focus_lost
|
||||
signal focus_gained
|
||||
signal pose_recentered
|
||||
|
||||
...
|
||||
|
||||
.. code-tab:: csharp
|
||||
|
||||
using Godot;
|
||||
|
||||
public partial class MyNode3D : Node3D
|
||||
{
|
||||
[Signal]
|
||||
public delegate void FocusLostEventHandler();
|
||||
|
||||
[Signal]
|
||||
public delegate void FocusGainedEventHandler();
|
||||
|
||||
[Signal]
|
||||
public delegate void PoseRecenteredEventHandler();
|
||||
|
||||
...
|
||||
|
||||
|
||||
Variables for our script
|
||||
------------------------
|
||||
|
||||
We introduce a few new variables to our script as well:
|
||||
|
||||
- ``maximum_refresh_rate`` will control the headsets refresh rate if this is supported by the headset.
|
||||
- ``xr_interface`` holds a reference to our XR interface, this already existed but we now type it to get full access to our :ref:`XRInterface <class_xrinterface>` API.
|
||||
- ``xr_is_focussed`` will be set to true whenever our game has focus.
|
||||
|
||||
.. tabs::
|
||||
.. code-tab:: gdscript GDScript
|
||||
|
||||
...
|
||||
|
||||
@export var maximum_refresh_rate : int = 90
|
||||
|
||||
var xr_interface : OpenXRInterface
|
||||
var xr_is_focussed = false
|
||||
|
||||
...
|
||||
|
||||
.. code-tab:: csharp
|
||||
|
||||
...
|
||||
|
||||
[Export]
|
||||
public int MaximumRefreshRate { get; set; } = 90;
|
||||
|
||||
private OpenXRInterface _xrInterface;
|
||||
|
||||
private bool _xrIsFocused;
|
||||
|
||||
...
|
||||
|
||||
Our updated ready function
|
||||
--------------------------
|
||||
|
||||
The ready function mostly remains the same but we hook up a number of signals that will be emitted by the :ref:`XRInterface <class_xrinterface>`.
|
||||
We'll provide more detail about these signals as we implement them.
|
||||
|
||||
We also quit our application if we couldn't successfully initialise OpenXR.
|
||||
Now this can be a choice.
|
||||
If you are making a mixed mode game you setup the VR mode of your game on success,
|
||||
and setup the non-VR mode of your game on failure.
|
||||
However, when running a VR only application on a standalone headset,
|
||||
it is nicer to exit on failure than to hang the system.
|
||||
|
||||
.. tabs::
|
||||
.. code-tab:: gdscript GDScript
|
||||
|
||||
...
|
||||
|
||||
# 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)
|
||||
|
||||
# Connect the OpenXR events
|
||||
xr_interface.connect("session_begun", _on_openxr_session_begun)
|
||||
xr_interface.connect("session_visible", _on_openxr_visible_state)
|
||||
xr_interface.connect("session_focussed", _on_openxr_focused_state)
|
||||
xr_interface.connect("session_stopping", _on_openxr_stopping)
|
||||
xr_interface.connect("pose_recentered", _on_openxr_pose_recentered)
|
||||
else:
|
||||
# We couldn't start OpenXR.
|
||||
print("OpenXR not instantiated!")
|
||||
get_tree().quit()
|
||||
|
||||
...
|
||||
|
||||
.. code-tab:: csharp
|
||||
|
||||
...
|
||||
|
||||
/// <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);
|
||||
|
||||
// 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();
|
||||
}
|
||||
}
|
||||
|
||||
...
|
||||
|
||||
|
||||
On session begun
|
||||
----------------
|
||||
|
||||
This signal is emitted by OpenXR when our session is setup.
|
||||
This means the headset has run through setting everything up and is ready to begin receiving content from us.
|
||||
Only at this time various information is properly available.
|
||||
|
||||
The main thing we do here is to check our headsets refresh rate.
|
||||
We also check the available refresh rates reported by the XR runtime to determine if we want to set our headset to a higher refresh rate.
|
||||
|
||||
Finally we match our physics update rate to our headset update rate.
|
||||
Godot runs at a physics update rate of 60 updates per second by default while headsets run at a minimum of 72,
|
||||
and for modern headsets often up to 144 frames per second.
|
||||
Not matching the physics update rate will cause stuttering as frames are rendered without objects moving.
|
||||
|
||||
.. tabs::
|
||||
.. code-tab:: gdscript GDScript
|
||||
|
||||
...
|
||||
|
||||
# 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
|
||||
|
||||
...
|
||||
|
||||
.. code-tab:: csharp
|
||||
|
||||
...
|
||||
|
||||
/// <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;
|
||||
}
|
||||
|
||||
...
|
||||
|
||||
On visible state
|
||||
----------------
|
||||
|
||||
This signal is emitted by OpenXR when our game becomes visible but is not focussed.
|
||||
This is a bit of a weird description in OpenXR but it basically means that our game has just started
|
||||
and we're about to switch to the focussed state next,
|
||||
that the user has opened a system menu or the users has just took their headset off.
|
||||
|
||||
On receiving this signal we'll update our focussed state,
|
||||
we'll change the process mode of our node to disabled which will pause processing on this node and it's children,
|
||||
and emit our ``focus_lost`` signal.
|
||||
|
||||
If you've added this script to your root node,
|
||||
this means your game will automatically pause when required.
|
||||
If you haven't, you can connect a method to the signal that performs additional changes.
|
||||
|
||||
.. note::
|
||||
|
||||
While your game is in visible state because the user has opened a system menu,
|
||||
Godot will keep rendering frames and head tracking will remain active so your game will remain visible in the background.
|
||||
However controller and hand tracking will be disabled until the user exits the system menu.
|
||||
|
||||
.. tabs::
|
||||
.. code-tab:: gdscript GDScript
|
||||
|
||||
...
|
||||
|
||||
# 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
|
||||
process_mode = Node.PROCESS_MODE_DISABLED
|
||||
|
||||
emit_signal("focus_lost")
|
||||
|
||||
...
|
||||
|
||||
.. code-tab:: csharp
|
||||
|
||||
...
|
||||
|
||||
/// <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
|
||||
ProcessMode = ProcessModeEnum.Disabled;
|
||||
|
||||
EmitSignal(SignalName.FocusLost);
|
||||
}
|
||||
}
|
||||
|
||||
...
|
||||
|
||||
On focussed state
|
||||
-----------------
|
||||
|
||||
This signal is emitted by OpenXR when our game gets focus.
|
||||
This is done at the completion of our startup,
|
||||
but it can also be emitted when the user exits a system menu, or put their headset back on.
|
||||
|
||||
Note also that when your game starts while the user is not wearing their headset,
|
||||
the game stays in 'visible' state until the user puts their headset on.
|
||||
|
||||
.. warning::
|
||||
|
||||
It is thus important to keep your game paused while in visible mode.
|
||||
If you don't the game will keep on running while your user isn't interacting with your game.
|
||||
Also when the game returns to focussed mode,
|
||||
suddenly all controller and hand tracking is re-enabled and could have game breaking consequences
|
||||
if you do not react to this accordingly.
|
||||
Be sure to test this behaviour in your game!
|
||||
|
||||
While handling our signal we will update the focusses state, unpause our node and emit our ``focus_gained`` signal.
|
||||
|
||||
.. tabs::
|
||||
.. code-tab:: gdscript GDScript
|
||||
|
||||
...
|
||||
|
||||
# Handle OpenXR focused state
|
||||
func _on_openxr_focused_state() -> void:
|
||||
print("OpenXR gained focus")
|
||||
xr_is_focussed = true
|
||||
|
||||
# unpause our game
|
||||
process_mode = Node.PROCESS_MODE_INHERIT
|
||||
|
||||
emit_signal("focus_gained")
|
||||
|
||||
...
|
||||
|
||||
.. code-tab:: csharp
|
||||
|
||||
...
|
||||
|
||||
/// <summary>
|
||||
/// Handle OpenXR focused state
|
||||
/// </summary>
|
||||
private void OnOpenXRFocusedState()
|
||||
{
|
||||
GD.Print("OpenXR gained focus");
|
||||
_xrIsFocused = true;
|
||||
|
||||
// Un-pause our game
|
||||
ProcessMode = ProcessModeEnum.Inherit;
|
||||
|
||||
EmitSignal(SignalName.FocusGained);
|
||||
}
|
||||
|
||||
...
|
||||
|
||||
On stopping state
|
||||
-----------------
|
||||
|
||||
This signal is emitted by OpenXR when we enter our stop state.
|
||||
There are some differences between platforms when this happens.
|
||||
On some platforms this is only emitted when the game is being closed.
|
||||
But on other platforms this will also be emitted every time the player takes off their headset.
|
||||
|
||||
For now this method is only a place holder.
|
||||
|
||||
.. tabs::
|
||||
.. code-tab:: gdscript GDScript
|
||||
|
||||
...
|
||||
|
||||
# Handle OpenXR stopping state
|
||||
func _on_openxr_stopping() -> void:
|
||||
# Our session is being stopped.
|
||||
print("OpenXR is stopping")
|
||||
|
||||
...
|
||||
|
||||
.. code-tab:: csharp
|
||||
|
||||
...
|
||||
|
||||
/// <summary>
|
||||
/// Handle OpenXR stopping state
|
||||
/// </summary>
|
||||
private void OnOpenXRStopping()
|
||||
{
|
||||
// Our session is being stopped.
|
||||
GD.Print("OpenXR is stopping");
|
||||
}
|
||||
|
||||
...
|
||||
|
||||
|
||||
On pose recentered
|
||||
------------------
|
||||
|
||||
This signal is emitted by OpenXR when the user requests their view to be recentered.
|
||||
Basically this communicates to your game that the user is now facing forward
|
||||
and you should re-orient the player so they are facing forward in the virtual world.
|
||||
|
||||
As doing so is dependent on your game, your game needs to react accordingly.
|
||||
|
||||
All we do here is emit the ``pose_recentered`` signal.
|
||||
You can connect to this signal and implement the actual recenter code.
|
||||
Often it is enough to call :ref:`center_on_hmd() <class_XRServer_method_center_on_hmd>`.
|
||||
|
||||
.. tabs::
|
||||
.. code-tab:: gdscript GDScript
|
||||
|
||||
...
|
||||
|
||||
# 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")
|
||||
|
||||
.. code-tab:: csharp
|
||||
|
||||
...
|
||||
|
||||
/// <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);
|
||||
}
|
||||
}
|
||||
|
||||
And that finished our script. It was written so that it can be re-used over multiple projects.
|
||||
Just add it as the script on your main node (and extend it if needed)
|
||||
or add it on a child node specific for this script.
|
||||
@@ -12,6 +12,7 @@ Basic Tutorial
|
||||
|
||||
setting_up_xr
|
||||
deploying_to_android
|
||||
a_better_xr_start_script
|
||||
introducing_xr_tools
|
||||
basic_xr_locomotion
|
||||
|
||||
|
||||
Reference in New Issue
Block a user