三国群英2吧 关注:46,023贴子:1,016,595

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

取消只看楼主收藏回复

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


IP属地:美国17楼2020-03-23 06:07
回复
    本节中,我们先实现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
    回复
      2026-02-16 11:06:33
      广告
      不感兴趣
      开通SVIP免广告
      这样就完成了上面的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
              回复
                七、等待异步函数执行完毕
                现在还有两个问题。
                首要的问题是,天空似乎变亮得太早了。我们希望能够根据箭支飞行的时间,决定武将技持续的时长;目前的代码,箭支飞到一半的时候天空就会变亮,这不是我们想要的效果。但是,如果简单粗暴地在主函数中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
                回复
                  2026-02-16 11:00:33
                  广告
                  不感兴趣
                  开通SVIP免广告
                  在我们的例子中,唯一从箭支射出到击中目标都在执行的函数是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
                          回复
                            本章引入的系统函数和官方函数
                            // 获取和设置物件的方向
                            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
                            回复
                              2026-02-16 10:54:33
                              广告
                              不感兴趣
                              开通SVIP免广告
                              @ezhou111
                              明白了,不是编译器出错。
                              根本原因是一开始define的时候,对OAF_ATTACK的define,必须在OAF_ATTACK8的后面。否则,编译器先看到这个:
                              #define OAF_ATTACK 4
                              于是所有OAF_ATTACK会被替换成4。因为这是一次粗暴的字符串查找替换过程(而不是token查找替换过程),后面的:
                              #define OAF_ATTACK8 0x400000
                              会变成:
                              #define 48 0x400000
                              于是出错的SetObjectScale就会从:
                              SetObjectScale(v1, Rand(12288, 20480), Rand(12288, 20480));
                              变成:
                              SetObjectScale(v1, Rand(12288, 200x4000000), Rand(12288, 200x4000000));
                              两个48被替换掉了。
                              如果注意到的话,magic cpp里OAF_ATTACK的定义是在所有OAF_ATTACK2-OAF_ATTACK8的后面的,事实上就是因为这个问题。改成下面这样就好了。
                              #define OAF_WAIT 1
                              #define OAF_WALK 2
                              #define OAF_DEFENSE 8
                              #define OAF_PAIN 0x10
                              #define OAF_DEATH 0x20
                              #define OAF_WIN 0x40
                              #define OAF_ATTACK2 0x800
                              #define OAF_ATTACK3 0x1000
                              #define OAF_ATTACK4 0x2000
                              #define OAF_ATTACK5 0x4000
                              #define OAF_SPELL1 0x8000
                              #define OAF_SPELL2 0x10000
                              #define OAF_SHOT1 0x20000
                              #define OAF_SHOT2 0x40000
                              #define OAF_SHOT3 0x80000
                              #define OAF_ATTACK6 0x100000
                              #define OAF_ATTACK7 0x200000
                              #define OAF_ATTACK8 0x400000
                              #define OAF_ATTACK 4


                              IP属地:美国34楼2020-03-24 00:19
                              收起回复