Browse Source

Fix #50: Fixed annoying text wraparound on dialogs

Refactored the dialog class to separate parsing story text and
displaying text into separate classes.
master
chrisgregan 10 years ago
parent
commit
58014d2a0d
  1. 523
      Assets/Fungus/Dialog/Scripts/Dialog.cs
  2. 165
      Assets/Fungus/Dialog/Scripts/DialogParser.cs
  3. 8
      Assets/Fungus/Dialog/Scripts/DialogParser.cs.meta
  4. 197
      Assets/Fungus/Dialog/Scripts/DialogText.cs
  5. 8
      Assets/Fungus/Dialog/Scripts/DialogText.cs.meta

523
Assets/Fungus/Dialog/Scripts/Dialog.cs

@ -4,7 +4,6 @@ using UnityEngine.Events;
using System;
using System.Collections;
using System.Collections.Generic;
using System.Text.RegularExpressions;
namespace Fungus
{
@ -35,30 +34,6 @@ namespace Fungus
protected bool wasPointerClicked;
protected enum TokenType
{
Character, // Text character
BoldStart, // b
BoldEnd, // /b
ItalicStart, // i
ItalicEnd, // /i
ColorStart, // color=red
ColorEnd, // /color
Wait, // w, w=0.5
WaitForInputNoClear, // wi
WaitForInputAndClear, // wc
WaitOnPunctuation, // wp, wp=0.5
Clear, // c
Speed, // s, s=60
Exit // x
}
protected class Token
{
public TokenType type = TokenType.Character;
public string param = "";
}
protected virtual void LateUpdate()
{
wasPointerClicked = false;
@ -142,16 +117,12 @@ namespace Fungus
protected virtual IEnumerator WriteText(string text, Action onWritingComplete, Action onExitTag)
{
storyText.text = "";
boldActive = false;
italicActive = false;
colorActive = false;
colorText = "";
currentSpeed = writingSpeed;
currentPunctuationPause = punctuationPause;
List<Token> tokens = MakeTokenList(text);
// Parse the story text & tag markup to produce a list of tokens for processing
DialogParser parser = new DialogParser();
parser.Tokenize(text);
if (tokens.Count == 0)
if (parser.tokens.Count == 0)
{
if (onWritingComplete != null)
{
@ -160,262 +131,134 @@ namespace Fungus
yield break;
}
// Zero speed means write instantly
if (currentSpeed == 0 ||
text.Contains("<"))
{
currentSpeed = 10000;
}
DialogText dialogText = new DialogText();
dialogText.writingSpeed = writingSpeed;
dialogText.punctuationPause = punctuationPause;
GameObject typingAudio = null;
if (writingSound != null)
if (characterTypingSound != null || writingSound != null)
{
typingAudio = new GameObject("WritingSound");
typingAudio.AddComponent<AudioSource>();
if (characterTypingSound != null)
{
typingAudio.audio.clip = characterTypingSound;
}
else
else if (writingSound != null)
{
typingAudio.audio.clip = writingSound;
}
typingAudio.audio.loop = loopWritingSound;
typingAudio.audio.Play();
}
float timeAccumulator = 0f;
dialogText.typingAudio = typingAudio.audio;
}
int i = 0;
while (i < tokens.Count)
foreach (Token token in parser.tokens)
{
timeAccumulator += Time.deltaTime;
float writeDelay = 0f;
if (currentSpeed > 0)
switch (token.type)
{
writeDelay = (1f / (float)currentSpeed);
}
while (timeAccumulator > writeDelay)
{
timeAccumulator -= writeDelay;
Token token = tokens[i];
case TokenType.Words:
dialogText.Append(token.param);
break;
case TokenType.BoldStart:
dialogText.boldActive = true;
break;
case TokenType.BoldEnd:
dialogText.boldActive = false;
break;
case TokenType.ItalicStart:
dialogText.italicActive = true;
break;
case TokenType.ItalicEnd:
dialogText.italicActive = false;
break;
case TokenType.ColorStart:
dialogText.colorActive = true;
dialogText.colorText = token.param;
break;
case TokenType.ColorEnd:
dialogText.colorActive = false;
break;
case TokenType.Wait:
float duration = 1f;
if (!Single.TryParse(token.param, out duration))
{
duration = 1f;
}
yield return StartCoroutine(WaitForSecondsOrInput(duration));
break;
case TokenType.WaitForInputNoClear:
OnWaitForInputTag(true);
yield return StartCoroutine(WaitForInput(null));
OnWaitForInputTag(false);
break;
case TokenType.WaitForInputAndClear:
OnWaitForInputTag(true);
yield return StartCoroutine(WaitForInput(null));
OnWaitForInputTag(false);
currentSpeed = writingSpeed;
dialogText.Clear();
break;
case TokenType.WaitOnPunctuation:
float newPunctuationPause = 0f;
if (!Single.TryParse(token.param, out newPunctuationPause))
{
newPunctuationPause = punctuationPause;
}
dialogText.punctuationPause = newPunctuationPause;
break;
switch (token.type)
case TokenType.Clear:
dialogText.Clear();
break;
case TokenType.Speed:
float newSpeed = 0;
if (!Single.TryParse(token.param, out newSpeed))
{
case TokenType.Character:
if (storyText.text.Length == 0 && token.param == "\n")
{
// Ignore leading newlines
}
else
{
// Wrap each individual character in rich text markup tags if required
// This must be done at the character level to support writing out the story text over time.
string start = "";
string end = "";
if (boldActive)
{
start += "<b>";
end += "</b>";
}
if (italicActive)
{
start += "<i>";
end = "</i>" + end; // Have to nest tags correctly
}
if (colorActive)
{
start += "<color=" + colorText + ">";
end += "</color>";
}
storyText.text += start + token.param + end;
if (wasPointerClicked)
{
currentSpeed = 10000; // Write instantly
wasPointerClicked = false;
}
}
// Add a wait token on punctuation marks
bool doPause = punctuationPause > 0 && IsPunctuation(token.param);
if (i == tokens.Count - 1)
{
doPause = false; // No pause on last character
}
else
{
// No pause if next token is a pause
TokenType nextType = tokens[i + 1].type;
if (nextType == TokenType.Wait ||
nextType == TokenType.WaitForInputAndClear ||
nextType == TokenType.WaitForInputNoClear)
{
doPause = false;
}
if (currentSpeed > 1000)
{
doPause = false;
}
}
if (doPause)
{
// Ignore if next token is also punctuation, or if punctuation is the last character.
bool skipCharacter = (i < tokens.Count - 1 &&
tokens[i + 1].type == TokenType.Character &&
IsPunctuation(tokens[i + 1].param));
if (!skipCharacter)
{
if (typingAudio != null)
typingAudio.audio.Pause();
yield return new WaitForSeconds(currentPunctuationPause);
if (typingAudio != null)
typingAudio.audio.Play();
}
}
break;
case TokenType.BoldStart:
boldActive = true;
break;
case TokenType.BoldEnd:
boldActive = false;
break;
newSpeed = 0f;
}
dialogText.writingSpeed = newSpeed;
break;
case TokenType.ItalicStart:
italicActive = true;
break;
case TokenType.ItalicEnd:
italicActive = false;
break;
case TokenType.ColorStart:
colorActive = true;
colorText = token.param;
break;
case TokenType.ColorEnd:
colorActive = false;
break;
case TokenType.Wait:
float duration = 1f;
if (!Single.TryParse(token.param, out duration))
{
duration = 1f;
}
if (typingAudio != null)
typingAudio.audio.Pause();
yield return new WaitForSeconds(duration);
if (typingAudio != null)
typingAudio.audio.Play();
timeAccumulator = 0f;
break;
case TokenType.WaitForInputNoClear:
OnWaitForInputTag(true);
if (typingAudio != null)
typingAudio.audio.Pause();
yield return StartCoroutine(WaitForInput(null));
if (typingAudio != null)
typingAudio.audio.Play();
timeAccumulator = 0f;
currentSpeed = writingSpeed;
OnWaitForInputTag(false);
break;
case TokenType.Exit:
case TokenType.WaitForInputAndClear:
OnWaitForInputTag(true);
if (typingAudio != null)
typingAudio.audio.Pause();
yield return StartCoroutine(WaitForInput(null));
if (typingAudio != null)
typingAudio.audio.Play();
OnWaitForInputTag(false);
timeAccumulator = 0f;
currentSpeed = writingSpeed;
storyText.text = "";
break;
case TokenType.Clear:
storyText.text = "";
timeAccumulator = 0f;
break;
case TokenType.Speed:
if (!Single.TryParse(token.param, out currentSpeed))
{
currentSpeed = 0f;
}
writeDelay = 0;
timeAccumulator = 0f;
if (currentSpeed > 0)
{
writeDelay = (1f / (float)currentSpeed);
}
break;
case TokenType.Exit:
if (typingAudio != null)
{
Destroy(typingAudio);
}
if (onExitTag != null)
{
onExitTag();
}
yield break;
case TokenType.WaitOnPunctuation:
if (!Single.TryParse(token.param, out currentPunctuationPause))
{
currentPunctuationPause = 0f;
}
break;
if (onExitTag != null)
{
Destroy(typingAudio);
onExitTag();
}
yield break;
}
if (++i >= tokens.Count)
{
break;
}
// Update text writing
while (!dialogText.UpdateGlyphs(wasPointerClicked))
{
storyText.text = dialogText.GetDialogText();
yield return null;
}
storyText.text = dialogText.GetDialogText();
wasPointerClicked = false;
yield return null;
// Now process next token
}
if (typingAudio != null)
{
Destroy(typingAudio);
}
Destroy(typingAudio);
if (onWritingComplete != null)
{
onWritingComplete();
@ -423,7 +266,7 @@ namespace Fungus
yield break;
}
public virtual void Clear()
{
ClearStoryText();
@ -448,163 +291,31 @@ namespace Fungus
}
}
protected virtual bool IsPunctuation(string character)
{
return character == "." ||
character == "?" ||
character == "!";
}
protected virtual List<Token> MakeTokenList(string storyText)
{
List<Token> tokenList = new List<Token>();
string pattern = @"\{.*?\}";
Regex myRegex = new Regex(pattern);
Match m = myRegex.Match(storyText); // m is the first match
int position = 0;
while (m.Success)
{
// Get bit leading up to tag
string preText = storyText.Substring(position, m.Index - position);
string tagText = m.Value;
foreach (char c in preText)
{
AddCharacterToken(tokenList, c.ToString());
}
AddTagToken(tokenList, tagText);
position = m.Index + tagText.Length;
m = m.NextMatch();
}
if (position < storyText.Length - 1)
{
string postText = storyText.Substring(position, storyText.Length - position);
foreach (char c in postText)
{
AddCharacterToken(tokenList, c.ToString());
}
}
return tokenList;
}
protected virtual void AddCharacterToken(List<Token> tokenList, string character)
{
Token token = new Token();
token.type = TokenType.Character;
token.param = character;
tokenList.Add(token);
}
protected virtual void AddTagToken(List<Token> tokenList, string tagText)
protected virtual IEnumerator WaitForInput(Action onInput)
{
if (tagText.Length < 3 ||
tagText.Substring(0,1) != "{" ||
tagText.Substring(tagText.Length - 1,1) != "}")
while (!wasPointerClicked)
{
return;
yield return null;
}
string tag = tagText.Substring(1, tagText.Length - 2);
TokenType type = TokenType.Character;
string paramText = "";
wasPointerClicked = false;
if (tag == "b")
{
type = TokenType.BoldStart;
}
else if (tag == "/b")
{
type = TokenType.BoldEnd;
}
else if (tag == "i")
{
type = TokenType.ItalicStart;
}
else if (tag == "/i")
{
type = TokenType.ItalicEnd;
}
else if (tag.StartsWith("color="))
{
type = TokenType.ColorStart;
paramText = tag.Substring(6, tag.Length - 6);
}
else if (tag == "/color")
{
type = TokenType.ColorEnd;
}
else if (tag == "wi")
{
type = TokenType.WaitForInputNoClear;
}
if (tag == "wc")
{
type = TokenType.WaitForInputAndClear;
}
else if (tag.StartsWith("wp="))
{
type = TokenType.WaitOnPunctuation;
paramText = tag.Substring(3, tag.Length - 3);
}
else if (tag == "wp")
{
type = TokenType.WaitOnPunctuation;
}
else if (tag.StartsWith("w="))
{
type = TokenType.Wait;
paramText = tag.Substring(2, tag.Length - 2);
}
else if (tag == "w")
{
type = TokenType.Wait;
}
else if (tag == "c")
{
type = TokenType.Clear;
}
else if (tag.StartsWith("s="))
{
type = TokenType.Speed;
paramText = tag.Substring(2, tag.Length - 2);
}
else if (tag == "s")
{
type = TokenType.Speed;
}
else if (tag == "x")
if (onInput != null)
{
type = TokenType.Exit;
onInput();
}
Token token = new Token();
token.type = type;
token.param = paramText.Trim();
tokenList.Add(token);
}
protected virtual IEnumerator WaitForInput(Action onInput)
protected virtual IEnumerator WaitForSecondsOrInput(float duration)
{
while (!wasPointerClicked)
float timer = duration;
while (timer > 0 && !wasPointerClicked)
{
timer -= Time.deltaTime;
yield return null;
}
wasPointerClicked = false;
if (onInput != null)
{
onInput();
}
}
protected virtual void OnWaitForInputTag(bool waiting)

165
Assets/Fungus/Dialog/Scripts/DialogParser.cs

@ -0,0 +1,165 @@
using UnityEngine;
using System.Collections;
using System.Collections.Generic;
using System.Text.RegularExpressions;
namespace Fungus
{
public enum TokenType
{
Words, // A string of words
BoldStart, // b
BoldEnd, // /b
ItalicStart, // i
ItalicEnd, // /i
ColorStart, // color=red
ColorEnd, // /color
Wait, // w, w=0.5
WaitForInputNoClear, // wi
WaitForInputAndClear, // wc
WaitOnPunctuation, // wp, wp=0.5
Clear, // c
Speed, // s, s=60
Exit // x
}
public class Token
{
public TokenType type = TokenType.Words;
public string param = "";
}
public class DialogParser
{
public List<Token> tokens = new List<Token>();
public virtual void Tokenize(string storyText)
{
tokens.Clear();
string pattern = @"\{.*?\}";
Regex myRegex = new Regex(pattern);
Match m = myRegex.Match(storyText); // m is the first match
int position = 0;
while (m.Success)
{
// Get bit leading up to tag
string preText = storyText.Substring(position, m.Index - position);
string tagText = m.Value;
AddWordsToken(tokens, preText);
AddTagToken(tokens, tagText);
position = m.Index + tagText.Length;
m = m.NextMatch();
}
if (position < storyText.Length - 1)
{
string postText = storyText.Substring(position, storyText.Length - position);
AddWordsToken(tokens, postText);
}
}
protected static void AddWordsToken(List<Token> tokenList, string words)
{
Token token = new Token();
token.type = TokenType.Words;
token.param = words;
tokenList.Add(token);
}
protected virtual void AddTagToken(List<Token> tokenList, string tagText)
{
if (tagText.Length < 3 ||
tagText.Substring(0,1) != "{" ||
tagText.Substring(tagText.Length - 1,1) != "}")
{
return;
}
string tag = tagText.Substring(1, tagText.Length - 2);
TokenType type = TokenType.Words;
string paramText = "";
if (tag == "b")
{
type = TokenType.BoldStart;
}
else if (tag == "/b")
{
type = TokenType.BoldEnd;
}
else if (tag == "i")
{
type = TokenType.ItalicStart;
}
else if (tag == "/i")
{
type = TokenType.ItalicEnd;
}
else if (tag.StartsWith("color="))
{
type = TokenType.ColorStart;
paramText = tag.Substring(6, tag.Length - 6);
}
else if (tag == "/color")
{
type = TokenType.ColorEnd;
}
else if (tag == "wi")
{
type = TokenType.WaitForInputNoClear;
}
if (tag == "wc")
{
type = TokenType.WaitForInputAndClear;
}
else if (tag.StartsWith("wp="))
{
type = TokenType.WaitOnPunctuation;
paramText = tag.Substring(3, tag.Length - 3);
}
else if (tag == "wp")
{
type = TokenType.WaitOnPunctuation;
}
else if (tag.StartsWith("w="))
{
type = TokenType.Wait;
paramText = tag.Substring(2, tag.Length - 2);
}
else if (tag == "w")
{
type = TokenType.Wait;
}
else if (tag == "c")
{
type = TokenType.Clear;
}
else if (tag.StartsWith("s="))
{
type = TokenType.Speed;
paramText = tag.Substring(2, tag.Length - 2);
}
else if (tag == "s")
{
type = TokenType.Speed;
}
else if (tag == "x")
{
type = TokenType.Exit;
}
Token token = new Token();
token.type = type;
token.param = paramText.Trim();
tokenList.Add(token);
}
}
}

8
Assets/Fungus/Dialog/Scripts/DialogParser.cs.meta

@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 674037e0ad6e34e149f9bbab6940e155
MonoImporter:
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:

197
Assets/Fungus/Dialog/Scripts/DialogText.cs

@ -0,0 +1,197 @@
using UnityEngine;
using System.Collections;
using System.Collections.Generic;
namespace Fungus
{
public class Glyph
{
public float hideTimer;
public string character;
public bool boldActive;
public bool italicActive;
public bool colorActive;
public string colorText;
public bool hasPunctuationPause;
}
public class DialogText
{
protected List<Glyph> glyphs = new List<Glyph>();
public bool boldActive { get; set; }
public bool italicActive { get; set; }
public bool colorActive { get; set; }
public string colorText { get; set; }
public float writingSpeed { get; set; }
public float punctuationPause { get; set; }
public AudioSource typingAudio { get; set; }
public virtual void Clear()
{
glyphs.Clear();
}
public virtual void Append(string words)
{
if (typingAudio != null)
{
typingAudio.Stop();
typingAudio.Play();
}
float hideTimer = 0f;
if (writingSpeed > 0f)
{
hideTimer = 1f / writingSpeed;
}
bool doPunctuationPause = false;
for (int i = 0; i < words.Length; ++i)
{
char c = words[i];
// Ignore leading newlines
if (glyphs.Count == 0 && c == '\n')
{
continue;
}
Glyph glyph = new Glyph();
glyph.hideTimer = hideTimer;
if (doPunctuationPause)
{
glyph.hasPunctuationPause = true;
glyph.hideTimer += punctuationPause;
doPunctuationPause = false;
}
glyph.character = c.ToString();
glyph.boldActive = boldActive;
glyph.italicActive = italicActive;
glyph.colorActive = colorActive;
glyph.colorText = colorText;
glyphs.Add(glyph);
if (i < words.Length - 2 &&
IsPunctuation(c) &&
!IsPunctuation(words[i + 1])) // No punctuation pause on last character, or if next character is also punctuation
{
doPunctuationPause = true;
}
}
}
protected virtual bool IsPunctuation(char character)
{
return character == '.' || character == '?' || character == '!';
}
/**
* Returns true when all glyphs are visible.
*/
public virtual bool UpdateGlyphs(bool instantComplete)
{
float elapsedTime = Time.deltaTime;
foreach (Glyph glyph in glyphs)
{
if (instantComplete)
{
glyph.hideTimer = 0f;
continue;
}
if (glyph.hideTimer > 0f)
{
if (typingAudio != null &&
glyph.hasPunctuationPause)
{
typingAudio.volume = 0f;
}
bool finished = false;
if (elapsedTime > glyph.hideTimer)
{
elapsedTime -= glyph.hideTimer;
glyph.hideTimer = 0f;
// Some elapsed time left over, so carry on to next glyph
}
else
{
glyph.hideTimer -= elapsedTime;
glyph.hideTimer = Mathf.Max(glyph.hideTimer, 0f);
finished = true;
}
// Check if we need to restore audio after a punctuation pause
if (typingAudio != null &&
glyph.hideTimer == 0f &&
typingAudio.volume == 0f)
{
typingAudio.volume = 1f;
}
if (finished)
{
return false; // Glyph is still hidden
}
}
}
if (typingAudio != null)
{
typingAudio.Stop();
}
return true;
}
public virtual string GetDialogText()
{
string outputText = "";
bool hideGlyphs = false;
foreach (Glyph glyph in glyphs)
{
// Wrap each individual character in rich text markup tags (if required)
string start = "";
string end = "";
if (glyph.boldActive)
{
start += "<b>";
end += "</b>";
}
if (glyph.italicActive)
{
start += "<i>";
end = "</i>" + end; // Have to nest tags correctly
}
if (!hideGlyphs &&
glyph.hideTimer > 0f)
{
hideGlyphs = true;
outputText += "<color=#FFFFFF00>";
}
if (!hideGlyphs &&
glyph.colorActive)
{
start += "<color=" + glyph.colorText + ">";
end += "</color>";
}
outputText += start + glyph.character + end;
}
if (hideGlyphs)
{
outputText += "</color>";
}
return outputText;
}
}
}

8
Assets/Fungus/Dialog/Scripts/DialogText.cs.meta

@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 4aada7218611f4257bddea1cd4ab8fcf
MonoImporter:
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
Loading…
Cancel
Save