Yeah I took a whack at doing the Rich Text formatting but unfortunately I think it's out of scope for the moment. I may get time to look back at it and implement Rich Text support some time soon but I would not recommend holding your development for it.
It was fairly easy to recognize the opening of a format block for the supported types (<size>, <color>, <b>and <i>) but i ran into problems trying to cache what type of formats have been opened and how many times. If the blocks are not closed and closed out the correct number of times then Rich Text won't recognize it and hence it becomes visible in the printed string. I tried a couple of quick tricks to get around it which actually work under simple circumstances but will fail if you add any kind of advanced formatting combinations to a string and would not be suitable for publishing.
I ended up with creating a suffix that goes on the end of the string and is modified when a closing block is detected so that the suffix on the end of the string will always complete the opener blocks while being typed. The opener (and closer) blocks are detected and skipped through the index so you don't see them being typed, of course.
If someone wants to continue the code the latest is below but be aware it is not working in this state and has a lot of loose ends. It's possibly not too far from working.
// (c) Copyright HutongGames, LLC 2010-2015. All rights reserved.
/*--- __ECO__ __ACTION__ ---*/
using UnityEngine;
using System;
using System.Collections;
using System.Collections.Generic;
namespace HutongGames.PlayMaker.Actions
{
[ActionCategory(ActionCategory.String)]
[Tooltip("Automatically types into a string.")]
public class StringTypewriter : FsmStateAction
{
[RequiredField]
[UIHint(UIHint.TextArea)]
[Tooltip("The string with the entire message to type out.")]
public FsmString baseString;
[RequiredField]
[UIHint(UIHint.Variable)]
[Tooltip("The target result string (can be the same as the base string).")]
public FsmString resultString;
[Tooltip("The time between letters appearing.")]
public FsmFloat pause;
[Tooltip("When punctuation is encountered then pause is multiplied by this.\n(period, exclamation, question, comma, semicolon, colon and ellipsis).\nIt also handles repeating characters and pauses only one time at their end.")]
public FsmFloat punctuationMultiplier;
[Tooltip("True is realtime: continues typing while game is paused. False will subject time variable to the game's timeScale.")]
public FsmBool realtime;
[Tooltip("Support Rich Text formatting, eg <color>, <b>, <i>, <size>.")]
public bool richText;
[Tooltip("Send this event when finished typing.")]
public FsmEvent finishEvent;
[UIHint(UIHint.Description)]
public string d1 = " Optional Sounds Section:";
[Tooltip("Check this to play sounds while typing.")]
public bool useSounds;
[Tooltip("Check this to not play a sound when it is a spacebar character.")]
public bool noSoundOnSpaces;
[ObjectType(typeof(AudioClip))]
[Tooltip("The sound to play for each letter typed.")]
public FsmObject typingSound;
[Tooltip("The GameObject with an AudioSource component.")]
public FsmOwnerDefault audioSourceObj;
// Time data
float p = 0.0f;
float startTime;
float timer = 0.0f;
// Character data
char[] punctuation = {'.', '!', '?', ',', ';', ':'};
string message = "";
int index = 0;
char lastChar;
char nextChar;
// Rich Text formatting
private string block;
private string suffix;
// Audio
private AudioSource audioSource;
private AudioClip sound;
public override void Reset()
{
// --- Basic ---
baseString = null;
resultString = null;
pause = 0.05f;
punctuationMultiplier = 10.0f;
realtime = false;
richText = true;
finishEvent = null;
// --- Sounds ---
useSounds = false;
noSoundOnSpaces = true;
typingSound = null;
audioSourceObj = null;
}
public override void OnEnter()
{
// sort out the sound stuff
if (useSounds){
var go = Fsm.GetOwnerDefaultTarget(audioSourceObj);
if (go != null){
audioSource = go.GetComponent<AudioSource>();
if (audioSource == null){
Debug.LogError ("String Typewriter Action reports: The <color=#ffa500ff>AudioSource component</color> was not found! Does the target object have an Audio Source component?");
useSounds = false;
}
sound = typingSound.Value as AudioClip;
if (sound == null){
Debug.LogError ("String Typewriter Action reports: The <color=#ffa500ff>AudioClip</color> was not found!");
useSounds = false;
}
}
else {
Debug.LogError ("String Typewriter Action reports: The <color=#ffa500ff>target Game Object</color> for the audio source was not found!");
useSounds = false;
}
}
index = 0;
message = baseString.Value; // clone the base string.
resultString.Value = ""; // clear the target string.
startTime = Time.realtimeSinceStartup; // get the actual time since the game started.
}
// Here in OnUpdate we handle...
// 1) Pausing between letters
// 2) Checking for punctuation marks
// 3) Processing Rich Text
public override void OnUpdate()
{
// Check if the string is complete
if (message == resultString.Value)
{
DoFinish();
}
// If the string is not complete, continue work
else
{
p = pause.Value; // clone the pause variable in OnUpdate in case it is changed by the user at runtime.
nextChar = message[index];
// fetch/compare the characters to see if they exist in the punctuation array or not
int _iLast = Array.IndexOf (punctuation, lastChar); // get last index
int _iNext = Array.IndexOf (punctuation, nextChar); // get next index
// compare the result
bool _lastIsMark = _iLast != -1; // if index is not -1, there is a punctuation mark.
bool _nextIsMark = _iNext != -1; // if index is not -1, there is a punctuation mark.
if (_lastIsMark)
{
// if the next char is a punctuation mark then we should not pause.
if (!_nextIsMark)
{
pause = (p * punctuationMultiplier.Value);
}
}
// if we run into a format opener, we should process the block!
if (richText && message[index] == '<')
{
DoRichText();
}
if (realtime.Value)
{
// check the current time minus the previous Typing event time.
// if that's more than the pause gap, then its time for another character.
if (Time.realtimeSinceStartup - startTime >= pause.Value)
{
DoTyping();
}
}
if (!realtime.Value)
{
// add delta time until its more than the pause gap.
timer += Time.deltaTime;
if (timer >= pause.Value)
{
DoTyping();
}
}
pause.Value = p; // done with pausing, so revert the pause in case it was changed for punctuation.
}
}
// Here in DoTyping we handle...
// 1) Playing sound
// 2) Adding text to the message
// 3) Iterating the character index value
// 4) Resetting the timer and getting time
public void DoTyping()
{
// play the sound if enabled
if (useSounds)
{
if (noSoundOnSpaces && message[index] != ' ')
{
audioSource.PlayOneShot (sound);
}
else
{
audioSource.PlayOneShot (sound);
}
}
// build the display string
if (richText)
{
resultString.Value = message.Substring(0, index) + (message[index] + suffix); // add one character to the string, and the suffix.
//Debug.Log ("INDEX: "+index+", current string result: "+resultString.Value);
Debug.Log ("RESULT: "+resultString.Value+", INDEX: "+index+", SUFFIX: "+suffix);
}
if (!richText)
{
resultString.Value += message[index]; // add one character to the string
}
lastChar = message[index]; // store the index that we just typed
index++; // iterate the index
timer = 0.0f; // reset timer
startTime = Time.realtimeSinceStartup; // update realtime
}
public void DoRichText()
{
block = "";
// Construct the <block>
while (index < message.Length)
{
block += message[index];
index++;
if (message[index] == '>')
{
block += message[index];
index = index+1;
break;
}
}
block = block.ToLower();
if (block.Contains("/"))
{
Debug.Log ("_________Removing : "+block+" from Suffix: "+suffix);
int remove = suffix.Length - block.Length;
if (remove<=0)
{
suffix.Equals("");
}
else{
suffix.Remove (remove);
}
Debug.Log ("_________Removed : "+block+" from Suffix: "+suffix);
}
if (!block.Contains("/"))
{
if (block.Contains ("<c")){
block = "</color>";}
if (block.Contains ("<s")){
block = "</size>";}
if (block.Contains ("<i")){
block = "</i>";}
if (block.Contains ("<b")){
block = "</b>";}
suffix += block;
}
}
public void DoFinish()
{
Finish();
if (finishEvent != null)
{
Fsm.Event(finishEvent);
}
}
public override void OnExit()
{
// if the state exits before finishing the string
// then it needs to be auto-completed.
resultString.Value = message;
}
}
}