Files
dungeons/Assets/ThirdParty/UltimateXR/Runtime/Scripts/Locomotion/UxrTeleportLocomotion.cs

542 lines
21 KiB
C#

// --------------------------------------------------------------------------------------------------------------------
// <copyright file="UxrTeleportLocomotion.cs" company="VRMADA">
// Copyright (c) VRMADA, All rights reserved.
// </copyright>
// --------------------------------------------------------------------------------------------------------------------
using UltimateXR.Devices;
using UnityEngine;
#pragma warning disable 0414
namespace UltimateXR.Locomotion
{
/// <summary>
/// Standard locomotion using an arc projected from the controllers.
/// </summary>
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
/// <summary>
/// Gets or sets whether the arc can be used.
/// </summary>
public bool IsArcAllowed { get; set; } = true;
#endregion
#region Public Overrides UxrLocomotion
/// <inheritdoc />
public override bool IsSmoothLocomotion => false;
#endregion
#region Unity
/// <summary>
/// Initializes the component.
/// </summary>
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<MeshFilter>();
_arcRenderer = _arcGameObject.AddComponent<MeshRenderer>();
_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;
}
/// <summary>
/// Resets the component when enabled.
/// </summary>
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);
}
/// <summary>
/// Disables the teleport graphics.
/// </summary>
protected override void OnDisable()
{
base.OnDisable();
UpdateTeleportState(false, false, false, Quaternion.identity);
}
#endregion
#region Protected Overrides UxrTeleportLocomotionBase
/// <inheritdoc />
protected override bool CanBackStep => !IsArcVisible;
/// <inheritdoc />
protected override bool CanRotate => !IsArcVisible;
/// <inheritdoc />
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);
}
}
/// <inheritdoc />
protected override void CancelTarget()
{
base.CancelTarget();
EnableArc(false, false);
_previousFrameHadArc = false;
_arcCancelled = true;
_arcCancelledByAngle = false;
}
#endregion
#region Private Methods
/// <summary>
/// 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.
/// </summary>
/// <param name="isArcEnabled">Is the arc enabled?</param>
/// <param name="isTargetEnabled">Is the target enabled?</param>
/// <param name="isValidTeleport">Is the teleport destination valid?</param>
/// <param name="teleportArrowLocalRotation">The teleport arrow's local rotation</param>
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 });
}
/// <summary>
/// Enables or disables the teleportation arc.
/// </summary>
/// <param name="enable">Whether the arc is visible</param>
/// <param name="isValidTeleport">Whether the current teleport destination is valid</param>
private void EnableArc(bool enable, bool isValidTeleport)
{
_arcGameObject.SetActive(enable);
_arcRenderer.sharedMaterial = isValidTeleport ? _arcMaterialValid : _arcMaterialInvalid;
}
/// <summary>
/// Computes the current teleport arc trajectory.
/// </summary>
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();
}
}
/// <summary>
/// Generates the arc mesh.
/// </summary>
/// <param name="isValidTeleport">Whether the current teleport destination is valid</param>
/// <param name="right">Arc world-space right vector</param>
/// <param name="parabolicSpeed">The start speed used for parabolic computation</param>
/// <param name="endTime">The time in the parabolic equation where the arc intersects with the first blocking element</param>
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);
}
/// <summary>
/// Computes the arc parabola.
/// </summary>
/// <param name="origin">World-space arc start position</param>
/// <param name="forward">World-space arc start direction</param>
/// <param name="speed">Start speed</param>
/// <param name="time">Time value to get the position for</param>
/// <returns>Position in the arc corresponding to the given time value</returns>
private Vector3 EvaluateArc(Vector3 origin, Vector3 forward, float speed, float time)
{
return origin + speed * time * forward - 0.5f * 9.8f * time * time * UpVector;
}
/// <summary>
/// Maps quality to steps.
/// </summary>
/// <param name="stepsQuality">Quality</param>
/// <returns>Step count used for ray-casting</returns>
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;
}
}
/// <summary>
/// Gets the parabolic speed to compute in the arc equation.
/// </summary>
/// <returns>Parabolic speed</returns>
private float GetCurrentParabolicSpeed()
{
return Mathf.Sqrt(ArcGravity * MaxAllowedDistance);
}
/// <summary>
/// Gets the parabolic angle to compute in the arc equation.
/// </summary>
/// <returns>Parabolic angle</returns>
private float GetCurrentParabolicAngle()
{
Vector3 right = Vector3.Cross(ControllerForward, UpVector).normalized;
Vector3 projectedForward = Vector3.ProjectOnPlane(ControllerForward, UpVector).normalized;
return -Vector3.SignedAngle(ControllerForward, projectedForward, right);
}
/// <summary>
/// Gets the time in seconds it would take a parabolic trajectory to travel up and down again.
/// </summary>
/// <param name="angle">Parabolic angle</param>
/// <returns>Time in seconds to go up and get back down again</returns>
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
/// <summary>
/// Gets whether the teleport arc is currently visible.
/// </summary>
private bool IsArcVisible => _arcGameObject.activeSelf;
/// <summary>
/// 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.
/// </summary>
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