Files
beyond/Assets/Plugins/Pixel Crushers/Dialogue System/Scripts/UI/Standard/Dialogue/StandardUISubtitlePanel.cs

570 lines
22 KiB
C#

// 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;
/// <summary>
/// Invoked when the subtitle panel gains focus.
/// </summary>
public UnityEvent onFocus = new UnityEvent();
/// <summary>
/// Invoked when the subtitle panel loses focus.
/// </summary>
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; }
}
/// <summary>
/// The database name of the actor whose display name appears in the Portrait Name field.
/// </summary>
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<Animator>(); 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<StandardDialogueUI>();
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
/// <summary>
/// Returns the typewriter effect on the subtitle text element, or null if there is none.
/// </summary>
public AbstractTypewriterEffect GetTypewriter()
{
return TypewriterUtility.GetTypewriter(subtitleText);
}
/// <summary>
/// Checks if the subtitle text element has a typewriter effect.
/// </summary>
public bool HasTypewriter()
{
return GetTypewriter() != null;
}
/// <summary>
/// Returns the speed of the typewriter effect on the subtitle element if it has one.
/// </summary>
public float GetTypewriterSpeed()
{
return TypewriterUtility.GetTypewriterSpeed(subtitleText);
}
/// <summary>
/// Sets the speed of the typewriter effect on the subtitle element if it has one.
/// </summary>
public void SetTypewriterSpeed(float charactersPerSecond)
{
TypewriterUtility.SetTypewriterSpeed(subtitleText, charactersPerSecond);
}
#endregion
#region Show & Hide
/// <summary>
/// Shows the panel at the start of the conversation; called if it's configured to be visible at the start.
/// </summary>
/// <param name="portraitSprite">The image of the first actor who will use this panel.</param>
/// <param name="portraitName">The name of the first actor who will use this panel.</param>
/// <param name="dialogueActor">The actor's DialogueActor component, or null if none.</param>
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();
}
}
/// <summary>
/// Shows a subtitle, playing the open and focus animations.
/// </summary>
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();
}
/// <summary>
/// Hides a subtitle, playing the unfocus and close animations.
/// </summary>
public virtual void HideSubtitle(Subtitle subtitle)
{
if (panelState != PanelState.Closed) Unfocus();
Close();
}
/// <summary>
/// Immediately hides the panel without playing any animations.
/// </summary>
public virtual void HideImmediate()
{
DeactivateUIElements();
}
protected override void OnHidden()
{
base.OnHidden();
DeactivateUIElements();
}
/// <summary>
/// Opens the panel.
/// </summary>
public override void Open()
{
base.Open();
}
/// <summary>
/// Closes the panel.
/// </summary>
public override void Close()
{
if (isOpen) base.Close();
if (clearTextOnClose) ClearText();
hasFocus = false;
}
/// <summary>
/// Focuses the panel.
/// </summary>
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;
}
/// <summary>
/// Unfocuses the panel.
/// </summary>
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<StandardUIContinueButtonFastForward>();
if (fastForward != null)
{
continueButton.onClick.AddListener(fastForward.OnFastForward);
}
else
{
continueButton.onClick.AddListener(OnContinue);
}
}
shouldShowContinueButton = true;
}
public virtual void HideContinueButton()
{
Tools.SetGameObjectActive(continueButton, false);
}
/// <summary>
/// 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.
/// </summary>
public virtual void FinishSubtitle()
{
HideContinueButton();
var typewriter = GetTypewriter();
if (typewriter != null && typewriter.isPlaying) typewriter.Stop();
}
/// <summary>
/// Selects the panel's continue button (i.e., navigates to it).
/// </summary>
/// <param name="allowStealFocus">Select continue button even if another element is already selected.</param>
public virtual void Select(bool allowStealFocus = true)
{
UITools.Select(continueButton, allowStealFocus);
}
/// <summary>
/// The continue button should call this method to continue.
/// </summary>
public virtual void OnContinue()
{
var dialogueUI = GetComponentInParent<AbstractDialogueUI>();
if (dialogueUI == null) dialogueUI = DialogueManager.dialogueUI as AbstractDialogueUI;
if (dialogueUI != null) dialogueUI.OnContinueConversation();
}
/// <summary>
/// Sets the content of the panel. Assumes the panel is already open.
/// </summary>
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
}
}