Yarn Spinner使用记录
安装过程
在注册unity的时候,如果id验证域名是.cn就要你绑手机号,真的是入乡随俗啊😅
把.cn改为.com就会自动跳回hub了,我用的是gmail。
或者**使用tun模式,但其实只有注册登录拿license的时候需要被恶心一下,tun模式有些软件经常出错的。
然后包管理器使用git安装,输入链接即可https://github.com/YarnSpinnerTool/YarnSpinner-Unity.git#current
真没想到,跟着教程用不了而“不显示字幕”的原因是不管你使用package下载还是git下载,给的3.1版本都不太正常!请在package manager中修改为2.5版本左右,可能只要低版本就行了。
定制化
直接使用肯定是不行的,因为我们肯定希望对话框能满足以下三点要求:
- 我想什么时候发生对话就什么时候发生,而不是一开始就自动运行(DialogueRunner)
- 默认的对话框太丑(LineView,OptionView)
- 给对话框添加更多功能,比如快进啥的(SupportComponent)
官方文档里面解释的很清楚,Yarn Spinner的dialogue system运行一个Yarn Project需要以下三个组件:
- strings tables:字符串
- metadata tables:元数据,比如角色id啊其他各种信息。它和字符串一起提供给Line Provider进行本地化处理(比如转换成其他国家语言)生成Lineview
- binary program:编译好的二进制文件,给Dialogue Class和Runner用来控制流程(story flow),编译好的程序只能看到文本的id,然后再使用LineProvider提供的台词填充(和got表一样,运行的时候才去找对应的函数)
基础语法
基础定义
节点头,节点体和分割线:
title: 这是节点头,名字不能重复
---
这是节点体(内容),上面是头和体的分割,下面是节点和节点分割
===
节点体与组
title:test
---
A:这是第一行内容
B:这是第二行,会被逐行运行 /// 这是注释
-> 这是选项1
-> 这是选项2
/// 此处空行
-> 这是另一个选项组
这是后续 /// 选择了该选项后的文字
===
台词组line group,会在台词里面抽一句返回,更加丰富:
title: Start
---
Captain: Navigator, fire the glitter torpedoes! That'll confuse the enemy ships!
=> Navigator: *sighs deeply* Sir, we don't have 'glitter torpedoes.' Those were in your dream last night.
=> Navigator: *eyes roll skyward* Captain, weaponizing craft supplies is not part of standard space combat protocol.
=> Navigator: *Slumps shoulders in defeat* I'll... make a note in the log that you suggested tactical glitter, sir.
===
节点组node group,能在一堆同名节点中挑选,还能添加条件:
title: Guard
when: once // 只出现一次
---
Guard: You there, traveller!
Player: Who, me?
Guard: Yes! Stay off the roads after dark!
===
title: Guard
when: always // 总是
---
Guard: I hear the king has a new advisor.
===
title: Guard
when: $has_sword // 条件
---
Guard: No weapons allowed in the city!
===
跳转
yarn中的命令都是使用 <<>>包裹的:
title: Start
---
Narrator: jump指令是最基本的跳转
<<jump Adventure>>
===
title: Adventure
---
-> Detour用于跳转,但是不同的是跳转的节点结束以后会回到调用处(就是call啦),碰到return会提前返回
<<detour try>>
-> 如果没有detour但是调用了return相当用stop
欢迎回来!
===
title: try
---
欢迎使用detour跳转过来
-> 现在你可以立即返回
<<return>>
-> 或者你等节点结束再回去
节点结束啦
===
变量
推荐专门用一个节点来声明所有的变量,变量前面都有一个 $。
<<declare $playerName = "Reginald the Wizard">> // 声明变量
<<set $greeting to "Hello, Yarn!">> // 设置变量,变量必须有值
<<set $numberOfSidesInASquare = $numberOfSidesInATriangle + 1>> // 同类型可以操作
The value of variableName is {$variableName}. // 使用方法很简单,就是大括号括起来
内部支持三种类型互相转换:string(),bool(),number()
变量是可以使用变量定义的,每次会自动更新,非常方便:
<<declare $is_evening = $game_hour >= 18 && $game_hour < 22>>
<<declare $town_shops_open = !$is_holiday && $game_hour >= 9 && $game_hour < 18>>
<<if $is_evening && !$town_shops_open>>
Innkeeper: Most shops are closed now, but you're welcome to stay here for the night.
<<endif>>
控制流
最基本的if语句:
<<set $gold_amount to 5>>
Player: I'd like to buy a pie!
<<if $gold_amount < 10>>
Baker: Well, you can't afford one!
<<elseif $gold_amount < 15>>
Baker: You can almost afford one!
<<else>>
Baker: You can afford a pie!
<<endif>>
条件选项,有用!:
Guard: You're not allowed in!
-> Sure I am! The boss knows me! <<if $reputation > 10>>
-> Please?
-> I'll come back later.
Once
once就是只出现一次,可以用在让一些对话只出现一次,其余时刻使用else:
<<once if $player_is_adventurer>>
Guard: I used to be an adventurer like you, but then I took an arrow in the knee.
<<else>>
Guard: Greetings.
<<endonce>>
甚至能直接加入对话中:
-> What's going on? <<once>>
Guard: The kingdom is under seige!
-> Where can I park my horse? <<once if $has_horse>>
Guard: Over by the tavern.
Enum
好用!让变量更清晰整洁:
<<enum Food>>
<<case Apple>>
<<case Orange>>
<<case Pear>>
<<endenum>>
<<declare $favouriteFood = Food.Apple>>
<<if $favouriteFood == Food.Apple>>
I love apples!
<<endif>>
和unity交互命令
命令是和line和option一样发送给runner的,所以,可以做很多事情,学会unity脚本编写以后就能做更多的互动了!
有一些内置命令:<<wait 666>>,<<stop>>
函数
用函数来“算值”,用命令来“控制游戏”。
也就是说函数最好只是拿来算值,“纯函数”,啥都不做。
更丰富游戏性的语法
Saliency and Storylet
storylet就是各种小片段,区别于传统的线性叙事,通过各种不同的环境场景和变量触发的对话能有更大的自由度。
Saliency就是重要性,具体表现在比如“睡觉”和“唱歌”,如果发生在“进入卧室”场景后,“睡觉”的重要性肯定更高。
yarn spinner通过node group和line group来提供storylet,同时saliency是可以自定义的,但我觉得内置的策略已经够了。
内置策略
yarn spinner里面内置了四种策略:
-
Best(最优)
Select the most salient.
选择当前“最显著”的 storylet(满足条件最多的)。 -
Best Least Recently Viewed(最优 + 最近未展示)
Prioritize salient content that hasn't been seen recently.
在满足条件的 storylet 中,优先选择最近没展示过的。 -
Random Best Least Recently Viewed(随机 + 最优 + 最近未展示)
Like above, with some randomization.
与上一个类似,但在候选 storylet 中加入部分随机性,避免过度可预测。 -
Random(完全随机)
Completely random selection.
从满足条件的 storylet 中完全随机选择。
权重计算
权重是这样计算的:
- always=0
- once+1
- condition+1(所有operator数目+1)
示例:
when: $a or $b has a complexity of 2.
when: once has a complexity of 1.
when: once if $a or $b has a complexity of 3.
设置策略组
设置的方法就是通过:
<<set_saliency first>>
<<set_saliency random>>
<<set_saliency best>>
<<set_saliency best_least_recent>>
<<set_saliency random_best_least_recent>>
自定义策略组
创建一个类型,实现 IContentSaliencyStrategy 接口,然后将其实例赋值给你的 Dialogue 对象的 ContentSaliencyStrategy 属性。
IContentSaliencyStrategy 接口包含两个必要方法:
ContentSaliencyOption? QueryBestContent(IEnumerable<ContentSaliencyOption> content):决定从给定的一组选项中(如果有)哪一个可以运行,你应当返回一个“最合适的内容”,如果都不合适,则返回 null。调用这个方法并不代表内容真的被选中,它只是一个“预选”过程。你的实现必须是只读的,不能修改任何状态。即使你返回了某个内容,也不能保证它一定会被执行。void ContentWasSelected(ContentSaliencyOption content):当系统真正选择并使用了某个 storylet时,会调用这个方法。你可以在这个方法中更新状态信息(例如记录这个 storylet 被用过几次)。你可以用 node group 来判断其内部是否有任何 storylet 可以执行。这在判断角色是否“准备好要说话”时特别有用。也可以用于判断该组节点中是否有特殊标签,比如“有重要信息”。
tag and metadata
标签可以通过函数和unity进行互动!在 Unity 中,通过 LocalizedLine.Metadata 属性读取。
更多提取节点的方法需要在“官方文档->C#->api”中查找。
还有更好用的用法!!!:
- 由于tag是metadata,是不公开的,可以作为TODO一样,比如打上
#need_revision,就能快速定位。并且unity的yarn spinner集成会把所有tag提取出来放进一个CSV文件,直接用excel搜索修改更方便! - 比如“日志”或者“道具描述”这些可能在台词中已经介绍出现过一次的,不用复制!打上tag以后用脚本让unity在运行yarn对话的时候自动将对应标签进行复制!
行标签
可以给所有内容打标签,标签是不公开的,但是标签必须位于同一行:
Homer: Hi, I'd like to order a tire balancing. #tone:sarcastic #duplicate
yarn spinner会自动帮你打一些标签,比如每个选项之前的一句话:#lastline,在定制化的时候我们见过它,比如出现选项的时候保留文句。
节点标签
节点的标签不一定用#开头,但是我建议统一都用#开头:
title: Train_Dialogue
tags: #camera2 background:conductor_cabin
---
Why did you stop the train?
Now we won't arrive in time at the next stop!
===
tracking
可以对“你是否进行过访问那个节点”作为一个条件,比如经历了一段故事,而没必要自己建变量傻乎乎的一个一个node进行管理:
<<if visited("SomeNode")>>
Welcome back!
<<else>>
Nice to meet you!
<</if>>
yarn spinner只对你调用了visited的节点进行跟踪(其实我觉得已经足够了),如果你想它总是生成跟踪的节点:
title: Node_Name
tracking: always // 就添加这个
---
I know how many times you've been here.
===
shadow line 复用
直接复用,记得指定“line”的id:
title: Tavern
---
Ava: Hello, barkeep!
Guy: Hi there, how can I help?
Ava: I should go. #line:departure
===
title: Kitchen
---
Ava: Greetings, chef!
Guy: What are you doing back here?
Ava: I should go. #shadow:departure
===
markup
有时候想给文本做些动态效果,或者某个选项触发“诡异的背景音乐”,但是这些小东西不想用tag来打标签?
那就用markup:
Oh, [wave]hello[/wave] there!
Oh, [wave]hello [bounce]there![/wave][/bounce]
[wave][bounce]Hello![/] // 这个是关闭所有标签,适合不想一个一个找闭合标签的懒人
[wave size=2]Wavy![/wave]
不过markup会自动修改掉一个空格,比如 A [wave/] B中间明明有2个空格但是最后只会显示一个,如果不想这样子就: A [wave trimwhitespace=false/] B
内置markup
选择标记(select marker)根据变量的值来选择某个结果:
<<declare $gender = "pig">>
I think [select value={$gender} m="he" f="she" nb="they" /] will be there!
复数标记(plural marker:one two few many other)使用数字的值:
PieMaker: Hey, look! [plural value={$pie_count} one="A pie" other="Some pies" /]!
序数标记(ordinal marker:one two few other)使用次序:
Runner: The race is over! I came in [ordinal value={$race_position} one="%st" two="%nd" few="%rd" other="%th" /] place!
转义
如果需要输出[]就要用到转义,就是 \ 就行了。。。
或者直接这样:[nomarkup] ab[]cc
和Unity联动
基础使用
首先需要创建一个yarn project,它会搜索某个目录下的所有yarn script。
就像先前讲解的一样,yarn project需要传递给dialogue runner才能被解析为二进制执行流并链接符号,此时才能使用。
在下载好的yarn spinner包中,是有直接能运行的prefab的,只需要创建一个script和project就行了:

可以观察里面的Line Presenter, Option Presenter和Line Advancer的使用方法和写法,默认提供的方法中官方提供了这些可以直接修改的功能:
- Line Presenter
- Text appearance and positioning
- Character name display
- Typewriter effect speed
- Fading effects
- Auto-advance settings
- Option Presenter
- Option appearance and positioning
- List layout
- Whether to show unavailable options
- Line Advancer
- Selecting your Line Presenter in the hierarchy
- Finding the associated Line Advancer component
- Configuring the input method (keycode, button, etc.)
Variable Storage
Yarn提供对脚本中变量的追踪,但是,这是in memory的,如果想让它持久保存需要学习:导出存储的方A。
在Dialogue System中:

如果想要debug的话,就创建一个 TextMeshPro Text Component,然后游戏运行的时候就能看得到了。
Command
在window->yarn->commands中可以查看所有命令。
有两种使用自定义命令的方式,一种是使用YarnCommand attribute,另一种是使用DialogueRunner's AddCommandHandler method。
我推荐使用第二种方法,因为第一种方法需要知晓具体的对象名才行,感觉第二种方法更加解耦。
YarnCommand attribute
public class CharacterMovement : MonoBehaviour {
[YarnCommand("leap")] // 🌟这就是YarnCommand attribute,就能在yarn脚本中调用
public void Leap(float height) {
Debug.Log($"{name} is leaping!");
}
}
将上面的文件保存为a.cs,然后绑定在一个物体MyCharacter上(yarn会先查找这个物体,然后顺着标签leap去寻找函数),就能在yarn脚本中调用:
<<leap MyCharacter 5.5>> // 🌟注意看参数的传递
// will print "MyCharacter is leaping!" in the console
如果函数是static的,就直接呼叫,相当于省去查找物体的步骤,毕竟static是全局的嘛:
// Note that we aren't subclassing MonoBehaviour here;
// static commands can be on any class.
public class FadeCamera {
[YarnCommand("fade_camera")]
public static void FadeCamera() {
Debug.Log("Fading the camera!");
}
}
就直接:
<<fade_camera>>
// will print "Fading the camera!" in the console
AddCommandHandler
也可以直接在Dialogue Runner中添加代码实现,通过AddCommandHandler方法。
AddCommandHandler需要两个参数:
- 这个command的名字,后面会在yarn spinner中叫这个名字
- 实际的方法
public class CustomCommands : MonoBehaviour {
// Drag and drop your Dialogue Runner into this variable.
public DialogueRunner dialogueRunner; // 🌟需要绑定runner对象
public void Awake() {
// Create a new command called 'camera_look', which looks at a target.
// Note how we're listing 'GameObject' as the parameter type.
dialogueRunner.AddCommandHandler<GameObject>(
"camera_look", // 🌟the name of the command
CameraLookAtTarget //🌟 the method to run
);
}
// 🌟The method that gets called when '<<camera_look>>' is run.
private void CameraLookAtTarget(GameObject target) {
if (target == null) {
debug.Log("Can't find the target!");
}
// Make the main camera look at this target
Camera.main.transform.LookAt(target.transform);
}
}
然后就能使用了:
<<camera_look LeftMarker>> // make the camera look at an object named LeftMarker
Function
希望你没有忘记先前讲的“函数用来计算,命令用来控制”:
- Command (命令): 执行一个动作。是单向的,从 Yarn 脚本发往游戏引擎(比如 Unity),告诉游戏“去做某件事”。它不关心结果,也不需要返回任何信息。
- Function (函数): 获取一个信息。是双向的,从 Yarn 脚本发往游戏引擎“问一个问题”,并且必须从游戏引擎得到一个返回值(比如一个数字、true/false、或一段文字),然后在对话中继续使用这个值。
Function的创建方法差不多,也是两种方法,一个是Attribut一个是AddFunction。
但是有两点必须注意:
- Functions must return a value.
- Functions are required to be static.
其实就是摆明了,function是拿来计算并返回值的。
public class AdderFunction {
[YarnFunction("add_numbers")] // 🌟Attibute方法!
public static int AddNumbers(int first, int second)
{
return first + second;
}
}
使用就是:
<<if add_numbers(1,1) == 2>> // 🌟毕竟是static,和command知识点是一样的
One plus one is {add_numbers(1, 1)}
<<endif>>
Localisation
这里是理论篇,使用篇。
如果你的对话是纯文本且只有一种语言,那你什么都不用做。但如果你想做任何其他事情(比如加配音),你就需要建立并使用 Yarn Spinner 的流程来管理本地化。即使你的游戏没有配音,只是多语言也必须根据这些流程添加本地化操作,相当于绑定在一起的😂也就是说配音 是 本地化 功能的一个子集。想用配音,就必须先启用本地化。
Yarn Spinner 能让你轻松地为游戏添加多种语言。要点如下:
- 在 Assets 面板中选中你的 Yarn Project。
- 在 Inspector 窗口中点击“Export Strings as CSV”(导出字符串为CSV)。
- 翻译导出的 CSV 文件。
- 将翻译导入回你的项目中。
你可以使用以下任一系统:
- 内置本地化系统:直接通过 Yarn Spinner 管理翻译。
- Unity 本地化系统:与 Unity 的官方 Localisation 包集成。
我们同时提供这两个系统,因为我们的内置系统更容易设置。Unity 的本地化系统功能更多,但需要大量的设置工作...
然后是一些术语:
- Localisation (本地化设置): 包含某个特定语言的所有文本和资源(如配音)的信息集合。
- Base Language (基础语言): 你写 .yarn 脚本的原始语言。
- Strings Files (字符串文件): 就是那个包含所有翻译文本的 CSV 表格文件。// 🌟刚刚导出的文件
- Line ID (行ID): 这是最重要的概念! 它是每一句台词、每一个选项的唯一身份证号。Yarn Spinner 就是靠这个 ID 来匹配不同语言的同一句话的。 // 🌟对话中的每句话的位置
- Localised Line (本地化后的台词): 指特定语言环境下的台词文本。例如,“Hello”这句台詞的日语 Localised Line 就是“こんにちは”。
- Localised Line Asset (本地化后的台词资产): 与特定台词关联的特定语言的资产(如音频)。例如,“Hello”这句台词的日语配音文件。
- Line Provider (台词供应器): 一个在后台工作的组件。它的工作是:从对话中接收一个“行ID”(身份证号),然后根据玩家选择的语言,去字符串文件里找到对应的文本和配音文件,再把它们提供给游戏去显示和播放。
in-built Localisation
Unity Localisation
参考资料
其他分支剧情管理工具
YarnSpinner官方文档
详细安装过程,基础定制化
关于定制化的官方文档,写的很好了
Yarn Spinner入门语法讲解
官方语法讲解
本文来自博客园,作者:muyiGin,转载请注明原文链接:https://www.cnblogs.com/muyiGin/p/18882607

浙公网安备 33010602011771号