这篇文章将阐述如何让你的函数能够在自己设置的限定条件下,通过外部输入(主要是鼠标)来触发执行。
对于很多有了一定MOD经验的制作者来说,最困扰的问题已经不是弄不清MOD的框架了,而是如何让写好的函数在某些特定情况下能够被执行。
在我还没有掌握动作的添加修改方法时,我主要是依靠组件自身提供的各种接口(如equippable组件的onequip)或者事件(combat组件的onhitother)来完成这个目标。但我逐渐地发现,用接口和事件来触发函数,并不总是一个好的选择。一来,你得弄清楚组件有哪些API和事件可用,二来,并不是所有的组件都有合适的接口和事件可用。
打个比方,用接口和事件驱动函数触发,就好像在一条已经建好的传输线路上添加新的数据传输内容,虽然是比较快捷,但你需要了解这条传输线路的传输方向以及传输的规则。有时候,光是要了解传输方向就需要耗费极大的精力,因为数据的传输是有分支的,脉络过大时很容易晕头转向。而如果你没有弄清楚传输规则,加进了破坏规则的东西,则有可能把这条传输线路毁掉。与此相对的,利用动作来实现函数触发,则像是另外搭建了一条轻量级的数据传输线路。虽然需要做一点前期准备,但熟练之后会非常快。不需要去了解那些动不动就包含几十个函数的复杂组件的工作方法,你只需要一个动作,一个组件,一个组件动作收集函数,一个状态图的动作处理器就可以搭建起一个简单的动作触发器,实现通过外部输入来触发函数的目的。
在叙述怎样搭建这条轻量级传输线路之前,先了解一下这条线路的工作方式,从这条线路的各个节点开始介绍。
节点介绍
- 起点:playercontroller
这个组件,在创建人物的时候,会被默认安装playercontroller检测各种外部的输入,然后通过一定的方式与游戏里的操作联系起来。你会在这个组件的代码里看到各种TheInput,也就是所谓的外部输入了。这个组件就是我们这条传输线路的起点。 - 中继点1:playeractionpicker
playercontroller是一个很大很复杂的组件,它的功能并不限于将外部输入转为动作触发。于是,游戏制作者就设计了另一个组件,专门负责处理动作触发,这就有了playeractionpicker。 - 中继点2:各个组件(component)
除了极少数动作(比如移动)之外,大部分动作都需要有组件的支持才能连结起来,要添加新动作也是如此。虽然如此,但对组件本身没什么要求,即使你写一个挂名的组件,除了动作搜集函数之外什么都不写,也是可以触发动作的。 - 终点:动作(action)
到了最后,大部分官方动作和所有的自定义新动作,都会触发ACTION表下的相应动作的fn函数。通过定义这个fn函数,我们就能实现想要通过鼠标操作实现的效果。ACTION表是游戏里的一张全局表,储存着绝大多数动作的数据,表中的元素都是一个ACTION类的实体。这张表和这个类,都定义在actions.lua文件里。
工作流程
上面写着起点和终点,但这是从外部输入进入游戏的过程来说的。实际上,这条线路的数据是双向传输的。鼠标能传递的信息只有左键、右键以及鼠标所指向的位置。而游戏传递到外部,能被我们看到信息则是通过显示屏来实现的。playercontroller会以很高的频率检测鼠标指向位置的信息,然后通过playeractionpicker下的GetLeftClickActions,GetRightClickActions,搜集当前人物能做的左键、右键动作并排序,然后选出优先级(优先级是在ACTION表中被定义的)最高的动作作为鼠标触发动作,这时候,如果动作的名字就会在屏幕上的鼠标位置下方显示。不过,如果通过这种方式取得的左、右动作一样的时候,只有左键能触发动作。
GetLeftClickActions,GetRightClickActions这两个函数,是通过各个搜索各个组件的动作搜集器来搜集当前能够被执行的动作的。许多组件都有一个或者多个组件动作搜集器,这些搜集器通过判断当前得到的信息(鼠标所指之处,当前人物的状态等等)来决定这一刻,这个组件能够触发哪些动作,并把它们插入到GetLeftClickActions,GetRightClickActions里的一张所有组件搜集器共用的可触发动作表里。同一个组件搜集器是可以一次插入多个动作的,而且也是可以自由选择插入什么动作的,与组件本身是可以毫无关联的。
注意了,我们就是在这里搭建起的新传输线路的。我们通过设置新的组件动作搜集器来进入playeractionpicker的动作搜集过程,设定合适的条件来决定何时能够触发我们想要触发的动作。有时候,设定的条件可能也会满足其他组件动作搜集器条件,这时候,就由它们的优先级来比较了。不过这个优先级比较是用lua的table里自带的比较函数来做的,所以同优先级的情况可能无法稳定排序,这时候建议修改动作的优先级。
在返回可做动作之后,如果你按下鼠标,就会触发相应动作的fn。不过除了少数动作比如装备武器外,大部分的动作都需要有相应的状态图(sg)的动作处理器(actionhandler)支持,否则只能显示名字,不能触发fn。actionhandler的作用是,根据当前要触发的动作,让人物转入相应的动画状态。比如说捡起物品,人物会有一个弯腰的动画,这就是捡起这个动作和shortaction这个状态连在一起的结果。
实践
基本的原理都已经弄明白了,那么就要开始实践了。
与做新物品不同,搭建一条全新的数据传输线路,要比修改原传输线路容易。所以,先给出一个实现新动作的例子,再给出一个修改动作的例子。
根据上面的流程,我们就弄清楚了,要搭建一条传输线路,我们需要一个组件以及相应的组件动作搜集器,以及一个动作,以及动作对于的sg里的动作处理器。
组件动作,在官方制作者那里是分好了类的,不过分类并不是唯一的,同一个组件动作,可能会同时有多个类的属性。比如说Book这个组件,就是读书。你可以在物品栏里按右键读,也可以左键拿起书,然后对着人物点左键读。虽然是同一个动作,但执行的场景不一样,前者是Inventory,也就是你的物品栏,后者是Scene,也就是屏幕。不同的场景下,传输给组件动作搜集器的数据是不一样的。也就是说,组件动作搜集器有5种。
官方默认分为5个类,SCENE,USEITEM,POINT,EQUIPPED,INVENTORY。这里翻译一下klei论坛上的教程里,对这5个类的介绍。原作者是rezecib。
SCENE - uses the arguments inst (the thing with the component), doer (the player doing the action), actions (which is where you add the action if it should be added-- all action types have this argument at the end), and right (whether the click was a right-click). SCENE actions are ones that are done by clicking on a specific thing, either in the inventory or in the world. The thing that is clicked on is what has the component that enables the action, as opposed to USEITEM and EQUIPPED, which enable actions on other things by items on your cursor or in your inventory. An example is harvesting for crops.
使用变量inst(拥有这个组件的东西),doer(做这个动作的玩家),actions(你添加的动作会被添加到哪张动作表中去,这个参数一般会在函数参数表的尾端。译者注:如果有right的话,在right前面),right(是否是一个右键点击动作)。SCENE 动作是通过点击一个在物品栏或者世界上物品来完成的。拥有这个组件动作的这个东西,让自己能够被点击从而执行动作,这一点与USEITEM和EQUIPPED相反,它们是让你能够点击在你的鼠标所指向的物品,或者物品栏的物品来执行动作。一个例子是收集作物这个动作。
译者注:这里补充几句。edible这个组件,是物品可食用的组件。这个组件没有SCENE 这个组件动作搜集器,只有USEITEM 和 INVENTORY。所以,你不能把食物放在地上,然后右键点击吃掉它(除了woodie的海狸形态,那个比较特殊,这里略过不谈)。要吃掉食物,你只能左键拿起食物,然后对着人点击鼠标,或者把食物放到物品栏里,右键点击。SCEN则就是,你能直接点击它然后完成对应动作。USEITEM - uses the arguments inst, doer, target (the thing being
clicked on), actions, and right. USEITEM actions are ones where you
have an item on your cursor, and are clicking it onto something in
the world. For example, fueling a fire.
使用变量 inst,doer,target(被点击的东西),actions和right。USEITEM 动作是这样的,你拿起这个物品(译者注:拥有这个组件动作的物品),去对着世界上的某些其它的物品,就可以激活该动作,按下去就会执行这个动作,典型的例子就是拿起燃料往火坑里添火。POINT - uses the arguments inst, doer, pos (the position that was clicked), actions, and right. POINT actions are enabled by a variety of things (having an item equipped, or having an item on the cursor), but are the only kind that are applied to a point in the world rather than a target. One example is deployable-- planting things and setting traps. Another example is teleporting with the Lazy Explorer (or "poof staff").
使用变量inst,doer,pos(被点击的位置),actions和right。POINT动作可以被很多东西激活(装备一个手持物品(其它部位不行),或者将一个物品拿起来(附在鼠标上)),但这是唯一一种对着地面而不是一个具体的物体作为变量的动作。典型的例子有deployable组件--种植东西以及放置陷阱。另一个例子则是橙宝石法杖(闪现)。EQUIPPED - uses the arguments inst, doer, target, actions, right. EQUIPPED actions are enabled by having a particular item equipped. Some examples are lighting things on fire with torches, or pickaxes for mining rocks, or weapons for attacking.
使用变量inst,doer,target,actions,right。EQUIPPED动作是在你让某个特别的物品被装备时激活。例子:装备火把可以激活点火动作,装备铥矿斧可以砍树,装备武器可以攻击。INVENTORY - uses the arguments inst, doer, actions, right. INVENTORY actions are ones you do by (right-)clicking on an item in your inventory, such as eating, equipping, healing, etc.
使用变量inst,doer,actions,right。INVENTORY动作可以通过右键点击物品栏执行。例子有吃东西,装备物品,治疗等等。
组件动作搜集器,在单机版和联机版里,出现的位置是不一样的。
在联机版中,是通过一个名为componentactions.lua的文件来储存所有的动作搜集器,并通过AddComponentAction这个函数来添加新的动作搜集器。
而在单机版中,则是通过在组件中定义类的函数来搜集组件动作。CollectSceneActions,CollectUseActions,CollectPointActions,CollectEquippedActions,CollectInventoryActions这五个函数,分别对应搜集Scene,Useitem,Point,Equipped和Inventory这五种类型的组件动作。
首先来看一个联机版的例子。
在这个例子中,我自定义了一个叫"占有"的动作,这个动作的动作处理器设定,当触发这个动作时,人物进入"doshortaction"的状态(state),实际上这个状态就是捡起物品时的state。游戏是允许多个动作处理器共有一个状态的,一般来说,懒得自己编写新的state的话,就用原本游戏中就已经有的就行。
与这个动作相关联的是我自己设计的一个组件villagerspawner,这个组件是附加在我自己设计的一个新的prefab上的。
由于这个组件只是需要挂名,所以有关组件的详细代码就不在这里写了。但有一点需要注意。挂名的组件,要么是在客机上也存在的,要么就需要存在replica,否则就无法使组件动作搜集器在客机上触发,也就谈不上能做出动作了。一般建议,可以添加一个不涉及游戏整体数据变化的组件,这个组件在主客机上都存在,用这个组件来挂名完成这条新数据传输通道的搭建。
--*************************************
--动作设定
--*************************************
local OCCUPY = Action()--Action是一个类,这样可以定义OCCUPY变量为一个Action类的实体
OCCUPY.id="OCCUPY"--动作的id,必须是唯一的,ACTIONS表中对应的KEY值
OCCUPY.str="占有"--动作的名字,会在可以实施这个动作时显示
OCCUPY.fn = function(act)--动作的操作函数,也就是我们想要控制执行的函数,
--这里固定只有act一个参数,它是BufferedAction类(这个类可以在bufferedaction.lua里看到具体定义)的一个实体,根据组件动作处理器的不同,act的数据会有变化。
--总的来说常用于函数操作的有4个数据doer,target,invobject,pos
--doer就是动作的执行方,target就是动作的目标,
--invobject就是动作执行时对应的物品,比如说EAT这个动作,invobject就是要吃的东西
--pos就是动作执行的地点,对地面执行的动作会用到这个数据。
local inst = act.target
local player = act.doer
if not inst:HasTag("private_base") then
inst:AddTag("private_base")
inst:AddTag('uid_base_'..player.userid)
inst.owner = player.userid
return true
end
end
AddAction(OCCUPY)--定义完动作后,要通过这个函数来将定义好的动作添加到游戏当中去
--*************************************
--动作对应的SG的动作处理器设定
--因为联机版玩家有两个SG——wilson和wilson_client,所以要设定两个
--*************************************
AddStategraphActionHandler("wilson", GLOBAL.ActionHandler(OCCUPY, "doshortaction"))
AddStategraphActionHandler("wilson_client", GLOBAL.ActionHandler(OCCUPY, "doshortaction"))--这个函数是用来给指定的SG添加ActionHandler的。
--*************************************
--组件动作搜集器设定
--*************************************
AddComponentAction("SCENE", "villagerspawner", function(inst, doer, actions, right)
--AddComponentAction这个函数,第一个变量对应着动作类型(上面说的5大类型之一),第二个对应的挂名的组件,
--第三个则是一个函数,在playeractionpicker中会被执行,用于判断是否添加,以及添加什么动作。
if right then
if not inst:HasTag("private_base") then
table.insert(actions, GLOBAL.ACTIONS.OCCUPY)
----满足判定条件后,就用table.insert函数将你想要添加的动作插入到actions表中。
end
end
end)
然后再来看一个单机版的例子
这是我帮忙制作的单机版saber MOD的一个片段。
想要实现一个功能:当玩家对自己使用剃刀时,saber就会黑化--这里就用一个函数来代替。
local BLACKING = Action()--定义动作这一段,和上面联机版的一样,不多说。
BLACKING.id="BLACKING"
BLACKING.str="Black"
BLACKING.fn = function(act)
act.doer:become("black") --这个就是我想要执行的黑化函数。
end
AddAction(BLACKING)
--
AddStategraphActionHandler("wilson",ActionHandler(BLACKING, "quicktele"))--单机玩家的sg只有一个wilson,就不需要像联机那样添加两个。
--单机版没有上面联机版的那个专用函数,我们只能用AddComponentPostInit来修改组件。
--我们通过重新定义组件的CollectInventoryActions函数,来让我们能够通过右键点击物品栏来触发自定义动作。
local function shaverpostinit(component)
local old = component.CollectInventoryActions
component.CollectInventoryActions= function(self,doer, actions)
if doer.components.saberblink and doer.state ~= "black" then
table.insert(actions, BLACKING)
end
old(self,doer, actions)
end
end
AddComponentPostInit("shaver", shaverpostinit)