// -------------------------------------------------------------------------------------------------------------------- // // 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(true)) { if (canvas.renderMode == RenderMode.WorldSpace && canvas.GetComponent() == null) { if (!canvas.TryGetComponent(out var canvasXR)) { if (UxrPointerInputModule.Instance.AutoEnableOnWorldCanvases) { canvasXR = canvas.gameObject.AddComponent(); canvasXR.SetupCanvas(UxrPointerInputModule.Instance); } } if (canvasXR && UxrPointerInputModule.Instance.AutoAssignEventCamera && UxrAvatar.LocalAvatar) { canvas.worldCamera = UxrAvatar.LocalAvatarCamera; } } } } } #endregion #region Private Types & Data /// /// Gets the enabled components that belong to enabled /// components whose is . This should be either /// none or one. The property allows to iterate over avatar controllers that require updating each frame. /// private IEnumerable 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; } } } } /// /// Gets the enabled components that belong to enabled /// components. /// private IEnumerable 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 _dynamicInstances; private Coroutine _teleportCoroutine; #endregion } }