/* Yarn Spinner is licensed to you under the terms found in the file LICENSE.md. */ using System; using System.Collections.Generic; using System.Threading; using UnityEngine; using Yarn.Markup; using Yarn.Unity.Attributes; #nullable enable #if USE_TMP using TMPro; #else using TextMeshProUGUI = Yarn.Unity.TMPShim; using TMP_Text = Yarn.Unity.TMPShim; #endif namespace Yarn.Unity { /// /// A Dialogue Presenter that presents lines of dialogue, using Unity UI /// elements. /// [HelpURL("https://docs.yarnspinner.dev/using-yarnspinner-with-unity/components/dialogue-view/line-view")] public class LinePresenter : DialoguePresenterBase { protected internal enum TypewriterType { Instant, ByLetter, ByWord, Custom, } /// /// The canvas group that contains the UI elements used by this Line /// View. /// /// /// If is true, then the alpha value of this /// will be animated during line presentation /// and dismissal. /// /// [Space] [MustNotBeNull] public CanvasGroup? canvasGroup; /// /// The object that displays the text of /// dialogue lines. /// [MustNotBeNull] public TMP_Text? lineText; /// /// Controls whether the object will show the /// character name present in the line or not. /// /// /// This value is only used if is . /// If this value is , any character names /// present in a line will be shown in the /// object. /// If this value is , character names will /// not be shown in the object. /// [Group("Character")] [Label("Shows Name In Line")] public bool showCharacterNameInLine = true; /// /// The object that displays the character /// names found in dialogue lines. /// /// /// If the receives a line that does not contain /// a character name, this object will be left blank. /// [Group("Character")] [Label("Name Field")] public TMP_Text? characterNameText = null; /// /// The game object that holds the text /// field. /// /// /// This is needed in situations where the character name is contained /// within an entirely different game object. Most of the time this will /// just be the same game object as . /// [Group("Character")] public GameObject? characterNameContainer = null; /// /// Controls whether the line view should fade in when lines appear, and /// fade out when lines disappear. /// /// If this value is , the object's alpha property will animate from 0 to /// 1 over the course of seconds when lines /// appear, and animate from 1 to zero over the course of seconds when lines disappear. /// If this value is , the object will appear instantaneously. /// /// /// /// [Group("Fade")] [Label("Fade UI")] public bool useFadeEffect = true; /// /// The time that the fade effect will take to fade lines in. /// /// This value is only used when is /// . /// [Group("Fade")] [ShowIf(nameof(useFadeEffect))] public float fadeUpDuration = 0.25f; /// /// The time that the fade effect will take to fade lines out. /// /// This value is only used when is /// . /// [Group("Fade")] [ShowIf(nameof(useFadeEffect))] public float fadeDownDuration = 0.1f; /// /// Controls whether this Line View will automatically to the Dialogue /// Runner that the line is complete as soon as the line has finished /// appearing. /// /// /// /// If this value is true, the Line View will /// /// The will not /// proceed to the next piece of content (e.g. the next line, or the /// next options) until all Dialogue Presenters have reported that they have /// finished presenting their lines. If a /// doesn't report that it's finished until it receives input, the will end up pausing. /// /// This is useful for games in which you want the player to be able to /// read lines of dialogue at their own pace, and give them control over /// when to advance to the next line. /// [Group("Automatically Advance Dialogue")] public bool autoAdvance = false; /// /// The amount of time after the line finishes appearing before /// automatically ending the line, in seconds. /// /// This value is only used when is /// . [Group("Automatically Advance Dialogue")] [ShowIf(nameof(autoAdvance))] [Label("Delay Before Advancing")] public float autoAdvanceDelay = 1f; // typewriter fields [Group("Typewriter")] [SerializeField] protected internal TypewriterType typewriterStyle = TypewriterType.ByLetter; /// /// The number of characters per second that should appear during a /// typewriter effect. /// [Group("Typewriter")] [ShowIf(nameof(typewriterStyle), TypewriterType.ByLetter)] [Label("Letters per Second")] [Min(0)] public int lettersPerSecond = 60; [Group("Typewriter")] [ShowIf(nameof(typewriterStyle), TypewriterType.ByWord)] [Label("Words per Second")] [Min(0)] public int wordsPerSecond = 10; [Group("Typewriter")] [ShowIf(nameof(typewriterStyle), TypewriterType.Custom)] [UnityEngine.Serialization.FormerlySerializedAs("CustomTypewriter")] public InterfaceContainer? customTypewriter; /// /// A list of objects that will be /// used to handle markers in the line. /// [Group("Typewriter")] [Label("Event Handlers")] [UnityEngine.Serialization.FormerlySerializedAs("actionMarkupHandlers")] [SerializeField] protected List eventHandlers = new List(); protected List ActionMarkupHandlers { get { var pauser = new PauseEventProcessor(); List ActionMarkupHandlers = new() { pauser, }; ActionMarkupHandlers.AddRange(eventHandlers); return ActionMarkupHandlers; } } /// public override YarnTask OnDialogueCompleteAsync() { if (canvasGroup != null) { canvasGroup.alpha = 0; } return YarnTask.CompletedTask; } /// public override YarnTask OnDialogueStartedAsync() { if (canvasGroup != null) { canvasGroup.alpha = 0; } return YarnTask.CompletedTask; } /// /// Called by /// after the line's text has been fully set up in the field, /// but before the typewriter effect has begun to display the line. /// protected virtual void PostProcessDisplayText() { } /// /// Called by Unity on first frame. /// private void Awake() { if (characterNameContainer == null && characterNameText != null) { characterNameContainer = characterNameText.gameObject; } switch (typewriterStyle) { case TypewriterType.Instant: Typewriter = new InstantTypewriter() { ActionMarkupHandlers = ActionMarkupHandlers, TextElement = this.lineText, }; break; case TypewriterType.ByLetter: Typewriter = new LetterTypewriter() { ActionMarkupHandlers = ActionMarkupHandlers, TextElement = this.lineText, CharactersPerSecond = this.lettersPerSecond, }; break; case TypewriterType.ByWord: Typewriter = new WordTypewriter() { ActionMarkupHandlers = ActionMarkupHandlers, TextElement = this.lineText, WordsPerSecond = this.wordsPerSecond, }; break; case TypewriterType.Custom: Typewriter = customTypewriter?.Interface; if (Typewriter == null) { Debug.LogWarning("Typewriter mode is set to custom but there is no typewriter set."); } else { Typewriter.ActionMarkupHandlers.AddRange(ActionMarkupHandlers); Typewriter.TextElement = this.lineText; } break; } } /// Presents a line using the configured text view. /// /// public override async YarnTask RunLineAsync(LocalizedLine line, LineCancellationToken token) { if (lineText == null) { Debug.LogError($"{nameof(LinePresenter)} does not have a text view. Skipping line {line.TextID} (\"{line.RawText}\")"); return; } MarkupParseResult text; // configuring the text fields if (characterNameText == null) { if (showCharacterNameInLine) { text = line.Text; } else { text = line.TextWithoutCharacterName; } } else { text = line.TextWithoutCharacterName; // we are configured to show character names in their own little box, but this line doesn't have one if (characterNameContainer != null) { if (string.IsNullOrWhiteSpace(line.CharacterName)) { characterNameContainer.SetActive(false); } else { characterNameContainer.SetActive(true); characterNameText.text = line.CharacterName; } } } YarnTagParser.Parse(line.Metadata, out Dictionary? keyValueTags, out HashSet? plainTags); if (keyValueTags.TryGetValue("mood", out string? valueTag)) { Debug.Log($"Line has mood tag with value {valueTag}"); } Typewriter ??= new InstantTypewriter() { ActionMarkupHandlers = this.ActionMarkupHandlers, TextElement = this.lineText, }; Typewriter.PrepareForContent(text); PostProcessDisplayText(); if (canvasGroup != null) { // fading up the UI if (useFadeEffect) { await Effects.FadeAlphaAsync(canvasGroup, 0, 1, fadeUpDuration, token.HurryUpToken); } else { // We're not fading up, so set the canvas group's alpha to 1 immediately. canvasGroup.alpha = 1; } } await Typewriter.RunTypewriter(text, token.HurryUpToken).SuppressCancellationThrow(); // if we are set to autoadvance how long do we hold for before continuing? if (autoAdvance) { await YarnTask.Delay((int)(autoAdvanceDelay * 1000), token.NextContentToken).SuppressCancellationThrow(); } else { await YarnTask.WaitUntilCanceled(token.NextContentToken).SuppressCancellationThrow(); } Typewriter.ContentWillDismiss(); if (canvasGroup != null) { // we fade down the UI if (useFadeEffect) { await Effects.FadeAlphaAsync(canvasGroup, 1, 0, fadeDownDuration, token.HurryUpToken).SuppressCancellationThrow(); } else { canvasGroup.alpha = 0; } } Typewriter.ContentDidDismiss(); } } }