How does typescript starts again?

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

How does typescript starts again?

Post by Dralks »

Hi Tony!

a few weeks ago we talked about some code I wrote to fade in (and out) paraphs, I got something working and doing 100% what I want, I used almost the same code that you have in the original typewritter but modifying the "Play()" method to fade in paraphs instead of adding characters, using the same RPG Maker pause symbols of the original, so my code basically fades in anything up to the next token and continues doing so until the end of the text, the issue that I'm having now is that if I have more dialogs in the same scene the first one does exactly what I would expect, but the next ones just jumps in the scene with no effect, for what I can see in the code in "Play()" is never called again after finishing with the first dialog, even when I start any new dialog just like I started the last one and "Stop()" is being called correctly at the end, it is like my custom typewritter never resets to type a fresh batch of text, but I'm also struggling to identify what you do different in the typewriter to always have typing any new text from scratch.

my question is: how do you start typing from scratch in every dialog? Why could my code be ignoring "Play()".

if it helps my full working code to fade in paraphs (again, if I don't remember wrong only "Play()" changes, I have a bunch of debugs trying to find the issue), I just assign it like the original typewritter:

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;
        }

        public int RolloverCharacterSpread = 1;
        public int textDelayWhenFinished = 1;
        /// <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 = @"\<";
        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()
        {
            Debug.Log("typewriter on enabled");
            base.OnEnable();
            if (!IsPlaying && playOnEnable && started)
            {
                StopTypewriterCoroutine();
                Debug.Log("about to start");

                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();
                

                // 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;

                var tokenValue = RPGMakerTokenType.None;
                var numberOfCharacters = 1;

                if (rpgMakerTokens.Any())
                {
                    var firstToken = rpgMakerTokens.FirstOrDefault();
                    numberOfCharacters = firstToken.Key;
                    tokenValue = firstToken.Value[0];
                    rpgMakerTokens.Remove(firstToken.Key);
                }

                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 (i > textInfo.characterInfo.Length - 1 || !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);
                    float tokenWait = 0;

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

                        switch (tokenValue)
                        {
                            case RPGMakerTokenType.QuarterPause:
                                tokenWait = quarterPauseDuration;
                                break;
                            case RPGMakerTokenType.FullPause:
                                tokenWait = fullPauseDuration;
                                break;
                            case RPGMakerTokenType.SkipToEnd:
                                tokenWait = fullPauseDuration * 2;
                                break;
                            case RPGMakerTokenType.InstantOpen:
                                tokenWait = fullPauseDuration * 5;
                                break;
                            case RPGMakerTokenType.InstantClose:
                                tokenWait = fullPauseDuration * 10;
                                break;
                            default:
                                tokenWait = fullPauseDuration;
                                break;
                        }

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

                            if (nextToken <= characterCount)
                            {                               
                                currentCharacter = rpgMakerTokens.First().Key;
                                tokenValue = rpgMakerTokens.First().Value[0];
                                rpgMakerTokens.Remove(rpgMakerTokens.First().Key);
                            }
                        }
                        else
                        {
                            tokenValue = RPGMakerTokenType.None;
                            //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;
                        }
                    }

                    if (tokenWait != 0)
                    {
                        yield return new WaitForSeconds(tokenWait);
                    }
                    else
                    {
                        yield return new WaitForSeconds(0.25f - charactersPerSecond * 0.01f);
                    }
                }
            }

            yield return new WaitForSeconds(textDelayWhenFinished);
            StartCoroutine(FadeOut());
        }
        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();
        }

        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();
            DialogueManager.StopConversation();
        }

        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
}


To start everything it I'm reusing the same conversation by just changing the message (a method that I use successfully for other dialogs), but I did try to completely stop the previous one, I even tried to reset the database when I ran out of ideas with no results, I suspect that this is not the issue and I'm missing a step to reset my custom typewritter somewhere else:

Code: Select all

        private void Start()
        {
            PrepareDialog();
        }
        private void PrepareDialog()
        {
            DialogueManager.StopConversation();
            DialogSystem = GetComponent<DialogueSystemTrigger>();
            var collectableActor = DialogueManager.masterDatabase.actors.SingleOrDefault(x => x.Name == InterfaceName.CUT_SCENE_ACTOR);
            conversation = DialogueManager.masterDatabase.conversations.First(x => x.ActorID == collectableActor.id);
            DialogSystem.trigger = DialogueSystemTriggerEvent.OnUse;
            DialogSystem.conversation = conversation.Title;
        }

        private void OnEnable()
        {
            DialogueManager.StopConversation();
            conversation.dialogueEntries[1].DialogueText = Message();
            DialogSystem.OnUse();
        }
User avatar
Tony Li
Posts: 22159
Joined: Thu Jul 18, 2013 1:27 pm

Re: How does typescript starts again?

Post by Tony Li »

Hi,

If you look at the StandardUISubtitlePanel script's SetContent() method, you'll see how the typewriter is started. Note that if you have ticked 'Accumulate Text' it will tell the typewriter to start at a specific character position, not at the beginning.
Dralks
Posts: 44
Joined: Mon May 24, 2021 6:19 pm

Re: How does typescript starts again?

Post by Dralks »

After several hours I made this work, had 3 main issues, I needed to wrap my code so TypewriterUtility could pick it up (I was using a plain string so it would never call the script again), I needed to set maxVisibleCharacters to make everything visible at the beggining of play (or everything was always invisible even if I tried to change the alpha) and I needed reset characters typed to 0, it took me an embarrassing amount of time to have it 100% working in every scenario

Code: Select all

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

using System;
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;
        }

        public int RolloverCharacterSpread = 1;
        public int textDelayWhenFinished = 1;
        /// <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 = @"\<";

        private int numberOfCharacters = 1;

        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))
            {
                charactersTyped = 0;
                ProcessRPGMakerCodes();

                // 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;
                textComponent.maxVisibleCharacters = textInfo.characterCount; ;

                numberOfCharacters = textInfo.characterCount;
                var tokenValue = RPGMakerTokenType.None;

                if (rpgMakerTokens.Any())
                {
                    numberOfCharacters = rpgMakerTokens.First().Key;
                    tokenValue = rpgMakerTokens.First().Value[0];
                    rpgMakerTokens.Remove(rpgMakerTokens.First().Key);
                }

                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 (i > textInfo.characterInfo.Length - 1 || !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);
                    float tokenWait = 0;

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

                        switch (tokenValue)
                        {
                            case RPGMakerTokenType.QuarterPause:
                                tokenWait = quarterPauseDuration;
                                break;
                            case RPGMakerTokenType.FullPause:
                                tokenWait = fullPauseDuration;
                                break;
                            case RPGMakerTokenType.SkipToEnd:
                                tokenWait = fullPauseDuration * 2;
                                break;
                            case RPGMakerTokenType.InstantOpen:
                                tokenWait = fullPauseDuration * 5;
                                break;
                            case RPGMakerTokenType.InstantClose:
                                tokenWait = fullPauseDuration * 10;
                                break;
                            default:
                                tokenWait = fullPauseDuration;
                                break;
                        }

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

                            if (nextToken <= characterCount)
                            {
                                currentCharacter = rpgMakerTokens.First().Key;
                                tokenValue = rpgMakerTokens.First().Value[0];
                                rpgMakerTokens.Remove(rpgMakerTokens.First().Key);
                            }
                        }
                        else
                        {
                            tokenValue = RPGMakerTokenType.None;
                            //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;
                        }
                    }

                    if (tokenWait != 0)
                    {
                        yield return new WaitForSeconds(tokenWait);
                    }
                    else
                    {
                        yield return new WaitForSeconds(0.25f - charactersPerSecond * 0.01f);
                    }
                }
            }

            yield return new WaitForSeconds(textDelayWhenFinished);
            StartCoroutine(FadeOut());
        }
        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 * 0.5f));
                yield return new WaitForSecondsRealtime(0.001f);
            }

            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: 22159
Joined: Thu Jul 18, 2013 1:27 pm

Re: How does typescript starts again?

Post by Tony Li »

Thanks again for sharing. Don't feel bad for anything. The original typewriter scripts have taken a surprising amount of development time over the years to handle all the features and weird edge cases that it covers. It's one of those things that seems like it would be dead simple at first glance but has lots of nuances.
Post Reply