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.