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 reads a compiled Yarn node from GDScript. Before a node runs, it finds every Command in the node, and works out which assets those Commands need, so they can be loaded ahead of time.

Nothing is loaded for real. The Commands print messages to Godot’s Output panel, and the first time a Command sees an asset, it waits a second to stand in for a slow load. When you run the sample, Capsley explains what’s going on, then asks whether to run the next node with the “preload” or without it. With the preload, the Commands finish instantly. Without it, each one pauses for a second.

A green capsule character in a walled arena with a chequered floor, facing Capsley, a blue capsule with a face, near the far wall. The dialogue box shows Capsley saying Hello this sample shows off how you can explore the internals of Yarn Spinner to extract more information.
The Node Internals sample, with Capsley's first line.

Running it

Open samples/node_internals/node_internals.tscn and press Run Current Scene. Keep the Output panel open, because that’s where the results appear.

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

You can talk to Capsley again after the conversation ends. The explanation at the start only appears the first time, because it’s in a <<once>> block.

How it works

The CommandAssetPreloader node registers five Commands with the Dialogue Runner, using add_command():

CommandWhat it does
<<preload_command_assets>>Finds the assets the commands node needs, and marks them as loaded.
<<clear_preload>>Forgets everything that’s been marked as loaded.
<<set_background>>Stands in for showing a background.
<<set_music>>Stands in for playing music.
<<character_avatar>>Stands in for showing a character’s picture.
The Commands CommandAssetPreloader adds.

Its Preload Node Name, set to commands in the Inspector, names the node it looks through.

Each Command is registered with its own add_command() call, so when the Yarn Project is imported, Yarn Spinner finds all five and lists them in Internals.ysls.json, which the Yarn Spinner extension for VS Code reads to suggest them as you type. See The .ysls.json file.

The Yarn Script

At the end of the Start node, the player picks whether to preload, and then the dialogue jumps to the commands node:

-> launch with the "preload"
    <<preload_command_assets>>
-> launch without the "preload"
    <<clear_preload>>

<<jump commands>>
===

title: commands
---
Capsley: this node has a lot of commands in it that are intended to simulate a bunch of commands that need to load assets and resources
<<set_background castle>>
<<set_music "cool beats to slay dragons to">>
<<character_avatar Glenn>>
<<character_avatar Liz>>
<<character_avatar EvilDave>>
<<character_avatar RegularDave>>
Capsley: aaaaaaand done
Lines 1–2 Look through the commands node now, and mark its assets as loaded.
Lines 3–4 Clear anything marked as loaded, so every asset has to “load” again.
Lines 12–17 Six Commands, each naming one asset. The music’s name has spaces in it, so it’s in quotes.
The end of the Start node, and the commands node, in Internals.yarn.

Finding the node

CommandAssetPreloader gets the compiled node from the Dialogue Runner’s Yarn Project:

func _get_node_to_preload() -> YarnNode:
	if dialogue_runner == null or dialogue_runner.yarn_project == null:
		return null
	var program := dialogue_runner.yarn_project.get_program()
	if program == null:
		return null
	return program.get_node(preload_node_name)
Line 4 The compiled program, with every node in the Yarn Project.
Line 7 The node called commands, as a YarnNode.
_get_node_to_preload in command_asset_preloader.gd.

A YarnNode holds the node’s compiled instructions, and its headers, which you can read with get_header().

Finding the Commands

<<preload_command_assets>> goes through the node’s instructions one at a time, and keeps the ones that run a Command:

func _preload_command_assets() -> void:
	var node := _get_node_to_preload()
	if node == null:
		return

	for instruction in node.instructions:
		# the vast majority of instructions are RunLine; we only want commands
		if instruction.opcode != YarnInstruction.OpCode.RUN_COMMAND:
			continue

		# split the command text the same way the runner does at dispatch
		var elements := YarnCommandParser.parse(instruction.command_text)

		# every command we care about takes exactly one argument, so anything
		# else can be skipped (a real game might handle these cases per command)
		if elements.size() != 2:
			continue

		match elements[0]:
			"set_background":
				_backgrounds[elements[1]] = true
			"set_music":
				_music[elements[1]] = true
			"character_avatar":
				_avatars[elements[1]] = true

	print("preloaded backgrounds: %s" % str(_backgrounds.keys()))
	print("preloaded music: %s" % str(_music.keys()))
	print("preloaded avatars: %s" % str(_avatars.keys()))
Lines 6–9 Skip every instruction that doesn’t run a Command, like the ones that show lines.
Line 12 Split the Command’s text into its name and arguments. set_music "cool beats to slay dragons to" becomes two parts, because the quotes keep the name together.
Lines 16–17 Skip anything that isn’t a name and one argument.
Lines 19–25 Mark the asset as loaded, in the list for its kind of Command.
Lines 27–29 Print what was found to the Output panel.
_preload_command_assets in command_asset_preloader.gd.

This finds every Command in the node, wherever it is. A Command inside an <<if>> or an option is found even if the dialogue never reaches it.

The Commands that use the assets

<<set_background>>, <<set_music>> and <<character_avatar>> all call _ensure_loaded(), with the list for their kind of asset:

func _ensure_loaded(cache: Dictionary, asset: String, done_message: String) -> Variant:
	if not cache.has(asset):
		cache[asset] = true
		push_warning("%s is not already \"loaded\", pretending to do that now" % asset)
		await get_tree().create_timer(1.0).timeout
	else:
		print("%s has already been \"loaded\"" % asset)
	print(done_message % asset)
	return null
Lines 2–5 The asset hasn’t been marked as loaded. Mark it, show a warning, and wait a second to stand in for loading it.
Lines 6–7 The asset was preloaded, so there’s no wait.
_ensure_loaded in command_asset_preloader.gd.

Because the method uses await, the dialogue waits for it before going on. If you chose to launch without the preload, the six Commands take six seconds between them, and the Output panel shows a warning for each asset. With the preload, each one prints that it has already been “loaded”, and Capsley’s next line appears immediately.

Capsley’s last line has a [link] on “CommandAssetPreloader”. The LinkOpener node under the Line Presenter opens command_asset_preloader.gd when it’s clicked.

Things to try

  • Change Preload Node Name on CommandAssetPreloader to Start, and launch with the preload. None of the six assets are found, because their Commands are in commands, so every one of them pauses.
  • Add <<character_avatar Glenn>> a second time in the commands node. Without the preload, the second one doesn’t pause, because the first one has already “loaded” Glenn.
Next step Replacement Markup Markup that changes a line's text before it's shown.