我们在之前已经介绍过了如何创建自己的第一个电池,其中里面提到了我们制作的大部分电池都需要至少完成下面三个部分的代码
RegisterInputParams
RegisterOutputParams
SolveInstance
其中 RegisterInputParams
和 RegisterOutputParams
是用来声明电池的输入和输出的两个部分,重要程度不言而喻,本文我们就来看看他们俩到底是什么。
进一步认识RegisterInputParam和RegisterOutputParams
Params是Parameters的简写,即“参数”,这两个函数的名字也十分的直观 “Register Input Parameters” 和 “Register Output Parameters”,就是我们需要把电池需要处理的输入和输出参数进行注册。
不过凡是都有个为什么,我们首先关注的问题是“为什么需要多出来这两个函数?”,直接用 SolveInstance
接收传递参数不是更直观吗?要回答这个问题,我们需要来看看Grasshopper电池是背后的架构是什么样的。前面已经提到过,我们所有的自定义电池都是继承自 GH_Component
,而下图展示了一个它的类图。
类图的每一个框代表着一个类,框之间的箭头代表着继承和派生关系,框里面被分割成三个部分,最上面为类名,中间为该类包含的属性,最下面为该类包含的方法。从上面的类图可以看出,GH_Component
是直接继承自 GH_ActiveObject
。
由于GH所特有的数据树结构(GH_Structure
以及 DataTree
)存在,每个GH电池内部对数据的匹配和处理逻辑会变得十分复杂,其中包括一系列问题,例如数据列表与数据树的分支要如何进行数据匹配、数据树与数据树之间要如何匹配等。举个例子,例如下图中,通过两个点构造直线的电池,在输入端分别对应数据列表和数据树的情况下,就会出现不同的结果。
我们还可以通过把鼠标悬停在电池图标的中间来知道这个电池被运行了几次(SolveInstance
被Call了多少次),图中可以看出,靠上方的输入端均为数据列表的电池运行了3次,而靠下方的输入端一个为数据列表另一个为数据树的电池运行了9次。
GH电池需要一个统一的管理数据匹配的逻辑才能保证所有电池以同一种方式运行,那如果将这种对数据逻辑的管理完全交给我们电池的开发者,那必然会导致两个后果:1)开发者的学习成本变高,我们不但要学习专注于数据处理的逻辑,还要为GH额外学习一部分的数据匹配逻辑;2)GH的用户体验变差,电池的质量参差不齐,每个电池对于数据匹配的逻辑很可能都是不一样的,对于用户来说,这就是一个灾难,在使用电池之前还需要“猜”一下。
因此GH在每个电池内部均内置了一套管理数据的类,即 GH_ComponentParamServer
。这样,作为开发者,无需纠结数据树、数据列表的匹配问题,可以专心设计电池的核心部分——SolveInstance
,仅需将电池的输入、输出端口在这个数据管理类中注册即可。
此外,GH_ComponentParamServer
也会包含一些UI相关的处理,例如输入端和输出端的数量增加会导致电池在画布上显得更大,电池没有输出端则会以锯齿状显示电池的右端(参考 Custom Preview 电池,下图右侧电池)等等情况。
每个电池实例中均包含一个 GH_ComponentParamServer
实例,而每个 GH_ComponentParamServer
包含一个 GH_InputParamManager
实例和一个 GH_OutputParamManager
实例,分别对应管理输入端的参数和输出端的参数。
我们在 RegisterInputParams
和 RegisterOutputParams
函数中可以看到一个传参为对应的 [Input|Output]ParamManager
。GH主线程在构造我们电池时,会调用这两个函数,并传入对应的实例。我们通过这个实例的一些方法,就可以实现在GH电池上添加输入/输出端了。常用的一些方法包含
AddBooleanParameter
AddCurveParameter
AddIntegerParameter
AddLineParameter
AddNumberParameter
AddTextParameter
大部分的方法都可以通过我们需要添加的输入/输出量的C#类名来确定,例如我们需要处理 Curve(曲线)类的参数,则使用 AddCurveParameter
,处理 Line(直线)类的参数则使用 AddLineParameter
。
有几个特殊的例子值得注意:
double
和float
所代表的浮点数,在GH中会统一使用AddNumberParameter
来添加,且此时底层会使用double
来保存该值string
字符串类型在GH会使用AddTextParameter
来添加,此时底层对数据的存储仍然是string
类型,似乎这里只是一个名字的改变
下面就是一个例子,我们在输入端添加了3个输入值,分别是布尔值、直线以及浮点数,而输出端则是一个整数值。而且当我们试图将一个曲线接入电池的直线输入端时,电池会直接报错,并提示错误信息“无法将曲线转换为直线”。
protected override void RegisterInputParams(GH_Component.GH_InputParamManager pManager)
{
pManager.AddBooleanParameter("布尔", "BOOL", "一个布尔值", GH_ParamAccess.item);
pManager.AddLineParameter("直线", "LINE", "一个直线", GH_ParamAccess.item);
pManager.AddNumberParameter("浮点数", "NUM", "一个浮点数值", GH_ParamAccess.item);
}
protected override void RegisterOutputParams(GH_Component.GH_OutputParamManager pManager)
{
pManager.AddIntegerParameter("整数", "INT", "一个整数值", GH_ParamAccess.item);
}
设置某输入参数为可选
在我们不接入任何数据的时候,电池也会提示我们数据输入口没有数据接入,且电池的 SolveInstance
也不会运行,从而提供某种程度上的数据验证功能,如下图所示。
我们当然可以选择不使用这种数据验证,将一个或某个电池的输入端数据为“可选”参数 —— 即该数据端口可不接入数据,电池仍可正常运作。
Grasshopper电池的数据管理类的底层是通过一个列表来存储我们添加的各个参数的,每个参数都是最终继承于 GH_Param
父类。我们可以通过使用列表获取到每个被添加的参数,然后将参数的 Optional
属性设置成 true 就可以实现该参数在不被连入数据时电池也可正常运作。具体看下列代码及结果演示,电池在 直线 和 浮点数 两个输入参数端口即便没有数据连入仍可正常工作:
protected override void RegisterInputParams(GH_Component.GH_InputParamManager pManager)
{
pManager.AddBooleanParameter("布尔", "BOOL", "一个布尔值", GH_ParamAccess.item);
pManager.AddLineParameter("直线", "LINE", "一个直线", GH_ParamAccess.item);
pManager.AddNumberParameter("浮点数", "NUM", "一个浮点数值", GH_ParamAccess.item);
pManager[1].Optional = true;
pManager[2].Optional = true;
}
为输入参数设置默认值
除了将参数设为“可选”之外,另一种可以让电池的某些输入端口不需要数据接入就可以工作的方法就是为参数指定默认值。电池在没有数据接入时就会自动采用默认值,Grasshopper原生电池的 Series 电池(生成等距数列)就使用了默认值。
值得注意的是,设置参数默认值与设置参数“可选”不同的是,即便电池的参数设置了默认值,用户仍然可以通过右键单击该参数,将默认值删除,此时电池将不会工作。
默认值可以在注册参数时设置,比如下列代码就可以将前文例中的电池的 布尔 参数设置一个默认值为 false。
pManager.AddBooleanParameter("布尔", "BOOL", "一个布尔值", GH_ParamAccess.item, false);
或者是添加一个默认值列表,这样可以将默认值设置成一个 list 输入。下例中的代码将默认值设置为一个列表。
IEnumerable<bool> boolList = new List<bool>{ true, false, true };
pManager.AddBooleanParameter("布尔", "BOOL", "一个布尔值", GH_ParamAccess.item, boolList);
其他参数属性
Grasshopper电池中的输入/输出参数对应的基类 GH_Param
除了上一节当中说提到的 Optional
属性之外,还有下列属性是常用到的
Name
Nickname
Description
Access
DataMapping
VolatileData
其中,Name
、Nickname
和Description
顾名思义,就是这个参数的一些描述性信息,在注册参数的时候也可以指定,也可以通过直接修改这些属性值进行再定义和修改。
Access
属性则决定了电池的SolveInstance
被调用时,会以什么样的方式来获取到连入的数据或者传出数据。Access
属性也是一个在注册参数时的一个必填项,其值为一个枚举类 GH_ParamAccess
,共有3个值,分别是 item、 list 和 tree。它们的具体的区别会在本文最后介绍。
DataMapping
属性可以改变参数的数据结构,它提供Flatten
、Graft
和None
这三个枚举类值可以赋予,分别对应将参数内的数据进行对应的数据结构改变。这里的改变与我们在正常使用Grasshopper时,在电池的输入/输出端通过右键点击每个参数,改变数据结构时完全相同。
VolatileData
属性则是可以直接以数据树形式获取该输入/输出参数的数值,这在某些特定场景十分有用,尤其是当我们不希望使用Grasshopper默认的数据管理模式的时候,通过VolatileData
属性可以直接获取到该参数对应的输入值或者输出值。
ParamAccess与数据结构
前文提到,Grasshopper电池中的每个输入/输出都对应有一个Access
属性,该属性是一个枚举类型的值,一共有三个值(item、list 和 tree)可选择,每个值对应的作用如下:
- item: 每次电池内
SolveInstance
函数被执行时,从该参数输入/输出端操作数据时,以每个参数的实例来进行操作 - list: 每次电池内
SolveInstance
函数被执行时,从该参数输入/输出端操作数据时,以封装参数实例的列表来进行操作 - tree: 每次电池内
SolveInstance
函数被执行时,从该参数输入/输出端操作数据时,以封装参数实例的数据树来进行操作
上面的文字描述读起来不是特别直观,下面就来用一个图来说明他们的区别
图中构造了一个简单的数据树,该数据树一共有三个树枝,第一个树枝中有三个实例,第二个树枝中有两个实例,第三个树枝中有四个实例,共计九个实例存储于该数据树中。此时,如果将该数据树连入某电池的输入端,当电池的输入端的参数具备不同的Access
属性时,该电池将由如下不同的执行逻辑:
- item:电池的
SolveInstance
将会被执行 9 次,且每次执行时,从该输入端获取数据时,将按实例来获取:第一次执行时,SolveInstance
将获得object1,第二次执行时,SolveInstance
将获得object2,以此类推 - list:电池的
SolveInstance
将会被执行 3 次,因为该数据树一共有3个树枝。每次执行时,将按树枝提取这个树枝上所有的实例,并按照列表方式读入:第一次执行时,SolveInstance
将获取到 List<object>{ object0, object1, object2 },第二次执行时,SolveInstance
将获取到 List<object>{ object3, object4 },以此类推,直至所有树枝被处理完毕 - tree:电池的
SolveInstance
将仅被执行 1 次,且数据树将会被完整地传入。此时,获取的数据与直接使用VolatileData
获取得到的数据树相同。
下图就是分别设置输入端为不同的 GH_ParamAccess
值时,电池所对应的不同的表现。