Best way to have multiple paragraphs fading in?

Announcements, support questions, and discussion for the Dialogue System.
Post Reply
Dralks
Posts: 44
Joined: Mon May 24, 2021 6:19 pm

Best way to have multiple paragraphs fading in?

Post by Dralks »

Hi Tony! I have been struggling for a couple of hours to get this working, I have a huge narrator text, and I would like to have each paraph with a fade in effect (and they all stay so at the end the player sees the whole text), something like:

[Whole text Fade in]
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nulla sit amet egestas erat, id maximus augue. Nam nec sollicitudin diam. Nullam massa orci, bibendum porta est non, fermentum bibendum quam. Praesent id ipsum ac sapien sagittis ullamcorper eget et lacus. Etiam et risus ac felis

delay(4)
[Whole text Fade in]
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nulla sit amet egestas erat, id maximus augue. Nam nec sollicitudin diam. Nullam massa orci, bibendum porta est non, fermentum bibendum quam. Praesent id ipsum ac sapien sagittis ullamcorper eget et lacus. Etiam et risus ac felis

delay(4)
[Whole text Fade in]
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nulla sit amet egestas erat, id maximus augue. Nam nec sollicitudin diam. Nullam massa orci, bibendum porta est non, fermentum bibendum quam. Praesent id ipsum ac sapien sagittis ullamcorper eget et lacus. Etiam et risus ac felis


I could get this the text to show up at the right times with typewrite and adding '\.' at the end of each paraph, but if I could get each paraph to just fade in (without typewriter) it would look much better, is that possible somehow?
User avatar
Tony Li
Posts: 21987
Joined: Thu Jul 18, 2013 1:27 pm

Re: Best way to have multiple paragraphs fading in?

Post by Tony Li »

Hi,

Anything's always possible because you can provide your own code -- either as a subclass of StandardUISubtitlePanel to inherit all of the Standard Dialogue UI's functionality, or as your own entirely custom implementation of the IDialogueUI C# interface.

However, in this case, you can probably do it without any coding by using the SMSDialogueUI component. You can see it in action on the SMS Dialogue UI prefab.

Here's a quick example scene that I put together using the SMS Dialogue UI. I changed the "NPC Message Template" subtitle panel to add an Animator and Canvas Group, and to remove the bubble frame. It uses a default canvas animator which fades in pretty fast. If you want the fade-in to be slower, you can duplicate the animator controller asset and set the Show state's speed to a lower number.

DS_FadeInParagraphsExample_2021-07-29.unitypackage
Dralks
Posts: 44
Joined: Mon May 24, 2021 6:19 pm

Re: Best way to have multiple paragraphs fading in?

Post by Dralks »

Thanks for answering Tony!

The scene you sent shows the text one by one (by replacing the previous one) with the usual typescript effect, but I was interested in knowing if I would be reinventing the wheel by putting some code together to do it, so I will work on something for that, thank you again for putting time for us!
User avatar
Tony Li
Posts: 21987
Joined: Thu Jul 18, 2013 1:27 pm

Re: Best way to have multiple paragraphs fading in?

Post by Tony Li »

Hi,

Are you playing the "Fade In Paragraphs Example" scene? Also, if you're on a very old version of the Dialogue System, it's possible it doesn't have the SMS Dialogue UI prefab that the scene is based on.

The scene should look similar to the screenshot below, where each paragraph fades in below the previous one:

fadeInParagraphs.png
fadeInParagraphs.png (17.99 KiB) Viewed 1300 times

And, to answer your question, yes you can write your own code. The example scene just shows a way you can do it without reinventing the wheel, but sometimes it's easier/more succinct to write bespoke code to do exactly what you want.
Dralks
Posts: 44
Joined: Mon May 24, 2021 6:19 pm

Re: Best way to have multiple paragraphs fading in?

Post by Dralks »

Thank you for your answer, I'm unsure of what version I have but we sure had it around for a while before actually start implementing it, I ended up getting what I wanted, I created 2 modifications of your typewriter, one works just like yours but instead of just show the character, all characters start fading in (producing a smooth typewriter/fade in effect)

The other uses the 'Pause characters' variable to know when to fade in a block of text, so basically everything between .\ will come in together and then it looks for the next, and so on:

this text comes in \. now this one fades in \. now this comes

(that last one produces the effect I was looking for, and more because I can put \. anywhere)

both have some variables to control the fade in times and speed (again, I'm using 95% of your code only modifying the 'Play()' method in both cases, here is the code in case you want to include it somehow, I think that the final result looks pretty cool, they still need some work (just finished now and I can't get bother to cleaned it up today) but as they stand now they cover all my needs:

Code: Select all

// Copyright (c) Pixel Crushers. All rights reserved.

using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Text.RegularExpressions;
using TMPro;
using UnityEngine;
using UnityEngine.Events;
using UnityEngine.UI;

namespace PixelCrushers.DialogueSystem
{

#if TMP_PRESENT

    /// <summary>
    /// This is a typewriter effect for TextMesh Pro.
    /// 
    /// Note: Handles RPGMaker codes, but not two codes next to each other.
    /// </summary>
    [AddComponentMenu("")] // Use wrapper.
    [DisallowMultipleComponent]
    public class FadeInParagraphEffect : AbstractTypewriterEffect
    {

        [System.Serializable]
        public class AutoScrollSettings
        {
            [Tooltip("Automatically scroll to bottom of scroll rect. Useful for long text. Works best with left justification.")]
            public bool autoScrollEnabled = false;
            public UnityEngine.UI.ScrollRect scrollRect = null;
            [Tooltip("Optional. Add a UIScrollBarEnabler to main dialogue panel, assign UI elements, then assign it here to automatically enable scrollbar if content is taller than viewport.")]
            public UIScrollbarEnabler scrollbarEnabler = null;
        }

        /// <summary>
        /// Optional auto-scroll settings.
        /// </summary>
        public AutoScrollSettings autoScrollSettings = new AutoScrollSettings();
        public bool fadeInToPauseCharacter;
        public UnityEvent onBegin = new UnityEvent();
        public UnityEvent onCharacter = new UnityEvent();
        public UnityEvent onEnd = new UnityEvent();

        /// <summary>
        /// Indicates whether the effect is playing.
        /// </summary>
        /// <value><c>true</c> if this instance is playing; otherwise, <c>false</c>.</value>
        public override bool isPlaying { get { return typewriterCoroutine != null; } }

        /// @cond FOR_V1_COMPATIBILITY
        public bool IsPlaying { get { return isPlaying; } }
        /// @endcond

        protected const string RPGMakerCodeQuarterPause = @"\,";
        protected const string RPGMakerCodeFullPause = @"\.";
        protected const string RPGMakerCodeSkipToEnd = @"\^";
        protected const string RPGMakerCodeInstantOpen = @"\>";
        protected const string RPGMakerCodeInstantClose = @"\<";
        public int RolloverCharacterSpread = 1;
        private int numberOfCharacters = 1;
        private int currentTokenIndex;

        protected enum RPGMakerTokenType
        {
            None,
            QuarterPause,
            FullPause,
            SkipToEnd,
            InstantOpen,
            InstantClose
        }

        protected Dictionary<int, List<RPGMakerTokenType>> rpgMakerTokens = new Dictionary<int, List<RPGMakerTokenType>>();

        protected TMPro.TMP_Text m_textComponent = null;
        protected TMPro.TMP_Text textComponent
        {
            get
            {
                if (m_textComponent == null) m_textComponent = GetComponent<TMPro.TMP_Text>();
                return m_textComponent;
            }
        }

        protected LayoutElement m_layoutElement = null;
        protected LayoutElement layoutElement
        {
            get
            {
                if (m_layoutElement == null)
                {
                    m_layoutElement = GetComponent<LayoutElement>();
                    if (m_layoutElement == null) m_layoutElement = gameObject.AddComponent<LayoutElement>();
                }
                return m_layoutElement;
            }
        }

        protected AudioSource runtimeAudioSource
        {
            get
            {
                if (audioSource == null) audioSource = GetComponent<AudioSource>();
                if (audioSource == null && (audioClip != null))
                {
                    audioSource = gameObject.AddComponent<AudioSource>();
                    audioSource.playOnAwake = false;
                    audioSource.panStereo = 0;
                }
                return audioSource;
            }
        }

        protected bool started = false;
        protected int charactersTyped = 0;
        protected Coroutine typewriterCoroutine = null;
        protected MonoBehaviour coroutineController = null;

        public override void Awake()
        {

            if (removeDuplicateTypewriterEffects) RemoveIfDuplicate();
        }

        protected void RemoveIfDuplicate()
        {
            var effects = GetComponents<TextMeshProTypewriterEffect>();
            if (effects.Length > 1)
            {
                var keep = effects[0];
                for (int i = 1; i < effects.Length; i++)
                {
                    if (effects[i].GetInstanceID() < keep.GetInstanceID())
                    {
                        keep = effects[i];
                    }
                }
                for (int i = 0; i < effects.Length; i++)
                {
                    if (effects[i] != keep)
                    {
                        Destroy(effects[i]);
                    }
                }
            }
        }

        public override void Start()
        {
            if (!IsPlaying && playOnEnable)
            {
                StopTypewriterCoroutine();
                StartTypewriterCoroutine(0);
            }
            started = true;
        }

        public override void OnEnable()
        {
            base.OnEnable();
            if (!IsPlaying && playOnEnable && started)
            {
                StopTypewriterCoroutine();
                StartTypewriterCoroutine(0);
            }
        }

        public override void OnDisable()
        {
            base.OnEnable();
            Stop();
        }

        /// <summary>
        /// Pauses the effect.
        /// </summary>
        public void Pause()
        {
            paused = true;
        }

        /// <summary>
        /// Unpauses the effect. The text will resume at the point where it
        /// was paused; it won't try to catch up to make up for the pause.
        /// </summary>
        public void Unpause()
        {
            paused = false;
        }

        public void Rewind()
        {
            charactersTyped = 0;
        }

        /// <summary>
        /// Starts typing, optionally from a starting index. Characters before the 
        /// starting index will appear immediately.
        /// </summary>
        /// <param name="text">Text to type.</param>
        /// <param name="fromIndex">Character index to start typing from.</param>
        public override void StartTyping(string text, int fromIndex = 0)
        {
            StopTypewriterCoroutine();
            textComponent.text = text;
            StartTypewriterCoroutine(fromIndex);
        }

        public override void StopTyping()
        {
            Stop();
        }

        /// <summary>
        /// Play typewriter on text immediately.
        /// </summary>
        /// <param name="text"></param>
        public virtual void PlayText(string text, int fromIndex = 0)
        {
            StopTypewriterCoroutine();
            textComponent.text = text;
            StartTypewriterCoroutine(fromIndex);
        }

        protected void StartTypewriterCoroutine(int fromIndex)
        {
            if (coroutineController == null || !coroutineController.gameObject.activeInHierarchy)
            {
                // This MonoBehaviour might not be enabled yet, so use one that's guaranteed to be enabled:
                MonoBehaviour controller = GetComponentInParent<AbstractDialogueUI>();
                if (controller == null) controller = DialogueManager.instance;
                coroutineController = controller;
                if (coroutineController == null) coroutineController = this;
            }
            typewriterCoroutine = coroutineController.StartCoroutine(Play());
        }

        /// <summary>
        /// Plays the typewriter effect.
        /// </summary>
        public virtual IEnumerator Play()
        {
            if ((textComponent != null) && (charactersPerSecond > 0))
            {
                ProcessRPGMakerCodes();

                if (fadeInToPauseCharacter)
                {
                    numberOfCharacters = rpgMakerTokens.First().Key;
                    rpgMakerTokens.Remove(rpgMakerTokens.First().Key);
                }


                // Set the whole text transparent
                textComponent.color = new Color
                    (
                        textComponent.color.r,
                        textComponent.color.g,
                        textComponent.color.b,
                        0
                    );
                // Need to force the text object to be generated so we have valid data to work with right from the start.
                textComponent.ForceMeshUpdate();

                TMP_TextInfo textInfo = textComponent.textInfo;
                Color32[] newVertexColors;

                int currentCharacter = numberOfCharacters;
                int startingCharacterRange = 0;
                bool isRangeMax = false;
                bool fullAlpha = false;

                while (!isRangeMax)
                {
                    int characterCount = textInfo.characterCount;

                    // Spread should not exceed the number of characters.
                    byte fadeSteps = (byte)Mathf.Max(1, 255 / RolloverCharacterSpread);

                    for (int i = startingCharacterRange; i < currentCharacter + 1; i++)
                    {
                        charactersTyped = i;
                        // Skip characters that are not visible (like white spaces)
                        if (!textInfo.characterInfo[i].isVisible) continue;

                        // Get the index of the material used by the current character.
                        int materialIndex = textInfo.characterInfo[i].materialReferenceIndex;

                        // Get the vertex colors of the mesh used by this text element (character or sprite).
                        newVertexColors = textInfo.meshInfo[materialIndex].colors32;

                        // Get the index of the first vertex used by this text element.
                        int vertexIndex = textInfo.characterInfo[i].vertexIndex;

                        // Get the current character's alpha value.
                        byte alpha = (byte)Mathf.Clamp(newVertexColors[vertexIndex + 0].a + fadeSteps, 0, 255);

                        // Set new alpha values.
                        newVertexColors[vertexIndex + 0].a = alpha;
                        newVertexColors[vertexIndex + 1].a = alpha;
                        newVertexColors[vertexIndex + 2].a = alpha;
                        newVertexColors[vertexIndex + 3].a = alpha;

                        fullAlpha = alpha == 255;
                    }

                    // Upload the changed vertex colors to the Mesh.
                    textComponent.UpdateVertexData(TMP_VertexDataUpdateFlags.Colors32);

                    //the previous text is totally visible
                    if (fullAlpha)
                    {
                        fullAlpha = false;
                        if (currentCharacter + numberOfCharacters < characterCount) currentCharacter += numberOfCharacters;

                        if (fadeInToPauseCharacter && rpgMakerTokens.Any())
                        {
                            //we use the next stop point to know what to show next
                            var nextToken = rpgMakerTokens.First().Key;

                            if (nextToken < characterCount)
                            {
                                numberOfCharacters = rpgMakerTokens.First().Key;
                                rpgMakerTokens.Remove(rpgMakerTokens.First().Key);

                                yield return DialogueTime.WaitForSeconds(fullPauseDuration);
                            }
                        }
                        else
                        {
                            //we are seeing full text, time to stop
                            if (characterCount - 1 == currentCharacter) isRangeMax = true;

                            //no stop points, we show all what is left
                            currentCharacter = characterCount - 1;
                        }
                    }

                    yield return new WaitForSeconds(0.25f - charactersPerSecond * 0.01f);

                }
            }

            Stop();
        }

        protected void ProcessRPGMakerCodes()
        {
            rpgMakerTokens.Clear();
            var source = textComponent.text;
            var result = string.Empty;
            if (!source.Contains("\\")) return;
            source = Tools.StripTextMeshProTags(source);
            int safeguard = 0;
            while (!string.IsNullOrEmpty(source) && safeguard < 9999)
            {
                safeguard++;
                RPGMakerTokenType token;
                if (PeelRPGMakerTokenFromFront(ref source, out token))
                {
                    int i = result.Length;
                    if (!rpgMakerTokens.ContainsKey(i))
                    {
                        rpgMakerTokens.Add(i, new List<RPGMakerTokenType>());
                    }
                    rpgMakerTokens[i].Add(token);
                }
                else
                {
                    result += source[0];
                    source = source.Remove(0, 1);
                }
            }
            textComponent.text = Regex.Replace(textComponent.text, @"\\[\.\,\^\<\>]", string.Empty);
        }

        protected bool PeelRPGMakerTokenFromFront(ref string source, out RPGMakerTokenType token)
        {
            token = RPGMakerTokenType.None;
            if (string.IsNullOrEmpty(source) || source.Length < 2 || source[0] != '\\') return false;
            var s = source.Substring(0, 2);
            if (string.Equals(s, RPGMakerCodeQuarterPause))
            {
                token = RPGMakerTokenType.QuarterPause;
            }
            else if (string.Equals(s, RPGMakerCodeFullPause))
            {
                token = RPGMakerTokenType.FullPause;
            }
            else if (string.Equals(s, RPGMakerCodeSkipToEnd))
            {
                token = RPGMakerTokenType.SkipToEnd;
            }
            else if (string.Equals(s, RPGMakerCodeInstantOpen))
            {
                token = RPGMakerTokenType.InstantOpen;
            }
            else if (string.Equals(s, RPGMakerCodeInstantClose))
            {
                token = RPGMakerTokenType.InstantClose;
            }
            else
            {
                return false;
            }
            source = source.Remove(0, 2);
            return true;
        }

        protected void StopTypewriterCoroutine()
        {
            if (typewriterCoroutine == null) return;
            if (coroutineController == null)
            {
                StopCoroutine(typewriterCoroutine);
            }
            else
            {
                coroutineController.StopCoroutine(typewriterCoroutine);
            }
            typewriterCoroutine = null;
            coroutineController = null;
        }

        /// <summary>
        /// Stops the effect.
        /// </summary>
        public override void Stop()
        {
            if (isPlaying)
            {
                onEnd.Invoke();
                Sequencer.Message(SequencerMessages.Typed);
            }
            StopTypewriterCoroutine();
            if (textComponent != null) textComponent.maxVisibleCharacters = textComponent.textInfo.characterCount;
            HandleAutoScroll();
        }

        protected void HandleAutoScroll()
        {
            if (!autoScrollSettings.autoScrollEnabled) return;

            var layoutElement = textComponent.GetComponent<LayoutElement>();
            if (layoutElement == null) layoutElement = textComponent.gameObject.AddComponent<LayoutElement>();
            layoutElement.preferredHeight = textComponent.textBounds.size.y;
            if (autoScrollSettings.scrollRect != null)
            {
                autoScrollSettings.scrollRect.normalizedPosition = new Vector2(0, 0);
            }
            if (autoScrollSettings.scrollbarEnabler != null)
            {
                autoScrollSettings.scrollbarEnabler.CheckScrollbar();
            }
        }
    }

#else

    [AddComponentMenu("")] // Use wrapper.
    public class TextMeshProTypewriterEffect : AbstractTypewriterEffect
    {
        public override bool isPlaying { get { return false; } }
        public override void Awake() { }
        public override void Start() { }
        public override void StartTyping(string text, int fromIndex = 0) { }
        public override void Stop() { }
        public override void StopTyping() { }
    }

#endif
}

Code: Select all

// Copyright (c) Pixel Crushers. All rights reserved.

using System.Collections;
using System.Collections.Generic;
using System.Text.RegularExpressions;
using TMPro;
using UnityEngine;
using UnityEngine.Events;
using UnityEngine.UI;

namespace PixelCrushers.DialogueSystem
{

#if TMP_PRESENT

    /// <summary>
    /// This is a typewriter effect for TextMesh Pro.
    /// 
    /// Note: Handles RPGMaker codes, but not two codes next to each other.
    /// </summary>
    [AddComponentMenu("")] // Use wrapper.
    [DisallowMultipleComponent]
    public class FadeInTypewritterEffect : AbstractTypewriterEffect
    {

        [System.Serializable]
        public class AutoScrollSettings
        {
            [Tooltip("Automatically scroll to bottom of scroll rect. Useful for long text. Works best with left justification.")]
            public bool autoScrollEnabled = false;
            public UnityEngine.UI.ScrollRect scrollRect = null;
            [Tooltip("Optional. Add a UIScrollBarEnabler to main dialogue panel, assign UI elements, then assign it here to automatically enable scrollbar if content is taller than viewport.")]
            public UIScrollbarEnabler scrollbarEnabler = null;
        }

        /// <summary>
        /// Optional auto-scroll settings.
        /// </summary>
        public AutoScrollSettings autoScrollSettings = new AutoScrollSettings();

        public UnityEvent onBegin = new UnityEvent();
        public UnityEvent onCharacter = new UnityEvent();
        public UnityEvent onEnd = new UnityEvent();

        /// <summary>
        /// Indicates whether the effect is playing.
        /// </summary>
        /// <value><c>true</c> if this instance is playing; otherwise, <c>false</c>.</value>
        public override bool isPlaying { get { return typewriterCoroutine != null; } }

        /// @cond FOR_V1_COMPATIBILITY
        public bool IsPlaying { get { return isPlaying; } }
        /// @endcond

        protected const string RPGMakerCodeQuarterPause = @"\,";
        protected const string RPGMakerCodeFullPause = @"\.";
        protected const string RPGMakerCodeSkipToEnd = @"\^";
        protected const string RPGMakerCodeInstantOpen = @"\>";
        protected const string RPGMakerCodeInstantClose = @"\<";
        public int RolloverCharacterSpread = 10;
        protected enum RPGMakerTokenType
        {
            None,
            QuarterPause,
            FullPause,
            SkipToEnd,
            InstantOpen,
            InstantClose
        }

        protected Dictionary<int, List<RPGMakerTokenType>> rpgMakerTokens = new Dictionary<int, List<RPGMakerTokenType>>();

        protected TMPro.TMP_Text m_textComponent = null;
        protected TMPro.TMP_Text textComponent
        {
            get
            {
                if (m_textComponent == null) m_textComponent = GetComponent<TMPro.TMP_Text>();
                return m_textComponent;
            }
        }

        protected LayoutElement m_layoutElement = null;
        protected LayoutElement layoutElement
        {
            get
            {
                if (m_layoutElement == null)
                {
                    m_layoutElement = GetComponent<LayoutElement>();
                    if (m_layoutElement == null) m_layoutElement = gameObject.AddComponent<LayoutElement>();
                }
                return m_layoutElement;
            }
        }

        protected AudioSource runtimeAudioSource
        {
            get
            {
                if (audioSource == null) audioSource = GetComponent<AudioSource>();
                if (audioSource == null && (audioClip != null))
                {
                    audioSource = gameObject.AddComponent<AudioSource>();
                    audioSource.playOnAwake = false;
                    audioSource.panStereo = 0;
                }
                return audioSource;
            }
        }

        protected bool started = false;
        protected int charactersTyped = 0;
        protected Coroutine typewriterCoroutine = null;
        protected MonoBehaviour coroutineController = null;

        public override void Awake()
        {

            if (removeDuplicateTypewriterEffects) RemoveIfDuplicate();
        }

        protected void RemoveIfDuplicate()
        {
            var effects = GetComponents<TextMeshProTypewriterEffect>();
            if (effects.Length > 1)
            {
                var keep = effects[0];
                for (int i = 1; i < effects.Length; i++)
                {
                    if (effects[i].GetInstanceID() < keep.GetInstanceID())
                    {
                        keep = effects[i];
                    }
                }
                for (int i = 0; i < effects.Length; i++)
                {
                    if (effects[i] != keep)
                    {
                        Destroy(effects[i]);
                    }
                }
            }
        }

        public override void Start()
        {
            if (!IsPlaying && playOnEnable)
            {
                StopTypewriterCoroutine();
                StartTypewriterCoroutine(0);
            }
            started = true;
        }

        public override void OnEnable()
        {
            base.OnEnable();
            if (!IsPlaying && playOnEnable && started)
            {
                StopTypewriterCoroutine();
                StartTypewriterCoroutine(0);
            }
        }

        public override void OnDisable()
        {
            base.OnEnable();
            Stop();
        }

        /// <summary>
        /// Pauses the effect.
        /// </summary>
        public void Pause()
        {
            paused = true;
        }

        /// <summary>
        /// Unpauses the effect. The text will resume at the point where it
        /// was paused; it won't try to catch up to make up for the pause.
        /// </summary>
        public void Unpause()
        {
            paused = false;
        }

        public void Rewind()
        {
            charactersTyped = 0;
        }

        /// <summary>
        /// Starts typing, optionally from a starting index. Characters before the 
        /// starting index will appear immediately.
        /// </summary>
        /// <param name="text">Text to type.</param>
        /// <param name="fromIndex">Character index to start typing from.</param>
        public override void StartTyping(string text, int fromIndex = 0)
        {
            StopTypewriterCoroutine();
            textComponent.text = text;
            StartTypewriterCoroutine(fromIndex);
        }

        public override void StopTyping()
        {
            Stop();
        }

        /// <summary>
        /// Play typewriter on text immediately.
        /// </summary>
        /// <param name="text"></param>
        public virtual void PlayText(string text, int fromIndex = 0)
        {
            StopTypewriterCoroutine();
            textComponent.text = text;
            StartTypewriterCoroutine(fromIndex);
        }

        protected void StartTypewriterCoroutine(int fromIndex)
        {
            if (coroutineController == null || !coroutineController.gameObject.activeInHierarchy)
            {
                // This MonoBehaviour might not be enabled yet, so use one that's guaranteed to be enabled:
                MonoBehaviour controller = GetComponentInParent<AbstractDialogueUI>();
                if (controller == null) controller = DialogueManager.instance;
                coroutineController = controller;
                if (coroutineController == null) coroutineController = this;
            }
            typewriterCoroutine = coroutineController.StartCoroutine(Play());
        }

        /// <summary>
        /// Plays the typewriter effect.
        /// </summary>
        public virtual IEnumerator Play()
        {
            if ((textComponent != null) && (charactersPerSecond > 0))
            {

                // Set the whole text transparent
                textComponent.color = new Color
                    (
                        textComponent.color.r,
                        textComponent.color.g,
                        textComponent.color.b,
                        0
                    );
                // Need to force the text object to be generated so we have valid data to work with right from the start.
                textComponent.ForceMeshUpdate();


                TMP_TextInfo textInfo = textComponent.textInfo;
                Color32[] newVertexColors;

                int currentCharacter = 0;
                int startingCharacterRange = currentCharacter;
                bool isRangeMax = false;
                ProcessRPGMakerCodes();

                while (!isRangeMax)
                {
                    int characterCount = textInfo.characterCount;

                    // Spread should not exceed the number of characters.
                    byte fadeSteps = (byte)Mathf.Max(1, 255 / RolloverCharacterSpread);

                    for (int i = startingCharacterRange; i < currentCharacter + 1; i++)
                    {
                        charactersTyped = i;
                        // Skip characters that are not visible (like white spaces)
                        if (!textInfo.characterInfo[i].isVisible) continue;

                        // Get the index of the material used by the current character.
                        int materialIndex = textInfo.characterInfo[i].materialReferenceIndex;

                        // Get the vertex colors of the mesh used by this text element (character or sprite).
                        newVertexColors = textInfo.meshInfo[materialIndex].colors32;

                        // Get the index of the first vertex used by this text element.
                        int vertexIndex = textInfo.characterInfo[i].vertexIndex;

                        // Get the current character's alpha value.
                        byte alpha = (byte)Mathf.Clamp(newVertexColors[vertexIndex + 0].a + fadeSteps, 0, 255);

                        // Set new alpha values.
                        newVertexColors[vertexIndex + 0].a = alpha;
                        newVertexColors[vertexIndex + 1].a = alpha;
                        newVertexColors[vertexIndex + 2].a = alpha;
                        newVertexColors[vertexIndex + 3].a = alpha;

                        if (alpha == 255)
                        {
                            startingCharacterRange += 1;

                            if (startingCharacterRange == characterCount)
                            {
                                // Update mesh vertex data one last time.
                                textComponent.UpdateVertexData(TMP_VertexDataUpdateFlags.Colors32);

                                yield return new WaitForSeconds(1.0f);

                                // Reset the text object back to original state.
                                textComponent.ForceMeshUpdate();

                                yield return new WaitForSeconds(1.0f);

                                // Reset our counters.
                                currentCharacter = 0;
                                startingCharacterRange = 0;
                                isRangeMax = true; // end the coroutine.
                            }
                        }
                    }

                    // Upload the changed vertex colors to the Mesh.
                    textComponent.UpdateVertexData(TMP_VertexDataUpdateFlags.Colors32);

                    if (currentCharacter + 1 < characterCount) currentCharacter += 1;

                    if (rpgMakerTokens.ContainsKey(charactersTyped))
                    {
                        var tokens = rpgMakerTokens[charactersTyped];
                        for (int i = 0; i < tokens.Count; i++)
                        {
                            var token = tokens[i];
                            switch (token)
                            {
                                case RPGMakerTokenType.QuarterPause:
                                    yield return DialogueTime.WaitForSeconds(quarterPauseDuration);
                                    break;
                                case RPGMakerTokenType.FullPause:
                                    yield return DialogueTime.WaitForSeconds(fullPauseDuration);
                                    break;
                                case RPGMakerTokenType.SkipToEnd:
                                    charactersTyped = characterCount - 1;
                                    break;
                                case RPGMakerTokenType.InstantOpen:
                                    var close = false;
                                    while (!close && charactersTyped < characterCount)
                                    {
                                        charactersTyped++;

                                        if (rpgMakerTokens.ContainsKey(charactersTyped) && rpgMakerTokens[charactersTyped].Contains(RPGMakerTokenType.InstantClose))
                                        {
                                            close = true;
                                        }
                                    }
                                    break;
                            }
                        }
                    }

                    yield return new WaitForSeconds(0.25f - charactersPerSecond * 0.01f);
                }
            }
        
            Stop();
        }

        protected void ProcessRPGMakerCodes()
        {
            rpgMakerTokens.Clear();
            var source = textComponent.text;
            var result = string.Empty;
            if (!source.Contains("\\")) return;
            source = Tools.StripTextMeshProTags(source);
            int safeguard = 0;
            while (!string.IsNullOrEmpty(source) && safeguard < 9999)
            {
                safeguard++;
                RPGMakerTokenType token;
                if (PeelRPGMakerTokenFromFront(ref source, out token))
                {
                    int i = result.Length;
                    if (!rpgMakerTokens.ContainsKey(i))
                    {
                        rpgMakerTokens.Add(i, new List<RPGMakerTokenType>());
                    }
                    rpgMakerTokens[i].Add(token);
                }
                else
                {
                    result += source[0];
                    source = source.Remove(0, 1);
                }
            }
            textComponent.text = Regex.Replace(textComponent.text, @"\\[\.\,\^\<\>]", string.Empty);
        }

        protected bool PeelRPGMakerTokenFromFront(ref string source, out RPGMakerTokenType token)
        {
            token = RPGMakerTokenType.None;
            if (string.IsNullOrEmpty(source) || source.Length < 2 || source[0] != '\\') return false;
            var s = source.Substring(0, 2);
            if (string.Equals(s, RPGMakerCodeQuarterPause))
            {
                token = RPGMakerTokenType.QuarterPause;
            }
            else if (string.Equals(s, RPGMakerCodeFullPause))
            {
                token = RPGMakerTokenType.FullPause;
            }
            else if (string.Equals(s, RPGMakerCodeSkipToEnd))
            {
                token = RPGMakerTokenType.SkipToEnd;
            }
            else if (string.Equals(s, RPGMakerCodeInstantOpen))
            {
                token = RPGMakerTokenType.InstantOpen;
            }
            else if (string.Equals(s, RPGMakerCodeInstantClose))
            {
                token = RPGMakerTokenType.InstantClose;
            }
            else
            {
                return false;
            }
            source = source.Remove(0, 2);
            return true;
        }

        protected void StopTypewriterCoroutine()
        {
            if (typewriterCoroutine == null) return;
            if (coroutineController == null)
            {
                StopCoroutine(typewriterCoroutine);
            }
            else
            {
                coroutineController.StopCoroutine(typewriterCoroutine);
            }
            typewriterCoroutine = null;
            coroutineController = null;
        }

        /// <summary>
        /// Stops the effect.
        /// </summary>
        public override void Stop()
        {
            if (isPlaying)
            {
                onEnd.Invoke();
                Sequencer.Message(SequencerMessages.Typed);
            }
            StopTypewriterCoroutine();
            if (textComponent != null) textComponent.maxVisibleCharacters = textComponent.textInfo.characterCount;
            HandleAutoScroll();
        }

        protected void HandleAutoScroll()
        {
            if (!autoScrollSettings.autoScrollEnabled) return;

            var layoutElement = textComponent.GetComponent<LayoutElement>();
            if (layoutElement == null) layoutElement = textComponent.gameObject.AddComponent<LayoutElement>();
            layoutElement.preferredHeight = textComponent.textBounds.size.y;
            if (autoScrollSettings.scrollRect != null)
            {
                autoScrollSettings.scrollRect.normalizedPosition = new Vector2(0, 0);
            }
            if (autoScrollSettings.scrollbarEnabler != null)
            {
                autoScrollSettings.scrollbarEnabler.CheckScrollbar();
            }
        }
    }

#else

    [AddComponentMenu("")] // Use wrapper.
    public class TextMeshProTypewriterEffect : AbstractTypewriterEffect
    {
        public override bool isPlaying { get { return false; } }
        public override void Awake() { }
        public override void Start() { }
        public override void StartTyping(string text, int fromIndex = 0) { }
        public override void Stop() { }
        public override void StopTyping() { }
    }

#endif
}

User avatar
Tony Li
Posts: 21987
Joined: Thu Jul 18, 2013 1:27 pm

Re: Best way to have multiple paragraphs fading in?

Post by Tony Li »

Thanks for sharing!
Dralks
Posts: 44
Joined: Mon May 24, 2021 6:19 pm

Re: Best way to have multiple paragraphs fading in?

Post by Dralks »

I did a little modification this morning, instead of calling 'stop()' a the end of 'Play()' I'm calling a fade out effect that made it look even better (so the text fades out when it finishes instead of just disappear):

Code: Select all

  private IEnumerator FadeOut()
        {
            var text = GetComponent<TextMeshProUGUI>();

            text.color = new Color(text.color.r, text.color.g, text.color.b, 1);
            while (text.color.a > 0.0f)
            {
                text.color = new Color(text.color.r, text.color.g, text.color.b, text.color.a - (Time.deltaTime * charactersPerSecond));
                yield return new WaitForSecondsRealtime(0.01f);
            }

            Stop();
        }
I'm just struggling with getting more than one node to be called it seems to get stuck in the first node, does "stop()" just blocks everything yet to come? The conversation seems to always be "playing" the same node after calling "stop()" when the whole typewriter effect ends
User avatar
Tony Li
Posts: 21987
Joined: Thu Jul 18, 2013 1:27 pm

Re: Best way to have multiple paragraphs fading in?

Post by Tony Li »

Hi,

Stop() just stops the typewriter effect, which also sends the sequencer message 'Stop' in case any sequencer commands are waiting for that message.

What effect are you going for? I thought you wanted each line to stay onscreen after fading in, and for new lines to fade in below them and accumulate?

Instead of a custom typewriter effect, I wonder if it might be easier to use an OnConversationLine() method or make a subclass of StandardUISubtitlePanel that overrides the ShowSubtitle() method?
Dralks
Posts: 44
Joined: Mon May 24, 2021 6:19 pm

Re: Best way to have multiple paragraphs fading in?

Post by Dralks »

the effect works fine if I have 1 node, but if I have more than one big block of text I would like to just create nodes for it (each one with many paraphs that fade in), I will take a look, thanks
Post Reply