Typewriter with Jitter effect
Posted: Mon Jan 27, 2020 6:43 pm
Before I started using Dialogue System for Unity, I'd written a script that looked through all <link> tags in a text mesh pro component and applied animations to the characters therein.
Example:
<link="jitter">this text will jitter</link>
or something like that.
The code looked something like this:
with some coroutines like this:
Definitely not super pretty, but it got the job done. The problem is I can't use this code when maxCharactersVisible is used. In order to use the typewriter and text animation together, the typewriter effect has to be done by setting individual character alpha.
Dialogue System is a lot of code so my question is, does this feature already exist and/or is it something that's planned at any point? Just trying to figure out how much custom code I've got to write.
Example:
<link="jitter">this text will jitter</link>
or something like that.
The code looked something like this:
Code: Select all
/// <summary>
/// Handles animation of rich text animation tags.
/// </summary>
[RequireComponent(typeof(TextMeshProUGUI))]
public class RichTextAnimationTagRunner : MonoBehaviour
{
// Constants
private const int ClockPrecisionDecimals = 2; // Centisecond
private static readonly float ClockIncrement = (1f / Mathf.Pow(10f, ClockPrecisionDecimals)) * 5;
// References
public TextMeshProUGUI textScript;
// Properties
private AnimationHandle animationHandle;
private readonly object animationHandleLock = new object();
void Reset()
{
textScript = GetComponent<TextMeshProUGUI>();
}
public void StartAnimations()
{
lock (animationHandleLock)
{
animationHandle = new AnimationHandle(textScript, ClockIncrement, ClockPrecisionDecimals);
TMP_TextInfo textInfo = textScript.textInfo;
int linkCount = textInfo.linkCount;
Coroutine[] coroutines = new Coroutine[linkCount];
for (int i = 0; i < linkCount; i++)
{
int start = textInfo.linkInfo[i].linkTextfirstCharacterIndex;
int end = start + textInfo.linkInfo[i].linkTextLength;
RecitationTextEmotion emotion = (RecitationTextEmotion)Enum.Parse(typeof(RecitationTextEmotion), textScript.textInfo.linkInfo[i].GetLinkID());
switch (emotion)
{
case RecitationTextEmotion.Jitter:
JitterAnimationConfig jitterConfig = new JitterAnimationConfig();
jitterConfig.curveScale = 1.0f;
jitterConfig.angleMultiplier = 1.0f;
jitterConfig.speedMultiplier = 0.85f;
jitterConfig.startIndex = start;
jitterConfig.endIndex = end;
jitterConfig.animationHandle = animationHandle;
coroutines[i] = StartCoroutine(SpeechAnimationRoutines.AnimateJitter(jitterConfig));
break;
case RecitationTextEmotion.SingSong:
SingSongAnimationConfig singSongConfig = new SingSongAnimationConfig();
singSongConfig.angle = 10.0f;
singSongConfig.angleIncrement = 2.5f;
singSongConfig.speedMultiplier = 1.0f;
singSongConfig.startIndex = start;
singSongConfig.endIndex = end;
singSongConfig.animationHandle = animationHandle;
coroutines[i] = StartCoroutine(SpeechAnimationRoutines.AnimateSingSong(singSongConfig));
break;
default:
throw new Exception("Unknown text emotion: " + emotion);
}
}
animationHandle.shutdownAnimation = () =>
{
foreach (Coroutine coroutine in coroutines)
{
StopCoroutine(coroutine);
}
};
}
}
public void StopAnimations()
{
lock (animationHandleLock)
{
animationHandle?.TearDown();
}
}
}
/// <summary>
/// Any animations that should be applied to the text. This is used in <link="Emotion here"> tags.
/// </summary>
public enum RecitationTextEmotion
{
Jitter,
SingSong,
}
Code: Select all
/// <summary>
/// A set of animation routines for speech text.
/// </summary>
public static class SpeechAnimationRoutines
{
/// <summary>
/// Animate a jitter in the letters.
/// </summary>
/// <param name="jitterConfig">Configuration information for the jitter.</param>
/// <returns>IEnumerator for the coroutine.</returns>
public static IEnumerator AnimateJitter(JitterAnimationConfig jitterConfig)
{
Matrix4x4 matrix;
TMP_TextInfo textInfo = jitterConfig.animationHandle.textScript.textInfo;
AnimationHandle animHandle = jitterConfig.animationHandle;
float waitTime = animHandle.clockIncrement / jitterConfig.speedMultiplier;
while (true)
{
int i;
for (i = jitterConfig.startIndex; i < jitterConfig.endIndex && animHandle.cachedMeshInfo != null; i++)
{
// Grab Character
TMP_CharacterInfo charInfo = textInfo.characterInfo[i];
if (charInfo.character == ' ')
{
continue;
}
// Make sure character is visible
int materialIndex = charInfo.materialReferenceIndex;
int vertexIndex = charInfo.vertexIndex;
Color32[] newVertexColors = textInfo.meshInfo[materialIndex].colors32;
if (newVertexColors[vertexIndex + 0].a == 0 || !charInfo.isVisible)
{
break;
}
// Get the cached vertices of the mesh used by this text element (character or sprite).
Vector3[] sourceVertices = animHandle.cachedMeshInfo[materialIndex].vertices;
// Determine the center point of each character.
Vector2 charMidBasline = (sourceVertices[vertexIndex + 0] + sourceVertices[vertexIndex + 2]) / 2;
// Need to translate all 4 vertices of each quad to aligned with middle of character / baseline.
// This is needed so the matrix TRS is applied at the origin for each character.
Vector3 offset = charMidBasline;
Vector3[] destinationVertices = textInfo.meshInfo[materialIndex].vertices;
destinationVertices[vertexIndex + 0] = sourceVertices[vertexIndex + 0] - offset;
destinationVertices[vertexIndex + 1] = sourceVertices[vertexIndex + 1] - offset;
destinationVertices[vertexIndex + 2] = sourceVertices[vertexIndex + 2] - offset;
destinationVertices[vertexIndex + 3] = sourceVertices[vertexIndex + 3] - offset;
// Apply Jitter
Vector3 jitterOffset = new Vector3(UnityEngine.Random.Range(-.25f, .25f), UnityEngine.Random.Range(-.25f, .25f), 0);
matrix = Matrix4x4.TRS(jitterOffset * jitterConfig.curveScale, Quaternion.Euler(0, 0, UnityEngine.Random.Range(-5f, 5f) * jitterConfig.angleMultiplier), Vector3.one);
destinationVertices[vertexIndex + 0] = matrix.MultiplyPoint3x4(destinationVertices[vertexIndex + 0]);
destinationVertices[vertexIndex + 1] = matrix.MultiplyPoint3x4(destinationVertices[vertexIndex + 1]);
destinationVertices[vertexIndex + 2] = matrix.MultiplyPoint3x4(destinationVertices[vertexIndex + 2]);
destinationVertices[vertexIndex + 3] = matrix.MultiplyPoint3x4(destinationVertices[vertexIndex + 3]);
// Translate all 4 vertices of each quad back to their original positions
destinationVertices[vertexIndex + 0] += offset;
destinationVertices[vertexIndex + 1] += offset;
destinationVertices[vertexIndex + 2] += offset;
destinationVertices[vertexIndex + 3] += offset;
// Push changes into meshes
textInfo.meshInfo[materialIndex].mesh.vertices = textInfo.meshInfo[materialIndex].vertices;
animHandle.textScript.UpdateGeometry(textInfo.meshInfo[materialIndex].mesh, materialIndex);
}
yield return new WaitForSeconds(waitTime);
}
}
/// <summary>
/// Animate sing song letters.
/// </summary>
/// <param name="singSongConfig">Configuration for the sing song animation.</param>
/// <returns>IEnumerator for the coroutine.</returns>
public static IEnumerator AnimateSingSong(SingSongAnimationConfig singSongConfig)
{
Matrix4x4 matrix;
TMP_TextInfo textInfo = singSongConfig.animationHandle.textScript.textInfo;
AnimationHandle animHandle = singSongConfig.animationHandle;
float waitTime = animHandle.clockIncrement / singSongConfig.speedMultiplier;
float angleCounter = 0f;
float doubleTheAngle = singSongConfig.angle * 2;
while (true)
{
angleCounter += singSongConfig.angleIncrement;
float newAngle = Mathf.PingPong(angleCounter++, doubleTheAngle) - singSongConfig.angle;
for (int i = singSongConfig.startIndex; i < singSongConfig.endIndex && animHandle.cachedMeshInfo != null; i++)
{
// Grab Character
TMP_CharacterInfo charInfo = textInfo.characterInfo[i];
if (charInfo.character == ' ')
{
continue;
}
// Make sure character is visible
int materialIndex = charInfo.materialReferenceIndex;
int vertexIndex = charInfo.vertexIndex;
Color32[] newVertexColors = textInfo.meshInfo[materialIndex].colors32;
if (newVertexColors[vertexIndex + 0].a == 0 || !charInfo.isVisible)
{
break;
}
// Get the cached vertices of the mesh used by this text element (character or sprite).
Vector3[] sourceVertices = animHandle.cachedMeshInfo[materialIndex].vertices;
// Determine the center point of each character.
Vector2 charMidBasline = (sourceVertices[vertexIndex + 0] + sourceVertices[vertexIndex + 2]) / 2;
// Need to translate all 4 vertices of each quad to aligned with middle of character / baseline.
// This is needed so the matrix TRS is applied at the origin for each character.
Vector3 offset = charMidBasline;
Vector3[] destinationVertices = textInfo.meshInfo[materialIndex].vertices;
destinationVertices[vertexIndex + 0] = sourceVertices[vertexIndex + 0] - offset;
destinationVertices[vertexIndex + 1] = sourceVertices[vertexIndex + 1] - offset;
destinationVertices[vertexIndex + 2] = sourceVertices[vertexIndex + 2] - offset;
destinationVertices[vertexIndex + 3] = sourceVertices[vertexIndex + 3] - offset;
// Apply SingSong
matrix = Matrix4x4.TRS(Vector3.one, Quaternion.Euler(0, 0, newAngle), Vector3.one);
destinationVertices[vertexIndex + 0] = matrix.MultiplyPoint3x4(destinationVertices[vertexIndex + 0]);
destinationVertices[vertexIndex + 1] = matrix.MultiplyPoint3x4(destinationVertices[vertexIndex + 1]);
destinationVertices[vertexIndex + 2] = matrix.MultiplyPoint3x4(destinationVertices[vertexIndex + 2]);
destinationVertices[vertexIndex + 3] = matrix.MultiplyPoint3x4(destinationVertices[vertexIndex + 3]);
// Translate all 4 vertices of each quad back to their original positions
destinationVertices[vertexIndex + 0] += offset;
destinationVertices[vertexIndex + 1] += offset;
destinationVertices[vertexIndex + 2] += offset;
destinationVertices[vertexIndex + 3] += offset;
// Push changes into meshes
textInfo.meshInfo[materialIndex].mesh.vertices = textInfo.meshInfo[materialIndex].vertices;
animHandle.textScript.UpdateGeometry(textInfo.meshInfo[materialIndex].mesh, materialIndex);
}
yield return new WaitForSeconds(waitTime);
}
}
}
/// <summary>
/// Handle for a running animation.
/// </summary>
public class AnimationHandle
{
public TextMeshProUGUI textScript;
public TMP_MeshInfo[] cachedMeshInfo;
public float clockIncrement;
public int clockPrecisionDecimals;
public UnityAction shutdownAnimation { private get; set; }
public AnimationHandle(TextMeshProUGUI textScript, float clockIncrement, int clockPrecisionDecimals)
{
this.textScript = textScript;
this.clockIncrement = clockIncrement;
this.clockPrecisionDecimals = clockPrecisionDecimals;
this.cachedMeshInfo = textScript.textInfo.CopyMeshInfoVertexData();
TMPro_EventManager.TEXT_CHANGED_EVENT.Add(OnTextChange);
}
public void TearDown()
{
TMPro_EventManager.TEXT_CHANGED_EVENT.Remove(OnTextChange);
shutdownAnimation();
}
public void UpdateCachedMesh()
{
cachedMeshInfo = textScript.textInfo.CopyMeshInfoVertexData();
}
private void OnTextChange(UnityEngine.Object obj)
{
if (textScript == obj)
{
UpdateCachedMesh();
}
}
}
/// <summary>
/// Base class for animation configuration.
/// </summary>
public abstract class AnimationConfig
{
public AnimationHandle animationHandle;
public int startIndex;
public int endIndex;
public float speedMultiplier;
public TextMeshProUGUI textScript;
public TMP_MeshInfo[] cachedMeshInfo;
public bool[] characterIsVisible;
public float clockIncrement;
public int clockPrecisionDecimals;
}
/// <summary>
/// Information for the jitter animation.
/// </summary>
public class JitterAnimationConfig : AnimationConfig
{
public float angleMultiplier;
public float curveScale;
}
/// <summary>
/// Information for the SingSong animation.
/// </summary>
public class SingSongAnimationConfig : AnimationConfig
{
public float angle;
public float angleIncrement;
}
Dialogue System is a lot of code so my question is, does this feature already exist and/or is it something that's planned at any point? Just trying to figure out how much custom code I've got to write.