using System;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.EventSystems;
using UnityEngine.VR;
using Framework.Services;
namespace Framework.Actors.Shooter {
/// Draws a red dot at the camera's center
public class RedDotPointer : ScriptComponent {
/// Maximum distance at which the dot will be visible
public float MaxDistance = 10.0f;
/// Prefab that will be used to render the red dot
///
/// Usually this prefab is either a small sphere or a game object with a billboard
/// component that renders a dot texture.
///
public UnityEngine.Object RedDotPrefab;
/// Layers that will prevent the red dot from showing
///
/// Usually you'd set this to zero, but if you use the red dot as a cursor that
/// can only select certain things (world space UI elements, for example), you
/// could set it to all layers but the UI layer to ensure the red dot only appears
/// when the view center is over a UI element.
///
public LayerMask BlockingLayerMask = 0;
/// Layers that can be illuminated by the red dot
///
/// This should be any solid in-game object. Usually you'd exclude glass, water,
/// user interface elements and such.
///
public LayerMask IlluminableLayerMask = Physics.DefaultRaycastLayers;
/// Whether to use a graphics raycast instead of a physics raycast
///
/// Using a graphics raycast is required if your red dot is intended as a cursor
/// on UI elements instead of a crosshair for aiming.
///
public bool UseGraphicsRaycast = false;
/// Scale of the red dot pointer
public float Scale = 1.0f / 75.0f;
/// Called once per visual frame after all animation updates are done
protected void LateUpdate() {
Vector3? target = RaycastPointer();
// Update the state of the red dot. Create the game object used to render
// the red dot in the scene as needed.
bool redDotVisible = target.HasValue;
if(redDotVisible) {
#if UNITY_EDITOR
Debug.DrawLine(transform.position, target.Value);
#endif //UNITY_EDITOR
if(this.redDotGameObject == null) {
createRedDot();
} else {
this.redDotGameObject.SetActive(true);
}
this.redDotGameObject.transform.position = target.Value;
float scale = Vector3.Distance(transform.position, target.Value) * this.Scale;
this.redDotGameObject.transform.localScale = Vector3.one * scale;
} else {
if(this.redDotGameObject != null) {
this.redDotGameObject.SetActive(false);
}
}
}
///
/// Called when the component gets destroyed
/// (including when the game object holding this component is destroyed)
///
protected void OnDestroy() {
if(this.redDotGameObject != null) {
GameObject.Destroy(this.redDotGameObject);
}
}
/// Does a raycast to determine the position of the red dot pointer
/// The world position hit by the red dot pointer
protected Vector3? RaycastPointer() {
if(this.UseGraphicsRaycast) {
// Reuse the pointer event data instance to avoid feeding the garbage collector
if (this.pointerEventData == null) {
this.pointerEventData = new PointerEventData(EventSystem.current);
this.raycastResults = new List();
}
// Pointer is always at the center of the view
if(VRSettings.enabled && VRSettings.isDeviceActive) {
this.pointerEventData.position = new Vector2(
VRSettings.eyeTextureWidth / 2.0f, VRSettings.eyeTextureHeight / 2.0f
);
} else {
this.pointerEventData.position = new Vector2(Screen.width / 2.0f, Screen.height / 2.0f);
}
this.pointerEventData.delta = Vector2.zero;
EventSystem.current.RaycastAll(this.pointerEventData, this.raycastResults);
// Did we hit any UGUI elements?
// - The world position isn't provided (as of Unity 5.6.0f3)
// - The distance is too short
// - There are two hits returned if there's one collision
//
// Solution? Look for first game object hit (same as BaseInputModule.FindFirstRaycast()),
// create plane for hit game object, do raycast onto plane, return that position :-/
for(int index = 0; index < this.raycastResults.Count; ++index) {
bool hitSomething = (this.raycastResults[index].gameObject != null);
if(hitSomething) {
Vector3 vector3;
hitSomething = refineGraphicsRaycast(
this.raycastResults[index].gameObject.transform, out vector3
);
if(hitSomething) {
return vector3;
}
}
}
} else {
RaycastHit hitInfo;
bool hitSomething = Physics.Raycast(
transform.position, transform.forward, out hitInfo,
this.MaxDistance, this.BlockingLayerMask | this.IlluminableLayerMask
);
// Determine if the red dot should be visible. If we hit a layer and that layer
// is illuminable, the red dot will be shown.
if(hitSomething) {
int gameObjectLayerMask = (1 << hitInfo.transform.gameObject.layer);
if((gameObjectLayerMask & this.IlluminableLayerMask) != 0) {
return hitInfo.point;
}
}
}
return null;
}
///
/// Does a second raycast for a canvas element to get a better position
///
/// Transform of the canvas element
///
/// Receives the accurate point at which the canvas element was hit
///
/// True if the canvas element was hit, false if not
///
/// For some reason Unity's graphics raycast through EventSystem.RaycastAll()
/// is terribly inaccurate. This method tries to fix that by doing
/// a raycast against a plane aligned with the canvas element that was hit.
///
private bool refineGraphicsRaycast(Transform canvasElementTransform, out Vector3 hitPoint) {
var canvasPlane = new Plane(
canvasElementTransform.forward, canvasElementTransform.position
);
var ray = new Ray(transform.position, transform.forward);
float realDistance;
if(canvasPlane.Raycast(ray, out realDistance)) {
hitPoint = transform.position + (transform.forward * realDistance);
return true;
} else {
hitPoint = Vector3.zero;
return false;
}
}
/// Creates the game object used to display the red dot in the scene
private void createRedDot() {
if(this.RedDotPrefab == null) {
this.redDotGameObject = new GameObject(name + "-RedDotRenderer");
} else {
this.redDotGameObject = (GameObject)GameObject.Instantiate(this.RedDotPrefab);
this.redDotGameObject.name = name + "-RedDotRenderer";
}
}
/// Game object that is moved around the scene to display the red dot
private GameObject redDotGameObject;
/// Billboard component added to the game object used to draw red dots
private BillboardRenderer redDotBillboardRenderer;
/// Pointer event that is used to make the event system raycast the UI
private PointerEventData pointerEventData;
/// List that stores the most recent raycast results
///
/// Only used within raycastPointer() but turned into an object variable to avoid feeding
/// the garbage collector when querying for UI elements once per frame.
///
private List raycastResults;
}
} // namespace Framework.Actors.Shooter