// Copyright (c) Pixel Crushers. All rights reserved. using UnityEngine; using UnityEngine.Events; using PixelCrushers.DialogueSystem.UnityGUI; namespace PixelCrushers.DialogueSystem { /// /// This component implements a selector that allows the player to target and use a usable /// object. /// /// To mark an object usable, add the Usable component and a collider to it. The object's /// layer should be in the layer mask specified on the Selector component. /// /// The selector can be configured to target items under the mouse cursor or the middle of /// the screen. When a usable object is targeted, the selector displays a targeting reticle /// and information about the object. If the target is in range, the inRange reticle /// texture is displayed; otherwise the outOfRange texture is displayed. /// /// If the player presses the use button (which defaults to spacebar and Fire2), the targeted /// object will receive an "OnUse" message. /// /// You can hook into SelectedUsableObject and DeselectedUsableObject to get notifications /// when the current target has changed. /// [AddComponentMenu("")] // Use wrapper. public class Selector : MonoBehaviour { /// /// This class defines the textures and size of the targeting reticle. /// [System.Serializable] public class Reticle { public Texture2D inRange; public Texture2D outOfRange; public float width = 64f; public float height = 64f; } /// /// Specifies how to target: center of screen or under the mouse cursor. This is where it raycasts to. /// public enum SelectAt { CenterOfScreen, MousePosition, CustomPosition } /// /// Specifies whether to compute range from the targeted object (distance to the camera /// or distance to the selector's game object). /// public enum DistanceFrom { Camera, GameObject } /// /// Specifies whether to do 2D or 3D raycasts. /// public enum Dimension { In2D, In3D } /// /// The default layermask is just the Default layer. /// private static LayerMask DefaultLayer = 1; /// /// How to target (center of screen or under mouse cursor). Default is center of screen. /// [Tooltip("How to target. This is where the raycast points to.")] public SelectAt selectAt = SelectAt.CenterOfScreen; /// /// The layer mask to use when targeting objects. Objects on others layers are ignored. /// [Tooltip("Layer mask to use when targeting objects; objects on others layers are ignored.")] public LayerMask layerMask = DefaultLayer; /// /// How to compute range to targeted object. Default is from the camera. /// [Tooltip("How to compute range to targeted object.")] public DistanceFrom distanceFrom = DistanceFrom.Camera; /// /// The max selection distance. The selector won't target objects farther than this. /// [Tooltip("Don't target objects farther than this; targets may still be unusable if beyond their usable range.")] public float maxSelectionDistance = 30f; /// /// Specifies whether to run 2D or 3D raycasts. /// public Dimension runRaycasts = Dimension.In3D; /// /// Set true to check all objects within the raycast range for usables. /// If false, the check stops on the first hit, even if it's not a usable. /// This prevents selection through walls. /// [Tooltip("Check all objects within raycast range for usables, even passing through obstacles.")] public bool raycastAll = false; /// /// If true, uses a default OnGUI to display a selection message and /// targeting reticle. /// [Tooltip("")] public bool useDefaultGUI = true; /// /// The GUI skin to use for the target's information (name and use message). /// [Tooltip("GUI skin to use for the target's information (name and use message).")] public GUISkin guiSkin; /// /// The name of the GUI style in the skin. /// [Tooltip("Name of the GUI style in the skin.")] public string guiStyleName = "label"; /// /// The text alignment. /// public TextAnchor alignment = TextAnchor.UpperCenter; /// /// The text style for the text. /// public TextStyle textStyle = TextStyle.Shadow; /// /// The color of the text style's outline or shadow. /// public Color textStyleColor = Color.black; /// /// The color of the information labels when the target is in range. /// [Tooltip("Color of the information labels when target is in range.")] public Color inRangeColor = Color.yellow; /// /// The color of the information labels when the target is out of range. /// [Tooltip("Color of the information labels when target is out of range.")] public Color outOfRangeColor = Color.gray; /// /// The reticle images. /// public Reticle reticle; /// /// The key that sends an OnUse message. /// public KeyCode useKey = KeyCode.Space; /// /// The button that sends an OnUse message. /// public string useButton = "Fire2"; /// /// The default use message. This can be overridden in the target's Usable component. /// [Tooltip("Default use message; can be overridden in the target's Usable component")] public string defaultUseMessage = "(spacebar to interact)"; /// /// If ticked, the OnUse message is broadcast to the usable object's children. /// [Tooltip("Tick to also broadcast to the usable object's children")] public bool broadcastToChildren = true; /// /// The actor transform to send with OnUse. Defaults to this transform. /// [Tooltip("Actor transform to send with OnUse; defaults to this transform")] public Transform actorTransform = null; /// /// If set, show this alert message if attempt to use something beyond its usable range. /// [Tooltip("If set, show this alert message if attempt to use something beyond its usable range")] public string tooFarMessage = string.Empty; public UsableUnityEvent onSelectedUsable = new UsableUnityEvent(); public UsableUnityEvent onDeselectedUsable = new UsableUnityEvent(); /// /// The too far event handler. /// public UnityEvent tooFarEvent = new UnityEvent(); /// /// If true, draws gizmos. /// [Tooltip("Tick to draw gizmos in Scene view")] public bool debug = false; /// /// Gets or sets the custom position used when the selectAt is set to SelectAt.CustomPosition. /// You can use, for example, to slide around a targeting icon onscreen using a gamepad. /// /// /// The custom position. /// public Vector3 CustomPosition { get; set; } /// /// Gets the current selection. /// /// The selection. public Usable CurrentUsable { get { return usable; } set { SetCurrentUsable(value); } } /// /// Gets the distance from the current usable. /// /// The current distance. public float CurrentDistance { get { return distance; } } /// /// Gets the GUI style. /// /// The GUI style. public GUIStyle GuiStyle { get { SetGuiStyle(); return guiStyle; } } /// /// Occurs when the selector has targeted a usable object. /// public event SelectedUsableObjectDelegate SelectedUsableObject = null; /// /// Occurs when the selector has untargeted a usable object. /// public event DeselectedUsableObjectDelegate DeselectedUsableObject = null; protected GameObject selection = null; // Currently under the selection point. protected Usable usable = null; // Usable component of the current selection. protected GameObject clickedDownOn = null; // Selection when the mouse button was pressed down. protected string heading = string.Empty; protected string useMessage = string.Empty; protected float distance = 0; protected GUIStyle guiStyle = null; protected float guiStyleLineHeight = 16f; protected Ray lastRay = new Ray(); protected RaycastHit lastHit = new RaycastHit(); protected RaycastHit[] lastHits = null; protected int numLastHits = 0; private const int MaxHits = 100; #if USE_PHYSICS2D || !UNITY_2018_1_OR_NEWER RaycastHit2D[] lastHits2D = null; #endif protected bool hasReportedInvalidCamera = false; public virtual void Start() { if (Camera.main == null) Debug.LogError("Dialogue System: The scene is missing a camera tagged 'MainCamera'. The Selector may not behave the way you expect.", this); } /// /// Runs a raycast to see what's under the selection point. Updates the selection and /// calls the selection delegates if the selection has changed. If the player hits the /// use button, sends an OnUse message to the selection. /// protected virtual void Update() { // Exit if disabled or paused: if (!enabled || (Time.timeScale <= 0)) return; // Exit if there's no camera: if (UnityEngine.Camera.main == null) return; // Exit if using mouse selection and is over a UI element: if ((selectAt == SelectAt.MousePosition) && (UnityEngine.EventSystems.EventSystem.current != null) && UnityEngine.EventSystems.EventSystem.current.IsPointerOverGameObject()) return; // Raycast 2D or 3D: switch (runRaycasts) { case Dimension.In2D: Run2DRaycast(); break; default: case Dimension.In3D: Run3DRaycast(); break; } // If the player presses the use key/button on a target: if (IsUseButtonDown()) UseCurrentSelection(); } /// /// Calls OnUse on the current selection. /// public virtual void UseCurrentSelection() { if (usable != null && usable.enabled && usable.gameObject.activeInHierarchy) { clickedDownOn = null; if (distance <= usable.maxUseDistance) { usable.OnUseUsable(); // If within range, send the OnUse message: var fromTransform = (actorTransform != null) ? actorTransform : this.transform; if (broadcastToChildren) { usable.gameObject.BroadcastMessage("OnUse", fromTransform, SendMessageOptions.DontRequireReceiver); } else { usable.gameObject.SendMessage("OnUse", fromTransform, SendMessageOptions.DontRequireReceiver); } } else { // Otherwise report too far if configured to do so: if (!string.IsNullOrEmpty(tooFarMessage)) { DialogueManager.ShowAlert(tooFarMessage); } tooFarEvent.Invoke(); } } } protected virtual void Run2DRaycast() { #if USE_PHYSICS2D || !UNITY_2018_1_OR_NEWER var mainCamera = UnityEngine.Camera.main; if (!mainCamera.orthographic && !hasReportedInvalidCamera) { hasReportedInvalidCamera = true; Debug.LogWarning("In 2D mode, Selector requires an orthographic camera.", this); } if (raycastAll) { // Run Physics2D.RaycastAll: if (lastHits2D == null) lastHits2D = new RaycastHit2D[MaxHits]; int numHits = Physics2D.RaycastNonAlloc(mainCamera.ScreenToWorldPoint(GetSelectionPoint()), Vector2.zero, lastHits2D, maxSelectionDistance, layerMask); bool foundUsable = false; for (int i = 0; i < numHits; i++) { var hit = lastHits2D[i]; float hitDistance = (distanceFrom == DistanceFrom.Camera) ? 0 : Vector3.Distance(gameObject.transform.position, hit.collider.transform.position); if (selection == hit.collider.gameObject) { foundUsable = true; distance = hitDistance; break; } else { Usable hitUsable = hit.collider.GetComponent(); if (hitUsable != null && hitUsable.enabled) { foundUsable = true; distance = hitDistance; SetCurrentUsable(hitUsable); break; } } } if (!foundUsable) { DeselectTarget(); } } else { // Cast a ray and see what we hit: RaycastHit2D hit; hit = Physics2D.Raycast(mainCamera.ScreenToWorldPoint(GetSelectionPoint()), Vector2.zero, maxSelectionDistance, layerMask); if (hit.collider != null) { distance = (distanceFrom == DistanceFrom.Camera) ? 0 : Vector3.Distance(gameObject.transform.position, hit.collider.transform.position); if (selection != hit.collider.gameObject) { Usable hitUsable = hit.collider.GetComponent(); if (hitUsable != null && hitUsable.enabled) { heading = string.Empty; useMessage = string.Empty; SetCurrentUsable(hitUsable); } else { DeselectTarget(); } } } else { DeselectTarget(); } } #endif } protected virtual void Run3DRaycast() { Ray ray = UnityEngine.Camera.main.ScreenPointToRay(GetSelectionPoint()); lastRay = ray; // New Variable rayCastDistance is used below for the raycasts instead of maxSelectionDistance to be able to set it to infinity (if using DistanceFrom.GameObject) instead of maxSelectionDistance: // Credit: Daniel D. (Thank you!) float raycastDistance = (distanceFrom == DistanceFrom.GameObject) ? Mathf.Infinity : maxSelectionDistance; if (raycastAll) { // Run RaycastAll: if (lastHits == null) lastHits = new RaycastHit[MaxHits]; numLastHits = Physics.RaycastNonAlloc(ray, lastHits, raycastDistance, layerMask); bool foundUsable = false; for (int i = 0; i < numLastHits; i++) { var hit = lastHits[i]; float hitDistance = (distanceFrom == DistanceFrom.Camera) ? hit.distance : Vector3.Distance(gameObject.transform.position, hit.collider.transform.position); if (selection == hit.collider.gameObject) { foundUsable = true; distance = hitDistance; break; } else { Usable hitUsable = hit.collider.GetComponent(); if (hitUsable != null && hitUsable.enabled && hitDistance <= maxSelectionDistance) { foundUsable = true; distance = hitDistance; SetCurrentUsable(hitUsable); break; } } } if (!foundUsable) { DeselectTarget(); } } else { // Cast a ray and see what we hit: RaycastHit hit; if (Physics.Raycast(ray, out hit, maxSelectionDistance, layerMask)) { distance = (distanceFrom == DistanceFrom.Camera) ? hit.distance : Vector3.Distance(gameObject.transform.position, hit.collider.transform.position); Usable hitUsable = hit.collider.GetComponent(); if (hitUsable != null && hitUsable.enabled) { if (selection != hit.collider.gameObject) { SetCurrentUsable(hitUsable); } } else { DeselectTarget(); } } else { DeselectTarget(); } lastHit = hit; } } public virtual void SetCurrentUsable(Usable usable) { if (usable == null) { DeselectTarget(); } else { this.usable = usable; selection = usable.gameObject; heading = string.Empty; useMessage = string.Empty; OnSelectedUsableObject(usable); } } protected void OnSelectedUsableObject(Usable usable) { if (SelectedUsableObject != null) SelectedUsableObject(usable); onSelectedUsable.Invoke(usable); if (usable != null) usable.OnSelectUsable(); } protected void OnDeselectedUsableObject(Usable usable) { if (DeselectedUsableObject != null) DeselectedUsableObject(usable); onDeselectedUsable.Invoke(usable); if (usable != null) usable.OnDeselectUsable(); } protected virtual void DeselectTarget() { OnDeselectedUsableObject(usable); usable = null; selection = null; heading = string.Empty; useMessage = string.Empty; } protected virtual bool IsUseButtonDown() { if (DialogueManager.IsDialogueSystemInputDisabled()) return false; // First check for button down to remember what was selected at the time: if (!string.IsNullOrEmpty(useButton) && DialogueManager.getInputButtonDown(useButton)) { clickedDownOn = selection; } // Check for use key or button (only if releasing button on same selection): if ((useKey != KeyCode.None) && InputDeviceManager.IsKeyDown(useKey)) return true; if (!string.IsNullOrEmpty(useButton)) { if (DialogueManager.instance != null && DialogueManager.getInputButtonDown == DialogueManager.instance.StandardGetInputButtonDown) { return InputDeviceManager.IsButtonUp(useButton) && (selection == clickedDownOn); } else { return DialogueManager.GetInputButtonDown(useButton); } } return false; } protected virtual Vector3 GetSelectionPoint() { switch (selectAt) { case SelectAt.MousePosition: return Input.mousePosition; case SelectAt.CustomPosition: return CustomPosition; default: case SelectAt.CenterOfScreen: return new Vector3(Screen.width / 2, Screen.height / 2); } } /// /// If useDefaultGUI is true and a usable object has been targeted, this method /// draws a selection message and targeting reticle. /// public virtual void OnGUI() { if (!useDefaultGUI) return; if (guiStyle == null && (Event.current.type == EventType.Repaint || usable != null)) { SetGuiStyle(); } if (usable != null) { bool inUseRange = (distance <= usable.maxUseDistance); guiStyle.normal.textColor = inUseRange ? inRangeColor : outOfRangeColor; if (string.IsNullOrEmpty(heading)) { heading = usable.GetName(); useMessage = DialogueManager.GetLocalizedText(string.IsNullOrEmpty(usable.overrideUseMessage) ? defaultUseMessage : usable.overrideUseMessage); } UnityGUITools.DrawText(new Rect(0, 0, Screen.width, Screen.height), heading, guiStyle, textStyle, textStyleColor); UnityGUITools.DrawText(new Rect(0, guiStyleLineHeight, Screen.width, Screen.height), useMessage, guiStyle, textStyle, textStyleColor); Texture2D reticleTexture = inUseRange ? reticle.inRange : reticle.outOfRange; if (reticleTexture != null) GUI.Label(new Rect(0.5f * (Screen.width - reticle.width), 0.5f * (Screen.height - reticle.height), reticle.width, reticle.height), reticleTexture); } } protected void SetGuiStyle() { guiSkin = UnityGUITools.GetValidGUISkin(guiSkin); GUI.skin = guiSkin; guiStyle = new GUIStyle(string.IsNullOrEmpty(guiStyleName) ? GUI.skin.label : (GUI.skin.FindStyle(guiStyleName) ?? GUI.skin.label)); guiStyle.alignment = alignment; guiStyleLineHeight = guiStyle.CalcSize(new GUIContent("Ay")).y; } /// /// Draws raycast result gizmos. /// public virtual void OnDrawGizmos() { if (!debug) return; Gizmos.color = Color.yellow; Gizmos.DrawLine(lastRay.origin, lastRay.origin + lastRay.direction * maxSelectionDistance); if (raycastAll) { if (lastHits != null) { for (int i = 0; i < numLastHits; i++) { var hit = lastHits[i]; var usable = hit.collider.GetComponent(); bool isUsable = (usable != null) && usable.enabled; Gizmos.color = isUsable ? Color.green : Color.red; Gizmos.DrawWireSphere(hit.point, 0.2f); } } } else { if (lastHit.collider != null) { var usable = lastHit.collider.GetComponent(); bool isUsable = (usable != null) && usable.enabled; Gizmos.color = isUsable ? Color.green : Color.red; Gizmos.DrawWireSphere(lastHit.point, 0.2f); } } } } }