// --------------------------------------------------------------------------------------------------------------------
//
// Copyright (c) VRMADA, All rights reserved.
//
// --------------------------------------------------------------------------------------------------------------------
using System;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.IO.Compression;
using System.Linq;
using System.Runtime.Serialization;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using UltimateXR.Animation.Interpolation;
using UltimateXR.Avatar;
using UltimateXR.Avatar.Controllers;
using UltimateXR.Core.Caching;
using UltimateXR.Core.Components;
using UltimateXR.Core.Components.Singleton;
using UltimateXR.Core.Serialization;
using UltimateXR.Core.Settings;
using UltimateXR.Core.StateSave;
using UltimateXR.Core.StateSync;
using UltimateXR.Core.Unique;
using UltimateXR.Extensions.System.IO;
using UltimateXR.Extensions.System.Threading;
using UltimateXR.Extensions.Unity;
using UltimateXR.Extensions.Unity.Math;
using UltimateXR.Extensions.Unity.Render;
using UltimateXR.Locomotion;
using UltimateXR.Manipulation;
using UltimateXR.Mechanics.Weapons;
using UltimateXR.UI.UnityInputModule;
using UnityEngine;
using UnityEngine.EventSystems;
using UnityEngine.SceneManagement;
using Debug = UnityEngine.Debug;
namespace UltimateXR.Core
{
///
///
/// Main manager in the UltimateXR framework. As a UxrSingleton it can be
/// accessed from any point in the application using UxrManager.Instance
/// .
/// It can be pre-instantiated in the scene in order to change default parameters through the inspector but it is
/// not required. When accessing the global UxrManager.Instance, if no
/// is currently available, one will be instantiated in the scene as the global
/// Singleton.
///
///
/// is responsible for updating all key framework entities such as avatars each frame in
/// the correct order. Events and callbacks are provided so that custom updates can be executed at appropriate
/// stages of the updating process.
///
///
/// also provides the following functionality:
///
/// -
/// Pre-caching prefabs when scenes are loaded to eliminate hiccups using the
/// interface.
///
/// - Moving/rotating/teleporting avatars.
/// - Events to get notified when avatars have been moved/rotated/teleported.
/// -
/// Events to get notified before and after updating a frame and at different stages of the updating
/// process for finer control: / and
/// /.
///
/// -
/// A single event to get notified of all state changes in any component in the framework or
/// any custom user class: . Also a way to execute back state change
/// events, helping implement network synchronization and save-to-file/replay functionality:
/// .
///
/// -
/// Provide ways to save the current state of the scene and load it back, helping implement
/// sync-on-join networking functionality and save-to-file/replays: and
/// .
///
///
///
///
public sealed class UxrManager : UxrSingleton
{
#region Inspector Properties/Serialized Fields
[SerializeField] private UxrPostUpdateMode _postUpdateMode = UxrPostUpdateMode.LateUpdate;
[SerializeField] private bool _usePrecaching = true;
[SerializeField] private int _precacheFrameCount = 50;
#endregion
#region Public Types & Data
// Events
///
/// Invoked to notify a state change in a component. This can be used to synchronize the
/// event over the network or capture it in a replay/save state system.
/// The interface is implemented in the base class,
/// serving as the foundation for all components within UltimateXR. While this interface is readily available in
/// , users may also implement it in custom classes where inheritance from
/// is not feasible due to limitations in multiple inheritance.
/// helps leverage this interface implementation.
/// is used to serialize the event.
/// Serialized events can subsequently be executed via .
/// This streamlined functionality simplifies networking implementation with UltimateXR:
///
/// -
/// A central entry point to capture all events in UltimateXR:
///
/// -
/// A method to serialize the event into a byte array:
///
/// -
/// A means to execute an event for replicating the state change in another device or session:
///
///
///
/// By default, nested state changes are ignored to optimize bandwidth usage and prevent redundant calls that
/// might result in inconsistencies.
/// Since the root state change already triggers the nested ones, there's no need to synchronize them again.
/// For additional details, refer to .
///
public static event Action ComponentStateChanged;
///
/// Called right before precaching is about to start. It's called on the first frame that is displayed black.
/// See .
///
public static event Action PrecachingStarting;
///
/// Called right after precaching finished. It's called on the first frame that starts to fade-in from black.
/// See .
///
public static event Action PrecachingFinished;
///
/// Called right before processing all update stages in the current frame. Equivalent to
/// for
///
public static event Action AvatarsUpdating;
///
/// Called right after processing all update stages in the current frame. Equivalent to for
///
///
public static event Action AvatarsUpdated;
///
/// Called right before an update stage in the current frame. See .
///
public static event Action StageUpdating;
///
/// Called right after an update stage in the current frame. See .
///
public static event Action StageUpdated;
///
/// Gets or sets whether the event will be triggered by top level
/// synchronization calls only. It is true by default to avoid redundant calls and inconsistencies.
/// When false, they will also be triggered by nested changes.
///
public static bool UseTopLevelStateChangesOnly { get; set; } = true;
///
/// Gets whether the manager is currently pre-caching. This happens right after the local avatar is enabled and
/// is set.
///
public bool IsPrecaching => _precacheCoroutine != null;
///
/// Gets whether the local avatar is being teleported, including in/out smooth transitions.
///
public bool IsTeleportingLocalAvatar => _teleportCoroutine != null;
///
/// Gets whether the manager is currently inside a StateSync call executed using .
///
public bool IsInsideStateSync { get; private set; }
// Properties
///
/// Gets or sets when to perform the post-update. The post-update updates among others the avatar animation (hand
/// poses, manipulation mechanics and Inverse Kinematics).
/// It is by default to make sure they are played on top of any animation
/// generated by Unity built-in animation components like .
///
public UxrPostUpdateMode PostUpdateMode
{
get => _postUpdateMode;
set => _postUpdateMode = value;
}
///
/// Gets or sets whether the manager uses pre-caching. Pre-caching happens right after the local avatar is enabled and
/// consists of instantiating objects described in all components in the scene. These
/// objects are placed right in front of the camera while it is faded black, so that they can't be seen, which forces
/// their resources to be loaded in order to reduce hiccups when they need to be instantiated during the session. After
/// that they are deleted and the scene is faded in.
///
public bool UsePrecaching
{
get => _usePrecaching;
set => _usePrecaching = value;
}
///
/// Gets or sets the number of frames pre-cached objects are shown. These frames are drawn in black and right after the
/// scene will fade in, so that pre-caching is hidden to the user.
///
public int PrecacheFrameCount
{
get => _precacheFrameCount;
set => _precacheFrameCount = value;
}
///
/// Gets or sets the color used when teleporting using screen fading transitions.
///
public Color TeleportFadeColor { get; set; }
#endregion
#region Public Methods
///
/// Given a component that requires an component in the hierarchy in order to work, logs an
/// error indicating that it's missing.
///
/// Component that requires an on its GameObject or any of its parents.
public static void LogMissingAvatarInHierarchyError(Component component)
{
if (UxrGlobalSettings.Instance.LogLevelCore >= UxrLogLevel.Errors)
{
Debug.LogError($"{UxrConstants.CoreModule}: {component.GetType().Name} requires to be part of an {nameof(UxrAvatar)} in order to work correctly. GameObject is {component.GetPathUnderScene()}.");
}
}
///
/// Given a component that requires an component in the scene in order to work, logs an error
/// indicating that it's missing.
///
/// Component that requires an in the scene.
public static void LogMissingAvatarInScene(Component component)
{
if (UxrGlobalSettings.Instance.LogLevelCore >= UxrLogLevel.Errors)
{
Debug.LogError($"{UxrConstants.CoreModule}: {component.GetType().Name} requires an avatar in the scene to work correctly. GameObject is {component.GetPathUnderScene()}.");
}
}
///
/// Saves the state of all components. This can be used in multiplayer, save-to-file or replay functionality.
/// Components are saved using to control the order in which the
/// components will be serialized. The serialization order will determine the deserialization order used by
/// .
///
/// A list of GameObjects whose hierarchy to serialize or null to serialize the whole scene
/// A list of GameObjects whose hierarchy to ignore, or null to not ignore anything
/// The level of changes to serialize
/// The serialization output format
/// A data stream that can be saved and loaded back using
public byte[] SaveStateChanges(List roots, List ignoreRoots, UxrStateSaveLevel level, UxrSerializationFormat format)
{
int count = 0;
int totalComponents = 0;
byte[] bytes = null;
int originalSize = 0;
Stopwatch sw = Stopwatch.StartNew();
using (MemoryStream stream = new MemoryStream())
{
// Write the first 4 bytes: format, level and serialization verison.
stream.WriteByte((byte)format);
stream.WriteByte((byte)level);
new BinaryWriter(stream).Write((ushort)UxrConstants.Serialization.CurrentBinaryVersion);
stream.Flush();
// Write the rest
using (BinaryWriter writer = new BinaryWriter(stream))
{
using UxrBinarySerializer serializer = new UxrBinarySerializer(writer);
using MemoryStream componentStream = new MemoryStream();
using BinaryWriter componentWriter = new BinaryWriter(componentStream);
using UxrBinarySerializer componentSerializer = new UxrBinarySerializer(componentWriter);
IEnumerable components;
if (roots == null)
{
components = level == UxrStateSaveLevel.Complete ? UxrStateSaveImplementer.AllSerializableComponents : UxrStateSaveImplementer.SaveRequiredComponents;
}
else
{
if (level == UxrStateSaveLevel.Complete)
{
components = roots.SelectMany(go => go.GetComponentsInChildren(true));
}
else
{
components = roots.SelectMany(go => go.GetComponentsInChildren(true).Where(c => c.Component.isActiveAndEnabled));
}
}
foreach (IUxrStateSave stateSave in components.OrderBy(c => c.SerializationOrder))
{
bool serialize = ignoreRoots == null || !ignoreRoots.Any(go => stateSave.Transform.HasParent(go.transform));
if (!serialize)
{
continue;
}
try
{
// First serialize using DontSerialize option, so that we can check whether any data needs to be saved or we can skip it entirely
if (stateSave.SerializeState(serializer, stateSave.StateSerializationVersion, level, UxrStateSaveOptions.DontCacheChanges | UxrStateSaveOptions.DontSerialize))
{
// Now serialize it to the secondary componentStream so that we can know the size beforehand.
// Knowing the size beforehand allows to write the size before the component so that, when deserializing, if the component
// fails, it can still skip the component and continue with the rest of the data.
componentStream.Position = 0;
componentStream.SetLength(0);
componentWriter.WriteUniqueComponent(stateSave);
componentWriter.WriteCompressedInt32(stateSave.StateSerializationVersion);
stateSave.SerializeState(componentSerializer, stateSave.StateSerializationVersion, level);
componentWriter.Flush();
byte[] componentBytes = componentStream.ToArray();
// Now write it in the main stream, with the size at the beginning
long before = writer.BaseStream.Position;
int length = componentBytes.Length;
serializer.Serialize(ref length);
writer.Write(componentBytes, 0, length);
long after = writer.BaseStream.Position;
if (UxrGlobalSettings.Instance.LogLevelCore >= UxrLogLevel.Debug)
{
Debug.Log($"{UxrConstants.CoreModule}: {nameof(UxrManager)}.{nameof(SaveStateChanges)}(): {count + 1}: Serialized {stateSave.Component.name} (type {stateSave.GetType().Name}) to {after - before} bytes. Id is {stateSave.UniqueId}.");
}
count++;
}
totalComponents++;
}
catch (SerializationException e)
{
if (UxrGlobalSettings.Instance.LogLevelCore >= UxrLogLevel.Errors)
{
Debug.LogError($"{UxrConstants.CoreModule}: {nameof(UxrManager)}.{nameof(SaveStateChanges)}(): Error serializing component {stateSave.Component.name} (type {stateSave.GetType().Name}). Most probably this type requires to implement the {nameof(ICloneable)} interface for state saving to work: {e}");
}
}
catch (Exception e)
{
if (UxrGlobalSettings.Instance.LogLevelCore >= UxrLogLevel.Errors)
{
Debug.LogError($"{UxrConstants.CoreModule}: {nameof(UxrManager)}.{nameof(SaveStateChanges)}(): Error serializing component {stateSave.Component.name} (type {stateSave.GetType().Name}): {e}");
}
}
}
}
stream.Flush();
bytes = stream.ToArray();
originalSize = bytes.Length;
}
// Compress if requested
if (format == UxrSerializationFormat.BinaryGzip)
{
using MemoryStream compressedStream = new MemoryStream();
// Write the first four bytes uncompressed
compressedStream.Write(bytes, 0, 4);
// Compress the remaining bytes and write them to the compressed stream, starting from index 4
using (GZipStream zipStream = new GZipStream(compressedStream, CompressionMode.Compress, true))
{
zipStream.Write(bytes, 4, bytes.Length - 4);
zipStream.Flush();
}
// Get the compressed bytes from the compressed stream
bytes = compressedStream.ToArray();
}
// Log if required
if (UxrGlobalSettings.Instance.LogLevelCore >= UxrLogLevel.Verbose)
{
string rootName = roots != null ? $"{roots.Count} root object(s)" : "scene";
string compressionInfo = string.Empty;
if (originalSize != bytes.Length)
{
compressionInfo = $" Compressed from {originalSize} bytes ({(float)originalSize / bytes.Length:0.00} compression ratio).";
}
sw.Stop();
TimeSpan elapsed = sw.Elapsed;
double milliseconds = (double)elapsed.Ticks / TimeSpan.TicksPerMillisecond;
Debug.Log($"{UxrConstants.CoreModule}: {nameof(UxrManager)}.{nameof(SaveStateChanges)}(): Serialized {count}/{totalComponents} component(s) from {rootName} to {bytes.Length} bytes in {milliseconds:F3}ms. Format: {format}, level: {level}.{compressionInfo}");
}
return bytes;
}
///
/// Loads the state of component changes from serialized data using .
///
/// Serialized state
public void LoadStateChanges(byte[] serializedState)
{
if (serializedState == null || serializedState.Length == 0)
{
if (UxrGlobalSettings.Instance.LogLevelCore >= UxrLogLevel.Warnings)
{
Debug.LogWarning($"{UxrConstants.CoreModule}: {nameof(UxrManager)}.{nameof(LoadStateChanges)}(): Input bytes is null or empty.");
}
return;
}
Stopwatch sw = Stopwatch.StartNew();
// We will use a CancelSync() at the end. This avoids propagation of any synchronization message while loading.
BeginSync();
int count = 0;
long uncompressedLength = -1;
List loadedAvatars = new List();
// Read the first 4 bytes: format, level and serialization version
using MemoryStream stream = new MemoryStream(serializedState);
UxrSerializationFormat format = (UxrSerializationFormat)stream.ReadByte();
UxrStateSaveLevel level = (UxrStateSaveLevel)stream.ReadByte();
int serializationVersion = new BinaryReader(stream).ReadUInt16();
// Read the rest
switch (format)
{
case UxrSerializationFormat.BinaryUncompressed:
DeserializeUncompressed(stream);
break;
case UxrSerializationFormat.BinaryGzip:
{
using MemoryStream compressedStream = new MemoryStream(serializedState, 4, serializedState.Length - 4);
using GZipStream gzipStream = new GZipStream(compressedStream, CompressionMode.Decompress);
using MemoryStream uncompressedStream = new MemoryStream();
gzipStream.CopyTo(uncompressedStream);
uncompressedLength = uncompressedStream.Length;
uncompressedStream.Position = 0;
DeserializeUncompressed(uncompressedStream);
break;
}
default:
if (UxrGlobalSettings.Instance.LogLevelCore >= UxrLogLevel.Errors)
{
Debug.LogError($"{UxrConstants.CoreModule}: {nameof(UxrManager)}.{nameof(LoadStateChanges)}(): Serialized data format is unknown ({format}).");
}
CancelSync();
return;
}
void DeserializeUncompressed(Stream inputStream)
{
using BinaryReader reader = new BinaryReader(inputStream);
UxrBinarySerializer serializer = new UxrBinarySerializer(reader, serializationVersion);
try
{
while (reader.BaseStream.Position < reader.BaseStream.Length)
{
int componentSize = -1;
IUxrUniqueId unique = null;
serializer.Serialize(ref componentSize);
long posBeforeComponent = reader.BaseStream.Position;
try
{
serializer.SerializeUniqueComponent(ref unique);
if (unique is IUxrStateSave stateSave)
{
int stateSerializationVersion = reader.ReadCompressedInt32(serializationVersion);
stateSave.SerializeState(serializer, stateSerializationVersion, level);
if (unique is UxrAvatar avatar)
{
loadedAvatars.Add(avatar);
}
if (UxrGlobalSettings.Instance.LogLevelCore >= UxrLogLevel.Debug)
{
Debug.Log($"{UxrConstants.CoreModule}: {nameof(UxrManager)}.{nameof(LoadStateChanges)}(): {count + 1}: Deserialized {unique.Component.name} (type {unique.GetType().Name}). Id is {unique.UniqueId}.");
}
count++;
}
}
catch (Exception e)
{
if (UxrGlobalSettings.Instance.LogLevelCore >= UxrLogLevel.Warnings)
{
Debug.LogWarning($"{UxrConstants.CoreModule}: {nameof(UxrManager)}.{nameof(LoadStateChanges)}(): Cannot deserialize a component. Skipping: {e}");
}
}
reader.BaseStream.Seek(posBeforeComponent + componentSize, SeekOrigin.Begin);
}
}
catch (Exception e)
{
if (UxrGlobalSettings.Instance.LogLevelCore >= UxrLogLevel.Errors)
{
Debug.LogError($"{UxrConstants.CoreModule}: {nameof(UxrManager)}.{nameof(LoadStateChanges)}(): Error deserializing a component length. Cannot continue with the remaining components: {e}");
}
}
}
// When deserializing, make sure to trigger the avatar movement events by using MoveAvatarTo using the current position.
foreach (UxrAvatar avatar in loadedAvatars)
{
MoveAvatarTo(avatar, avatar.CameraFloorPosition);
}
// This avoids propagation of any synchronization message while loading.
CancelSync();
if (UxrGlobalSettings.Instance.LogLevelCore >= UxrLogLevel.Verbose)
{
string compressionInfo = string.Empty;
if (uncompressedLength != -1)
{
compressionInfo = $" Compressed from {uncompressedLength} bytes ({(float)uncompressedLength / serializedState.Length:0.00} compression ratio).";
}
sw.Stop();
TimeSpan elapsed = sw.Elapsed;
double milliseconds = (double)elapsed.Ticks / TimeSpan.TicksPerMillisecond;
Debug.Log($"{UxrConstants.CoreModule}: {nameof(UxrManager)}.{nameof(LoadStateChanges)}(): Deserialized {count} components from {serializedState.Length} bytes in {milliseconds:F3}ms. Format: {format}, level: {level}.{compressionInfo}");
}
}
///
/// Executes a state change, serialized using , so that it is
/// processed by the same component.
///
/// Event serialized using
///
/// The result, containing the target of the event and the event data. If there were errors deserializing the
/// data or trying to execute the event, will return true and
/// will get the error message.
///
public UxrStateSyncResult ExecuteStateSyncEvent(byte[] serializedEvent)
{
IUxrStateSync stateSync = null;
UxrSyncEventArgs eventArgs = null;
string errorMessage = null;
try
{
if (UxrSyncEventArgs.DeserializeEventBinary(serializedEvent, out stateSync, out eventArgs, out errorMessage))
{
errorMessage = null;
IsInsideStateSync = true;
stateSync.SyncState(eventArgs);
}
}
catch (Exception e)
{
errorMessage = e.ToString();
}
finally
{
IsInsideStateSync = false;
}
UxrStateSyncResult result = new UxrStateSyncResult(stateSync, eventArgs, errorMessage);
if (UxrGlobalSettings.Instance.LogLevelCore >= UxrLogLevel.Verbose)
{
Debug.Log($"{UxrConstants.CoreModule}: {nameof(UxrManager)}.{nameof(ExecuteStateSyncEvent)}(): Deserialized and processed {serializedEvent.Length} bytes of event data: {result}");
}
return result;
}
///
/// Translates an avatar.
///
/// The avatar to translate
/// Translation offset
///
/// Whether to propagate /
/// events
///
public void TranslateAvatar(UxrAvatar avatar, Vector3 translation, bool propagateEvents = true)
{
MoveAvatarTo(avatar, avatar.CameraFloorPosition + translation, avatar.ProjectedCameraForward, propagateEvents);
}
///
/// Moves an avatar to a new position on the floor, keeping the same viewing direction. The eye level is maintained.
///
/// The avatar to move
///
/// The position on the floor above which the avatar's camera will be positioned.
/// Coordinates need to be specified at ground level since the eye camera level over the floor will be maintained.
///
///
/// Whether to propagate /
/// events
///
public void MoveAvatarTo(UxrAvatar avatar, Vector3 newFloorPosition, bool propagateEvents = true)
{
MoveAvatarTo(avatar, newFloorPosition, avatar.ProjectedCameraForward, propagateEvents);
}
///
/// Moves an avatar to a new position on the floor and a viewing direction. The eye level is maintained.
///
/// The avatar to move
///
/// The position on the floor above which the avatar's camera will be positioned.
/// Coordinates need to be specified at ground level since the eye camera level over the floor will be maintained.
///
/// The new viewing direction of the avatar, including the camera.
///
/// Whether to propagate /
/// events
///
public void MoveAvatarTo(UxrAvatar avatar, Vector3 newFloorPosition, Vector3 newForward, bool propagateEvents = true)
{
// This method will be synchronized through network
BeginSync(UxrStateSyncOptions.Network);
Transform avatarTransform = avatar.transform;
Vector3 oldPosition = avatarTransform.position;
Quaternion oldRotation = avatarTransform.rotation;
Vector3 newPosition = oldPosition;
Quaternion newRotation = oldRotation;
TransformExt.ApplyAlignment(ref newPosition, ref newRotation, avatar.CameraFloorPosition, Quaternion.LookRotation(avatar.ProjectedCameraForward), newFloorPosition, Quaternion.LookRotation(newForward));
OnAvatarMoving(avatar, new UxrAvatarMoveEventArgs(oldPosition, oldRotation, newPosition, newRotation), propagateEvents);
avatarTransform.SetPositionAndRotation(newPosition, newRotation);
// We place the EndSyncMethod() before the OnAvatarMoved() so that any synchronized events depending on AvatarMoved don't get nested and are processed instead.
EndSyncMethod(new object[] { avatar, newFloorPosition, newForward, propagateEvents });
OnAvatarMoved(avatar, new UxrAvatarMoveEventArgs(oldPosition, oldRotation, newPosition, newRotation), propagateEvents);
}
///
/// See MoveAvatarTo.
///
/// The avatar to move
/// The position and orientation on the floor
///
/// Whether to propagate /
/// events
///
public void MoveAvatarTo(UxrAvatar avatar, Transform destination, bool propagateEvents = true)
{
if (avatar && destination)
{
MoveAvatarTo(avatar, destination.position, destination.forward, propagateEvents);
}
}
///
/// Moves the avatar to a new floor level.
///
/// The avatar to move
/// The new floor level (Y)
///
/// Whether to propagate /
/// events
///
public void MoveAvatarTo(UxrAvatar avatar, float floorLevel, bool propagateEvents = true)
{
if (avatar)
{
Vector3 newPosition = avatar.CameraFloorPosition;
newPosition.y = floorLevel;
MoveAvatarTo(avatar, newPosition, propagateEvents);
}
}
///
/// Rotates the avatar around its vertical axis, where a positive angle turns it to the right and a negative angle to
/// the left.
///
/// The avatar to rotate
/// The degrees to rotate
///
/// Whether to propagate /
/// events
///
public void RotateAvatar(UxrAvatar avatar, float degrees, bool propagateEvents = true)
{
Transform avatarTransform = avatar.transform;
MoveAvatarTo(avatar, avatar.CameraFloorPosition, avatar.ProjectedCameraForward.GetRotationAround(avatarTransform.up, degrees), propagateEvents);
}
///
/// Teleports the local . The local avatar is the avatar controlled by the user using the
/// headset and input controllers. Non-local avatars are other avatars instantiated in the scene but not controlled by
/// the user, either other users through the network or other scenarios such as automated replays.
///
///
/// World-space floor-level position the avatar will be teleported over. The camera position will be on top of the
/// floor position, keeping the original eye-level.
///
///
/// World-space rotation the avatar will be teleported to. The camera will point in the rotation's forward direction.
///
/// The type of translation to use. By default it will teleport immediately
///
/// If has a duration, it will specify how long the
/// teleport transition will take in seconds. By default it is
///
///
/// Optional callback executed depending on the teleportation mode:
///
/// - : Right after finishing the teleportation.
/// -
/// : When the screen is completely faded out and the avatar has been
/// moved, before fading back in. This can be used to enable/disable/change GameObjects in the scene since the
/// screen at this point is fully rendered using the fade color.
///
/// - : Right after finishing the teleportation.
///
///
///
/// Optional callback executed right after the teleportation finished. It will receive a boolean parameter telling
/// whether the teleport finished completely (true) or was cancelled (false). If a fade effect has been requested, the
/// callback is executed right after the screen has faded back in.
///
///
/// Whether to propagate /
/// events
///
/// Coroutine enumerator
///
/// If translation mode was specified, the default black fade color can be
/// changed using .
///
public void TeleportLocalAvatar(Vector3 newFloorPosition,
Quaternion newRotation,
UxrTranslationType translationType = UxrTranslationType.Immediate,
float transitionSeconds = UxrConstants.TeleportTranslationSeconds,
Action teleportedCallback = null,
Action finishedCallback = null,
bool propagateEvents = true)
{
if (_teleportCoroutine != null)
{
StopCoroutine(_teleportCoroutine);
}
bool hasFinished = false;
_teleportCoroutine = StartCoroutine(TeleportLocalAvatarCoroutine(newFloorPosition, newRotation, translationType, transitionSeconds, teleportedCallback, () => hasFinished = true, propagateEvents));
finishedCallback?.Invoke(hasFinished);
}
///
/// Teleports the local while making sure to keep relative position/orientation on moving
/// objects. Some values have a transition before the teleport to avoid motion
/// sickness. On worlds with moving platforms it is important to specify the destination transform so that:
///
/// - Relative position/orientation to the destination is preserved.
/// - Optionally the local avatar can be parented to the new destination.
///
/// The local avatar is the avatar controlled by the user using the headset and input controllers. Non-local avatars
/// are other avatars instantiated in the scene but not controlled by the user, either other users through the network
/// or other scenarios such as automated replays.
///
///
/// The object the avatar should keep relative position/orientation to. This should be the moving object the avatar has
/// teleported on top of
///
///
/// Whether to parent the avatar to . The avatar should be parented if it's being
/// teleported to a moving hierarchy it is not part of
///
///
/// World-space floor-level position the avatar will be teleported over. The camera position will be on top of the
/// floor position, keeping the original eye-level.
///
///
/// World-space rotation the avatar will be teleported to. The camera will point in the rotation's forward direction.
///
/// The type of translation to use. By default it will teleport immediately
///
/// If has a duration, it will specify how long the
/// teleport transition will take in seconds. By default it is
///
///
/// Optional callback executed depending on the teleportation mode:
///
/// - : Right after finishing the teleportation.
/// -
/// : When the screen is completely faded out and the avatar has been
/// moved, before fading back in. This can be used to enable/disable/change GameObjects in the scene since the
/// screen at this point is fully rendered using the fade color.
///
/// - : Right after finishing the teleportation.
///
///
///
/// Optional callback executed right after the teleportation finished. It will receive a boolean parameter telling
/// whether the teleport finished completely (true) or was cancelled (false). If a fade effect has been requested, the
/// callback is executed right after the screen has faded back in.
///
///
/// Whether to propagate /
/// events
///
/// Coroutine enumerator
///
/// If translation mode was specified, the default black fade color can be
/// changed using .
///
public void TeleportLocalAvatarRelative(Transform referenceTransform,
bool parentToReference,
Vector3 newFloorPosition,
Quaternion newRotation,
UxrTranslationType translationType = UxrTranslationType.Immediate,
float transitionSeconds = UxrConstants.TeleportTranslationSeconds,
Action teleportedCallback = null,
Action finishedCallback = null,
bool propagateEvents = true)
{
if (_teleportCoroutine != null)
{
StopCoroutine(_teleportCoroutine);
}
Vector3 newRelativeFloorPosition = referenceTransform != null ? referenceTransform.InverseTransformPoint(newFloorPosition) : newFloorPosition;
Quaternion newRelativeRotation = referenceTransform != null ? Quaternion.Inverse(referenceTransform.rotation) * newRotation : newRotation;
bool hasFinished = false;
_teleportCoroutine = StartCoroutine(TeleportLocalAvatarRelativeCoroutine(referenceTransform, parentToReference, newRelativeFloorPosition, newRelativeRotation, translationType, transitionSeconds, teleportedCallback, () => hasFinished = true, propagateEvents));
finishedCallback?.Invoke(hasFinished);
}
///
///
/// Asynchronous version of TeleportLocalAvatar.
///
/// Teleports the local . The local avatar is the avatar controlled by the user using the
/// headset and input controllers. Non-local avatars are other avatars instantiated in the scene but not controlled by
/// the user, either other users through the network or other scenarios such as automated replays.
///
///
/// World-space floor-level position the avatar will be teleported over. The camera position will be on top of the
/// floor position, keeping the original eye-level.
///
///
/// World-space rotation the avatar will be teleported to. The camera will point in the rotation's forward direction.
///
/// The type of translation to use. By default it will teleport immediately
///
/// If has a duration, it will specify how long the
/// teleport transition will take in seconds. By default it is
///
///
/// Optional callback executed depending on the teleportation mode:
///
/// - : Right after finishing the teleportation.
/// -
/// : When the screen is completely faded out and the avatar has been
/// moved, before fading back in. This can be used to enable/disable/change GameObjects in the scene since the
/// screen at this point is fully rendered using the fade color.
///
/// - : Right after finishing the teleportation.
///
///
/// Optional cancellation token that can be used to cancel the task
///
/// Whether to propagate /
/// events
///
/// Awaitable that will finish after the avatar was teleported or if it was cancelled
/// Task was canceled using
///
/// If translation mode was specified, the default black fade color can be
/// changed using .
///
public async Task TeleportLocalAvatarAsync(Vector3 newFloorPosition,
Quaternion newRotation,
UxrTranslationType translationType = UxrTranslationType.Immediate,
float transitionSeconds = UxrConstants.TeleportTranslationSeconds,
Action teleportedCallback = null,
CancellationToken ct = default,
bool propagateEvents = true)
{
if (_teleportCoroutine != null)
{
StopCoroutine(_teleportCoroutine);
}
_teleportCoroutine = StartCoroutine(TeleportLocalAvatarCoroutine(newFloorPosition, newRotation, translationType, transitionSeconds, teleportedCallback, null, propagateEvents));
await TaskExt.WaitUntil(() => _teleportCoroutine == null, ct);
if (ct.IsCancellationRequested)
{
StopCoroutine(_teleportCoroutine);
_teleportCoroutine = null;
}
}
///
///
/// Asynchronous version of TeleportLocalAvatar.
///
/// Teleports the local . The local avatar is the avatar controlled by the user using the
/// headset and input controllers. Non-local avatars are other avatars instantiated in the scene but not controlled by
/// the user, either other users through the network or other scenarios such as automated replays.
///
///
/// The object the avatar should keep relative position/orientation to. This should be the moving object the avatar has
/// teleported on top of
///
///
/// Whether to parent the avatar to . The avatar should be parented if it's being
/// teleported to a moving hierarchy it is not part of
///
///
/// World-space floor-level position the avatar will be teleported over. The camera position will be on top of the
/// floor position, keeping the original eye-level.
///
///
/// World-space rotation the avatar will be teleported to. The camera will point in the rotation's forward direction.
///
/// The type of translation to use. By default it will teleport immediately
///
/// If has a duration, it will specify how long the
/// teleport transition will take in seconds. By default it is
///
///
/// Optional callback executed depending on the teleportation mode:
///
/// - : Right after finishing the teleportation.
/// -
/// : When the screen is completely faded out and the avatar has been
/// moved, before fading back in. This can be used to enable/disable/change GameObjects in the scene since the
/// screen at this point is fully rendered using the fade color.
///
/// - : Right after finishing the teleportation.
///
///
/// Optional cancellation token that can be used to cancel the task
///
/// Whether to propagate /
/// events
///
/// Awaitable that will finish after the avatar was teleported or if it was cancelled
/// Task was canceled using
///
/// If translation mode was specified, the default black fade color can be
/// changed using .
///
public async Task TeleportLocalAvatarRelativeAsync(Transform referenceTransform,
bool parentToReference,
Vector3 newFloorPosition,
Quaternion newRotation,
UxrTranslationType translationType = UxrTranslationType.Immediate,
float transitionSeconds = UxrConstants.TeleportTranslationSeconds,
Action teleportedCallback = null,
CancellationToken ct = default,
bool propagateEvents = true)
{
if (_teleportCoroutine != null)
{
StopCoroutine(_teleportCoroutine);
}
Vector3 newRelativeFloorPosition = referenceTransform != null ? referenceTransform.InverseTransformPoint(newFloorPosition) : newFloorPosition;
Quaternion newRelativeRotation = referenceTransform != null ? Quaternion.Inverse(referenceTransform.rotation) * newRotation : newRotation;
_teleportCoroutine = StartCoroutine(TeleportLocalAvatarRelativeCoroutine(referenceTransform, parentToReference, newRelativeFloorPosition, newRelativeRotation, translationType, transitionSeconds, teleportedCallback, null, propagateEvents));
await TaskExt.WaitUntil(() => _teleportCoroutine == null, ct);
if (ct.IsCancellationRequested)
{
StopCoroutine(_teleportCoroutine);
_teleportCoroutine = null;
}
}
///
/// Rotates the local avatar around its vertical axis, where a positive angle turns it to the right and a negative
/// angle to the left. The rotation can be performed in different ways using .
///
/// The degrees to rotate
/// The type of rotation to use. By default it will rotate immediately
///
/// If has a duration, it will specify how long the
/// rotation transition will take in seconds. By default it is
///
///
/// Optional callback executed depending on the rotation mode:
///
/// - : Right after finishing the rotation.
/// -
/// : When the screen is completely faded out and the avatar has rotated,
/// before fading back in. This can be used to enable/disable/change GameObjects in the scene since the screen
/// at this point is fully rendered using the fade color.
///
/// - : Right after finishing the rotation.
///
///
///
/// Optional callback executed right after the teleportation finished. It will receive a boolean parameter telling
/// whether the teleport finished completely (true) or was cancelled (false). If a fade effect has been requested, the
/// callback is executed right after the screen has faded back in.
///
///
/// Whether to propagate /
/// events
///
///
/// If translation mode was specified, the default black fade color can be
/// changed using .
///
public void RotateLocalAvatar(float degrees,
UxrRotationType rotationType = UxrRotationType.Immediate,
float transitionSeconds = UxrConstants.TeleportRotationSeconds,
Action rotatedCallback = null,
Action finishedCallback = null,
bool propagateEvents = true)
{
if (_teleportCoroutine != null)
{
StopCoroutine(_teleportCoroutine);
}
bool hasFinished = false;
_teleportCoroutine = StartCoroutine(RotateLocalAvatarCoroutine(degrees, rotationType, transitionSeconds, rotatedCallback, () => hasFinished = true, propagateEvents));
finishedCallback?.Invoke(hasFinished);
}
///
/// Asynchronous version of .
///
/// Rotates the local avatar around its vertical axis, where a positive angle turns it to the right and a
/// negative angle to the left. The rotation can be performed in different ways using
/// .
///
///
/// The degrees to rotate
/// The type of rotation to use. By default it will rotate immediately
///
/// If has a duration, it will specify how long the
/// rotation transition will take in seconds. By default it is
///
///
/// Optional callback executed depending on the rotation mode:
///
/// - : Right after finishing the rotation.
/// -
/// : When the screen is completely faded out and the avatar has rotated,
/// before fading back in. This can be used to enable/disable/change GameObjects in the scene since the screen
/// at this point is fully rendered using the fade color.
///
/// - : Right after finishing the rotation.
///
///
/// Optional cancellation token to cancel the operation
///
/// Whether to propagate /
/// events
///
/// Awaitable that will finish when the rotation finished
public async Task RotateLocalAvatarAsync(float degrees,
UxrRotationType rotationType = UxrRotationType.Immediate,
float transitionSeconds = UxrConstants.TeleportRotationSeconds,
Action rotatedCallback = null,
CancellationToken ct = default,
bool propagateEvents = true)
{
if (_teleportCoroutine != null)
{
StopCoroutine(_teleportCoroutine);
}
_teleportCoroutine = StartCoroutine(RotateLocalAvatarCoroutine(degrees, rotationType, transitionSeconds, rotatedCallback, null, propagateEvents));
await TaskExt.WaitUntil(() => _teleportCoroutine == null, ct);
if (ct.IsCancellationRequested)
{
StopCoroutine(_teleportCoroutine);
_teleportCoroutine = null;
}
}
#endregion
#region Internal Methods
///
/// Registers a new component with the interface.
///
/// Custom component
internal void RegisterStateSyncComponent(Component component) where T : IUxrStateSync
{
StateSync_Registered(component as IUxrStateSync);
}
///
/// Removes a component with the interface, because it was destroyed.
///
/// Custom component
internal void UnregisterStateSyncComponent(Component component) where T : IUxrStateSync
{
StateSync_Unregistered(component as IUxrStateSync);
}
#endregion
#region Unity
///
/// Subscribes to global events.
///
protected override void Awake()
{
base.Awake();
UxrAvatar.GlobalEnabled += Avatar_Enabled;
SceneManager.sceneLoaded += SceneManager_SceneLoaded;
SceneManager.sceneUnloaded += SceneManager_SceneUnloaded;
}
///
/// Unsubscribes from global events.
///
protected override void OnDestroy()
{
base.OnDestroy();
UxrAvatar.GlobalEnabled -= Avatar_Enabled;
SceneManager.sceneLoaded -= SceneManager_SceneLoaded;
SceneManager.sceneUnloaded -= SceneManager_SceneUnloaded;
DestroyPrecachedInstances();
}
///
/// Tries to find Unity canvases ( components) and automatically set them up so that they can be
/// used by the framework using .
///
protected override void Start()
{
if (UxrPointerInputModule.Instance == null && UxrGlobalSettings.Instance.LogLevelUI >= UxrLogLevel.Warnings)
{
Debug.LogWarning($"{UxrConstants.UiModule}: no {nameof(EventSystem)} GameObject with a {nameof(UxrPointerInputModule)} component found. Add an {nameof(EventSystem)} using the menu GameObject->UI->EventSystem and add an {nameof(UxrPointerInputModule)} to it to use the Unity UI using UltimateXR");
}
SetupCanvases();
}
///
/// Updates the key entities to the current frame. If the is set to
/// , all the animation (hand poses, manipulation mechanics and Inverse
/// Kinematics) will also be updated.
///
private void Update()
{
OnUpdating();
OnUpdatingStage(UxrUpdateStage.Update);
foreach (UxrAvatarController avatarController in LocalAvatarControllers)
{
OnAvatarUpdating(avatarController.Avatar, new UxrAvatarUpdateEventArgs(avatarController.Avatar, UxrUpdateStage.Update));
((IUxrAvatarControllerUpdater)avatarController).UpdateAvatar();
OnAvatarUpdated(avatarController.Avatar, new UxrAvatarUpdateEventArgs(avatarController.Avatar, UxrUpdateStage.Update));
}
OnStageUpdated(UxrUpdateStage.Update);
if (PostUpdateMode == UxrPostUpdateMode.Update)
{
PostUpdate();
}
}
///
/// Updates the key entities to the current frame. If the is set to
/// , all the animation (hand poses, manipulation mechanics and Inverse
/// Kinematics) will also be updated.
///
private void LateUpdate()
{
if (PostUpdateMode == UxrPostUpdateMode.LateUpdate)
{
PostUpdate();
}
UxrStateSaveImplementer.NotifyEndOfFrame();
}
#endregion
#region Coroutines
///
/// Public teleporting coroutine that can be yielded from an external coroutine.
/// Teleports the local . The local avatar is the avatar controlled by the user using the
/// headset and input controllers. Non-local avatars are other avatars instantiated in the scene but not controlled by
/// the user, either other users through the network or other scenarios such as automated replays.
///
///
/// Floor-level position the avatar will be teleported over. The camera position will be on top of the floor position,
/// keeping the original eye-level.
///
///
/// Rotation the avatar will be teleported to. The camera will point in the rotation's forward
/// direction
///
/// The type of translation to use. By default it will teleport immediately
///
/// If has a duration, it will specify how long the
/// teleport transition will take in seconds. By default it is
///
///
/// Optional callback executed depending on the teleportation mode:
///
/// - : Right after finishing the teleportation.
/// -
/// : When the screen is completely faded out and the avatar has been
/// moved, before fading back in. This can be used to enable/disable/change GameObjects in the scene since the
/// screen at this point is fully rendered using the fade color.
///
/// - : Right after finishing the teleportation.
///
///
///
/// Optional callback executed right after the teleportation finished. If a fade effect has been requested, the
/// callback is executed right after the screen has faded back in.
///
///
/// Whether to propagate /
/// events
///
/// Coroutine enumerator
///
/// If translation mode was specified, the default black fade color can be
/// changed using .
///
public IEnumerator TeleportLocalAvatarCoroutine(Vector3 newFloorPosition,
Quaternion newRotation,
UxrTranslationType translationType = UxrTranslationType.Immediate,
float transitionSeconds = UxrConstants.TeleportTranslationSeconds,
Action teleportedCallback = null,
Action finishedCallback = null,
bool propagateEvents = true)
{
yield return TeleportLocalAvatarRelativeCoroutine(null, false, newFloorPosition, newRotation, translationType, transitionSeconds, teleportedCallback, finishedCallback, propagateEvents);
}
///
/// Public teleporting coroutine that can be yielded from an external coroutine.
/// Teleports the local while making sure to keep relative position/orientation on moving
/// objects. Some values have a transition before the teleport to avoid motion
/// sickness. On worlds with moving platforms it is important to specify the destination transform so that:
///
/// - Relative position/orientation to the destination is preserved.
/// - Optionally the local avatar can be parented to the new destination.
///
/// The local avatar is the avatar controlled by the user using the headset and input controllers. Non-local avatars
/// are other avatars instantiated in the scene but not controlled by the user, either other users through the network
/// or other scenarios such as automated replays.
///
///
/// The object the avatar should keep relative position/orientation to. This should be the moving object the avatar has
/// teleported on top of. If null, and
/// will be interpreted as world coordinates.
///
///
/// Whether to parent the avatar to . The avatar should be parented if it's being
/// teleported to a moving hierarchy it is not part of
///
///
/// New floor-level position the avatar will be teleported over in local
/// coordinates. If is null, coordinates will be interpreted as being in
/// world-space. The camera position will be on top of the floor position, keeping the original eye-level.
///
///
/// Local rotation the avatar will be teleported to with respect to . If
/// is null, rotation will be in world-space. The camera will point in the
/// rotation's forward direction.
///
/// The type of translation to use. By default it will teleport immediately
///
/// If has a duration, it will specify how long the
/// teleport transition will take in seconds. By default it is
///
///
/// Optional callback executed depending on the teleportation mode:
///
/// - : Right after finishing the teleportation.
/// -
/// : When the screen is completely faded out and the avatar has been
/// moved, before fading back in. This can be used to enable/disable/change GameObjects in the scene since the
/// screen at this point is fully rendered using the fade color.
///
/// - : Right after finishing the teleportation.
///
///
///
/// Optional callback executed right after the teleportation finished. If a fade effect has been requested, the
/// callback is executed right after the screen has faded back in.
///
///
/// Whether to propagate /
/// events
///
/// Coroutine enumerator
///
/// If translation mode was specified, the default black fade color can be
/// changed using .
///
public IEnumerator TeleportLocalAvatarRelativeCoroutine(Transform referenceTransform,
bool parentToReference,
Vector3 newRelativeFloorPosition,
Quaternion newRelativeRotation,
UxrTranslationType translationType = UxrTranslationType.Immediate,
float transitionSeconds = UxrConstants.TeleportTranslationSeconds,
Action teleportedCallback = null,
Action finishedCallback = null,
bool propagateEvents = true)
{
if (UxrAvatar.LocalAvatar)
{
Vector3 oldFloorPosition = UxrAvatar.LocalAvatar.CameraFloorPosition;
Quaternion oldFloorRotation = Quaternion.LookRotation(UxrAvatar.LocalAvatar.ProjectedCameraForward);
Quaternion inverseReferenceRotation = referenceTransform != null ? Quaternion.Inverse(referenceTransform.rotation) : Quaternion.identity;
Matrix4x4 inverseReferenceMatrix = referenceTransform != null ? referenceTransform.localToWorldMatrix.inverse : Matrix4x4.identity;
Vector3 oldRelativePosition = inverseReferenceMatrix * oldFloorPosition;
Quaternion oldRelativeRotation = inverseReferenceRotation * oldFloorRotation;
void TranslateAvatarInternal(float t = 1.0f)
{
Vector3 newPos = Vector3.Lerp(oldRelativePosition, newRelativeFloorPosition, t);
Quaternion newRot = oldRelativeRotation;
if (Mathf.Approximately(t, 1.0f))
{
newRot = newRelativeRotation;
}
if (referenceTransform != null)
{
newPos = referenceTransform.TransformPoint(newPos);
newRot = referenceTransform.rotation * newRot;
}
MoveAvatarTo(UxrAvatar.LocalAvatar, newPos, newRot * Vector3.forward, propagateEvents);
}
switch (translationType)
{
case UxrTranslationType.Immediate:
TranslateAvatarInternal();
teleportedCallback?.Invoke();
break;
case UxrTranslationType.Fade:
yield return UxrAvatar.LocalAvatar.CameraFade.StartFadeCoroutine(transitionSeconds * 0.5f, TeleportFadeColor.WithAlpha(0.0f), TeleportFadeColor.WithAlpha(1.0f));
TranslateAvatarInternal();
teleportedCallback?.Invoke();
yield return null;
yield return UxrAvatar.LocalAvatar.CameraFade.StartFadeCoroutine(transitionSeconds * 0.5f, TeleportFadeColor.WithAlpha(1.0f), TeleportFadeColor.WithAlpha(0.0f));
break;
case UxrTranslationType.Smooth:
yield return this.LoopCoroutine(transitionSeconds, TranslateAvatarInternal, UxrEasing.Linear, true);
break;
}
if (parentToReference && referenceTransform != null)
{
UxrAvatar.LocalAvatar.transform.SetParent(referenceTransform);
}
}
_teleportCoroutine = null;
finishedCallback?.Invoke();
}
///
/// Public avatar rotation coroutine that can be yielded from an external coroutine.
/// Rotates the avatar around its vertical axis, where a positive angle turns it to the right and a negative angle to
/// the left.
///
/// The degrees to rotate
/// The type of rotation to use. By default it will rotate immediately
///
/// If has a duration, it will specify how long the
/// rotation transition will take in seconds. By default it is
///
///
/// Optional callback executed depending on the rotation mode:
///
/// - : Right after finishing the rotation.
/// -
/// : When the screen is completely faded out and the avatar has rotated,
/// before fading back in. This can be used to enable/disable/change GameObjects in the scene since the screen
/// at this point is fully rendered using the fade color.
///
/// - : Right after finishing the rotation.
///
///
///
/// Optional callback executed right after the rotation finished. If a fade effect has been requested, the callback is
/// executed right after the screen has faded back in.
///
///
/// Whether to propagate /
/// events
///
/// Coroutine enumerator
///
/// If translation mode was specified, the default black fade color can be changed
/// using .
///
public IEnumerator RotateLocalAvatarCoroutine(float degrees,
UxrRotationType rotationType = UxrRotationType.Immediate,
float transitionSeconds = UxrConstants.TeleportRotationSeconds,
Action rotatedCallback = null,
Action finishedCallback = null,
bool propagateEvents = true)
{
if (UxrAvatar.LocalAvatar)
{
void RotateAvatarInternal(float t = 1.0f)
{
Transform avatarTransform = UxrAvatar.LocalAvatar.transform;
Vector3 initialForward = UxrAvatar.LocalAvatar.ProjectedCameraForward;
MoveAvatarTo(UxrAvatar.LocalAvatar, UxrAvatar.LocalAvatar.CameraFloorPosition, initialForward.GetRotationAround(avatarTransform.up, degrees * t), propagateEvents);
}
switch (rotationType)
{
case UxrRotationType.Immediate:
RotateAvatarInternal();
rotatedCallback?.Invoke();
break;
case UxrRotationType.Fade:
yield return UxrAvatar.LocalAvatar.CameraFade.StartFadeCoroutine(transitionSeconds * 0.5f, TeleportFadeColor.WithAlpha(0.0f), TeleportFadeColor.WithAlpha(1.0f));
RotateAvatarInternal();
rotatedCallback?.Invoke();
yield return null;
yield return UxrAvatar.LocalAvatar.CameraFade.StartFadeCoroutine(transitionSeconds * 0.5f, TeleportFadeColor.WithAlpha(1.0f), TeleportFadeColor.WithAlpha(0.0f));
break;
case UxrRotationType.Smooth:
yield return this.LoopCoroutine(transitionSeconds, RotateAvatarInternal, UxrEasing.Linear, true);
break;
}
}
_teleportCoroutine = null;
finishedCallback?.Invoke();
}
///
///
/// Precaching coroutine. It will try to find all components in the scene and
/// pre-instantiate their objects in front of the camera while the screen is still faded black.
/// The goal is to make sure all resources (meshes, textures) are in memory so that when they are instantiated at
/// any point, the resources are already available lowering the chances of rendering hiccups.
/// The scene is rendered black on top during a pre-determined number of frames ()
/// after which the pre-instantiated objects will be destroyed and the scene will be faded in.
///
///
/// Another preprocessing that takes place is finding initially disabled components and
/// force registering their Unique IDs to overcome the limitation of initially disabled components not being able
/// to receive state synchronization messages.
///
///
/// Optional callback called when precaching is right about to start
/// Optional callback called right after precaching finished
/// Coroutine enumerator
private IEnumerator PrecacheCoroutine(Action onStarting = null, Action onFinished = null)
{
UxrAvatar avatar = UxrAvatar.LocalAvatar;
while (avatar == null)
{
yield return null;
avatar = UxrAvatar.LocalAvatar;
}
onStarting?.Invoke();
DestroyPrecachedInstances();
_dynamicInstances = new Dictionary();
for (int sceneIndex = 0; sceneIndex < SceneManager.sceneCount; ++sceneIndex)
{
Scene scene = SceneManager.GetSceneAt(sceneIndex);
AddScenePrecachedInstances(_dynamicInstances, scene, avatar);
}
AddScenePrecachedInstances(_dynamicInstances, Instance.gameObject.scene, avatar);
for (int frame = 0; frame < _precacheFrameCount; ++frame)
{
if (avatar == null)
{
// Another scene loaded
break;
}
avatar.CameraFade.EnableFadeColor(Color.black, 1.0f);
yield return null;
}
DestroyPrecachedInstances();
onFinished?.Invoke();
float startFadeTime = Time.time;
float fadeDuration = 0.5f;
while (Time.time - startFadeTime < fadeDuration)
{
if (avatar == null)
{
// Another scene loaded
break;
}
avatar.CameraFade.EnableFadeColor(Color.black, 1.0f - (Time.time - startFadeTime) / fadeDuration);
yield return null;
}
if (avatar)
{
avatar.CameraFade.DisableFadeColor();
}
_precacheCoroutine = null;
}
#endregion
#region Event Handling Methods
///
/// Called when any component with the interface has a state change.
///
/// Sender (component implementing )
/// Event parameters
private void StateSync_StateChanged(object sender, UxrSyncEventArgs eventArgs)
{
// Don't generate ComponentChanged events inside ExecuteStateSyncEvent() to avoid infinite message loop
if (!IsInsideStateSync && sender is IUxrStateSync component)
{
OnComponentStateChanged(component, eventArgs);
}
}
///
/// Called when an is enabled. If the avatar is the local avatar, it is used as a signal to
/// set up canvases in the scene and start the pre-caching process.
///
/// Avatar that was enabled
private void Avatar_Enabled(UxrAvatar avatar)
{
if (avatar.AvatarMode == UxrAvatarMode.Local)
{
if (UxrPointerInputModule.Instance != null && UxrPointerInputModule.Instance.AutoAssignEventCamera)
{
foreach (UxrCanvas canvas in UxrCanvas.AllComponents)
{
if (canvas.UnityCanvas)
{
canvas.UnityCanvas.worldCamera = avatar.CameraComponent;
}
}
}
// In multiplayer environments the avatar might be instantiated in Local mode but switched
// later to UpdateExternally. Don't precache when there is more than 1.
if (UxrAvatar.AllComponents.Count(a => a.AvatarMode == UxrAvatarMode.Local) == 1)
{
TryPrecaching();
}
}
}
///
/// Called when a Unity scene was loaded. It is used to try to automatically set up the canvases in the scene so that
/// they can be used with UltimateXR.
///
/// Scene that was loaded.
/// The mode used to load the scene.
private void SceneManager_SceneLoaded(Scene scene, LoadSceneMode loadSceneMode)
{
SetupCanvases();
}
///
/// Called when a Unity scene was unloaded. It is used to try to automatically set up the canvases in the scene so that
/// they can be used with UltimateXR.
///
/// Scene that was unloaded.
private void SceneManager_SceneUnloaded(Scene scene)
{
SetupCanvases();
}
///
/// Called when a component implementing is being enabled. We use it to subscribe to the
/// event to keep track of any state changes in components in UltimateXR.
///
/// Component that was enabled
private void StateSync_Registered(IUxrStateSync component)
{
component.StateChanged += StateSync_StateChanged;
}
///
/// Called when a component implementing is being disabled. We use it to unsubscribe from
/// the event.
///
/// Component that was disabled
private void StateSync_Unregistered(IUxrStateSync component)
{
component.StateChanged -= StateSync_StateChanged;
}
#endregion
#region Event Trigger Methods
///
/// Event trigger for the event.
///
/// Component with the state change
/// Event parameters
private void OnComponentStateChanged(IUxrStateSync component, UxrSyncEventArgs eventArgs)
{
if (UxrStateSyncImplementer.SyncCallDepth == 1 || !UseTopLevelStateChangesOnly || eventArgs.Options.HasFlag(UxrStateSyncOptions.IgnoreNestingCheck))
{
ComponentStateChanged?.Invoke(component, eventArgs);
}
}
///
/// event trigger.
///
private void OnPrecachingStarting()
{
PrecachingStarting?.Invoke();
}
///
/// event trigger.
///
private void OnPrecachingFinished()
{
PrecachingFinished?.Invoke();
}
///
/// event trigger.
///
/// The avatar that is about to move
/// Event parameters
/// Whether to propagate events
private void OnAvatarMoving(UxrAvatar avatar, UxrAvatarMoveEventArgs args, bool propagateEvents = true)
{
if (propagateEvents)
{
avatar.RaiseAvatarMoving(args);
}
}
///
/// event trigger.
///
/// The avatar that moved
/// Event parameters
/// Whether to propagate events
private void OnAvatarMoved(UxrAvatar avatar, UxrAvatarMoveEventArgs args, bool propagateEvents = true)
{
if (propagateEvents)
{
avatar.RaiseAvatarMoved(args);
}
}
///
/// event trigger.
///
private void OnUpdating()
{
AvatarsUpdating?.Invoke();
}
///
/// event trigger.
///
private void OnUpdated()
{
AvatarsUpdated?.Invoke();
}
///
/// event trigger.
///
private void OnUpdatingStage(UxrUpdateStage stage)
{
StageUpdating?.Invoke(stage);
}
///
/// event trigger.
///
private void OnStageUpdated(UxrUpdateStage stage)
{
StageUpdated?.Invoke(stage);
}
///
/// UxrAvatar.AvatarUpdating event trigger.
///
private void OnAvatarUpdating(UxrAvatar avatar, UxrAvatarUpdateEventArgs e)
{
avatar.RaiseAvatarUpdating(e);
}
///
/// UxrAvatar.AvatarUpdated event trigger.
///
private void OnAvatarUpdated(UxrAvatar avatar, UxrAvatarUpdateEventArgs e)
{
avatar.RaiseAvatarUpdated(e);
}
#endregion
#region Private Methods
///
/// Performs the post-update: Updates the animation and interaction of all key entities, while sending all related
/// events during the process.
/// Main updates are:
///
/// - Avatar animation.
/// - Manipulation mechanics and constraints.
/// - Other managers in the framework such as the .
/// - Inverse kinematics.
///
///
private void PostUpdate()
{
// Avatar bones that are tracked
OnUpdatingStage(UxrUpdateStage.AvatarUsingTracking);
foreach (UxrAvatarController avatarController in LocalAvatarControllers)
{
OnAvatarUpdating(avatarController.Avatar, new UxrAvatarUpdateEventArgs(avatarController.Avatar, UxrUpdateStage.AvatarUsingTracking));
((IUxrAvatarControllerUpdater)avatarController).UpdateAvatarUsingTrackingDevices();
OnAvatarUpdated(avatarController.Avatar, new UxrAvatarUpdateEventArgs(avatarController.Avatar, UxrUpdateStage.AvatarUsingTracking));
}
OnStageUpdated(UxrUpdateStage.AvatarUsingTracking);
// Update manipulation. Non-local avatars manipulation will also be updated to sync manipulation events.
OnUpdatingStage(UxrUpdateStage.Manipulation);
foreach (UxrAvatarController avatarController in EnabledAvatarControllers)
{
OnAvatarUpdating(avatarController.Avatar, new UxrAvatarUpdateEventArgs(avatarController.Avatar, UxrUpdateStage.Manipulation));
}
UxrGrabManager.Instance.UpdateManager();
UxrWeaponManager.Instance.UpdateManager();
foreach (UxrAvatarController avatarController in EnabledAvatarControllers)
{
((IUxrAvatarControllerUpdater)avatarController).UpdateAvatarManipulation();
}
foreach (UxrAvatarController avatarController in EnabledAvatarControllers)
{
OnAvatarUpdated(avatarController.Avatar, new UxrAvatarUpdateEventArgs(avatarController.Avatar, UxrUpdateStage.Manipulation));
}
OnStageUpdated(UxrUpdateStage.Manipulation);
// Update animation
OnUpdatingStage(UxrUpdateStage.Animation);
foreach (UxrAvatar avatar in UxrAvatar.EnabledComponents)
{
if (avatar.AvatarController && avatar.AvatarController.enabled && avatar.AvatarController.Initialized)
{
if (avatar.AvatarMode == UxrAvatarMode.Local)
{
OnAvatarUpdating(avatar, new UxrAvatarUpdateEventArgs(avatar, UxrUpdateStage.Animation));
((IUxrAvatarControllerUpdater)avatar.AvatarController).UpdateAvatarAnimation();
OnAvatarUpdated(avatar, new UxrAvatarUpdateEventArgs(avatar, UxrUpdateStage.Animation));
}
else
{
// This makes sure that hand poses are updated
OnAvatarUpdating(avatar, new UxrAvatarUpdateEventArgs(avatar, UxrUpdateStage.Animation));
avatar.UpdateHandPoseTransforms();
OnAvatarUpdated(avatar, new UxrAvatarUpdateEventArgs(avatar, UxrUpdateStage.Animation));
}
}
}
OnStageUpdated(UxrUpdateStage.Animation);
// Update post-process. All enabled avatar controllers are updated, not just the local one, so that IK is computed in all.
OnUpdatingStage(UxrUpdateStage.PostProcess);
foreach (UxrAvatarController avatarController in EnabledAvatarControllers)
{
OnAvatarUpdating(avatarController.Avatar, new UxrAvatarUpdateEventArgs(avatarController.Avatar, UxrUpdateStage.PostProcess));
((IUxrAvatarControllerUpdater)avatarController).UpdateAvatarPostProcess();
avatarController.Avatar.AvatarRigInfo.UpdateInfo();
OnAvatarUpdated(avatarController.Avatar, new UxrAvatarUpdateEventArgs(avatarController.Avatar, UxrUpdateStage.PostProcess));
}
OnStageUpdated(UxrUpdateStage.PostProcess);
OnUpdated();
}
///
/// Processes all components in a scene and instantiates all required prefabs in front
/// of the avatar camera. The goal is to make sure all their resources are loaded into memory afterwards.
/// It also registers initially disabled UltimateXR components so that their Unique ID is known and can receive sync
/// messages too.
///
/// List of loaded instances.
/// Scene to get the components from.
/// Current avatar.
private void AddScenePrecachedInstances(Dictionary dynamicInstances, Scene scene, UxrAvatar avatar)
{
for (int rootIndex = 0; rootIndex < scene.rootCount; ++rootIndex)
{
MonoBehaviour[] behaviours = scene.GetRootGameObjects()[rootIndex].GetComponentsInChildren(true);
for (int behaviourIndex = 0; behaviourIndex < behaviours.Length; ++behaviourIndex)
{
if (behaviours[behaviourIndex] is IUxrPrecacheable precacheable)
{
foreach (GameObject precachedInstance in precacheable.PrecachedInstances)
{
if (precachedInstance != null && dynamicInstances.ContainsKey(precachedInstance.GetInstanceID()) == false)
{
// Instantiate
GameObject dynamicInstance = Instantiate(precachedInstance,
avatar.CameraTransform.position + avatar.CameraTransform.forward * 5.0f,
avatar.CameraTransform.rotation,
Instance.transform);
dynamicInstances.Add(precachedInstance.GetInstanceID(), dynamicInstance);
// Avoid sounds
AudioSource[] audioSources = dynamicInstance.GetComponentsInChildren(true);
foreach (AudioSource audioSource in audioSources)
{
audioSource.enabled = false;
}
}
}
}
// Ensure registering initially disabled components so that their UniqueId is known and can exchange sync messages too
if (behaviours[behaviourIndex] != null && !behaviours[behaviourIndex].enabled && behaviours[behaviourIndex] is IUxrUniqueId unique)
{
unique.RegisterIfNecessary();
}
}
}
}
///
/// Destroys the currently loaded pre-cached instances.
///
private void DestroyPrecachedInstances()
{
if (_dynamicInstances != null)
{
foreach (KeyValuePair dynamicInstancePair in _dynamicInstances)
{
if (dynamicInstancePair.Value != null)
{
Destroy(dynamicInstancePair.Value);
}
}
_dynamicInstances.Clear();
}
}
///
/// Starts the pre-caching process. If a pre-caching process is currently running, it will be stopped before starting
/// again.
///
private void TryPrecaching()
{
if (_precacheCoroutine != null)
{
StopCoroutine(_precacheCoroutine);
}
if (UsePrecaching)
{
_precacheCoroutine = StartCoroutine(PrecacheCoroutine(OnPrecachingStarting, OnPrecachingFinished));
}
}
///
/// Tries to set up all components currently in the scene so that they can work with UltimateXR
/// through the component.
///
private void SetupCanvases()
{
if (UxrPointerInputModule.Instance)
{
foreach (Canvas canvas in ComponentExt.GetAllComponentsInOpenScenes