Markup
Markup is tags in square brackets that you put in your lines, like [b]this[/b]. Yarn Spinner for Unity uses them to format the text, change the words, or run your own code as the line types out.
Markup comes in two kinds:
- Markup that changes the text. It’s applied before the line is shown. Formatting like
[b], and tags that pick words like[plural], work this way. - Markup that does something while the line types out. It marks a point in the line, and runs when the typewriter reaches it.
[pause/]works this way.
Where formatting comes from
On its own, a Line Presenter doesn’t format any markup. It shows the line as plain text, with the tags removed. Formatting comes from markup processors, which turn tags like [b] into TextMesh Pro rich text tags like <b>.
Markup processors work inside the Dialogue Runner’s Line Provider, before the line reaches any Presenter. That means the formatting shows up everywhere the line’s text is shown: in the Line Presenter, in the Options Presenter’s buttons, and in any Presenter you build yourself.
The Dialogue System prefab comes with two markup processors, on its Markup Processors GameObject:
- The Palette Marker Processor applies a Markup Palette, a list of tag names and the formatting each one applies. In the prefab, its Palette is Common Tags, a palette that comes with Yarn Spinner.
- The Style Marker Processor handles
[style].
Both have a Line Provider setting. Leave it empty, and they use the Line Provider of the Dialogue Runner they’re under, or the first Dialogue Runner in the scene.
If you set up your own Dialogue Runner without the prefab, it formats nothing until you add these components to a GameObject in your scene.
Formatting text
With the Dialogue System prefab, these tags are formatted for you, with no extra code:
| Markup | What it does |
|---|---|
[b]text[/b] | Bold. |
[i]text[/i] | Italic. |
[u]text[/u] | Underline. |
[s]text[/s] | Strikethrough. |
[br/] | Starts a new line. |
[style=title]text[/style] | Applies a style from your TextMesh Pro style sheet. See Your own styles. |
Rosa: The lamp is [b]never[/b] supposed to go out.There’s no tag for colour in Common Tags. To colour text, add a tag of your own to a palette. See Your own formatting tags.
If a line has markup that no markup processor handles, like [wave]text[/wave], it’s shown as the text without the tags. The markup is still part of the line, so your own code can use it. See Running code partway through a line.
To show square brackets in a line, put a backslash before each one: \[like this\]. To show a whole run of text without treating any of it as markup, wrap it in [nomarkup]:
Rosa: The label says [nomarkup][DO NOT TOUCH][/nomarkup].Changing the words
These tags are replaced with different words, depending on a value. They’re self-closing: they end in /] and have no closing tag. They work with or without any markup processors.
[select] picks one of several options by name:
<<declare $mood = "happy">>
Rosa: I'm [select value={$mood} happy="glad" sad="sorry"/] you came.[plural] picks the right word for a number. % is replaced with the number:
<<declare $coins = 3>>
Rosa: You have [plural value={$coins} one="% coin" other="% coins"/].[ordinal] does the same for positions, like 1st, 2nd and 3rd:
<<declare $visits = 2>>
Rosa: It's your [ordinal value={$visits} one="%st" two="%nd" few="%rd" other="%th"/] visit.[plural] and [ordinal] follow the rules of the line’s language, so you only need to give the forms that language uses. English uses one and other for plurals, and one, two, few and other for ordinals.
Pausing partway through a line
[pause/] stops the typewriter for a moment. Every Line Presenter handles it, with no setup:
Rosa: The lamp went out, and then [pause/]I heard footsteps.
Rosa: Somebody was on the stairs. [pause=500/]Somebody heavy.On its own, [pause/] waits for 1 second. Give it a whole number to wait that many milliseconds, like [pause=500/], or a decimal to wait that many seconds, like [pause=0.5/]. If the player hurries the line during a pause, the pause ends.
A self-closing tag removes the space that comes after it. Put the space before the tag instead: then [pause/]I heard, not then[pause/] I heard.
See Typewriter and Effects for more on how text appears.
Your own formatting tags
To add your own tags, like [happy], that apply formatting you choose, make a Markup Palette. A Markup Palette is an asset with a list of tag names, each with the formatting it applies.
Common Tags is part of the Yarn Spinner package, so make your own palette rather than changing it:
- In the Project window, choose .
- Select the new palette, and add your tags to its Basic Markers and Custom Markers lists. See below.
- Select the Markup Processors GameObject in your Dialogue System, click Add Component, and add another Palette Marker Processor.
- Set the new Palette Marker Processor’s Palette to your palette.
A basic marker turns on the formatting you tick:
| Setting | What it does |
|---|---|
| Marker | The tag name, like happy for [happy]. |
| Custom Color | Whether to change the text’s colour. |
| Color | The colour to use, when Custom Color is on. |
| Boldened | Makes the text bold. |
| Italicised | Makes the text italic. |
| Underlined | Underlines the text. |
| Strikedthrough | Strikes through the text. |
A custom marker puts TextMesh Pro rich text of your own before and after the text. Use one for anything a basic marker can’t do, like changing the size:
| Setting | What it does |
|---|---|
| Marker | The tag name, like loud for [loud]. |
| Start | The rich text to put before the text, like <size=150%>. |
| End | The rich text to put after the text, like </size>. Close the tags in the opposite order to how you opened them. |
| Marker Offset | How many visible characters Start adds. Leave it at 0 unless Start adds characters the player can see. |
| Total Visible Character Count | How many visible characters Start and End add between them. Leave it at 0 unless they add characters the player can see. |
Rich text tags like <size> aren’t visible, so most custom markers leave both numbers at 0. If Start or End adds something the player sees, like a [ or a sprite, set the numbers so that pauses and other markup inside the tag still happen at the right place.
Rosa: I'm [happy]so pleased[/happy] you're here!
Rosa: [loud]HELLO?[/loud]Each tag name can only be handled by one markup processor. If your palette has a tag that Common Tags already has, like b, the Palette Marker Processor that starts second logs an error, and some of its tags don’t work. Give your tags names of their own.
A Palette Marker Processor also handles none of its tags if its palette has no basic markers. Give every palette at least one basic marker.
Your own styles
The Style Marker Processor turns [style=name] into TextMesh Pro’s <style="name"> tag. To add a style, add it to your TextMesh Pro style sheet, with the rich text to put before and after the text. Then use its name in [style]:
Rosa: Have you read [style=title]The Keeper's Log[/style]?The Style Marker Processor can’t tell whether a style adds characters the player can see. If one does, pauses and other markup after it in the line happen in the wrong place. For a style like that, use a custom marker in a Markup Palette instead, and set its Marker Offset and Total Visible Character Count.
Writing your own text-changing markup
If a palette can’t do what you need, write a marker processor. It gets the text inside the tag, and can change it however you like. This one turns [whisper]text[/whisper] into grey italics:
using System.Collections.Generic;
using System.Text;
using UnityEngine;
using Yarn.Markup;
using Yarn.Unity;
public class WhisperMarkerProcessor : ReplacementMarkupHandler
{
[SerializeField] DialogueRunner dialogueRunner;
const string StartTag = "<color=#808080><i>";
const string EndTag = "</i></color>";
void Start()
{
dialogueRunner.LineProvider.RegisterMarkerProcessor("whisper", this);
}
public override ReplacementMarkerResult ProcessReplacementMarker(
MarkupAttribute marker,
StringBuilder childBuilder,
List<MarkupAttribute> childAttributes,
string localeCode)
{
childBuilder.Insert(0, StartTag);
childBuilder.Append(EndTag);
return new ReplacementMarkerResult(StartTag.Length + EndTag.Length);
}
}ReplacementMarkupHandler. It’s a MonoBehaviour, so the processor is a component you add to a GameObject.whisper tag.ProcessReplacementMarker is called for each [whisper] in a line, before the line reaches any Presenter. childBuilder holds the text inside the tag.childBuilder.To use it, add it to a GameObject in your scene, such as the Markup Processors GameObject, and set its Dialogue Runner.
Rosa: [whisper]Don't tell the others.[/whisper]To handle more than one tag with the same processor, call RegisterMarkerProcessor once for each tag name. marker is the tag itself: its name is in marker.Name, and any values you gave it, such as [whisper volume=2], are in marker.Properties. To read one, use marker.TryGetProperty("volume", out int volume).
If you add characters the player can see at the start of the text, the markup inside the tag needs to move along by the same amount. Replace each item in childAttributes with childAttributes[i].Shift(count), where count is how many visible characters you added.
Running code partway through a line
Markup can trigger your own code when the typewriter reaches a certain point in a line, like changing a character’s expression, playing a sound, or shaking the screen. To do this, write an event handler and add it to the Line Presenter’s Event Handlers list.
This one shakes the dialogue box wherever a line has [shake_box/]:
using System.Collections.Generic;
using System.Threading;
using TMPro;
using UnityEngine;
using Yarn.Markup;
using Yarn.Unity;
public class ShakeBoxHandler : ActionMarkupHandler
{
[SerializeField] RectTransform panel;
[SerializeField] float strength = 12f;
[SerializeField] float duration = 0.4f;
readonly List<int> positions = new();
public override void OnPrepareForLine(MarkupParseResult line, TMP_Text text)
{
positions.Clear();
foreach (var attribute in line.Attributes)
{
if (attribute.Name == "shake_box")
{
positions.Add(attribute.Position);
}
}
}
public override async YarnTask OnCharacterWillAppear(
int currentCharacterIndex,
MarkupParseResult line,
CancellationToken cancellationToken)
{
if (!positions.Contains(currentCharacterIndex))
{
return;
}
var start = panel.anchoredPosition;
var elapsed = 0f;
while (elapsed < duration && !cancellationToken.IsCancellationRequested)
{
panel.anchoredPosition = start + Random.insideUnitCircle * strength;
await YarnTask.Yield();
elapsed += Time.deltaTime;
}
panel.anchoredPosition = start;
}
public override void OnLineDisplayBegin(MarkupParseResult line, TMP_Text text) { }
public override void OnLineDisplayComplete() { }
public override void OnLineWillDismiss() { }
}ActionMarkupHandler. It’s a MonoBehaviour, so they’re components in your scene and can use other GameObjects.[shake_box/] is.OnPrepareForLine is called when a line arrives, before any of it is shown. Look through the line’s markup, and remember the position of each [shake_box/].OnCharacterWillAppear is called before each character appears. The typewriter waits for the task it returns.[shake_box/] here, return straight away, so the typewriter carries on without waiting.cancellationToken is cancelled when the player hurries the line. Stop shaking early if that happens.ActionMarkupHandler needs all five methods, so the ones this handler doesn’t use are left empty.To use it:
- Add the script to a GameObject in your scene.
- Set its Panel to the dialogue box’s RectTransform. In the Dialogue System prefab, that’s the Line Presenter GameObject.
- Select the Line Presenter, and add the GameObject to its Event Handlers list.
An event handler has these methods. They’re all abstract, so you need to write all five, even if some are empty:
| Method | When it’s called |
|---|---|
OnPrepareForLine(line, text) | A line has arrived, before any of it is shown. |
OnLineDisplayBegin(line, text) | Before the first character appears, once the dialogue box has faded in. |
OnCharacterWillAppear(currentCharacterIndex, line, cancellationToken) | Before each character appears. The typewriter waits for the task it returns. |
OnLineDisplayComplete() | The whole line is showing. |
OnLineWillDismiss() | The line is about to leave the screen. |
line is the line’s text and markup, as a MarkupParseResult. text is the TextMesh Pro text the line is shown in.
If the player hurries the line while your handler is waiting, cancellationToken is cancelled. For anything that takes a while, check it and finish early.
Samples
Two samples show more of this. To add them to your project, see Samples.
- Replacement Markup uses Markup Palettes, styles and marker processors, including ones that add sprites to a line.
- Inline Events uses event handlers to move a character and change their expression partway through a line.