// -------------------------------------------------------------------------------------------------------------------- // // Copyright (c) VRMADA, All rights reserved. // // -------------------------------------------------------------------------------------------------------------------- using UltimateXR.Devices; using UnityEngine; #pragma warning disable 0414 namespace UltimateXR.Locomotion { /// /// Standard locomotion using an arc projected from the controllers. /// public partial class UxrTeleportLocomotion : UxrTeleportLocomotionBase { #region Inspector Properties/Serialized Fields // Arc [SerializeField] [Range(2, 1000)] private int _arcSegments = 100; [SerializeField] [Range(0.01f, 0.4f)] private float _arcWidth = 0.1f; [SerializeField] private float _arcScrollSpeedValid = 1.0f; [SerializeField] private float _arcScrollSpeedInvalid = 0.5f; [SerializeField] private Material _arcMaterialValid; [SerializeField] private Material _arcMaterialInvalid; [SerializeField] private float _arcFadeLength = 2.0f; [SerializeField] private UxrRaycastStepsQuality _raycastStepsQuality = UxrRaycastStepsQuality.HighQuality; #endregion #region Public Types & Data /// /// Gets or sets whether the arc can be used. /// public bool IsArcAllowed { get; set; } = true; #endregion #region Public Overrides UxrLocomotion /// public override bool IsSmoothLocomotion => false; #endregion #region Unity /// /// Initializes the component. /// protected override void Awake() { base.Awake(); // Create arc GameObject _arcGameObject = new GameObject("Arc"); _arcGameObject.transform.SetPositionAndRotation(transform.position, transform.rotation); _arcGameObject.transform.parent = Avatar.transform; _arcMeshFilter = _arcGameObject.AddComponent(); _arcRenderer = _arcGameObject.AddComponent(); _arcMesh = new Mesh(); _vertices = new Vector3[(_arcSegments + 1) * 2]; _vertexColors = new Color32[(_arcSegments + 1) * 2]; _vertexMapping = new Vector2[(_arcSegments + 1) * 2]; _accumulatedArcLength = new float [(_arcSegments + 1) * 2]; _indices = new int[_arcSegments * 4]; for (int i = 0; i < _arcSegments; i++) { int baseIndex = (_arcSegments - 1 - i) * 2; _indices[i * 4 + 0] = baseIndex; _indices[i * 4 + 1] = baseIndex + 2; _indices[i * 4 + 2] = baseIndex + 3; _indices[i * 4 + 3] = baseIndex + 1; } _arcMesh.vertices = _vertices; _arcMesh.colors32 = _vertexColors; _arcMesh.uv = _vertexMapping; _arcMesh.SetIndices(_indices, MeshTopology.Quads, 0); _arcMeshFilter.mesh = _arcMesh; _arcRenderer.sharedMaterial = _arcMaterialValid; } /// /// Resets the component when enabled. /// protected override void OnEnable() { base.OnEnable(); // Set initial state _previousFrameHadArc = false; _arcCancelled = false; _arcCancelledByAngle = false; _scroll = 0.0f; _lastSyncIsArcEnabled = false; _lastSyncIsTargetEnabled = false; _lastSyncIsValidTeleport = false; _lastSyncTargetArrowLocalRot = Quaternion.identity; EnableArc(false, false); } /// /// Disables the teleport graphics. /// protected override void OnDisable() { base.OnDisable(); UpdateTeleportState(false, false, false, Quaternion.identity); } #endregion #region Protected Overrides UxrTeleportLocomotionBase /// protected override bool CanBackStep => !IsArcVisible; /// protected override bool CanRotate => !IsArcVisible; /// protected override void UpdateTeleportLocomotion() { Vector2 joystickValue = Avatar.ControllerInput.GetInput2D(HandSide, UxrInput2D.Joystick); if (joystickValue == Vector2.zero) { _arcCancelled = false; _arcCancelledByAngle = false; } else { if (_arcCancelled) { joystickValue = Vector2.zero; } } bool teleportArcActive = false; // Check if the arc is active. if (IsArcVisible || _arcCancelledByAngle) { // To support both touchpads and joysticks, we need to check in the case of touchpads that it is also pressed. if (Avatar.ControllerInput.MainJoystickIsTouchpad) { if (Avatar.ControllerInput.GetButtonsPress(HandSide, UxrInputButtons.Joystick)) { teleportArcActive = true; _arcCancelledByAngle = false; } } else { if (joystickValue != Vector2.zero) { teleportArcActive = true; _arcCancelledByAngle = false; } } } else if (Avatar.ControllerInput.GetButtonsPressDown(HandSide, UxrInputButtons.JoystickUp)) { teleportArcActive = true; } if (!IsArcAllowed) { teleportArcActive = false; } // If teleport is active update arc & target bool isArcEnabled; bool isTargetEnabled; bool isValidTeleport; if (teleportArcActive) { // Disable others if this one just activated if (_previousFrameHadArc == false) { CancelOtherTeleportTargets(); } // Compute trajectory ComputeCurrentArcTrajectory(out isArcEnabled, out isTargetEnabled, out isValidTeleport); } else { if (_previousFrameHadArc && IsArcAllowed) { TryTeleportUsingCurrentTarget(); } _previousFrameHadArc = false; isArcEnabled = false; isTargetEnabled = false; isValidTeleport = false; EnableArc(isArcEnabled, isValidTeleport); NotifyNoDestinationRaycast(); } // Notify state changes in a simpler way to avoid unnecessary traffic if (_lastSyncIsArcEnabled != isArcEnabled || _lastSyncIsTargetEnabled != isTargetEnabled || _lastSyncIsValidTeleport != isValidTeleport || Quaternion.Angle(_lastSyncTargetArrowLocalRot, TeleportArrowLocalRotation) > ArrowAngleChangeThreshold) { UpdateTeleportState(isArcEnabled, isTargetEnabled, isValidTeleport, TeleportArrowLocalRotation); } } /// protected override void CancelTarget() { base.CancelTarget(); EnableArc(false, false); _previousFrameHadArc = false; _arcCancelled = true; _arcCancelledByAngle = false; } #endregion #region Private Methods /// /// Updates the state of elements in the teleport. This is mainly to synchronize the state on a networking environment. /// It is performed in a separate method in order to have better control over the amount of traffic that is being /// generated because of the arrow rotation. /// /// Is the arc enabled? /// Is the target enabled? /// Is the teleport destination valid? /// The teleport arrow's local rotation private void UpdateTeleportState(bool isArcEnabled, bool isTargetEnabled, bool isValidTeleport, Quaternion teleportArrowLocalRotation) { // This method will be synchronized through network BeginSync(); _lastSyncIsArcEnabled = isArcEnabled; _lastSyncIsTargetEnabled = isTargetEnabled; _lastSyncIsValidTeleport = isValidTeleport; _lastSyncTargetArrowLocalRot = teleportArrowLocalRotation; if (isArcEnabled) { // TODO: The target enabled and valid teleport state are computed using the current // hand transform, which might be slightly different in a network environment. ComputeCurrentArcTrajectory(out bool _, out bool _, out bool _); TeleportArrowLocalRotation = teleportArrowLocalRotation; } else { EnableArc(isArcEnabled, isValidTeleport); NotifyNoDestinationRaycast(); } EndSyncMethod(new object[] { isArcEnabled, isTargetEnabled, isValidTeleport, teleportArrowLocalRotation }); } /// /// Enables or disables the teleportation arc. /// /// Whether the arc is visible /// Whether the current teleport destination is valid private void EnableArc(bool enable, bool isValidTeleport) { _arcGameObject.SetActive(enable); _arcRenderer.sharedMaterial = isValidTeleport ? _arcMaterialValid : _arcMaterialInvalid; } /// /// Computes the current teleport arc trajectory. /// private void ComputeCurrentArcTrajectory(out bool isArcEnabled, out bool isTargetEnabled, out bool isValidTeleport) { Vector3 right = Vector3.Cross(ControllerForward, Vector3.up).normalized; float angle = GetCurrentParabolicAngle(); float timeToTravelHorizontally = GetTimeToTravelHorizontally(angle); float parabolicSpeed = GetCurrentParabolicSpeed(); if (Mathf.Abs(angle) < AbsoluteMaxArcAngleThreshold && _arcWidth > 0.0f) { isTargetEnabled = false; isValidTeleport = false; float endTime = timeToTravelHorizontally * 2; float deltaTime = endTime / BlockingRaycastStepsQualityToSteps(_raycastStepsQuality); bool hitSomething = false; for (float time = 0.0f; time < endTime; time += deltaTime) { Vector3 point1 = EvaluateArc(ControllerStart, ControllerForward, parabolicSpeed, time); Vector3 point2 = EvaluateArc(ControllerStart, ControllerForward, parabolicSpeed, time + deltaTime); float distanceBetweenPoints = Vector3.Distance(point1, point2); Vector3 direction = (point2 - point1) / distanceBetweenPoints; // Process blocking hit. // Use RaycastAll to avoid putting "permitted" objects in between "blocking" objects to teleport through walls or any other cheats. if (HasBlockingRaycastHit(point1, direction, distanceBetweenPoints, out RaycastHit hit)) { endTime = time + deltaTime * (hit.distance / distanceBetweenPoints); hitSomething = true; isValidTeleport = NotifyDestinationRaycast(hit, false, out isTargetEnabled); break; } } if (hitSomething == false) { NotifyNoDestinationRaycast(); } _previousFrameHadArc = true; isArcEnabled = true; GenerateArcMesh(isValidTeleport, right, parabolicSpeed, endTime); EnableArc(isArcEnabled, isValidTeleport); } else { _arcCancelledByAngle = true; isArcEnabled = false; isTargetEnabled = false; isValidTeleport = false; EnableArc(isArcEnabled, isValidTeleport); NotifyNoDestinationRaycast(); } } /// /// Generates the arc mesh. /// /// Whether the current teleport destination is valid /// Arc world-space right vector /// The start speed used for parabolic computation /// The time in the parabolic equation where the arc intersects with the first blocking element private void GenerateArcMesh(bool isValidTeleport, Vector3 right, float parabolicSpeed, float endTime) { Vector3 previousPoint = Vector3.zero; float currentLength = 0.0f; float totalLength = 0.0f; _arcGameObject.transform.SetPositionAndRotation(ControllerStart, Quaternion.LookRotation(ControllerForward, UpVector)); _scroll += Time.deltaTime * (isValidTeleport ? _arcScrollSpeedValid : _arcScrollSpeedInvalid); for (int i = 0; i <= _arcSegments; ++i) { float time = endTime * ((float)i / _arcSegments); Vector3 point = EvaluateArc(ControllerStart, ControllerForward, parabolicSpeed, time); _vertices[i * 2 + 0] = _arcGameObject.transform.InverseTransformPoint(point - _arcWidth * 0.5f * right); _vertices[i * 2 + 1] = _arcGameObject.transform.InverseTransformPoint(point + _arcWidth * 0.5f * right); float pointDistance = i == 0 ? 0.0f : Vector3.Distance(previousPoint, point); if (i > 0) { currentLength += pointDistance; } _accumulatedArcLength[i] = currentLength; totalLength = currentLength; float v = currentLength / _arcWidth - _scroll; _vertexMapping[i * 2 + 0] = new Vector2(0.0f, v); _vertexMapping[i * 2 + 1] = new Vector2(1.0f, v); previousPoint = point; } // After creating the vertices, assign the colors after because knowing the exact arc length we can fade it nicely at both ends for (int i = 0; i <= _arcSegments; ++i) { byte alpha = 255; if (_arcFadeLength > 0.0f) { float alphaFloat = 1.0f; if (_accumulatedArcLength[i] < _arcFadeLength) { alphaFloat = _accumulatedArcLength[i] / _arcFadeLength; alpha = (byte)(alphaFloat * 255); } if (totalLength - _accumulatedArcLength[i] < _arcFadeLength) { alphaFloat = alphaFloat * ((totalLength - _accumulatedArcLength[i]) / _arcFadeLength); alpha = (byte)(alphaFloat * 255); } } _vertexColors[i * 2 + 0] = new Color32(255, 255, 255, alpha); _vertexColors[i * 2 + 1] = new Color32(255, 255, 255, alpha); } // Assign mesh _arcMesh.vertices = _vertices; _arcMesh.colors32 = _vertexColors; _arcMesh.uv = _vertexMapping; _arcMesh.SetIndices(_indices, MeshTopology.Quads, 0); _arcMesh.bounds = new Bounds(Vector3.zero, Vector3.one * Avatar.CameraComponent.farClipPlane); } /// /// Computes the arc parabola. /// /// World-space arc start position /// World-space arc start direction /// Start speed /// Time value to get the position for /// Position in the arc corresponding to the given time value private Vector3 EvaluateArc(Vector3 origin, Vector3 forward, float speed, float time) { return origin + speed * time * forward - 0.5f * 9.8f * time * time * UpVector; } /// /// Maps quality to steps. /// /// Quality /// Step count used for ray-casting private int BlockingRaycastStepsQualityToSteps(UxrRaycastStepsQuality stepsQuality) { switch (stepsQuality) { case UxrRaycastStepsQuality.LowQuality: return BlockingRaycastStepsQualityLow; case UxrRaycastStepsQuality.MediumQuality: return BlockingRaycastStepsQualityMedium; case UxrRaycastStepsQuality.HighQuality: return BlockingRaycastStepsQualityHigh; case UxrRaycastStepsQuality.VeryHighQuality: return BlockingRaycastStepsQualityVeryHigh; default: return BlockingRaycastStepsQualityHigh; } } /// /// Gets the parabolic speed to compute in the arc equation. /// /// Parabolic speed private float GetCurrentParabolicSpeed() { return Mathf.Sqrt(ArcGravity * MaxAllowedDistance); } /// /// Gets the parabolic angle to compute in the arc equation. /// /// Parabolic angle private float GetCurrentParabolicAngle() { Vector3 right = Vector3.Cross(ControllerForward, UpVector).normalized; Vector3 projectedForward = Vector3.ProjectOnPlane(ControllerForward, UpVector).normalized; return -Vector3.SignedAngle(ControllerForward, projectedForward, right); } /// /// Gets the time in seconds it would take a parabolic trajectory to travel up and down again. /// /// Parabolic angle /// Time in seconds to go up and get back down again private float GetTimeToTravelHorizontally(float angle) { return Mathf.Max(2.0f, 2.0f * GetCurrentParabolicSpeed() * Mathf.Abs(Mathf.Sin(angle * Mathf.Deg2Rad)) / ArcGravity); } #endregion #region Private Types & Data /// /// Gets whether the teleport arc is currently visible. /// private bool IsArcVisible => _arcGameObject.activeSelf; /// /// The change in degrees of the arrow direction to consider it a state change and raise the event. It is used to avoid /// sending repeated data. /// private const float ArrowAngleChangeThreshold = 2.0f; private const int BlockingRaycastStepsQualityLow = 10; private const int BlockingRaycastStepsQualityMedium = 20; private const int BlockingRaycastStepsQualityHigh = 40; private const int BlockingRaycastStepsQualityVeryHigh = 80; private const float AbsoluteMaxArcAngleThreshold = 75.0f; private const float ArcGravity = 9.8f; private bool _previousFrameHadArc; private bool _arcCancelled; private bool _arcCancelledByAngle; private GameObject _arcGameObject; private MeshFilter _arcMeshFilter; private MeshRenderer _arcRenderer; private Mesh _arcMesh; private Vector3[] _vertices; private Color32[] _vertexColors; private Vector2[] _vertexMapping; private int[] _indices; private float[] _accumulatedArcLength; private float _scroll; private bool _lastSyncIsArcEnabled; private bool _lastSyncIsTargetEnabled; private bool _lastSyncIsValidTeleport; private Quaternion _lastSyncTargetArrowLocalRot; #endregion } } #pragma warning restore 0414