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