三国群英2吧 关注:45,920贴子:1,015,475

回复:【脚本基础教程】第六章:异步与回调(武将技:毒箭)

只看楼主收藏回复

五、回调函数
回调函数是这样一类函数:它在被设置了打击目标标记的物件击中目标时被调用。
在上面的例子中,我们的箭支被设置为OF_ATTACKENEMY标记,亦即它会同时击中小兵和主将;不论击中的是小兵还是主将,都会触发回调函数机制。之所以没有触发,是因为我们没有设置回调函数。
设置回调函数,需用到SetCallbackProcedure函数:
void SetCallbackProcedure(int object, intcallbackFuncCallsign);
在这里,第一个参数是回调函数对应的物件:当该物件击中目标时触发回调函数。第二个参数是回调函数的调用代码,也即callsign。注意,输入的是调用代码,不能将函数名直接填入参数中。
对应地,回调函数总是应该接受两个参数。参数名没有限制,但必须都为int型;回调函数不应有返回值。第一个参数接收打击目标的物件(在我们的例子中是箭支);第二个参数则接收被打中的物件(主将或士兵的物件)。
我们来实际操作一下。如下是我们创建的回调函数的定义;函数体暂时留空——

接着,为了配置这个回调函数,我们在主函数中,在设置好打击目标标记后,使用SetCallbackProcedure为箭支指定回调函数:

(通常而言,在奥汀的设计中,武将技的“内部函数”(亦即不在magic点ini中由SCRIPT=XXX调用的函数)的调用代码为四位数或五位数,其中,千位和万位的部分是奥汀的武将技“内部编号”,后面则从1开始递增。调用代码的位数有助于区分函数的作用,因此笔者也建议在此类函数上使用五位数的调用代码。在设置好编号后,可以打开magic.cpp按Ctrl+F查询,看看有没有函数已经用过了这个调用代码。不过,即使没有发现代码冲突,也可能会出现无效的情况,原因尚不明确;目前的经验表明,使用五位数的调用代码通常是比较安全的。)


IP属地:中国香港本楼含有高级字体16楼2020-03-23 06:03
回复
    我们已经为我们的物件设置好了回调函数。接下来,我们要编写这个回调函数,也就是当箭支射中对方主将时需要做的事。
    击中目标之后,我们需要做什么呢?
    1. 首先,作为一个打击主将的武将技,我们希望对目标主将造成伤害;
    2. 造成伤害的同时,制造一个击中的动画效果;
    3. 为了阻止箭支继续飞下去,我们需要销毁箭支物件;
    4. 注意到在销毁箭支的同时,正在运行的LockCameraOnObject函数中,while循环的条件是箭支物件存在,因此,销毁箭支即会导致摄像机解除锁定;
    5. 同时,和箭支伴飞的绿色光芒物件也是需要销毁的;
    6. 作为“毒箭”,我们希望除了击中的伤害外,还对主将造成持续的杀伤。


    IP属地:中国香港17楼2020-03-23 06:07
    回复
      2026-01-16 08:20:39
      广告
      不感兴趣
      开通SVIP免广告
      本节中,我们先实现1-4。理论上,我们可以直接FreeObject、DoHarmToMajor、CreateObjectByReference一套带走,最后为爆炸效果做一个手动动画;不过,奥汀已经预先编写了一些函数,可以简化我们的代码。
      我们将用到以下系统函数:
      void SetObjectFadeOut (int objectHandle, int fraction, int frames);
      该函数用于为物件创建淡出效果,初始为100%的不透明度,然后每过frames Tick,不透明度下降100%的fraction分之一。待完全淡出后,自动将物件销毁。该函数不会阻断脚本的运行,调用该函数后,照常直接执行下面的代码,不会等待淡出结束。在这里,我们希望在2 Tick之内将箭支彻底淡出掉,因此我们可以使用:
      SetObjectFadeOut(arrow, 2, 1);
      亦即每1 Tick将不透明度下调1/2。
      紧接着,我们将对主将造成伤害。我们直接调用magic点cpp里预置的Hurt函数,它同时完成三个功能,即造成伤害、清除物件打击主将标记,以及播放击中的音效:
      void Hurt (int object, int targetMajor, int attackValue);
      该函数的含义是,使用武将技物件object打击主将targetMajor,造成attackValue点伤害。因此我们可以直接写下:
      Hurt(arrow, intvDefenderMajor, intvMagicAttackValue / 3);
      最后,我们希望创建击中的“爆炸效果”。这一部分我们可以直接调用预置的HitGenereal函数。它的定义如下:
      void HitGeneral (int magicObject, int target, int effectSequence, int ballType, int zOffset, int delayTime);
      这个函数有6个参数。其中,第一、第二个参数表示造成伤害的物件和被击中的主将;第三个参数effectSequence表示爆炸效果物件在Things点ini中的原型的sequence,该函数会自动创建多个这样的爆炸效果物件;第四个参数ballType表示以何种方式创建爆炸产生的“小光球”(见下图中橙黄色的碎屑),填1、2、3分别表示三种不同的方式,填0则表示不创建“小光球”。第五个参数zOffset表示爆炸效果距地面的高度。最后一个参数是爆炸效果拖延的“时间”,即该效果结束之后多少时间内,新的HitGeneral函数造成的效果无效。

      对于我们的需求而言,我们已经定义好了爆炸的绿色特效物件原型(即13102号“毒箭爆炸的光”),并且希望看到飞泻的小碎屑(这样可以反映“击中”的伤害),因此我们只需写下:
      HitGeneral(arrow, target, 13102, 1, 60, 0);


      IP属地:中国香港18楼2020-03-23 06:10
      回复
        这样就完成了上面的1-4的内容。整个回调函数如下:

        得到的效果正如上面的图所示,击中时,造成10点伤害。


        IP属地:中国香港19楼2020-03-23 06:16
        回复
          在结束本节之前,需要说明几点:
          1. 回调函数对被击中的所有主将和士兵都有效。在这个例子中,我们没有考虑被击中的士兵,因为该武将技本身设计上并不打击士兵;不过,我们可以设置目标为士兵的情况。一般而言,作用于士兵上的回调函数主要是为了给士兵制造死亡效果(因为回调函数触发时,士兵已经被武将技物件击中而死亡),如被烧死、被地泉冲上天、被旋龙卷上天等。
          在下面的例子中,我们暂时不对毒箭的打击目标和z轴高度进行调整;相反,进一步地,我们为回调函数添加了一个else分支,用来处理击中士兵的情况:

          我们使用了magic点cpp预置的FireMan函数,以让士兵“燃烧”起来:
          void FireMan (int soldierHandle, int fireObjectSequence, int duration);
          其中,第一个参数指定着火的士兵物件,第二个参数指定了“火”的sequence,其中,10015为橙黄色的火,10016为紫色的火;第三个参数指定了士兵要烧多久,一般为60 Tick。
          得到效果如下:

          2. 回调函数可以被触发多次;击中几个目标就被触发几次。从上面的例子中也可以看出来,对站在最前面的第一个士兵触发回调函数,并不影响击中第二个士兵时再次触发回调函数,第二个士兵身上仍然是着火的。
          3. (显而易见地,)类似于异步执行的函数,回调函数的执行与其它函数的执行是独立的、同时的,互不干扰。


          IP属地:中国香港20楼2020-03-23 06:20
          回复
            六、物件额外参数
            (注:注册命名为Callback Context。Context一般译作“上下文”,不过笔者觉得该译法过于强行。事实上,这个参数不仅仅被用于回调函数关联的物件;因此,甚至连Callback Context这个命名也是不完整的。笔者根据其在三国2脚本中的通常用法称为“物件额外参数”,同时新增Object Context的命名。)
            在上面的例子中,我们碰到了一个问题:我们没办法在回调函数中获得和弓箭伴飞的光芒物件,我们只能获得造成杀伤的物件(箭支本身)和打击目标(主将)。有什么办法能让主函数把这个物件的句柄传递给回调函数呢?
            在三国2脚本中,这类需求通过设置物件的额外参数来完成;物件额外参数是一类与物件关联的人为设置的属性。在这个例子中,我们希望当击中主将时,除了造成杀伤的物件本身以外,回调函数还应知道需要同时删除的光芒物件;因此,我们可以在主函数中,将光芒物件设置为箭支物件的额外参数,这样只要回调函数可以获取到箭支物件,就可以顺藤摸瓜得到光芒物件。
            我们使用SetObjectContext函数来设置物件额外参数(注2):
            void SetObjectContext (int objectHandle, int contextNo, int value);
            该函数有三个参数。第一个参数是用来触发回调函数的物件,第二个参数表示这是几号额外参数,第三个参数传入物件额外参数的值。受底层限制,一个物件最多只能有3个额外参数,因此contextNo必须为0、1、2中的一个。
            我们可以用GetObjectContext函数取得物件额外参数的值:
            int GetObjectContext (int objectHandle, int contextNo);


            IP属地:中国香港本楼含有高级字体21楼2020-03-23 06:23
            回复
              下面我们来着手解决毒箭光芒的问题。首先,在主函数ShootPoisonArrow中,我们将箭支的高度和打击目标恢复:

              紧接着,在主函数使用SetCallbackProcedure设置回调函数之后,我们加入一行,将光芒物件设置为0号额外参数:

              在回调函数中,我们使用和箭支类似的方法,创建一个短达2 Tick的淡出效果,完成对光芒物件的销毁:

              由于不再打击士兵,我们将上一节末尾新增的else段删除,不再考虑火人问题。
              目前的整个回调函数如下:


              IP属地:中国香港22楼2020-03-23 06:28
              回复
                编译运行。现在,击中对方主将时,光芒不再会飞过主将身后。

                (注2:笔者没有测试过物件额外参数是否可以不是int型。在奥汀编写的所有代码中,还没有出现过额外参数为int以外的类型的情况;因此,目前三国2脚本伪代码编译器只支持额外参数为int型的函数定义。如果读者希望测试相应的情况,需要在编译器的config/syscall_table点txt中,改动GetObjectContext函数的返回值类型。我们将在第十章中探讨这类较深入的话题。)


                IP属地:中国香港23楼2020-03-23 06:33
                回复
                  2026-01-16 08:14:39
                  广告
                  不感兴趣
                  开通SVIP免广告
                  七、等待异步函数执行完毕
                  现在还有两个问题。
                  首要的问题是,天空似乎变亮得太早了。我们希望能够根据箭支飞行的时间,决定武将技持续的时长;目前的代码,箭支飞到一半的时候天空就会变亮,这不是我们想要的效果。但是,如果简单粗暴地在主函数中Delay一个很大的时长,则有可能击中主将后还要过很久天空才会变量,这样也不好。
                  我们希望主函数能够获知异步调用的其它函数的执行情况。在之前为了表述方便,我们一直称并行的两个函数是“互不干扰”的;但是,如果真的彻底一点也不干扰,反而会使得完成我们的任务变得很麻烦。
                  事实上,在三国2脚本中,一个函数是可以获知其它函数的运行情况的;虽然它无法直接用代码干预另一个函数的正常运行流程,但是,它可以根据另一个函数是否在运行,决定自己要不要继续运行下去。这一功能由Wait语句IsRunning语句实现:
                  void Wait (string targetFunctionName);
                  int IsRunning (string targetFunctionName);
                  (底层上,它们不是系统函数,而是脚本指令集的一部分,因此称之为“语句”。不过,使用的时候和系统函数基本没有区别。)
                  这两个函数都接收目标函数的函数名的字符串作为参数。IsRunning语句用于判断是否有目标函数正在运行,有则返回1,无则返回0;Wait语句则会阻断当前函数的执行,直到所有正在运行的目标函数都退出运行为止。
                  Wait语句的使用较IsRunning要更广泛。它对代码执行的影响可以借用时序图的格式表示为:


                  IP属地:中国香港本楼含有高级字体24楼2020-03-23 06:37
                  回复
                    在我们的例子中,唯一从箭支射出到击中目标都在执行的函数是LockCameraOnObject函数,该函数在箭支击中目标、回调函数销毁箭支后,自动解除摄像机的锁定并返回。因此,我们只需要等待LockCameraOnObject函数执行完毕,然后稍微等一会儿(等爆炸效果播完),再把天空变亮。于是我们按如下方式修改主函数,使用Wait语句等待LockCameraOnObject函数执行完毕:

                    读者应该注意到,Wait语句和IsRunning语句接受一个字符串作为参数。这是因为函数名在三国2脚本中不是常量或变量,它不能被放入表达式中;因此,我们需要用字符串的一对引号把函数名括起来。
                    我们先用Wait语句等待LockCameraOnObject结束执行。代码会中断在这里;下面的Delay语句暂时不会被执行。击中对方主将后,LockCameraOnObject结束执行,于是主函数的代码执行被释放,执行Delay(60);一句,再次等待60 Tick让爆炸效果播完,然后再让天空变白,武将技结束。
                    编译运行,可以看到,武将技现在会在正确的时间结束;不会飞到一半天空就变亮。

                    (读者可能注意到,之前天空变亮后箭支仍在飞行,此时仍可以判定击中主将,尽管武将技理论上已经结束。这是因为任何在武将技执行期间通过武将技的脚本生成的物件,都会被EXE与施放武将技的主将联系起来,因此即使天空变白后对方施放武将技,EXE仍可判定这些物件打击目标的敌我。我们将在第九章的必杀技部分充分利用这一关联特性。)
                    有一点需要特别注意: Wait和IsRunning语句获取到的运行状态,仅指通过EXE调用(如施放武将技的入口函数,以及回调函数)或通过asynccall方式调用的函数的运行状态;如果目标函数是利用常规方法调用的(例如在其它函数中以非asynccall方式直接调用),则IsRunning总是返回0,Wait语句无效。还请特别留意。


                    IP属地:中国香港本楼含有高级字体25楼2020-03-23 06:41
                    回复
                      八、通过异步调用实现中毒伤害
                      最后一个未尽事宜则是毒箭剩下的三分之二伤害。作为毒箭的效果,我们在击中目标时,只立即造成了intvMagicAttackValue的三分之一伤害,剩下的三分之二伤害将以中毒的方式陆续造成。我们专门编写一个函数PoisonHarm来结束剩下的部分:

                      回调函数中,使用异步调用(放在Hurt的后面,HitGeneral的前面):
                      // 运行中毒效果,持续造成中毒伤害
                      asynccallPoisonHarm(target, intvAttackerMajor, intvMagicAttackValue * 2 / 3 / 5, 120, 5);
                      这段代码做的事情有点多,但并不复杂。首先,我们对主将造成伤害(DoHarmToMajor),让目标主将显示被击中的效果(SetObjectAnimate),并且发出一声惨叫(PlaySound,这里是有源的音效,音量特意被我砍到了180而不是255)。紧接着,我们重新利用之前主将被击中时用到的光球,创建一个光球物件,并根据主将面朝的方向调整其位置,让该物件处于武将身体偏后10像素、距离地面70像素的位置,以显示该伤害是“中毒”造成。最后,我们添加了一个12 Tick内淡出的效果。
                      整个PoisonHarm函数的函数体是一个循环。参数interval指定了两次中毒伤害之间的时间;参数times指定了循环的次数(亦即造成伤害的次数),每次造成的伤害值由参数attackValue指定。该函数在回调函数PoisonArrowCallback中由异步方式调用;这意味着当箭支击中主将,触发回调函数后,才会开始执行该函数。
                      读者可能已经注意到,主函数不会等待PoisonHarm函数执行完毕。因此,这段代码在武将技执行结束、天空变亮以后仍然在运行,可以在天空变白后继续对对方主将造成中毒伤害。
                      这里有一个细节值得注意:我们要求调用者以参数的形式,将攻击方主将和被攻击方主将物件传入,而没有使用intvDefenderMajor等预设变量。这是因为该函数的执行期间武将技会结束;如果对方在中毒效果持续期间施放新的武将技,预设变量会被重新设置,此时反而可能造成意外的结果。因此我们需要以参数的形式把这些值固定下来。
                      (在一些以前的修改版中,曾经有人利用回天术等技巧造成对方组合武将技的反噬。这是因为当时用EXE组合武将技时,往往一个子武将技结束后,就执行了EnablePlayMagic,允许双方施放武将技,但另一个子武将技仍然在运行;此时如果我方施放武将技,会导致intvDefenderMajor指向对方主将,于是对面的武将技伤害就打自己身上了。)


                      IP属地:中国香港本楼含有高级字体26楼2020-03-23 06:47
                      回复


                        IP属地:中国香港27楼2020-03-23 06:52
                        回复


                          IP属地:中国香港28楼2020-03-23 06:56
                          收起回复


                            完成!
                            本章介绍的异步与回调相关内容是创建复杂武将技的基础,也是三国2脚本的强大之处。出于演示的目的,我们创造的是一个相对简单的武将技;但是,即使是更为复杂的武将技中进行的更为复杂的调度,其基本结构仍然是异步调用、Wait语句、IsRunning语句和回调函数等。
                            在下一章中,我们将介绍群2战场的物理引擎。我们会介绍速度、阻力和重力的作用,同时也会涉及一些球坐标的相关函数。我们还将介绍如何让我们的武将技瞄准人堆。目前我们创造的武将技都有固定的打击目标,而大多数针对士兵的武将技,其打击目标都是人群的密集处,因此需要有瞄准人堆的方法。我们将在下一章中介绍这部分内容。


                            IP属地:中国香港本楼含有高级字体29楼2020-03-23 07:01
                            回复
                              2026-01-16 08:08:39
                              广告
                              不感兴趣
                              开通SVIP免广告
                              本章引入的系统函数和官方函数
                              // 获取和设置物件的方向
                              int GetObjectDir (int object);
                              void SetObjectDir (int object, int dir);
                              // 根据参考物件、指定方向、指定半径和指定z轴偏移移动物件
                              void SetCoordinateByReference_Cylind (int object, int referenceObject, int dir, int radius, int zOffset);
                              // 在当前物件的方向上和z轴方向上指定物件速度
                              void SetObjectSpeed_Cylind (int object, int dirSpeed, int zSpeed);
                              // 为物件创建淡出效果,淡出后自动销毁物件
                              void SetObjectFadeOut (int objectHandle, int fraction, int frames);
                              // 设置回调函数,设置回调函数物件额外参数,以及获取回调函数物件额外参数
                              void SetCallbackProcedure (int object, int callbackFuncCallsign);
                              void SetObjectContext (int objectHandle, int contextNo, int value);
                              int GetObjectContext (int objectHandle, int contextNo);
                              // 击中目标武将造成伤害,取消当前物件打击目标标记,并播放击中音效(magic点cpp)
                              void Hurt (int object, int targetMajor, int attackValue);
                              // 创建击中目标武将的爆炸效果(magic点cpp)
                              void HitGeneral(int magicObject, int target, int effectSequence, int ballType, int zOffset, int delayTime);


                              IP属地:中国香港本楼含有高级字体30楼2020-03-23 07:05
                              回复