// Copyright (c) Pixel Crushers. All rights reserved. using UnityEngine; using UnityEngine.Events; using System.Collections; using System.Collections.Generic; namespace PixelCrushers.DialogueSystem { /// /// Response menu controls for UnityUIDialogueUI. /// [System.Serializable] public class UnityUIResponseMenuControls : AbstractUIResponseMenuControls { /// /// The panel containing the response menu controls. A panel is optional, but you may want one /// so you can include a background image, panel-wide effects, etc. /// [Tooltip("The panel containing the response menu controls. A panel is optional, but you may want one so you can include a background image, panel-wide effects, etc.")] public UnityEngine.UI.Graphic panel; /// /// The PC portrait image to show during the response menu. /// [Tooltip("The PC portrait image to show during the response menu.")] public UnityEngine.UI.Image pcImage; /// /// The label that will show the PC name. /// [Tooltip("The label that will show the PC name.")] public UnityEngine.UI.Text pcName; /// /// The reminder of the last subtitle. /// [Tooltip("The reminder of the last subtitle.")] public UnityUISubtitleControls subtitleReminder; /// /// The (optional) timer. /// [Tooltip("The (optional) timer.")] public UnityEngine.UI.Slider timer; /// /// If ticked, then select the currently-focused response on timeout. /// [Tooltip("Select the currently-focused response on timeout.")] public bool selectCurrentOnTimeout = false; /// /// The response buttons, if you want to specify buttons at design time. /// [Tooltip("Design-time positioned response buttons")] public UnityUIResponseButton[] buttons; [Tooltip("Template from which to instantiate response buttons; optional to use instead of positioned buttons above")] public UnityUIResponseButton buttonTemplate; [Tooltip("If using Button Template, instantiated buttons are parented under this GameObject")] public UnityEngine.UI.Graphic buttonTemplateHolder; [Tooltip("Optional scrollbar if the instantiated button holder is in a scroll rect")] public UnityEngine.UI.Scrollbar buttonTemplateScrollbar; [Tooltip("Reset the scroll bar to this value when preparing the response menu")] public float buttonTemplateScrollbarResetValue = 1; [Tooltip("Automatically set up explicit navigation for instantiated template buttons instead of using Automatic navigation")] public bool explicitNavigationForTemplateButtons = true; [Tooltip("If explicit navigation is enabled, loop around when navigating past end of menu")] public bool loopExplicitNavigation = false; public UIAutonumberSettings autonumber = new UIAutonumberSettings(); public UIAnimationTransitions animationTransitions = new UIAnimationTransitions(); public UnityEvent onContentChanged = new UnityEvent(); /// /// The instantiated buttons. These are only valid during a specific response menu, /// and only if you're using templates. Each showing of the response menu clears /// this list and re-populates it with new buttons. /// [HideInInspector] public List instantiatedButtons = new List(); /// /// Assign this delegate if you want it to replace the default timeout handler. /// public System.Action TimeoutHandler = null; public bool isVisible { get { return (panel != null) ? panel.gameObject.activeInHierarchy : false; } } private UIShowHideController m_showHideController = null; public UIShowHideController showHideController { get { if (m_showHideController == null) { m_showHideController = new UIShowHideController(null, panel, animationTransitions.transitionMode, animationTransitions.debug); m_showHideController.state = UIShowHideController.State.Hidden; } return m_showHideController; } } private UnityUITimer unityUITimer = null; private Sprite pcPortraitSprite = null; private string pcPortraitName = null; private Animator animator = null; private bool lookedForAnimator = false; /// /// Sets the PC portrait name and sprite to use in the response menu. /// /// Portrait sprite. /// Portrait name. public override void SetPCPortrait(Sprite portraitSprite, string portraitName) { pcPortraitSprite = portraitSprite; pcPortraitName = portraitName; } /// /// Sets the Portrait sprite to use in the response menu if the named actor is the player. /// This is used to immediately update the GUI control if the SetPortrait() sequencer /// command changes the Portrait sprite. /// /// Actor name in database. /// Portrait sprite. public override void SetActorPortraitSprite(string actorName, Sprite portraitSprite) { if (string.Equals(actorName, pcPortraitName)) { var actorPortraitSprite = AbstractDialogueUI.GetValidPortraitSprite(actorName, portraitSprite); pcPortraitSprite = actorPortraitSprite; if ((pcImage != null) && (DialogueManager.masterDatabase.IsPlayer(actorName))) { pcImage.sprite = actorPortraitSprite; } } } public override AbstractUISubtitleControls subtitleReminderControls { get { return subtitleReminder; } } /// /// Sets the controls active/inactive, except this method never activates the timer. If the /// UI's display settings specify a timeout, then the UI will call StartTimer() to manually /// activate the timer. /// /// /// Value (true for active; otherwise inactive). /// public override void SetActive(bool value) { subtitleReminder.SetActive(value && subtitleReminder.HasText); Tools.SetGameObjectActive(buttonTemplate, false); foreach (var button in buttons) { if (button != null) { if (value == true) { Tools.SetGameObjectActive(button, button.visible); } else { button.clickable = false; } } } Tools.SetGameObjectActive(timer, false); Tools.SetGameObjectActive(pcName, value); Tools.SetGameObjectActive(pcImage, value); if (value == true) { if ((pcImage != null) && (pcPortraitSprite != null)) pcImage.sprite = pcPortraitSprite; if ((pcName != null) && (pcPortraitName != null)) pcName.text = pcPortraitName; Tools.SetGameObjectActive(panel, true); animationTransitions.ClearTriggers(showHideController); showHideController.Show(animationTransitions.showTrigger, false, null); if (explicitNavigationForTemplateButtons) SetupTemplateButtonNavigation(); } else { if (isVisible && CanTriggerAnimation(animationTransitions.hideTrigger)) { animationTransitions.ClearTriggers(showHideController); showHideController.Hide(animationTransitions.hideTrigger, DeactivateUIElements); } else { if (panel != null) Tools.SetGameObjectActive(panel, false); } } } private void DeactivateUIElements() { if (panel != null) Tools.SetGameObjectActive(panel, false); ClearResponseButtons(); } /// /// Clears the response buttons. /// protected override void ClearResponseButtons() { DestroyInstantiatedButtons(); if (buttons != null) { for (int i = 0; i < buttons.Length; i++) { if (buttons[i] == null) continue; buttons[i].Reset(); buttons[i].visible = showUnusedButtons; } } } /// /// Sets the response buttons. /// /// /// Responses. /// /// /// Target that will receive OnClick events from the buttons. /// protected override void SetResponseButtons(Response[] responses, Transform target) { DestroyInstantiatedButtons(); if ((buttons != null) && (responses != null)) { int buttonNumber = 0; // Add explicitly-positioned buttons: for (int i = 0; i < responses.Length; i++) { if (responses[i].formattedText.position != FormattedText.NoAssignedPosition) { int position = responses[i].formattedText.position; if (0 <= position && position < buttons.Length && buttons[position] != null) { SetResponseButton(buttons[position], responses[i], target, buttonNumber++); } else { Debug.LogWarning(DialogueDebug.Prefix + ": Buttons list doesn't contain a button for position " + position); } } } if ((buttonTemplate != null) && (buttonTemplateHolder != null)) { // Reset scrollbar to top: if (buttonTemplateScrollbar != null) { buttonTemplateScrollbar.value = buttonTemplateScrollbarResetValue; } // Instantiate buttons from template: for (int i = 0; i < responses.Length; i++) { if (responses[i].formattedText.position != FormattedText.NoAssignedPosition) continue; GameObject buttonGameObject = GameObject.Instantiate(buttonTemplate.gameObject) as GameObject; if (buttonGameObject == null) { Debug.LogError(string.Format("{0}: Couldn't instantiate response button template", DialogueDebug.Prefix)); } else { instantiatedButtons.Add(buttonGameObject); buttonGameObject.transform.SetParent(buttonTemplateHolder.transform, false); buttonGameObject.SetActive(true); UnityUIResponseButton responseButton = buttonGameObject.GetComponent(); SetResponseButton(responseButton, responses[i], target, buttonNumber++); if (responseButton != null) buttonGameObject.name = "Response: " + responseButton.Text; } } } else { // Auto-position remaining buttons: if (buttonAlignment == ResponseButtonAlignment.ToFirst) { // Align to first, so add in order to front: for (int i = 0; i < Mathf.Min(buttons.Length, responses.Length); i++) { if (responses[i].formattedText.position == FormattedText.NoAssignedPosition) { int position = Mathf.Clamp(GetNextAvailableResponseButtonPosition(0, 1), 0, buttons.Length - 1); SetResponseButton(buttons[position], responses[i], target, buttonNumber++); } } } else { // Align to last, so add in reverse order to back: for (int i = Mathf.Min(buttons.Length, responses.Length) - 1; i >= 0; i--) { if (responses[i].formattedText.position == FormattedText.NoAssignedPosition) { int position = Mathf.Clamp(GetNextAvailableResponseButtonPosition(buttons.Length - 1, -1), 0, buttons.Length - 1); SetResponseButton(buttons[position], responses[i], target, buttonNumber++); } } } } } NotifyContentChanged(); } private void SetResponseButton(UnityUIResponseButton button, Response response, Transform target, int buttonNumber) { if (button != null) { button.visible = true; button.clickable = response.enabled; button.target = target; if (response != null) button.SetFormattedText(response.formattedText); button.response = response; // Auto-number: if (autonumber.enabled) { button.Text = string.Format(autonumber.format, buttonNumber + 1, button.Text); var keyTrigger = button.GetComponent(); if (autonumber.regularNumberHotkeys) { if (keyTrigger == null) keyTrigger = button.gameObject.AddComponent(); keyTrigger.key = (KeyCode)((int)KeyCode.Alpha1 + buttonNumber); } if (autonumber.numpadHotkeys) { if (autonumber.regularNumberHotkeys || keyTrigger == null) keyTrigger = button.gameObject.AddComponent(); keyTrigger.key = (KeyCode)((int)KeyCode.Keypad1 + buttonNumber); } } } } private int GetNextAvailableResponseButtonPosition(int start, int direction) { if (buttons != null) { int position = start; while ((0 <= position) && (position < buttons.Length)) { if (buttons[position].visible && buttons[position].response != null) { position += direction; } else { return position; } } } return 5; } public void SetupTemplateButtonNavigation() { // Assumes buttons are active (since uses GetComponent), so call after activating panel. if (instantiatedButtons == null || instantiatedButtons.Count == 0) return; for (int i = 0; i < instantiatedButtons.Count; i++) { var button = instantiatedButtons[i].GetComponent().button; var above = (i == 0) ? (loopExplicitNavigation ? instantiatedButtons[instantiatedButtons.Count - 1].GetComponent().button : null) : instantiatedButtons[i - 1].GetComponent().button; var below = (i == instantiatedButtons.Count - 1) ? (loopExplicitNavigation ? instantiatedButtons[0].GetComponent().button : null) : instantiatedButtons[i + 1].GetComponent().button; var navigation = new UnityEngine.UI.Navigation(); navigation.mode = UnityEngine.UI.Navigation.Mode.Explicit; navigation.selectOnUp = above; navigation.selectOnLeft = above; navigation.selectOnDown = below; navigation.selectOnRight = below; button.navigation = navigation; } } public void DestroyInstantiatedButtons() { foreach (var instantiatedButton in instantiatedButtons) { GameObject.Destroy(instantiatedButton); } instantiatedButtons.Clear(); NotifyContentChanged(); } public void NotifyContentChanged() { onContentChanged.Invoke(); } /// /// Starts the timer. /// /// /// Timeout duration in seconds. /// public override void StartTimer(float timeout) { if (timer != null) { if (unityUITimer == null) { Tools.SetGameObjectActive(timer, true); unityUITimer = timer.GetComponent(); if (unityUITimer == null) unityUITimer = timer.gameObject.AddComponent(); Tools.SetGameObjectActive(timer, false); } if (unityUITimer != null) { Tools.SetGameObjectActive(timer, true); unityUITimer.StartCountdown(timeout, OnTimeout); } else { if (DialogueDebug.logWarnings) Debug.LogWarning(string.Format("{0}: No UnityUITimer component found on timer", DialogueDebug.Prefix)); } } } /// /// This method is called if the timer runs out. It selects the first response. /// public virtual void OnTimeout() { if (TimeoutHandler != null) { TimeoutHandler.Invoke(); } else { DefaultTimeoutHandler(); } } public void DefaultTimeoutHandler() { if (selectCurrentOnTimeout || DialogueManager.displaySettings.inputSettings.responseTimeoutAction == ResponseTimeoutAction.ChooseCurrentResponse) { var currentButton = (UnityEngine.EventSystems.EventSystem.current != null && UnityEngine.EventSystems.EventSystem.current.currentSelectedGameObject != null) ? UnityEngine.EventSystems.EventSystem.current.currentSelectedGameObject.GetComponent() : null; if (currentButton != null) { currentButton.OnClick(); return; } } DialogueManager.instance.SendMessage(DialogueSystemMessages.OnConversationTimeout); } /// /// Auto-focuses the first response. Useful for gamepads. /// public void AutoFocus(GameObject lastSelection = null, bool allowStealFocus = true) { if (UnityEngine.EventSystems.EventSystem.current == null) return; var currentSelection = UnityEngine.EventSystems.EventSystem.current.currentSelectedGameObject; if (currentSelection == null) { currentSelection = lastSelection; UnityEngine.EventSystems.EventSystem.current.SetSelectedGameObject(lastSelection); } // If anything is selected and we're not allowed to steal focus, stop: if (currentSelection != null && !allowStealFocus) return; // If a button is already auto-focused, keep it focused and stop there: if (instantiatedButtons.Find(x => x.gameObject == currentSelection) != null) return; for (int i = 0; i < buttons.Length; i++) { if (buttons[i] != null && buttons[i].gameObject == currentSelection) return; } if (instantiatedButtons.Count > 0) { // Otherwise try to focus an instantiated button: UITools.Select(instantiatedButtons[0].GetComponent(), allowStealFocus); } else { // Failing that, focus a designed button: for (int i = 0; i < buttons.Length; i++) { if (buttons[i] != null && buttons[i].clickable) { UITools.Select(buttons[i].button, allowStealFocus); return; } } } } private bool CanTriggerAnimation(string triggerName) { return CanTriggerAnimations() && !string.IsNullOrEmpty(triggerName); } private bool CanTriggerAnimations() { if ((animator == null) && !lookedForAnimator) { lookedForAnimator = true; if (panel != null) animator = panel.GetComponentInParent(); } return (animator != null) && (animationTransitions != null); } } }