Files
dungeons/Assets/UltimateXR/Runtime/Scripts/Core/UxrManager.cs
2024-08-06 21:58:35 +02:00

2055 lines
106 KiB
C#

// --------------------------------------------------------------------------------------------------------------------
// <copyright file="UxrManager.cs" company="VRMADA">
// Copyright (c) VRMADA, All rights reserved.
// </copyright>
// --------------------------------------------------------------------------------------------------------------------
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
{
/// <summary>
/// <para>
/// Main manager in the UltimateXR framework. As a <see cref="UxrSingleton{T}">UxrSingleton</see> it can be
/// accessed from any point in the application using <see cref="UxrSingleton{T}.Instance">UxrManager.Instance</see>
/// .
/// 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 <see cref="UxrSingleton{T}.Instance">UxrManager.Instance</see>, if no
/// <see cref="UxrManager" /> is currently available, one will be instantiated in the scene as the global
/// Singleton.
/// </para>
/// <para>
/// <see cref="UxrManager" /> 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.
/// </para>
/// <para>
/// <see cref="UxrManager" /> also provides the following functionality:
/// <list type="bullet">
/// <item>
/// Pre-caching prefabs when scenes are loaded to eliminate hiccups using the
/// <see cref="IUxrPrecacheable" /> interface.
/// </item>
/// <item>Moving/rotating/teleporting avatars.</item>
/// <item>Events to get notified when avatars have been moved/rotated/teleported.</item>
/// <item>
/// Events to get notified before and after updating a frame and at different stages of the updating
/// process for finer control: <see cref="AvatarsUpdating" />/<see cref="AvatarsUpdated" /> and
/// <see cref="StageUpdating" />/<see cref="StageUpdated" />.
/// </item>
/// <item>
/// A single event to get notified of all state changes in any component in the framework or
/// any custom user class: <see cref="ComponentStateChanged" />. Also a way to execute back state change
/// events, helping implement network synchronization and save-to-file/replay functionality:
/// <see cref="ExecuteStateSyncEvent" /> .
/// </item>
/// <item>
/// 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: <see cref="SaveStateChanges" /> and
/// <see cref="LoadStateChanges" />.
/// </item>
/// </list>
/// </para>
/// </summary>
public sealed class UxrManager : UxrSingleton<UxrManager>
{
#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
/// <summary>
/// Invoked to notify a state change in a <see cref="IUxrStateSync" /> component. This can be used to synchronize the
/// event over the network or capture it in a replay/save state system. <br />
/// The <see cref="IUxrStateSync" /> interface is implemented in the <see cref="UxrComponent" /> base class,
/// serving as the foundation for all components within UltimateXR. While this interface is readily available in
/// <see cref="UxrComponent" />, users may also implement it in custom classes where inheritance from
/// <see cref="UxrComponent" /> is not feasible due to limitations in multiple inheritance.
/// <see cref="UxrStateSyncImplementer{T}" /> helps leverage this interface implementation.<br />
/// <see cref="UxrSyncEventArgs.SerializeEventBinary" /> is used to serialize the event.
/// Serialized events can subsequently be executed via <see cref="ExecuteStateSyncEvent" />.
/// This streamlined functionality simplifies networking implementation with UltimateXR:
/// <list type="bullet">
/// <item>
/// A central entry point to capture all events in UltimateXR: <see cref="ComponentStateChanged" />
/// </item>
/// <item>
/// A method to serialize the event into a byte array: <see cref="UxrSyncEventArgs.SerializeEventBinary" />
/// </item>
/// <item>
/// A means to execute an event for replicating the state change in another device or session:
/// <see cref="ExecuteStateSyncEvent" />
/// </item>
/// </list>
/// By default, nested state changes are ignored to optimize bandwidth usage and prevent redundant calls that
/// might result in inconsistencies.<br />
/// Since the root state change already triggers the nested ones, there's no need to synchronize them again.
/// For additional details, refer to <see cref="UxrManager.UseTopLevelStateChangesOnly" />.
/// </summary>
public static event Action<IUxrStateSync, UxrSyncEventArgs> ComponentStateChanged;
/// <summary>
/// Called right before precaching is about to start. It's called on the first frame that is displayed black.
/// See <see cref="UsePrecaching" />.
/// </summary>
public static event Action PrecachingStarting;
/// <summary>
/// Called right after precaching finished. It's called on the first frame that starts to fade-in from black.
/// See <see cref="UsePrecaching" />.
/// </summary>
public static event Action PrecachingFinished;
/// <summary>
/// Called right before processing all update stages in the current frame. Equivalent to <see cref="StageUpdating" />
/// for <see cref="UxrUpdateStage.Update" />
/// </summary>
public static event Action AvatarsUpdating;
/// <summary>
/// Called right after processing all update stages in the current frame. Equivalent to <see cref="StageUpdated" /> for
/// <see cref="UxrUpdateStage.PostProcess" />
/// </summary>
public static event Action AvatarsUpdated;
/// <summary>
/// Called right before an update stage in the current frame. See <see cref="UxrUpdateStage" />.
/// </summary>
public static event Action<UxrUpdateStage> StageUpdating;
/// <summary>
/// Called right after an update stage in the current frame. See <see cref="UxrUpdateStage" />.
/// </summary>
public static event Action<UxrUpdateStage> StageUpdated;
/// <summary>
/// Gets or sets whether the <see cref="ComponentStateChanged" /> 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.
/// </summary>
public static bool UseTopLevelStateChangesOnly { get; set; } = true;
/// <summary>
/// Gets whether the manager is currently pre-caching. This happens right after the local avatar is enabled and
/// <see cref="UsePrecaching" /> is set.
/// </summary>
public bool IsPrecaching => _precacheCoroutine != null;
/// <summary>
/// Gets whether the local avatar is being teleported, including in/out smooth transitions.
/// </summary>
public bool IsTeleportingLocalAvatar => _teleportCoroutine != null;
/// <summary>
/// Gets whether the manager is currently inside a StateSync call executed using <see cref="ExecuteStateSyncEvent" />.
/// </summary>
public bool IsInsideStateSync { get; private set; }
// Properties
/// <summary>
/// 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 <see cref="UxrPostUpdateMode.LateUpdate" /> by default to make sure they are played on top of any animation
/// generated by Unity built-in animation components like <see cref="Animator" />.
/// </summary>
public UxrPostUpdateMode PostUpdateMode
{
get => _postUpdateMode;
set => _postUpdateMode = value;
}
/// <summary>
/// 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 <see cref="IUxrPrecacheable" /> 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.
/// </summary>
public bool UsePrecaching
{
get => _usePrecaching;
set => _usePrecaching = value;
}
/// <summary>
/// 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.
/// </summary>
public int PrecacheFrameCount
{
get => _precacheFrameCount;
set => _precacheFrameCount = value;
}
/// <summary>
/// Gets or sets the color used when teleporting using screen fading transitions.
/// </summary>
public Color TeleportFadeColor { get; set; }
#endregion
#region Public Methods
/// <summary>
/// Given a component that requires an <see cref="UxrAvatar" /> component in the hierarchy in order to work, logs an
/// error indicating that it's missing.
/// </summary>
/// <param name="component">Component that requires an <see cref="UxrAvatar" /> on its GameObject or any of its parents.</param>
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()}.");
}
}
/// <summary>
/// Given a component that requires an <see cref="UxrAvatar" /> component in the scene in order to work, logs an error
/// indicating that it's missing.
/// </summary>
/// <param name="component">Component that requires an <see cref="UxrAvatar" /> in the scene.</param>
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()}.");
}
}
/// <summary>
/// Saves the state of all components. This can be used in multiplayer, save-to-file or replay functionality.
/// Components are saved using <see cref="IUxrStateSave.SerializationOrder" /> to control the order in which the
/// components will be serialized. The serialization order will determine the deserialization order used by
/// <see cref="LoadStateChanges" />.
/// </summary>
/// <param name="roots">A list of GameObjects whose hierarchy to serialize or null to serialize the whole scene</param>
/// <param name="ignoreRoots">A list of GameObjects whose hierarchy to ignore, or null to not ignore anything</param>
/// <param name="level">The level of changes to serialize</param>
/// <param name="format">The serialization output format</param>
/// <returns>A data stream that can be saved and loaded back using <see cref="LoadStateChanges" /></returns>
public byte[] SaveStateChanges(List<GameObject> roots, List<GameObject> 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<IUxrStateSave> components;
if (roots == null)
{
components = level == UxrStateSaveLevel.Complete ? UxrStateSaveImplementer.AllSerializableComponents : UxrStateSaveImplementer.SaveRequiredComponents;
}
else
{
if (level == UxrStateSaveLevel.Complete)
{
components = roots.SelectMany(go => go.GetComponentsInChildren<IUxrStateSave>(true));
}
else
{
components = roots.SelectMany(go => go.GetComponentsInChildren<IUxrStateSave>(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;
}
/// <summary>
/// Loads the state of component changes from serialized data using <see cref="SaveStateChanges" />.
/// </summary>
/// <param name="serializedState">Serialized state</param>
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<UxrAvatar> loadedAvatars = new List<UxrAvatar>();
// 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}");
}
}
/// <summary>
/// Executes a state change, serialized using <see cref="UxrSyncEventArgs.SerializeEventBinary" />, so that it is
/// processed by the same component.
/// </summary>
/// <param name="serializedEvent">Event serialized using <see cref="UxrSyncEventArgs.SerializeEventBinary" /></param>
/// <returns>
/// 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, <see cref="UxrStateSyncResult.IsError" /> will return true and
/// <see cref="UxrStateSyncResult.ErrorMessage" /> will get the error message.
/// </returns>
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;
}
/// <summary>
/// Translates an avatar.
/// </summary>
/// <param name="avatar">The avatar to translate</param>
/// <param name="translation">Translation offset</param>
/// <param name="propagateEvents">
/// Whether to propagate <see cref="UxrAvatar.GlobalAvatarMoving" />/
/// <see cref="UxrAvatar.GlobalAvatarMoved" /> events
/// </param>
public void TranslateAvatar(UxrAvatar avatar, Vector3 translation, bool propagateEvents = true)
{
MoveAvatarTo(avatar, avatar.CameraFloorPosition + translation, avatar.ProjectedCameraForward, propagateEvents);
}
/// <summary>
/// Moves an avatar to a new position on the floor, keeping the same viewing direction. The eye level is maintained.
/// </summary>
/// <param name="avatar">The avatar to move</param>
/// <param name="newFloorPosition">
/// 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.
/// </param>
/// <param name="propagateEvents">
/// Whether to propagate <see cref="UxrAvatar.GlobalAvatarMoving" />/
/// <see cref="UxrAvatar.GlobalAvatarMoved" /> events
/// </param>
public void MoveAvatarTo(UxrAvatar avatar, Vector3 newFloorPosition, bool propagateEvents = true)
{
MoveAvatarTo(avatar, newFloorPosition, avatar.ProjectedCameraForward, propagateEvents);
}
/// <summary>
/// Moves an avatar to a new position on the floor and a viewing direction. The eye level is maintained.
/// </summary>
/// <param name="avatar">The avatar to move</param>
/// <param name="newFloorPosition">
/// 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.
/// </param>
/// <param name="newForward">The new viewing direction of the avatar, including the camera.</param>
/// <param name="propagateEvents">
/// Whether to propagate <see cref="UxrAvatar.GlobalAvatarMoving" />/
/// <see cref="UxrAvatar.GlobalAvatarMoved" /> events
/// </param>
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);
}
/// <summary>
/// See <see cref="MoveAvatarTo(UxrAvatar,UnityEngine.Vector3,UnityEngine.Vector3)">MoveAvatarTo</see>.
/// </summary>
/// <param name="avatar">The avatar to move</param>
/// <param name="destination">The position and orientation on the floor</param>
/// <param name="propagateEvents">
/// Whether to propagate <see cref="UxrAvatar.GlobalAvatarMoving" />/
/// <see cref="UxrAvatar.GlobalAvatarMoved" /> events
/// </param>
public void MoveAvatarTo(UxrAvatar avatar, Transform destination, bool propagateEvents = true)
{
if (avatar && destination)
{
MoveAvatarTo(avatar, destination.position, destination.forward, propagateEvents);
}
}
/// <summary>
/// Moves the avatar to a new floor level.
/// </summary>
/// <param name="avatar">The avatar to move</param>
/// <param name="floorLevel">The new floor level (Y)</param>
/// <param name="propagateEvents">
/// Whether to propagate <see cref="UxrAvatar.GlobalAvatarMoving" />/
/// <see cref="UxrAvatar.GlobalAvatarMoved" /> events
/// </param>
public void MoveAvatarTo(UxrAvatar avatar, float floorLevel, bool propagateEvents = true)
{
if (avatar)
{
Vector3 newPosition = avatar.CameraFloorPosition;
newPosition.y = floorLevel;
MoveAvatarTo(avatar, newPosition, propagateEvents);
}
}
/// <summary>
/// Rotates the avatar around its vertical axis, where a positive angle turns it to the right and a negative angle to
/// the left.
/// </summary>
/// <param name="avatar">The avatar to rotate</param>
/// <param name="degrees">The degrees to rotate</param>
/// <param name="propagateEvents">
/// Whether to propagate <see cref="UxrAvatar.GlobalAvatarMoving" />/
/// <see cref="UxrAvatar.GlobalAvatarMoved" /> events
/// </param>
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);
}
/// <summary>
/// Teleports the local <see cref="UxrAvatar" />. 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.
/// </summary>
/// <param name="newFloorPosition">
/// 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.
/// </param>
/// <param name="newRotation">
/// World-space rotation the avatar will be teleported to. The camera will point in the rotation's forward direction.
/// </param>
/// <param name="translationType">The type of translation to use. By default it will teleport immediately</param>
/// <param name="transitionSeconds">
/// If <paramref name="translationType" /> has a duration, it will specify how long the
/// teleport transition will take in seconds. By default it is <see cref="UxrConstants.TeleportTranslationSeconds" />
/// </param>
/// <param name="teleportedCallback">
/// Optional callback executed depending on the teleportation mode:
/// <list type="bullet">
/// <item><see cref="UxrTranslationType.Immediate" />: Right after finishing the teleportation.</item>
/// <item>
/// <see cref="UxrTranslationType.Fade" />: 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.
/// </item>
/// <item><see cref="UxrTranslationType.Smooth" />: Right after finishing the teleportation.</item>
/// </list>
/// </param>
/// <param name="finishedCallback">
/// 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.
/// </param>
/// <param name="propagateEvents">
/// Whether to propagate <see cref="UxrAvatar.GlobalAvatarMoving" />/
/// <see cref="UxrAvatar.GlobalAvatarMoved" /> events
/// </param>
/// <returns>Coroutine enumerator</returns>
/// <remarks>
/// If <see cref="UxrTranslationType.Fade" /> translation mode was specified, the default black fade color can be
/// changed using <see cref="TeleportFadeColor" />.
/// </remarks>
public void TeleportLocalAvatar(Vector3 newFloorPosition,
Quaternion newRotation,
UxrTranslationType translationType = UxrTranslationType.Immediate,
float transitionSeconds = UxrConstants.TeleportTranslationSeconds,
Action teleportedCallback = null,
Action<bool> 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);
}
/// <summary>
/// Teleports the local <see cref="UxrAvatar" /> while making sure to keep relative position/orientation on moving
/// objects. Some <paramref name="translationType" /> 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:
/// <list type="bullet">
/// <item>Relative position/orientation to the destination is preserved.</item>
/// <item>Optionally the local avatar can be parented to the new destination.</item>
/// </list>
/// 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.
/// </summary>
/// <param name="referenceTransform">
/// The object the avatar should keep relative position/orientation to. This should be the moving object the avatar has
/// teleported on top of
/// </param>
/// <param name="parentToReference">
/// Whether to parent the avatar to <paramref name="referenceTransform" />. The avatar should be parented if it's being
/// teleported to a moving hierarchy it is not part of
/// </param>
/// <param name="newFloorPosition">
/// 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.
/// </param>
/// <param name="newRotation">
/// World-space rotation the avatar will be teleported to. The camera will point in the rotation's forward direction.
/// </param>
/// <param name="translationType">The type of translation to use. By default it will teleport immediately</param>
/// <param name="transitionSeconds">
/// If <paramref name="translationType" /> has a duration, it will specify how long the
/// teleport transition will take in seconds. By default it is <see cref="UxrConstants.TeleportTranslationSeconds" />
/// </param>
/// <param name="teleportedCallback">
/// Optional callback executed depending on the teleportation mode:
/// <list type="bullet">
/// <item><see cref="UxrTranslationType.Immediate" />: Right after finishing the teleportation.</item>
/// <item>
/// <see cref="UxrTranslationType.Fade" />: 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.
/// </item>
/// <item><see cref="UxrTranslationType.Smooth" />: Right after finishing the teleportation.</item>
/// </list>
/// </param>
/// <param name="finishedCallback">
/// 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.
/// </param>
/// <param name="propagateEvents">
/// Whether to propagate <see cref="UxrAvatar.GlobalAvatarMoving" />/
/// <see cref="UxrAvatar.GlobalAvatarMoved" /> events
/// </param>
/// <returns>Coroutine enumerator</returns>
/// <remarks>
/// If <see cref="UxrTranslationType.Fade" /> translation mode was specified, the default black fade color can be
/// changed using <see cref="TeleportFadeColor" />.
/// </remarks>
public void TeleportLocalAvatarRelative(Transform referenceTransform,
bool parentToReference,
Vector3 newFloorPosition,
Quaternion newRotation,
UxrTranslationType translationType = UxrTranslationType.Immediate,
float transitionSeconds = UxrConstants.TeleportTranslationSeconds,
Action teleportedCallback = null,
Action<bool> 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);
}
/// <summary>
/// <para>
/// Asynchronous version of <see cref="TeleportLocalAvatar"> TeleportLocalAvatar</see>.
/// </para>
/// Teleports the local <see cref="UxrAvatar" />. 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.
/// </summary>
/// <param name="newFloorPosition">
/// 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.
/// </param>
/// <param name="newRotation">
/// World-space rotation the avatar will be teleported to. The camera will point in the rotation's forward direction.
/// </param>
/// <param name="translationType">The type of translation to use. By default it will teleport immediately</param>
/// <param name="transitionSeconds">
/// If <paramref name="translationType" /> has a duration, it will specify how long the
/// teleport transition will take in seconds. By default it is <see cref="UxrConstants.TeleportTranslationSeconds" />
/// </param>
/// <param name="teleportedCallback">
/// Optional callback executed depending on the teleportation mode:
/// <list type="bullet">
/// <item><see cref="UxrTranslationType.Immediate" />: Right after finishing the teleportation.</item>
/// <item>
/// <see cref="UxrTranslationType.Fade" />: 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.
/// </item>
/// <item><see cref="UxrTranslationType.Smooth" />: Right after finishing the teleportation.</item>
/// </list>
/// </param>
/// <param name="ct">Optional cancellation token that can be used to cancel the task</param>
/// <param name="propagateEvents">
/// Whether to propagate <see cref="UxrAvatar.GlobalAvatarMoving" />/
/// <see cref="UxrAvatar.GlobalAvatarMoved" /> events
/// </param>
/// <returns>Awaitable <see cref="Task" /> that will finish after the avatar was teleported or if it was cancelled</returns>
/// <exception cref="TaskCanceledException">Task was canceled using <paramref name="ct" /></exception>
/// <remarks>
/// If <see cref="UxrTranslationType.Fade" /> translation mode was specified, the default black fade color can be
/// changed using <see cref="TeleportFadeColor" />.
/// </remarks>
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;
}
}
/// <summary>
/// <para>
/// Asynchronous version of <see cref="TeleportLocalAvatarRelative"> TeleportLocalAvatar</see>.
/// </para>
/// Teleports the local <see cref="UxrAvatar" />. 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.
/// </summary>
/// <param name="referenceTransform">
/// The object the avatar should keep relative position/orientation to. This should be the moving object the avatar has
/// teleported on top of
/// </param>
/// <param name="parentToReference">
/// Whether to parent the avatar to <paramref name="referenceTransform" />. The avatar should be parented if it's being
/// teleported to a moving hierarchy it is not part of
/// </param>
/// <param name="newFloorPosition">
/// 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.
/// </param>
/// <param name="newRotation">
/// World-space rotation the avatar will be teleported to. The camera will point in the rotation's forward direction.
/// </param>
/// <param name="translationType">The type of translation to use. By default it will teleport immediately</param>
/// <param name="transitionSeconds">
/// If <paramref name="translationType" /> has a duration, it will specify how long the
/// teleport transition will take in seconds. By default it is <see cref="UxrConstants.TeleportTranslationSeconds" />
/// </param>
/// <param name="teleportedCallback">
/// Optional callback executed depending on the teleportation mode:
/// <list type="bullet">
/// <item><see cref="UxrTranslationType.Immediate" />: Right after finishing the teleportation.</item>
/// <item>
/// <see cref="UxrTranslationType.Fade" />: 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.
/// </item>
/// <item><see cref="UxrTranslationType.Smooth" />: Right after finishing the teleportation.</item>
/// </list>
/// </param>
/// <param name="ct">Optional cancellation token that can be used to cancel the task</param>
/// <param name="propagateEvents">
/// Whether to propagate <see cref="UxrAvatar.GlobalAvatarMoving" />/
/// <see cref="UxrAvatar.GlobalAvatarMoved" /> events
/// </param>
/// <returns>Awaitable <see cref="Task" /> that will finish after the avatar was teleported or if it was cancelled</returns>
/// <exception cref="TaskCanceledException">Task was canceled using <paramref name="ct" /></exception>
/// <remarks>
/// If <see cref="UxrTranslationType.Fade" /> translation mode was specified, the default black fade color can be
/// changed using <see cref="TeleportFadeColor" />.
/// </remarks>
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;
}
}
/// <summary>
/// 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 <paramref name="rotationType" />.
/// </summary>
/// <param name="degrees">The degrees to rotate</param>
/// <param name="rotationType">The type of rotation to use. By default it will rotate immediately</param>
/// <param name="transitionSeconds">
/// If <paramref name="rotationType" /> has a duration, it will specify how long the
/// rotation transition will take in seconds. By default it is <see cref="UxrConstants.TeleportRotationSeconds" />
/// </param>
/// <param name="rotatedCallback">
/// Optional callback executed depending on the rotation mode:
/// <list type="bullet">
/// <item><see cref="UxrRotationType.Immediate" />: Right after finishing the rotation.</item>
/// <item>
/// <see cref="UxrRotationType.Fade" />: 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.
/// </item>
/// <item><see cref="UxrRotationType.Smooth" />: Right after finishing the rotation.</item>
/// </list>
/// </param>
/// <param name="finishedCallback">
/// 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.
/// </param>
/// <param name="propagateEvents">
/// Whether to propagate <see cref="UxrAvatar.GlobalAvatarMoving" />/
/// <see cref="UxrAvatar.GlobalAvatarMoved" /> events
/// </param>
/// <remarks>
/// If <see cref="UxrTranslationType.Fade" /> translation mode was specified, the default black fade color can be
/// changed using <see cref="TeleportFadeColor" />.
/// </remarks>
public void RotateLocalAvatar(float degrees,
UxrRotationType rotationType = UxrRotationType.Immediate,
float transitionSeconds = UxrConstants.TeleportRotationSeconds,
Action rotatedCallback = null,
Action<bool> 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);
}
/// <summary>
/// <para>Asynchronous version of <see cref="RotateLocalAvatar" />.</para>
/// <para>
/// 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
/// <paramref name="rotationType" />.
/// </para>
/// </summary>
/// <param name="degrees">The degrees to rotate</param>
/// <param name="rotationType">The type of rotation to use. By default it will rotate immediately</param>
/// <param name="transitionSeconds">
/// If <paramref name="rotationType" /> has a duration, it will specify how long the
/// rotation transition will take in seconds. By default it is <see cref="UxrConstants.TeleportRotationSeconds" />
/// </param>
/// <param name="rotatedCallback">
/// Optional callback executed depending on the rotation mode:
/// <list type="bullet">
/// <item><see cref="UxrRotationType.Immediate" />: Right after finishing the rotation.</item>
/// <item>
/// <see cref="UxrRotationType.Fade" />: 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.
/// </item>
/// <item><see cref="UxrRotationType.Smooth" />: Right after finishing the rotation.</item>
/// </list>
/// </param>
/// <param name="ct">Optional cancellation token to cancel the operation</param>
/// <param name="propagateEvents">
/// Whether to propagate <see cref="UxrAvatar.GlobalAvatarMoving" />/
/// <see cref="UxrAvatar.GlobalAvatarMoved" /> events
/// </param>
/// <returns>Awaitable <see cref="Task" /> that will finish when the rotation finished</returns>
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
/// <summary>
/// Registers a new component with the <see cref="IUxrStateSync" /> interface.
/// </summary>
/// <param name="component">Custom component</param>
internal void RegisterStateSyncComponent<T>(Component component) where T : IUxrStateSync
{
StateSync_Registered(component as IUxrStateSync);
}
/// <summary>
/// Removes a component with the <see cref="IUxrStateSync" /> interface, because it was destroyed.
/// </summary>
/// <param name="component">Custom component</param>
internal void UnregisterStateSyncComponent<T>(Component component) where T : IUxrStateSync
{
StateSync_Unregistered(component as IUxrStateSync);
}
#endregion
#region Unity
/// <summary>
/// Subscribes to global events.
/// </summary>
protected override void Awake()
{
base.Awake();
UxrAvatar.GlobalEnabled += Avatar_Enabled;
SceneManager.sceneLoaded += SceneManager_SceneLoaded;
SceneManager.sceneUnloaded += SceneManager_SceneUnloaded;
}
/// <summary>
/// Unsubscribes from global events.
/// </summary>
protected override void OnDestroy()
{
base.OnDestroy();
UxrAvatar.GlobalEnabled -= Avatar_Enabled;
SceneManager.sceneLoaded -= SceneManager_SceneLoaded;
SceneManager.sceneUnloaded -= SceneManager_SceneUnloaded;
DestroyPrecachedInstances();
}
/// <summary>
/// Tries to find Unity canvases (<see cref="Canvas" /> components) and automatically set them up so that they can be
/// used by the framework using <see cref="UxrCanvas" />.
/// </summary>
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();
}
/// <summary>
/// Updates the key entities to the current frame. If the <see cref="PostUpdateMode" /> is set to
/// <see cref="UxrPostUpdateMode.Update" />, all the animation (hand poses, manipulation mechanics and Inverse
/// Kinematics) will also be updated.
/// </summary>
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();
}
}
/// <summary>
/// Updates the key entities to the current frame. If the <see cref="PostUpdateMode" /> is set to
/// <see cref="UxrPostUpdateMode.LateUpdate" />, all the animation (hand poses, manipulation mechanics and Inverse
/// Kinematics) will also be updated.
/// </summary>
private void LateUpdate()
{
if (PostUpdateMode == UxrPostUpdateMode.LateUpdate)
{
PostUpdate();
}
UxrStateSaveImplementer.NotifyEndOfFrame();
}
#endregion
#region Coroutines
/// <summary>
/// Public teleporting coroutine that can be yielded from an external coroutine.
/// Teleports the local <see cref="UxrAvatar" />. 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.
/// </summary>
/// <param name="newFloorPosition">
/// 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.
/// </param>
/// <param name="newRotation">
/// Rotation the avatar will be teleported to. The camera will point in the rotation's forward
/// direction
/// </param>
/// <param name="translationType">The type of translation to use. By default it will teleport immediately</param>
/// <param name="transitionSeconds">
/// If <paramref name="translationType" /> has a duration, it will specify how long the
/// teleport transition will take in seconds. By default it is <see cref="UxrConstants.TeleportTranslationSeconds" />
/// </param>
/// <param name="teleportedCallback">
/// Optional callback executed depending on the teleportation mode:
/// <list type="bullet">
/// <item><see cref="UxrTranslationType.Immediate" />: Right after finishing the teleportation.</item>
/// <item>
/// <see cref="UxrTranslationType.Fade" />: 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.
/// </item>
/// <item><see cref="UxrTranslationType.Smooth" />: Right after finishing the teleportation.</item>
/// </list>
/// </param>
/// <param name="finishedCallback">
/// 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.
/// </param>
/// <param name="propagateEvents">
/// Whether to propagate <see cref="UxrAvatar.GlobalAvatarMoving" />/
/// <see cref="UxrAvatar.GlobalAvatarMoved" /> events
/// </param>
/// <returns>Coroutine enumerator</returns>
/// <remarks>
/// If <see cref="UxrTranslationType.Fade" /> translation mode was specified, the default black fade color can be
/// changed using <see cref="TeleportFadeColor" />.
/// </remarks>
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);
}
/// <summary>
/// Public teleporting coroutine that can be yielded from an external coroutine.
/// Teleports the local <see cref="UxrAvatar" /> while making sure to keep relative position/orientation on moving
/// objects. Some <paramref name="translationType" /> 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:
/// <list type="bullet">
/// <item>Relative position/orientation to the destination is preserved.</item>
/// <item>Optionally the local avatar can be parented to the new destination.</item>
/// </list>
/// 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.
/// </summary>
/// <param name="referenceTransform">
/// 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, <paramref name="newRelativeFloorPosition" /> and
/// <paramref name="newRelativeRotation" /> will be interpreted as world coordinates.
/// </param>
/// <param name="parentToReference">
/// Whether to parent the avatar to <paramref name="referenceTransform" />. The avatar should be parented if it's being
/// teleported to a moving hierarchy it is not part of
/// </param>
/// <param name="newRelativeFloorPosition">
/// New floor-level position the avatar will be teleported over in <paramref name="referenceTransform" /> local
/// coordinates. If <paramref name="referenceTransform" /> 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.
/// </param>
/// <param name="newRelativeRotation">
/// Local rotation the avatar will be teleported to with respect to <see cref="referenceTransform" />. If
/// <paramref name="referenceTransform" /> is null, rotation will be in world-space. The camera will point in the
/// rotation's forward direction.
/// </param>
/// <param name="translationType">The type of translation to use. By default it will teleport immediately</param>
/// <param name="transitionSeconds">
/// If <paramref name="translationType" /> has a duration, it will specify how long the
/// teleport transition will take in seconds. By default it is <see cref="UxrConstants.TeleportTranslationSeconds" />
/// </param>
/// <param name="teleportedCallback">
/// Optional callback executed depending on the teleportation mode:
/// <list type="bullet">
/// <item><see cref="UxrTranslationType.Immediate" />: Right after finishing the teleportation.</item>
/// <item>
/// <see cref="UxrTranslationType.Fade" />: 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.
/// </item>
/// <item><see cref="UxrTranslationType.Smooth" />: Right after finishing the teleportation.</item>
/// </list>
/// </param>
/// <param name="finishedCallback">
/// 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.
/// </param>
/// <param name="propagateEvents">
/// Whether to propagate <see cref="UxrAvatar.GlobalAvatarMoving" />/
/// <see cref="UxrAvatar.GlobalAvatarMoved" /> events
/// </param>
/// <returns>Coroutine enumerator</returns>
/// <remarks>
/// If <see cref="UxrTranslationType.Fade" /> translation mode was specified, the default black fade color can be
/// changed using <see cref="TeleportFadeColor" />.
/// </remarks>
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();
}
/// <summary>
/// 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.
/// </summary>
/// <param name="degrees">The degrees to rotate</param>
/// <param name="rotationType">The type of rotation to use. By default it will rotate immediately</param>
/// <param name="transitionSeconds">
/// If <paramref name="rotationType" /> has a duration, it will specify how long the
/// rotation transition will take in seconds. By default it is <see cref="UxrConstants.TeleportRotationSeconds" />
/// </param>
/// <param name="rotatedCallback">
/// Optional callback executed depending on the rotation mode:
/// <list type="bullet">
/// <item><see cref="UxrRotationType.Immediate" />: Right after finishing the rotation.</item>
/// <item>
/// <see cref="UxrRotationType.Fade" />: 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.
/// </item>
/// <item><see cref="UxrRotationType.Smooth" />: Right after finishing the rotation.</item>
/// </list>
/// </param>
/// <param name="finishedCallback">
/// 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.
/// </param>
/// <param name="propagateEvents">
/// Whether to propagate <see cref="UxrAvatar.GlobalAvatarMoving" />/
/// <see cref="UxrAvatar.GlobalAvatarMoved" /> events
/// </param>
/// <returns>Coroutine enumerator</returns>
/// <remarks>
/// If <see cref="UxrRotationType.Fade" /> translation mode was specified, the default black fade color can be changed
/// using <see cref="TeleportFadeColor" />.
/// </remarks>
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();
}
/// <summary>
/// <para>
/// Precaching coroutine. It will try to find all <see cref="IUxrPrecacheable" /> 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 (<see cref="PrecacheFrameCount" />)
/// after which the pre-instantiated objects will be destroyed and the scene will be faded in.
/// </para>
/// <para>
/// Another preprocessing that takes place is finding initially disabled <see cref="UxrComponent" /> components and
/// force registering their Unique IDs to overcome the limitation of initially disabled components not being able
/// to receive state synchronization messages.
/// </para>
/// </summary>
/// <param name="onStarting">Optional callback called when precaching is right about to start</param>
/// <param name="onFinished">Optional callback called right after precaching finished</param>
/// <returns>Coroutine enumerator</returns>
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<int, GameObject>();
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
/// <summary>
/// Called when any component with the <see cref="IUxrStateSync" /> interface has a state change.
/// </summary>
/// <param name="sender">Sender (component implementing <see cref="IUxrStateSync" />)</param>
/// <param name="eventArgs">Event parameters</param>
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);
}
}
/// <summary>
/// Called when an <see cref="UxrAvatar" /> 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.
/// </summary>
/// <param name="avatar">Avatar that was enabled</param>
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();
}
}
}
/// <summary>
/// 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.
/// </summary>
/// <param name="scene">Scene that was loaded.</param>
/// <param name="loadSceneMode">The mode used to load the scene.</param>
private void SceneManager_SceneLoaded(Scene scene, LoadSceneMode loadSceneMode)
{
SetupCanvases();
}
/// <summary>
/// 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.
/// </summary>
/// <param name="scene">Scene that was unloaded.</param>
private void SceneManager_SceneUnloaded(Scene scene)
{
SetupCanvases();
}
/// <summary>
/// Called when a component implementing <see cref="IUxrStateSync" /> is being enabled. We use it to subscribe to the
/// <see cref="UxrComponent.StateChanged" /> event to keep track of any state changes in components in UltimateXR.
/// </summary>
/// <param name="component">Component that was enabled</param>
private void StateSync_Registered(IUxrStateSync component)
{
component.StateChanged += StateSync_StateChanged;
}
/// <summary>
/// Called when a component implementing <see cref="IUxrStateSync" /> is being disabled. We use it to unsubscribe from
/// the <see cref="UxrComponent.StateChanged" /> event.
/// </summary>
/// <param name="component">Component that was disabled</param>
private void StateSync_Unregistered(IUxrStateSync component)
{
component.StateChanged -= StateSync_StateChanged;
}
#endregion
#region Event Trigger Methods
/// <summary>
/// Event trigger for the <see cref="ComponentStateChanged" /> event.
/// </summary>
/// <param name="component">Component with the state change</param>
/// <param name="eventArgs">Event parameters</param>
private void OnComponentStateChanged(IUxrStateSync component, UxrSyncEventArgs eventArgs)
{
if (UxrStateSyncImplementer.SyncCallDepth == 1 || !UseTopLevelStateChangesOnly || eventArgs.Options.HasFlag(UxrStateSyncOptions.IgnoreNestingCheck))
{
ComponentStateChanged?.Invoke(component, eventArgs);
}
}
/// <summary>
/// <see cref="PrecachingStarting" /> event trigger.
/// </summary>
private void OnPrecachingStarting()
{
PrecachingStarting?.Invoke();
}
/// <summary>
/// <see cref="PrecachingFinished" /> event trigger.
/// </summary>
private void OnPrecachingFinished()
{
PrecachingFinished?.Invoke();
}
/// <summary>
/// <see cref="UxrAvatar.GlobalAvatarMoving" /> event trigger.
/// </summary>
/// <param name="avatar">The avatar that is about to move</param>
/// <param name="args">Event parameters</param>
/// <param name="propagateEvents">Whether to propagate <see cref="UxrAvatar.GlobalAvatarMoving" /> events</param>
private void OnAvatarMoving(UxrAvatar avatar, UxrAvatarMoveEventArgs args, bool propagateEvents = true)
{
if (propagateEvents)
{
avatar.RaiseAvatarMoving(args);
}
}
/// <summary>
/// <see cref="UxrAvatar.GlobalAvatarMoved" /> event trigger.
/// </summary>
/// <param name="avatar">The avatar that moved</param>
/// <param name="args">Event parameters</param>
/// <param name="propagateEvents">Whether to propagate <see cref="UxrAvatar.GlobalAvatarMoved" /> events</param>
private void OnAvatarMoved(UxrAvatar avatar, UxrAvatarMoveEventArgs args, bool propagateEvents = true)
{
if (propagateEvents)
{
avatar.RaiseAvatarMoved(args);
}
}
/// <summary>
/// <see cref="AvatarsUpdating" /> event trigger.
/// </summary>
private void OnUpdating()
{
AvatarsUpdating?.Invoke();
}
/// <summary>
/// <see cref="AvatarsUpdated" /> event trigger.
/// </summary>
private void OnUpdated()
{
AvatarsUpdated?.Invoke();
}
/// <summary>
/// <see cref="StageUpdating" /> event trigger.
/// </summary>
private void OnUpdatingStage(UxrUpdateStage stage)
{
StageUpdating?.Invoke(stage);
}
/// <summary>
/// <see cref="StageUpdated" /> event trigger.
/// </summary>
private void OnStageUpdated(UxrUpdateStage stage)
{
StageUpdated?.Invoke(stage);
}
/// <summary>
/// <see cref="UxrAvatar.AvatarUpdating">UxrAvatar.AvatarUpdating</see> event trigger.
/// </summary>
private void OnAvatarUpdating(UxrAvatar avatar, UxrAvatarUpdateEventArgs e)
{
avatar.RaiseAvatarUpdating(e);
}
/// <summary>
/// <see cref="UxrAvatar.AvatarUpdated">UxrAvatar.AvatarUpdated</see> event trigger.
/// </summary>
private void OnAvatarUpdated(UxrAvatar avatar, UxrAvatarUpdateEventArgs e)
{
avatar.RaiseAvatarUpdated(e);
}
#endregion
#region Private Methods
/// <summary>
/// Performs the post-update: Updates the animation and interaction of all key entities, while sending all related
/// events during the process.
/// Main updates are:
/// <list type="bullet">
/// <item>Avatar animation.</item>
/// <item>Manipulation mechanics and constraints.</item>
/// <item>Other managers in the framework such as the <see cref="UxrWeaponManager" />.</item>
/// <item>Inverse kinematics.</item>
/// </list>
/// </summary>
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();
}
/// <summary>
/// Processes all <see cref="IUxrPrecacheable" /> 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.<br />
/// It also registers initially disabled UltimateXR components so that their Unique ID is known and can receive sync
/// messages too.
/// </summary>
/// <param name="dynamicInstances">List of loaded instances.</param>
/// <param name="scene">Scene to get the components from.</param>
/// <param name="avatar">Current avatar.</param>
private void AddScenePrecachedInstances(Dictionary<int, GameObject> dynamicInstances, Scene scene, UxrAvatar avatar)
{
for (int rootIndex = 0; rootIndex < scene.rootCount; ++rootIndex)
{
MonoBehaviour[] behaviours = scene.GetRootGameObjects()[rootIndex].GetComponentsInChildren<MonoBehaviour>(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<AudioSource>(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();
}
}
}
}
/// <summary>
/// Destroys the currently loaded pre-cached instances.
/// </summary>
private void DestroyPrecachedInstances()
{
if (_dynamicInstances != null)
{
foreach (KeyValuePair<int, GameObject> dynamicInstancePair in _dynamicInstances)
{
if (dynamicInstancePair.Value != null)
{
Destroy(dynamicInstancePair.Value);
}
}
_dynamicInstances.Clear();
}
}
/// <summary>
/// Starts the pre-caching process. If a pre-caching process is currently running, it will be stopped before starting
/// again.
/// </summary>
private void TryPrecaching()
{
if (_precacheCoroutine != null)
{
StopCoroutine(_precacheCoroutine);
}
if (UsePrecaching)
{
_precacheCoroutine = StartCoroutine(PrecacheCoroutine(OnPrecachingStarting, OnPrecachingFinished));
}
}
/// <summary>
/// Tries to set up all <see cref="Canvas" /> components currently in the scene so that they can work with UltimateXR
/// through the <see cref="UxrCanvas" /> component.
/// </summary>
private void SetupCanvases()
{
if (UxrPointerInputModule.Instance)
{
foreach (Canvas canvas in ComponentExt.GetAllComponentsInOpenScenes<Canvas>(true))
{
if (canvas.renderMode == RenderMode.WorldSpace && canvas.GetComponent<UxrIgnoreCanvas>() == null)
{
if (!canvas.TryGetComponent<UxrCanvas>(out var canvasXR))
{
if (UxrPointerInputModule.Instance.AutoEnableOnWorldCanvases)
{
canvasXR = canvas.gameObject.AddComponent<UxrCanvas>();
canvasXR.SetupCanvas(UxrPointerInputModule.Instance);
}
}
if (canvasXR && UxrPointerInputModule.Instance.AutoAssignEventCamera && UxrAvatar.LocalAvatar)
{
canvas.worldCamera = UxrAvatar.LocalAvatarCamera;
}
}
}
}
}
#endregion
#region Private Types & Data
/// <summary>
/// Gets the enabled <see cref="UxrAvatarController" /> components that belong to enabled <see cref="UxrAvatar" />
/// components whose <see cref="UxrAvatar.AvatarMode" /> is <see cref="UxrAvatarMode.Local" />. This should be either
/// none or one. The property allows to iterate over avatar controllers that require updating each frame.
/// </summary>
private IEnumerable<UxrAvatarController> LocalAvatarControllers
{
get
{
foreach (UxrAvatar avatar in UxrAvatar.EnabledComponents)
{
if (avatar.AvatarMode == UxrAvatarMode.Local && avatar.AvatarController != null && avatar.AvatarController.enabled && avatar.AvatarController.Initialized)
{
yield return avatar.AvatarController;
}
}
}
}
/// <summary>
/// Gets the enabled <see cref="UxrAvatarController" /> components that belong to enabled <see cref="UxrAvatar" />
/// components.
/// </summary>
private IEnumerable<UxrAvatarController> EnabledAvatarControllers
{
get
{
foreach (UxrAvatar avatar in UxrAvatar.EnabledComponents)
{
if (avatar.AvatarController != null && avatar.AvatarController.enabled && avatar.AvatarController.Initialized)
{
yield return avatar.AvatarController;
}
}
}
}
private Coroutine _precacheCoroutine;
private Dictionary<int, GameObject> _dynamicInstances;
private Coroutine _teleportCoroutine;
#endregion
}
}