由上表可以看出, 除了integer, real的空值为 0 和 boolean 的空值为false以外, 其他数据类型的空值都是null. 原因很简单: 除了integer, real, boolean外, 其他数据类型都是handle(句柄)的子类型, handle类型的空值为null, 它的儿子们也跟着null到底了. 2) 什么是common.j, Blizzard.j, common.ai? 这3个文件都是支持War3 Jass的公共库文件, 文件里面包含和声明了所有(几乎是所有)Jass可以调用的函数和全局变量. common.j 是最基础的API库文件, 在Trigger Jass和AI Jass中都可以调用. Blizzard.j 包含使用Trigger Editor时生成的Trigger Jass中经常调用的库函数/全局变量, 实际上是基于common.j写的子函数. Blizzard.j只能在Trigger Jass中调用. common.ai 只能在AI Jass中调用, 它有AI中需要使用的函数和全局变量. 注意: 在Trigger Jass中, 不能调用common.ai中的函数和全局变量; 同样地, 在AI Jass中, 也不能使用Blizzard.j的函数和全局变量. 3) 如何获得最新的common.j, Blizzard.j, common.ai? 在war3.mpq/war3x.mpq/war3patch.mpq都有common.j/Blizzard.j/common.ai, 要获得最新版本的文件, 可以用WINMPQ打开war3patch.mpq, 在WINMPQ右上方的输入框中输入: scripts\* ,如果你的war3patch.mpq没有"加密"过, 那么可以找到这3个文件. 如果是"加密"过的, 则需要把war3patch.mpq的文件全部解压到临时目录, 然后用Ultra-Edit中的在文件中搜索字符的功能寻找这3个文件. 4) 可以在自己的地图中使用自定义的common.j, Blizzard.j, common.ai吗? 可以. 用输入管理器输入自定义的common.j, Blizzard.j, common.ai, 输入目录应为\scripts\. 这样, 运行地图中的脚本时, War3就不会到war3patch.mpq寻找这3个文件了而直接从地图中的\scripts\目录调用. 这样做的好处就是可以使库文件的同步一致. 通常, 版本相同的WAR3, 其库文件也相同, 所以多数时候不需要在地图中输入这3个文件. 但如果你更改了这3个基本文件来支持你的Jass, 或者确保为了库文件的一致性, 便可以在地图中输入基础库文件. 5) 怎么解读公共库文件(common.j, Blizzard.j, common.ai)的函数/全局变量? Blizzard对函数/全局变量的命名是相当规范简明易懂的(当然需要一定的英文基础), 它定义的函数/全局变量的名基本上是英语语句. 我举些例子: native CreateUnit takes player id, integer unitid, real x, real y, real face returns unit 本地函数CreateUnit() 即是Create Unit, 意思为创建单位, 从参数表(takes 和 returns之间的声明)来看, 这个函数需要输入玩家, 单位编码, 坐标x, 坐标y, 方向角度, 返回值是单位类型变量. 从函数的名字和参数表, 可以猜测/确认CreateUnit()是用来在地图的x/y坐标中为指定玩家创建指定单位, 并且使创建单位面向所定义的角度, 返回值是创建单位. 在地图入口函数main()中, CreateUnit()被大量调用, 用来重现地图设计时用WORLD EDITOR(GUI)放置的单位. 注意: 就如前面所说过, integer可以用单引号括起4个英文字母/数字来表示. 所以CreateUnit()中integer unitid参数可以用 'Xxxx'来表示. 如: set u = CreateUnit( Player(5), 'ndgt', -2944.0, -3136.0, 270.000 ) 上句意思是为玩家5在地图坐标(-2944.0, -3136.0)中创建编码为'ndgt'的单位, 并使创建单位面向角度为270度, 然后把创建单位保存在变量u中. 总结下Blizzard函数名/全局变量名的规律: a) 前坠Get 取得某属性, 此类函数一般有返回值 b) 前缀Set 设置某属性 c) 前缀Is 是否判断, 此类函数返回类型都是boolean, 返回true或false d) 中间有2 数据类型转化函数, 如S2I(), I2R, I2S()等 e) 有Item/Unit/... 肯定是与物品/单位/...相关的函数 f) 后缀BJ 此函数肯定是在Blizzard.j中定义的, 不能在AI中使用. g) 前缀bj_ 在Blizzard.j中定义声明的全局常量(常量是指定义并赋值后不能再改变的值) h) 前缀Create 创建 i) 前缀Remove 移除 k) 全部大写 在common.j中定义声明的全局常量 6) 实用问题: 如何创建外观随机可变的地图? 上面说过, main()是地图入口函数, 在main()调用了一些再现地图设计时原貌的子函数如CreateAllUnits(). 因此, 我们可以在CreateAllUnits()中加入随机函数GetRandomInt()来控制单位/物品/可破坏物等初始化过程. 我们可以用WINMPQ打开要修改的地图, 提取war3map.j来修改main()函数及其相关函数. 修改完war3map.j后, 再用WINMPQ导入修改后的war3map.j. a) 函数修改/增加方法: 这是3C地图中的CreateAllUnits()函数 function CreateAllUnits takes nothing returns nothing call CreateNeutralPassiveBuildings() //用户自定义函数 call CreatePlayerBuildings() //用户自定义函数 call CreateNeutralHostile() //用户自定义函数 call CreateNeutralPassive() //用户自定义函数 call CreatePlayerUnits() //用户自定义函数 endfunction 注意: CreateAllUnits()也不是库函数, 是用户自定义函数): 我们可以根据CreateAllUnits()创建另一个函数: function CreateAllUnitsRandom takes nothing returns nothing // randomint 是 1,2,3中的随机数 local integer randomint = GetRandomInt(1, 3) call CreateNeutralPassiveBuildingsRandom(randomint) call CreatePlayerBuildingsRandom(randomint) call CreateNeutralHostileRandom(randomint) call CreateNeutralPassiveRandom(randomint) call CreatePlayerUnitsRandom(randomint) endfunction 因为原来调用的函数都是无参数函数, 所以相应的函数应该做些修改, 如: //创建玩家单位, 以CreatePlayerUnits()为蓝本增加新函数 function CreatePlayerUnitsRandom takes integer randomint returns nothing //此函数以CreateUnitsForPlayer0为蓝本增加新函数 call CreateUnitsForPlayer0Random(randomint) ...... endfunction //创建中立敌对单位 //此函数以CreateNeutralHostile为蓝本增加新函数 function CreateNeutralHostileRandom integer randomint returns nothing local player p = Player(PLAYER_NEUTRAL_AGGRESSIVE) local unit u local integer unitID local trigger t local real life //加入条件控制, 根据随机数创建不同单位组合. if randomint == 1 then //可掉宝物的单位 set u = CreateUnit( p, 'nC26', -6533.2, 445.1, 220.680 ) set t = CreateTrigger( ) call TriggerRegisterUnitEvent( t, u, EVENT_UNIT_DEATH ) call TriggerRegisterUnitEvent( t, u, EVENT_UNIT_CHANGE_OWNER ) call TriggerAddAction( t, function Unit000012_DropItems ) //不掉宝物单位 set u = CreateUnit( p, 'nelb', -373.5, 3533.4, 44.518 ) set u = CreateUnit( p, 'nomg', -233.6, 3436.2, 140.871 ) elseif randomint == 2 then //不掉宝物单位 set u = CreateUnit( p, 'a001', -373.5, 3533.4, 44.518 ) set u = CreateUnit( p, 'a002', -233.6, 3436.2, 140.871 ) else ...... endif endfunction 别忘了修改main() function main takes nothing returns nothing ........ //call CreateAllUnits() //原版的单位初始化函数, 不再使用 call CreateAllUnitsRandom() //使用新的单位初始化函数 ........ endfunction b) 初始设置复制方法: 问题的关键就是, 这么多初始单位, 怎么样修改起来不累人? 这里介绍一种方法: 1. 先把要修改的地图复制 2. 修改复制地图初始单位的类型/位置/宝物等, 删除所有TRIGGER, 并保存 3. 用WINMPQ打开复制地图, 提取war3map.j 4. 复制war3map.j中对应函数中创建单位的语句 5. 把复制的语句粘贴在原地图中相应函数的适当位置 6. 重复1-5, 复制更多的随机初始化设置 注意: 如果用World Editor再次修改并保存, main()将使用GUI默认生成的call CreateAllUnits(), 而不是call CreateAllUnitsRandom(). 此时还需要打开war3map.j修改main()函数. 同样, 使可破坏物等初始设置有随机变化也可以用此方法. Jass基础教程 第四章 库函数
这章是Jass基础教程中最难的部分, 也是最实用的部分. Jass的关键字有26个, 跟英文字母一样多. 我们来回顾一下: and, array, call, constant, else, elseif, endfunction, endglobals, endif, endloop, exitwhen, extends, function, globals, if, local, loop, native, not, or, return, returns, set, takes, type, then 以此看来, Jass是语法结构最简单的一种准计算机语言. Jass实现功能基本依靠调用common.j, blizzad.j, common.ai中的库函数.
本章所述内容不属于Jass语法部分, 最主要讲述: 触发器(Triggers) 跨脚本通讯(Inter-Script Communication) 队列(Enumerations) 队列过滤器(Filters) 线程(Threads) 1) 触发器(Triggers) 触发器用于地图触发器脚本(Trigger Jass), 用于响应特定事件. 它是种响应信号(callback), 触发器不能应用于AI, 即是说AI Jass中不能有触发器的语句. 触发器包含创建触发器, 设置触发条件, 设置动作. a)创建触发器 触发器的数据类型为trigger(触发器), 是handle(句柄)的子类型. 创建触发器的函数在common.j中的声明: native CreateTrigger takes nothing returns trigger 参数: nothing //无参数 返回: trigger //返回创建的触发器 使用格式: set 表达式 = CreateTrigger() 其中表达式是trigger类型的变量 新数据类型: trigger类型: 是handle的子类型, 用于调用和处理触发器 b)触发事件 触发事件的数据类型为event(事件). 可以引起触发响应的触发器必须先在游戏中注册触发事件, 用于监视游戏事件发生. 注册触发事件的函数在common.j中的声明: native TriggerRegister*Event takes trigger whichTrigger, ... returns event 函数名: 根据事件对象的不同, 有不同的事件响应. TriggerRegister*Event中的 * 号为对象名, 如: TriggerRegisterUnitEvent, TriggerRegisterEnterRegion 等, 注册不同对象的事件要求传递相应指定对象作为参数, 有些事件注册要求传递下面所说的过滤器(Filters) 参数: trigger whichTrigger //触发器变量 ... //相应指定对象的变量 返回: event //返回该事件 使用方法(举注册单位事件的例子): call TriggerRegisterUnitEvent(哪个触发器, 哪个单位, 哪种类型的单位事件) 新数据类型: event //事件 c)触发条件 触发条件是一组布尔表达式(boolexpr)数据, 它的数据类型为条件函数(conditionfunc), 是布尔表达式(boolexpr)类型的子类型. 建立触发条件的函数在common.j中的声明: native Condition takes code func returns conditionfunc 参数: code func //函数代码作为参数 返回: conditionfunc //返回建立的条件 其中takes code func是指需要函数代码func作为参数, 参数函数 func 必须的声明格式必须是: takes nothing returns boolean. 即是说, 用作参数的函数本身应该是无参数且返回值为布尔型(boolean). 使用格式: set 表达式 = Condition(function 布尔型函数名) 其中表达式是条件函数(conditionfunc) 新数据类型: boolexpr 布尔表达式 conditionfunc 条件函数, 是布尔表达式的子类型 比如: function Girl1 takes nothing returns boolean ... return true endfunction function Girl2 takes integer i returns boolean ... return true endfunction function Girl3 takes nothing returns nothing ... return endfunction // 假设 c 为conditionfunc类型变量 set c = Condition(function Girl1) //(A) set c = Condition(function Girl2) //(B) 本身要求参数 set c = Condition(function Girl3) //(C) 返回值不是boolean类型 解释: Girl1() 是无参数且返回为boolean的函数, 可以用作 Condiction() 中的参数函数code func. 而Girl2() - 本身要求参数, Girl3() - 返回值不是boolean类型, 所以(B)(C)函数不可作为Condiction()的参数. 在第一章变量篇中我没有详细说明code类型的数据, 因为怕读者不能理解. 现在我说明下code类型数据. 我们用例子说明, 比如: call myfuction1(IsGirl()) //(A) call myfuction2(function IsGirl()) //(B) (A)和(B)之间有什么不同呢? 按我的理解: (A)(B)都把IsGirl()运行返回后的值当作myfunction1()/myfunction2()的参数. IsGirl()在(A)中是一次性的处理过程. 而(B)是创建了一个运行IsGirl()逻辑的监视线程, 只要线程没给清除/终止, IsGirl()将一直监视变化. 注意这里所说的线程跟下面要说的AI线程是两码事. d) 增加触发条件: 触发条件可以用TriggerAddCondition()增加 增加触发条件的函数在common.j中的声明: native TriggerAddCondition takes trigger whichTrigger, boolexpr condition returns triggercondition 参数: trigger whichTrigger //触发器变量 boolexpr condition //触发条件变量 返回: triggercondition //返回该触发条件的handle(句柄) 使用格式: set tc = TriggerAddCondition(触发器, 触发条件) 其中 tc 为triggercondition类型变量 新数据类型: triggercondition 触发条件句柄 (注意: 触发条件和上面要说的触发事件是不一样的!!!) 例: 这是增加/移除/改变触发条件的例子 //文件头声明个全局变量tc globals ... triggercondition tc = null trigger mytrigger = null ... endglobals function Girl takes nothing returns boolean ... return true endfunction function notGirl takes nothing returns boolean ... return false endfunction //为 mytrigger 增加触发条件 function addcondition takes nothing returns nothing ... set mytrigger = CreateTrigger() set tc = TriggerAddCondition(mytrigger, Condition(function Girl)) ... endfunction //因为tc是mytrigger触发条件的句柄, 把tc清空便清除了指向的触发条件. function removecondition takes nothing returns nothing ... set tc = null .. endfunction //改变 mytrigger 的触发条件 function modifycondition takes nothing returns nothing ... set tc = null //先把原来的触发条件移掉 set tc = TriggerAddCondition(mytrigger, Condition(function noGirl)) ... endfunction e)触发器动作 触发器动作是当指定事件发生并符合触发条件的执行的语句. 增加触发器动作的函数在common.j中的声明: native TriggerAddAction takes trigger whichTrigger, code actionFunc returns triggeraction 参数: trigger whichTrigger //触发器变量 code actionFunc //执行函数 注意: 执行函数必须是无参数无返回值(takes nothing returns nothing)的函数 返回: triggeraction //触发动作 新数据类型: triggeraction //触发动作, handle字类 把有关触发器主要函数糅合在一起, 我们来看个例子, 研究下触发器是怎么创建的: //文件头声明全局变量mytrigger //假设mytrigger是监视单位死亡事件的触发器 globals ... trigger mytrigger = null ... endglobals //判断是否掉物品 function isDrop takes nothing returns boolean local boolean conditcion ... if conditcion = true then return true else return false endif endfunction //掉物品 function DropItems takes nothing returns nothing //获得触发单位 local unit trigUnit = GetTriggerUnit() .... call UnitDropItem( trigUnit, 'IC21' ) ... endfunction function ThisIsMyTrigger takes nothing returns nothing local unit u //在-5630.5, -4723.1坐标为玩家5创建单位'n00I', 面向90.260度 set u = CreateUnit(Player(5), 'n00I', -5630.5, -4723.1, 90.260 ) //创建触发器(相当于mytrigger的初始化) set mytrigger = CreateTrigger() //为mytrigger注册触发器事件, 让游戏系统监视所创建单位u的死亡事件 //EVENT_UNIT_DEATH是common.j中定义的常量 call TriggerRegisterUnitEvent( mytrigger, u, EVENT_UNIT_DEATH ) //为mytrigger增加触发条件 //isDrop是无参数返回为布尔值的函数 set tc = TriggerAddCondition(mytrigger, Condition(function isDrop)) //为mytrigger增加触发器动作 - 掉宝物 //DropItems是无参数无返回值的函数 call TriggerAddAction(mytrigger, function DropItems ) ... endfunction 可以看出, 创建触发器顺序是: 1. 触发器初始化 - CreateTrigger() 2. 触发器事件注册 - TriggerRegister*Event() 3. 定义触发条件(可选) - TriggerAddCondition() 4. 触发器动作 - TriggerAddAction() 注意: 1. 创建触发器必须先用CreateTrigger()初始化. 2. 没有注册事件的触发器只是个处理过程, 不会响应事件执行程序 3. 触发器可以不加触发条件, 因为可以用触发动作调用的函数来控制逻辑. 2)线程(Threads) (这部分属于AI部分, 作为入门者做一般性了解就行了, 因为AI都是纯JASS写的, 也没有真正好的AI EDITOR. 本人也对此一知半解, 关于AI的文章也不多, 没什么好参考的.) 线程只应用于AI脚本(AI JASS), 不能用于触发器脚本(Trigger Jass). 通常, 当AI脚本开始运行时只创建一个线程, 创建更多的线程可以用comman.j的本地函数: native StartThread takes code func returns nothing 调用 call StartThread(function myfunc) 将创建一个从函数myfunc开始执行的线程. 每个玩家最多可以拥有6个线程, 包括一开始执行的主线程. 当一个玩家有6个线程数时, 调用StartThread()的语句将被忽略. 线程不能回收, 当你为某玩家创建了5个自定义线程, 将无法为该玩家创建更多的线程. 当新线程创建时, 线程立即生效. 当线程让步执行时, 创建此线程的父线程将继续执行. 在同一玩家中的所有线程都共享全局状态(包括变量). 即是修改某个全局变量, 修改后的值在此玩家的所有线程中都是可见的. 线程在以下的情况让步执行, 返回父线程 a) 当线程中的操作码(opcode)超出限制, 线程会自动休眠 1 秒 b) 当线程中用使用 Sleep(n), 线程将休眠 n 秒, 然后继续执行. 线程在以下情况会中止, 返回父线程 a) 如果 call StartThread(null)中, 线程中止 b) 当线程的主函数返回, 线程中止. (StartThread()中之间调用的函数就是主函数.) c) 当线程中使用没有声明的变量, 线程中止. 在使用之前, 变量必须声明. d) 当线程中出现零为被除数时, 线程中止 e) 线程主函数出现语法错误. 注意: 虽然AI脚本可以使用大部分common.j的库函数, 但有些类型的函数在AI不能正常工作, 如: a) 返回字符串类型(string)的本地函数, 如I2S(), SubString()等 b) 需要以code, trigger, boolexpr 等类型数据为参数的本地函数, 如触发器函数, 队列函数(ForGroup, 等) 注意: AI中不可以使用Blizzard.j的函数, 触发器中也不可以使用common.ai的函数, AI和触发器都可以使用common.j的函数(当然, 对于AI, 还受上面所说的限制) common.ai和common.j是写AI时可以调用和参考库文件, 要研究AI, 先去读这2个文件. 3) 跨脚本通讯(Inter-Script Communication) 在游戏中, 可能会有多个独立的Jass脚本文件同时运行. 比如在对战地图中的游戏, 运行触发器脚本文件的同时, 也可能运行了每个电脑玩家的AI脚本文件. 每个脚本文件之间的全局变量不是共享的. 所以, 一个电脑玩家的AI脚本中设置的全局变量不会影响另一个电脑玩家的AI脚本的执行. 触发器脚本也不可以和AI脚本共享全局变量. 但可以用传递命令的方法进行脚本之间的数据交换. 命令由一对数值型数据(integer)组成: 命令值(command value)和数据值(data value). 从触发器脚本向AI脚本发出通讯命令, 可以使用common.j中定义的以下本地函数: native CommandAI takes player num, integer command, integer data returns nothing 参数: player num //玩家 integer command //命令 integer data //命令数据 以下是AI中使用的common.j函数, 注意: 每个电脑玩家都会有独立的AI脚本, 所以, 以下函数都没有要求玩家作为函数参数. 每个电脑玩家都有命令堆来堆放接受到的命令. 想知道有多数个命令堆放在命令堆, 可以用下面的函数: native CommandsWaiting takes nothing returns integer 参数: 无 返回: 命令堆的命令数(integer) 获得存放在命令堆中最顶端的命令(): //返回命令 native GetLastCommand takes nothing returns integer //返回命令数据 native GetLastData takes nothing returns integer 上面2个函数都不会移除命令堆中的命令, 要移除堆中的命令, 可以用: native PopLastCommand takes nothing returns nothing
4) 队列(Enumerations) 虽然JASS不能自定义数据结构(因为JASS缺少指针操作符), 但API库中提供了一些实现队列操作的函数. 如一组单位为单位组(group), 一组玩家为势力(force), 虽然一组可破坏物没有明确定义它的数据类型, 但也可以用API函数来操作. 单位组和势力的操作函数很类似. 单位组处理函数 // 初始化单位组 native CreateGroup takes nothing returns group // 在指定单位组中增加指定单位 native GroupAddUnit takes group whichGroup, unit whichUnit returns nothing // 在指定单位组中移除指定单位 native GroupRemoveUnit takes group whichGroup, unit whichUnit returns nothing 势力处理函数 // 初始化势力 native CreateForce takes nothing returns force // 在指定势力中增加指定玩家 native ForceAddPlayer takes force whichForce, player whichPlayer returns nothing // 在指定势力中移除指定玩家 native ForceRemovePlayer takes force whichForce, player whichPlayer returns nothing JASS不能直接操作队列里面的元素, 它是通过callback类型的函数来实现对队列的操作: // 对指定单位组中的每个单位都运行指定callback函数callback // (对应GUI语言的For Each Unit in <Group>) native ForGroup takes group whichGroup, code callback returns nothing // 对指定势力中的每个玩家都运行指定callback函数callback // (对应GUI语言的For Each Player in <Force>) native ForForce takes force whichForce, code callback returns nothing 输入上面两个函数的callback函数必须是无参数无返回值函数(takes nothing returns nothing) 同样, 操作可破坏物也可以用在区域内的可破坏物作为队列, 可以以用类似的方法: // 在指定区域r内符合指定过滤器filter的都运行指定callback函数actionFunc // (过滤器见下节的讲解) native EnumDestructablesInRect takes rect r, boolexpr filter, code actionFunc returns nothing 在callback函数, 可以用下面的函数获得队列中的下一个元素: // 获得单位组中的下一个单位 // (对应GUI语言的Pick Every Unit in <Group>) constant native GetEnumUnit takes nothing returns unit // 获得势力中的下一个玩家 // (对应GUI语言的Pick Every Player in <Force>) constant native GetEnumPlayer takes nothing returns player // 获得可破坏物组中的下一个可破坏物 // (对应GUI语言的Pick Every Destructables in <Region>) constant native GetEnumDestructable takes nothing returns destructable 注意: AI中不支持队列函数的使用. 这是杀死单位组中所有单位的实例: // 这是callback函数, 无参数并无返回值 function KillGroupCallback takes nothing returns nothing // 获得单位组中的下一个单位 local unit nextUnit = GetEnumUnit() // 杀死该单位 call KillUnit(nextUnit) endfunction // 调用ForGroup // 对单位组groupToKill中的每个单位都运行函数KillGroupCallback call ForGroup(groupToKill, function KillGroupCallback) 另一个经常是用的例子是在队列中查找特定条件的元素. 不幸的是, 因为JASS只支持callback函数来处理队列中的元素, 所以只有用全局变量来保存不同单位的属性. 下面是找出单位组里生命最高的单位的例子: //定义全局变量 globals //用于储存两单位比较后较高的生命值, 初始化为 0 real mostLifeSoFar //用于储存两单位比较后有较高生命值的单位, 初始化为 null unit unitWithMostLifeSoFar endglobals //比较单位生命值的callback函数 function MostLifeCallback takes nothing returns nothing //获得单位组中的下一个单位 local unit nextUnit = GetEnumUnit() //获得单位属性 - 生命 //UNIT_STATE_LIFE是common.j中定义的常量 local real life = GetUnitState(nextUnit, UNIT_STATE_LIFE) //比较生命值 if life > mostLifeSoFar then //把较大的生命值储存 set mostLifeSoFar = life //把有较大生命的单位储存 set unitWithMostLifeSoFar = nextUnit endif endfunction ... //初始化全局变量的值为空值 set mostLifeSoFar = 0 set unitWithMostLifeSoFar = null //调用ForGroup //对单位组myGroup中的每个单位都运行函数MostLifeCallback比较生命 call ForGroup(myGroup, function MostLifeCallback) //上句运行后, 全局单位类型变量unitWithMostLifeSoFar便指向单位组myGroup中最高生命的单位, 或: //如果单位组myGroup是空组, 那么unitWithMostLifeSoFar便是空值null ... 当然, 实现队列操作, 也可以用数组的方法来处理. 但, 数组不能使用紧接着要说的队列过滤器, 也不能定义数组中包含数组. 这些都是队列所拥有的优势, 如可以有数组型的单位组(相当于数组中包含数组), 也可以用队列过滤器. |