// --------------------------------------------------------------------------------------------------------------------
//
// Copyright (c) VRMADA, All rights reserved.
//
// --------------------------------------------------------------------------------------------------------------------
using System.Collections;
using System.Collections.Generic;
using UltimateXR.Animation.Interpolation;
using UltimateXR.Avatar;
using UltimateXR.Core.Components.Singleton;
using UltimateXR.Extensions.Unity.Render;
using UnityEngine;
namespace UltimateXR.Guides
{
///
/// Compass component that assists the user by giving visual hints to know where to look or the action to perform.
/// It will show an arrow in front of the view that will help getting the target into sight.
/// When the target gets into sight it can optionally show an action icon:
///
/// - Location: To let the user know where to move next
/// - Grab: To let the user know an object should be grabbed
/// - Look: To focus attention on an object
/// - Use: To let the user know an operation should be performed on an object
///
///
///
/// Since the compass is a , it is unique and can be invoked from any point using
/// UxrCompass.Instance.
///
public class UxrCompass : UxrSingleton
{
#region Inspector Properties/Serialized Fields
[SerializeField] private float _distanceToCamera = 1.0f;
[SerializeField] private Transform _focusedObjectTarget;
[SerializeField] private Transform _compassArrowPivot;
[SerializeField] private Renderer _compassArrowRenderer;
[SerializeField] private Transform _transitionArrow;
[SerializeField] private GameObject _rootOnScreenIcons;
[SerializeField] private Transform _iconLocationPivot;
[SerializeField] private Transform _iconLocationBottom;
[SerializeField] private MeshRenderer _iconLocationRenderer;
[SerializeField] private Transform _iconLookPivot;
[SerializeField] private MeshRenderer _iconLookRenderer;
[SerializeField] private Transform _iconGrabPivot;
[SerializeField] private MeshRenderer _iconGrabRenderer;
[SerializeField] private Transform _iconUsePivot;
[SerializeField] private MeshRenderer _iconUseRenderer;
#endregion
#region Public Types & Data
///
/// Gets whether the compass is currently focused on an object.
///
public bool HasTarget => _focusedObjectTarget != null || _targetIsRawPos;
///
/// Gets the target's .
///
public Transform TargetTransform => _targetHint != null ? _targetHint.GetTransform(this) : _focusedObjectTarget;
///
/// Gets the target's position.
///
public Vector3 TargetPosition
{
get
{
if (_targetIsRawPos)
{
return _rawTargetPos;
}
return TargetTransform != null ? TargetTransform.position : Vector3.zero;
}
}
///
/// Gets or sets the current display mode.
///
public UxrCompassDisplayMode DisplayMode { get; set; } = UxrCompassDisplayMode.Location;
#endregion
#region Public Methods
///
/// Sets the current target.
///
/// New target or null to stop
/// The display mode
/// The icon size multiplier
public void SetTarget(Transform target, UxrCompassDisplayMode displayMode = UxrCompassDisplayMode.OnlyCompass, float iconScale = 1.0f)
{
DisplayMode = displayMode;
_focusedObjectTarget = target;
_targetStartTime = Time.unscaledTime;
_onScreenStartTime = Time.unscaledTime;
_targetIsRawPos = false;
_targetHint = target != null ? target.gameObject.GetComponent() : null;
_iconScale = iconScale;
_isTemporary = false;
}
///
/// Sets the current target. When the object gets into sight it will show the icon described by
/// during a limited amount of time (). The
/// timer is reset each time the object gets out of sight.
///
/// New target or null to stop
/// The display mode
/// The icon size multiplier
public void SetTargetTemporary(Transform target, UxrCompassDisplayMode displayMode = UxrCompassDisplayMode.OnlyCompass, float iconScale = 1.0f)
{
SetTarget(target, displayMode, iconScale);
_isTemporary = true;
}
///
/// Sets the current target.
///
/// The target position
/// The display mode
/// The icon size multiplier
public void SetTarget(Vector3 position, UxrCompassDisplayMode displayMode = UxrCompassDisplayMode.OnlyCompass, float iconScale = 1.0f)
{
DisplayMode = displayMode;
_focusedObjectTarget = null;
_targetStartTime = Time.unscaledTime;
_onScreenStartTime = Time.unscaledTime;
_targetIsRawPos = true;
_rawTargetPos = position;
_targetHint = null;
_iconScale = iconScale;
_isTemporary = false;
}
///
/// Sets the current target. When the object gets into sight it will show the icon described by
/// during a limited amount of time (). The
/// timer is reset each time the object gets out of sight.
///
/// The target position
/// The display mode
/// The icon size multiplier
public void SetTargetTemporary(Vector3 position, UxrCompassDisplayMode displayMode = UxrCompassDisplayMode.OnlyCompass, float iconScale = 1.0f)
{
SetTarget(position, displayMode, iconScale);
_isTemporary = true;
}
#endregion
#region Unity
///
/// Initializes the compass.
///
protected override void Awake()
{
base.Awake();
_compassArrowPivot.gameObject.SetActive(false);
_transitionArrow.gameObject.SetActive(false);
_rootOnScreenIcons.SetActive(false);
_initialIconScales = new Dictionary();
foreach (MeshRenderer iconRenderer in IconRenderers)
{
_initialIconScales.Add(iconRenderer, iconRenderer.transform.localScale);
}
}
///
/// Updates the compass.
///
private void Update()
{
if (!HasTarget)
{
// No object focused anymore
if (_targetFocused)
{
_targetFocused = false;
if (_coroutineArrowTransition != null)
{
StopCoroutine(_coroutineArrowTransition);
}
_compassArrowPivot.gameObject.SetActive(false);
_transitionArrow.gameObject.SetActive(false);
_rootOnScreenIcons.SetActive(false);
}
}
else
{
// Object focused. Check if the object is onscreen or offscreen to show compass or bouncing arrow.
// Also check if we need to trigger the transition arrow when going from offscreen to onscreen.
if (!_targetFocused)
{
_targetFocused = true;
}
if (_isTemporary && Time.unscaledTime - _targetStartTime > TemporaryDurationSeconds)
{
SetTarget(null);
return;
}
Camera avatarCamera = UxrAvatar.LocalAvatarCamera;
Vector3 targetInCameraPos = avatarCamera.WorldToScreenPoint(TargetPosition);
float percentMargin = 0.20f;
float marginWidth = avatarCamera.pixelWidth * percentMargin;
float marginHeight = avatarCamera.pixelHeight * percentMargin;
if (targetInCameraPos.x >= marginWidth &&
targetInCameraPos.x <= avatarCamera.pixelWidth - marginWidth &&
targetInCameraPos.y >= marginHeight &&
targetInCameraPos.y <= avatarCamera.pixelHeight - marginHeight &&
targetInCameraPos.z > 0.0f)
{
// Object onscreen
if (!_rootOnScreenIcons.activeSelf && !_transitionArrow.gameObject.activeSelf)
{
// Transition offscreen -> onscreen
_transitionArrow.gameObject.SetActive(true);
if (_coroutineArrowTransition != null)
{
StopCoroutine(_coroutineArrowTransition);
}
_coroutineArrowTransition = StartCoroutine(ArrowTransitionCoroutine(_compassArrowRenderer.transform.position, TargetPosition));
}
_rootOnScreenIcons.transform.position = TargetPosition;
_compassArrowPivot.gameObject.SetActive(false);
UpdateOnScreenIcon(Time.unscaledTime);
}
else
{
// Object offscreen -> show compass
_rootOnScreenIcons.gameObject.SetActive(false);
_compassArrowPivot.gameObject.SetActive(true);
Vector3 direction = avatarCamera.transform.InverseTransformPoint(TargetPosition);
direction.z = 0.0f;
direction.Normalize();
direction = new Vector3(targetInCameraPos.x - avatarCamera.pixelWidth * 0.5f, targetInCameraPos.y - avatarCamera.pixelHeight * 0.5f, 0.0f).normalized;
if (targetInCameraPos.z < 0.0f)
{
direction = -direction;
}
_compassArrowPivot.transform.SetPositionAndRotation(avatarCamera.transform.position + avatarCamera.transform.forward * _distanceToCamera, Quaternion.LookRotation(avatarCamera.transform.forward, avatarCamera.transform.TransformDirection(direction)));
}
}
Color color = Color.white;
color.a = (Mathf.Sin(Time.unscaledTime * Mathf.PI * 2.0f * 5.0f) + 1.0f) * 0.5f;
_compassArrowRenderer.material.color = color;
}
#endregion
#region Coroutines
///
/// Coroutine that transitions between the compass arrow to the arrow that moves to the target when it comes into
/// sight.
///
///
///
///
private IEnumerator ArrowTransitionCoroutine(Vector3 posStart, Vector3 posEnd)
{
_transitionArrow.rotation = Quaternion.LookRotation(posEnd - posStart);
float duration = 0.2f;
float startTime = Time.unscaledTime;
while (Time.unscaledTime - startTime < duration)
{
float t = (Time.unscaledTime - startTime) / duration;
_transitionArrow.transform.position = Vector3.Lerp(posStart, posEnd, t);
yield return null;
}
_transitionArrow.gameObject.SetActive(false);
// _onScreenStartTime will ensure that the effects will align in a cool way when the transition arrow disappears. The animation curve will always start correctly.
_onScreenStartTime = Time.unscaledTime;
_rootOnScreenIcons.SetActive(true);
UpdateOnScreenIcon(Time.unscaledTime);
_coroutineArrowTransition = null;
}
#endregion
#region Private Methods
///
/// Updates the icon.
///
/// Time in seconds the icon has been on screen
private void UpdateOnScreenIcon(float time)
{
if (UxrAvatar.LocalAvatarCamera == null)
{
return;
}
float frequency = 2.0f;
float timeSinceOnScreen = time - _onScreenStartTime;
float interpolationTime = timeSinceOnScreen * frequency;
float effectBounceT = UxrInterpolator.GetInterpolationFactor(interpolationTime, UxrEasing.EaseOutQuad, UxrLoopMode.PingPong);
float effectSineT = UxrInterpolator.GetInterpolationFactor(interpolationTime, UxrEasing.EaseInOutSine, UxrLoopMode.PingPong);
_rootOnScreenIcons.transform.position = TargetPosition;
_iconLocationPivot.gameObject.SetActive(DisplayMode == UxrCompassDisplayMode.Location);
_iconLookPivot.gameObject.SetActive(DisplayMode == UxrCompassDisplayMode.Look && timeSinceOnScreen < TemporaryDurationSeconds);
_iconGrabPivot.gameObject.SetActive(DisplayMode == UxrCompassDisplayMode.Grab);
_iconUsePivot.gameObject.SetActive(DisplayMode == UxrCompassDisplayMode.Use);
if (DisplayMode == UxrCompassDisplayMode.Location)
{
_iconLocationBottom.transform.localPosition = Vector3.up * (effectBounceT * 0.4f);
}
else if (DisplayMode == UxrCompassDisplayMode.Grab)
{
_iconGrabRenderer.material.color = ColorExt.ColorAlpha(Color.white, effectSineT);
}
else if (DisplayMode == UxrCompassDisplayMode.Look)
{
_iconLookRenderer.material.color = ColorExt.ColorAlpha(Color.white, effectSineT);
}
else if (DisplayMode == UxrCompassDisplayMode.Use)
{
_iconUseRenderer.material.color = ColorExt.ColorAlpha(Color.white, effectSineT);
}
// Scale visible icon based on size
_rootOnScreenIcons.transform.localScale = Vector3.one * _iconScale;
foreach (KeyValuePair iconScale in _initialIconScales)
{
if (iconScale.Key.gameObject.activeInHierarchy)
{
float distance = Vector3.Distance(iconScale.Key.transform.position, UxrAvatar.LocalAvatar.CameraPosition);
iconScale.Key.transform.localScale = Vector3.Max(iconScale.Value, (distance * 0.3f) * iconScale.Value);
}
}
}
#endregion
#region Private Types & Data
///
/// Gets the icon renderer components.
///
private IEnumerable IconRenderers
{
get
{
yield return _iconLocationRenderer;
yield return _iconLookRenderer;
yield return _iconGrabRenderer;
yield return _iconUseRenderer;
}
}
///
/// Duration in seconds to show the look icon while the target is in view. After that, do not show the look icon unless
/// it comes into sight again. It is also used by .
///
private const float TemporaryDurationSeconds = 3.0f;
private bool _targetFocused;
private bool _targetIsRawPos;
private Vector3 _rawTargetPos;
private UxrCompassTargetHint _targetHint;
private Coroutine _coroutineArrowTransition;
private float _targetStartTime;
private float _onScreenStartTime;
private Dictionary _initialIconScales;
private float _iconScale;
private bool _isTemporary;
#endregion
}
}