init
This commit is contained in:
@@ -0,0 +1,143 @@
|
||||
using UnityEngine.XR.Interaction.Toolkit.Locomotion.Climbing;
|
||||
using UnityEngine.XR.Interaction.Toolkit.Locomotion.Teleportation;
|
||||
using UnityEngine.XR.Interaction.Toolkit.Utilities;
|
||||
|
||||
namespace UnityEngine.XR.Interaction.Toolkit.Samples.StarterAssets
|
||||
{
|
||||
/// <summary>
|
||||
/// Affordance component used in conjunction with a <see cref="ClimbTeleportInteractor"/> to display an object
|
||||
/// pointing at the target teleport destination while climbing.
|
||||
/// </summary>
|
||||
public class ClimbTeleportDestinationIndicator : MonoBehaviour
|
||||
{
|
||||
[SerializeField]
|
||||
[Tooltip("The interactor that drives the display and placement of the pointer object.")]
|
||||
ClimbTeleportInteractor m_ClimbTeleportInteractor;
|
||||
|
||||
/// <summary>
|
||||
/// The interactor that drives the display and placement of the pointer object.
|
||||
/// </summary>
|
||||
public ClimbTeleportInteractor climbTeleportInteractor
|
||||
{
|
||||
get => m_ClimbTeleportInteractor;
|
||||
set => m_ClimbTeleportInteractor = value;
|
||||
}
|
||||
|
||||
[SerializeField]
|
||||
[Tooltip("The prefab to spawn when a teleport destination is chosen. The instance will spawn next to the " +
|
||||
"destination and point its forward vector at the destination and its up vector at the camera.")]
|
||||
GameObject m_PointerPrefab;
|
||||
|
||||
/// <summary>
|
||||
/// The prefab to spawn when a teleport destination is chosen. The instance will spawn next to the destination
|
||||
/// and point its forward vector at the destination and its up vector at the camera.
|
||||
/// </summary>
|
||||
public GameObject pointerPrefab
|
||||
{
|
||||
get => m_PointerPrefab;
|
||||
set => m_PointerPrefab = value;
|
||||
}
|
||||
|
||||
[SerializeField]
|
||||
[Tooltip("The distance from the destination at which the pointer object spawns.")]
|
||||
float m_PointerDistance = 0.3f;
|
||||
|
||||
/// <summary>
|
||||
/// The distance from the destination at which the pointer object spawns.
|
||||
/// </summary>
|
||||
public float pointerDistance
|
||||
{
|
||||
get => m_PointerDistance;
|
||||
set => m_PointerDistance = value;
|
||||
}
|
||||
|
||||
TeleportationMultiAnchorVolume m_ActiveTeleportVolume;
|
||||
Transform m_PointerInstance;
|
||||
|
||||
/// <summary>
|
||||
/// See <see cref="MonoBehaviour"/>.
|
||||
/// </summary>
|
||||
protected void OnEnable()
|
||||
{
|
||||
if (m_ClimbTeleportInteractor == null)
|
||||
{
|
||||
if (!ComponentLocatorUtility<ClimbTeleportInteractor>.TryFindComponent(out m_ClimbTeleportInteractor))
|
||||
{
|
||||
Debug.LogError($"Could not find {nameof(ClimbTeleportInteractor)} in scene.");
|
||||
enabled = false;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
m_ClimbTeleportInteractor.hoverEntered.AddListener(OnInteractorHoverEntered);
|
||||
m_ClimbTeleportInteractor.hoverExited.AddListener(OnInteractorHoverExited);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// See <see cref="MonoBehaviour"/>.
|
||||
/// </summary>
|
||||
protected void OnDisable()
|
||||
{
|
||||
HideIndicator();
|
||||
|
||||
if (m_ActiveTeleportVolume != null)
|
||||
{
|
||||
m_ActiveTeleportVolume.destinationAnchorChanged -= OnClimbTeleportDestinationAnchorChanged;
|
||||
m_ActiveTeleportVolume = null;
|
||||
}
|
||||
|
||||
if (m_ClimbTeleportInteractor != null)
|
||||
{
|
||||
m_ClimbTeleportInteractor.hoverEntered.RemoveListener(OnInteractorHoverEntered);
|
||||
m_ClimbTeleportInteractor.hoverExited.RemoveListener(OnInteractorHoverExited);
|
||||
}
|
||||
}
|
||||
|
||||
void OnInteractorHoverEntered(HoverEnterEventArgs args)
|
||||
{
|
||||
if (m_ActiveTeleportVolume != null || !(args.interactableObject is TeleportationMultiAnchorVolume teleportVolume))
|
||||
return;
|
||||
|
||||
m_ActiveTeleportVolume = teleportVolume;
|
||||
if (m_ActiveTeleportVolume.destinationAnchor != null)
|
||||
OnClimbTeleportDestinationAnchorChanged(m_ActiveTeleportVolume);
|
||||
|
||||
m_ActiveTeleportVolume.destinationAnchorChanged += OnClimbTeleportDestinationAnchorChanged;
|
||||
}
|
||||
|
||||
void OnInteractorHoverExited(HoverExitEventArgs args)
|
||||
{
|
||||
if (!(args.interactableObject is TeleportationMultiAnchorVolume teleportVolume) || teleportVolume != m_ActiveTeleportVolume)
|
||||
return;
|
||||
|
||||
HideIndicator();
|
||||
m_ActiveTeleportVolume.destinationAnchorChanged -= OnClimbTeleportDestinationAnchorChanged;
|
||||
m_ActiveTeleportVolume = null;
|
||||
}
|
||||
|
||||
void OnClimbTeleportDestinationAnchorChanged(TeleportationMultiAnchorVolume teleportVolume)
|
||||
{
|
||||
HideIndicator();
|
||||
|
||||
var destinationAnchor = teleportVolume.destinationAnchor;
|
||||
if (destinationAnchor == null)
|
||||
return;
|
||||
|
||||
m_PointerInstance = Instantiate(m_PointerPrefab).transform;
|
||||
var cameraTrans = teleportVolume.teleportationProvider.mediator.xrOrigin.Camera.transform;
|
||||
var cameraPosition = cameraTrans.position;
|
||||
var destinationPosition = destinationAnchor.position;
|
||||
var destinationDirectionInScreenSpace = cameraTrans.InverseTransformDirection(destinationPosition - cameraPosition);
|
||||
destinationDirectionInScreenSpace.z = 0f;
|
||||
var pointerDirection = cameraTrans.TransformDirection(destinationDirectionInScreenSpace).normalized;
|
||||
m_PointerInstance.position = destinationPosition - pointerDirection * m_PointerDistance;
|
||||
m_PointerInstance.rotation = Quaternion.LookRotation(pointerDirection, -cameraTrans.forward);
|
||||
}
|
||||
|
||||
void HideIndicator()
|
||||
{
|
||||
if (m_PointerInstance != null)
|
||||
Destroy(m_PointerInstance.gameObject);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: e766f86cb7d2461683eb37d8a971fb14
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,84 @@
|
||||
using UnityEngine.XR.Interaction.Toolkit.Inputs.Readers;
|
||||
|
||||
namespace UnityEngine.XR.Interaction.Toolkit.Samples.StarterAssets
|
||||
{
|
||||
/// <summary>
|
||||
/// Component which reads input values and drives the thumbstick, trigger, and grip transforms
|
||||
/// to animate a controller model.
|
||||
/// </summary>
|
||||
public class ControllerAnimator : MonoBehaviour
|
||||
{
|
||||
[Header("Thumbstick")]
|
||||
[SerializeField]
|
||||
Transform m_ThumbstickTransform;
|
||||
|
||||
[SerializeField]
|
||||
Vector2 m_StickRotationRange = new Vector2(30f, 30f);
|
||||
|
||||
[SerializeField]
|
||||
XRInputValueReader<Vector2> m_StickInput = new XRInputValueReader<Vector2>("Thumbstick");
|
||||
|
||||
[Header("Trigger")]
|
||||
[SerializeField]
|
||||
Transform m_TriggerTransform;
|
||||
|
||||
[SerializeField]
|
||||
Vector2 m_TriggerXAxisRotationRange = new Vector2(0f, -15f);
|
||||
|
||||
[SerializeField]
|
||||
XRInputValueReader<float> m_TriggerInput = new XRInputValueReader<float>("Trigger");
|
||||
|
||||
[Header("Grip")]
|
||||
[SerializeField]
|
||||
Transform m_GripTransform;
|
||||
|
||||
[SerializeField]
|
||||
Vector2 m_GripRightRange = new Vector2(-0.0125f, -0.011f);
|
||||
|
||||
[SerializeField]
|
||||
XRInputValueReader<float> m_GripInput = new XRInputValueReader<float>("Grip");
|
||||
|
||||
void OnEnable()
|
||||
{
|
||||
if (m_ThumbstickTransform == null || m_GripTransform == null || m_TriggerTransform == null)
|
||||
{
|
||||
enabled = false;
|
||||
Debug.LogWarning($"Controller Animator component missing references on {gameObject.name}", this);
|
||||
return;
|
||||
}
|
||||
|
||||
m_StickInput?.EnableDirectActionIfModeUsed();
|
||||
m_TriggerInput?.EnableDirectActionIfModeUsed();
|
||||
m_GripInput?.EnableDirectActionIfModeUsed();
|
||||
}
|
||||
|
||||
void OnDisable()
|
||||
{
|
||||
m_StickInput?.DisableDirectActionIfModeUsed();
|
||||
m_TriggerInput?.DisableDirectActionIfModeUsed();
|
||||
m_GripInput?.DisableDirectActionIfModeUsed();
|
||||
}
|
||||
|
||||
void Update()
|
||||
{
|
||||
if (m_StickInput != null)
|
||||
{
|
||||
var stickVal = m_StickInput.ReadValue();
|
||||
m_ThumbstickTransform.localRotation = Quaternion.Euler(-stickVal.y * m_StickRotationRange.x, 0f, -stickVal.x * m_StickRotationRange.y);
|
||||
}
|
||||
|
||||
if (m_TriggerInput != null)
|
||||
{
|
||||
var triggerVal = m_TriggerInput.ReadValue();
|
||||
m_TriggerTransform.localRotation = Quaternion.Euler(Mathf.Lerp(m_TriggerXAxisRotationRange.x, m_TriggerXAxisRotationRange.y, triggerVal), 0f, 0f);
|
||||
}
|
||||
|
||||
if (m_GripInput != null)
|
||||
{
|
||||
var gripVal = m_GripInput.ReadValue();
|
||||
var currentPos = m_GripTransform.localPosition;
|
||||
m_GripTransform.localPosition = new Vector3(Mathf.Lerp(m_GripRightRange.x, m_GripRightRange.y, gripVal), currentPos.y, currentPos.z);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 4a5f76f9ea8c80547973ab01877f9567
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,530 @@
|
||||
using System.Collections.Generic;
|
||||
using Unity.XR.CoreUtils.Bindings;
|
||||
using UnityEngine.Events;
|
||||
using UnityEngine.InputSystem;
|
||||
using UnityEngine.Serialization;
|
||||
using UnityEngine.XR.Interaction.Toolkit.Attachment;
|
||||
using UnityEngine.XR.Interaction.Toolkit.Inputs.Readers;
|
||||
using UnityEngine.XR.Interaction.Toolkit.Interactors;
|
||||
using UnityEngine.XR.Interaction.Toolkit.UI;
|
||||
|
||||
namespace UnityEngine.XR.Interaction.Toolkit.Samples.StarterAssets
|
||||
{
|
||||
/// <summary>
|
||||
/// Use this class to mediate the interactors for a controller under different interaction states
|
||||
/// and the input actions used by them.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// If the teleport ray input is engaged, the Ray Interactor used for distant manipulation is disabled
|
||||
/// and the Ray Interactor used for teleportation is enabled. If the Ray Interactor is selecting and it
|
||||
/// is configured to allow for attach transform manipulation, all locomotion input actions are disabled
|
||||
/// (teleport ray, move, and turn controls) to prevent input collision with the manipulation inputs used
|
||||
/// by the ray interactor.
|
||||
/// <br />
|
||||
/// A typical hierarchy also includes an XR Interaction Group component to mediate between interactors.
|
||||
/// The interaction group ensures that the Direct and Ray Interactors cannot interact at the same time,
|
||||
/// with the Direct Interactor taking priority over the Ray Interactor.
|
||||
/// </remarks>
|
||||
[AddComponentMenu("XR/Controller Input Action Manager")]
|
||||
public class ControllerInputActionManager : MonoBehaviour
|
||||
{
|
||||
[Space]
|
||||
[Header("Interactors")]
|
||||
|
||||
[SerializeField]
|
||||
[Tooltip("The interactor used for distant/ray manipulation. Use this or Near-Far Interactor, not both.")]
|
||||
XRRayInteractor m_RayInteractor;
|
||||
|
||||
[SerializeField]
|
||||
[Tooltip("Near-Far Interactor used for distant/ray manipulation. Use this or Ray Interactor, not both.")]
|
||||
NearFarInteractor m_NearFarInteractor;
|
||||
|
||||
[SerializeField]
|
||||
[Tooltip("The interactor used for teleportation.")]
|
||||
XRRayInteractor m_TeleportInteractor;
|
||||
|
||||
[Space]
|
||||
[Header("Controller Actions")]
|
||||
|
||||
[SerializeField]
|
||||
[Tooltip("The reference to the action to start the teleport aiming mode for this controller.")]
|
||||
[FormerlySerializedAs("m_TeleportModeActivate")]
|
||||
InputActionReference m_TeleportMode;
|
||||
|
||||
[SerializeField]
|
||||
[Tooltip("The reference to the action to cancel the teleport aiming mode for this controller.")]
|
||||
InputActionReference m_TeleportModeCancel;
|
||||
|
||||
[SerializeField]
|
||||
[Tooltip("The reference to the action of continuous turning the XR Origin with this controller.")]
|
||||
InputActionReference m_Turn;
|
||||
|
||||
[SerializeField]
|
||||
[Tooltip("The reference to the action of snap turning the XR Origin with this controller.")]
|
||||
InputActionReference m_SnapTurn;
|
||||
|
||||
[SerializeField]
|
||||
[Tooltip("The reference to the action of moving the XR Origin with this controller.")]
|
||||
InputActionReference m_Move;
|
||||
|
||||
[SerializeField]
|
||||
[Tooltip("The reference to the action of scrolling UI with this controller.")]
|
||||
InputActionReference m_UIScroll;
|
||||
|
||||
[Space]
|
||||
[Header("Locomotion Settings")]
|
||||
|
||||
[SerializeField]
|
||||
[Tooltip("If true, continuous movement will be enabled. If false, teleport will be enabled.")]
|
||||
bool m_SmoothMotionEnabled;
|
||||
|
||||
[SerializeField]
|
||||
[Tooltip("If true, continuous turn will be enabled. If false, snap turn will be enabled. Note: If smooth motion is enabled and enable strafe is enabled on the continuous move provider, turn will be overriden in favor of strafe.")]
|
||||
bool m_SmoothTurnEnabled;
|
||||
|
||||
[SerializeField]
|
||||
[Tooltip("With the Near-Far Interactor, if true, teleport will be enabled during near interaction. If false, teleport will be disabled during near interaction.")]
|
||||
bool m_NearFarEnableTeleportDuringNearInteraction = true;
|
||||
|
||||
[Space]
|
||||
[Header("UI Settings")]
|
||||
|
||||
[SerializeField]
|
||||
[Tooltip("If true, UI scrolling will be enabled. Locomotion will be disabled when pointing at UI to allow it to be scrolled.")]
|
||||
bool m_UIScrollingEnabled = true;
|
||||
|
||||
[Space]
|
||||
[Header("Mediation Events")]
|
||||
|
||||
[SerializeField]
|
||||
[Tooltip("Event fired when the active ray interactor changes between interaction and teleport.")]
|
||||
UnityEvent<IXRRayProvider> m_RayInteractorChanged;
|
||||
|
||||
public bool smoothMotionEnabled
|
||||
{
|
||||
get => m_SmoothMotionEnabled;
|
||||
set
|
||||
{
|
||||
m_SmoothMotionEnabled = value;
|
||||
UpdateLocomotionActions();
|
||||
}
|
||||
}
|
||||
|
||||
public bool smoothTurnEnabled
|
||||
{
|
||||
get => m_SmoothTurnEnabled;
|
||||
set
|
||||
{
|
||||
m_SmoothTurnEnabled = value;
|
||||
UpdateLocomotionActions();
|
||||
}
|
||||
}
|
||||
|
||||
public bool uiScrollingEnabled
|
||||
{
|
||||
get => m_UIScrollingEnabled;
|
||||
set
|
||||
{
|
||||
m_UIScrollingEnabled = value;
|
||||
UpdateUIActions();
|
||||
}
|
||||
}
|
||||
|
||||
bool m_StartCalled;
|
||||
bool m_PostponedDeactivateTeleport;
|
||||
bool m_PostponedNearRegionLocomotion;
|
||||
bool m_HoveringScrollableUI;
|
||||
|
||||
readonly HashSet<InputAction> m_LocomotionUsers = new HashSet<InputAction>();
|
||||
readonly BindingsGroup m_BindingsGroup = new BindingsGroup();
|
||||
|
||||
void SetupInteractorEvents()
|
||||
{
|
||||
if (m_NearFarInteractor != null)
|
||||
{
|
||||
m_NearFarInteractor.uiHoverEntered.AddListener(OnUIHoverEntered);
|
||||
m_NearFarInteractor.uiHoverExited.AddListener(OnUIHoverExited);
|
||||
m_BindingsGroup.AddBinding(m_NearFarInteractor.selectionRegion.Subscribe(OnNearFarSelectionRegionChanged));
|
||||
}
|
||||
|
||||
if (m_RayInteractor != null)
|
||||
{
|
||||
m_RayInteractor.selectEntered.AddListener(OnRaySelectEntered);
|
||||
m_RayInteractor.selectExited.AddListener(OnRaySelectExited);
|
||||
m_RayInteractor.uiHoverEntered.AddListener(OnUIHoverEntered);
|
||||
m_RayInteractor.uiHoverExited.AddListener(OnUIHoverExited);
|
||||
}
|
||||
|
||||
var teleportModeAction = GetInputAction(m_TeleportMode);
|
||||
if (teleportModeAction != null)
|
||||
{
|
||||
teleportModeAction.performed += OnStartTeleport;
|
||||
teleportModeAction.performed += OnStartLocomotion;
|
||||
teleportModeAction.canceled += OnCancelTeleport;
|
||||
teleportModeAction.canceled += OnStopLocomotion;
|
||||
}
|
||||
|
||||
var teleportModeCancelAction = GetInputAction(m_TeleportModeCancel);
|
||||
if (teleportModeCancelAction != null)
|
||||
{
|
||||
teleportModeCancelAction.performed += OnCancelTeleport;
|
||||
}
|
||||
|
||||
var moveAction = GetInputAction(m_Move);
|
||||
if (moveAction != null)
|
||||
{
|
||||
moveAction.started += OnStartLocomotion;
|
||||
moveAction.canceled += OnStopLocomotion;
|
||||
}
|
||||
|
||||
var turnAction = GetInputAction(m_Turn);
|
||||
if (turnAction != null)
|
||||
{
|
||||
turnAction.started += OnStartLocomotion;
|
||||
turnAction.canceled += OnStopLocomotion;
|
||||
}
|
||||
|
||||
var snapTurnAction = GetInputAction(m_SnapTurn);
|
||||
if (snapTurnAction != null)
|
||||
{
|
||||
snapTurnAction.started += OnStartLocomotion;
|
||||
snapTurnAction.canceled += OnStopLocomotion;
|
||||
}
|
||||
}
|
||||
|
||||
void TeardownInteractorEvents()
|
||||
{
|
||||
m_BindingsGroup.Clear();
|
||||
|
||||
if (m_NearFarInteractor != null)
|
||||
{
|
||||
m_NearFarInteractor.uiHoverEntered.RemoveListener(OnUIHoverEntered);
|
||||
m_NearFarInteractor.uiHoverExited.RemoveListener(OnUIHoverExited);
|
||||
}
|
||||
|
||||
if (m_RayInteractor != null)
|
||||
{
|
||||
m_RayInteractor.selectEntered.RemoveListener(OnRaySelectEntered);
|
||||
m_RayInteractor.selectExited.RemoveListener(OnRaySelectExited);
|
||||
m_RayInteractor.uiHoverEntered.RemoveListener(OnUIHoverEntered);
|
||||
m_RayInteractor.uiHoverExited.RemoveListener(OnUIHoverExited);
|
||||
}
|
||||
|
||||
var teleportModeAction = GetInputAction(m_TeleportMode);
|
||||
if (teleportModeAction != null)
|
||||
{
|
||||
teleportModeAction.performed -= OnStartTeleport;
|
||||
teleportModeAction.performed -= OnStartLocomotion;
|
||||
teleportModeAction.canceled -= OnCancelTeleport;
|
||||
teleportModeAction.canceled -= OnStopLocomotion;
|
||||
}
|
||||
|
||||
var teleportModeCancelAction = GetInputAction(m_TeleportModeCancel);
|
||||
if (teleportModeCancelAction != null)
|
||||
{
|
||||
teleportModeCancelAction.performed -= OnCancelTeleport;
|
||||
}
|
||||
|
||||
var moveAction = GetInputAction(m_Move);
|
||||
if (moveAction != null)
|
||||
{
|
||||
moveAction.started -= OnStartLocomotion;
|
||||
moveAction.canceled -= OnStopLocomotion;
|
||||
}
|
||||
|
||||
var turnAction = GetInputAction(m_Turn);
|
||||
if (turnAction != null)
|
||||
{
|
||||
turnAction.started -= OnStartLocomotion;
|
||||
turnAction.canceled -= OnStopLocomotion;
|
||||
}
|
||||
|
||||
var snapTurnAction = GetInputAction(m_SnapTurn);
|
||||
if (snapTurnAction != null)
|
||||
{
|
||||
snapTurnAction.started -= OnStartLocomotion;
|
||||
snapTurnAction.canceled -= OnStopLocomotion;
|
||||
}
|
||||
}
|
||||
|
||||
void OnStartTeleport(InputAction.CallbackContext context)
|
||||
{
|
||||
m_PostponedDeactivateTeleport = false;
|
||||
|
||||
if (m_TeleportInteractor != null)
|
||||
m_TeleportInteractor.gameObject.SetActive(true);
|
||||
|
||||
if (m_RayInteractor != null)
|
||||
m_RayInteractor.gameObject.SetActive(false);
|
||||
|
||||
if (m_NearFarInteractor != null && m_NearFarInteractor.selectionRegion.Value != NearFarInteractor.Region.Near)
|
||||
m_NearFarInteractor.gameObject.SetActive(false);
|
||||
|
||||
m_RayInteractorChanged?.Invoke(m_TeleportInteractor);
|
||||
}
|
||||
|
||||
void OnCancelTeleport(InputAction.CallbackContext context)
|
||||
{
|
||||
// Do not deactivate the teleport interactor in this callback.
|
||||
// We delay turning off the teleport interactor in this callback so that
|
||||
// the teleport interactor has a chance to complete the teleport if needed.
|
||||
// OnAfterInteractionEvents will handle deactivating its GameObject.
|
||||
m_PostponedDeactivateTeleport = true;
|
||||
|
||||
if (m_RayInteractor != null)
|
||||
m_RayInteractor.gameObject.SetActive(true);
|
||||
|
||||
if (m_NearFarInteractor != null)
|
||||
m_NearFarInteractor.gameObject.SetActive(true);
|
||||
|
||||
m_RayInteractorChanged?.Invoke(m_RayInteractor);
|
||||
}
|
||||
|
||||
void OnStartLocomotion(InputAction.CallbackContext context)
|
||||
{
|
||||
m_LocomotionUsers.Add(context.action);
|
||||
}
|
||||
|
||||
void OnStopLocomotion(InputAction.CallbackContext context)
|
||||
{
|
||||
m_LocomotionUsers.Remove(context.action);
|
||||
|
||||
if (m_LocomotionUsers.Count == 0 && m_HoveringScrollableUI)
|
||||
{
|
||||
DisableAllLocomotionActions();
|
||||
UpdateUIActions();
|
||||
}
|
||||
}
|
||||
|
||||
void OnNearFarSelectionRegionChanged(NearFarInteractor.Region selectionRegion)
|
||||
{
|
||||
m_PostponedNearRegionLocomotion = false;
|
||||
|
||||
if (selectionRegion == NearFarInteractor.Region.None)
|
||||
{
|
||||
UpdateLocomotionActions();
|
||||
return;
|
||||
}
|
||||
|
||||
var manipulateAttachTransform = false;
|
||||
var attachController = m_NearFarInteractor.interactionAttachController as InteractionAttachController;
|
||||
if (attachController != null)
|
||||
{
|
||||
manipulateAttachTransform = attachController.useManipulationInput &&
|
||||
(attachController.manipulationInput.inputSourceMode == XRInputValueReader.InputSourceMode.InputActionReference && attachController.manipulationInput.inputActionReference != null) ||
|
||||
(attachController.manipulationInput.inputSourceMode != XRInputValueReader.InputSourceMode.InputActionReference && attachController.manipulationInput.inputSourceMode != XRInputValueReader.InputSourceMode.Unused);
|
||||
}
|
||||
|
||||
if (selectionRegion == NearFarInteractor.Region.Far)
|
||||
{
|
||||
if (manipulateAttachTransform)
|
||||
DisableAllLocomotionActions();
|
||||
else
|
||||
DisableTeleportActions();
|
||||
}
|
||||
else if (selectionRegion == NearFarInteractor.Region.Near)
|
||||
{
|
||||
// Determine if the user entered the near region due to pulling back on the thumbstick.
|
||||
// If so, postpone enabling locomotion until the user releases the thumbstick
|
||||
// in order to avoid an immediate snap turn around from triggering on region change.
|
||||
var hasStickInput = manipulateAttachTransform && HasStickInput(attachController);
|
||||
if (hasStickInput)
|
||||
{
|
||||
m_PostponedNearRegionLocomotion = true;
|
||||
DisableAllLocomotionActions();
|
||||
}
|
||||
else
|
||||
{
|
||||
UpdateLocomotionActions();
|
||||
if (!m_NearFarEnableTeleportDuringNearInteraction)
|
||||
DisableTeleportActions();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void OnRaySelectEntered(SelectEnterEventArgs args)
|
||||
{
|
||||
if (m_RayInteractor.manipulateAttachTransform)
|
||||
{
|
||||
// Disable locomotion and turn actions
|
||||
DisableAllLocomotionActions();
|
||||
}
|
||||
}
|
||||
|
||||
void OnRaySelectExited(SelectExitEventArgs args)
|
||||
{
|
||||
if (m_RayInteractor.manipulateAttachTransform)
|
||||
{
|
||||
// Re-enable the locomotion and turn actions
|
||||
UpdateLocomotionActions();
|
||||
}
|
||||
}
|
||||
|
||||
void OnUIHoverEntered(UIHoverEventArgs args)
|
||||
{
|
||||
m_HoveringScrollableUI = m_UIScrollingEnabled && args.deviceModel.isScrollable;
|
||||
UpdateUIActions();
|
||||
|
||||
// If locomotion is occurring, wait
|
||||
if (m_HoveringScrollableUI && m_LocomotionUsers.Count == 0)
|
||||
{
|
||||
// Disable locomotion and turn actions
|
||||
DisableAllLocomotionActions();
|
||||
}
|
||||
}
|
||||
|
||||
void OnUIHoverExited(UIHoverEventArgs args)
|
||||
{
|
||||
m_HoveringScrollableUI = false;
|
||||
UpdateUIActions();
|
||||
|
||||
// Re-enable the locomotion and turn actions
|
||||
UpdateLocomotionActions();
|
||||
}
|
||||
|
||||
protected void OnEnable()
|
||||
{
|
||||
if (m_RayInteractor != null && m_NearFarInteractor != null)
|
||||
{
|
||||
Debug.LogWarning("Both Ray Interactor and Near-Far Interactor are assigned. Only one should be assigned, not both. Clearing Ray Interactor.", this);
|
||||
m_RayInteractor = null;
|
||||
}
|
||||
|
||||
if (m_TeleportInteractor != null)
|
||||
m_TeleportInteractor.gameObject.SetActive(false);
|
||||
|
||||
// Allow the actions to be refreshed when this component is re-enabled.
|
||||
// See comments in Start for why we wait until Start to enable/disable actions.
|
||||
if (m_StartCalled)
|
||||
{
|
||||
UpdateLocomotionActions();
|
||||
UpdateUIActions();
|
||||
}
|
||||
|
||||
SetupInteractorEvents();
|
||||
}
|
||||
|
||||
protected void OnDisable()
|
||||
{
|
||||
TeardownInteractorEvents();
|
||||
}
|
||||
|
||||
protected void Start()
|
||||
{
|
||||
m_StartCalled = true;
|
||||
|
||||
// Ensure the enabled state of locomotion and turn actions are properly set up.
|
||||
// Called in Start so it is done after the InputActionManager enables all input actions earlier in OnEnable.
|
||||
UpdateLocomotionActions();
|
||||
UpdateUIActions();
|
||||
}
|
||||
|
||||
protected void Update()
|
||||
{
|
||||
// Since this behavior has the default execution order, it runs after the XRInteractionManager,
|
||||
// so selection events have been finished by now this frame. This means that the teleport interactor
|
||||
// has had a chance to process its select interaction event and teleport if needed.
|
||||
if (m_PostponedDeactivateTeleport)
|
||||
{
|
||||
if (m_TeleportInteractor != null)
|
||||
m_TeleportInteractor.gameObject.SetActive(false);
|
||||
|
||||
m_PostponedDeactivateTeleport = false;
|
||||
}
|
||||
|
||||
// If stick input caused the near region to be entered,
|
||||
// wait until the stick is released before enabling locomotion.
|
||||
if (m_PostponedNearRegionLocomotion)
|
||||
{
|
||||
var hasStickInput = false;
|
||||
if (m_NearFarInteractor != null &&
|
||||
m_NearFarInteractor.interactionAttachController is InteractionAttachController attachController
|
||||
&& attachController != null)
|
||||
{
|
||||
hasStickInput = HasStickInput(attachController);
|
||||
}
|
||||
|
||||
if (!hasStickInput)
|
||||
{
|
||||
m_PostponedNearRegionLocomotion = false;
|
||||
|
||||
UpdateLocomotionActions();
|
||||
if (!m_NearFarEnableTeleportDuringNearInteraction)
|
||||
DisableTeleportActions();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void UpdateLocomotionActions()
|
||||
{
|
||||
// Disable/enable Teleport and Turn when Move is enabled/disabled.
|
||||
SetEnabled(m_Move, m_SmoothMotionEnabled);
|
||||
SetEnabled(m_TeleportMode, !m_SmoothMotionEnabled);
|
||||
SetEnabled(m_TeleportModeCancel, !m_SmoothMotionEnabled);
|
||||
|
||||
// Disable ability to turn when using continuous movement
|
||||
SetEnabled(m_Turn, !m_SmoothMotionEnabled && m_SmoothTurnEnabled);
|
||||
SetEnabled(m_SnapTurn, !m_SmoothMotionEnabled && !m_SmoothTurnEnabled);
|
||||
}
|
||||
|
||||
void DisableTeleportActions()
|
||||
{
|
||||
DisableAction(m_TeleportMode);
|
||||
DisableAction(m_TeleportModeCancel);
|
||||
}
|
||||
|
||||
void DisableMoveAndTurnActions()
|
||||
{
|
||||
DisableAction(m_Move);
|
||||
DisableAction(m_Turn);
|
||||
DisableAction(m_SnapTurn);
|
||||
}
|
||||
|
||||
void DisableAllLocomotionActions()
|
||||
{
|
||||
DisableTeleportActions();
|
||||
DisableMoveAndTurnActions();
|
||||
}
|
||||
|
||||
void UpdateUIActions()
|
||||
{
|
||||
SetEnabled(m_UIScroll, m_UIScrollingEnabled && m_HoveringScrollableUI && m_LocomotionUsers.Count == 0);
|
||||
}
|
||||
|
||||
static bool HasStickInput(InteractionAttachController attachController)
|
||||
{
|
||||
// 75% of default 0.5 press threshold
|
||||
const float sqrStickReleaseThreshold = 0.375f * 0.375f;
|
||||
|
||||
return attachController.manipulationInput.TryReadValue(out var stickInput) &&
|
||||
stickInput.sqrMagnitude > sqrStickReleaseThreshold;
|
||||
}
|
||||
|
||||
static void SetEnabled(InputActionReference actionReference, bool enabled)
|
||||
{
|
||||
if (enabled)
|
||||
EnableAction(actionReference);
|
||||
else
|
||||
DisableAction(actionReference);
|
||||
}
|
||||
|
||||
static void EnableAction(InputActionReference actionReference)
|
||||
{
|
||||
var action = GetInputAction(actionReference);
|
||||
action?.Enable();
|
||||
}
|
||||
|
||||
static void DisableAction(InputActionReference actionReference)
|
||||
{
|
||||
var action = GetInputAction(actionReference);
|
||||
action?.Disable();
|
||||
}
|
||||
|
||||
static InputAction GetInputAction(InputActionReference actionReference)
|
||||
{
|
||||
#pragma warning disable IDE0031 // Use null propagation -- Do not use for UnityEngine.Object types
|
||||
return actionReference != null ? actionReference.action : null;
|
||||
#pragma warning restore IDE0031
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: f9ac216f0eb04754b1d938aac6380b31
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,29 @@
|
||||
namespace UnityEngine.XR.Interaction.Toolkit.Samples.StarterAssets
|
||||
{
|
||||
/// <summary>
|
||||
/// Destroys the GameObject it is attached to after a specified amount of time.
|
||||
/// </summary>
|
||||
public class DestroySelf : MonoBehaviour
|
||||
{
|
||||
[SerializeField]
|
||||
[Tooltip("The amount of time, in seconds, to wait after Start before destroying the GameObject.")]
|
||||
float m_Lifetime = 0.25f;
|
||||
|
||||
/// <summary>
|
||||
/// The amount of time, in seconds, to wait after Start before destroying the GameObject.
|
||||
/// </summary>
|
||||
public float lifetime
|
||||
{
|
||||
get => m_Lifetime;
|
||||
set => m_Lifetime = value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// See <see cref="MonoBehaviour"/>.
|
||||
/// </summary>
|
||||
void Start()
|
||||
{
|
||||
Destroy(gameObject, m_Lifetime);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 717c12e2a4cfe764ab2580b1135e10fd
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,190 @@
|
||||
using Unity.XR.CoreUtils;
|
||||
using UnityEngine.Assertions;
|
||||
using UnityEngine.XR.Interaction.Toolkit.Locomotion.Movement;
|
||||
|
||||
namespace UnityEngine.XR.Interaction.Toolkit.Samples.StarterAssets
|
||||
{
|
||||
/// <summary>
|
||||
/// A version of continuous movement that automatically controls the frame of reference that
|
||||
/// determines the forward direction of movement based on user preference for each hand.
|
||||
/// For example, can configure to use head relative movement for the left hand and controller relative movement for the right hand.
|
||||
/// </summary>
|
||||
public class DynamicMoveProvider : ContinuousMoveProvider
|
||||
{
|
||||
/// <summary>
|
||||
/// Defines which transform the XR Origin's movement direction is relative to.
|
||||
/// </summary>
|
||||
/// <seealso cref="leftHandMovementDirection"/>
|
||||
/// <seealso cref="rightHandMovementDirection"/>
|
||||
public enum MovementDirection
|
||||
{
|
||||
/// <summary>
|
||||
/// Use the forward direction of the head (camera) as the forward direction of the XR Origin's movement.
|
||||
/// </summary>
|
||||
HeadRelative,
|
||||
|
||||
/// <summary>
|
||||
/// Use the forward direction of the hand (controller) as the forward direction of the XR Origin's movement.
|
||||
/// </summary>
|
||||
HandRelative,
|
||||
}
|
||||
|
||||
[Space, Header("Movement Direction")]
|
||||
[SerializeField]
|
||||
[Tooltip("Directs the XR Origin's movement when using the head-relative mode. If not set, will automatically find and use the XR Origin Camera.")]
|
||||
Transform m_HeadTransform;
|
||||
|
||||
/// <summary>
|
||||
/// Directs the XR Origin's movement when using the head-relative mode. If not set, will automatically find and use the XR Origin Camera.
|
||||
/// </summary>
|
||||
public Transform headTransform
|
||||
{
|
||||
get => m_HeadTransform;
|
||||
set => m_HeadTransform = value;
|
||||
}
|
||||
|
||||
[SerializeField]
|
||||
[Tooltip("Directs the XR Origin's movement when using the hand-relative mode with the left hand.")]
|
||||
Transform m_LeftControllerTransform;
|
||||
|
||||
/// <summary>
|
||||
/// Directs the XR Origin's movement when using the hand-relative mode with the left hand.
|
||||
/// </summary>
|
||||
public Transform leftControllerTransform
|
||||
{
|
||||
get => m_LeftControllerTransform;
|
||||
set => m_LeftControllerTransform = value;
|
||||
}
|
||||
|
||||
[SerializeField]
|
||||
[Tooltip("Directs the XR Origin's movement when using the hand-relative mode with the right hand.")]
|
||||
Transform m_RightControllerTransform;
|
||||
|
||||
public Transform rightControllerTransform
|
||||
{
|
||||
get => m_RightControllerTransform;
|
||||
set => m_RightControllerTransform = value;
|
||||
}
|
||||
|
||||
[SerializeField]
|
||||
[Tooltip("Whether to use the specified head transform or left controller transform to direct the XR Origin's movement for the left hand.")]
|
||||
MovementDirection m_LeftHandMovementDirection;
|
||||
|
||||
/// <summary>
|
||||
/// Whether to use the specified head transform or controller transform to direct the XR Origin's movement for the left hand.
|
||||
/// </summary>
|
||||
/// <seealso cref="MovementDirection"/>
|
||||
public MovementDirection leftHandMovementDirection
|
||||
{
|
||||
get => m_LeftHandMovementDirection;
|
||||
set => m_LeftHandMovementDirection = value;
|
||||
}
|
||||
|
||||
[SerializeField]
|
||||
[Tooltip("Whether to use the specified head transform or right controller transform to direct the XR Origin's movement for the right hand.")]
|
||||
MovementDirection m_RightHandMovementDirection;
|
||||
|
||||
/// <summary>
|
||||
/// Whether to use the specified head transform or controller transform to direct the XR Origin's movement for the right hand.
|
||||
/// </summary>
|
||||
/// <seealso cref="MovementDirection"/>
|
||||
public MovementDirection rightHandMovementDirection
|
||||
{
|
||||
get => m_RightHandMovementDirection;
|
||||
set => m_RightHandMovementDirection = value;
|
||||
}
|
||||
|
||||
Transform m_CombinedTransform;
|
||||
Pose m_LeftMovementPose = Pose.identity;
|
||||
Pose m_RightMovementPose = Pose.identity;
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Awake()
|
||||
{
|
||||
base.Awake();
|
||||
|
||||
m_CombinedTransform = new GameObject("[Dynamic Move Provider] Combined Forward Source").transform;
|
||||
m_CombinedTransform.SetParent(transform, false);
|
||||
m_CombinedTransform.localPosition = Vector3.zero;
|
||||
m_CombinedTransform.localRotation = Quaternion.identity;
|
||||
|
||||
forwardSource = m_CombinedTransform;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override Vector3 ComputeDesiredMove(Vector2 input)
|
||||
{
|
||||
// Don't need to do anything if the total input is zero.
|
||||
// This is the same check as the base method.
|
||||
if (input == Vector2.zero)
|
||||
return base.ComputeDesiredMove(input);
|
||||
|
||||
// Initialize the Head Transform if necessary, getting the Camera from XR Origin
|
||||
if (m_HeadTransform == null)
|
||||
{
|
||||
var xrOrigin = mediator.xrOrigin;
|
||||
if (xrOrigin != null)
|
||||
{
|
||||
var xrCamera = xrOrigin.Camera;
|
||||
if (xrCamera != null)
|
||||
m_HeadTransform = xrCamera.transform;
|
||||
}
|
||||
}
|
||||
|
||||
// Get the forward source for the left hand input
|
||||
switch (m_LeftHandMovementDirection)
|
||||
{
|
||||
case MovementDirection.HeadRelative:
|
||||
if (m_HeadTransform != null)
|
||||
m_LeftMovementPose = m_HeadTransform.GetWorldPose();
|
||||
|
||||
break;
|
||||
|
||||
case MovementDirection.HandRelative:
|
||||
if (m_LeftControllerTransform != null)
|
||||
m_LeftMovementPose = m_LeftControllerTransform.GetWorldPose();
|
||||
|
||||
break;
|
||||
|
||||
default:
|
||||
Assert.IsTrue(false, $"Unhandled {nameof(MovementDirection)}={m_LeftHandMovementDirection}");
|
||||
break;
|
||||
}
|
||||
|
||||
// Get the forward source for the right hand input
|
||||
switch (m_RightHandMovementDirection)
|
||||
{
|
||||
case MovementDirection.HeadRelative:
|
||||
if (m_HeadTransform != null)
|
||||
m_RightMovementPose = m_HeadTransform.GetWorldPose();
|
||||
|
||||
break;
|
||||
|
||||
case MovementDirection.HandRelative:
|
||||
if (m_RightControllerTransform != null)
|
||||
m_RightMovementPose = m_RightControllerTransform.GetWorldPose();
|
||||
|
||||
break;
|
||||
|
||||
default:
|
||||
Assert.IsTrue(false, $"Unhandled {nameof(MovementDirection)}={m_RightHandMovementDirection}");
|
||||
break;
|
||||
}
|
||||
|
||||
// Combine the two poses into the forward source based on the magnitude of input
|
||||
var leftHandValue = leftHandMoveInput.ReadValue();
|
||||
var rightHandValue = rightHandMoveInput.ReadValue();
|
||||
|
||||
var totalSqrMagnitude = leftHandValue.sqrMagnitude + rightHandValue.sqrMagnitude;
|
||||
var leftHandBlend = 0.5f;
|
||||
if (totalSqrMagnitude > Mathf.Epsilon)
|
||||
leftHandBlend = leftHandValue.sqrMagnitude / totalSqrMagnitude;
|
||||
|
||||
var combinedPosition = Vector3.Lerp(m_RightMovementPose.position, m_LeftMovementPose.position, leftHandBlend);
|
||||
var combinedRotation = Quaternion.Slerp(m_RightMovementPose.rotation, m_LeftMovementPose.rotation, leftHandBlend);
|
||||
m_CombinedTransform.SetPositionAndRotation(combinedPosition, combinedRotation);
|
||||
|
||||
return base.ComputeDesiredMove(input);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 9b1e8c997df241c1a67045eeac79b41b
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,95 @@
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine.InputSystem;
|
||||
|
||||
namespace UnityEngine.XR.Interaction.Toolkit.Samples.StarterAssets
|
||||
{
|
||||
/// <summary>
|
||||
/// Manages input fallback for <see cref="XRGazeInteractor"/> when eye tracking is not available.
|
||||
/// </summary>
|
||||
public class GazeInputManager : MonoBehaviour
|
||||
{
|
||||
// This is the name of the layout that is registered by EyeGazeInteraction in the OpenXR Plugin package
|
||||
const string k_EyeGazeLayoutName = "EyeGaze";
|
||||
|
||||
[SerializeField]
|
||||
[Tooltip("Enable fallback to head tracking if eye tracking is unavailable.")]
|
||||
bool m_FallbackIfEyeTrackingUnavailable = true;
|
||||
|
||||
/// <summary>
|
||||
/// Enable fallback to head tracking if eye tracking is unavailable.
|
||||
/// </summary>
|
||||
public bool fallbackIfEyeTrackingUnavailable
|
||||
{
|
||||
get => m_FallbackIfEyeTrackingUnavailable;
|
||||
set => m_FallbackIfEyeTrackingUnavailable = value;
|
||||
}
|
||||
|
||||
|
||||
bool m_EyeTrackingDeviceFound;
|
||||
|
||||
/// <summary>
|
||||
/// See <see cref="MonoBehaviour"/>.
|
||||
/// </summary>
|
||||
protected void Awake()
|
||||
{
|
||||
// Check if we have eye tracking support
|
||||
var inputDeviceList = new List<InputDevice>();
|
||||
InputDevices.GetDevicesWithCharacteristics(InputDeviceCharacteristics.EyeTracking, inputDeviceList);
|
||||
if (inputDeviceList.Count > 0)
|
||||
{
|
||||
Debug.Log("Eye tracking device found!", this);
|
||||
m_EyeTrackingDeviceFound = true;
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (var device in InputSystem.InputSystem.devices)
|
||||
{
|
||||
if (device.layout == k_EyeGazeLayoutName)
|
||||
{
|
||||
Debug.Log("Eye gaze device found!", this);
|
||||
m_EyeTrackingDeviceFound = true;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
Debug.LogWarning($"Could not find a device that supports eye tracking on Awake. {this} has subscribed to device connected events and will activate the GameObject when an eye tracking device is connected.", this);
|
||||
|
||||
InputDevices.deviceConnected += OnDeviceConnected;
|
||||
InputSystem.InputSystem.onDeviceChange += OnDeviceChange;
|
||||
|
||||
gameObject.SetActive(m_FallbackIfEyeTrackingUnavailable);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// See <see cref="MonoBehaviour"/>.
|
||||
/// </summary>
|
||||
protected void OnDestroy()
|
||||
{
|
||||
InputDevices.deviceConnected -= OnDeviceConnected;
|
||||
InputSystem.InputSystem.onDeviceChange -= OnDeviceChange;
|
||||
}
|
||||
|
||||
void OnDeviceConnected(InputDevice inputDevice)
|
||||
{
|
||||
if (m_EyeTrackingDeviceFound || !inputDevice.characteristics.HasFlag(InputDeviceCharacteristics.EyeTracking))
|
||||
return;
|
||||
|
||||
Debug.Log("Eye tracking device found!", this);
|
||||
m_EyeTrackingDeviceFound = true;
|
||||
gameObject.SetActive(true);
|
||||
}
|
||||
|
||||
void OnDeviceChange(InputSystem.InputDevice device, InputDeviceChange change)
|
||||
{
|
||||
if (m_EyeTrackingDeviceFound || change != InputDeviceChange.Added)
|
||||
return;
|
||||
|
||||
if (device.layout == k_EyeGazeLayoutName)
|
||||
{
|
||||
Debug.Log("Eye gaze device found!", this);
|
||||
m_EyeTrackingDeviceFound = true;
|
||||
gameObject.SetActive(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 6ef0e4723b64c884699a375196c13ac0
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,242 @@
|
||||
using UnityEngine.Rendering;
|
||||
using System.Collections.Generic;
|
||||
#if UNITY_EDITOR
|
||||
using UnityEditor;
|
||||
#endif
|
||||
|
||||
namespace UnityEngine.XR.Interaction.Toolkit.Samples.StarterAssets
|
||||
{
|
||||
#if UNITY_EDITOR
|
||||
[InitializeOnLoad]
|
||||
static class RenderPipelineValidation
|
||||
{
|
||||
static RenderPipelineValidation()
|
||||
{
|
||||
foreach (var pipelineHandler in GetAllInstances())
|
||||
pipelineHandler.AutoRefreshPipelineShaders();
|
||||
}
|
||||
|
||||
static List<MaterialPipelineHandler> GetAllInstances()
|
||||
{
|
||||
var instances = new List<MaterialPipelineHandler>();
|
||||
|
||||
// Find all GUIDs for objects that match the type MaterialPipelineHandler
|
||||
var guids = AssetDatabase.FindAssets("t:MaterialPipelineHandler");
|
||||
for (int i = 0; i < guids.Length; i++)
|
||||
{
|
||||
string path = AssetDatabase.GUIDToAssetPath(guids[i]);
|
||||
var asset = AssetDatabase.LoadAssetAtPath<MaterialPipelineHandler>(path);
|
||||
if (asset != null)
|
||||
instances.Add(asset);
|
||||
}
|
||||
|
||||
return instances;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
/// <summary>
|
||||
/// Serializable class that contains the shader information for a material.
|
||||
/// </summary>
|
||||
[System.Serializable]
|
||||
public class ShaderContainer
|
||||
{
|
||||
public Material material;
|
||||
public bool useSRPShaderName = true;
|
||||
public string scriptableRenderPipelineShaderName = "Universal Render Pipeline/Lit";
|
||||
public Shader scriptableRenderPipelineShader;
|
||||
public bool useBuiltinShaderName = true;
|
||||
public string builtInPipelineShaderName = "Standard";
|
||||
public Shader builtInPipelineShader;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Scriptable object that allows for setting the shader on a material based on the current render pipeline.
|
||||
/// Will run automatically OnEnable in the editor to set the shaders on project bootup. Can be refreshed manually with editor button.
|
||||
/// This exists because while objects render correctly using shadergraph shaders, others do not and using the standard shader resolves various rendering issues.
|
||||
/// </summary>
|
||||
[CreateAssetMenu(fileName = "MaterialPipelineHandler", menuName = "XR/MaterialPipelineHandler", order = 0)]
|
||||
public class MaterialPipelineHandler : ScriptableObject
|
||||
{
|
||||
[SerializeField]
|
||||
[Tooltip("List of materials and their associated shaders.")]
|
||||
List<ShaderContainer> m_ShaderContainers;
|
||||
|
||||
[SerializeField]
|
||||
[Tooltip("If true, the shaders will be refreshed automatically when the editor opens and when this scriptable object instance is enabled.")]
|
||||
bool m_AutoRefreshShaders = true;
|
||||
|
||||
#if UNITY_EDITOR
|
||||
void OnEnable()
|
||||
{
|
||||
if (Application.isPlaying)
|
||||
return;
|
||||
AutoRefreshPipelineShaders();
|
||||
}
|
||||
#endif
|
||||
|
||||
public void AutoRefreshPipelineShaders()
|
||||
{
|
||||
if (m_AutoRefreshShaders)
|
||||
SetPipelineShaders();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Applies the appropriate shader to the materials based on the current render pipeline.
|
||||
/// </summary>
|
||||
public void SetPipelineShaders()
|
||||
{
|
||||
if (m_ShaderContainers == null)
|
||||
return;
|
||||
|
||||
bool isBuiltinRenderPipeline = GraphicsSettings.currentRenderPipeline == null;
|
||||
|
||||
foreach (var info in m_ShaderContainers)
|
||||
{
|
||||
if (info.material == null)
|
||||
continue;
|
||||
|
||||
// Find the appropriate shaders based on the toggle
|
||||
Shader birpShader = info.useBuiltinShaderName ? Shader.Find(info.builtInPipelineShaderName) : info.builtInPipelineShader;
|
||||
Shader srpShader = info.useSRPShaderName ? Shader.Find(info.scriptableRenderPipelineShaderName) : info.scriptableRenderPipelineShader;
|
||||
|
||||
// Determine current shader for comparison
|
||||
Shader currentShader = info.material.shader;
|
||||
|
||||
// Update shader for the current render pipeline only if necessary
|
||||
if (isBuiltinRenderPipeline && birpShader != null && currentShader != birpShader)
|
||||
{
|
||||
info.material.shader = birpShader;
|
||||
MarkMaterialModified(info.material);
|
||||
}
|
||||
else if (!isBuiltinRenderPipeline && srpShader != null && currentShader != srpShader)
|
||||
{
|
||||
info.material.shader = srpShader;
|
||||
MarkMaterialModified(info.material);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static void MarkMaterialModified(Material material)
|
||||
{
|
||||
#if UNITY_EDITOR
|
||||
EditorUtility.SetDirty(material);
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
#if UNITY_EDITOR
|
||||
/// <summary>
|
||||
/// Custom property drawer for the shader container class.
|
||||
/// </summary>
|
||||
[CustomPropertyDrawer(typeof(ShaderContainer))]
|
||||
public class ShaderContainerDrawer : PropertyDrawer
|
||||
{
|
||||
public override void OnGUI(Rect position, SerializedProperty property, GUIContent label)
|
||||
{
|
||||
EditorGUI.BeginProperty(position, label, property);
|
||||
|
||||
float singleLineHeight = EditorGUIUtility.singleLineHeight;
|
||||
float verticalSpacing = EditorGUIUtility.standardVerticalSpacing;
|
||||
|
||||
SerializedProperty materialProp = property.FindPropertyRelative("material");
|
||||
SerializedProperty useSRPShaderNameProp = property.FindPropertyRelative("useSRPShaderName");
|
||||
SerializedProperty scriptableShaderNameProp = property.FindPropertyRelative("scriptableRenderPipelineShaderName");
|
||||
SerializedProperty scriptableShaderProp = property.FindPropertyRelative("scriptableRenderPipelineShader");
|
||||
SerializedProperty useShaderNameProp = property.FindPropertyRelative("useBuiltinShaderName");
|
||||
SerializedProperty builtInNameProp = property.FindPropertyRelative("builtInPipelineShaderName");
|
||||
SerializedProperty builtInShaderProp = property.FindPropertyRelative("builtInPipelineShader");
|
||||
|
||||
// Draw Material without the header.
|
||||
position.height = singleLineHeight;
|
||||
EditorGUI.PropertyField(position, materialProp);
|
||||
position.y += singleLineHeight + verticalSpacing;
|
||||
|
||||
// SRP Shader header and fields.
|
||||
EditorGUI.LabelField(position, "Scriptable Render Pipeline Shader", EditorStyles.boldLabel);
|
||||
position.y += EditorGUIUtility.singleLineHeight + verticalSpacing;
|
||||
|
||||
EditorGUI.PropertyField(position, useSRPShaderNameProp);
|
||||
position.y += singleLineHeight + verticalSpacing;
|
||||
|
||||
if (useSRPShaderNameProp.boolValue)
|
||||
{
|
||||
EditorGUI.PropertyField(position, scriptableShaderNameProp);
|
||||
position.y += singleLineHeight + verticalSpacing;
|
||||
}
|
||||
else
|
||||
{
|
||||
EditorGUI.PropertyField(position, scriptableShaderProp);
|
||||
position.y += singleLineHeight + verticalSpacing;
|
||||
}
|
||||
|
||||
// Built-in Shader header and fields.
|
||||
EditorGUI.LabelField(position, "Built-In Render Pipeline Shader", EditorStyles.boldLabel);
|
||||
position.y += singleLineHeight + verticalSpacing;
|
||||
|
||||
EditorGUI.PropertyField(position, useShaderNameProp);
|
||||
position.y += singleLineHeight + verticalSpacing;
|
||||
|
||||
if (useShaderNameProp.boolValue)
|
||||
{
|
||||
EditorGUI.PropertyField(position, builtInNameProp);
|
||||
position.y += singleLineHeight + verticalSpacing;
|
||||
}
|
||||
else
|
||||
{
|
||||
EditorGUI.PropertyField(position, builtInShaderProp);
|
||||
position.y += singleLineHeight + verticalSpacing;
|
||||
}
|
||||
|
||||
// Draw a separator line at the end.
|
||||
position.y += verticalSpacing / 2; // Extra space for the line.
|
||||
position.height = 1;
|
||||
EditorGUI.DrawRect(new Rect(position.x, position.y, position.width, 1), Color.gray);
|
||||
|
||||
EditorGUI.EndProperty();
|
||||
}
|
||||
|
||||
public override float GetPropertyHeight(SerializedProperty property, GUIContent label)
|
||||
{
|
||||
const int baseFieldCount = 4; // The Material field, the two toggles, and one for an optional field.
|
||||
int extraLineCount = property.FindPropertyRelative("useBuiltinShaderName").boolValue ? 0 : 1;
|
||||
extraLineCount += property.FindPropertyRelative("useSRPShaderName").boolValue ? 0 : 1;
|
||||
|
||||
float singleLineHeight = EditorGUIUtility.singleLineHeight;
|
||||
float verticalSpacing = EditorGUIUtility.standardVerticalSpacing;
|
||||
float headerHeight = EditorGUIUtility.singleLineHeight; // No longer need extra height for headers.
|
||||
|
||||
// Calculate height for fields and headers
|
||||
float fieldsHeight = baseFieldCount * singleLineHeight + (baseFieldCount - 1 + extraLineCount) * verticalSpacing;
|
||||
|
||||
// Allow space for header, separator line, and a bit of padding before the line.
|
||||
float headersHeight = 2 * (headerHeight + verticalSpacing);
|
||||
float separatorSpace = verticalSpacing / 2 + 1; // Additional vertical spacing and line height.
|
||||
|
||||
return fieldsHeight + headersHeight + separatorSpace + singleLineHeight * 1.5f;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Custom editor MaterialPipelineHandler
|
||||
/// </summary>
|
||||
[CustomEditor(typeof(MaterialPipelineHandler)), CanEditMultipleObjects]
|
||||
public class MaterialPipelineHandlerEditor : Editor
|
||||
{
|
||||
public override void OnInspectorGUI()
|
||||
{
|
||||
base.OnInspectorGUI();
|
||||
|
||||
// Draw the "Refresh Shaders" button
|
||||
if (GUILayout.Button("Refresh Shaders"))
|
||||
{
|
||||
foreach (var t in targets)
|
||||
{
|
||||
var handler = (MaterialPipelineHandler)t;
|
||||
handler.SetPipelineShaders();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: e7883133e628dff4a86f50c082f77055
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,271 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine.XR.Interaction.Toolkit.Utilities;
|
||||
|
||||
namespace UnityEngine.XR.Interaction.Toolkit.Samples.StarterAssets
|
||||
{
|
||||
/// <summary>
|
||||
/// Behavior with an API for spawning objects from a given set of prefabs.
|
||||
/// </summary>
|
||||
public class ObjectSpawner : MonoBehaviour
|
||||
{
|
||||
[SerializeField]
|
||||
[Tooltip("The camera that objects will face when spawned. If not set, defaults to the main camera.")]
|
||||
Camera m_CameraToFace;
|
||||
|
||||
/// <summary>
|
||||
/// The camera that objects will face when spawned. If not set, defaults to the <see cref="Camera.main"/> camera.
|
||||
/// </summary>
|
||||
public Camera cameraToFace
|
||||
{
|
||||
get
|
||||
{
|
||||
EnsureFacingCamera();
|
||||
return m_CameraToFace;
|
||||
}
|
||||
set => m_CameraToFace = value;
|
||||
}
|
||||
|
||||
[SerializeField]
|
||||
[Tooltip("The list of prefabs available to spawn.")]
|
||||
List<GameObject> m_ObjectPrefabs = new List<GameObject>();
|
||||
|
||||
/// <summary>
|
||||
/// The list of prefabs available to spawn.
|
||||
/// </summary>
|
||||
public List<GameObject> objectPrefabs
|
||||
{
|
||||
get => m_ObjectPrefabs;
|
||||
set => m_ObjectPrefabs = value;
|
||||
}
|
||||
|
||||
[SerializeField]
|
||||
[Tooltip("Optional prefab to spawn for each spawned object. Use a prefab with the Destroy Self component to make " +
|
||||
"sure the visualization only lives temporarily.")]
|
||||
GameObject m_SpawnVisualizationPrefab;
|
||||
|
||||
/// <summary>
|
||||
/// Optional prefab to spawn for each spawned object.
|
||||
/// </summary>
|
||||
/// <remarks>Use a prefab with <see cref="DestroySelf"/> to make sure the visualization only lives temporarily.</remarks>
|
||||
public GameObject spawnVisualizationPrefab
|
||||
{
|
||||
get => m_SpawnVisualizationPrefab;
|
||||
set => m_SpawnVisualizationPrefab = value;
|
||||
}
|
||||
|
||||
[SerializeField]
|
||||
[Tooltip("The index of the prefab to spawn. If outside the range of the list, this behavior will select " +
|
||||
"a random object each time it spawns.")]
|
||||
int m_SpawnOptionIndex = -1;
|
||||
|
||||
/// <summary>
|
||||
/// The index of the prefab to spawn. If outside the range of <see cref="objectPrefabs"/>, this behavior will
|
||||
/// select a random object each time it spawns.
|
||||
/// </summary>
|
||||
/// <seealso cref="isSpawnOptionRandomized"/>
|
||||
public int spawnOptionIndex
|
||||
{
|
||||
get => m_SpawnOptionIndex;
|
||||
set => m_SpawnOptionIndex = value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Whether this behavior will select a random object from <see cref="objectPrefabs"/> each time it spawns.
|
||||
/// </summary>
|
||||
/// <seealso cref="spawnOptionIndex"/>
|
||||
/// <seealso cref="RandomizeSpawnOption"/>
|
||||
public bool isSpawnOptionRandomized => m_SpawnOptionIndex < 0 || m_SpawnOptionIndex >= m_ObjectPrefabs.Count;
|
||||
|
||||
[SerializeField]
|
||||
[Tooltip("Whether to only spawn an object if the spawn point is within view of the camera.")]
|
||||
bool m_OnlySpawnInView = true;
|
||||
|
||||
/// <summary>
|
||||
/// Whether to only spawn an object if the spawn point is within view of the <see cref="cameraToFace"/>.
|
||||
/// </summary>
|
||||
public bool onlySpawnInView
|
||||
{
|
||||
get => m_OnlySpawnInView;
|
||||
set => m_OnlySpawnInView = value;
|
||||
}
|
||||
|
||||
[SerializeField]
|
||||
[Tooltip("The size, in viewport units, of the periphery inside the viewport that will not be considered in view.")]
|
||||
float m_ViewportPeriphery = 0.15f;
|
||||
|
||||
/// <summary>
|
||||
/// The size, in viewport units, of the periphery inside the viewport that will not be considered in view.
|
||||
/// </summary>
|
||||
public float viewportPeriphery
|
||||
{
|
||||
get => m_ViewportPeriphery;
|
||||
set => m_ViewportPeriphery = value;
|
||||
}
|
||||
|
||||
[SerializeField]
|
||||
[Tooltip("When enabled, the object will be rotated about the y-axis when spawned by Spawn Angle Range, " +
|
||||
"in relation to the direction of the spawn point to the camera.")]
|
||||
bool m_ApplyRandomAngleAtSpawn = true;
|
||||
|
||||
/// <summary>
|
||||
/// When enabled, the object will be rotated about the y-axis when spawned by <see cref="spawnAngleRange"/>
|
||||
/// in relation to the direction of the spawn point to the camera.
|
||||
/// </summary>
|
||||
public bool applyRandomAngleAtSpawn
|
||||
{
|
||||
get => m_ApplyRandomAngleAtSpawn;
|
||||
set => m_ApplyRandomAngleAtSpawn = value;
|
||||
}
|
||||
|
||||
[SerializeField]
|
||||
[Tooltip("The range in degrees that the object will randomly be rotated about the y axis when spawned, " +
|
||||
"in relation to the direction of the spawn point to the camera.")]
|
||||
float m_SpawnAngleRange = 45f;
|
||||
|
||||
/// <summary>
|
||||
/// The range in degrees that the object will randomly be rotated about the y axis when spawned, in relation
|
||||
/// to the direction of the spawn point to the camera.
|
||||
/// </summary>
|
||||
public float spawnAngleRange
|
||||
{
|
||||
get => m_SpawnAngleRange;
|
||||
set => m_SpawnAngleRange = value;
|
||||
}
|
||||
|
||||
[SerializeField]
|
||||
[Tooltip("Whether to spawn each object as a child of this object.")]
|
||||
bool m_SpawnAsChildren;
|
||||
|
||||
/// <summary>
|
||||
/// Whether to spawn each object as a child of this object.
|
||||
/// </summary>
|
||||
public bool spawnAsChildren
|
||||
{
|
||||
get => m_SpawnAsChildren;
|
||||
set => m_SpawnAsChildren = value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Event invoked after an object is spawned.
|
||||
/// </summary>
|
||||
/// <seealso cref="TrySpawnObject"/>
|
||||
public event Action<GameObject> objectSpawned;
|
||||
|
||||
/// <summary>
|
||||
/// See <see cref="MonoBehaviour"/>.
|
||||
/// </summary>
|
||||
void Awake()
|
||||
{
|
||||
EnsureFacingCamera();
|
||||
}
|
||||
|
||||
void EnsureFacingCamera()
|
||||
{
|
||||
if (m_CameraToFace == null)
|
||||
m_CameraToFace = Camera.main;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets this behavior to select a random object from <see cref="objectPrefabs"/> each time it spawns.
|
||||
/// </summary>
|
||||
/// <seealso cref="spawnOptionIndex"/>
|
||||
/// <seealso cref="isSpawnOptionRandomized"/>
|
||||
public void RandomizeSpawnOption()
|
||||
{
|
||||
m_SpawnOptionIndex = -1;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets the <see cref="spawnOptionIndex"/> so that a specific object will spawn. If the index is out
|
||||
/// of bounds of the list defined in <see cref="objectPrefabs"/>, the index will not be changed.
|
||||
/// </summary>
|
||||
/// <param name="index">Index of the object to be spawned.</param>
|
||||
/// <seealso cref="objectPrefabs"/>
|
||||
/// <seealso cref="spawnOptionIndex"/>
|
||||
public void SetSpawnObjectIndex(int index)
|
||||
{
|
||||
if (index < m_ObjectPrefabs.Count)
|
||||
m_SpawnOptionIndex = index;
|
||||
else
|
||||
Debug.LogWarning("Object index specified larger than number of Object Prefabs.", this);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Attempts to spawn an object from <see cref="objectPrefabs"/> at the given position. The object will have a
|
||||
/// yaw rotation that faces <see cref="cameraToFace"/>, plus or minus a random angle within <see cref="spawnAngleRange"/>.
|
||||
/// </summary>
|
||||
/// <param name="spawnPoint">The world space position at which to spawn the object.</param>
|
||||
/// <param name="spawnNormal">The world space normal of the spawn surface.</param>
|
||||
/// <returns>Returns <see langword="true"/> if the spawner successfully spawned an object. Otherwise returns
|
||||
/// <see langword="false"/>, for instance if the spawn point is out of view of the camera.</returns>
|
||||
/// <remarks>
|
||||
/// The object selected to spawn is based on <see cref="spawnOptionIndex"/>. If the index is outside
|
||||
/// the range of <see cref="objectPrefabs"/>, this method will select a random prefab from the list to spawn.
|
||||
/// Otherwise, it will spawn the prefab at the index.
|
||||
/// </remarks>
|
||||
/// <seealso cref="objectSpawned"/>
|
||||
public bool TrySpawnObject(Vector3 spawnPoint, Vector3 spawnNormal)
|
||||
{
|
||||
if (m_OnlySpawnInView)
|
||||
{
|
||||
var inViewMin = m_ViewportPeriphery;
|
||||
var inViewMax = 1f - m_ViewportPeriphery;
|
||||
var pointInViewportSpace = cameraToFace.WorldToViewportPoint(spawnPoint);
|
||||
if (pointInViewportSpace.z < 0f || pointInViewportSpace.x > inViewMax || pointInViewportSpace.x < inViewMin ||
|
||||
pointInViewportSpace.y > inViewMax || pointInViewportSpace.y < inViewMin)
|
||||
{
|
||||
Debug.LogWarning("Object spawn point out of view and OnlySpawnInView is set to true.", this);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
var objectIndex = isSpawnOptionRandomized ? Random.Range(0, m_ObjectPrefabs.Count) : m_SpawnOptionIndex;
|
||||
var newObject = Instantiate(m_ObjectPrefabs[objectIndex]);
|
||||
if (m_SpawnAsChildren)
|
||||
newObject.transform.parent = transform;
|
||||
|
||||
newObject.transform.position = spawnPoint;
|
||||
EnsureFacingCamera();
|
||||
|
||||
var facePosition = m_CameraToFace.transform.position;
|
||||
var forward = facePosition - spawnPoint;
|
||||
BurstMathUtility.ProjectOnPlane(forward, spawnNormal, out var projectedForward);
|
||||
newObject.transform.rotation = Quaternion.LookRotation(projectedForward, spawnNormal);
|
||||
|
||||
if (m_ApplyRandomAngleAtSpawn)
|
||||
{
|
||||
var randomRotation = Random.Range(-m_SpawnAngleRange, m_SpawnAngleRange);
|
||||
newObject.transform.Rotate(Vector3.up, randomRotation);
|
||||
}
|
||||
|
||||
if (m_SpawnVisualizationPrefab != null)
|
||||
{
|
||||
var visualizationTrans = Instantiate(m_SpawnVisualizationPrefab).transform;
|
||||
visualizationTrans.position = spawnPoint;
|
||||
visualizationTrans.rotation = newObject.transform.rotation;
|
||||
}
|
||||
|
||||
objectSpawned?.Invoke(newObject);
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Attempts to spawn an object from <see cref="objectPrefabs"/> at the given position. The object will have a
|
||||
/// yaw rotation that faces <see cref="cameraToFace"/>, plus or minus a random angle within <see cref="spawnAngleRange"/>.
|
||||
/// </summary>
|
||||
/// <param name="spawnPoint">The world space position at which to spawn the object.</param>
|
||||
/// <param name="spawnNormal">The world space normal of the spawn surface.</param>
|
||||
/// <remarks>
|
||||
/// The object selected to spawn is based on <see cref="spawnOptionIndex"/>. If the index is outside
|
||||
/// the range of <see cref="objectPrefabs"/>, this method will select a random prefab from the list to spawn.
|
||||
/// Otherwise, it will spawn the prefab at the index.
|
||||
/// </remarks>
|
||||
/// <seealso cref="objectSpawned"/>
|
||||
public void SpawnObject(Vector3 spawnPoint, Vector3 spawnNormal)
|
||||
{
|
||||
if (!TrySpawnObject(spawnPoint, spawnNormal))
|
||||
Debug.LogWarning("Could not spawn object.", this);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 956dd6cf70eaca449a45b6a95b96c8c1
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,162 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
#if UNITY_ANDROID
|
||||
using UnityEngine.Android;
|
||||
#endif
|
||||
using UnityEngine.Events;
|
||||
|
||||
namespace UnityEngine.XR.Interaction.Toolkit.Samples.StarterAssets
|
||||
{
|
||||
/// <summary>
|
||||
/// Utility class to help define and manage Android device permissions and specify corresponding permission callbacks via <see cref="UnityEvent"/>.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This component is currently designed to work with Android platform permissions only.
|
||||
/// </remarks>
|
||||
[DefaultExecutionOrder(-9999)]
|
||||
public class PermissionsManager : MonoBehaviour
|
||||
{
|
||||
const string k_DefaultPermissionId = "com.oculus.permission.USE_SCENE";
|
||||
|
||||
[SerializeField, Tooltip("Enables or disables the processing of permissions on Awake. If disabled, permissions will not be processed until the ProcessPermissions method is called.")]
|
||||
bool m_ProcessPermissionsOnAwake = true;
|
||||
|
||||
[SerializeField, Tooltip("The system permissions to request when this component starts.")]
|
||||
List<PermissionRequestGroup> m_PermissionGroups = new List<PermissionRequestGroup>();
|
||||
|
||||
/// <summary>
|
||||
/// Current platform permission group to process. This is determined during the <see cref="Awake"/> method using based on <see cref="XRPlatformUnderstanding"/>.
|
||||
/// </summary>
|
||||
PermissionRequestGroup m_CurrentPlatformPermissionGroup = new PermissionRequestGroup();
|
||||
|
||||
/// <summary>
|
||||
/// A group of permissions to request based on a specific platform.
|
||||
/// </summary>
|
||||
[Serializable]
|
||||
class PermissionRequestGroup
|
||||
{
|
||||
[Tooltip("The platform type for which these permissions is intended for.")]
|
||||
public XRPlatformType platformType;
|
||||
public List<PermissionRequest> permissions;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A permission request to be made to the Android operating system.
|
||||
/// </summary>
|
||||
[Serializable]
|
||||
class PermissionRequest
|
||||
{
|
||||
[Tooltip("The Android system permission to request when this component starts.")]
|
||||
public string permissionId = k_DefaultPermissionId;
|
||||
|
||||
[Tooltip("Whether to request permission from the operating system.")]
|
||||
public bool enabled = true;
|
||||
|
||||
[HideInInspector]
|
||||
public bool requested = false;
|
||||
|
||||
[HideInInspector]
|
||||
public bool responseReceived = false;
|
||||
|
||||
[HideInInspector]
|
||||
public bool granted = false;
|
||||
|
||||
public UnityEvent<string> onPermissionGranted;
|
||||
|
||||
public UnityEvent<string> onPermissionDenied;
|
||||
}
|
||||
|
||||
void Awake()
|
||||
{
|
||||
if (m_ProcessPermissionsOnAwake)
|
||||
ProcessPermissions();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Process the permissions defined in the <see cref="m_PermissionGroups"/> list.
|
||||
/// </summary>
|
||||
public void ProcessPermissions()
|
||||
{
|
||||
#if UNITY_ANDROID
|
||||
// Grab the current platform permission group based on the current platform in use.
|
||||
var currentPlatform = XRPlatformUnderstanding.CurrentPlatform;
|
||||
m_CurrentPlatformPermissionGroup = m_PermissionGroups.Find(g => g.platformType == currentPlatform);
|
||||
if (m_CurrentPlatformPermissionGroup == null)
|
||||
{
|
||||
// No permission group defined for the current platform.
|
||||
// No permissions will be requested by this component.
|
||||
return;
|
||||
}
|
||||
|
||||
var permissionIds = new List<string>();
|
||||
|
||||
// Loop through the current platform's permissions and add them to the
|
||||
// list of permissions to request if they are enabled and not already requested.
|
||||
for (var i = 0; i < m_CurrentPlatformPermissionGroup.permissions.Count; i++)
|
||||
{
|
||||
var permission = m_CurrentPlatformPermissionGroup.permissions[i];
|
||||
if (!permission.enabled)
|
||||
continue;
|
||||
|
||||
// If permission is not granted and not requested, add it to the list of permissions to request
|
||||
if (!Permission.HasUserAuthorizedPermission(permission.permissionId) && !permission.requested)
|
||||
{
|
||||
permissionIds.Add(permission.permissionId);
|
||||
permission.requested = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
Debug.Log($"User has permission for: {permission.permissionId}", this);
|
||||
}
|
||||
}
|
||||
|
||||
// Process permissions that were not already granted
|
||||
if (permissionIds.Count > 0)
|
||||
{
|
||||
var callbacks = new PermissionCallbacks();
|
||||
callbacks.PermissionDenied += OnPermissionDenied;
|
||||
callbacks.PermissionGranted += OnPermissionGranted;
|
||||
|
||||
Permission.RequestUserPermissions(permissionIds.ToArray(), callbacks);
|
||||
}
|
||||
#endif // UNITY_ANDROID
|
||||
}
|
||||
|
||||
void OnPermissionGranted(string permissionStr)
|
||||
{
|
||||
// Find the permission
|
||||
var permission = m_CurrentPlatformPermissionGroup.permissions.Find(p => p.permissionId == permissionStr);
|
||||
if (permission == null)
|
||||
{
|
||||
Debug.LogWarning($"Permission granted callback received for an unexpected permission request, permission ID {permissionStr}", this);
|
||||
return;
|
||||
}
|
||||
|
||||
// Enable permission
|
||||
permission.granted = true;
|
||||
permission.responseReceived = true;
|
||||
|
||||
Debug.Log($"User granted permission for: {permissionStr}", this);
|
||||
permission.onPermissionGranted.Invoke(permissionStr);
|
||||
}
|
||||
|
||||
void OnPermissionDenied(string permissionStr)
|
||||
{
|
||||
// Find the permission
|
||||
var permission = m_CurrentPlatformPermissionGroup.permissions.Find(p => p.permissionId == permissionStr);
|
||||
if (permission == null)
|
||||
{
|
||||
Debug.LogWarning($"Permission denied callback received for an unexpected permission request, permission ID {permissionStr}", this);
|
||||
return;
|
||||
}
|
||||
|
||||
// Disable permission
|
||||
permission.granted = false;
|
||||
permission.responseReceived = true;
|
||||
|
||||
Debug.LogWarning($"User denied permission for: {permissionStr}", this);
|
||||
permission.onPermissionDenied.Invoke(permissionStr);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 4f62c7b7418ee024aa16285921e63d56
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,117 @@
|
||||
using System;
|
||||
|
||||
#if OPENXR_1_6_OR_NEWER
|
||||
using UnityEngine.XR.OpenXR;
|
||||
#endif
|
||||
|
||||
namespace UnityEngine.XR.Interaction.Toolkit.Samples.StarterAssets
|
||||
{
|
||||
/// <summary>
|
||||
/// Enumeration of supported XR Platforms for OpenXR.
|
||||
/// </summary>
|
||||
public enum XRPlatformType
|
||||
{
|
||||
/// <summary>
|
||||
/// Meta Quest devices supported through OpenXR.
|
||||
/// </summary>
|
||||
[InspectorName("OpenXR: Meta")]
|
||||
OpenXRMeta,
|
||||
|
||||
/// <summary>
|
||||
/// Android XR devices supported through OpenXR.
|
||||
/// </summary>
|
||||
[InspectorName("OpenXR: Android XR")]
|
||||
OpenXRAndroidXR,
|
||||
|
||||
/// <summary>
|
||||
/// Other OpenXR devices.
|
||||
/// </summary>
|
||||
[InspectorName("OpenXR: Other")]
|
||||
OpenXROther,
|
||||
|
||||
/// <summary>
|
||||
/// Other device that does not support OpenXR or not running on an OpenXR runtime.
|
||||
/// </summary>
|
||||
Other,
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Helper class that determines the current XR platform based on the active runtime.
|
||||
/// Currently, this only supports OpenXR Runtimes from Meta and Google.
|
||||
/// </summary>
|
||||
public static class XRPlatformUnderstanding
|
||||
{
|
||||
const string k_RuntimeNameMeta = "Oculus";
|
||||
const string k_RuntimeNameAndroidXR = "Android XR";
|
||||
|
||||
/// <summary>
|
||||
/// The current platform based on the OpenXR Runtime name.
|
||||
/// </summary>
|
||||
public static XRPlatformType CurrentPlatform
|
||||
{
|
||||
get
|
||||
{
|
||||
if (!s_Initialized)
|
||||
{
|
||||
s_CurrentPlatform = GetCurrentXRPlatform();
|
||||
s_Initialized = true;
|
||||
}
|
||||
return s_CurrentPlatform;
|
||||
}
|
||||
}
|
||||
|
||||
static XRPlatformType s_CurrentPlatform = XRPlatformType.Other;
|
||||
|
||||
static bool s_Initialized;
|
||||
|
||||
/// <summary>
|
||||
/// Returns the current platform based on the active OpenXR Runtime name.
|
||||
/// </summary>
|
||||
/// <returns>The current platform based on the active OpenXR Runtime name.</returns>
|
||||
static XRPlatformType GetCurrentXRPlatform()
|
||||
{
|
||||
// If we have already initialized, just return the current platform
|
||||
if (s_Initialized)
|
||||
return s_CurrentPlatform;
|
||||
|
||||
#if OPENXR_1_6_OR_NEWER
|
||||
try
|
||||
{
|
||||
var openXRRuntimeName = OpenXRRuntime.name;
|
||||
if (string.IsNullOrEmpty(openXRRuntimeName))
|
||||
{
|
||||
s_CurrentPlatform = XRPlatformType.Other;
|
||||
}
|
||||
else
|
||||
{
|
||||
switch (openXRRuntimeName)
|
||||
{
|
||||
case k_RuntimeNameMeta:
|
||||
Debug.Log("Meta runtime detected.");
|
||||
s_CurrentPlatform = XRPlatformType.OpenXRMeta;
|
||||
break;
|
||||
case k_RuntimeNameAndroidXR:
|
||||
Debug.Log("Android XR runtime detected.");
|
||||
s_CurrentPlatform = XRPlatformType.OpenXRAndroidXR;
|
||||
break;
|
||||
default:
|
||||
Debug.Log($"Unknown OpenXR runtime detected: \"{openXRRuntimeName}\"");
|
||||
s_CurrentPlatform = XRPlatformType.OpenXROther;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Debug.LogWarning($"Failed to get OpenXR runtime: {e.Message}");
|
||||
s_CurrentPlatform = XRPlatformType.Other;
|
||||
}
|
||||
#else
|
||||
s_CurrentPlatform = XRPlatformType.Other;
|
||||
#endif
|
||||
|
||||
s_Initialized = true;
|
||||
return s_CurrentPlatform;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 3ce213c1f32595b4888e78a36e017d3e
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,45 @@
|
||||
using UnityEngine.XR.Interaction.Toolkit.Interactables;
|
||||
using UnityEngine.XR.Interaction.Toolkit.Transformers;
|
||||
|
||||
namespace UnityEngine.XR.Interaction.Toolkit.Samples.StarterAssets
|
||||
{
|
||||
/// <summary>
|
||||
/// An XR grab transformer that allows for the locking of specific rotation axes. When an object is grabbed and manipulated,
|
||||
/// this class ensures that rotations are only applied to the specified axes, preserving the initial rotation for the others.
|
||||
/// </summary>
|
||||
public class RotationAxisLockGrabTransformer : XRBaseGrabTransformer
|
||||
{
|
||||
[SerializeField]
|
||||
[Tooltip("Defines which rotation axes are allowed when an object is grabbed. Axes not selected will maintain their initial rotation.")]
|
||||
XRGeneralGrabTransformer.ManipulationAxes m_PermittedRotationAxis = XRGeneralGrabTransformer.ManipulationAxes.All;
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override RegistrationMode registrationMode => RegistrationMode.SingleAndMultiple;
|
||||
|
||||
Vector3 m_InitialEulerRotation;
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void OnLink(XRGrabInteractable grabInteractable)
|
||||
{
|
||||
base.OnLink(grabInteractable);
|
||||
m_InitialEulerRotation = grabInteractable.transform.rotation.eulerAngles;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void Process(XRGrabInteractable grabInteractable, XRInteractionUpdateOrder.UpdatePhase updatePhase, ref Pose targetPose, ref Vector3 localScale)
|
||||
{
|
||||
Vector3 newRotationEuler = targetPose.rotation.eulerAngles;
|
||||
|
||||
if ((m_PermittedRotationAxis & XRGeneralGrabTransformer.ManipulationAxes.X) == 0)
|
||||
newRotationEuler.x = m_InitialEulerRotation.x;
|
||||
|
||||
if ((m_PermittedRotationAxis & XRGeneralGrabTransformer.ManipulationAxes.Y) == 0)
|
||||
newRotationEuler.y = m_InitialEulerRotation.y;
|
||||
|
||||
if ((m_PermittedRotationAxis & XRGeneralGrabTransformer.ManipulationAxes.Z) == 0)
|
||||
newRotationEuler.z = m_InitialEulerRotation.z;
|
||||
|
||||
targetPose.rotation = Quaternion.Euler(newRotationEuler);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 4dd2e41114c62b44fbd334ca5b314352
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,97 @@
|
||||
using System;
|
||||
using UnityEngine.XR.Interaction.Toolkit.AffordanceSystem.State;
|
||||
using UnityEngine.XR.Interaction.Toolkit.Interactables;
|
||||
using UnityEngine.XR.Interaction.Toolkit.Locomotion.Teleportation;
|
||||
|
||||
namespace UnityEngine.XR.Interaction.Toolkit.Samples.StarterAssets
|
||||
{
|
||||
/// <summary>
|
||||
/// Helper component that binds an <see cref="XRInteractableAffordanceStateProvider"/> to a
|
||||
/// <see cref="TeleportationMultiAnchorVolume"/> when the teleport volume sets its destination anchor to a child transform
|
||||
/// of the state provider's originally bound interactable.
|
||||
/// </summary>
|
||||
[RequireComponent(typeof(XRInteractableAffordanceStateProvider))]
|
||||
[Obsolete("The Affordance System namespace and all associated classes have been deprecated. The existing affordance system will be moved, replaced and updated with a new interaction feedback system in a future version of XRI.")]
|
||||
public class TeleportVolumeAnchorAffordanceStateLink : MonoBehaviour
|
||||
{
|
||||
[SerializeField]
|
||||
[Tooltip("The teleport volume that will drive affordance states when its destination anchor belongs to this interactable.")]
|
||||
TeleportationMultiAnchorVolume m_ContainingTeleportVolume;
|
||||
|
||||
/// <summary>
|
||||
/// The teleport volume that will drive affordance states when its destination anchor belongs to the
|
||||
/// state provider's originally bound interactable.
|
||||
/// </summary>
|
||||
public TeleportationMultiAnchorVolume containingTeleportVolume
|
||||
{
|
||||
get => m_ContainingTeleportVolume;
|
||||
set => m_ContainingTeleportVolume = value;
|
||||
}
|
||||
|
||||
XRInteractableAffordanceStateProvider m_AffordanceStateProvider;
|
||||
IXRInteractable m_Interactable;
|
||||
|
||||
/// <summary>
|
||||
/// See <see cref="MonoBehaviour"/>.
|
||||
/// </summary>
|
||||
protected void OnEnable()
|
||||
{
|
||||
m_AffordanceStateProvider = GetComponent<XRInteractableAffordanceStateProvider>();
|
||||
if (m_AffordanceStateProvider == null)
|
||||
{
|
||||
Debug.LogError($"Missing {nameof(XRInteractableAffordanceStateProvider)} on {gameObject.name}.", this);
|
||||
enabled = false;
|
||||
return;
|
||||
}
|
||||
|
||||
if (m_ContainingTeleportVolume == null)
|
||||
{
|
||||
Debug.LogError($"Missing {nameof(TeleportationMultiAnchorVolume)} reference on {gameObject.name}.", this);
|
||||
enabled = false;
|
||||
return;
|
||||
}
|
||||
|
||||
var interactableSource = m_AffordanceStateProvider.interactableSource;
|
||||
m_Interactable = interactableSource != null && interactableSource is IXRInteractable interactable
|
||||
? interactable
|
||||
: m_AffordanceStateProvider.GetComponentInParent<IXRInteractable>();
|
||||
|
||||
if (m_Interactable == null)
|
||||
{
|
||||
Debug.LogError($"Interactable source must be an {nameof(IXRInteractable)}.", this);
|
||||
enabled = false;
|
||||
return;
|
||||
}
|
||||
|
||||
m_ContainingTeleportVolume.destinationAnchorChanged += OnDestinationAnchorChanged;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// See <see cref="MonoBehaviour"/>.
|
||||
/// </summary>
|
||||
protected void OnDisable()
|
||||
{
|
||||
if (m_ContainingTeleportVolume != null)
|
||||
m_ContainingTeleportVolume.destinationAnchorChanged -= OnDestinationAnchorChanged;
|
||||
|
||||
if (m_AffordanceStateProvider != null)
|
||||
m_AffordanceStateProvider.SetBoundInteractionReceiver(m_Interactable);
|
||||
}
|
||||
|
||||
void OnDestinationAnchorChanged(TeleportationMultiAnchorVolume anchorVolume)
|
||||
{
|
||||
var anchor = anchorVolume.destinationAnchor;
|
||||
if (anchor == null)
|
||||
{
|
||||
m_AffordanceStateProvider.SetBoundInteractionReceiver(m_Interactable);
|
||||
return;
|
||||
}
|
||||
|
||||
// Use teleport volume to drive affordance states if its current anchor belongs to this interactable
|
||||
m_AffordanceStateProvider.SetBoundInteractionReceiver(
|
||||
anchor.IsChildOf(m_Interactable.transform)
|
||||
? m_ContainingTeleportVolume
|
||||
: m_Interactable);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 7da98a0edd844d83b9b4de3f91de030c
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,70 @@
|
||||
using UnityEngine.UI;
|
||||
|
||||
namespace UnityEngine.XR.Interaction.Toolkit.Samples.StarterAssets
|
||||
{
|
||||
/// <summary>
|
||||
/// Updates the normal color of a toggle based on the state of the toggle.
|
||||
/// </summary>
|
||||
[RequireComponent(typeof(Toggle))]
|
||||
public class ToggleColorToggler : MonoBehaviour
|
||||
{
|
||||
[SerializeField]
|
||||
[Tooltip("Normal color for the toggle in the on state.")]
|
||||
Color m_OnColor = new Color(32 / 255f, 150 / 255f, 243 / 255f);
|
||||
|
||||
/// <summary>
|
||||
/// Normal color for the toggle in the on state.
|
||||
/// </summary>
|
||||
public Color onColor
|
||||
{
|
||||
get => m_OnColor;
|
||||
set => m_OnColor = value;
|
||||
}
|
||||
|
||||
[SerializeField]
|
||||
[Tooltip("Normal color for the toggle in the off state.")]
|
||||
Color m_OffColor = new Color(46 / 255f, 46 / 255f, 46 / 255f);
|
||||
|
||||
/// <summary>
|
||||
/// Normal color for the toggle in the off state.
|
||||
/// </summary>
|
||||
public Color offColor
|
||||
{
|
||||
get => m_OffColor;
|
||||
set => m_OffColor = value;
|
||||
}
|
||||
|
||||
Toggle m_TargetToggle;
|
||||
|
||||
/// <summary>
|
||||
/// See <see cref="MonoBehaviour"/>.
|
||||
/// </summary>
|
||||
void Awake()
|
||||
{
|
||||
m_TargetToggle = GetComponent<Toggle>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// See <see cref="MonoBehaviour"/>.
|
||||
/// </summary>
|
||||
void OnEnable()
|
||||
{
|
||||
m_TargetToggle.onValueChanged.AddListener(OnToggleValueChanged);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// See <see cref="MonoBehaviour"/>.
|
||||
/// </summary>
|
||||
void OnDisable()
|
||||
{
|
||||
m_TargetToggle.onValueChanged.RemoveListener(OnToggleValueChanged);
|
||||
}
|
||||
|
||||
void OnToggleValueChanged(bool isOn)
|
||||
{
|
||||
var toggleColors = m_TargetToggle.colors;
|
||||
toggleColors.normalColor = isOn ? m_OnColor : m_OffColor;
|
||||
m_TargetToggle.colors = toggleColors;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: cb280e627cedda749b93045b8dd8d327
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,173 @@
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace UnityEngine.XR.Interaction.Toolkit.Samples.StarterAssets
|
||||
{
|
||||
/// <summary>
|
||||
/// This component is designed to easily toggle a specific component and GameObject on or off when an object
|
||||
/// enters the specified <see cref="triggerVolume"/>.
|
||||
/// </summary>
|
||||
[RequireComponent(typeof(Collider))]
|
||||
public class ToggleComponentZone : MonoBehaviour
|
||||
{
|
||||
[SerializeField]
|
||||
[Tooltip("Collider that will trigger the component to turn on or off when entering the Trigger Volume. Must have a Rigidbody component and be on the same physics layer as the Trigger Volume.")]
|
||||
Collider m_ActivationObject;
|
||||
|
||||
/// <summary>
|
||||
/// Collider that will trigger the component to turn on or off when entering the Trigger Volume.
|
||||
/// Must have a Rigidbody component and be on the same physics layer as the Trigger Volume.
|
||||
/// </summary>
|
||||
public Collider activationObject
|
||||
{
|
||||
get => m_ActivationObject;
|
||||
set => m_ActivationObject = value;
|
||||
}
|
||||
|
||||
[SerializeField]
|
||||
[Tooltip("Sets whether to enable or disable the Component To Toggle and GameObject To Toggle upon entry into the Trigger Volume.")]
|
||||
bool m_EnableOnEntry = true;
|
||||
|
||||
/// <summary>
|
||||
/// Sets whether to enable or disable the Component To Toggle and GameObject To Toggle upon entry into the Trigger Volume.
|
||||
/// </summary>
|
||||
public bool enableOnEntry
|
||||
{
|
||||
get => m_EnableOnEntry;
|
||||
set => m_EnableOnEntry = value;
|
||||
}
|
||||
|
||||
[SerializeField]
|
||||
[Tooltip("Components to set the enabled state for. Will set the value to the Enable On Entry value upon entry and revert to original value on exit.")]
|
||||
List<Behaviour> m_ComponentsToToggle = new List<Behaviour>();
|
||||
|
||||
/// <summary>
|
||||
/// Component to set the enabled state for. Will set the value to the
|
||||
/// Enable On Entry value upon entry and revert to original value on exit.
|
||||
/// </summary>
|
||||
public List<Behaviour> componentsToToggle
|
||||
{
|
||||
get => m_ComponentsToToggle;
|
||||
set => m_ComponentsToToggle = value;
|
||||
}
|
||||
|
||||
[SerializeField]
|
||||
[Tooltip("Array of GameObjects to set the enabled state for. Will set the value to the Enable On Entry value upon entry and revert to original value on exit.")]
|
||||
List<GameObject> m_GameObjectsToToggle = new List<GameObject>();
|
||||
|
||||
/// <summary>
|
||||
/// GameObject to set the enabled state for. Will set the value to the
|
||||
/// Enable On Entry value upon entry and revert to original value on exit.
|
||||
/// </summary>
|
||||
public List<GameObject> gameObjectsToToggle
|
||||
{
|
||||
get => m_GameObjectsToToggle;
|
||||
set => m_GameObjectsToToggle = value;
|
||||
}
|
||||
|
||||
Collider m_TriggerVolume;
|
||||
Dictionary<Behaviour, bool> m_InitialComponentStateOnEntry;
|
||||
Dictionary<GameObject, bool> m_InitialGameObjectStateOnEntry;
|
||||
|
||||
/// <summary>
|
||||
/// See <see cref="MonoBehaviour"/>.
|
||||
/// </summary>
|
||||
void Start()
|
||||
{
|
||||
if (m_TriggerVolume == null && !TryGetComponent(out m_TriggerVolume))
|
||||
{
|
||||
enabled = false;
|
||||
return;
|
||||
}
|
||||
|
||||
if (!m_TriggerVolume.isTrigger)
|
||||
{
|
||||
m_TriggerVolume.isTrigger = true;
|
||||
Debug.LogWarning($"Trigger Volume \"{m_TriggerVolume}\" was not set as trigger, which the Toggle Component Zone expects. It has been forced to be a trigger.", this);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// See <see cref="MonoBehaviour"/>.
|
||||
/// </summary>
|
||||
void OnTriggerEnter(Collider other)
|
||||
{
|
||||
if (other == null || other != m_ActivationObject)
|
||||
return;
|
||||
|
||||
// Save the target GameObject(s) active state to restore when leaving the zone
|
||||
if (m_GameObjectsToToggle != null && m_GameObjectsToToggle.Count > 0)
|
||||
{
|
||||
m_InitialGameObjectStateOnEntry ??= new Dictionary<GameObject, bool>(m_GameObjectsToToggle.Count);
|
||||
m_InitialGameObjectStateOnEntry.Clear();
|
||||
|
||||
for (var i = 0; i < m_GameObjectsToToggle.Count; ++i)
|
||||
{
|
||||
var target = m_GameObjectsToToggle[i];
|
||||
m_InitialGameObjectStateOnEntry.Add(target, target.activeSelf);
|
||||
target.SetActive(m_EnableOnEntry);
|
||||
}
|
||||
}
|
||||
|
||||
// Save the target component(s) enabled state to restore when leaving the zone
|
||||
if (m_ComponentsToToggle != null && m_ComponentsToToggle.Count > 0)
|
||||
{
|
||||
m_InitialComponentStateOnEntry ??= new Dictionary<Behaviour, bool>(m_ComponentsToToggle.Count);
|
||||
m_InitialComponentStateOnEntry.Clear();
|
||||
|
||||
for (var i = 0; i < m_ComponentsToToggle.Count; ++i)
|
||||
{
|
||||
var target = m_ComponentsToToggle[i];
|
||||
m_InitialComponentStateOnEntry.Add(target, target.enabled);
|
||||
target.enabled = m_EnableOnEntry;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// See <see cref="MonoBehaviour"/>.
|
||||
/// </summary>
|
||||
void OnTriggerExit(Collider other)
|
||||
{
|
||||
if (other == null || other != m_ActivationObject)
|
||||
return;
|
||||
|
||||
// Restore original target component(s) enabled state
|
||||
if (m_ComponentsToToggle != null && m_ComponentsToToggle.Count > 0 && m_InitialComponentStateOnEntry != null)
|
||||
{
|
||||
if (m_InitialComponentStateOnEntry.Count == m_ComponentsToToggle.Count)
|
||||
{
|
||||
for (var i = 0; i < m_ComponentsToToggle.Count; ++i)
|
||||
{
|
||||
var component = m_ComponentsToToggle[i];
|
||||
if (m_InitialComponentStateOnEntry.TryGetValue(component, out var initialState))
|
||||
component.enabled = initialState;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
Debug.LogWarning("List of Components to Toggle changed in count between entering and exiting the Trigger Volume," +
|
||||
" which is not supported by this component. Cannot restore original enabled state.", this);
|
||||
}
|
||||
}
|
||||
|
||||
// Restore original target GameObject(s) active state
|
||||
if (m_GameObjectsToToggle != null && m_GameObjectsToToggle.Count > 0 && m_InitialGameObjectStateOnEntry != null)
|
||||
{
|
||||
if (m_InitialGameObjectStateOnEntry.Count == m_GameObjectsToToggle.Count)
|
||||
{
|
||||
for (var i = 0; i < m_GameObjectsToToggle.Count; ++i)
|
||||
{
|
||||
var go = m_GameObjectsToToggle[i];
|
||||
if (m_InitialGameObjectStateOnEntry.TryGetValue(go, out var initialState))
|
||||
go.SetActive(initialState);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
Debug.LogWarning("List of GameObjects to Toggle changed in count between entering and exiting the Trigger Volume," +
|
||||
" which is not supported by this component. Cannot restore original active state.", this);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 5fe39fdeebcc91a45919bfcb77ef6eb4
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,298 @@
|
||||
using System;
|
||||
using Unity.Mathematics;
|
||||
using Unity.XR.CoreUtils.Bindings;
|
||||
using UnityEngine.XR.Interaction.Toolkit.AffordanceSystem.State;
|
||||
using UnityEngine.XR.Interaction.Toolkit.Filtering;
|
||||
using UnityEngine.XR.Interaction.Toolkit.Utilities.Tweenables.Primitives;
|
||||
|
||||
namespace UnityEngine.XR.Interaction.Toolkit.Samples.StarterAssets
|
||||
{
|
||||
/// <summary>
|
||||
/// Follow animation affordance for <see cref="IPokeStateDataProvider"/>, such as <see cref="XRPokeFilter"/>.
|
||||
/// Used to animate a pressed transform, such as a button to follow the poke position.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The Affordance System namespace and all associated classes have been deprecated.
|
||||
/// The existing affordance system will be moved, replaced and updated with a new interaction
|
||||
/// feedback system in a future version of XRI, including this sample script.
|
||||
/// </remarks>
|
||||
[AddComponentMenu("XR/XR Poke Follow Affordance", 22)]
|
||||
public class XRPokeFollowAffordance : MonoBehaviour
|
||||
{
|
||||
[SerializeField]
|
||||
[Tooltip("Transform that will move in the poke direction when this or a parent GameObject is poked." +
|
||||
"\nNote: Should be a direct child GameObject.")]
|
||||
Transform m_PokeFollowTransform;
|
||||
|
||||
/// <summary>
|
||||
/// Transform that will animate along the axis of interaction when this interactable is poked.
|
||||
/// Note: Must be a direct child GameObject as it moves in local space relative to the poke target's transform.
|
||||
/// </summary>
|
||||
public Transform pokeFollowTransform
|
||||
{
|
||||
get => m_PokeFollowTransform;
|
||||
set => m_PokeFollowTransform = value;
|
||||
}
|
||||
|
||||
[SerializeField]
|
||||
[Range(0f, 20f)]
|
||||
[Tooltip("Multiplies transform position interpolation as a factor of Time.deltaTime. If 0, no smoothing will be applied.")]
|
||||
float m_SmoothingSpeed = 16f;
|
||||
|
||||
/// <summary>
|
||||
/// Multiplies transform position interpolation as a factor of <see cref="Time.deltaTime"/>. If <c>0</c>, no smoothing will be applied.
|
||||
/// </summary>
|
||||
public float smoothingSpeed
|
||||
{
|
||||
get => m_SmoothingSpeed;
|
||||
set => m_SmoothingSpeed = value;
|
||||
}
|
||||
|
||||
[SerializeField]
|
||||
[Tooltip("When this component is no longer the target of the poke, the Poke Follow Transform returns to the original position.")]
|
||||
bool m_ReturnToInitialPosition = true;
|
||||
|
||||
/// <summary>
|
||||
/// When this component is no longer the target of the poke, the <see cref="pokeFollowTransform"/> returns to the original position.
|
||||
/// </summary>
|
||||
public bool returnToInitialPosition
|
||||
{
|
||||
get => m_ReturnToInitialPosition;
|
||||
set => m_ReturnToInitialPosition = value;
|
||||
}
|
||||
|
||||
[SerializeField]
|
||||
[Tooltip("Whether to apply the follow animation if the target of the poke is a child of this transform. " +
|
||||
"This is useful for UI objects that may have child graphics.")]
|
||||
bool m_ApplyIfChildIsTarget = true;
|
||||
|
||||
/// <summary>
|
||||
/// Whether to apply the follow animation if the target of the poke is a child of this transform.
|
||||
/// This is useful for UI objects that may have child graphics.
|
||||
/// </summary>
|
||||
public bool applyIfChildIsTarget
|
||||
{
|
||||
get => m_ApplyIfChildIsTarget;
|
||||
set => m_ApplyIfChildIsTarget = value;
|
||||
}
|
||||
|
||||
[SerializeField]
|
||||
[Tooltip("Whether to keep the Poke Follow Transform from moving past a maximum distance from the poke target.")]
|
||||
bool m_ClampToMaxDistance;
|
||||
|
||||
/// <summary>
|
||||
/// Whether to keep the <see cref="pokeFollowTransform"/> from moving past <see cref="maxDistance"/> from the poke target.
|
||||
/// </summary>
|
||||
public bool clampToMaxDistance
|
||||
{
|
||||
get => m_ClampToMaxDistance;
|
||||
set => m_ClampToMaxDistance = value;
|
||||
}
|
||||
|
||||
[SerializeField]
|
||||
[Tooltip("The maximum distance from this transform that the Poke Follow Transform can move.")]
|
||||
float m_MaxDistance;
|
||||
|
||||
/// <summary>
|
||||
/// The maximum distance from this transform that the <see cref="pokeFollowTransform"/> can move when
|
||||
/// <see cref="clampToMaxDistance"/> is <see langword="true"/>.
|
||||
/// </summary>
|
||||
public float maxDistance
|
||||
{
|
||||
get => m_MaxDistance;
|
||||
set => m_MaxDistance = value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The original position of this interactable before any pushes have been applied.
|
||||
/// </summary>
|
||||
public Vector3 initialPosition
|
||||
{
|
||||
get => m_InitialPosition;
|
||||
set => m_InitialPosition = value;
|
||||
}
|
||||
|
||||
IPokeStateDataProvider m_PokeDataProvider;
|
||||
IMultiPokeStateDataProvider m_MultiPokeStateDataProvider;
|
||||
|
||||
#pragma warning disable CS0618 // Type or member is obsolete
|
||||
readonly Vector3TweenableVariable m_TransformTweenableVariable = new Vector3TweenableVariable();
|
||||
#pragma warning restore CS0618 // Type or member is obsolete
|
||||
readonly BindingsGroup m_BindingsGroup = new BindingsGroup();
|
||||
Vector3 m_InitialPosition;
|
||||
bool m_IsFirstFrame;
|
||||
|
||||
[HideInInspector]
|
||||
[SerializeField]
|
||||
XRPokeFilter m_PokeFilter = null;
|
||||
|
||||
/// <summary>
|
||||
/// See <see cref="MonoBehaviour"/>.
|
||||
/// </summary>
|
||||
protected void Awake()
|
||||
{
|
||||
m_MultiPokeStateDataProvider = GetComponentInParent<IMultiPokeStateDataProvider>();
|
||||
if (m_MultiPokeStateDataProvider == null)
|
||||
m_PokeDataProvider = GetComponentInParent<IPokeStateDataProvider>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// See <see cref="MonoBehaviour"/>.
|
||||
/// </summary>
|
||||
protected void Start()
|
||||
{
|
||||
if (m_PokeFollowTransform != null)
|
||||
{
|
||||
m_InitialPosition = m_PokeFollowTransform.localPosition;
|
||||
m_BindingsGroup.AddBinding(m_TransformTweenableVariable.Subscribe(OnTransformTweenableVariableUpdated));
|
||||
|
||||
if (m_MultiPokeStateDataProvider != null)
|
||||
m_BindingsGroup.AddBinding(m_MultiPokeStateDataProvider.GetPokeStateDataForTarget(transform).Subscribe(OnPokeStateDataUpdated));
|
||||
else if (m_PokeDataProvider != null)
|
||||
m_BindingsGroup.AddBinding(m_PokeDataProvider.pokeStateData.SubscribeAndUpdate(OnPokeStateDataUpdated));
|
||||
}
|
||||
else
|
||||
{
|
||||
enabled = false;
|
||||
Debug.LogWarning($"Missing Poke Follow Transform assignment on {this}. Disabling component.", this);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// See <see cref="MonoBehaviour"/>.
|
||||
/// </summary>
|
||||
protected void OnDestroy()
|
||||
{
|
||||
m_BindingsGroup.Clear();
|
||||
m_TransformTweenableVariable?.Dispose();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// See <see cref="MonoBehaviour"/>.
|
||||
/// </summary>
|
||||
protected void LateUpdate()
|
||||
{
|
||||
if (m_IsFirstFrame)
|
||||
{
|
||||
m_TransformTweenableVariable.HandleTween(1f);
|
||||
m_IsFirstFrame = false;
|
||||
return;
|
||||
}
|
||||
|
||||
m_TransformTweenableVariable.HandleTween(m_SmoothingSpeed > 0f ? Time.deltaTime * m_SmoothingSpeed : 1f);
|
||||
}
|
||||
|
||||
protected virtual void OnTransformTweenableVariableUpdated(float3 position)
|
||||
{
|
||||
// UI Anchors can cause this to not work correctly, so we check if it's a RectTransform and set the localPosition Z only
|
||||
if (m_PokeFollowTransform is RectTransform)
|
||||
{
|
||||
var targetPosition = m_PokeFollowTransform.localPosition;
|
||||
targetPosition.z = position.z;
|
||||
m_PokeFollowTransform.localPosition = targetPosition;
|
||||
}
|
||||
else
|
||||
{
|
||||
m_PokeFollowTransform.localPosition = position;
|
||||
}
|
||||
}
|
||||
|
||||
void OnPokeStateDataUpdated(PokeStateData data)
|
||||
{
|
||||
var pokeTarget = data.target;
|
||||
var applyFollow = m_ApplyIfChildIsTarget
|
||||
? pokeTarget != null && pokeTarget.IsChildOf(transform)
|
||||
: pokeTarget == transform;
|
||||
|
||||
if (applyFollow)
|
||||
{
|
||||
var targetPosition = pokeTarget.InverseTransformPoint(data.axisAlignedPokeInteractionPoint);
|
||||
if (m_ClampToMaxDistance && targetPosition.sqrMagnitude > m_MaxDistance * m_MaxDistance)
|
||||
targetPosition = Vector3.ClampMagnitude(targetPosition, m_MaxDistance);
|
||||
|
||||
m_TransformTweenableVariable.target = targetPosition;
|
||||
}
|
||||
else if (m_ReturnToInitialPosition)
|
||||
{
|
||||
m_TransformTweenableVariable.target = m_InitialPosition;
|
||||
}
|
||||
}
|
||||
|
||||
public void ResetFollowTransform()
|
||||
{
|
||||
if (!m_ClampToMaxDistance || m_PokeFollowTransform == null)
|
||||
return;
|
||||
|
||||
m_PokeFollowTransform.localPosition = m_InitialPosition;
|
||||
}
|
||||
|
||||
void OnDrawGizmos()
|
||||
{
|
||||
if (!TryGetTargetEndPoint(out var endPoint))
|
||||
return;
|
||||
|
||||
Gizmos.color = Color.yellow;
|
||||
Gizmos.DrawLine(transform.position, endPoint);
|
||||
}
|
||||
|
||||
bool TryGetTargetEndPoint(out Vector3 endPoint)
|
||||
{
|
||||
if (!m_ClampToMaxDistance || m_PokeFilter == null)
|
||||
{
|
||||
endPoint = Vector3.zero;
|
||||
return false;
|
||||
}
|
||||
|
||||
Vector3 origin = transform.position;
|
||||
Vector3 direction = ComputeRotatedDepthEvaluationAxis(m_PokeFilter.pokeConfiguration);
|
||||
endPoint = origin + direction.normalized * m_MaxDistance;
|
||||
return true;
|
||||
}
|
||||
|
||||
Vector3 ComputeRotatedDepthEvaluationAxis(PokeThresholdData pokeThresholdData)
|
||||
{
|
||||
if (pokeThresholdData == null)
|
||||
return Vector3.zero;
|
||||
|
||||
Vector3 rotatedDepthEvaluationAxis = Vector3.zero;
|
||||
switch (pokeThresholdData.pokeDirection)
|
||||
{
|
||||
case PokeAxis.X:
|
||||
case PokeAxis.NegativeX:
|
||||
rotatedDepthEvaluationAxis = transform.right;
|
||||
break;
|
||||
case PokeAxis.Y:
|
||||
case PokeAxis.NegativeY:
|
||||
rotatedDepthEvaluationAxis = transform.up;
|
||||
break;
|
||||
case PokeAxis.Z:
|
||||
case PokeAxis.NegativeZ:
|
||||
rotatedDepthEvaluationAxis = transform.forward;
|
||||
break;
|
||||
}
|
||||
|
||||
switch (pokeThresholdData.pokeDirection)
|
||||
{
|
||||
case PokeAxis.X:
|
||||
case PokeAxis.Y:
|
||||
case PokeAxis.Z:
|
||||
rotatedDepthEvaluationAxis = -rotatedDepthEvaluationAxis;
|
||||
break;
|
||||
}
|
||||
|
||||
return rotatedDepthEvaluationAxis;
|
||||
}
|
||||
|
||||
void OnValidate()
|
||||
{
|
||||
if (m_PokeFilter == null)
|
||||
{
|
||||
m_PokeFilter = GetComponentInParent<XRPokeFilter>();
|
||||
}
|
||||
|
||||
// Visually update the end point to match the target clamped position
|
||||
if (m_PokeFollowTransform != null && TryGetTargetEndPoint(out var endPoint))
|
||||
m_PokeFollowTransform.position = endPoint;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 07b3638c2f5db5b479ff24c2859713d4
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
Reference in New Issue
Block a user