|
演算体(Actor)相位技术之二——为不同玩家异步发送任意演算体消息
在上一篇中,我已经用两个简单的函数向大家讲解了异步显隐/销毁演算体的方法。也许对有些同学来说有点复杂,不过坦白说好了,与第二篇相比,第一篇的内存纯粹是小打小闹,实非严肃之举。实际上第一篇存在的意义就是为了给这二篇服务,因为没有第一篇的基础知识,直接写第二篇的话,整个教程的难度就会变得太过艰深,如果我写了教程只有我自己看得懂,那这种教程写出来也没有任何意义。
实际上本来我写了第一篇以后应该直接写第二篇的,但是由于事情实在太多,抽不出啥时间来。我本月初的时候其实本篇已经写了一半了,但是因为突然断电而丢失了,心情很糟糕,于是又拖了一个月。断电事件之后其实我已经把第二篇的内容在群里讲解了一遍,因此现在已经有几个人已经知道异步发送任意演算体的方法了。
好了,进入正题,本篇的内容就是告诉你如何实现一个自定义函数(本篇中将这个函数命名为Send Actor Message To Actor For Player Group),可以“为不同玩家异步发送任意演算体消息”。注意到我把“任意”两字加粗了么?因为,如果单纯的是“异步发送消息”,我们在上一篇里已经实现了。对上一篇有印象的同学可能还记得,我是用在数据编辑器里事先定好的“信号->消息”转发对这种机制来异步发送演算体消息的。一个消息对应一个信号。
但是,现在的问题在于,如果我希望做一个自定义函数,可以异步发送“任意”消息怎么办呢?有人也许会说:“我们事先在代理演算体里写下所有可能的消息,然后给它们一一一指派对应的信号”这样就能解决一切问题了。很可惜,这是根本不可能做到的。消息是存在参数的,而某些消息的参数可以是字符串。比如SetText消息,你可以"SetText A","SetText B","SetText 12312324","SetText {Hello World}","SetText {The cake is a lie }"等等。单单一个字符串参数就可以有无限多种组合,所以故而所有消息的组合自然也是无限的。你无法把这无限个消息写到数据编辑器里去。
因此,从“有限”到“无限”,我们要来个“大跃进”,一次加二个“超”。直接跳过超三变成超四。
超·超·超·超越
好了,于是大家不妨思考下,如何让代理演算体"Trigger Per Player Actor Agent"可以转发任意消息而不需要写上无限个“信号->消息”转发对呢?
.
.
.
.
.
.
.
.
——答案很简单:做不到。
数据编辑器是死的,我们不可能让它做出这么灵活的事情。就算你想让它把触发器发来的任意消息不经处理地原样发给目标演算体也做不到。
那么,我们用触发器来动态修改代理演算体的事件么?当需要发送某个消息的时候就用Galaxy给它动态指派一个对应的信号,把这个“信号-消息”对动态写进代理演算体的事件字段里?
——也做不到,因为我们无法动态修改演算体事件。
因此,我们的代理演算体根本无法做到任何形式的“转发任意演算体消息”。我们一开始就不应该往这个方向去考虑。
但如果在这里停止思考,我这篇教程就得就此结束了。肯定有什么方法,否则我这篇教程就毫无意义了。没错,确实有一个方法,这方法和第一篇的方法看似类似,但本质上却完全不同。
让我们放开代理演算体。回过头去思考,我们想要实现的东西到底是什么:使用自定义函数来异步发送任意演算体消息。
在上一篇教程里我灌输了这样一个概念:数据编辑器的长处是能做到更多事情,而触发器的长处是其自由度。那么考虑到“任意”这个东西,显然是跟触发器关系更大一点,既然数据编辑器无法实现任意,那么只有回过头来诉诸触发编辑器了。
触发编辑器可以发送任意消息么?当然可以,原本发送消息这个函数就可以发出任意消息。但问题是,你不可以异步指定发送目标。因为暴雪不想让你异步执行脚本。比方说,如果两个玩家联机,我们不能只在玩家1的电脑上杀死单位A,而玩家2的电脑上却一个单位都不杀。因为这样会直接导致游戏进程不一致而断线。而如果你在玩家1的电脑上执行了以下脚本:
[codes=galaxy]ActorSend(lv_a,"SetText {The cake is a lie}");[/codes]
那么也无法用合法的方式不在玩家2的电脑上执行这一脚本。
又是死路一条?
当没有路可走的时候。就创造一条路出来吧。没错,不论在哪台电脑上,我们都必须对演算体变量"lv_a"所指向的演算体发送"SetText {The cake is a lie}"这条消息。但别忘了,我在上一篇中讲过,演算体系统本身是可以异步的。那么玩家1和玩家2电脑上的"lv_a"变量,就必须指向同一个演算体么?
答案当然是:否。想想看,既然有些演算体在显卡差的电脑上根本不会被创建,那么如果我们把“最后创建的演算体”赋值给一个演算体变量,那么这个演算体变量所指向的对象,自然可以是不同的,很显然如果玩家1的显卡好,那么它的变量指向的是它刚创建的那个对象,而玩家2的显卡不好,那个演算体根本没被创建,那么实际上这台电脑上指向的其实就是“上上个”演算体。
再一个例子:假设我们玩家1和玩家2的电脑上,lv_a都指向了同一个演算体。但是我用上一篇的方法,将玩家2上的这个演算体异步地“销毁”掉。那么结果,两台电脑上的lv_a指向的对象还会是一样的么?很显然,玩家2的电脑上lv_a指向了一个不存在的演算体。
这两个例子都表明,同样的一个演算体变量,在不同电脑上是可以指向不同对象的。
说到这里,大家能想到了吗?虽然,如果使用galaxy来发送演算体消息,那么在所有的电脑上都必须同步地发送这个消息,也必须都发送给同一个变量。但不同电脑上,这个变量指向的对象却可以不同!于是,如果我们让A组玩家的电脑上的某个变量指向我们想要异步发送消息的目标,而B组玩家的电脑上,这个变量指向的目标为空。那么在发送消息时,就只有A组的演算体能收到消息并作出反应,而B组的消息发向了一个不存在的对象,故而没有任何效果。于是我们不就实现了,异步发送任意演算体消息了么?
于是在这里,我们成功地将“对不同玩家电脑上的同一对象异步发送演算体消息”这个概念通过一个相当神奇的方式等价代换成了“向不同玩家电脑上的不同对象同步发送消息”的概念,将一种异步变成了另一种异步。道路已经打开了。你,可以看到结局了么?
我希望有想法的同学可以先不要看下文,自行尝试一下,试试看如何把我这个概念具体化为实际可行的操作步骤。至于其余迫不及待想看答案的同学们,可以直接继续看下去...
那么,具体怎么做呢?首先,我们要异步为演算体变量赋值。有些人可能会考虑异步销毁的方式,我们先把变量指向我们发送消息的对象,然后把异步销毁它——这根本就是本末倒置了。我们只是想对目标异步发送一条消息而已,你把目标都干掉了,搞什么啊。同理,我们也不能用异步创建的方式——我们发送消息的目标是一个既存的演算体,而不是临时创建出来的。
于是,这里延伸出一个额外的附加问题:怎么正确对变量进行异步赋值?这里,我们又要再一次感谢演算体系统里的“引用”系统了。在对演算体变量进行赋值的时候,我们不但可以通过“最后创建的演算体”等等直接赋值的方式外,我们也可以通过别名和引用来间接赋值。如果我们可以给一个引用进行异步赋值,再把这个引用赋给变量,那么这个变量也就间接地实现异步赋值了。
于是,很快地,这里产生了第三个问题:如何正确对引用进行异步赋值?看起来我们只是把本该由变量解决的问题推给了引用来解决。这样“异步任意消息->异步变量赋值->异步引用赋值”地一层层踢皮球下去真的能有尽头么?
能。我刚才说啥了,数据编辑器和触发器各有什么好处来着?引用是演算体系统的一部分,是数据编辑器范畴的东西。触发器范畴的变量无法解决的事情,不代表数据编辑器范畴内无法解决,反之亦然。所以本篇教程把踢皮球的功力发挥到了极致,把一个原本双发都无法解决的问题拆了开来,将细节部分不断进行变形和等价代换,来回地交给两个系统来逐一解决——只要能解决问题,踢几脚皮球又算得了什么呢?
好了,回到问题,怎么实现异步引用赋值?
答案很简单:就在上一篇里。别忘记了,上一篇教程里我们已经实现了异步发送固定消息。而“给引用赋值”偏偏就可以是一个固定的消息。只要请我们的代理演算体再次出场,将galaxy传递过来的消息目标都异步赋给一个固定的引用就好了。不过,就算到了这一步,有些同学可能还没有完全想通——
那么,似乎还有第四个问题:如何用代理演算体把Galaxy传来的任意消息目标都异步赋给一个固定的引用?这里依然还是出现了“任意”关键字啊。这是我创造的这条道路上的最后一个障碍了。
让我们来碾碎这最后一个障碍吧。因为,不要忘记,Galaxy传递过来的消息目标本身就是一个演算体。那么既然它是演算体,自己也可以进行引用操作,它们可以用"::Self"这个系统引用来指代自己。这样,只要用触发器对它们发送消息,让他们把自己赋给一个固定的全局引用A,然后代理演算体就可以直接对固定的全局引用A进行操作,在满足条件的玩家的电脑上,将其值再赋给固定的引用B即可。
呼,大功告成。于是,让我们把整个过程的思路整理如下吧,这也就是我的整个演算体相位技术的真正核心了。
异步演算体消息大致思路:
触发编辑器中:为不同玩家锁定不同的飞行单位映射->创建代理演算体->对将消息目标发送消息,让它把自己赋给全局引用A->对代理演算体发送信号X->将引用B指向的对象赋给脚本变量W->对W发送想要发送的任意信号->销毁代理演算体->解锁飞行单位映射。
数据编辑器中:在数据编辑器里设定好代理演算体的事件,让它在收到信号X时,判断当前电脑上的飞行单位映射,如果为All(全部),则将引用A指向的对象赋给引用B。
进一步优化
但是按照以上思路来做,仅仅是能达到目的而已。在上一篇里我们可以这样乱来,因为那在我这整个相位技术体系当中只是入门级别。但在本篇里,我们却需要好好地进行严肃对待了。所以我们需要进行一些微调和优化:
我们这个自定义函数是为了能对目标演算体发送异步消息。但是大家可能已经注意到了——我们在发送真正要发送消息之前,实际上已经对目标演算体发送了另一个消息:即让它把自己赋给全局引用A。这可不是件好事,我们在上一篇里可以这么做,因为上一篇还仅仅是学习用的。而本篇的自定义函数要考虑到实际使用时遇到的多种状况。比方说,如果目标演算体本身正好自带了个演算体事件,触发条件刚好是收到“设置引用”的消息怎么办?另外,由于在引用传递过程中,需要设置全局引用,这对actor系统来说又是另一个影响,这些都会破坏自定义函数的黑箱,如果你把这函数拿去做通用函数,必然会造成函数使用者预期之外的影响。
好消息是,其实我们可以用Galaxy来直接设置actor引用,而不需要通过发送消息的方式。虽然GUI里找不到这样的动作,但其实Galaxy有这样一个函数可以直接设置引用。
[codes=galaxy]native void ActorRefSet (actor a, string refName, actor aValue);[/codes]
由于没有GUI支持,我们得用自定义脚本,但是使用这个函数得来的好处远远大于这一小小的麻烦。尤其是,这个函数可以把一个演算体变量指向的对象直接赋给目标引用!还可以指定引用所在的演算体。还记得我之前为什么说需要全局引用么?因为用发送演算体消息的方式,只有用全局引用才能在两个不同演算体之间传递引用。但是用了这个函数,可以直接把所有引用赋值都限制在代理演算体里。这样就用不着任何全局引用,只需要代理演算体上的“局部”引用就好。而且该函数不会发送消息,也就不会在目标演算体身上引发任何预料外的演算体事件。
实际的自定义函数和代理演算体的内部代码
以下就是我的通用自定义函数Send Actor Message To Actor For Player Group及其相关代理演算体的实际代码了。可以对任意指定演算体发送异步消息,我计划将其加入到GAx3 Mod了。并附注了本函数中需要注意的几个额外知识点:
注:实际演示地图见板凳楼
[trigger]
Send Actor Message To Actor For Player Group
Options: Action
Return Type: (None)
Parameters
Players = (Active Players) <Player Group>
Actor <Actor>
Message <Actor Message>
Grammar Text: Send message Message to actor Actor for Players
Hint Text: (None)
Custom Script Code
Local Variables
Agent = ActorCreate(null, "TriggerPerPlayerActorAgent", null, null, null) <Actor>
ActualTarget = No Actor <Actor>
OtherPlayers = (Active Players) <Player Group>
Actions
Player Group - Remove all players in Players from OtherPlayers
General - Custom Script: ActorRefSet(lv_agent,"::actor.MsgTarget",lp_actor);
UI - Lock flyer helper display for players in Players to All
UI - Lock flyer helper display for players in OtherPlayers to None
Actor - Send message "Signal SetTargetActual" to actor Agent
UI - Unlock flyer helper display for players in (All players)
Variable - Set ActualTarget = (Actor connected to Agent via reference "::actor.MsgTargetActual")
Actor - Send message "Destroy" to actor Agent
Actor - Send message Message to actor ActualTarget
[/trigger]
仅仅9条动作。是不是觉得很奇特啊,我里两篇教程这么多字,结果写成代码竟然只有9行动作……
额外知识点1:ActorCreate().这里我们直接使用ActorCreate()函数来创建代理演算体,而不是像上一篇一样用向目标演算体发送创建消息的法子。这样可以确保不对目标演算体发送任何预期外的消息。
额外知识点2:飞行单位映射。大家可以发现,在本函数中,锁定飞行单位映射的动作是写在了中间,夹在了对代理演算体发送消息动作的前后,而不是像前一篇那样分别写在最上和最下。这是为了最大限度的减少锁定飞行单位映射对目标演算体的条件判断所造成的污染。
额外知识点3:引用中的“actor”命名空间。大家可以发现,本函数中用到的引用名字都以"::actor."开头,这是因为"actor"命名空间可以限制引用的作用域,这样一设置后,这些引用就只会在代理演算体中生效,不会污染到整个演算体系统了。
额外知识点4:当临时代理演算体改成常驻型时要注意的事情。由于本函数是以成为通用函数为目标设计的,所以代理演算体每次都是临时创建,并在完成任务后立刻销毁。但是某些同学在实际使用的时候可能会想要把它改造成常驻状态,这样就不需要每次创建和删除了。但是这里要特别注意一下的是,如果这样做的话,务必要记得每次重置"::actor.MsgTargetActual"引用,否则在某些未符合条件的电脑上,这个引用可能会错指向上一个演算体,而造成一片混乱的问题。这个问题在临时创建/删除的代理演算体身上并不存在,因为它们身上的引用会直接随着代理演算体的销毁而被销毁。
代理演算体的代码:
[codes=xml]
<CActorSimple id="TriggerPerPlayerActorAgent">
<On Terms="Signal.*.SetTargetActual; FlyerHelper All" Send="RefSet ::actor.MsgTargetActual ::actor.MsgTarget"/>
</CActorSimple>
[/codes]
只有一句话。轻松愉快。只是用来异步设定目标演算体的而已。
因此,这整个实现了异步发送任意消息的系统,其实只有区区9句触发器动作加1句演算体事件而已。但是内里所包含的知识量却是如此的庞大,以至于我需要写上整整两篇这么多字的教程才能把它们说清楚。其实我这个人很懒,这里总共10代码,因此要我把这两篇教程精简一下,我也能把它们浓缩到10句话以内,但是这样的教程写出来只有自己能看懂,根本毫无意义。 而就算是写到现在这么多,恐怕也无法让所有人能看得明白。因为实际上读者还是需要不少Actor基础才能完全看明白我这整部教程呢。
不管能看明白多少,希望大家都能有所收获。
其实本系列还有第三篇的,不过并非是提高篇,第二篇已经拔得够高了。所以,为了让大家能放松下第三篇的内容将会很短,而且超简单,算是外传性质的,是告诉大家如何对一个区域内所有符合条件的演算体全体发送异步消息的方法。原理也很简单,将是本系列三篇之中最简单的一篇教程。 |
|