// -------------------------------------------------------------------------------------------------------------------- // // Copyright (c) VRMADA, All rights reserved. // // -------------------------------------------------------------------------------------------------------------------- using System; using System.Collections.Generic; using System.Linq; using UltimateXR.Avatar; using UltimateXR.CameraUtils; using UltimateXR.Core; using UltimateXR.Core.Caching; using UltimateXR.Devices; using UltimateXR.Extensions.Unity; using UnityEngine; namespace UltimateXR.Locomotion { /// /// Base component for teleport locomotion. /// public abstract partial class UxrTeleportLocomotionBase : UxrLocomotion, IUxrPrecacheable { #region Inspector Properties/Serialized Fields // General parameters [SerializeField] private UxrHandSide _controllerHand = UxrHandSide.Left; [SerializeField] private bool _useControllerForward = true; [SerializeField] private bool _parentToDestination; [SerializeField] private float _shakeFilter = 0.4f; [SerializeField] private UxrTranslationType _translationType = UxrTranslationType.Fade; [SerializeField] private Color _fadeTranslationColor = Color.black; [SerializeField] private float _fadeTranslationSeconds = UxrConstants.TeleportTranslationSeconds; [SerializeField] private float _smoothTranslationSeconds = UxrConstants.TeleportTranslationSeconds; [SerializeField] private bool _allowJoystickBackStep = true; [SerializeField] private float _backStepDistance = 2.0f; [SerializeField] private UxrRotationType _rotationType = UxrRotationType.Immediate; [SerializeField] private float _rotationStepDegrees = 45.0f; [SerializeField] private Color _fadeRotationColor = Color.black; [SerializeField] private float _fadeRotationSeconds = UxrConstants.TeleportRotationSeconds; [SerializeField] private float _smoothRotationSeconds = UxrConstants.TeleportRotationSeconds; [SerializeField] private UxrReorientationType _reorientationType = UxrReorientationType.AllowUserJoystickRedirect; // Target [SerializeField] private UxrTeleportTarget _target; [SerializeField] private float _targetPlacementAboveHit = 0.01f; [SerializeField] private bool _showTargetAlsoWhenInvalid; [SerializeField] private Color _validMaterialColorTargets = Color.white; [SerializeField] private Color _invalidMaterialColorTargets = Color.red; // Constraints [SerializeField] private QueryTriggerInteraction _triggerCollidersInteraction = QueryTriggerInteraction.Ignore; [SerializeField] private float _maxAllowedDistance = 20.0f; [SerializeField] private float _maxAllowedHeightDifference = 10.0f; [SerializeField] private float _maxAllowedSlopeDegrees = 30.0f; [SerializeField] private float _destinationValidationRadius = 0.25f; [SerializeField] private LayerMask _validTargetLayers = ~0; [SerializeField] private LayerMask _blockingTargetLayers = ~0; #endregion #region Public Types & Data /// /// Called when a destination validator using with /// invalidated a destination. If there is more than one /// validator, it will be raised only by the first validator that returns false during the frame. /// public event Action ValidatorInvalidated; /// /// Called when a destination validator using with /// canceled a destination. IF there is more than one /// validator, it will be raised only by the first validator that returns false. /// public event Action ValidatorCanceled; /// /// Gets the hand used to control the teleport component. /// public UxrHandSide HandSide => _controllerHand; /// /// Gets or sets the teleport translation type. /// public UxrTranslationType TranslationType { get => _translationType; set => _translationType = value; } /// /// Gets or sets the fade color when using translation teleporting. /// public Color FadeTranslationColor { get => _fadeTranslationColor; set => _fadeTranslationColor = value; } /// /// Gets or sets the transition duration in seconds for the translation type. /// public float FadeTranslationSeconds { get => _fadeTranslationSeconds; set => _fadeTranslationSeconds = value; } /// /// Gets or sets the transition duration in seconds for the translation type. /// public float SmoothTranslationSeconds { get => _smoothTranslationSeconds; set => _smoothTranslationSeconds = value; } /// /// Gets or sets whether the back-step is permitted. /// public bool AllowJoystickBackStep { get => _allowJoystickBackStep; set => _allowJoystickBackStep = value; } /// /// Gets or sets the back-step distance. /// public float BackStepDistance { get => _backStepDistance; set => _backStepDistance = value; } /// /// Gets or sets the teleport rotation type. /// public UxrRotationType RotationType { get => _rotationType; set => _rotationType = value; } /// /// Gets or sets the amount of degrees rotated around the avatar axis when the user presses the left or right joystick /// buttons. /// public float RotationStepDegrees { get => _rotationStepDegrees; set => _rotationStepDegrees = value; } /// /// Gets or sets the fade color when using translation teleporting. /// public Color FadeRotationColor { get => _fadeRotationColor; set => _fadeRotationColor = value; } /// /// Gets or sets the transition duration in seconds for the rotation type. /// public float FadeRotationSeconds { get => _fadeRotationSeconds; set => _fadeRotationSeconds = value; } /// /// Gets or sets the transition duration in seconds for the rotation type. /// public float SmoothRotationSeconds { get => _smoothRotationSeconds; set => _smoothRotationSeconds = value; } /// /// Gets or sets how the teleport target direction is set. /// public UxrReorientationType ReorientationType { get => _reorientationType; set => _reorientationType = value; } /// /// Gets or sets the target object. /// public UxrTeleportTarget Target { get => _teleportTarget; set => _teleportTarget = value; } /// /// Gets or sets the distance above the ground the target is positioned. /// public float TargetPlacementAboveHit { get => _targetPlacementAboveHit; set => _targetPlacementAboveHit = value; } /// /// Gets or sets whether the target should also be visible when the teleport destination is not valid. /// public bool ShowTargetAlsoWhenInvalid { get => _showTargetAlsoWhenInvalid; set => _showTargetAlsoWhenInvalid = value; } /// /// When is true, sets the teleport target color used when the destination is /// valid. /// public Color ValidMaterialColorTargets { get => _validMaterialColorTargets; set => _validMaterialColorTargets = value; } /// /// When is true, sets the teleport target color used when the destination is /// invalid. /// public Color InvalidMaterialColorTargets { get => _invalidMaterialColorTargets; set => _invalidMaterialColorTargets = value; } /// /// Gets or sets the behaviour for raycasts against trigger volumes. /// public QueryTriggerInteraction TriggerCollidersInteraction { get => _triggerCollidersInteraction; set => _triggerCollidersInteraction = value; } /// /// Gets or sets the maximum teleport distance. /// public float MaxAllowedDistance { get => _maxAllowedDistance; set => _maxAllowedDistance = value; } /// /// Gets or sets the maximum height difference allowed from the current position to a destination. /// public float MaxAllowedHeightDifference { get => _maxAllowedHeightDifference; set => _maxAllowedHeightDifference = value; } /// /// Gets or sets the maximum slop for a destination to be considered valid. /// public float MaxAllowedSlopeDegrees { get => _maxAllowedSlopeDegrees; set => _maxAllowedSlopeDegrees = value; } /// /// Gets or sets the radius of a cylinder used when validating if a teleport destination is valid. /// public float DestinationValidationRadius { get => _destinationValidationRadius; set => _destinationValidationRadius = value; } /// /// Gets or sets the layers over which teleportation is allowed. /// public LayerMask ValidTargetLayers { get => _validTargetLayers; set => _validTargetLayers = value; } /// /// Gets or sets the layers which should be considered when ray-casting looking for either valid or invalid /// teleportation surfaces. /// public LayerMask BlockingTargetLayers { get => _blockingTargetLayers; set => _blockingTargetLayers = value; } #endregion #region Implicit IUxrPrecacheable /// public IEnumerable PrecachedInstances { get { if (Target != null) { yield return Target.gameObject; } } } #endregion #region Public Methods /// /// Adds a destination validator, which can cancel a teleport based on custom conditions. /// /// /// The destination validator, a function that receives a and /// returns a boolean telling whether the teleport can be executed or not /// /// The validator execution mode /// The validator is null public void AddDestinationValidator(Func validator, UxrDestinationValidatorMode mode) { if (validator == null) { throw new ArgumentNullException(nameof(validator)); } _destinationValidators.Add(new Validator(validator, mode)); } /// /// Removes a destination validator added using . /// /// Validator to remove /// Whether the validator was removed, or false if the validator was not found /// the validator function is null public bool RemoveDestinationValidator(Func validator) { if (validator == null) { throw new ArgumentNullException(nameof(validator)); } for (int i = 0; i < _destinationValidators.Count; ++i) { if (_destinationValidators[i].ValidatorFunc == validator) { _destinationValidators.RemoveAt(i); return true; } } return false; } /// /// Removes all destination validators added using . /// public void RemoveAllDestinationValidators() { _destinationValidators.Clear(); } #endregion #region Unity /// /// Initializes the component. Should also be called in child classes. /// protected override void Awake() { base.Awake(); // Look for other avatar teleports _otherAvatarTeleports = new List(); UxrTeleportLocomotionBase[] allAvatarTeleports = Avatar.GetComponentsInChildren(); _otherAvatarTeleports.AddRange(allAvatarTeleports.Where(teleport => teleport != this)); // If the teleport target is a prefab, instantiate. Otherwise just reference the object in the scene if (_target.IsInPrefab()) { _teleportTarget = Instantiate(_target, Avatar.transform); } else { _teleportTarget = _target; if (_teleportTarget != null) { _teleportTarget.transform.parent = Avatar.transform; } } // Set initial state if (_teleportTarget != null) { _teleportTarget.transform.rotation = Avatar.transform.rotation; TeleportReference = null; TeleportLocalDirection = Avatar.ProjectedCameraForward; TeleportLocalPosition = Avatar.transform.position; } _layerMaskRaycast.value = BlockingTargetLayers.value | ValidTargetLayers.value; } /// /// Resets the component and subscribes to events. /// protected override void OnEnable() { base.OnEnable(); UxrAvatar.GlobalAvatarMoved += UxrAvatar_GlobalAvatarMoved; UxrManager.AvatarsUpdated += UxrManager_AvatarsUpdated; EnableTeleportObjects(false, false); _isBackStepAvailable = true; _isValidTeleport = false; IsTeleporting = false; ControllerStart = RawControllerStart; ControllerForward = RawControllerForward; } /// /// Clear some states and unsubscribes from events. /// protected override void OnDisable() { base.OnDisable(); UxrAvatar.GlobalAvatarMoved -= UxrAvatar_GlobalAvatarMoved; UxrManager.AvatarsUpdated -= UxrManager_AvatarsUpdated; NotifyTeleportSpawnCollider(null); EnableTeleportObjects(false, false); } #endregion #region Event Handling Methods /// /// Called when the avatar moved. /// /// Event sender /// Event parameters private void UxrAvatar_GlobalAvatarMoved(object sender, UxrAvatarMoveEventArgs e) { if (ReferenceEquals(sender, Avatar)) { ControllerStart = RawControllerStart; ControllerForward = RawControllerForward; } } /// /// When the avatar is in UpdateExternally mode, still smooth the transforms to support multiplayer and replays. /// private void UxrManager_AvatarsUpdated() { if (Avatar.AvatarMode != UxrAvatarMode.Local) { UpdateSmoothTransforms(); } } #endregion #region Protected Overrides UxrLocomotion /// protected override void UpdateLocomotion() { if (Avatar == null) { return; } // Check for back-step and rotations if (Avatar.ControllerInput.GetButtonsPressUp(_controllerHand, UxrInputButtons.JoystickDown)) { _isBackStepAvailable = true; } bool backStepInput = false; if (IsAllowedToTeleport) { backStepInput = _allowJoystickBackStep && CanBackStep && Avatar.ControllerInput.GetButtonsPress(_controllerHand, UxrInputButtons.JoystickDown); if (RotationType != UxrRotationType.NotAllowed) { if (Avatar.ControllerInput.GetButtonsPressDown(_controllerHand, UxrInputButtons.JoystickLeft) && CanRotate) { Rotate(-RotationStepDegrees); return; } if (Avatar.ControllerInput.GetButtonsPressDown(_controllerHand, UxrInputButtons.JoystickRight) && CanRotate) { Rotate(RotationStepDegrees); return; } } } // Back step? if (backStepInput && _isBackStepAvailable && IsAllowedToTeleport) { Vector3 newPosition = Avatar.CameraFloorPosition - Avatar.ProjectedCameraForward * _backStepDistance; if (HasBlockingRaycastHit(Avatar, newPosition + UpVector * RaycastAboveGround, -UpVector, _maxAllowedHeightDifference > 0.0f ? _maxAllowedHeightDifference : RaycastLongDistance, BlockingTargetLayers, TriggerCollidersInteraction, out RaycastHit backStepRaycast)) { if (NotifyDestinationRaycast(backStepRaycast, true, out bool _)) { _isBackStepAvailable = false; TeleportLocalDirection = TransformExt.GetLocalDirection(TeleportReference, Avatar.ProjectedCameraForward); TryTeleportUsingCurrentTarget(); return; } } } // Update smoothing of transforms that track the hands UpdateSmoothTransforms(); // Update locomotion in child classes UpdateTeleportLocomotion(); } #endregion #region Protected Methods /// /// Can be overriden in child classes to execute the additional per-frame teleport locomotion logic. /// protected virtual void UpdateTeleportLocomotion() { } /// /// Cancels the current teleport target. When overriden in child classes the base class should be called too. /// protected virtual void CancelTarget() { EnableTeleportObjects(false, false); _isValidTeleport = false; } /// /// Checks if the teleport position is valid. /// /// /// Should it check for blocking elements in a straight line from the current position to the new position? /// /// /// Teleport position. If should passed as reference because it may have slight corrections /// /// The hit normal that generated the teleport position candidate /// Returns a boolean telling if the slope at the destination is valid or not /// Boolean telling whether the new position is a valid teleport destination or not protected bool IsValidTeleport(bool checkBlockingInBetween, ref Vector3 newPosition, Vector3 hitNormal, out bool isValidSlope) { isValidSlope = true; if (!IsAllowedToTeleport) { return false; } Vector3 localNewPosition = Avatar.transform.InverseTransformPoint(newPosition); if (Mathf.Abs(localNewPosition.y) > MaxAllowedHeightDifference) { return false; } float eyeHeight = Avatar.transform.InverseTransformPoint(Avatar.CameraPosition).y; Vector3 eyePosStart = Avatar.CameraPosition; Vector3 eyePosEnd = newPosition + UpVector * eyeHeight; // Check if there is something blocking in a straight line if requested, used in a back step if (checkBlockingInBetween) { Vector3 direction = eyePosEnd - eyePosStart; if (HasBlockingRaycastHit(eyePosStart, direction.normalized, direction.magnitude, out RaycastHit _)) { // There is something blocking in between return false; } } else { // First perform a sphere test on the place where the head would be teleported to see if we can have an early negative. if (Physics.CheckSphere(eyePosEnd, HeadRadius, BlockingTargetLayers, TriggerCollidersInteraction)) { return false; } if (MaxAllowedHeightDifference > 0.0f && Vector3.Angle(hitNormal, UpVector) > MaxAllowedSlopeDegrees) { // Check if we are hitting a tall enough wall to see if we can have an early negative. This avoids the filtering // below to allow climbing up the first portion of the wall. // What we do is raycast in an inclined upwards direction to see if the wall is significantly enough above the raycast. Vector3 rayStart = newPosition + hitNormal * 0.1f; Vector3 rayEnd = newPosition + UpVector * MaxAllowedHeightDifference; if (HasBlockingRaycastHit(Avatar, rayStart, (rayEnd - rayStart).normalized, Vector3.Distance(rayStart, rayEnd) + 0.01f, BlockingTargetLayers, TriggerCollidersInteraction, out RaycastHit _)) { // We are hitting the base of a tall wall isValidSlope = false; return false; } } } // If not, we want to check also a radius around the destination pos. If a certain number of positions within // this radius are valid we consider it also a valid destination. This removes some unwanted false negatives due // to small height or slope differences on the ground. int positives = 0; int validSlopes = 0; for (int i = 0; i < DestinationValidationSubdivisions; ++i) { float offsetT = 1.0f / DestinationValidationSubdivisions * 0.5f; float radians = Mathf.PI * 2.0f * (i * (1.0f / DestinationValidationSubdivisions) + offsetT); Vector3 offset = new Vector3(Mathf.Cos(radians), 0.0f, Mathf.Sin(radians)); if (IsValidDestination(newPosition, eyePosEnd + offset.normalized * DestinationValidationRadius, out bool isValidSlopeSubdivision)) { positives++; } validSlopes += isValidSlopeSubdivision ? 1 : 0; } isValidSlope = validSlopes >= DestinationValidationPositivesNeeded; return positives >= DestinationValidationPositivesNeeded && isValidSlope; } /// /// Cancels all other current teleport targets. When overriden in child classes the base class should be called too. /// protected void CancelOtherTeleportTargets() { foreach (UxrTeleportLocomotionBase otherTeleport in _otherAvatarTeleports) { otherTeleport.CancelTarget(); } } /// /// Checks whether the given raycast hits have any that are blocking. A blocking raycast can either be a valid or /// invalid teleport destination depending on many factors. Use to check whether the /// given position is valid. /// This method filters out invalid raycasts such as against anything part of an avatar or a grabbed object. /// /// Ray origin /// Ray direction /// Raycast maximum distance /// Result blocking raycast /// Whether there is a blocking raycast returned in protected bool HasBlockingRaycastHit(Vector3 origin, Vector3 direction, float maxDistance, out RaycastHit outputHit) { return HasBlockingRaycastHit(Avatar, origin, direction, maxDistance, LayerMaskRaycast, TriggerCollidersInteraction, out outputHit); } /// /// Notifies a raycast was selected to be a potential destination. Computes whether the destination is valid. If it is, /// sets the appropriate internal state that can later be executed using . /// /// Raycast that will be processed as a potential teleport destination /// /// Should it check for blocking elements in a straight line from the current position to the new position? /// /// Will return whether the target was enabled /// Whether the destination is a valid teleport location protected bool NotifyDestinationRaycast(RaycastHit hit, bool checkBlockingInBetween, out bool isTargetEnabled) { _isValidTeleport = true; isTargetEnabled = true; UxrIgnoreTeleportDestination ignoreDestinationComponent = hit.collider.GetComponentInParent(); bool ignoreDestination = ignoreDestinationComponent != null && ignoreDestinationComponent.enabled; TeleportReference = hit.collider != null ? hit.collider.transform : null; _hitInfo = hit; // Check for UxrTeleportSpawnCollider component UxrTeleportSpawnCollider teleportSpawnCollider = hit.collider.GetComponentInParent(); NotifyTeleportSpawnCollider(teleportSpawnCollider); if (teleportSpawnCollider != null && teleportSpawnCollider.enabled && !ignoreDestination) { Transform spawnPos = teleportSpawnCollider.GetSpawnPos(Avatar, out Vector3 _); if (spawnPos != null) { TeleportReference = spawnPos; TeleportLocalPosition = TransformExt.GetLocalPosition(TeleportReference, spawnPos.position); TeleportLocalDirection = TransformExt.GetLocalDirection(TeleportReference, Vector3.ProjectOnPlane(spawnPos.forward, spawnPos.up)); _isValidTeleport = true; isTargetEnabled = true; EnableTeleportObjects(isTargetEnabled, _isValidTeleport); } } else { Vector3 teleportPos = hit.point; Vector3 teleportLocalDirection = Vector3.zero; isTargetEnabled = true; // Compute the new local avatar direction if (ReorientationType == UxrReorientationType.KeepOrientation) { teleportLocalDirection = TransformExt.GetLocalDirection(TeleportReference, Avatar.ProjectedCameraForward); } else if (ReorientationType == UxrReorientationType.UseTeleportFromToDirection) { teleportLocalDirection = TransformExt.GetLocalDirection(TeleportReference, Vector3.ProjectOnPlane(teleportPos - Avatar.CameraPosition, UpVector)); } else if (ReorientationType == UxrReorientationType.AllowUserJoystickRedirect) { Vector2 joystickValue = Avatar.ControllerInput.GetInput2D(HandSide, UxrInput2D.Joystick); Vector3 projectedForward = Vector3.ProjectOnPlane(ControllerForward, UpVector).normalized; Vector3 joystickDirection = new Vector3(joystickValue.x, 0.0f, joystickValue.y).normalized; teleportLocalDirection = TransformExt.GetLocalDirection(TeleportReference, Quaternion.LookRotation(projectedForward, UpVector) * Quaternion.LookRotation(joystickDirection, UpVector) * Vector3.forward); } // Run "EveryFrame" validators if there are any UxrTeleportDestination destination = null; foreach (Validator validator in _destinationValidators) { if (validator.Mode == UxrDestinationValidatorMode.EveryFrame) { if (destination == null) { destination = new UxrTeleportDestination(hit, teleportPos, Quaternion.LookRotation(TransformExt.GetWorldDirection(TeleportReference, teleportLocalDirection), UpVector)); } if (!validator.ValidatorFunc.Invoke(destination)) { _isValidTeleport = false; isTargetEnabled = ShowTargetAlsoWhenInvalid; ValidatorInvalidated?.Invoke(destination); break; } } } if (_isValidTeleport) { // Run internal validation _isValidTeleport = IsValidTeleport(checkBlockingInBetween, ref teleportPos, hit.normal, out bool validSlope) && !ignoreDestination; if (_isValidTeleport && validSlope) { // Hit against valid target EnableTeleportObjects(true, _isValidTeleport); } else { // Hit against blocking object or invalid slope _isValidTeleport = false; isTargetEnabled = ShowTargetAlsoWhenInvalid && validSlope; EnableTeleportObjects(isTargetEnabled, _isValidTeleport); } } if (isTargetEnabled) { // Place target TeleportLocalPosition = TransformExt.GetLocalPosition(TeleportReference, teleportPos); if (Avatar.AvatarMode == UxrAvatarMode.Local) { TeleportLocalDirection = teleportLocalDirection; } } } return _isValidTeleport; } /// /// Notifies that no raycast were found to be processed as a potential teleport destination. /// protected void NotifyNoDestinationRaycast() { _isValidTeleport = false; EnableTeleportObjects(false, false); NotifyTeleportSpawnCollider(null); } /// /// Tries to teleport the avatar using the current and /// values, only if the current destination is valid and the avatar isn't currently being teleported. /// protected void TryTeleportUsingCurrentTarget() { // Teleport if we can! if (_isValidTeleport && !IsTeleporting && !IsOtherComponentTeleporting && IsAllowedToTeleport) { Transform avatarTransform = Avatar.transform; Vector3 avatarPos = avatarTransform.position; Vector3 avatarUp = avatarTransform.up; Quaternion avatarRot = avatarTransform.rotation; if (TranslationType == UxrTranslationType.Fade) { UxrManager.Instance.TeleportFadeColor = FadeTranslationColor; } bool parentToDestination = ParentToDestination; if (TeleportReference != null && TeleportReference.TryGetComponent(out UxrParentAvatarDestination parentAvatarDestination)) { parentToDestination = parentAvatarDestination.ParentAvatar; } UxrTeleportSpawnCollider spawnCollider = _lastSpawnCollider; Vector3 teleportPos = TransformExt.GetWorldPosition(TeleportReference, TeleportLocalPosition); Quaternion teleportRot = Quaternion.LookRotation(TransformExt.GetWorldDirection(TeleportReference, TeleportLocalDirection), UpVector); // Run validators if there are any bool isValid = true; if (spawnCollider == null) { UxrTeleportDestination destination = null; foreach (Validator validator in _destinationValidators) { if (validator.Mode == UxrDestinationValidatorMode.OnConfirmationOnly) { if (destination == null) { destination = new UxrTeleportDestination(_hitInfo, teleportPos, teleportRot); } if (!validator.ValidatorFunc.Invoke(destination)) { ValidatorCanceled?.Invoke(destination); isValid = false; break; } } } } if (isValid) { IsTeleporting = true; UxrManager.Instance.TeleportLocalAvatarRelative(TeleportReference, parentToDestination, teleportPos, teleportRot, _translationType, TranslationSeconds, () => { if (spawnCollider != null) { spawnCollider.RaiseTeleported(Avatar, new UxrAvatarMoveEventArgs(avatarPos, avatarRot, Avatar.CameraFloorPosition, Quaternion.LookRotation(Avatar.ProjectedCameraForward, avatarUp))); } }, finished => { _isValidTeleport = false; IsTeleporting = false; ControllerStart = RawControllerStart; ControllerForward = RawControllerForward; }); } } NotifyTeleportSpawnCollider(null); } #endregion #region Private Methods /// /// Applies smoothing to the source transforms to avoid too much jitter. /// private void UpdateSmoothTransforms() { if (_shakeFilter > 0.0f && IsTeleporting && IsOtherComponentTeleporting == false) { float deltaTimeMultiplier = Mathf.Lerp(DeltaTimeMultiplierFilterMin, DeltaTimeMultiplierFilterMax, Mathf.Clamp01(_shakeFilter)); ControllerStart = Vector3.Lerp(ControllerStart, RawControllerStart, Time.deltaTime * deltaTimeMultiplier); ControllerForward = Vector3.Lerp(ControllerForward, RawControllerForward, Time.deltaTime * deltaTimeMultiplier); } else { ControllerStart = RawControllerStart; ControllerForward = RawControllerForward; } } /// /// Checks if a given teleport position is valid. We use the eye position of the teleport destination as /// a reference to be able to raycast to the ground and check for valid layers and slope angle and if there /// is a discrepancy between the raycast and the expected floor height. /// /// The floor level position of the teleport /// The eye position that will be used as reference for the teleport destination /// Will return if it is a valid slope /// Boolean telling whether newEyePos is a valid teleport position or not private bool IsValidDestination(Vector3 teleportPos, Vector3 newEyePos, out bool isValidSlope) { isValidSlope = false; Vector3 localNewEyePos = Avatar.transform.InverseTransformPoint(newEyePos); Vector3 localTeleportPos = Avatar.transform.InverseTransformPoint(teleportPos); float eyeHeight = localNewEyePos.y - localTeleportPos.y; if (HasBlockingRaycastHit(Avatar, newEyePos, -UpVector, eyeHeight * 1.2f, LayerMaskRaycast, TriggerCollidersInteraction, out RaycastHit hit)) { float slopeDegrees = Mathf.Abs(Vector3.Angle(hit.normal, UpVector)); isValidSlope = slopeDegrees < MaxAllowedSlopeDegrees; bool valid = isValidSlope && (ValidTargetLayers.value & 1 << hit.collider.gameObject.layer) != 0; Vector3 localHitPoint = Avatar.transform.InverseTransformPoint(hit.point); valid = valid && Mathf.Abs(localHitPoint.y - localTeleportPos.y) < MaxVerticalHeightDisparity; UxrIgnoreTeleportDestination ignoreDestinationComponent = hit.collider.GetComponentInParent(); if (ignoreDestinationComponent != null && ignoreDestinationComponent.enabled) { return false; } if (valid) { // Raycast upwards to see if there is something between the ground and eye level. Since we can be teleported inside a box // at eye level for instance, the previous raycast will not handle that case. We need to raycast from outside as well return !HasBlockingRaycastHit(Avatar, teleportPos, UpVector, eyeHeight, LayerMaskRaycast, TriggerCollidersInteraction, out RaycastHit _); } } return false; } /// /// Notifies a change in the currently targeted component. /// /// New currently targeted component or null if none is selected private void NotifyTeleportSpawnCollider(UxrTeleportSpawnCollider teleportSpawnCollider) { if (teleportSpawnCollider && teleportSpawnCollider.enabled) { if (_lastSpawnCollider != null && teleportSpawnCollider != _lastSpawnCollider && _lastSpawnCollider.EnableWhenSelected) { _lastSpawnCollider.EnableWhenSelected.SetActive(false); } else if (_lastSpawnCollider != teleportSpawnCollider && teleportSpawnCollider.EnableWhenSelected) { if (teleportSpawnCollider.EnableWhenSelected.activeSelf == false) { teleportSpawnCollider.EnableWhenSelected.SetActive(true); } } _lastSpawnCollider = teleportSpawnCollider; } else { if (_lastSpawnCollider != null && _lastSpawnCollider.EnableWhenSelected) { _lastSpawnCollider.EnableWhenSelected.SetActive(false); } _lastSpawnCollider = null; } } /// /// Enables or disables teleport graphical components. /// /// Whether to enable the teleport target object /// Whether the current teleport destination is valid private void EnableTeleportObjects(bool enableTarget, bool validTeleport) { _teleportTargetEnabled = enableTarget; _teleportTargetValid = validTeleport; // Enable / disable if (_teleportTarget != null) { _teleportTarget.gameObject.SetActive(enableTarget); } // Set materials if (ShowTargetAlsoWhenInvalid) { _teleportTarget.SetMaterialColor(validTeleport ? ValidMaterialColorTargets : InvalidMaterialColorTargets); } } /// /// Rotates the avatar around its vertical axis, where a positive angle turns it to the right and a negative angle to /// the left. /// /// Degrees to rotate private void Rotate(float degrees) { if (!IsTeleporting && !IsOtherComponentTeleporting) { IsTeleporting = true; Transform avatarTransform = Avatar.transform; if (RotationType == UxrRotationType.Fade) { UxrManager.Instance.TeleportFadeColor = FadeRotationColor; } UxrManager.Instance.RotateLocalAvatar(degrees, RotationType, RotationSeconds, null, finished => { IsTeleporting = false; ControllerStart = RawControllerStart; ControllerForward = RawControllerForward; }); } NotifyTeleportSpawnCollider(null); } #endregion #region Protected Types & Data /// /// Gets whether the avatar can currently receive input to step backwards. /// protected virtual bool CanBackStep => true; /// /// Gets whether the avatar can currently receive input to rotate around. /// protected virtual bool CanRotate => true; /// /// Gets whether other teleport component is currently teleporting the avatar. /// protected bool IsOtherComponentTeleporting => _otherAvatarTeleports != null && _otherAvatarTeleports.Any(otherTeleport => otherTeleport.IsTeleporting); /// /// Gets whether the component is currently allowed to teleport the avatar. /// protected bool IsAllowedToTeleport { get { if (IsTeleporting) { // Component is currently teleporting return false; } if (UxrCameraWallFade.IsAvatarPeekingThroughGeometry(Avatar)) { // Head is currently inside a wall. Avoid teleportation for "cheating". return false; } Vector3 cameraPos = Avatar.CameraPosition; Vector3 cameraToController = ControllerStart - cameraPos; return !HasBlockingRaycastHit(cameraPos, cameraToController.normalized, cameraToController.magnitude, out RaycastHit hit); } } /// /// Gets the used for ray-casting either valid or invalid teleport destinations. /// protected LayerMask LayerMaskRaycast => _layerMaskRaycast; /// /// Gets the up vector used to compute rotations so that it is always computed in the correct space. /// protected Vector3 UpVector => Avatar.transform.up; /// /// Gets or sets whether to parent the avatar to the destination object () after /// teleporting. /// This can also be overriden using a component. /// protected bool ParentToDestination { get => _parentToDestination; set => _parentToDestination = value; } /// /// Gets or sets whether the component is currently teleporting the avatar. /// protected bool IsTeleporting { get; private set; } /// /// Gets the smoothed source of ray-casting when it starts on the controller. /// protected Vector3 ControllerStart { get; private set; } /// /// Gets the smoothed direction of ray-casting when starts on the controller. /// protected Vector3 ControllerForward { get; private set; } /// /// Gets or sets the transform that will be used as reference for and /// to keep the relative positioning/orientation to while performing potential /// transitions, such as fades, before the actual teleporting. It is usually assigned the transform of the object that /// was hit with the destination raycast. /// The reference transform is used to make teleport transitions work correctly when the avatar is on a moving object. /// Without it, using absolute position and rotation only, would spawn the avatar with an incorrect offset due to the /// delay the transition introduces before the teleport. /// protected Transform TeleportReference { get; private set; } /// /// Gets or sets the current teleport destination in space. If /// is null, it will be considered as world-space position. /// protected Vector3 TeleportLocalPosition { get => _teleportLocalPosition; set { _teleportLocalPosition = value; if (_teleportTarget != null) { _teleportTarget.transform.position = TransformExt.GetWorldPosition(TeleportReference, value) + UpVector * TargetPlacementAboveHit; _teleportTarget.OrientArrow(Quaternion.LookRotation(TransformExt.GetWorldDirection(TeleportReference, TeleportLocalDirection), UpVector)); } } } /// /// Gets or sets the current teleport direction in in space. If /// is null, it will be considered as world-space rotation. /// protected Vector3 TeleportLocalDirection { get => _teleportLocalDirection; set { _teleportLocalDirection = value; if (_teleportTarget != null) { _teleportTarget.OrientArrow(Quaternion.LookRotation(TeleportReference != null ? TeleportReference.rotation * value : value, UpVector)); } } } /// /// Gets or sets teleport arrow's local rotation. /// protected Quaternion TeleportArrowLocalRotation { get => _teleportTarget.ReorientArrowLocalRotation; set => _teleportTarget.ReorientArrowLocalRotation = value; } #endregion #region Private Types & Data /// /// Gets the raw unprocessed world position on the controller where the ray-casting starts. /// private Vector3 RawControllerStart { get { if (_useControllerForward && Avatar != null) { Transform forwardTransform = Avatar.GetControllerInputForward(_controllerHand); return forwardTransform ? forwardTransform.position : transform.position; } return transform.position; } } /// /// Gets the raw unprocessed world direction on the controller used for ray-casting. /// private Vector3 RawControllerForward { get { if (_useControllerForward && Avatar != null) { Transform forwardTransform = Avatar.GetControllerInputForward(_controllerHand); return forwardTransform ? forwardTransform.forward : transform.forward; } return transform.forward; } } /// /// Gets the translation transition in seconds depending on . /// private float TranslationSeconds { get { return _translationType switch { UxrTranslationType.Fade => _fadeTranslationSeconds, UxrTranslationType.Smooth => _smoothTranslationSeconds, _ => 0.0f }; } } /// /// Gets the rotation transition in seconds depending on . /// private float RotationSeconds { get { return _rotationType switch { UxrRotationType.Fade => _fadeRotationSeconds, UxrRotationType.Smooth => _smoothRotationSeconds, _ => 0.0f }; } } private const float RaycastAboveGround = 0.05f; private const float RaycastLongDistance = 1000.0f; private const float HeadRadius = 0.2f; private const float DeltaTimeMultiplierFilterMin = 25.0f; private const float DeltaTimeMultiplierFilterMax = 5.0f; private const float MaxVerticalHeightDisparity = 0.2f; private const float DestinationValidationSubdivisions = 8; private const float DestinationValidationPositivesNeeded = 5; private readonly List _destinationValidators = new List(); private List _otherAvatarTeleports; private bool _isBackStepAvailable; private bool _isValidTeleport; private Vector3 _teleportLocalPosition; private Vector3 _teleportLocalDirection; private bool _teleportTargetEnabled; private bool _teleportTargetValid; private LayerMask _layerMaskRaycast = 0; private UxrTeleportTarget _teleportTarget; private UxrTeleportSpawnCollider _lastSpawnCollider; private RaycastHit _hitInfo; #endregion } }