Replacement Markup

This sample uses markup to change a line’s text before the Line Presenter shows it. Some of it is a Markup Palette and a TextMeshPro style, which need no code of their own. The rest is three marker processors written for the sample: one hides part of a line, one colours people’s names, and one adds an icon from a sprite sheet.

When you run it, there are four characters in the arena, and each one shows something different:

CharacterYarn ScriptWhat it shows
AnikaBuiltinReplacements.yarnA Markup Palette with [b], [i], [u], [custom] and [fancy], and a [style=h2] style.
AliceNamedReplacement.yarn[name], which colours and bolds people’s names.
LizSpriteReplacer.yarn[lightning], [ice], [heart] and [fire], which add an icon and colour the text.
BobDynamicReplacement.yarn[obscurity], which hides some of Bob’s line, and less of it each time you talk to him.
The four characters in the sample.
Capsule characters in a walled arena with a chequered floor: an orange one with a face on the left, a green one and a purple one in the middle, and a red one at the edge of the screen. The dialogue box shows Liz saying well it is called a flamethrower so I think you can work that one out yourself, with a small red flame icon and the word flame in bold orange inside square brackets.
Liz’s line about the flamethrower, with the fire icon.

Running it

In the Project window, open the Replacement Markup sample’s folder, open the Replacement Markers scene, and press Play.

  • Move with W, A, S and D, or the arrow keys.
  • Walk up to a character and press Space to talk to them.
  • Press Space, or click the continue button, to continue.

How it works

The scene uses the Dialogue System prefab. The sample adds its own marker processors to the prefab’s Markup Processors GameObject, next to the two that are already there.

Replacement Markers
Dialogue System Dialogue RunnerIn Memory Variable Storage
Canvas CanvasCanvas ScalerGraphic Raycaster
Line Presenter Content Size FitterVertical Layout GroupCanvas GroupLine Presenter
Background ImageLayout Element
Character Name TextMeshPro - Text (UI)
Text TextMeshPro - Text (UI)
Button Container Line Presenter Button HandlerLayout Element
Continue Button ImageButton
Options Presenter Vertical Layout GroupContent Size FitterCanvas GroupOptions Presenter
Background ImageLayout Element
Last Line TextMeshPro - Text (UI)
Markup Processors Palette Marker ProcessorStyle Marker ProcessorObscurity Markup ProcessorEntity ColourerSprite Replacement Marker ProcessorLink Markup Handler
Line Advancer Line Advancer
EventSystem Event SystemEnsure Input Module Available
Alice Character ControllerSimple CharacterCharacter AppearanceMouth ViewDialogue Interactable
Bob Character ControllerSimple CharacterCharacter AppearanceMouth ViewDialogue Interactable
Liz Character ControllerSimple CharacterCharacter AppearanceMouth ViewDialogue Interactable
Anika Character ControllerSimple CharacterCharacter AppearanceMouth ViewDialogue Interactable
The dialogue part of the Replacement Markers scene.
ComponentWhat it’s for
Palette Marker ProcessorAnika’s formatting tags, from the Palette set to the sample’s Demo Tags Markup Palette.
Style Marker Processor[style], which passes the text to a TextMeshPro style.
Obscurity Markup Processor[obscurity], from the sample’s ObscurityMarkupProcessor.cs.
Entity Colourer[name], from the sample’s EntityColourer.cs. Its Entities list has a colour for each name.
Sprite Replacement Marker ProcessorThe four sprite tags, from the sample’s SpriteReplacementMarkerProcessor.cs. Its Buff and Debuff are the colours for good and harmful effects.
Link Markup Handler[link], from the samples’ shared scripts.
The components on Markup Processors.

Each marker processor is a ReplacementMarkupHandler, a component you set up in the Inspector. When the scene starts, each one finds the Dialogue Runner’s Line Provider and registers itself for the tags it handles. The Line Provider then calls it whenever a line has one of those tags, before the line goes to the Presenters.

Alice, Liz and Bob each end with a [link] to the script for their markup, and Anika links to the Demo Tags Markup Palette. The Link Markup Handler opens them in the Unity Editor when they’re clicked.

Anika: a palette and a style

Anika’s lines use tags from the Markup Palette:

Anika: or [b][i]wombo[u]combo[/i] them[/u] together[/b] even.
Player: that's awesome.
Anika: you can even define [custom]custom[/custom] markers for [fancy]fancy[/fancy] looks.
Some of Anika’s lines, in BuiltinReplacements.yarn.

The Markup Palette is Demo Tags. Its Basic Markers have a tag for each of b, i, u and s, which make the text bold, italic, underlined or struck through. It adds two tags of its own:

  • custom is a basic marker that makes the text green, bold and underlined.
  • fancy is in Custom Markers. It makes the text yellow and twice the size, and puts it in square brackets.
FieldValueWhat it means
MarkerfancyThe tag’s name.
Start<color=#ffff00><size=2em>[The TextMeshPro rich text to put before the text.
End]</size></color>The TextMeshPro rich text to put after the text.
Marker Offset1How many visible characters it adds before the text: the [.
The fancy tag, in the Demo Tags Markup Palette’s Custom Markers.

Then Anika uses the h2 style:

Anika: If you have a defined [style=h2]style[/style] in a stylesheet we can pass it through to TMP.
The h2 style, in BuiltinReplacements.yarn.

The Style Marker Processor wraps the text in a TextMeshPro <style="h2"> tag. H2 is one of the styles in TextMeshPro’s Default Style Sheet, which makes the text bold, blue, and one and a half times the size of the rest of the line.

See Markup for how Markup Palettes and styles work.

Alice: names

Alice’s lines wrap people’s names in [name]:

Alice: Hey there [name]Player[/name].
Player: Hey there [name]Alice[/name], what up?
Alice: I show off how you can use markup to flag important elements based on contents.
Player: oh?
Alice: yes, so for example I am just wondering what [name]Bob[/name] is up to.
Bob: Why are [name=alice]you[/name] wondering what [name=bob]I[/name] am up to?
Some of the lines in NamedReplacement.yarn.

EntityColourer works out who each tag is about, and colours them:

public override ReplacementMarkerResult ProcessReplacementMarker(MarkupAttribute marker, StringBuilder childBuilder, List<MarkupAttribute> childAttributes, string localeCode)
{
    string nameText = string.Empty;
    if (marker.TryGetProperty("name", out string? value))
    {
        nameText = value.ToLower();
    }

    if (nameText == string.Empty)
    {
        nameText = childBuilder.ToString().ToLower();
    }

    int invisibleCharactersAdded = 0;

    foreach (var entity in entities)
    {
        if (entity.name.ToLower() == nameText)
        {
            var prefix = $"<color=#{UnityEngine.ColorUtility.ToHtmlStringRGBA(entity.colour)}><b>";
            var suffix = "</b></color>";
            childBuilder.Insert(0, prefix);
            childBuilder.Append(suffix);
            invisibleCharactersAdded += prefix.Length + suffix.Length;
        }
    }

    return new ReplacementMarkerResult(invisibleCharactersAdded);
}

protected void Start()
{
    var lineProvider = (LineProviderBehaviour)GameObject.FindAnyObjectByType<DialogueRunner>().LineProvider;
    lineProvider.RegisterMarkerProcessor("name", this);
}
Lines 3–12 With [name=bob]I[/name], the person is the tag’s value, so the word “I” is coloured as Bob. With [name]Bob[/name], there’s no value, so it’s the text inside the tag.
Lines 16–18 Look the person up in Entities, ignoring capitals. Text for anyone who isn’t in the list is left as it is.
Lines 20–23 Wrap the text in TextMeshPro rich text for that person’s colour and bold.
Line 24 All the added characters are rich text tags, so none of them are visible.
Lines 31–35 Register for [name] with the Dialogue Runner’s Line Provider.
EntityColourer.cs.

Entities on the Entity Colourer has a colour for Player, Alice and Bob.

Liz: icons

Liz’s lines use one of four sprite tags, depending on which item the player asks about:

-> My Sword
    Liz: sure, it deals additional [lightning]zap damage[/lightning].
-> My Sunglasses
    Liz: sure, it makes you [ice]look cool[/ice].
-> My pet lizard
    Liz: sure, it provides [heart]companionship[/heart].
-> My flamethrower
    Liz: well it is called a [fire]flame[/fire]thrower so I think you can work that one out yourself.
Part of SpriteReplacer.yarn.

SpriteReplacementMarkerProcessor puts an icon from the effects TextMeshPro sprite asset in front of the text, and wraps both in bold square brackets. The sprite asset is in the sample’s Resources/Sprite Assets folder, which is where TextMeshPro looks for sprite assets by name. [ice] and [heart] use the Buff colour, blue, as good effects, and [lightning] and [fire] use the Debuff colour, orange, as harmful ones.

public override ReplacementMarkerResult ProcessReplacementMarker(MarkupAttribute marker, StringBuilder childBuilder, List<MarkupAttribute> childAttributes, string localeCode)
{
    var start = "<b>[<color=#{0}><sprite=\"effects\" name=\"{1}\">";
    var end = "</color>]</b>";

    string prefix;

    int invisibleCharactersAdded = 0;

    switch (marker.Name.ToLower())
    {
        case "lightning":
            prefix = string.Format(start, ColorUtility.ToHtmlStringRGB(debuff), "lightning");
            break;

        case "ice":
            prefix = string.Format(start, ColorUtility.ToHtmlStringRGB(buff), "water");
            break;

        case "heart":
            prefix = string.Format(start, ColorUtility.ToHtmlStringRGB(buff), "heart");
            break;

        case "fire":
            prefix = string.Format(start, ColorUtility.ToHtmlStringRGB(debuff), "fire");
            break;

        default:
            var diagnostic = new LineParser.MarkupDiagnostic($"was unable to find a matching sprite for {marker.Name}");
            return new ReplacementMarkerResult(new List<LineParser.MarkupDiagnostic>() { diagnostic }, 0);
    }

    childBuilder.Insert(0, prefix);
    childBuilder.Append(end);

    invisibleCharactersAdded += (prefix.Length - 1) + (end.Length - 1);

    for (int i = 0; i < childAttributes.Count; i++)
    {
        childAttributes[i] = childAttributes[i].Shift(2);
    }

    return new ReplacementMarkerResult(invisibleCharactersAdded);
}
Lines 3–4 The rich text to wrap around the text. {0} is the colour, and {1} is the name of the sprite in the sprite asset.
Lines 10–31 Pick the colour and the sprite for the tag. [ice] uses the sprite called water.
Line 36 Count how many of the added characters are hidden rich text, rather than visible characters like the brackets.
Lines 38–41 The [ and the icon are two visible characters added before the text, so any markup inside the tag moves along by two, to stay lined up with the right characters.
ProcessReplacementMarker() in SpriteReplacementMarkerProcessor.cs.

Bob: text that gets clearer

Bob’s line is wrapped in [obscurity], with the value of $obscurity:

title: Bob
---
<<declare $obscurity = 0>>

Bob: [obscurity = {$obscurity}]Why hello there, it's nice to meet you friend.[/obscurity]

=> Alice: yeah sorry about Bob, he's hard to understand. <<if $obscurity < 3>>
    Alice: stick with it, you'll get it though.
    <<set $obscurity += 1>>
=> Player: Hey Bob, nice to meet you <<if $obscurity > 2>>
    Alice: See, practice makes perfect!
    Bob: Check out the [link="b79d53f5035d040a1a5118b71586ab5d"]Dynamic Replacement[/link] replacement processor to see how this was done.

===
Line 3 $obscurity starts at 0, so the first time, all of Bob’s line is hidden.
Line 5 The tag’s value is filled in from $obscurity each time the line runs.
Lines 7–9 A line group. Until $obscurity reaches 3, Alice apologises for Bob, and $obscurity goes up by one.
Lines 10–12 Once $obscurity is 3, the player can understand Bob. The link’s value is the GUID of ObscurityMarkupProcessor.cs, so clicking “Dynamic Replacement” opens it.
DynamicReplacement.yarn.

ObscurityMarkupProcessor reads the tag’s value, and hides part of the text inside it:

public override ReplacementMarkerResult ProcessReplacementMarker(MarkupAttribute marker, StringBuilder childBuilder, List<MarkupAttribute> childAttributes, string localeCode)
{
    if (!marker.TryGetProperty("obscurity", out int value))
    {
        var diagnostic = new LineParser.MarkupDiagnostic("Missing the obscurity property, we cannot continue without it.");
        return new ReplacementMarkerResult(new List<LineParser.MarkupDiagnostic>() { diagnostic }, 0);
    }

    switch (value)
    {
        case 0:
            {
                Obscure(childBuilder, 1);
                break;
            }
        case 1:
            {
                Obscure(childBuilder, 0.67f);

                break;
            }
        case 2:
            {
                Obscure(childBuilder, 0.25f);
                break;
            }
    }

    return new ReplacementMarkerResult(0);
}
Lines 3–7 A tag with no value can’t be processed, so report a problem and leave the text alone.
Lines 9–27 Hide all of the text, about two thirds, or about a quarter, depending on the level. Any other level leaves the text as it is.
Line 29 It only swaps characters for other characters, so it adds no hidden characters.
ProcessReplacementMarker() in ObscurityMarkupProcessor.cs.

Obscure() picks characters at random, and swaps each one for a symbol, like ?, # or @. Spaces are never hidden, so the words keep their shape. Matched Replacement is off in the scene, so each hidden character gets a random symbol. With it on, the same character always gets the same symbol.

See Markup for more about marker processors.

Things to try

  • Open the Demo Tags Markup Palette, and change the colour of the custom tag. Then talk to Anika again.
  • In DynamicReplacement.yarn, change <<declare $obscurity = 0>> to 1. The first time you talk to Bob, only some of his line is hidden.
  • Select Markup Processors and turn on Matched Replacement on the Obscurity Markup Processor.
  • Add an entry for Anika to Entities on the Entity Colourer, and use [name]Anika[/name] in one of Alice’s lines.
Next step Themed Line Presenter Restyling the standard Line Presenter and Options Presenter with sprites and fonts.