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!
Google.Protobuf
license with your game as well!The Strings and Metadata Tables
Compiling your .yarn
files produces three files:
- A bytecode program (
.yarnc
) - A strings table (
.csv
) - A metadata table (
.csv
)
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.
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.