08 TextMode – Text Effects

12 min read

Animate the grid over time: typewriter and decode reveals, wipes and dissolves between screens, digital rain, and ambient pulses that keep a title screen alive.

Summary

A text screen that appears all at once reads as a static image. A text screen that types itself in, decodes out of noise, or wipes across from the left reads as a machine doing something. That difference is most of what a title screen, a boot sequence or a scene transition is selling, and the effects library exists so you do not have to write the timing code for it every time.

Effects fall into three families. Reveals animate content into place: the typewriter that types character by character, the decode that scrambles glyphs before settling, fades and slides. Transitions move a region from one screen to another in the order a mask decides, which is where wipes, dissolves, irises, blinds and clock sweeps all come from. Ambient effects decorate whatever is already there and keep going until stopped: a pulse, a flicker, a wave, a glitch, a field of twinkling stars.

Every effect animates a CellRect of a CellGrid, which means they compose with everything else in the module. An effect can reveal text your drawing code wrote, animate output the terminal produced, or run underneath a menu. They are driven by a TextModeEffectPlayer that every view owns, ticked automatically just before the grid uploads.

The library is deliberately data-first. Each effect is authorable as a preset asset so a designer can tune a look in the Inspector without touching code, and a sequencer chains effects with waits and callbacks so a multi-step intro is a fluent expression rather than a coroutine.

Use text effects for:

  • Title screens and intros that assemble themselves rather than appearing.
  • Transitions between screens: a map to an inventory, a menu to a game.
  • Boot sequences, decrypt animations, and anything meant to look like a machine working.
  • Ambient life on an otherwise static screen: a flickering CRT, a glitching display, a starfield.
  • Drawing attention to a region without moving it.

Features

  • Reveals. Typewriter with per-character attributes and an insertion cursor, decode with settling scramble, fades in and out, and slides from any edge.
  • Transitions. One mask-driven effect covers every ordered transition: wipes, dissolves, irises, box wipes, clock sweeps, blinds and checkerboards, in crisp or cross-faded form.
  • Set pieces. Digital rain and static tune-in, which carry more state than a mask can express.
  • Ambient. Pulse, color cycle, flicker, wave, glitch and twinkle, all of which restore the region when stopped.
  • Cell masks. Thirteen patterns decide the order cells are reached in, with direction, inversion, band count and a custom evaluator, so adding a pattern adds a transition everywhere masks are used.
  • Sequencer. Chain effects with waits and callbacks, and loop the result.
  • Presets. Every effect has a ScriptableObject preset with the shared timing fields plus its own, so looks are authorable and shareable.
  • Pooled buffers. The player rents and returns cell buffers and threshold arrays, so steady-state playback allocates nothing.
  • Reduced motion. A configuration switch the effects honor, so an accessibility preference works across the whole module rather than per call.
  • Concurrency cap. A configurable ceiling on simultaneous effects, refusing rather than queueing when reached.

The three families

Knowing which family you are in tells you what an effect will do to your screen when it finishes.

FamilyWhat it animatesOn completionExamples
RevealContent into place, either its own text or what is already on the gridLands on the final state and stopsTypewriter, Decode, FadeIn, FadeOut, SlideIn
TransitionA region from what is there now to a target screen you supplyLands exactly on the targetWipe, Dissolve, Iris, BoxWipe, ClockWipe, Blinds, CrossFade, CrispTransition, Push, DigitalRain, StaticTuneIn
AmbientWhatever is already on the grid, indefinitelyPuts the region back the way it found itPulse, ColorCycle, Flicker, Wave, Glitch, Twinkle

A transition needs to know where it is going. Supply that with a draw callback that runs against a scratch grid, another grid to copy from, or a cleared target, and the transition captures the current state as its starting point automatically.

Recipes

Type a screen in like an old terminal

Problem. Your intro should look like a machine printing to a teletype, character by character, with a cursor chasing the text.

Solution. The typewriter derives its duration from the text length and a characters-per-second rate, so a long paragraph takes proportionally longer than a short one without you computing anything.

/* Typewriter writes its own text, unlike the reveals that animate what is already there. */
var intro = TextEffects.Typewriter(region, bootText, ANSIColor.Green, ANSIColor.Black)
    .SetCharactersPerSecond(60f)
    .SetCursor('█')              /* A solid block chasing the text. */
    .SetKeepCursorOnCompletion(true); /* Leave it blinking when the text finishes. */

_view.Effects.Play(intro);

Decode a screen out of noise

Problem. A decryption sequence should look like the answer resolving out of scrambled characters, not like text fading in.

Solution. Decode scrambles every cell holding text and settles them in the order its mask decides.

/* Draw the target text first; Decode animates what is already on the grid. */
DrawSecretMessage();

/* The preset defaults order the settling as a noise field and wind the churn down as it goes,
   so late characters change more slowly and the screen reads as closing in on an answer. */
_view.Effects.Play(TextEffects.Decode(region).SetDuration(4f));

Wipe from one screen to another

Problem. Switching from the map to the inventory should sweep across rather than cut, and you do not want to hand-animate two screens.

Solution. A transition captures the current screen and takes a target. The mask decides the order.

/* The target is drawn on demand against a scratch grid, so the inventory never flashes on
   screen before the transition brings it in. */
_view.Effects.Play(
    TextEffects.Wipe(region, TextEffectDirection.LeftToRight)
        .To(console => DrawInventory(console))
        .SetDuration(0.6f));

Chain an intro sequence

Problem. Your boot sequence is three steps: type a header, pause, then dissolve into the menu. Coroutines for this get tangled quickly, especially when you want to loop or cancel.

Solution. The sequencer chains effects with waits and callbacks.

_view.Effects.Play(
    TextEffects.Sequence()
        .Then(TextEffects.Typewriter(headerRegion, "SCYLLA SYSTEMS", ANSIColor.BrightGreen, ANSIColor.Black))
        .Wait(0.8f)
        .Call(() => audioSource.PlayOneShot(_bootChime))
        .Then(TextEffects.Dissolve(fullRegion).To(DrawMainMenu))
        .Call(() => _menuInputEnabled = true));

Keep a title screen alive

Problem. Your title art is static and the screen feels dead, but you do not want anything that distracts from the menu.

Solution. An ambient effect decorates what is there and puts it back when stopped.

/* Twinkle places points of light in the empty cells only, so the logo and the menu text are
   left alone. Peak brightness is where the subtlety lives: points spend most of their life
   part way up the fade, so the field averages about half of whatever you set. */
_ambient = _view.Effects.Play(
    TextEffects.Twinkle(region)
        .SetDensity(0.05f)
        .SetLifetime(1f, 3f)
        .SetColor(TextModeThemes.Active.Foreground));

/* Stopping restores the region, so nothing needs redrawing afterwards. */
_view.Effects.Stop(_ambient);

Author a look in the Inspector

Problem. Your designer wants to tune the glitch on the corrupted-save screen without asking you to rebuild each time.

Solution. Every effect has a preset asset. Create one, tune it, and instantiate from it.

/* The preset carries the shared timing fields plus the effect's own. CreateEffect returns a
   fresh instance every call, so one asset can drive many simultaneous effects. */
[SerializeField] private GlitchEffectPreset _corruptionLook;

private void ShowCorruptedSave()
{
    _view.Effects.Play(_corruptionLook.CreateEffect());
}

Stop everything before a scene change

Problem. You are tearing down a screen and a running ambient effect is still animating cells you are about to reuse.

Solution. Stop the player’s effects, optionally landing them on their final state.

/* Stops every effect. Ambient effects restore their region, transitions land on their target. */
_view.Effects.StopAll();

/* Or pause and resume, for a game-pause that should not lose the animation's position. */
_view.Effects.PauseAll();
_view.Effects.ResumeAll();

API Sketch

TextEffects is the factory; the player lives on every view.

static class TextEffects {
    /* Reveals */
    TypewriterEffect Typewriter(...);   DecodeEffect Decode(...);
    FadeEffect FadeIn(...);             FadeEffect FadeOut(...);
    SlideInEffect SlideIn(...);

    /* Transitions: cross-faded unless noted */
    CrossFadeEffect Transition(...);    CrossFadeEffect CrossFade(...);
    CrossFadeEffect Wipe(...);          CrossFadeEffect Dissolve(...);
    CrossFadeEffect NoiseDissolve(...); CrossFadeEffect Iris(...);
    CrossFadeEffect BoxWipe(...);       CrossFadeEffect ClockWipe(...);
    CrossFadeEffect Blinds(...);
    MaskTransitionEffect CrispTransition(...);   /* hard swap */
    PushEffect Push(...);
    DigitalRainEffect DigitalRain(...); StaticTuneInEffect StaticTuneIn(...);

    /* Ambient */
    PulseEffect Pulse(...);             ColorCycleEffect ColorCycle(...);
    FlickerEffect Flicker(...);         WaveEffect Wave(...);
    GlitchEffect Glitch(...);           TwinkleEffect Twinkle(...);

    /* Composition */
    TextEffectSequencer Sequence();
}

class TextModeEffectPlayer {
    TextEffectSequencer Play(ITextEffect effect);
    bool Stop(ITextEffect effect); void StopAll();
    void PauseAll(); void ResumeAll();
    bool IsPlaying(ITextEffect effect); bool IsRegionBusy(in CellRect region);
    int ActiveCount, Capacity; bool HasActiveEffects;
    bool IsEnabled, ReduceMotion; float TimeScale;
    void NotifyGridChanged(CellGrid grid);
}

interface ITextEffect {
    string Name; TextEffectState State;
    bool IsRunning, IsComplete; float Progress;
    CellRect Region; TextEffectTimeMode TimeMode;
    void Start(); void Stop(); void Skip(); void Pause(); void Resume(); void Tick(float dt);
}

class TextEffectSequencer {
    TextEffectSequencer Then(ITextEffect effect);
    TextEffectSequencer Wait(float seconds);
    TextEffectSequencer Call(Action callback);
    TextEffectSequencer Loop(int count);
    void Stop();
}

See the API reference for full signatures, remarks, and exception contracts on every member.

Cell masks

A mask decides the order cells are reached in. One mask-driven effect plus thirteen patterns is where most of the transition library comes from.

PatternOrderTypical use
UniformEvery cell at onceA hard cut, or a fade with no stagger at all.
SequentialReading order, left to right and top to bottomA teletype feel without the typewriter’s per-character machinery.
LinearAlong a directionThe plain wipe. Direction picks which edge it starts from.
DiagonalAlong a diagonalA sweep that reads as more dynamic than a straight wipe.
BoxOutward from a rectangleA box wipe that grows or, inverted, closes.
RadialOutward from the centerThe iris. Inverted, it closes to a point.
ClockAngular sweepA radar or clock hand sweep.
BarnDoorFrom the center out to both edgesParting doors, or shutting ones when inverted.
BlindsBanded, several at onceVenetian blinds; the band count sets how many.
CheckerboardAlternating blocksA deliberately digital, blocky changeover.
DissolveRandom per cellThe classic dissolve.
NoiseA noise fieldBlotchy and spatially coherent, unlike the even scatter of a dissolve.
CustomWhatever your evaluator returnsAnything the built-ins do not cover; the result is normalized, so only relative ordering matters.

Alongside the pattern, a mask carries a direction, an inversion flag, a band count, and a per-cell duration that decides how spread out the stagger is. A cell duration of one puts every cell on the same schedule and changes the whole region as one.

Best Practices

  • Draw first, then animate, for reveals. Everything except the typewriter animates content already on the grid. Playing a decode over an empty region decodes nothing.
  • Use a draw callback for transition targets. It runs against a scratch grid, so the destination screen never flashes before the transition brings it in.
  • Let an effect own its region. Two effects on overlapping regions both write every frame and fight. IsRegionBusy tells you before you start one.
  • Prefer the sequencer to coroutines. It exists for exactly this: chaining, waiting and calling back, and it cancels as a unit.
  • Reach for presets when a designer is tuning. The look ends up in an asset rather than in a constant, and one asset can drive many instances.
  • Honor reduced motion through the configuration, not per call. One switch that the whole module reads beats a conditional at every call site.
  • Stop ambient effects before you redraw underneath them. They work from a copy taken when they started, so redrawing without telling them leaves your new content liable to be overwritten.
  • Pick CrispTransition when you want a clean edge. The named transitions all cross-fade, which is softer than people expect from a name like Wipe.
  • Keep the concurrency cap in mind. When the player is at capacity, further plays are refused rather than queued, which is a silent no-op if you are not checking.
  • Use TimeMode deliberately. An effect on scaled time freezes with the game; one on unscaled time keeps running through a pause, which suits a pause menu’s own animation.

Pitfalls