Advanced-Text-Animator

Advanced Text Animator

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


Table of Contents


Quick Start

  1. Add a TextMeshPro - Text (UI) component to your Canvas.
  2. Attach the AdvancedTextAnimator component to the same GameObject.
  3. Type animation tags directly into the TextMeshPro text input field.
  4. Enable Live Preview in the inspector to preview animations without entering Play Mode.

Basic Example

<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>

Tag Cheat Sheet

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>

1. Continuous Effects

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

2. Intro Effects (Appearing)

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

3. Outro Effects (Disappearing)

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

Triggering Outro Effects via Script

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");

Typewriter Commands

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!

Useful TextMeshPro Tags & Tricks

The parser runs before TextMeshPro, so native TMP tags can be mixed freely inside Advanced Text Animator tags.

Adding Outlines

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.

Method 1: Outline the Entire Text Box

  1. Select your TextMeshPro object.
  2. Scroll to the Material section in the Inspector.
  3. Expand the Outline property block.
  4. Enable the outline checkbox.
  5. Adjust:
    • Color
    • Thickness

This applies an outline to the entire text component.

Method 2: Outline Specific Words Dynamically

To outline only certain words:

  1. Duplicate your main Font Asset in the Project window.

Example:

MyFont Outline
  1. Open the duplicated font asset.
  2. Enable Outline in the material settings.
  3. Adjust the thickness and styling.

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.


Text Styling

Color

<color=#FF0000>Red Text</color>

Sets the base color.

Size

<size=150%>Huge</size>

Scales the text.

Basic Formatting

<b>Bold</b>
<i>Italics</i>
<u>Underline</u>

Basic text formatting.


Highlighting & Sprites

Highlighting

<mark=#FFFF0080>Highlighted</mark>

Adds a background box.

Inline Sprites

<sprite=0>

or:

<sprite name="Coin">

Inserts an inline icon from your Sprite Asset.


Test Examples

Copy and paste these directly into your TextMeshPro text box to test the system.

The Classic

Hello <wave>World</wave>!

Loot Popup — Intro + Continuous

You found a <pop><float>Secret Item!</float></pop>

Intense Warning

Warning: <wiggle speed=15 amp=4><color=red>Core Meltdown</color></wiggle>

Dramatic Entrance

Incoming... <pause=1> <dropin height=200>BOOM</dropin>

Beautiful Magic Text

<gradient c1=#00E5FF c2=#0044FF speed=4 freq=0.5>
Mana fully restored.
</gradient>

Animated Text Pooler (Floating Text)

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.

Step 1: Create the Text Prefab

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:

Set the RectTransform Scale to:

(1, 1, 1)

Add a TextMeshPro - Text (UI) component directly to this Canvas object.

Set:

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.


Step 2: Setup the Pooler Manager

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.

Adjust Settings

Initial Pool Size

How many instances to generate on Awake to prevent lag spikes.

Default Lifetime

How long text stays alive before returning to the pool.

Example:

3 seconds

Face Camera

Keep this checked so the text automatically rotates to face the player.


Step 3: How to Call It in Your Code

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.

Harvesting Example

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);

Damage Number Example

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)
);

Custom Lifetime Example

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
);

Notification System Architecture

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:

  1. Contextual / Crosshair Notifications — text that stays on screen while the player focuses on an object and fades out when they look away.
  2. Stacking Feed Notifications — toast-style popups such as +50 Coins that appear in a screen corner, stack smoothly, and automatically disappear after a set duration.

1. The Core: Notification Event Bus & Channels

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")
}

Why Channels Matter

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.


2. Contextual (Crosshair) Notifications

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.

How to Use It in Code

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"
);

What Makes It Special? — The DisappearText() Upgrade

Previously, 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.


3. The Stacking Feed System

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:

Setup Instructions

Step 1: Create the Feed Container

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.

Step 2: Configure the Feed Container RectTransform

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.

Step 3: Create the Notification Template

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.

Step 4: Configure the Notification RectTransform

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)

Step 5: Disable the Template

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.

Step 6: Configure NotificationFeedView

Set 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.

Step 7: Configure the Notification Text

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.


Notification Lifetime

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.


How the Feed Insertion Animation Works

When ShowFeed is called:

  1. The notification request is placed into the feed’s internal queue.
  2. The feed captures the current positions of all visible notifications.
  3. Existing notifications are assigned their new positions one slot lower.
  4. Every existing notification animates downward simultaneously.
  5. Only after that animation completes is the new notification created and placed at the top.
  6. The new notification is activated, its text is assigned, and its intro animation is played.

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.


Why There Is No VerticalLayoutGroup

The 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:

The result is a simpler and cheaper feed implementation.


How to Use It in Code

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.


4. Extending the Notification System

Because the notification architecture is heavily decoupled, new notification types and listeners can be added without changing existing gameplay systems.

A. Adding New Screen Regions

Suppose you want an Achievements feed at the bottom of the screen.

1. Add a new channel

Open:

NotificationChannel.cs

Add:

Achievement

to the enum.

2. Create a new feed container

Duplicate your existing:

LootFeedContainer

Move it to the bottom of the screen.

Change its NotificationFeedView channel to:

Achievement

3. Send notifications

NotificationEventBus.ShowFeed(
    NotificationChannel.Achievement,
    "Level Up!"
);

The gameplay code still does not need a reference to the UI container.


B. Adding Custom UI Logic to Events

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.


C. Adding Custom Outro Animations

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.


Creating Custom Effects

You can build custom math-based effects by implementing the IAnimationEffect interface.

1. Create the Effect Class

Create a new C# script and manipulate the CharacterTransformationData struct.

Example A: A Continuous Effect

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;
        }
    }
}

Usage

<hover amount=10>
Floating text!
</hover>

Example B: An “Outro” Effect

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
    )
    {
    }
}

Registering Custom Tags

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>

Architecture at a Glance

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.