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
ScriptableObjectpreset 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.
| Family | What it animates | On completion | Examples |
|---|---|---|---|
| Reveal | Content into place, either its own text or what is already on the grid | Lands on the final state and stops | Typewriter, Decode, FadeIn, FadeOut, SlideIn |
| Transition | A region from what is there now to a target screen you supply | Lands exactly on the target | Wipe, Dissolve, Iris, BoxWipe, ClockWipe, Blinds, CrossFade, CrispTransition, Push, DigitalRain, StaticTuneIn |
| Ambient | Whatever is already on the grid, indefinitely | Puts the region back the way it found it | Pulse, 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.
| Pattern | Order | Typical use |
|---|---|---|
Uniform | Every cell at once | A hard cut, or a fade with no stagger at all. |
Sequential | Reading order, left to right and top to bottom | A teletype feel without the typewriter’s per-character machinery. |
Linear | Along a direction | The plain wipe. Direction picks which edge it starts from. |
Diagonal | Along a diagonal | A sweep that reads as more dynamic than a straight wipe. |
Box | Outward from a rectangle | A box wipe that grows or, inverted, closes. |
Radial | Outward from the center | The iris. Inverted, it closes to a point. |
Clock | Angular sweep | A radar or clock hand sweep. |
BarnDoor | From the center out to both edges | Parting doors, or shutting ones when inverted. |
Blinds | Banded, several at once | Venetian blinds; the band count sets how many. |
Checkerboard | Alternating blocks | A deliberately digital, blocky changeover. |
Dissolve | Random per cell | The classic dissolve. |
Noise | A noise field | Blotchy and spatially coherent, unlike the even scatter of a dissolve. |
Custom | Whatever your evaluator returns | Anything 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.
IsRegionBusytells 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
CrispTransitionwhen you want a clean edge. The named transitions all cross-fade, which is softer than people expect from a name likeWipe. - 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
TimeModedeliberately. 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
Related
- 01 Scylla TextMode
- 04 TextMode – Drawing
- 09 TextMode – Color Themes
- 12 TextMode – Configuration
- API reference:
Scylla.TextMode.TextEffects - API reference:
Scylla.TextMode.TextModeEffectPlayer - API reference:
Scylla.TextMode.ITextEffect - API reference:
Scylla.TextMode.TextEffectSequencer - API reference:
Scylla.TextMode.CellMask