A zero-allocation, tag-based text animation system for TextMeshPro.
Advanced Text Animator lets you control per-character animation directly from your TextMeshPro text using simple XML-style tags. It also integrates with typewriter controls, dynamic outro animations, world-space floating text, and an event-driven notification system.
Author: Vishal Pundir
Support Contact: pundirvishal.dev@gmail.com
<wave speed=10 amp=20>Hello</wave>
Animation tags can be nested together:
<pop><float><color=#00FF00>Secret Item!</color></float></pop>
Parameters can be overridden directly inside a tag:
<wiggle speed=15 amp=4>Warning!</wiggle>
Wrap text in tags to animate it.
Each effect has default parameters, which can be overridden by specifying parameters inside the tag.
For example:
<wave speed=10 amp=20>Hello</wave>
These effects loop infinitely as long as the text is visible.
| Tag | Description | Default Parameters |
|---|---|---|
<wave> |
Smooth sine wave movement up and down. | speed=4 amp=5 freq=1 |
<wiggle> |
Jittery, randomized organic shake. | speed=10 amp=2.5 |
<float> |
Gentle floating levitation. | speed=3 amp=10 freq=0.5 |
<pulse> |
Heartbeat scaling (shrinks and grows). | speed=4 amp=0.3 freq=0.2 |
<shake> |
Violent translation shake. | speed=20 amp=3 |
<swing> |
Pendulum-like rotation. | speed=3 amp=15 freq=0.5 |
<rainbow> |
Scrolling HSV rainbow colors. | speed=1 freq=0.1 |
<gradient> |
Applies a color gradient. Add speed to animate it. |
c1=#FF6464 c2=#64FF64 speed=0 freq=0.15 |
<jitter> |
Choppy, low-framerate translation shake. | speed=15 amp=3 |
<breathe> |
Smooth, slow scaling breathing effect. | speed=2 amp=0.2 |
<spin> |
Continuous circular rotation. | speed=100 freq=10 |
<flicker> |
Randomly drops alpha for a glitchy look. | speed=10 freq=1 |
<tornado> |
Circular orbital movement. | speed=5 amp=5 freq=1 |
<hop> |
Bounces upward from the baseline. | speed=5 amp=15 freq=1 |
<strobe> |
Rapidly flashes between two colors. | speed=10 freq=0 c1=#FFFFFF c2=#000000 |
<sway> |
Horizontal back-and-forth movement. | speed=2 amp=10 freq=0.5 |
<squish> |
Alternating X and Y scale distortion. | speed=5 amp=0.2 freq=1 |
<beat> |
Sharp, rhythmic scale pulsing. | speed=3 amp=0.3 |
These effects play once when the character first appears on screen.
| Tag | Description | Default Parameters |
|---|---|---|
<pop> |
Snaps scale from 0 → 1 with an elastic overshoot. | dur=0.4 |
<bounce> |
Drops characters in from above with a bounce. | dur=0.5 height=30 |
<fadein> |
Alpha transitions from 0 → 1. | dur=0.5 |
<slidein> |
Slides in from a specific X/Y offset. | dur=0.4 x=-50 y=0 |
<zoomin> |
Scales down from a massive size to 1. | dur=0.4 scale=3 |
<rollin> |
Rotates 360° while sliding in. | dur=0.5 x=-100 |
<dropin> |
A gravity-based drop with a heavy bounce. | dur=0.6 height=100 |
<flyin> |
Flies downward while rotating 90° and fading in. | dur=0.5 height=100 |
<risein> |
Rises up from below while fading in. | dur=0.5 depth=50 |
<expandin> |
Smoothly scales up from 0 to 1 without overshoot. | dur=0.3 |
<spiralin> |
Scales up from 0 while spinning 360°. | dur=0.5 |
<stretchin> |
Snaps into place from an extreme vertical stretch. | dur=0.4 stretch=5 |
<flipin> |
Flips open along the X-axis (horizontal scale 0 to 1). | dur=0.4 |
These effects wait for a specified delay before animating the character out of existence. Once finished, the character is hidden.
| Tag | Description | Default Parameters |
|---|---|---|
<popout> |
Scales down to 0 with a sharp elastic snap. | delay=2 dur=0.4 |
<bounceout> |
Drops down into the floor and fades. | delay=2 dur=0.5 height=30 |
<fadeout> |
Alpha transitions from 1 → 0. | delay=2 dur=0.5 |
<slideout> |
Slides away to a specific X/Y offset. | delay=2 dur=0.4 x=50 y=0 |
<shrinkout> |
Scales linearly to 0. | delay=2 dur=0.3 |
<flyaway> |
Flies upwards, rotates, and fades out. | delay=2 dur=0.5 height=100 |
<sinkout> |
Sinks directly downward and fades. | delay=2 dur=0.5 depth=50 |
<spiralout> |
Scales down to 0 while spinning 360°. | delay=2 dur=0.5 |
<stretchout> |
Vertically stretches to an extreme size while fading. | delay=2 dur=0.4 stretch=5 |
<flipout> |
Flips shut along the X-axis (horizontal scale 1 to 0). | delay=2 dur=0.4 |
Instead of relying on a hardcoded delay= tag, an outro effect can be triggered manually from code.
This is useful for dialogue boxes, crosshair interactions, feed notifications, and other UI where the exact disappearance moment is controlled by gameplay.
// Instantly applies the "slideout" effect to the text over 0.25 seconds
myTextAnimator.DisappearText(0.25f, "slideout");
These tags only function when Use Typewriter is enabled on the main component.
| Tag | Description | Example |
|---|---|---|
<pause=X> |
Pauses typing for X seconds. | Wait... <pause=1.5> GO! |
<speed=X> |
Multiplies typing speed. Close the tag to reset. | <speed=5>FAST</speed> |
<event=Name> |
Triggers the OnCustomEvent UnityEvent. | Explosion <event=Boom> NOW! |
The parser runs before TextMeshPro, so native TMP tags can be mixed freely inside Advanced Text Animator tags.
Because TextMeshPro uses a single geometry mesh for both the text face and the outline, outlines are handled using TMP material settings, not animation tags.
These settings work alongside all Advanced Text Animator effects.
This applies an outline to the entire text component.
To outline only certain words:
Example:
MyFont Outline
Then swap fonts mid-sentence using TMP’s native <font> tag:
<slidein><pulse>Hello <font="MyFont Outline">how are</font> you</pulse></slidein>
The entire sentence animates seamlessly, while only “how are” renders with the outline.
<color=#FF0000>Red Text</color>
Sets the base color.
<size=150%>Huge</size>
Scales the text.
<b>Bold</b>
<i>Italics</i>
<u>Underline</u>
Basic text formatting.
<mark=#FFFF0080>Highlighted</mark>
Adds a background box.
<sprite=0>
or:
<sprite name="Coin">
Inserts an inline icon from your Sprite Asset.
Copy and paste these directly into your TextMeshPro text box to test the system.
Hello <wave>World</wave>!
You found a <pop><float>Secret Item!</float></pop>
Warning: <wiggle speed=15 amp=4><color=red>Core Meltdown</color></wiggle>
Incoming... <pause=1> <dropin height=200>BOOM</dropin>
<gradient c1=#00E5FF c2=#0044FF speed=4 freq=0.5>
Mana fully restored.
</gradient>
The system includes a highly optimized Object Pooling manager for world-space text.
Typical uses include:
Because the system relies on animation tags for movement and fading, floating text remains lightweight and modular.
In your Unity hierarchy, right-click and create a:
UI → Canvas
Rename it:
Animated Text Prefab
In the Canvas component, change Render Mode to:
World Space
Set the Canvas RectTransform:
31Set the RectTransform Scale to:
(1, 1, 1)
Add a TextMeshPro - Text (UI) component directly to this Canvas object.
Set:
~0.25 (or enable Auto Size)Add the AdvancedTextAnimator component to this object.
Disable:
Drag the Animated Text Prefab from your hierarchy into your Project window to save it as a Prefab, then delete it from the scene.
Create an empty GameObject in your scene and name it:
TextPooler
Add the:
AnimatedTextPooler
script to it.
Drag your newly created Animated Text Prefab into the Prefab slot on the script.
How many instances to generate on Awake to prevent lag spikes.
How long text stays alive before returning to the pool.
Example:
3 seconds
Keep this checked so the text automatically rotates to face the player.
Whenever a player mines a rock, hits an enemy, or picks up ammo, call the static Spawn function from any script.
Because it uses Advanced Text Animator, animation tags can be passed directly into the string.
string itemName = "Wood";
int amount = 1;
// Uses <zoomin> and <float> to appear, then <flyaway> to disappear
string harvestText =
$"<zoomin><float><color=#00FF00>+{amount} {itemName}</color></float><flyaway delay=1.5></flyaway></zoomin>";
Vector3 offset = new Vector3(0.5f, 0.5f, 0f);
AnimatedTextPooler.Spawn(harvestText, tree.transform.position, offset);
int damage = 45;
// Uses <pop> to appear, <wiggle> for impact, and <shrinkout> to vanish
string damageText =
$"<pop><shrinkout delay=1><wiggle speed=15 amp=2><color=red>-{damage}</color></wiggle></shrinkout></pop>";
AnimatedTextPooler.Spawn(
damageText,
enemy.transform.position,
new Vector3(0.5f, 0f, 0f)
);
string pickup =
"<gradient c1=#FFD700 c2=#FFA500><popout delay=4>+10 Gold</popout></gradient>";
// The 4th parameter overrides the default lifetime of the pooler.
// Here we make sure it stays alive in the pool for 5 seconds
// to accommodate the long 4-second delay.
AnimatedTextPooler.Spawn(
pickup,
player.transform.position + Vector3.up,
default,
5f
);
The Notification System is a completely decoupled, event-driven architecture designed to handle dynamic text throughout the game.
It acts as a bridge between gameplay logic—such as picking up items or looking at objects—and the UI, without either system needing direct references to the other.
The architecture is divided into two main notification types:
+50 Coins that appear in a screen corner, stack smoothly, and automatically disappear after a set duration.At the heart of the system is the NotificationEventBus.
This is a runtime MonoBehaviour singleton created on demand and used to route notification messages from gameplay scripts to the appropriate UI listeners.
To route notifications to different parts of the UI, the system uses the NotificationChannel enum:
public enum NotificationChannel
{
Crosshair, // Center screen interactables
Loot, // Top-left feed (e.g., "+50 Coins")
Quest, // Top-right feed (e.g., "Quest Updated")
Warning // Center screen alerts (e.g., "Inventory Full")
}
Gameplay code does not need to know which UI object displays the notification.
It only specifies the channel:
NotificationChannel.Loot
The corresponding UI listener decides where and how the message is displayed.
This keeps gameplay systems and UI systems loosely coupled.
Crosshair notifications are designed for interactable objects in your game (game-specific helper classes such as InteractableInstance are not included with this package).
The message appears while the player is looking at an object and is manually removed when focus is lost. If your project already has a focus/interaction system, call the event bus from your focus and blur callbacks to display contextual messages. Example integration:
// Example integration — adapt to your project's focus callbacks
public class ExampleFocusHandler : MonoBehaviour
{
private void OnFocus()
{
NotificationEventBus.ShowMessage("<fadein dur=0.2>Open Door", null);
}
private void OnBlur()
{
NotificationEventBus.HideMessage(0.25f, "slideout");
}
}
If you do not ship a focus helper with your project, implement a small glue script like the example above and hook it into your existing player or camera look system.
Show a message when an object receives focus:
// Show the text, for example inside OnFocus.
// Intro tags such as <fadein> or <slidein> can be used here.
NotificationEventBus.ShowMessage(
"<fadein dur=0.2>Open Door",
null
);
Hide the message when focus is lost:
// Trigger an outro effect instead of abruptly hiding the text.
NotificationEventBus.HideMessage(
0.25f,
"slideout"
);
DisappearText() UpgradePreviously, text animators required a hardcoded delay= tag to know when to disappear.
The current:
AdvancedTextAnimator.DisappearText()
method allows an outro effect to be injected on command.
When HideMessage is called, the animator dynamically retrieves the requested effect—such as slideout, shrinkout, or fadeout—from the tag registry and applies it immediately.
This makes contextual UI behavior controllable by gameplay rather than by fixed timing values embedded in the text.
The feed system handles multiple notifications appearing at the same time.
It uses a manual fixed-position stack rather than Unity’s VerticalLayoutGroup. This gives the feed deterministic insertion animation, avoids repeated layout rebuilds, and keeps CPU usage low.
New notifications are inserted at the top only after existing notifications have finished pushing downward.
The insertion sequence is:
Existing notifications
↓
Push every existing notification down one slot
↓
Push animation finishes
↓
Add new notification at the top
↓
Play new notification's intro animation
For example:
First notification:
[A]
Second notification:
[A] ← pushed down
↓
[B]
[A]
Third notification:
[B] ← pushed down
[A] ← pushed down
↓
[C]
[B]
[A]
Multiple notifications arriving rapidly are processed sequentially. A new notification waits for the current push-and-insert operation to finish instead of starting a second push animation on top of the first.
Typical uses include:
Create an Empty UI GameObject inside your Canvas.
For example:
LootFeedContainer
Add:
NotificationFeedView
to the container.
Do not add a VerticalLayoutGroup.
The feed positions its notification items manually, which avoids Unity layout rebuilds during every notification insertion.
Set the container to use a top-left coordinate system:
Anchor Min: X = 0, Y = 1
Anchor Max: X = 0, Y = 1
Pivot: X = 0, Y = 1
For example:
Position:
X = 0
Y = 0
Z = 0
Width:
400
Height:
600
The container height should be large enough to contain the maximum number of visible notifications.
Create a TextMeshPro - Text (UI) child under the feed container.
Configure it with:
AdvancedTextAnimator
Do not add a Content Size Fitter.
The feed uses a fixed notification height, so the notification does not need Unity’s preferred-size layout system.
For the notification template, use:
Anchor Min: X = 0, Y = 1
Anchor Max: X = 0, Y = 1
Pivot: X = 0, Y = 1
Recommended initial size:
Width: 400
Height: 100
This top-left pivot is important because the feed calculates positions from the notification’s top-left corner.
With a top-left container and top-left notification:
First notification:
Pos X = 0
Pos Y = 0
Second notification:
Pos X = 0
Pos Y = -100
Third notification:
Pos X = 0
Pos Y = -200
When using spacing, the vertical position becomes:
Y = -index * (itemHeight + itemSpacing)
The template is only used as the visual definition for newly created notifications.
It should not appear on screen at runtime.
NotificationFeedView automatically disables the template during Awake(), so no additional setup is required.
NotificationFeedViewSet the fields approximately as follows:
Listening Channel:
Loot
Template Text:
Your notification TextMeshPro object
Item Height:
100
Item Spacing:
8
Push Duration:
0.18
Recommended push duration ranges:
0.14 - 0.18 = fast / snappy
0.18 - 0.25 = normal
0.25 - 0.35 = softer
Use an Ease Out style Push Curve so the notifications move quickly at first and settle smoothly into their new positions.
Style the template exactly as you want every notification to look:
The template can use any supported Advanced Text Animator intro, continuous, and outro effects.
For example:
<slidein>+50 Coins
or:
<pop><color=red>Inventory Full!</color>
Drag the notification TextMeshPro object into the:
Template Text
field of NotificationFeedView.
No separate project prefab is required.
Each visible notification receives a NotificationFeedItem component automatically.
Its lifecycle is:
Create notification
↓
Keep it inactive during the push animation
↓
Push existing notifications downward
↓
Activate the new notification
↓
Set its text
↓
Start its lifetime
↓
Play intro animation
↓
Wait for display duration
↓
Trigger outro animation
↓
Wait for OnTextDisappeared
↓
Destroy notification
The lifetime timer begins when the notification is actually shown, not while it is waiting behind the current push animation.
This prevents rapid notification bursts from consuming display time while the notification is still waiting to enter the feed.
When ShowFeed is called:
The feed intentionally does not use:
SetAsFirstSibling();
to control visual ordering.
Visual ordering is controlled by RectTransform.anchoredPosition, which makes the animation deterministic and independent of Unity’s layout system.
VerticalLayoutGroupThe previous feed implementation relied on Unity’s VerticalLayoutGroup to reposition existing notifications.
The current implementation does not use it because the feed items have a known fixed height.
Instead, positions are calculated directly:
private Vector2 GetPositionForIndex(int index)
{
return new Vector2(
0f,
-index * (itemHeight + itemSpacing)
);
}
This avoids:
ContentSizeFitter recalculationRectTransform animationThe result is a simpler and cheaper feed implementation.
From any gameplay system—inventory, health, quest manager, combat system, and so on—call the event bus:
// Sends "+50 Coins" to the Loot feed.
NotificationEventBus.ShowFeed(
NotificationChannel.Loot,
"<slidein>+50 Coins",
3f,
"slideout"
);
// Sends a warning to the Warning feed.
NotificationEventBus.ShowFeed(
NotificationChannel.Warning,
"<pop><color=red>Inventory Full!</color>",
2f,
"popout"
);
The gameplay system does not need to know where the notification is displayed.
Because the notification architecture is heavily decoupled, new notification types and listeners can be added without changing existing gameplay systems.
Suppose you want an Achievements feed at the bottom of the screen.
Open:
NotificationChannel.cs
Add:
Achievement
to the enum.
Duplicate your existing:
LootFeedContainer
Move it to the bottom of the screen.
Change its NotificationFeedView channel to:
Achievement
NotificationEventBus.ShowFeed(
NotificationChannel.Achievement,
"Level Up!"
);
The gameplay code still does not need a reference to the UI container.
The Event Bus can also drive systems other than text.
For example, you could play a global sound whenever a Warning notification is displayed.
public class AudioNotificationListener : MonoBehaviour
{
private void OnEnable()
{
var bus = NotificationEventBus.Instance;
if (bus != null)
{
bus.OnShowFeedMessage += PlaySound;
}
}
private void OnDisable()
{
var bus = NotificationEventBus.Instance;
if (bus != null)
{
bus.OnShowFeedMessage -= PlaySound;
}
}
private void PlaySound(
NotificationChannel channel,
string msg,
float dur,
string outro
)
{
if (channel == NotificationChannel.Warning)
{
// Play an error buzzer sound!
AudioSource.PlayClipAtPoint(
errorSound,
Camera.main.transform.position
);
}
}
}
This means the same notification event can simultaneously drive:
without requiring those systems to know about one another.
Custom notification behavior can use the same IAnimationEffect system as the built-in text effects.
For example, suppose you create a new:
MeltOutEffect
Implement IAnimationEffect, register it in:
TagRegistry.cs
using:
Register("meltout", () => new MeltOutEffect());
You can then trigger it dynamically:
NotificationEventBus.HideMessage(
0.5f,
"meltout"
);
The notification system will retrieve the registered effect and apply it automatically.
This allows new text behaviors to be added without modifying the notification system itself.
You can build custom math-based effects by implementing the IAnimationEffect interface.
Create a new C# script and manipulate the CharacterTransformationData struct.
using AdvancedTextAnimator.Animation;
using System.Collections.Generic;
using UnityEngine;
public class HoverEffect : IAnimationEffect
{
public float Amount = 5f;
public void ApplyEffect(
ref CharacterTransformationData data,
float time
)
{
// Move the character upward
data.PositionOffset.y += Amount;
}
public void SetParameters(
IReadOnlyDictionary<string, string> parameters
)
{
if (
parameters.TryGetValue("amount", out string amtStr)
&& float.TryParse(amtStr, out float parsed)
)
{
Amount = parsed;
}
}
}
<hover amount=10>
Floating text!
</hover>
If you want an effect to hide text after it finishes, set:
data.IsVisible = false;
Example:
public class SpinOutEffect : IAnimationEffect
{
public float Delay = 1f;
public float Duration = 0.5f;
public void ApplyEffect(
ref CharacterTransformationData data,
float time
)
{
if (data.TimeSinceVisible < Delay)
return;
float elapsed = data.TimeSinceVisible - Delay;
if (elapsed >= Duration)
{
data.IsVisible = false;
return;
}
float t = elapsed / Duration;
data.Rotation *= Quaternion.Euler(0, 0, t * 360f);
data.Scale *= (1f - t);
}
public void SetParameters(
IReadOnlyDictionary<string, string> parameters
)
{
}
}
Open:
Runtime/Parsing/TagRegistry.cs
Inside the static constructor, register your custom effect:
static TagRegistry()
{
Register("wave", () => new WaveEffect());
// ... built-in effects ...
Register("hover", () => new HoverEffect());
Register("spinout", () => new SpinOutEffect());
}
The current API remains the same: register a tag name with a factory that returns an IAnimationEffect instance. This is still the correct pattern for custom effects.
You can now use the tags anywhere Advanced Text Animator is supported:
<hover amount=10>Hello World</hover>
<spinout>
Goodbye!
</spinout>
The main systems can be thought of as four layers:
Gameplay
│
▼
NotificationEventBus
│
├── Crosshair ──► Contextual UI
│
├── Loot ───────► Loot Feed
│
├── Quest ──────► Quest Feed
│
└── Warning ────► Warning UI
│
▼
AdvancedTextAnimator
│
▼
TagRegistry / Effects
This separation allows gameplay code to send a notification without knowing:
The result is a modular text and notification pipeline that can be expanded without tightly coupling gameplay and UI systems.