小编的大兄弟在之前已经介绍过了如何创建自己的第一个电池(详见文章《制作你的第一个grasshopper电池》,另外有关于Grasshopper的可视化探索《Grasshopper可视化编程初步》),其中里面提到了我们制作的大部分电池都需要至少完成下面三个部分的代码
-
RegisterInputParams
-
RegisterOutputParams
-
SolveInstance
其中RegisterInputParams和RegisterOutputParams是用来声明电池的输入和输出(I/O)的两个部分,重要程度不言而喻,本文我们就来看看他们俩到底是什么。
进一步认识RegisterInputParam和RegisterOutputParams
Params是Parameters的简写,即“参数”,这两个函数的名字也十分的直观RegisterInputParameters和RegisterOutputParameters,就是我们需要把电池需要处理的输入和输出参数进行注册。
不过凡是都有个为什么,我们首先关注的问题是“为什么需要多出来这两个函数?”,直接用SolveInstance接收传递参数不是更直观吗?要回答这个问题,我们需要来看看Grasshopper电池背后的架构是什么样的。前面已经提到过,我们所有的自定义电池都是继承自GH_Component。
由于GH所特有的数据树结构(GH_Structure以及DataTree)存在,每个GH电池内部对数据的匹配和处理逻辑会变得十分复杂,其中包括一系列问题,例如数据列表与数据树的分支要如何进行数据匹配、数据树与数据树之间要如何匹配等。举个例子,例如下图中,通过两个点构造直线的电池,在输入端分别对应数据列表和数据树的情况下,就会出现不同的结果。
我们还可以通过把鼠标悬停在电池图标的中间来知道这个电池被运行了几次(SolveInstance被执行了多少次),图中可以看出,靠上方的输入端均为数据列表的电池运行了3次,而靠下方的输入端一个为数据列表另一个为数据树的电池运行了9次。
GH电池需要一个统一的管理数据匹配的逻辑才能保证所有电池以同一种方式运行,那如果将这种对数据逻辑的管理完全交给我们电池的开发者,那必然会导致两个后果:
-
开发者的学习成本变高,我们不但要学习专注于数据处理的逻辑,还要为GH额外学习一部分的数据匹配逻辑;
-
GH的用户体验变差,电池的质量参差不齐,每个电池对于数据匹配的逻辑很可能都是不一样的,对于用户来说,这就是一个灾难,在使用电池之前还需要“猜”一下。
因此GH在每个电池内部均内置了一套数据管理的类,仅仅用来负责处理在电池背后的数据输入/输出,即GH_ComponentParamServer。这样,作为开发者,无需纠结数据树、数据列表的匹配问题,可以专心设计电池的核心部分——SolveInstance,仅需将电池的输入、输出端口在这个数据管理类中注册即可。
此外,GH_ComponentParamServer也会包含一些UI相关的处理,例如输入端和输出端的数量增加会导致电池在画布上显得更大,电池没有输出端则会以锯齿状显示电池的右端(参考CustomPreview电池,下图右侧电池)等等情况。
每个GH_Component电池实例中均包含一个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电池的数据管理类的底层是通过一个列表(List)来存储我们添加的各个参数的,每个参数都是最终继承于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值时,电池所对应的不同的表现。
以上就是今天小编给大家带来的关于Grasshopper电池的输入/输出的内容啦,如果有更多的疑问,欢迎联系转自:非解构-公众号加入我们的 参数化交流群 讨论
往期精彩