Articy Draft 3 与 Unity

  • Articy Draft 3 是Steam上的一个做叙事游戏数值剧情策划的软件,为了去结合Unity或是Unreal,可以作为一个独立的第三方软件(不是插件)使用。最近出了免费试用版,但是有一定的容量限制(660个游戏物体或对话),但是能够符合一个基本的小型游戏或弱叙事游戏使用了。
  • 鉴于属于一个比较方便的文字建模软件,于是我就其官网上的生肉以及实践做出了以下学习笔记。
  • 这个笔记只涉及一些文字游戏的基础功能,像高级功能诸如地点和旅程就不涉及了。
  • 当然,还有B站好心人搬运的视频,AD的基础使用按键操作什么的就不讲了,看视频就能明白。

**翻译中比较让人困惑的地方:

  • 在软件中有一堆以“复数”为开头的中文翻译,例如:“复数资源”,实际上指的是资源单词的复数形式:”Assets”,请自动省略掉”复数”两字。
  • 软件中把Location翻译成了地点,实际上Spot才是地点,但是Spot被翻译成了“斑点”,这也是翻译错误。

(甲)主要世界观

  • 属性(Property):
    • 属性是代码层面上的最基础的单位,基本对应的是对于数值类型的定义:布尔值、基础数值。
  • 特性(Feature):
    • 特性是由许多属性构成的结构体,比方说玩家和npc们都有基础特性:血量、攻击力、敏捷,而玩家可能在此基础上还拥有只属于玩家的玩家特性:经验、背包容量等。
  • 模板(Template):
    • 每一个流程中的节点、每一个工程里的资源文件,都可以使用其规定类型的自定义模板。模板是由许多特性构成的集合,比如玩家PC的模板与电脑NPC的模板中特性的分布不同。
  • 实体(Entity):
    • 实体可以类比Unity中的GameObject,Unreal里的Actor,每个实体能使用一个模板。人物可以是实体,道具可以是实体,一些抽象的命令也可以是实体。
  • 流程(Flow):
    • 流程是故事叙事大纲,是一个节点面板,可以在此处编写逻辑和故事。
  • 全局变量(Global Variables):
    • 全局变量规定必须属于一个变量集,所以必须先创建变量集,在里面才能添加全局变量。就比如说一些基础判断布尔,整数数值的累积,以及字符串。
  • 地点(Location):(本指南不涉及)
    • 许多日式解谜游戏都喜欢在一个固定的图片画面里藏一些鼠标点击才触发剧情的东西,这里边有一个基础的地图绘制系统。(免费版地图上限只有10个)
  • 旅程(Journeys):(本指南不涉及)
    • 这里的旅程指的是RPG游戏中的好结局(GE)坏结局(BE)之类,规定玩家必须走哪个流程才能达成结局。

(乙)流程(Flow)中节点

  • 流程片段(流片段)(Flow Fragment)
    • 流程片段是一个最基础的节点,可以作为一个剧情容器,将其他剧情对话放在容器中。
    • 左上角需要选择预览图像;右上角需要拖拽一些引用的实体;在下方填入说明文本。
  • 对话(Dialogue)
    • 对话节点主要代表一个完整的对话,而不是一个人说的一句话。与流程片段节点几乎等价。
    • 左上角需要选择预览图像;右上角需要拖拽一些引用的实体;在下方填入说明文本。
  • 对话片段(Dialogue Fragment)
    • 对话片段是一个人说的一句话,需要“沉浸”到对话片段中(放于节点子集)。
    • 上方发言者需要引用一个实体;头像槽右方需要填写菜单文本,菜单文本显示在当该节点需要通过分支选项按钮点击进来时,分支按钮上显示的文字;下方舞台指示区是一些剧本的提示信息,比如人物实体说话时的语气,供后期配音演员比对;最下方是对话的实际文字。
  • 核心(Hub)
    • 核心是一个抽象的状态,多个对话可以链接到同一核心,核心也可以输出到多个对话中,相当于集线器。
  • 跳转(Jump)
    • 方便跳转到某个节点上,例如核心节点。
    • 跳转需要在槽中填写将要跳转的节点,一般是核心(Hub)节点。
  • 条件(Condition)
    • 条件需要你去写脚本,当条件满足,跳到绿色输出针脚;当条件不满足,则跳到红色输出针脚。
    • 需要填写条件。
  • 命令(说明)(Instruction)
    • 命令说明节点是设置条件,修改数值。
    • 需要填写命令。
  • 注释(Annotation)
    • 就是注释,没别的活儿。

(丙)物体基础信息查看

  • 以流程片段(FlowFragment)节点为例,在流程(Flow)面板里选择该节点,右键打开属性(F8):
    • 技术名称:该节点在代码中调用时所称呼的名称,没有两个游戏物体的技术名是一致的。
    • ID:代码中也可以调用该物体的ID,削微有一点不常用。
  • 点开“模板”标签,如果提示没有应用模板,则按右边的按钮选择一个模板。
    • 例如此时的模板名为FlowFragMessage,该模板下有两个特性:NodeMessage和DiaMessage,NodeMessage特性下又有一个布尔属性:isLastNode。

(丁)Articy:Expresso表达式

  • AE表达式主要是为了配合条件和命令两个节点来使用的,基本上就是普通编程语言的青春版。

如何获取基础信息

  • 基本上获取下列两种游戏信息就足够了:
    • 全局变量:
      • 可Get可Set:[变量集名].[变量名]
    • 属性变量:
      • Get方法:getProp(getObj("[物体技术名]"), "[特征名].[属性名]")
      • Set方法:setProp(getObj("[物体技术名]"), "[特征名].[属性名]", [新值])

条件节点(Condition)

  • 条件节点里面唯一要填写的是一个布尔变量;这个布尔变量由判断表达式构成:
    • 判断操作符:等于== 不等于!= 小于等于<= 大于等于>= 小于< 大于>
    • 逻辑操作符:与&& 或||
    • 基础运算符:加减乘除余+-*/%
  • 比方说我在全局变量集MyVariableSet里有布尔变量isOpened,我需要在一个节点前先判断是否isOpened是真,就可以这么写:
    • MyVariableSet. isOpened == true(没有分号结尾)
  • 比方说玩家人物的技术名称为PC_Player,判断其模板中BasicFeature特性中attack数值属性是否大于10,就可以这么写:
    • getProp(getObj("PC_player"), "BasicFeature. attack") > 10(没有分号结尾)
  • 如果该节点是对话片段(Dialogue Fragment)节点,发言者正好是玩家,就可以直接调用其发言者speaker:
    • getProp(speaker, "BasicFeature. attack") > 10(没有分号结尾)
  • 在流程(Flow)中的带引脚的节点的左引脚可以双击,在弹出的对话框里可以写入条件。

命令节点(Instruction)

  • 命令节点里需要填写赋值命令,以英文分号间隔;命令节点里除了基础运算符之外,还有:
    • 引用运算符:自增+= 自减-= 自乘*= 自除/= 自余%=
  • 如果我要修改MyVariableSet变量集里的isOpened布尔值:
    • MyVariableSet. isOpened = true;(每一句都要有分号结尾)
  • 如果我要修改玩家实体的BasicFeature特性中的attack数值:
    • setProp(getProp("PC_Player"), "BasicFeature. attack", 10);(每一句都要有分号结尾)
  • 如果玩家属性是需要自增的,就必须先getProp,再setProp:
    • setProp(getProp("PC_Player"), "BasicFeature. attack", getProp(getObj("PC_player"), "BasicFeature. attack") + 1);(每一句都要有分号结尾)
  • 在流程(Flow)中的带引脚的节点的右引脚可以双击,在弹出的对话框里可以写入命令。

(戊)Unity代码层面获取信息

  • 引用
1
2
3
4
using Articy.Unity;//Articy的主要内容
using Articy.Unity.Interfaces;//Articy的接口
//这个引用需要与 Articy Draft 3 的工程名对应,不然会报错,比如说示例的工程名为Samplenarrative,其自动生成的脚本都会自动隶属于这个命名空间内。
using Articy.Samplenarrative;
  • 使用检视界面来获取物体引用
1
2
3
[ArticyTypeConstraint(typeof([模板名]))]//使用属性限制取值类型
public Articy.Unity.ArticyRef playerRef;//使用Inspector来检索Articy打包的物体
//此处模板名指的是在Unity的Assets/ArticyImporter/Content/Generated路径下的系统自动生成的脚本的类名,一般物体都会以其所应用模板的名来命名。
  • 获取或设置全局变量
1
2
3
4
bool globalBool =
ArticyGlobalVariables.Default.[变量集名].[变量名];//获取Get
//
ArticyGlobalVariables.Default.[变量集名].[变量名] = globalBool;//设置Set
  • 使用数据库来获取物体属性
1
2
3
4
5
6
7
8
float property = 0;
//为防止获取不到而报错,所以应该先判断是否可以投射(Cast)到模板类。
if (ArticyDatabase. GetObject("[技术名]") as [模板名])
{
property = (ArticyDatabase. GetObject("[技术名]") as [模板名]).
getProp("[特性名].[属性名]");
}
//此处模板名指的是在Unity的Assets/ArticyImporter/Content/Generated路径下的系统自动生成的脚本的类名,一般物体都会以其所应用模板的名来命名。

(己)自制AD模板与其所对应的Unity模板

警告

  • 在此Unity模板中需要有第三方Unity插件:
    • Odin Inspector:做编辑器界面的,没有也没关系,把爆红的删掉就行。
    • Text Mesh Pro:Unity的一个隐性内置文字插件,需要在Package Manager里面安装。
    • Anima Text:做文字动画的插件,没有也可以,删掉就行,不妨碍,大不了没动画。

AD模板设置

  • 特性定义:
    • ActorFeature:
      • (数值)hitPoint:100,0,100,0(默认值,最小值,最大值,小数位数)
      • (数值)attack:1,1,20,0(默认值,最小值,最大值,小数位数)
      • (数值)defense:0,0,100,0(默认值,最小值,最大值,小数位数)
      • (文本小)actorName:_, , , (默认值,……)
    • NodeMessage:
      • (布尔)isLastNode:False(默认值)
    • DiaMessage:
      • (数值)startDelay:0,0,100,2(默认值,最小值,最大值,小数位数)
      • (数值)branchDelay:0.25,0,100,2(默认值,最小值,最大值,小数位数)
      • (数值)passDelay:0.1,0,100,2(默认值,最小值,最大值,小数位数)
      • (数值)wordSpeed:5,1,10,0(默认值,最小值,最大值,小数位数)
  • 模板定义:
    • (复数流片段)FlowFragMessage:NodeMessage,DiaMessage
    • (复数对话)DialogueMessage:NodeMessage,DiaMessage
    • (复数对话片段)BasicDialogue:NodeMessage,DiaMessage
    • (复数核心)HubMessage:NodeMessage
    • (复数条件)ConditionMessage:NodeMessage
    • (复数说明)InstructionMessage:NodeMessage
    • (实体)Player:ActorFeature
    • (实体)NPC:ActorFeature
  • 模板使用方法:
    • 在流程(Flow)根画布上新建流程片段,表示第一章的所有内容归类。
      • 注意:包括此节点在内,接下来所有节点都必须应用上面所规定的模板!
    • 在此流程片段上按回车,进入节点子集,新建对话(不是对话片段),表示一大段的对话。
      • 注意:对话节点需要与虚框上左右两边的输入输出节点相连!
    • 在此对话节点上按回车,进入节点子集,新建一些对话片段,彼此相连(包括虚框),在最后一个对话片段节点上右键属性,将NodeMessage特性里isLastNode设为true,表示这是对话的最后一个节点。
    • 在最后一个对话片段之后再新建一个流程片段节点,其标题名设置为此段对话的结束按钮名。
      • 注意:首尾节点需要与虚框上左右两边的输入输出节点相连!

将AD的工程信息导入到Unity中

  • 点击AD界面左上角圆形菜单,点导出,跳出导出页面:
    • 选择技术输出中的Unity导出,应用默认的导出规则集,选择Unity工程的Assets路径为导出文件夹。
    • 点“好的”。(前提是Unity需要到Asset Store上下载导入器,否则无效)

Unity模板

  • Unity的Asset Store上面搜索Articy Draft,将导入器下到工程里。
    • 注意:导入器不可变更其绝对路径!
  • 当在AD里将工程信息导入之后,按照如下步骤建立对话系统:
    • 在大纲里新建Canvas画布,添加ArticyFlowPlayer组件。
    • 在此Canvas画布里构建一个拥有:
      • 作为对话头像预览的Image组件。
      • 作为对话的实际文本框的TextMeshProUGUI组件。(此文字组件再加上AnimatextTMProOld组件,如果有Animatext插件的话)
      • 作为自动生成的分支按钮所存放的父级,这里推荐使用ScrollView卷轴。
    • 新建脚本ArticyBranch,将其加在一个新建的按钮Button上,将按钮设为预制体,并将实例删除。
    • 新建脚本ArticyFlowManager,并将其加在Canvas画布的ArticyFlowPlayer下。
    • 将Canvas画布设为预制体。
    • 新建脚本ArticyInitTrigger,并将其加在一个trigger触发器上,将Canvas画布预制体放到trigger触发器的子集,将画布的Enable设置为false,将trigger触发器设为预制体。

脚本ArticyFlowManager内容

  • 脚本功能介绍:将ArticyFlowPlayer里面所播放的节点的隐性数据使用UI来公之于众。(为了实现Articy的IArticyFlowPlayerCallbacks接口)

  • 点击查看(如果点不开就复制下面的代码吧)

  • branchPrefab:按钮预制体

  • txt_mainText:对话文本框

  • rTrans_branchParent:按钮父级

  • img_previewImage:对话头像预览UI

  • IsPlaying:对话运行状态

  • showFalseBranches:是否允许出现非法分支

  • flowPlayer:绑定的ArticyFlowPlayer播放器组件

  • StartOnFlow:对话从哪个节点开始

  • key_forceJumpDelay:跳过按钮出现等待时间的按键

  • textEffect:Animatext插件模板(在文末有模板数据)

  • intervalRange:每个字出现耗时的最小值与最大值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
//
using Sirenix.OdinInspector;
using TMPro;
using Animatext.Effects;
//
using Articy.Unity;
using Articy.Unity.Interfaces;
using Articy.Samplenarrative;//这个引用需要与 Articy Draft 3 的工程名对应,不然会报错,比如说示例的工程名为Samplenarrative

[RequireComponent(typeof(ArticyFlowPlayer))]
public class ArticyFlowManager : MonoBehaviour, IArticyFlowPlayerCallbacks
{
#region 变量声明
[BoxGroup("预制体", true, true)]
[SerializeField] public GameObject branchPrefab;//用来生成分支实例的分支预制

[BoxGroup("界面元素", true, true)]
[SerializeField] public TextMeshProUGUI txt_mainText;//节点对话显示UI

[BoxGroup("界面元素", true, true)]
[SerializeField] public RectTransform rTrans_branchParent;//分支选项父级

[BoxGroup("界面元素", true, true)]
[SerializeField] public Image img_previewImage;//节点预览图显示槽UI

[BoxGroup("流程控制", true, true), ReadOnly]
[SerializeField] public bool isPlaying = false;//是否正在运行(目前被ArticyInitTrigger引用)

[BoxGroup("流程控制", true, true)]
[SerializeField] public bool showFalseBranches = false;//是否显示已经无法通过的分支

[BoxGroup("流程控制", true, true)]
[SerializeField] public ArticyFlowPlayer flowPlayer;//流程播放器组件

[BoxGroup("流程控制", true, true)]
[SerializeField] public ArticyRef startOnFlow;//开始节点位置

[BoxGroup("流程控制", true, true)]
[SerializeField] public KeyCode key_forceJumpDelay = KeyCode.Mouse0;//强制跳出延迟的按键

[BoxGroup("特效", true, true)]
[SerializeField] public DefaultTemplateEffect textEffect;//文字动画特效模板

[BoxGroup("特效", true, true), MinMaxSlider(0.0f, 0.5f, ShowFields=true), LabelWidth(100)]
[SerializeField] public Vector2 intervalRange = new Vector2(0.025f, 0.25f);
//
private float startDelayTimer = 0.0f;//开始延迟计时器
private float startDelayTimerMax = 0.0f;//开始延迟计时器最大值
private float initBranchTimer = 0.0f;//生成选项的延迟计时器
private float initBranchTimerMax = 0.0f;//生成选项的延迟计时器最大值
private float passDelayTimer = 0.0f;//跳过选项的延迟计时器
private float passDelayTimerMax = 0.0f;//跳过选项的延迟计时器最大值
private float cur_startDelay = 0.0f;//当前语段的初始延迟(经过初始延迟之后,文字才会出来)
private float cur_branchDelay = 0.25f;//当前语段的分支延迟(在文字出来之后,再经过分支延迟,分支才会出来)
private float cur_passDelay = 0.1f;//当前语段的跳过延迟(在文字出来之后,再经过跳过延迟,就可以提前跳过分支延迟,直接显示分支)
private int cur_wordSpeed = 5;//当前语段的文字动画速度
[HideInInspector] public bool cur_isLastNode = false;//当前语段是否是最后一句话
private IFlowObject cur_flowObject;//当前接口参数流程物体
private IList<Branch> cur_branches;//当前语段生成的分支
[HideInInspector] public List<Button> cur_branchButtons = new List<Button>();//当前语段分支的按钮组件
//
private bool isStartDelay = false;//控制开始延迟的计数
private bool isInitBranch = false;//控制分支选项的生成
private bool isPassDelay = false;//控制跳过选项的计数
//
#endregion
//——————————————————————————————————————————————————————————————————————————
#region Mono流程函数
void Start()
{
ClearAllBranches();//清理所有之前的分支
JumpToFlow(startOnFlow);//设置开始节点
}
void Update()
{
UpdateStartDelayTimer();//根据计时器来控制文字的显示
UpdateBranchTimer();//根据计时器来控制选项的生成
}
#endregion
//——————————————————————————————————————————————————————————————————————————
#region 原型接口实现
//人物对话都可在jumpDelay之后跳过,需要有一种特殊的跳过按钮,上面没有字,生成的位置与普通选项不一样
public void OnFlowPlayerPaused(IFlowObject aObject)//必须实现的接口:当进入新的对话节点时
{
ExtractCurrentPausePreviewImage(aObject);//显示此时的节点预览图
img_previewImage.enabled = IsDialogueFragment(aObject);//是否开启预览图 //背景图的显示,当当前的节点不是语段时,隐藏背景图,反之。
SetCurrentProperties(aObject);//刷新主要数值
//
txt_mainText.text = string.Empty;//清空文档
//
isStartDelay = true;//开始执行
}
private void OnFlowPlayerPausedAlt(IFlowObject aObject)//接口第二部分
{
var modelWithText = aObject as IObjectWithText;//使用接口的方法获取节点对话
string speakerName = (aObject as IObjectWithSpeaker) != null ?
$"{(string)(aObject as IObjectWithSpeaker).Speaker.getProp("ActorFeature.actorName")}:\n " : string.Empty;
string mainText = $"{speakerName}<flow>{modelWithText.Text}</flow>";
if (modelWithText != null) { txt_mainText.text = mainText; }//将节点对话显示在UI上
else { txt_mainText.text = string.Empty; }//否则设为空
textEffect.interval = Mapping(cur_wordSpeed, 10, 1, intervalRange.x, intervalRange.y);//设置文字播放速度
}
//
private void UpdateStartDelayTimer()//根据计时器来控制文字的显示
{
if (isStartDelay == true)
{
if (startDelayTimer < startDelayTimerMax)//计时器
{
startDelayTimer += Time.deltaTime;
}
else
{
startDelayTimer = 0;
OnFlowPlayerPausedAlt(cur_flowObject);//执行接口第二部分:进入新的对话
isStartDelay = false;//退出执行
}
}
}
//
private bool GetIfIsLastNode(IFlowObject aObject)//判断是否是一句话最后一个节点
{
if (IsFlowFragment(aObject))
{
return GetFlowFragmentProperty(aObject, "NodeMessage.isLastNode");//当前节点是否为最后一个节点///
}
else if (IsDialogueFragment(aObject))
{
return GetDialogueFragmentProperty(aObject, "NodeMessage.isLastNode");//当前节点是否为最后一个节点
}
else if (IsHub(aObject))
{
return GetHubProperty(aObject, "NodeMessage.isLastNode");//当前节点是否为最后一个节点
}
else if (IsCondition(aObject))
{
return GetConditionProperty(aObject, "NodeMessage.isLastNode");//当前节点是否为最后一个节点
}
else if (IsInstruction(aObject))
{
return GetInstructionProperty(aObject, "NodeMessage.isLastNode");//当前节点是否为最后一个节点
}
return false;
}
private void SetCurrentProperties(IFlowObject aObject)//刷新获取到的数值
{
cur_flowObject = aObject;
cur_startDelay = GetUnknownFloat(aObject, "DiaMessage.startDelay");//当前语段的初始延迟
cur_branchDelay = GetUnknownFloat(aObject, "DiaMessage.branchDelay");//当前语段的分支延迟
cur_passDelay = GetUnknownFloat(aObject, "DiaMessage.passDelay");//当前语段的跳过分支延迟的延迟
cur_wordSpeed = GetUnknownInt(aObject, "DiaMessage.wordSpeed");//当前语段的文字动画速度
cur_isLastNode = GetIfIsLastNode(aObject);//判断是否是一句话最后一个节点
startDelayTimerMax = cur_startDelay;//延迟最大值计算
}

//——————————————————————————————————————————————————————————————————————————

public void OnBranchesUpdated(IList<Branch> aBranches)//必须实现的接口:当进入分支选项时
{
ClearAllBranches();//清理所有之前的分支
cur_branches = aBranches;//当前分支
initBranchTimerMax = cur_startDelay + cur_branchDelay;//延迟最大值
passDelayTimerMax = cur_passDelay;
//
isInitBranch = true;//开始执行
}
private void OnBranchesUpdateAlt(IList<Branch> aBranches)//接口第二部分
{
cur_branchButtons = new List<Button>();
foreach (var branch in aBranches)//遍历所有当前分支
{
if (!branch.IsValid && !showFalseBranches) continue;//当分支不合法时,放弃生成该分支
var btn = Instantiate(branchPrefab, rTrans_branchParent);//生成一个按钮
var branchBtn = btn.GetComponent<ArticyBranch>();//获取按钮的ArticyDebugBranch自定义组件
if (branchBtn == null) { branchBtn = btn.AddComponent<ArticyBranch>(); }//没有就加一个
branchBtn.AssignBranch(flowPlayer, branch, cur_isLastNode);//将按钮与分支相连
if (cur_isLastNode)//如果是最后一个节点
{
branchBtn.GetComponent<Button>().onClick.AddListener(
() => isPlaying = false);//设置按钮点击后,isPlaying设置为false
branchBtn.GetComponent<Button>().onClick.AddListener(
() => gameObject.SetActive(false));//设置按钮点击后,关闭本UI对话
}
cur_branchButtons.Add(branchBtn.GetComponent<Button>());
}
}
//
private void UpdateBranchTimer()//根据计时器来控制选项的生成
{
if (isInitBranch == true)
{
if (passDelayTimer < passDelayTimerMax) { passDelayTimer += Time.deltaTime; }//计时器
else
{
passDelayTimer = 0;
isPassDelay = true;
}
//
if (isPassDelay)
{
if (Input.GetKeyDown(key_forceJumpDelay))//如果干预
{
initBranchTimer = initBranchTimerMax;//直接跳过
}
}
//
if (initBranchTimer < initBranchTimerMax) { initBranchTimer += Time.deltaTime; }//计时器
else
{
initBranchTimer = 0;
OnBranchesUpdateAlt(cur_branches);//执行接口第二部分:生成分支选项
isInitBranch = false;//退出执行
isPassDelay = false;//干预完毕
}
}
}
#endregion
//——————————————————————————————————————————————————————————————————————————
#region 原型基础函数
private void ClearAllBranches()//清理所有之前的分支
{
foreach (Transform child in rTrans_branchParent)
{
Destroy(child.gameObject);
}
}
private void ExtractCurrentPausePreviewImage(IFlowObject aObject)//显示此时的节点预览图
{
IAsset articyAsset = null;//声明资源文件
img_previewImage.sprite = null;
//
var dlgSpeaker = aObject as IObjectWithSpeaker;//使用接口的方法获取节点的演讲人
if (dlgSpeaker != null)
{
ArticyObject speaker = dlgSpeaker.Speaker;//获取演讲人
if (speaker != null)
{
var speakerWithPreviewImage = speaker as IObjectWithPreviewImage;//使用接口的方法获取节点的演讲人的预览图
if (speakerWithPreviewImage != null)//演讲人的预览图不为空时
{
articyAsset = speakerWithPreviewImage.PreviewImage.Asset;//为资源文件赋值
}
}
}
if (articyAsset == null)//资源文件为空时
{
var objectWithPreviewImage = aObject as IObjectWithPreviewImage;//使用接口的方法获取节点的"该节点"的预览图,而不是演讲人的
if (objectWithPreviewImage != null)
{
articyAsset = objectWithPreviewImage.PreviewImage.Asset;//为资源文件赋值
}
}
if (articyAsset != null)//资源文件不为空时
{
img_previewImage.sprite = articyAsset.LoadAssetAsSprite();//设置预览图图片文件
}
}
public void JumpToFlow(ArticyRef reference)//直接跳到某个确切的节点开始播放
{
SetCurrentProperties(reference.GetObject());//刷新主要数值
flowPlayer.StartOn = reference.GetObject();
isPlaying = true;
}
//
[Button("跳到开始(只能在运行中执行)", ButtonSizes.Medium), BoxGroup("流程控制", true, true)]
public void JumpToStart()//直接跳到开始
{
JumpToFlow(startOnFlow);//设置开始节点
}
#endregion
//——————————————————————————————————————————————————————————————————————————
#region 获取信息Samplenarrative
//"using Articy.Samplenarrative;"需要与AD工程导入的生成代码的实际命名空间为主,需要及时修改,不然会报错
//是否属于FlowFragment类
private static bool IsFlowFragment(IFlowObject aObject) { return (aObject as IArticyObject) as FlowFragment; }
//获取自动生成的FlowFragment类的物体特性属性,例如DiaMessage.startDelay
private static ScriptDataProxy GetFlowFragmentProperty(IFlowObject aObject, string aFeatureProperty)
{ return ((aObject as IArticyObject) as FlowFragment).getProp(aFeatureProperty); }

//是否属于Dialogue类
private static bool IsDialogue(IFlowObject aObject) { return (aObject as IArticyObject) as Dialogue; }
//获取自动生成的Dialogue类的物体特性属性,例如DiaMessage.startDelay
private static ScriptDataProxy GetDialogueProperty(IFlowObject aObject, string aFeatureProperty)
{ return ((aObject as IArticyObject) as Dialogue).getProp(aFeatureProperty); }

//是否属于DialogueFragment类
private static bool IsDialogueFragment(IFlowObject aObject) { return (aObject as IArticyObject) as DialogueFragment; }
//获取自动生成的DialogueFragment类的物体特性属性,例如DiaMessage.startDelay
private static ScriptDataProxy GetDialogueFragmentProperty(IFlowObject aObject, string aFeatureProperty)
{ return ((aObject as IArticyObject) as DialogueFragment).getProp(aFeatureProperty); }

//是否属于Hub类
private static bool IsHub(IFlowObject aObject) { return (aObject as IArticyObject) as Hub; }
//获取自动生成的Hub类的物体特性属性,例如DiaMessage.startDelay
private static ScriptDataProxy GetHubProperty(IFlowObject aObject, string aFeatureProperty)
{ return ((aObject as IArticyObject) as Hub).getProp(aFeatureProperty); }

//是否属于Jump类
private static bool IsJump(IFlowObject aObject) { return (aObject as IArticyObject) as Jump; }
//获取自动生成的Jump类的物体特性属性,例如DiaMessage.startDelay
private static ScriptDataProxy GetJumpProperty(IFlowObject aObject, string aFeatureProperty)
{ return ((aObject as IArticyObject) as Jump).getProp(aFeatureProperty); }

//是否属于Condition类
private static bool IsCondition(IFlowObject aObject) { return (aObject as IArticyObject) as Condition; }
//获取自动生成的Condition类的物体特性属性,例如DiaMessage.startDelay
private static ScriptDataProxy GetConditionProperty(IFlowObject aObject, string aFeatureProperty)
{ return ((aObject as IArticyObject) as Condition).getProp(aFeatureProperty); }

//是否属于Instruction类
private static bool IsInstruction(IFlowObject aObject) { return (aObject as IArticyObject) as Instruction; }
//获取自动生成的Instruction类的物体特性属性,例如DiaMessage.startDelay
private static ScriptDataProxy GetInstructionProperty(IFlowObject aObject, string aFeatureProperty)
{ return ((aObject as IArticyObject) as Instruction).getProp(aFeatureProperty); }

//获取陌生节点的属性,限制节点于FlowFragment\Dialogue\DialogueFragment\Hub\Jump\Condition\Instruction之间,其他类返回空
private int GetUnknownInt(IFlowObject aObject, string aFeatureProperty)
{
if (IsFlowFragment(aObject)) { return GetFlowFragmentProperty(aObject, aFeatureProperty); }
else if (IsDialogue(aObject)) { return GetDialogueProperty(aObject, aFeatureProperty); }
else if (IsDialogueFragment(aObject)) { return GetDialogueFragmentProperty(aObject, aFeatureProperty); }
else if (IsHub(aObject)) { return GetHubProperty(aObject, aFeatureProperty); }
else if (IsJump(aObject)) { return GetJumpProperty(aObject, aFeatureProperty); }
else if (IsCondition(aObject)) { return GetConditionProperty(aObject, aFeatureProperty); }
else if (IsInstruction(aObject)) { return GetInstructionProperty(aObject, aFeatureProperty); }
return 0;
}
private float GetUnknownFloat(IFlowObject aObject, string aFeatureProperty)
{
if (IsFlowFragment(aObject)) { return GetFlowFragmentProperty(aObject, aFeatureProperty); }
else if (IsDialogue(aObject)) { return GetDialogueProperty(aObject, aFeatureProperty); }
else if (IsDialogueFragment(aObject)) { return GetDialogueFragmentProperty(aObject, aFeatureProperty); }
else if (IsHub(aObject)) { return GetHubProperty(aObject, aFeatureProperty); }
else if (IsJump(aObject)) { return GetJumpProperty(aObject, aFeatureProperty); }
else if (IsCondition(aObject)) { return GetConditionProperty(aObject, aFeatureProperty); }
else if (IsInstruction(aObject)) { return GetInstructionProperty(aObject, aFeatureProperty); }
return 0.0f;
}
private string GetUnknownString(IFlowObject aObject, string aFeatureProperty)
{
if (IsFlowFragment(aObject)) { return GetFlowFragmentProperty(aObject, aFeatureProperty).ToString(); }
else if (IsDialogue(aObject)) { return GetDialogueProperty(aObject, aFeatureProperty).ToString(); }
else if (IsDialogueFragment(aObject)) { return GetDialogueFragmentProperty(aObject, aFeatureProperty).ToString(); }
else if (IsHub(aObject)) { return GetHubProperty(aObject, aFeatureProperty).ToString(); }
else if (IsJump(aObject)) { return GetJumpProperty(aObject, aFeatureProperty).ToString(); }
else if (IsCondition(aObject)) { return GetConditionProperty(aObject, aFeatureProperty).ToString(); }
else if (IsInstruction(aObject)) { return GetInstructionProperty(aObject, aFeatureProperty).ToString(); }
return string.Empty;
}
#endregion
//——————————————————————————————————————————————————————————————————————————
#region 功能函数
public static float Mapping(float val, float min1, float max1, float min2, float max2)//MAPPING变量映射
{
return ((val - min1) * (max2 - min2) + min2 * (max1 - min1)) / (max1 - min1);
}
#endregion
}

脚本ArticyBranch内容

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using Articy.Unity;
using Articy.Unity.Interfaces;
using UnityEngine.UI;
using TMPro;

public class ArticyBranch : MonoBehaviour
{
private TextMeshProUGUI dialogText;//对话显示UI
private Branch branch;//分支
private ArticyFlowPlayer processor;//流程播放器
//
public void AssignBranch(ArticyFlowPlayer aProcessor, Branch aBranch, bool isLastNode)//被外部调用,为分支赋值
{
GetComponentInChildren<Button>().onClick.AddListener(OnBranchSelected);//获取该物体或其子集身上的按钮组件,实现触发接口
//
dialogText = GetComponentInChildren<TextMeshProUGUI>();//获取该物体或其子集身上的文字组件
branch = aBranch;//分支信息由外部获取
processor = aProcessor;//流程播放器由外部获取
//
var target = aBranch.Target;//分支的目标
dialogText.text = "";//初始化文字
//
var menuText = target as IObjectWithMenuText;//使用接口的方法获取节点分支菜单文字
//
if (menuText != null)//如果菜单文字存在
{
dialogText.text = menuText.MenuText;//设置菜单文字
//
if (dialogText.text == "")//如果菜单文字为空字符串
{
var txtObj = menuText as IObjectWithText;//使用接口的方法获取节点文字
if (txtObj != null)//当节点文字存在时
dialogText.text = txtObj.Text;//显示节点文字作为选项菜单文字
else
dialogText.text = "...";
}
}
//
if (dialogText.text == "")//如果菜单文字仍然空置,显示节点名
{
var dspObj = target as IObjectWithDisplayName;//使用接口的方法获取节点名
if (dspObj != null)//当节点名不为空时
dialogText.text = dspObj.DisplayName;//显示节点名
else
{
var articyObject = target as IArticyObject;
if (articyObject != null)
dialogText.text = articyObject.TechnicalName;
else
{
dialogText.text = target == null ? "null" : target.GetType().Name;
}
}
}
//
GetComponent<RectTransform>().sizeDelta = new Vector2(GetComponent<RectTransform>().sizeDelta.x, dialogText.preferredHeight + 50);
}
//
public void OnBranchSelected()//当按钮被触发时
{
processor.Play(branch);//流程播放器播放分支,进入下一条
}
}

脚本ArticyInitTrigger内容

  • 脚本功能介绍:这个脚本可有可无,主要在实现当某个游戏物体(特别是玩家)进入触发器时能够从头开始播放。

  • 点击查看(如果点不开就复制下面的代码吧)

  • flowPlayer:所绑定的ArticyFlowPlayer播放器组件

  • tagConstraint:触发器检测到当拥有限制的标签的碰撞箱时,开始触发流程

  • isDestroyWhenPlayed:当流程结束时,是否删除这一切

  • onTriggered:当触发的那一瞬间所执行的委托

  • onFinished:当播放结束的那一瞬间所执行的委托

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using Sirenix.OdinInspector;
using Articy;
using Articy.Unity;
using UnityEngine.Events;

[System.Serializable]
public class ADTriggerEvent : UnityEvent<string> { }//AD触发器事件委托

[RequireComponent(typeof(Collider))]
public class ArticyInitTrigger : MonoBehaviour
{
[BoxGroup("组件信息", true, true)]
[SerializeField] public ArticyFlowPlayer flowPlayer;//流程播放器

[BoxGroup("流程信息", true, true)]
[SerializeField] public string tagConstraint = "Player";//标签限制

[BoxGroup("流程信息", true, true)]
[SerializeField] public bool isDestroyWhenPlayed;//是否在流程播放完后删除

[BoxGroup("流程委托", true, true)]
[SerializeField] public bool useEventOnTriggered = false;//是否使用触发时的委托

[BoxGroup("流程委托", true, true)]
[SerializeField] public bool useEventOnFinished = false;//是否使用结束时的委托

[BoxGroup("流程委托", true, true), ShowIf("useEventOnTriggered")]
[SerializeField] public ADTriggerEvent onTriggered;//触发时

[BoxGroup("流程委托", true, true), ShowIf("useEventOnFinished")]
[SerializeField] public ADTriggerEvent onFinished;//结束时

private ArticyFlowManager flowManager;
private bool tempIsPlay = false;

void Start()
{
flowManager = flowPlayer.GetComponent<ArticyFlowManager>();
}

void Update()
{
if (tempIsPlay == true && flowManager.isPlaying == false)//当两个数值不一样时
{
OnFinished();
}
tempIsPlay = flowManager.isPlaying;//暂存
}

private void OnFinished()//当结束时
{
onFinished.Invoke("");
if (isDestroyWhenPlayed) { Destroy(gameObject); }
}

private void OnTriggerEnter(Collider other)
{
if (other.CompareTag(tagConstraint))//如果是玩家
{
flowPlayer.gameObject.SetActive(true);
flowPlayer.GetComponent<ArticyFlowManager>().JumpToStart();
onTriggered.Invoke("");//触发
}
}
}

Animatext模板(有插件的情况下)

  • 在Project工程目录里右键新建Animatext Preset->Transition-Word->Step->01
  • 如下图设置,并赋予给所有AnimatextTMProOld组件上。