动作详解

2016-12-31  本文已影响0人  LongFei_aot

这篇文章将阐述如何让你的函数能够在自己设置的限定条件下,通过外部输入(主要是鼠标)来触发执行。

对于很多有了一定MOD经验的制作者来说,最困扰的问题已经不是弄不清MOD的框架了,而是如何让写好的函数在某些特定情况下能够被执行。

在我还没有掌握动作的添加修改方法时,我主要是依靠组件自身提供的各种接口(如equippable组件的onequip)或者事件(combat组件的onhitother)来完成这个目标。但我逐渐地发现,用接口和事件来触发函数,并不总是一个好的选择。一来,你得弄清楚组件有哪些API和事件可用,二来,并不是所有的组件都有合适的接口和事件可用。

打个比方,用接口和事件驱动函数触发,就好像在一条已经建好的传输线路上添加新的数据传输内容,虽然是比较快捷,但你需要了解这条传输线路的传输方向以及传输的规则。有时候,光是要了解传输方向就需要耗费极大的精力,因为数据的传输是有分支的,脉络过大时很容易晕头转向。而如果你没有弄清楚传输规则,加进了破坏规则的东西,则有可能把这条传输线路毁掉。与此相对的,利用动作来实现函数触发,则像是另外搭建了一条轻量级的数据传输线路。虽然需要做一点前期准备,但熟练之后会非常快。不需要去了解那些动不动就包含几十个函数的复杂组件的工作方法,你只需要一个动作,一个组件,一个组件动作收集函数,一个状态图的动作处理器就可以搭建起一个简单的动作触发器,实现通过外部输入来触发函数的目的。

在叙述怎样搭建这条轻量级传输线路之前,先了解一下这条线路的工作方式,从这条线路的各个节点开始介绍。

节点介绍

工作流程

上面写着起点和终点,但这是从外部输入进入游戏的过程来说的。实际上,这条线路的数据是双向传输的。鼠标能传递的信息只有左键、右键以及鼠标所指向的位置。而游戏传递到外部,能被我们看到信息则是通过显示屏来实现的。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。

组件动作搜集器,在单机版和联机版里,出现的位置是不一样的。
在联机版中,是通过一个名为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)
上一篇 下一篇

猜你喜欢

热点阅读