FittsLaw/Assets/GoogleVR/Scripts/EventSystem/GvrBasePointer.cs
2018-10-08 23:54:11 -04:00

515 lines
20 KiB
C#

// Copyright 2017 Google Inc. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
using UnityEngine;
using UnityEngine.EventSystems;
/// This abstract class should be implemented for pointer based input, and used with
/// the GvrPointerInputModule script.
///
/// It provides methods called on pointer interaction with in-game objects and UI,
/// trigger events, and 'BaseInputModule' class state changes.
///
/// To have the methods called, an instance of this (implemented) class must be
/// registered with the **GvrPointerManager** script in 'Start' by calling
/// GvrPointerInputModule.OnPointerCreated.
///
/// This abstract class should be implemented by pointers doing 1 of 2 things:
/// 1. Responding to movement of the users head (Cardboard gaze-based-pointer).
/// 2. Responding to the movement of the daydream controller (Daydream 3D pointer).
public abstract class GvrBasePointer : MonoBehaviour, IGvrControllerInputDeviceReceiver {
public enum RaycastMode {
/// Casts a ray from the camera through the target of the pointer.
/// This is ideal for reticles that are always rendered on top.
/// The object that is selected will always be the object that appears
/// underneath the reticle from the perspective of the camera.
/// This also prevents the reticle from appearing to "jump" when it starts/stops hitting an object.
///
/// Recommended for reticles that are always rendered on top such as the GvrReticlePointer
/// prefab which is used for cardboard apps.
///
/// Note: This will prevent the user from pointing around an object to hit something that is out of sight.
/// This isn't a problem in a typical use case.
///
/// When used with the standard daydream controller,
/// the hit detection will not account for the laser correctly for objects that are closer to the
/// camera than the end of the laser.
/// In that case, it is recommended to do one of the following things:
///
/// 1. Hide the laser.
/// 2. Use a full-length laser pointer in Direct mode.
/// 3. Use the Hybrid raycast mode.
Camera,
/// Cast a ray directly from the pointer origin.
///
/// Recommended for full-length laser pointers.
Direct,
/// Default method for casting ray.
///
/// Combines the Camera and Direct raycast modes.
/// Uses a Direct ray up until the CameraRayIntersectionDistance, and then switches to use
/// a Camera ray starting from the point where the two rays intersect.
///
/// Recommended for use with the standard settings of the GvrControllerPointer prefab.
/// This is the most versatile raycast mode. Like Camera mode, this prevents the reticle
/// appearing jumpy. Additionally, it still allows the user to target objects that are close
/// to them by using the laser as a visual reference.
Hybrid,
}
/// Represents a ray segment for a series of intersecting rays.
/// This is useful for Hybrid raycast mode, which uses two sequential rays.
public struct PointerRay {
/// The ray for this segment of the pointer.
public Ray ray;
/// The distance along the pointer from the origin of the first ray to this ray.
public float distanceFromStart;
/// Distance that this ray extends to.
public float distance;
}
/// Determines which raycast mode to use for this raycaster.
/// • Camera - Ray is cast from the camera through the pointer.
/// • Direct - Ray is cast forward from the pointer.
/// • Hybrid - Begins with a Direct ray and transitions to a Camera ray.
[Tooltip("Determines which raycast mode to use for this raycaster.\n" +
" • Camera - Ray is cast from camera.\n" +
" • Direct - Ray is cast from pointer.\n" +
" • Hybrid - Transitions from Direct ray to Camera ray.")]
public RaycastMode raycastMode = RaycastMode.Hybrid;
/// Determines the eventCamera for _GvrPointerPhysicsRaycaster_ and _GvrPointerGraphicRaycaster_.
/// Additionaly, this is used to control what camera to use when calculating the Camera ray for
/// the Hybrid and Camera raycast modes.
[Tooltip("Optional: Use a camera other than Camera.main.")]
public Camera overridePointerCamera;
#if UNITY_EDITOR
/// Determines if the rays used for raycasting will be drawn in the editor.
[Tooltip("Determines if the rays used for raycasting will be drawn in the editor.")]
public bool drawDebugRays = false;
#endif // UNITY_EDITOR
/// Convenience function to access what the pointer is currently hitting.
public RaycastResult CurrentRaycastResult {
get {
return GvrPointerInputModule.CurrentRaycastResult;
}
}
[System.Obsolete("Replaced by CurrentRaycastResult.worldPosition")]
public Vector3 PointerIntersection {
get {
RaycastResult raycastResult = CurrentRaycastResult;
return raycastResult.worldPosition;
}
}
[System.Obsolete("Replaced by CurrentRaycastResult.gameObject != null")]
public bool IsPointerIntersecting {
get {
RaycastResult raycastResult = CurrentRaycastResult;
return raycastResult.gameObject != null;
}
}
/// This is used to determine if the enterRadius or the exitRadius should be used for the raycast.
/// It is set by GvrPointerInputModule and doesn't need to be controlled manually.
public bool ShouldUseExitRadiusForRaycast { get; set; }
/// If ShouldUseExitRadiusForRaycast is true, returns the exitRadius.
/// Otherwise, returns the enterRadius.
public float CurrentPointerRadius {
get {
float enterRadius, exitRadius;
GetPointerRadius(out enterRadius, out exitRadius);
if (ShouldUseExitRadiusForRaycast) {
return exitRadius;
} else {
return enterRadius;
}
}
}
/// Returns the transform that represents this pointer.
/// It is used by GvrBasePointerRaycaster as the origin of the ray.
public virtual Transform PointerTransform {
get {
return transform;
}
}
public GvrControllerInputDevice ControllerInputDevice { get; set; }
/// If true, the trigger was just pressed. This is an event flag:
/// it will be true for only one frame after the event happens.
/// Defaults to mouse button 0 down on Cardboard or
/// ControllerInputDevice.GetButtonDown(TouchPadButton) on Daydream.
/// Can be overridden to change the trigger.
public virtual bool TriggerDown {
get {
bool isTriggerDown = Input.GetMouseButtonDown(0);
if (ControllerInputDevice != null) {
isTriggerDown |=
ControllerInputDevice.GetButtonDown(GvrControllerButton.TouchPadButton);
}
return isTriggerDown;
}
}
/// If true, the trigger is currently being pressed. This is not
/// an event: it represents the trigger's state (it remains true while the trigger is being
/// pressed).
/// Defaults to mouse button 0 state on Cardboard or
/// ControllerInputDevice.GetButton(TouchPadButton) on Daydream.
/// Can be overridden to change the trigger.
public virtual bool Triggering {
get {
bool isTriggering = Input.GetMouseButton(0);
if (ControllerInputDevice != null) {
isTriggering |=
ControllerInputDevice.GetButton(GvrControllerButton.TouchPadButton);
}
return isTriggering;
}
}
/// If true, the trigger was just released. This is an event flag:
/// it will be true for only one frame after the event happens.
/// Defaults to mouse button 0 up on Cardboard or
/// ControllerInputDevice.GetButtonUp(TouchPadButton) on Daydream.
/// Can be overridden to change the trigger.
public virtual bool TriggerUp {
get {
bool isTriggerUp = Input.GetMouseButtonUp(0);
if (ControllerInputDevice == null) {
isTriggerUp |=
ControllerInputDevice.GetButtonUp(GvrControllerButton.TouchPadButton);
}
return isTriggerUp;
}
}
/// If true, the user just started touching the touchpad. This is an event flag (it is true
/// for only one frame after the event happens, then reverts to false).
/// Used by _GvrPointerScrollInput_ to generate OnScroll events using Unity's Event System.
/// Defaults to ControllerInputDevice.GetButtonDown(TouchPadTouch), can be overridden to change
/// the input source.
public virtual bool TouchDown {
get {
if (ControllerInputDevice == null) {
return false;
} else {
return ControllerInputDevice.GetButtonDown(GvrControllerButton.TouchPadTouch);
}
}
}
/// If true, the user is currently touching the touchpad.
/// Used by _GvrPointerScrollInput_ to generate OnScroll events using Unity's Event System.
/// Defaults to ControllerInputDevice.GetButton(TouchPadTouch), can be overridden to change
/// the input source.
public virtual bool IsTouching {
get {
if (ControllerInputDevice == null) {
return false;
} else {
return ControllerInputDevice.GetButton(GvrControllerButton.TouchPadTouch);
}
}
}
/// If true, the user just stopped touching the touchpad. This is an event flag (it is true
/// for only one frame after the event happens, then reverts to false).
/// Used by _GvrPointerScrollInput_ to generate OnScroll events using Unity's Event System.
/// Defaults to ControllerInputDevice.GetButtonUp(TouchPadTouch), can be overridden to change
/// the input source.
public virtual bool TouchUp {
get {
if (ControllerInputDevice == null) {
return false;
} else {
return ControllerInputDevice.GetButtonUp(GvrControllerButton.TouchPadTouch);
}
}
}
/// Position of the current touch, if touching the touchpad.
/// If not touching, this is the position of the last touch (when the finger left the touchpad).
/// The X and Y range is from 0 to 1.
/// (0, 0) is the top left of the touchpad and (1, 1) is the bottom right of the touchpad.
/// Used by `GvrPointerScrollInput` to generate OnScroll events using Unity's Event System.
/// Defaults to `ControllerInputDevice.TouchPos` but translated to top-left-relative coordinates
/// for backwards compatibility. Can be overridden to change the input source.
public virtual Vector2 TouchPos {
get {
if (ControllerInputDevice == null) {
return Vector2.zero;
} else {
Vector2 touchPos = ControllerInputDevice.TouchPos;
touchPos.x = (touchPos.x / 2.0f) + 0.5f;
touchPos.y = (-touchPos.y / 2.0f) + 0.5f;
return touchPos;
}
}
}
/// Returns the end point of the pointer when it is MaxPointerDistance away from the origin.
public virtual Vector3 MaxPointerEndPoint {
get {
Transform pointerTransform = PointerTransform;
if (pointerTransform == null) {
return Vector3.zero;
}
Vector3 maxEndPoint = GetPointAlongPointer(MaxPointerDistance);
return maxEndPoint;
}
}
/// If true, the pointer will be used for generating input events by _GvrPointerInputModule_.
public virtual bool IsAvailable {
get {
Transform pointerTransform = PointerTransform;
if (pointerTransform == null) {
return false;
}
if (!enabled) {
return false;
}
return pointerTransform.gameObject.activeInHierarchy;
}
}
/// When using the Camera raycast mode, this is used to calculate
/// where the ray from the pointer will intersect with the ray from the camera.
public virtual float CameraRayIntersectionDistance {
get {
return MaxPointerDistance;
}
}
public Camera PointerCamera {
get {
if (overridePointerCamera != null) {
return overridePointerCamera;
}
return Camera.main;
}
}
/// Returns the max distance from the pointer that raycast hits will be detected.
public abstract float MaxPointerDistance { get; }
/// Called when the pointer is facing a valid GameObject. This can be a 3D
/// or UI element.
///
/// **raycastResult** is the hit detection result for the object being pointed at.
/// **isInteractive** is true if the object being pointed at is interactive.
public abstract void OnPointerEnter(RaycastResult raycastResult, bool isInteractive);
/// Called every frame the user is still pointing at a valid GameObject. This
/// can be a 3D or UI element.
///
/// **raycastResult** is the hit detection result for the object being pointed at.
/// **isInteractive** is true if the object being pointed at is interactive.
public abstract void OnPointerHover(RaycastResult raycastResultResult, bool isInteractive);
/// Called when the pointer no longer faces an object previously
/// intersected with a ray projected from the camera.
/// This is also called just before **OnInputModuleDisabled**
/// previousObject will be null in this case.
///
/// **previousObject** is the object that was being pointed at the previous frame.
public abstract void OnPointerExit(GameObject previousObject);
/// Called when a click is initiated.
public abstract void OnPointerClickDown();
/// Called when click is finished.
public abstract void OnPointerClickUp();
/// Return the radius of the pointer. It is used by GvrPointerPhysicsRaycaster when
/// searching for valid pointer targets. If a radius is 0, then a ray is used to find
/// a valid pointer target. Otherwise it will use a SphereCast.
/// The *enterRadius* is used for finding new targets while the *exitRadius*
/// is used to see if you are still nearby the object currently pointed at
/// to avoid a flickering effect when just at the border of the intersection.
///
/// NOTE: This is only works with GvrPointerPhysicsRaycaster. To use it with uGUI,
/// add 3D colliders to your canvas elements.
public abstract void GetPointerRadius(out float enterRadius, out float exitRadius);
/// Returns a point in worldspace a specified distance along the pointer.
/// What this point will be is different depending on the raycastMode.
///
/// Because raycast modes differ, use this function instead of manually calculating a point
/// projected from the pointer.
public Vector3 GetPointAlongPointer(float distance) {
PointerRay pointerRay = GetRayForDistance(distance);
return pointerRay.ray.GetPoint(distance - pointerRay.distanceFromStart);
}
/// Returns the ray used for projecting points out of the pointer for the given distance.
/// In Hybrid raycast mode, the ray will be different depending upon the distance.
/// In Camera or Direct raycast mode, the ray will always be the same.
public PointerRay GetRayForDistance(float distance) {
PointerRay result = new PointerRay();
if (raycastMode == RaycastMode.Hybrid) {
float directDistance = CameraRayIntersectionDistance;
if (distance < directDistance) {
result = CalculateHybridRay(this, RaycastMode.Direct);
} else {
result = CalculateHybridRay(this, RaycastMode.Camera);
}
} else {
result = CalculateRay(this, raycastMode);
}
return result;
}
/// Calculates the ray for a given Raycast mode.
/// Will throw an exception if the raycast mode Hybrid is passed in.
/// If you need to calculate the ray for the direct or camera segment of the Hybrid raycast,
/// use CalculateHybridRay instead.
public static PointerRay CalculateRay(GvrBasePointer pointer, RaycastMode mode) {
PointerRay result = new PointerRay();
if (pointer == null || !pointer.IsAvailable) {
Debug.LogError("Cannot calculate ray when the pointer isn't available.");
return result;
}
Transform pointerTransform = pointer.PointerTransform;
if (pointerTransform == null) {
Debug.LogError("Cannot calculate ray when pointerTransform is null.");
return result;
}
result.distance = pointer.MaxPointerDistance;
switch (mode) {
case RaycastMode.Camera:
Camera camera = pointer.PointerCamera;
if (camera == null) {
Debug.LogError("Cannot calculate ray because pointer.PointerCamera is null." +
"To fix this, either tag a Camera as \"MainCamera\" or set overridePointerCamera.");
return result;
}
Vector3 rayPointerStart = pointerTransform.position;
Vector3 rayPointerEnd = rayPointerStart +
(pointerTransform.forward * pointer.CameraRayIntersectionDistance);
Vector3 cameraLocation = camera.transform.position;
Vector3 finalRayDirection = rayPointerEnd - cameraLocation;
finalRayDirection.Normalize();
Vector3 finalRayStart = cameraLocation + (finalRayDirection * camera.nearClipPlane);
result.ray = new Ray(finalRayStart, finalRayDirection);
break;
case RaycastMode.Direct:
result.ray = new Ray(pointerTransform.position, pointerTransform.forward);
break;
default:
throw new UnityException("Invalid RaycastMode " + mode + " passed into CalculateRay.");
}
return result;
}
/// Calculates the ray for the segment of the Hybrid raycast determined by the raycast mode
/// passed in. Throws an exception if Hybrid is passed in.
public static PointerRay CalculateHybridRay(GvrBasePointer pointer, RaycastMode hybridMode) {
PointerRay result;
switch (hybridMode) {
case RaycastMode.Direct:
result = CalculateRay(pointer, hybridMode);
result.distance = pointer.CameraRayIntersectionDistance;
break;
case RaycastMode.Camera:
result = CalculateRay(pointer, hybridMode);
PointerRay directRay = CalculateHybridRay(pointer, RaycastMode.Direct);
result.ray.origin = directRay.ray.GetPoint(directRay.distance);
result.distanceFromStart = directRay.distance;
result.distance = pointer.MaxPointerDistance - directRay.distance;
break;
default:
throw new UnityException("Invalid RaycastMode " + hybridMode + " passed into CalculateHybridRay.");
}
return result;
}
protected virtual void Start() {
GvrPointerInputModule.OnPointerCreated(this);
}
#if UNITY_EDITOR
protected virtual void OnDrawGizmos() {
if (drawDebugRays && Application.isPlaying && isActiveAndEnabled) {
switch (raycastMode) {
case RaycastMode.Camera:
// Camera line.
Gizmos.color = Color.green;
PointerRay pointerRay = CalculateRay(this, RaycastMode.Camera);
Gizmos.DrawLine(pointerRay.ray.origin, pointerRay.ray.GetPoint(pointerRay.distance));
Camera camera = PointerCamera;
// Pointer to intersection dotted line.
Vector3 intersection =
PointerTransform.position + (PointerTransform.forward * CameraRayIntersectionDistance);
UnityEditor.Handles.DrawDottedLine(PointerTransform.position, intersection, 1.0f);
break;
case RaycastMode.Direct:
// Direct line.
Gizmos.color = Color.blue;
pointerRay = CalculateRay(this, RaycastMode.Direct);
Gizmos.DrawLine(pointerRay.ray.origin, pointerRay.ray.GetPoint(pointerRay.distance));
break;
case RaycastMode.Hybrid:
// Direct line.
Gizmos.color = Color.blue;
pointerRay = CalculateHybridRay(this, RaycastMode.Direct);
Gizmos.DrawLine(pointerRay.ray.origin, pointerRay.ray.GetPoint(pointerRay.distance));
// Camera line.
Gizmos.color = Color.green;
pointerRay = CalculateHybridRay(this, RaycastMode.Camera);
Gizmos.DrawLine(pointerRay.ray.origin, pointerRay.ray.GetPoint(pointerRay.distance));
// Camera to intersection dotted line.
camera = PointerCamera;
if (camera != null) {
UnityEditor.Handles.DrawDottedLine(camera.transform.position, pointerRay.ray.origin, 1.0f);
}
break;
default:
break;
}
}
}
#endif // UNITY_EDITOR
}