撰写了文章 更新于 2020-04-16 01:49:20
Articy Importer Guide - 03 对话与流程遍历
03 对话与流程遍历
articy:draft将能够让你利用各样的节点(node)和连接,创造(用于故事线的)流程(flow)。你可以利用该功能创建你的对话;你的菜单框架,你的技能树,甚至包括AI状态机在内的你能想到的一切。但是创建流程图只是整体工作的一部分,articy:draft当中,你可以用演示模式(Presentation view)执行你的流程图。演示模式将会根据引脚(pins)和连接,跟着你的流程,像PowerPoint一样展示你的节点。
articy:draft enables you to create flows using different node types and connecting them. You can use this to create your dialogues; your menu structure; your tech trees even your AI behaviour or anything else you can imagine. But creating your flows is only one part of the work, in articy:draft you can execute those flows using the Presentation view. The presentation view will follow your flow, guide by pins and connections and showing you relevant nodes in a PowerPoint®-like view.
如果在unity当中你需要演示模式之类自动走完流程的功能,你需要用到ArticyFlowPlayer组件。
If you need something similar to the presentation view that walks automatically through your flow charts in unity you use the ArticyFlowPlayer component.
本篇包含以下内容:
This topic contains the following sections:
流程遍历基础介绍
Basic introduction to flow traversal
在开始介绍如何使用ArticyFlowPlayer之前,我们需要向您解释一些基础概念,以及明确一些定义。
Before we look at how you can work with the ArticyFlowPlayer we have to explain some fundamental concepts and clarify some wordings.

流程遍历的过程中有一些重要的概念,需要牢记于心:
The process of flow traversal has a few parts that are important to keep in mind:
- 节点 Nodes: 所有可以在articy:draft的flow功能中放置的对象。如FlowFragment(流程片段),对话(Dialogue),集线器(Hub)等。
Every articy:draft object that can be placed in the flow. e.g FlowFragment, Dialogue, Hub etc. - 引脚 Pin: 多数的流程对象都有两个预设的引脚,或者是可调整的多个引脚。节点左侧的引脚被叫做输入引脚,右侧则称为输出引脚。
Most flow objects have 2 predefined or dynamic list of pins. Pins on the left side of the node are called InputPins while pins on the right side of a node are called OutputPins. - 连接 Connections: 两个节点之间的连线。连接总是会同引脚连接,从输出连到输入。一个引脚可以有多个连接。
The cable between two nodes. A Connection always is connected via pins and always from one output pin to one input pin. A pin can have more than one connection. - 脚本 Scripts: 每个引脚和特定的条件(Condition)与指令(Instruction)节点能够容纳脚本代码,可用于设计师操作流程执行。
Every pin and the two specialized nodes Condition and Instruction are capable of containing script code, that the designer can use to manipulate the flow execution.
遍历是什么意思?
当我们谈及遍历(Taversal)时,意思是在对象的框架和连接引导下,半自动地访问流程中的每个对象。
When we talk about traversal we mean the semi-automatic process of visiting each object in our flow guided by the structure and connection of our objects.
让我们设想两个人之间的对话。每个人都说一句话,A说完之后B说。这种情况下,句子的图形化就叫做对话片段(DialogueFragment)。谁第一个说和谁第二个说则需要用到引脚和连接。这时的遍历只需要一个开始节点,然后顺着节点、引脚和连接走下去。这样想象起来还挺简单的,特别是只考虑线性流程时。但游戏是交互的,这就为我们带来一个新概念:
Think of a dialogue between two people. Each Person saying one sentence, first person A says something then person B. The sentences in this case would be diagrammed using DialogueFragments. The order of who speaks first; who second is built using pins and connections. The process of traversal now just needs a start node and automatically flows along the nodes, pins and connections. Imagining this for a simple flow like this is easy, especially considering linear flows. But games are interactive which brings us to another important concept:
分支 Branching
分支本身没什么特殊的:它是流程中的一个分支,可以分出两条或更多条潜在路线。构建非线性游戏时,玩家会有很多不同的选择,自然也会产生很多所谓的分支。我们会讨论分支多一些,它在ArticyFlowPlayer中起着重要作用。
Branching itself is nothing special: Its a fork in the flow, where two or more potential routes could be taken. When you are building a non-linear game, where the player has alot of different choices to make you will naturally have a lot of these so called branches. We will talk about branches more because they play an important part in the way how to work with the ArticyFlowPlayer.
设置ArticyFlowPlayer组件
Setup ArticyFlowPlayer component
ArticyFlowPlayer组件旨在为我们在流程遍历方面减轻繁重的工作。让我们看看如何使用它:
The ArticyFlowPlayer component is built to do all the heavy lifting for us in regards of flow traversal. Lets see how we can use it:
如一切unity组件,我们的第一步需要将其附加到scene中的对象
Like every other unity component we have to attach it first to one of our game objects in the scene


尽管流程播放器会尝试自动执行尽可能多的操作,但我们仍希望它将某些特定事情通知给我们。比如当我们建构对话时,如果流程播放器只是沿流程移动并走过每个对话片段,却没有将其告知我们,那就是没用的。所以我们要提出一个新概念,暂停(pause)。
While the flow player tries to do as much as possible automatically, we want it to notify us about certain things. For example when we are building a dialog, it is of no use to us if the flow player just walks the flow and passing every dialogue fragment without telling us about it. So lets talk about a new concept called pause.
暂停 Pause
暂停的目标代表了我们感兴趣的对象,意味着流程播放器会如预期的中断其自动遍历,并通知我们。举例来说:如果我们正在建构一个系统,想在UI中显示对话,就要在选择的流程中遇到对话片段(DialogueFragment)时获得提醒。
A pause target defines what objects we are interested in which means we want the flow player to interrupt its automatic traversal and notify us. For example: If we are building a system that shows dialogues in our UI we have to get notified about encountered DialogueFragments along our chosen flow.

值得一提的是:当流程播放器暂停时,它会无期限地等待我们使用其中一种Play方法。稍后再讲更详细的。
As a sidenote: When the flow player pauses, it will wait indefinitely until we use one of the Play methods. More on that later.
但我们该如何获得一个暂停的提醒?
But how do we get notified about a pause?执行回调 Execution Callbacks
如果你此前了解过C#接口,或者习惯于使用unity的新Event系统,这部分就很简单。但如果没有,也不用担心!
If you have worked with C# interfaces before or are used to unitys new Event system, this part is easy, if you don't: Don't worry!
首先我们需要在此前加入了流程播放器的对象里,添加一个新的脚本。
First lets attach a new script to the same game object with our flow player component.

在你的IDE里面,将看到如下代码:
Depending on how you setup unity your favorite IDE should open up, showing our new script file:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class MyDialogueHandler : MonoBehaviour {
// Use this for initialization
void Start ()
{
}
// Update is called once per frame
void Update ()
{
}
}
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using Articy.Unity;
public class MyDialogueHandler : MonoBehaviour, IArticyFlowPlayerCallbacks
{
// Use this for initialization
void Start ()
{
}
// Update is called once per frame
void Update ()
{
}
public void OnFlowPlayerPaused(IFlowObject aObject)
{
}
public void OnBranchesUpdated(IList<Branch> aBranches)
{
}
}
事实上,这就是你要为设置unity流程遍历所做的所有事了。当然现在什么还都没发生,因为我们只建立了基础。现在,如何让流程播放器执行这两个方法就交给你了。在下一节里我们将给你一些相关的点子和引导。
And in fact, this is everything you need to setup your flow traversal in unity. Of course nothing is happening just yet, because we only built the foundation. It is now up to you and your goal with the flow player how you implement those two methods. In the next section we will give you some ideas and guidelines how to do it.
如何执行OnFLowPlayerPaused()
How to implement OnFlowPlayerPaused()
如此前所述,OnFlowPlayerPaused(IFlowObject)的目的是为了让ArticyFlowPlayer能够根据PauseOn的设定,提醒我们流程中有重要元素。
As mentioned before, the purpose of OnFlowPlayerPaused(IFlowObject) is for the ArticyFlowPlayer to notify us about something in the flow that was marked as important via the pauseOn setting.
让我们看看可对该方法做什么:
Lets see what we can do with this method:
public void OnFlowPlayerPaused(IFlowObject aObject) {
}
该方法的最重要目的是将实际参数aObject传递给它。它是流程播放器暂停的对象,但是其类型IFlowObject是新的。简单来说,IFlowObject是流程播放器在遍历时会遇到的任何对象的基本类型。也就是说,每个节点、引脚和连接都是IFlowObject类型的。任何情况下如果通过的aObject是空值,就意味着我们遇到了Dead End。
The most important thing about this method is the argument aObject passed into it. Not suprisingly it is the object that the flow player paused on, but its type IFlowObject is something new. To put it simple: IFlowObject is the basic type of anything the FlowPlayer can encounter while traversing.. That means every node, every pin and every connection is also an IFlowObject. If for some reason the passed aObject is null, we have encountered a Dead End.
那我们该怎样区分不同的类型?编写代码,就能够测试出传递的对象是否是我们感兴趣的类型。让我们看看如何做到这一点:
But how can we distinguish between the different types? By writing code that will test if the passed object is actually of the type we are interested in. Lets see how to do this:
public void OnFlowPlayerPaused(IFlowObject aObject)
{
var dlgFragment = aObject as DialogueFragment;
if(dlgFragment != null)
{
// 这里是一个DialogueFragment!
}
}
作为简单的示例,让我们使用DialogueFragment来更新一个Unity UI text 区域。
As a small example, lets update a Unity UI text fields with the spoken text of a DialogueFragment.
public UnityEngine.UI.Text descriptionLabel;
public void OnFlowPlayerPaused(IFlowObject aObject)
{
var dlgFragment = aObject as DialogueFragment;
if(dlgFragment != null)
{
descriptionLabel.text = dlgFragment.Text;
}
}
一些属性(例如Color,DisplayName,Text等)会出现在多种类型上。使用到目前为止所学的知识,我们将不得不编写如下代码
Some properties like Color, DisplayName, Text etc. appear on multiple types. Using what we have learned till now, we would have to write code like this
public UnityEngine.UI.Text descriptionLabel;
public void OnFlowPlayerPaused(IFlowObject aObject)
{
var dlgFragment = aObject as DialogueFragment;
if(dlgFragment != null)
{
descriptionLabel.text = dlgFragment.Text;
}
var flwFragment = aObject as FlowFragment;
if(flwFragment != null)
{
descriptionLabel.text = flwFragment.Text;
}
}
这将用到大量的复制和粘贴,会增加代码量,并产出难以维护的代码。因此,所有这些共享的属性都有一个特殊的接口,每个共享该属性的类型都具有此接口。我们称这些对象属性为接口。让我们修复前面的示例:
This would increase our code by a lot of copy and paste and creates difficult to maintain code. For that reason all of those shared properties have a special interface that every type shares that has this property. We call these object property interfaces. Lets fix the previous example:
public UnityEngine.UI.Text descriptionLabel;
public void OnFlowPlayerPaused(IFlowObject aObject)
{
var objWithText = aObject as IObjectWithText;
if(objWithText != null)
{
descriptionLabel.text = objWithText.Text;
}
}
Now as mentioned above how you implement this method is highly up to you and your use cases.
此处有些可供参考的建议:
Here are some ideas to get you going:
- 获取DialogueFragment的Speaker库,以更新当前Speaker的UI中的图像。
Access the Speaker asset of a DialogueFragment to update an image in your UI of the current speaker. - 在articy draft里面创建一个特殊的DialogueFragment模板。创建新的Feature并放一个Slot属性进去,将其限制为仅允许库资源(asset)。用这个属性为DialogueFragment绑定声音文件。在unity中可以获取到这些模板,也就可以或许并播放声音文件。要使对话与遍历同步,可以使用Invoke()传递一个延迟,该延迟与音频资产的长度一样长,并调用适当的Play方法或将不同的分支呈现给用户。
Create a special DialogueFragment template in articy draft. Create new Feature and place a Slot property in it, constrainted to only allow assets. Use this property to attach sound files to your DialogueFragment. In unity you can access these Templates, access the sound asset attached and play it. To synchronize the dialogue with the traversal you can use Invoke() passing a delay as long as the audio assets length and calling the appropriate Play method or presenting the different branches to the user. - 非对话也可以使用FlowPlayer。您可以让它遍历流程,在节点上暂停,执行某些操作并立即再次调用Play,而无需用户进行交互。可以像这样构建初级的AI。
The FlowPlayer can also be used for non dialogues. You could let it traverse the flow, pause on nodes, do something and immediately call Play again without the need for the user to interact. A primitive AI could be built like this.
如何执行OnBranchesUpdated()
How to implement OnBranchesUpdated()
OnBranchesUpdated(IList<Branch>)绝对会在OnFlowPlayerPaused(IFlowObject)之后被调用,用以告知继续遍历的可能路线。
The method OnBranchesUpdated(IList<Branch> ) is always called after OnFlowPlayerPaused(IFlowObject) to tell us about the potential paths to continue the traversal.
这是方法的本体:
Lets first look at the method itself:
public void OnBranchesUpdated(IList<Branch> aBranches)
{
}
分支 Branch
流程播放器暂停的时候会收集每一条即将到来的路径,放在一个列表中,并传入OnBranchesUpdated。换句话说,分支是一种对未来可能路线的展示,展示出流程接下来的不同去向。
When the flow player pauses it will gather every upcoming path, put them into a list and pass this list into OnBranchesUpdated. So in other words Branches are a look into the future showing us potential routes the flow player could continue the flow.
为了更好地说明,请考虑这三个DialogueFragment构成的小流程。
To give a clearer example, consider this small flow of 3 DialogueFragments.

In the example above we get 2 branches one branch to the upper DialogueFragment"Manfred: I don't know..." and another to the lower DialogueFragment"Manfred: I'll get you out,...". But now that we have these branches, what are they used for?
分支的作用主要有两个:第一,也是最重要的,取消流程暂停:
There are 2 main purposes for a Branch: The first and most important is a way to unpause the flow:
如何使用分支取消流程播放器的暂停。
How to unpause the flow player using a Branch.
前面提到,取消流程播放器的暂停,必须调用任一Play方法。但是让我们仔细看一下Play方法的一个特定的重载,其他重载只是为了方便,而此重载实际上是在后台使用。
As mentioned before, to unpause the flow player you have to call one of the Play methods. But lets look closer at one specific overload of the Play method as the other overloads are just convenience and in fact use this one under the hood.
Play(Branch): 这个方法会取消流程播放器的暂停,但是需要一个特定的Branch作为参数,以便获取流程播放器将会继续进行自动遍历的地方。换句话说,我们必须告诉流程播放器要播放哪个分支。
既然已经知道我们需要分支,这样才能选择从哪里继续流程,我们可以来了解分支的第二个目的了:保存关于该分支要到哪去的信息。
Now that we know that we need our Branches to choose where to continue the flow, we look at the second purpose of branches: Holding information where that branch leads to.
Branch对象包含有关遍历此路线(称为目标 Target)的下一个暂停的信息。举例来说,你可以用这个办法构建UI,显示所有即将到来的DialogueFragment的MenuTexts(菜单文字)。你还可以访问流程播放器在该路线(Path)之后会遇到的所有对象。当然也会有个标志告诉我们这个分支是否能够有效地遍历,稍后会详细讲解。
The Branch object contains informations about the next pause when traversing this route called Target. You can use this to build an UI showing the MenuTexts of all upcoming DialogueFragment for example. You also have access to all objects that the flow player would encounter following this Path. And of course a flag telling us if this branch is actually valid to traverse, more on that later.
像OnFlowPlayerPaused()一样,它高度依赖于你想要用它实现什么。虽然很难在一个小示例中展示出来,但还是让我们看看这段代码,作为概念,证明这两个回调可以一起工作。
Similar to the OnFlowPlayerPaused() method it is highly dependant on what you want to achieve with it. While difficult to show in a small example, lets check this piece of code as a proof of concept both callbacks work together.
using Articy.Unity;
using Articy.Unity.Interfaces;
using System.Collections.Generic;
using UnityEngine;
public class MyDialogueHandler : MonoBehaviour, IArticyFlowPlayerCallbacks
{
// 每次暂停,我们都会记住第一个分支
// 因此我们可以使用它继续流程
private Branch firstBranch;
public void OnFlowPlayerPaused(IFlowObject aObject)
{
// 我们将每个文本都打印到了控制台中
var textObject = aObject as IObjectWithText;
if (textObject != null)
{
Debug.Log(textObject.Text);
}
}
public void OnBranchesUpdated(IList<Branch> aBranches)
{
// 只需在每个暂停时存储第一个分支
if (aBranches.Count > 0)
firstBranch = aBranches[0];
}
// Update is called once per frame
void Update()
{
// 一旦流程播放器暂停,将持续等待到我们调用Play()后才继续,
// 这样我们就能有时间看完我们在控制台打印的内容,
// 最后我们则可以点击“空格”,让流程播放器恢复。
if (Input.GetKeyUp(KeyCode.Space))
{
if (firstBranch != null)
GetComponent<ArticyFlowPlayer>().Play(firstBranch);
}
}
}
总结如下:
So to summarize:
- 当流程播放器暂停时,会在OnFlowPlayerPaused()之后直接自动调用OnBranchesUpdated()。
When the flow player pauses, it will always call OnBranchesUpdated() directly after OnFlowPlayerPaused() - Branch是当前暂停时的潜在路线。
A Branch is one potential route from the current pause. - 使用Branch可以取消流程播放器的暂停,只需将它传给Play(Branch)。
Use a Branch to unpause the flow player by passing it into Play(Branch). - 构建多选项对话时,使用“分支”属性可向用户显示每个分支。
Use the Branches properties to present each branch to the user when building multiple choice dialogs.
流程播放器和编程
FlowPlayer and Scripting
幸运的是,流程播放器将负责执行遍历流程时遇到的所有条件和指令。但它是如何运转的?
Lucky for us, the flow player will take care of executing all the conditions and instructions encountered while traversing the flow. But how exactly does this work?
有2个不同的脚本元素,具有不同的用途
There are 2 distinct script elements with different purposes
- 输入引脚或条件用于检测一个分支在被流程播放器forecast时是否为可用的。
Input Pins or Conditions are used to test if a Branch is valid while the flow player forecasts it. - 输出引脚或指令用以调用响应传递此对象的脚本方法,或更改全局变量。
Output Pins or Instructions to change global variables or call script methods in response to passing this object.
输入引脚和条件 Input Pins and Conditions
When working with scripts in the flow, you usually want to ignore branches where the condition to follow that branch is false. The flow player will call all the necessary scripts and will store the result in the IsValid property of every branch. You just have to make sure to only work with branches where IsValid is true.
public void OnBranchesUpdated(IList<Branch> aBranches)
{
foreach(var branch in aBranches)
{
// 我们只想要可用(valid)的分支
if(!branch.IsValid) continue;
// ... 使用可用的分支
}
}
Instruction are even easier, because we have to do nothing. We only have to remember that instructions are only called when the object is passed. This means if we have paused on an OutputPin/Instruction the underlying script is not yet executed, we have to continue once and leave the pause to get it executed.
When working with GlobalVariables the flow player component has a handy view of all global variables. When you expand the foldout Variable default values you will find all your namespaces and their variables. You can even modify their values here for debug purposes.

注意 |
|---|
多个ArticyFlowPlayer将共享相同的默认全局变量实例。您可以取消选中“使用默认全局变量”选项,并提供一个新的GlobalVariables实例。 |

注意