// Copyright (c) Pixel Crushers. All rights reserved. using UnityEngine; using UnityEngine.Events; using System.Collections; using System; namespace PixelCrushers.DialogueSystem { [AddComponentMenu("")] // Use wrapper. public class StandardUISubtitlePanel : UIPanel { #region Serialized Fields [Tooltip("(Optional) Main panel for subtitle.")] public RectTransform panel; [Tooltip("(Optional) Image for actor's portrait.")] public UnityEngine.UI.Image portraitImage; [Tooltip("(Optional) Text element for actor's name.")] public UITextField portraitName; [Tooltip("Subtitle text.")] public UITextField subtitleText; [Tooltip("Add speaker's name to subtitle text.")] public bool addSpeakerName = false; [Tooltip("Format to add speaker name, where {0} is name and {1} is subtitle text.")] public string addSpeakerNameFormat = "{0}: {1}"; [Tooltip("Each subtitle adds to Subtitle Text instead of replacing it.")] public bool accumulateText = false; [Tooltip("If panel has a typewriter effect, don't start typing until panel's Show animation has completed.")] public bool delayTypewriterUntilOpen = false; [Tooltip("(Optional) Continue button. Only shown if Dialogue Manager's Continue Button mode uses continue button.")] public UnityEngine.UI.Button continueButton; [Tooltip("When the subtitle UI elements should be visible.")] public UIVisibility visibility = UIVisibility.OnlyDuringContent; [Tooltip("When focusing panel, set this animator trigger.")] public string focusAnimationTrigger = string.Empty; [Tooltip("When unfocusing panel, set this animator trigger.")] public string unfocusAnimationTrigger = string.Empty; [Tooltip("Check Dialogue Actors for portrait animator controllers. Portrait image must have an Animator.")] public bool useAnimatedPortraits = false; [Tooltip("If a player actor uses this panel, don't show player portrait name & image; keep previous NPC portrait visible instead.")] public bool onlyShowNPCPortraits = false; [Tooltip("Wait for panels within this dialogue UI (not external panels) to close before showing.")] public bool waitForClose = false; [Tooltip("Clear text when closing panel, including when hiding using SetDialoguePanel().")] public bool clearTextOnClose = true; /// /// Invoked when the subtitle panel gains focus. /// public UnityEvent onFocus = new UnityEvent(); /// /// Invoked when the subtitle panel loses focus. /// public UnityEvent onUnfocus = new UnityEvent(); #endregion #region Public Properties [SerializeField, Tooltip("Panel is currently in focused state.")] private bool m_hasFocus = true; public bool hasFocus { get { return m_hasFocus; } set { m_hasFocus = value; } } public override bool waitForShowAnimation { get { return true; } } private Subtitle m_currentSubtitle = null; public virtual Subtitle currentSubtitle { get { return m_currentSubtitle; } protected set { m_currentSubtitle = value; } } /// /// The database name of the actor whose display name appears in the Portrait Name field. /// public string portraitActorName { get; protected set; } #endregion #region Internal Properties private bool m_haveSavedOriginalColor = false; protected bool haveSavedOriginalColor { get { return m_haveSavedOriginalColor; } set { m_haveSavedOriginalColor = value; } } protected Color originalColor { get; set; } private string m_accumulatedText = string.Empty; public string accumulatedText { get { return m_accumulatedText; } set { m_accumulatedText = value; } } //private Animator m_animator = null; protected Animator animator { get { if (m_animator == null && portraitImage != null) m_animator = portraitImage.GetComponent(); return m_animator; } } private bool m_isDefaultNPCPanel = false; public bool isDefaultNPCPanel { get { return m_isDefaultNPCPanel; } set { m_isDefaultNPCPanel = value; } } private bool m_isDefaultPCPanel = false; public bool isDefaultPCPanel { get { return m_isDefaultPCPanel; } set { m_isDefaultPCPanel = value; } } private int m_panelNumber = -1; public int panelNumber { get { return m_panelNumber; } set { m_panelNumber = value; } } public Transform m_actorOverridingPanel = null; public Transform actorOverridingPanel { get { return m_actorOverridingPanel; } set { m_actorOverridingPanel = value; } } protected int frameLastSetContent = -1; // Frame when we last set this panel's content. protected bool shouldShowContinueButton = false; protected const float WaitForCloseTimeoutDuration = 8f; private StandardDialogueUI m_dialogueUI = null; protected StandardDialogueUI dialogueUI { get { if (m_dialogueUI == null) m_dialogueUI = GetComponentInParent(); return m_dialogueUI; } } private Coroutine m_focusWhenOpenCoroutine = null; #endregion #region Initialization protected virtual void Awake() { if (addSpeakerName) { addSpeakerNameFormat = addSpeakerNameFormat.Replace("\\n", "\n").Replace("\\t", "\t"); } if (waitForClose) clearTextOnClose = false; } #endregion #region Typewriter Control /// /// Returns the typewriter effect on the subtitle text element, or null if there is none. /// public AbstractTypewriterEffect GetTypewriter() { return TypewriterUtility.GetTypewriter(subtitleText); } /// /// Checks if the subtitle text element has a typewriter effect. /// public bool HasTypewriter() { return GetTypewriter() != null; } /// /// Returns the speed of the typewriter effect on the subtitle element if it has one. /// public float GetTypewriterSpeed() { return TypewriterUtility.GetTypewriterSpeed(subtitleText); } /// /// Sets the speed of the typewriter effect on the subtitle element if it has one. /// public void SetTypewriterSpeed(float charactersPerSecond) { TypewriterUtility.SetTypewriterSpeed(subtitleText, charactersPerSecond); } #endregion #region Show & Hide /// /// Shows the panel at the start of the conversation; called if it's configured to be visible at the start. /// /// The image of the first actor who will use this panel. /// The name of the first actor who will use this panel. /// The actor's DialogueActor component, or null if none. public virtual void OpenOnStartConversation(Sprite portraitSprite, string portraitName, DialogueActor dialogueActor) { Open(); SetUIElementsActive(true); Tools.SetGameObjectActive(this.portraitImage, portraitSprite != null); if (this.portraitImage != null) this.portraitImage.sprite = portraitSprite; if (this.portraitName != null) this.portraitName.text = portraitName; if (subtitleText.text != null) subtitleText.text = string.Empty; portraitActorName = (dialogueActor != null) ? dialogueActor.actor : portraitName; CheckDialogueActorAnimator(dialogueActor); } [System.Obsolete("Use OpenOnStartConversation(Sprite,string,DialogueActor) instead.")] public virtual void OpenOnStartConversation(Texture2D portraitTexture, string portraitName, DialogueActor dialogueActor) { OpenOnStartConversation(UITools.CreateSprite(portraitTexture), portraitName, dialogueActor); } public virtual void OnConversationStart(Transform actor) { if (frameLastSetContent < (Time.frameCount - 1)) // If we just set content, don't clear the text. { ClearText(); } } /// /// Shows a subtitle, playing the open and focus animations. /// public virtual void ShowSubtitle(Subtitle subtitle) { if (waitForClose && dialogueUI.AreAnyPanelsClosing()) { DialogueManager.instance.StartCoroutine(ShowSubtitleAfterClosing(subtitle)); } else { ShowSubtitleNow(subtitle); } } protected virtual void ShowSubtitleNow(Subtitle subtitle) { SetUIElementsActive(true); if (!isOpen) hasFocus = false; Open(); Focus(); SetContent(subtitle); actorOverridingPanel = null; } protected virtual IEnumerator ShowSubtitleAfterClosing(Subtitle subtitle) { shouldShowContinueButton = false; float safeguardTime = Time.realtimeSinceStartup + WaitForCloseTimeoutDuration; while (dialogueUI.AreAnyPanelsClosing() && Time.realtimeSinceStartup < safeguardTime) { yield return null; } ShowSubtitleNow(subtitle); if (shouldShowContinueButton) ShowContinueButton(); } /// /// Hides a subtitle, playing the unfocus and close animations. /// public virtual void HideSubtitle(Subtitle subtitle) { if (panelState != PanelState.Closed) Unfocus(); Close(); } /// /// Immediately hides the panel without playing any animations. /// public virtual void HideImmediate() { DeactivateUIElements(); } protected override void OnHidden() { base.OnHidden(); DeactivateUIElements(); } /// /// Opens the panel. /// public override void Open() { base.Open(); } /// /// Closes the panel. /// public override void Close() { if (isOpen) base.Close(); if (clearTextOnClose) ClearText(); hasFocus = false; } /// /// Focuses the panel. /// public virtual void Focus() { if (panelState == PanelState.Opening && enabled && gameObject.activeInHierarchy) { if (m_focusWhenOpenCoroutine != null) StopCoroutine(m_focusWhenOpenCoroutine); m_focusWhenOpenCoroutine = StartCoroutine(FocusWhenOpen()); } else { FocusNow(); } } protected IEnumerator FocusWhenOpen() { float timeout = Time.realtimeSinceStartup + 5f; while (panelState != PanelState.Open && Time.realtimeSinceStartup < timeout) { yield return null; } m_focusWhenOpenCoroutine = null; FocusNow(); } protected virtual void FocusNow() { panelState = PanelState.Open; if (hasFocus) return; if (string.IsNullOrEmpty(focusAnimationTrigger)) { OnFocused(); } else { animatorMonitor.SetTrigger(focusAnimationTrigger, OnFocused, true); } onFocus.Invoke(); } private void OnFocused() { hasFocus = true; } /// /// Unfocuses the panel. /// public virtual void Unfocus() { if (m_focusWhenOpenCoroutine != null) { StopCoroutine(m_focusWhenOpenCoroutine); m_focusWhenOpenCoroutine = null; } if (!string.IsNullOrEmpty(focusAnimationTrigger) && animatorMonitor.currentTrigger == focusAnimationTrigger) { animatorMonitor.CancelCurrentAnimation(); } else { if (!hasFocus) return; } if (panelState == PanelState.Opening) panelState = PanelState.Open; hasFocus = false; animatorMonitor.SetTrigger(unfocusAnimationTrigger, null, false); onUnfocus.Invoke(); } public virtual void ActivateUIElements() { SetUIElementsActive(true); } public virtual void DeactivateUIElements() { SetUIElementsActive(false); if (clearTextOnClose) ClearText(); } protected virtual void SetUIElementsActive(bool value) { Tools.SetGameObjectActive(panel, value); Tools.SetGameObjectActive(portraitImage, value && portraitImage != null && portraitImage.sprite != null); portraitName.SetActive(value); subtitleText.SetActive(value); Tools.SetGameObjectActive(continueButton, false); // Let ConversationView determine if continueButton should be shown. } public virtual void ClearText() { m_accumulatedText = string.Empty; subtitleText.text = string.Empty; } public virtual void ShowContinueButton() { Tools.SetGameObjectActive(continueButton, true); if (InputDeviceManager.autoFocus) Select(); if (continueButton != null && continueButton.onClick.GetPersistentEventCount() == 0) { continueButton.onClick.RemoveAllListeners(); var fastForward = continueButton.GetComponent(); if (fastForward != null) { continueButton.onClick.AddListener(fastForward.OnFastForward); } else { continueButton.onClick.AddListener(OnContinue); } } shouldShowContinueButton = true; } public virtual void HideContinueButton() { Tools.SetGameObjectActive(continueButton, false); } /// /// Finishes the subtitle without hiding the panel. Called if the panel is configured to stay open. /// Hides the continue button and stops the typewriter effect. /// public virtual void FinishSubtitle() { HideContinueButton(); var typewriter = GetTypewriter(); if (typewriter != null && typewriter.isPlaying) typewriter.Stop(); } /// /// Selects the panel's continue button (i.e., navigates to it). /// /// Select continue button even if another element is already selected. public virtual void Select(bool allowStealFocus = true) { UITools.Select(continueButton, allowStealFocus); } /// /// The continue button should call this method to continue. /// public virtual void OnContinue() { var dialogueUI = GetComponentInParent(); if (dialogueUI == null) dialogueUI = DialogueManager.dialogueUI as AbstractDialogueUI; if (dialogueUI != null) dialogueUI.OnContinueConversation(); } /// /// Sets the content of the panel. Assumes the panel is already open. /// public virtual void SetContent(Subtitle subtitle) { if (subtitle == null) return; currentSubtitle = subtitle; CheckSubtitleAnimator(subtitle); if (!onlyShowNPCPortraits || subtitle.speakerInfo.isNPC) { if (portraitImage != null) { var sprite = subtitle.GetSpeakerPortrait(); portraitImage.sprite = sprite; Tools.SetGameObjectActive(portraitImage, sprite != null); } portraitActorName = subtitle.speakerInfo.nameInDatabase; portraitName.text = subtitle.speakerInfo.Name; UITools.SendTextChangeMessage(portraitName); } TypewriterUtility.StopTyping(subtitleText); var previousText = accumulateText ? m_accumulatedText : string.Empty; var previousChars = accumulateText ? UITools.StripRPGMakerCodes(Tools.StripTextMeshProTags(Tools.StripRichTextCodes(previousText))).Length : 0; SetFormattedText(subtitleText, previousText, subtitle.formattedText); if (accumulateText) m_accumulatedText = subtitleText.text + "\n"; if (delayTypewriterUntilOpen && !hasFocus) { StartCoroutine(StartTypingWhenFocused(subtitleText, subtitleText.text, previousChars)); } else { TypewriterUtility.StartTyping(subtitleText, subtitleText.text, previousChars); } frameLastSetContent = Time.frameCount; } protected virtual IEnumerator StartTypingWhenFocused(UITextField subtitleText, string text, int fromIndex) { subtitleText.text = string.Empty; float timeout = Time.realtimeSinceStartup + 5f; while (!hasFocus && panelState != PanelState.Open && Time.realtimeSinceStartup < timeout) { yield return null; } subtitleText.text = text; TypewriterUtility.StartTyping(subtitleText, text, fromIndex); } protected virtual void SetFormattedText(UITextField textField, string previousText, FormattedText formattedText) { textField.text = previousText + UITools.GetUIFormattedText(formattedText); UITools.SendTextChangeMessage(textField); if (!haveSavedOriginalColor) { originalColor = textField.color; haveSavedOriginalColor = true; } textField.color = (formattedText.emphases != null && formattedText.emphases.Length > 0) ? formattedText.emphases[0].color : originalColor; } public virtual void SetActorPortraitSprite(string actorName, Sprite portraitSprite) { if (portraitImage == null) return; var sprite = AbstractDialogueUI.GetValidPortraitSprite(actorName, portraitSprite); portraitImage.sprite = sprite; Tools.SetGameObjectActive(portraitImage, sprite != null); } public void CheckSubtitleAnimator(Subtitle subtitle) { if (subtitle != null && useAnimatedPortraits && animator != null) { var dialogueActor = DialogueActor.GetDialogueActorComponent(subtitle.speakerInfo.transform); if (dialogueActor != null) // && dialogueActor.standardDialogueUISettings.portraitAnimatorController != null) { var speakerPanelNumber = dialogueActor.GetSubtitlePanelNumber(); var isMyPanel = (actorOverridingPanel == subtitle.speakerInfo.transform) || (PanelNumberUtility.GetSubtitlePanelIndex(speakerPanelNumber) == this.panelNumber) || (speakerPanelNumber == SubtitlePanelNumber.Default && subtitle.speakerInfo.isNPC && isDefaultNPCPanel) || (speakerPanelNumber == SubtitlePanelNumber.Default && subtitle.speakerInfo.isPlayer && isDefaultPCPanel) || (speakerPanelNumber == SubtitlePanelNumber.Custom && dialogueActor.standardDialogueUISettings.customSubtitlePanel == this); if (isMyPanel) { StartCoroutine(SetAnimatorAtEndOfFrame(dialogueActor.standardDialogueUISettings.portraitAnimatorController)); } } else { StartCoroutine(SetAnimatorAtEndOfFrame(null)); } } } protected void CheckDialogueActorAnimator(DialogueActor dialogueActor) { if (dialogueActor != null && useAnimatedPortraits && animator != null && dialogueActor.standardDialogueUISettings.portraitAnimatorController != null) { StartCoroutine(SetAnimatorAtEndOfFrame(dialogueActor.standardDialogueUISettings.portraitAnimatorController)); } } private IEnumerator SetAnimatorAtEndOfFrame(RuntimeAnimatorController animatorController) { if (animator.runtimeAnimatorController != animatorController) { animator.runtimeAnimatorController = animatorController; } yield return new WaitForEndOfFrame(); if (animator.runtimeAnimatorController != animatorController) { animator.runtimeAnimatorController = animatorController; } } #endregion } }