// Copyright (c) Pixel Crushers. All rights reserved. using PixelCrushers.DialogueSystem.UnityGUI; using System.Collections.Generic; using UnityEngine; namespace PixelCrushers.DialogueSystem { /// /// This component implements a proximity-based selector that allows the player to move into /// range 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 proximity selector tracks the most recent usable object whose trigger the player has /// entered. It 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 ProximitySelector : 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; } /// /// If true, uses a default OnGUI to display a selection message and /// targeting reticle. /// [Tooltip("Use a default OnGUI to display selection message and targeting reticle.")] 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 color of the information labels when the target is in range. /// [Tooltip("Color of the information labels when the target is in range.")] public Color color = Color.yellow; /// /// The text style for the text. /// public TextStyle textStyle = TextStyle.Shadow; /// /// The color of the text style's outline or shadow. /// [Tooltip("Color of the text style's outline or shadow.")] public Color textStyleColor = Color.black; /// /// The default use message. This can be overridden in the target's Usable component. /// [Tooltip("Default use message. This can be overridden in the target's Usable component.")] public string defaultUseMessage = "(spacebar to interact)"; /// /// The key that sends an OnUse message. /// [Tooltip("Key that sends an OnUse message.")] public KeyCode useKey = KeyCode.Space; /// /// The button that sends an OnUse message. /// [Tooltip("Input button that sends an OnUse message.")] public string useButton = "Fire2"; /// /// Tick to enable touch triggering. /// [Tooltip("Enable touch triggering.")] public bool enableTouch = false; /// /// If touch triggering is enabled and there's a touch in this area, /// the selector triggers. /// public ScaledRect touchArea = new ScaledRect(ScaledRect.empty); /// /// If ticked, the OnUse message is broadcast to the usable object's children. /// [Tooltip("Broadcast OnUse message to 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; public UsableUnityEvent onSelectedUsable = new UsableUnityEvent(); public UsableUnityEvent onDeselectedUsable = new UsableUnityEvent(); /// /// 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; /// /// Gets the current usable. /// /// The usable. public Usable CurrentUsable { get { return currentUsable; } set { SetCurrentUsable(value); } } /// /// Gets the GUI style. /// /// The GUI style. public GUIStyle GuiStyle { get { SetGuiStyle(); return guiStyle; } } /// /// Keeps track of which usable objects' triggers the selector is currently inside. /// protected List usablesInRange = new List(); /// /// The current usable that will receive an OnUse message if the player hits the use button. /// protected Usable currentUsable = null; protected string currentHeading = string.Empty; protected string currentUseMessage = string.Empty; protected bool toldListenersHaveUsable = false; /// /// Caches the GUI style to use when displaying the selection message in OnGUI. /// protected GUIStyle guiStyle = null; protected float guiStyleLineHeight = 16f; protected const float MinTimeBetweenUseButton = 0.5f; protected float timeToEnableUseButton = 0; public virtual void Start() { bool found = false; if (GetComponent() != null) found = true; #if USE_PHYSICS2D || !UNITY_2018_1_OR_NEWER if (!found && GetComponent() != null) found = true; #endif if (!found && DialogueDebug.logWarnings) Debug.LogWarning("Dialogue System: Proximity Selector requires a collider, but it has no collider component. If your project is 2D, did you enable 2D support? (Tools > Pixel Crushers > Dialogue System > Welcome Window)", this); } public virtual void OnConversationStart(Transform actor) { timeToEnableUseButton = Time.time + MinTimeBetweenUseButton; } public virtual void OnConversationEnd(Transform actor) { timeToEnableUseButton = Time.time + MinTimeBetweenUseButton; } /// /// Sends an OnUse message to the current selection if the player presses the use button. /// protected virtual void Update() { // Exit if disabled or paused: if (!enabled || (Time.timeScale <= 0)) return; //--- Replaced by OnConversationStart: if (DialogueManager.isConversationActive) timeToEnableUseButton = Time.time + MinTimeBetweenUseButton; // If the currentUsable went missing (was destroyed, deactivated, or we changed scene), tell listeners: if (toldListenersHaveUsable && (currentUsable == null || !currentUsable.enabled || !currentUsable.gameObject.activeInHierarchy)) { SetCurrentUsable(null); OnDeselectedUsableObject(null); toldListenersHaveUsable = false; } // If the player presses the use key/button, send the OnUse message: if (IsUseButtonDown()) UseCurrentSelection(); } 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(); } /// /// Calls OnUse on the current selection. /// public virtual void UseCurrentSelection() { if ((currentUsable != null) && currentUsable.enabled && (currentUsable.gameObject != null) && (Time.time >= timeToEnableUseButton)) { currentUsable.OnUseUsable(); var fromTransform = (actorTransform != null) ? actorTransform : this.transform; if (broadcastToChildren) { currentUsable.gameObject.BroadcastMessage("OnUse", fromTransform, SendMessageOptions.DontRequireReceiver); } else { currentUsable.gameObject.SendMessage("OnUse", fromTransform, SendMessageOptions.DontRequireReceiver); } } } /// /// Checks whether the player has just pressed the use button. /// /// /// true if the use button/key is down; otherwise, false. /// protected virtual bool IsUseButtonDown() { if (DialogueManager.IsDialogueSystemInputDisabled()) return false; if (enableTouch && IsTouchDown()) return true; return ((useKey != KeyCode.None) && InputDeviceManager.IsKeyDown(useKey)) || (!string.IsNullOrEmpty(useButton) && DialogueManager.GetInputButtonDown(useButton)); } protected bool IsTouchDown() { if (Input.touchCount >= 1) { foreach (Touch touch in Input.touches) { Vector2 screenPosition = new Vector2(touch.position.x, Screen.height - touch.position.y); if (touchArea.GetPixelRect().Contains(screenPosition)) return true; } } return false; } /// /// If we entered a trigger, check if it's a usable object. If so, update the selection /// and raise the SelectedUsableObject event. /// /// /// The trigger collider. /// protected void OnTriggerEnter(Collider other) { CheckTriggerEnter(other.gameObject); } /// /// If we just left a trigger, check if it's the current selection. If so, clear the /// selection and raise the DeselectedUsableObject event. If we're still in range of /// any other usables, select one of them. /// /// /// The trigger collider. /// protected void OnTriggerExit(Collider other) { CheckTriggerExit(other.gameObject); } #if USE_PHYSICS2D || !UNITY_2018_1_OR_NEWER /// /// If we entered a 2D trigger, check if it's a usable object. If so, update the selection /// and raise the SelectedUsableObject event. /// /// /// The 2D trigger collider. /// protected void OnTriggerEnter2D(Collider2D other) { CheckTriggerEnter(other.gameObject); } /// /// If we just left a 2D trigger, check if it's the current selection. If so, clear the /// selection and raise the DeselectedUsableObject event. If we're still in range of /// any other usables, select one of them. /// /// /// The 2D trigger collider. /// protected void OnTriggerExit2D(Collider2D other) { CheckTriggerExit(other.gameObject); } #endif protected virtual void CheckTriggerEnter(GameObject other) { if (!enabled) return; Usable usable = other.GetComponent(); if (usable != null && usable.enabled) { SetCurrentUsable(usable); if (!usablesInRange.Contains(usable)) usablesInRange.Add(usable); OnSelectedUsableObject(usable); toldListenersHaveUsable = true; } } protected virtual void CheckTriggerExit(GameObject other) { Usable usable = other.GetComponent(); if (usable != null && usable.enabled) { RemoveUsableFromDetectedList(usable); } } public virtual void RemoveGameObjectFromDetectedList(GameObject other) { if (other != null) RemoveUsableFromDetectedList(other.GetComponent()); } public virtual void RemoveUsableFromDetectedList(Usable usable) { if (usable != null) { if (usablesInRange.Contains(usable)) usablesInRange.Remove(usable); if (currentUsable == usable) { OnDeselectedUsableObject(usable); toldListenersHaveUsable = false; Usable newUsable = null; usablesInRange.RemoveAll(x => x == null || !x.gameObject.activeInHierarchy); if (usablesInRange.Count > 0) { newUsable = usablesInRange[0]; OnSelectedUsableObject(newUsable); toldListenersHaveUsable = true; } SetCurrentUsable(newUsable); } } } public virtual void SetCurrentUsable(Usable usable) { currentUsable = usable; if (usable != null) { currentHeading = currentUsable.GetName(); currentUseMessage = DialogueManager.GetLocalizedText(string.IsNullOrEmpty(currentUsable.overrideUseMessage) ? defaultUseMessage : currentUsable.overrideUseMessage); } else { currentHeading = string.Empty; currentUseMessage = string.Empty; } } /// /// 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 || currentUsable != null)) { SetGuiStyle(); } if (currentUsable != null) { GUI.skin = guiSkin; UnityGUITools.DrawText(new Rect(0, 0, Screen.width, Screen.height), currentHeading, guiStyle, textStyle, textStyleColor); UnityGUITools.DrawText(new Rect(0, guiStyleLineHeight, Screen.width, Screen.height), currentUseMessage, guiStyle, textStyle, textStyleColor); } } 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; guiStyle.normal.textColor = color; guiStyleLineHeight = guiStyle.CalcSize(new GUIContent("Ay")).y; } } }