Skip to Content

Using Yarn Spinner Without Engine Integration Packages

This post was last updated July 2023. Software changes rapidly, and this note may not be up to date.

This is more of a loose collection of notes than a tutorial or guide.

Keep the Source Nearby

Working with Yarn Spinner without the Unity integration package is definitely not the usual path, and it can occasionally help to search through the source for YarnSpinner (the package you’re using directly in game), YarnSpinner-Console (which you’re using to compile .yarn sources, but it also has a console-based runner), and YarnSpinner-Unity (which you’re not using at all, but it can be useful to see how it goes about things).

For example, although the API docs will tell you the delegate Dialogue.PrepareForLinesHandler is a PrepareForLinesHandler, it does not tell you what that type actually is. (Is it passed an IEnumerable<Line>? No, an IEnumerable<string> of course.)

How do you reify a Program? (Program.Parser.ParseFrom(byte[]), natch, which is apparently the usual way of working with protocol buffers, but there are only the merest of hints that Yarn Spinner uses protocol buffers in the first place.)11 Depending on how your C# dependencies get packaged up, don’t forget to include a copy of the Google.Protobuf license with your game as well!

1 Depending on how your C# dependencies get packaged up, don’t forget to include a copy of the Google.Protobuf license with your game as well!

The Strings and Metadata Tables

Compiling your .yarn files produces three files:

The .yarnc file can be shipped as is with the game, but you might want to do some processing on the strings and metadata tables before importing them as assets. If you’re localizing the game, this is where that work would happen, but consider processing the tables even if you’re not.

The strings table looks like this:

I’m stealing all of my examples from Friends at the Table, a podcast I like a lot.

id,text,file,node,lineNumber
line:e28f46ec,Lazer Ted: Expertize with a z also. Expertize.,/Users​/username​/Documents​/Projects​/BlahBlah​/Dialogue​/Test1.yarn,LazerTedIntro,4
...

The only fields you really need are id and text. The others are mostly useful for localization, or possibly for tracing an error back to the source files, but you don’t need all of that redundancy, and you probably don’t want to ship the full path of your development directories with your game.22 I recognize that paths get leaked in games all the time, but that doesn’t mean it should be that way.

2 I recognize that paths get leaked in games all the time, but that doesn’t mean it should be that way.

Finally, the main thing we’ll be doing with this table is taking a line id and finding the associated text. We could build an index as we read in the table, or we could use an encoded form where that’s already done:

{
    "line:e28f46ec": "Lazer Ted: Expertize with a z also. Expertize.",
    ...
}

The metadata table is similar:

id,node,lineNumber,tags
line:e28f46ec,LazerTedIntro,4,incharacter,lastline
...

The one thing worth noting is that multiple tags aren’t encoded in one field; each one is a new, comma-separated field. If you’re using a schmancy parser that reads in the header and generates accessors for each record, those additional tags won’t be associated with a header.

Loading is (Relatively) Slow

You should of course be cautious loading anything during gameplay, but reifying the Program and initializing the Dialogue object is surprisingly slow, like tens of ms slow. Set up your dialogue during scene loading, rather than on demand.

On Handlers (heh)

There are a couple of Dialogue delegates you have to set up, even if it’s not obvious that they’re required:

OptionsHandler

The virtual machine will throw an exception the first time Continue() is called, even if your dialogue doesn’t have any options.

Strangely, LineHandler is not required.

NodeCompleteHandler

Invoked without null checks when switching to a new node while one is running, or when the dialogue stops, either because of an explicit call to Stop() or because the dialogue ends.

Character Markup is a Mess

Consider this dialogue:

title: LazerTedIntro
---
Lazer Ted: Oh, you [i]know[/i] I’m [wavy]wavy[/wavy]! You know!

If we run this through Dialogue.ParseMarkup, we get a MarkupParseResult that looks like this:

Text: "Lazer Ted: Oh, you know I’m wavy! You know!"
Attributes:
    [0]: Name: "character", Position: 0, Length: 11
    [1]: Name: "i", Position: 19, Length: 4
    [2]: Name: "wavy", Position: 28, Length 4

It’s somewhat annoying that the character name is left in the string, even though it’s recognized as a markup attribute. If we do remove it, all of the other attributes’ positions are now wrong and need to be offset by the character name’s length.

The character name is just whatever text is before the first :, if any, which leads to some really weird situations. Imagine this line as a title card, which wouldn’t have a character speaking it:

title: EpisodeIntro
---
[i]COUNTER/Weight 31: Expertize with a Z[/i]

The MarkupParseResult:

Text: "COUNTER/Weight 31: Expertize with a Z"
Attributes:
    [0]: Name: "character", Position: 0, Length: 22
    [1]: Name: "i", Position: 0, Length: 37

Note that “COUNTER/Weight 31: ” is only 19 characters; the [i] tag is being counted as part of the character name, before it’s subsequently removed!

The easiest workaround is probably to add a “Title:” character name, and then watch out for that in our code, but we could also mark up the line with a tag, or use a command to show the title card. It’s just something to be aware of.