Pardon our dust while we move our docs here. The Godot (GDScript) docs are on this site, and the Yarn language, Unity, Godot (C#) and Unreal docs are still at docs.yarnspinner.dev.

This sample uses markup to change a line’s text before the Line Presenter shows it. Some of it is a palette and a 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 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.
Three capsule characters in a walled arena: an orange one with a face on the left, a green one in the middle, and a purple one on the right. The dialogue box shows Liz saying sure, it deals additional zap damage, with zap damage in bold orange inside square brackets, after a small lightning bolt icon.
Liz's line about the sword, with the lightning icon.

Running it

Open samples/replacement_markup/replacement_markup.tscn and press Run Current Scene.

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

How it works

All the markup is set up by the ReplacementMarkupSetup node. Its settings are in the Inspector:

PropertyWhat it’s for
Dialogue RunnerThe Dialogue Runner whose Line Presenters show the markup.
Demo Tags PaletteA YarnMarkupPalette resource with Anika’s formatting tags.
StylesThe styles for [style], with the BBCode that goes before and after the text.
Name ColoursThe colour for each person [name] can mark.
Buff Colour and Debuff ColourThe colours for the sprite tags.
Icon SizeHow big the sprite icons are, in pixels.
Matched ObscurityWhether [obscurity] always swaps the same letter for the same symbol.
The ReplacementMarkupSetup node’s properties.

When the scene starts, its script makes a processor for each kind of markup from those settings, and registers them on each Line Presenter:

func _ready() -> void:
    if dialogue_runner == null:
        push_error("replacement markup setup: no dialogue runner set")
        return

    var palette_processor := YarnPaletteMarkerProcessor.new(demo_tags_palette)

    var style_processor := YarnStyleMarkerProcessor.new()
    style_processor.styles = styles

    var obscurity := ObscurityMarkupProcessor.new()
    obscurity.matched_replacement = matched_obscurity

    var name_processor := NameMarkupProcessor.new()
    name_processor.entities = name_colours

    var sprite := SpriteMarkupProcessor.new()
    sprite.buff = buff_colour
    sprite.debuff = debuff_colour
    sprite.icon_size = icon_size

    for presenter in dialogue_runner.get_presenters():
        if presenter is YarnLinePresenter:
            palette_processor.register_with_line_provider(presenter)
            presenter.register_marker_processor("style", style_processor)
            presenter.register_marker_processor("obscurity", obscurity)
            presenter.register_marker_processor("name", name_processor)
            for marker in ["lightning", "ice", "heart", "fire"]:
                presenter.register_marker_processor(marker, sprite)
Lines 6–20 Make the processors, and give each one its settings from the Inspector.
Lines 22–23 Only Line Presenters show markup, so skip the Options Presenter.
Line 24 Register the palette for every tag in it.
Lines 25–29 Register the other processors, each for the tags it handles. One sprite processor handles all four sprite tags.
_ready in replacement_markup_setup.gd.

Marker processors aren’t resources, so they can’t be set up in the Inspector. The script makes them in code, using the settings on the node.

Alice, Liz and Bob each end with a [link] to the script for their markup, and Anika links to this page. The LinkOpener node under the Line Presenter opens them when they’re clicked.

Anika: a palette and a style

Anika’s lines use tags from the 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 palette is the node’s Demo Tags Palette. 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.
KeyValueWhat it means
markerfancyThe tag’s name.
start[color=#ffff00][font_size=48][lb]The BBCode to put before the text. [lb] shows a [.
end[rb][/font_size][/color]The BBCode to put after the text. [rb] shows a ].
marker_offset1How many visible characters it adds before the text: the [.
total_visible_character_count2How many visible characters it adds in all: the [ and the ].
The fancy tag, in the Demo Tags Palette’s Custom Markers.

Then Anika uses the h2 style:

Anika: If you have a defined [style=h2]style[/style] in a style marker processor we can pass it through to the text label.
The h2 style, in BuiltinReplacements.yarn.

The h2 style is in the node’s Styles. Its start is [font_size=36][b][color=#4080ff] and its end is [/color][/b][/font_size], so the text is bold, blue, and one and a half times the size of the rest of the line.

See Your own formatting tags and Your own styles for how 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.

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

func process_replacement_marker(
    marker: YarnMarkupAttribute,
    child_builder: Array,
    _child_attributes: Array,
    _locale_code: String
) -> ReplacementMarkerResult:
    var entity := marker.try_get_string_property("name")
    if entity.is_empty():
        entity = child_builder[0]

    var invisible := 0
    var key := entity.to_lower()
    if entities.has(key):
        var colour: Color = entities[key]
        var prefix := YarnMarkupParser.brackets_to_tags("[color=#%s][b]" % colour.to_html(false))
        var suffix := YarnMarkupParser.brackets_to_tags("[/b][/color]")
        child_builder[0] = "%s%s%s" % [prefix, child_builder[0], suffix]
        # The wrapped tags add no visible characters.
        invisible = prefix.length() + suffix.length()

    return ReplacementMarkerResult.new([], invisible)
Lines 7–9 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 12–13 Look the person up, ignoring capitals. Text for anyone who isn’t in Name Colours is left as it is.
Lines 15–17 Wrap the text in BBCode for that person’s colour and bold. BBCode you add has to go through brackets_to_tags, or its brackets are shown on screen as text.
Line 19 All the added characters are BBCode, so none of them are visible.
process_replacement_marker in name_markup_processor.gd.

The node’s Name Colours 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.

SpriteMarkupProcessor puts an icon from sprites/effects.png in front of the text, and wraps both in bold square brackets. Each tag uses a different part of the sheet. [ice] and [heart] use the Buff Colour, blue, as good effects, and [lightning] and [fire] use the Debuff Colour, orange, as harmful ones.

    var sprite: Dictionary = _sprites[marker_name]
    var colour := buff if sprite["buff"] else debuff
    var image := "[img width=%d height=%d region=%d,0,%d,%d]%s[/img]" % [
        icon_size, icon_size, sprite["column"] * SPRITE_CELL, SPRITE_CELL, SPRITE_CELL, SPRITE_SHEET
    ]

    # Bold brackets around everything, the effect colour on the icon and the
    # wrapped text.
    var prefix := YarnMarkupParser.brackets_to_tags("[b][lb][color=#%s]%s" % [colour.to_html(false), image])
    var suffix := YarnMarkupParser.brackets_to_tags("[/color][rb][/b]")

    child_builder[0] = "%s%s%s" % [prefix, child_builder[0], suffix]

    # The bracket and the icon are the only visible characters added at the
    # front, and the closing bracket at the end; the rest is bbcode.
    var visible_prefix := 2
    var visible_suffix := 1
    var invisible := (prefix.length() - visible_prefix) + (suffix.length() - visible_suffix)

    for i in range(child_attributes.size()):
        var attr: YarnMarkupAttribute = child_attributes[i]
        child_attributes[i] = attr.shift(visible_prefix)
Lines 1–2 Find the tag’s column in the sprite sheet, and whether it’s a good or a harmful effect.
Lines 3–5 An [img] tag that shows one square of the sheet, at the Icon Size.
Lines 9–10 The BBCode to wrap around the text, put through brackets_to_tags.
Lines 16–18 Everything added is hidden apart from the two brackets and the icon.
Lines 20–22 The icon and the [ 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.
Part of process_replacement_marker in sprite_markup_processor.gd.

Bob: text that gets clearer

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

title: Bob
---
/// tracks how much of Bob's speech we understand. The more we talk to him the more we learn of how he speaks
<<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="obscurity_markup_processor.gd"]Dynamic Replacement[/link] replacement processor to see how this was done.

===
Line 4 $obscurity starts at 0, so the first time, all of Bob’s line is hidden.
Line 6 The tag’s value is filled in from $obscurity each time the line runs.
Lines 8–10 A line group. Until $obscurity reaches 3, Alice apologises for Bob, and $obscurity goes up by one.
Lines 11–13 Once $obscurity is 3, the player can understand Bob. Clicking “Dynamic Replacement” opens obscurity_markup_processor.gd.
DynamicReplacement.yarn.

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

func process_replacement_marker(
	marker: YarnMarkupAttribute,
	child_builder: Array,
	_child_attributes: Array,
	_locale_code: String
) -> ReplacementMarkerResult:
	var obscurity_prop := marker.try_get_property("obscurity")
	if obscurity_prop == null:
		var diag := MarkupDiagnostic.new("Missing the obscurity property, we cannot continue without it.")
		return ReplacementMarkerResult.new([diag], 0)

	var level := marker.try_get_int_property("obscurity")

	# 0 hides everything, 1 hides roughly two thirds, 2 hides about a quarter,
	# anything else leaves the text untouched.
	match level:
		0:
			_obscure(child_builder, 1.0)
		1:
			_obscure(child_builder, 0.67)
		2:
			_obscure(child_builder, 0.25)

	return ReplacementMarkerResult.new([], 0)
Lines 7–10 A tag with no value can’t be processed, so report a problem and leave the text alone.
Line 12 The tag’s value as a whole number, from [obscurity = {$obscurity}].
Lines 16–22 Hide all of the text, about two thirds, or about a quarter, depending on the level.
Line 24 It only swaps characters for other characters, so it adds no hidden characters.
process_replacement_marker in obscurity_markup_processor.gd.

_obscure() picks letters at random, and swaps each one for a symbol, like ?, # or @. Spaces are never hidden, so the words keep their shape. Matched Obscurity is off, so each hidden letter gets a random symbol. With it on, the same letter always gets the same symbol.

See Writing your own text-changing markup for more about marker processors.

Things to try

  • Select the ReplacementMarkupSetup node, and in the Demo Tags Palette, 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.
  • In the node’s Styles, change #4080ff in the h2 style to another colour.
  • Add a colour for anika to Name Colours, 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 textures and fonts.