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

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

取消只看楼主收藏回复

视频来自:百度贴吧


IP属地:中国香港1楼2020-03-22 14:22回复
    本章我们讨论异步调用(asynccall)和回调函数(Callback Procedure),同时介绍打击目标相关标记。
    异步与回调是组织三国2脚本执行的核心工具;由于武将技“多线并行”的天然特性,在执行复杂武将技时,异步与回调会使得武将技的编写变得简单很多。
    我们先在Magic点ini中设置好本章的武将技:
    [MAGIC]
    SEQUENCE =114
    NAME =毒箭
    MP =23
    POWER =100
    ATTACK =30
    SCRIPT=803
    ATTRIB =主將2
    TITLE =
    NOTE =主將射箭
    ACTIVE =敵方主將


    IP属地:中国香港本楼含有高级字体3楼2020-03-23 04:34
    回复
      2026-01-16 08:20:14
      广告
      不感兴趣
      开通SVIP免广告
      一、异步调用
      所谓异步调用,指的是调用某个函数后,不等待该函数返回,继续往下执行的调用。在进行异步调用后,两个函数实际上是并行执行的。借用时序图的格式,下图说明了在执行异步调用后的运行过程:


      IP属地:中国香港本楼含有高级字体4楼2020-03-23 04:37
      回复
        在三国2脚本中,使用asynccall关键字指定以异步调用方式调用函数:
        asynccall 函数名(参数1, 参数2,...);
        和一般的函数调用不同,异步调用不会等待函数返回,目标函数的返回值没有效果;因此,异步调用必须是独立的语句,即使目标函数有返回值也不能将异步调用放在表达式的中间,不能将返回值赋给任何变量。例如,下面的做法任何情况下都会被编译器拒绝,无论func函数有没有返回值:
        int y = asynccall func();
        一个目标函数可以被多次异步调用。此时,它们具有不同的生命周期和局部变量,互不干扰。例如,上一章的前后伏兵,就通过循环多次异步调用了MyStepShow函数;虽然属于同一个函数,但每个异步调用操作一个单独的士兵,不会弄混。
        受底层限制,异步调用的目标函数,不能超过10个参数。
        下面的例子中,我们先生成了两个木轮攻的木轮,将它们错开了一下位置,一左一右。A函数异步调用了B函数,然后,等待50 Tick,将左边的木桶打上WHITELIGHT标记;B函数执行后,立即等待50 Tick,然后将右边的木桶打上WHITELIGHT标记。编译运行后会发现,两个木桶是同时变色的,说明A函数和B函数是并行执行的。



        IP属地:中国香港本楼含有高级字体5楼2020-03-23 04:52
        回复
          二、准备工作和设置物件速度
          让我们进入本章的正题。
          本章中我们使用一个“魔改”过的落月弓效果。读者可以在目录贴的资源文件里找到相应的素材,文件放在shape/magic/114/目录下。我们在Things点ini中,接在落月落日部分的下方,定义相应的武将技物件:
          [OBJECT]
          Name = 毒箭的光
          Sequence=13101
          Type = %TYPE_FORCE
          Space = 0,0,0,0
          Flags = OF_WHITELIGHT, OF_NOGRAVITY
          Process =%MagicObjectProcess
          Directory= \magic\114
          Wait = #9999, m003WaveG1
          [OBJECT]
          Name = 毒箭爆炸的光
          Sequence=13102
          Type = %TYPE_FORCE
          Space = 0,0,0,0
          Flags = OF_WHITELIGHT, OF_NOGRAVITY
          Process =%MagicObjectProcess
          Directory= \magic\114
          Wait = #4, M003GreenBallB1, M003GreenBallB2, M003GreenBallB3, M003GreenBallB4, M003GreenBallB5, M003GreenBallB6, M003GreenBallB6, @%OM_REMOVE, SCROVER
          我们还会用到一个现有物件:
          [OBJECT]
          Name = 飛行武器箭(M012)
          Sequence=13001
          Type = %TYPE_FORCE
          Space = 0,0,0,0
          Flags = OF_NOGRAVITY
          Process =%MagicObjectProcess
          Directory= \magic\003
          Wait =#9999, m003WA1


          IP属地:中国香港本楼含有高级字体6楼2020-03-23 04:56
          回复
            在射出“毒箭”武将技时,实际上会射出两个物件:一个是主要的物件,也即已有的13001号物件;另一个是随着主物件伴飞的光芒物件,即新定义的13101号物件。
            如果根据之前的思路,读者很自然地会想到利用循环移动箭支的方式,让箭支射向敌人。不过,本章中,为了介绍回调的相关内容,我们换一个思路,使用专门的系统函数来设置物件的速度。
            (武将技基本流程部分我们予以略过。)
            首先,完成基本流程后,我们根据主将的位置创建箭支,箭支的方向与主将面朝的方向相同:
            int dir = GetObjectDir(intvAttackerMajor);
            int arrow = CreateObjectByReference(intvAttackerMajor, 13001, dir, 0);
            接着,由于无法在CreateObjectByReference中顺便指定x/y轴方向上的偏移,我们使用SetCoordinateByReference_Cylind系统函数,将箭支向主将面朝着的dir方向(我方放武将技时,向左方)移动80像素,并同时向上方也移动80像素。
            SetCoordinateByReference_Cylind (arrow, intvAttackerMajor, dir, 80, 80);
            将箭支的初始位置确定好后,我们直接以箭支的位置为参考创建光芒:
            int light = CreateObjectByReference(arrow, 13101,dir, 0);
            最后,和SetCoordinateByReference函数类似,我们使用SetObjectSpeed_Cylind函数,设置物件在自身方向上的速度(之前创建物件时已经设置好了物件的方向和主将面朝方向相同),将箭支和箭支的光芒抛出:
            SetObjectSpeed_Cylind(arrow, 16, 0);
            SetObjectSpeed_Cylind(light, 16, 0);


            IP属地:中国香港本楼含有高级字体7楼2020-03-23 05:00
            回复
              在上面的代码中,我们用到了两个带_Cylind后缀的系统函数。首先,我们在创建箭支后,利用SetCoordinateByReference_Cylind系统函数,以主将为参考点、根据主将面朝着的方向,调整了箭支的位置。该函数的定义如下:
              void SetCoordinateByReference_Cylind (int object, int referenceObject, int dir, int radius, int zOffset);
              这个函数的含义是,将object物件移动到以referenceObject为参考点,dir方向上外移radius像素,并紧接着在z轴方向上(向正上方)移动zOffset像素的位置。具体可以参考下图,原点处为referenceObject的位置,而绿点处则是object物件将要被移动到的位置。

              其次,在创建了光芒之后,我们用SetObjectSpeed_Cylind函数分别设置了箭支和光芒的速度。该系统函数的定义如下:
              void SetObjectSpeed_Cylind (int object, intdirSpeed, int zSpeed);
              其中,第一个参数是被设置速度的物件,第二个参数是物件在(已经设置好的)自身方向上的速度,第三个参数是物件在z轴方向上的速度。在这里,由于两个物件在CreateObjectByReference时,都已经指定好了方向(与主将的朝向相同),因此直接设置速度为16像素每Tick即可。
              如需手动设置方向,可以使用SetObjectDir函数。它和GetObjectDir为一对:
              int GetObjectDir (int object);
              void SetObjectDir (int object, int dir);
              注意到设置物件速度时,SetObjectSpeed_Cylind函数的逻辑和SetCoordinateByReference_Cylind非常相似:它们都是先在平面上指定一个方向,然后设置该方向上的位移量或速度,最后在z轴上设置位移量或速度。事实上,这一套逻辑是柱坐标系的逻辑:这是该系统函数的名字中_Cylind后缀的由来。


              IP属地:中国香港8楼2020-03-23 05:04
              回复
                目前为止的代码如下。读者会注意到,一开始将镜头对准武将时,有一个180像素的偏移。这一设计和落日/落月是相同的。


                IP属地:中国香港9楼2020-03-23 05:09
                回复
                  2026-01-16 08:14:14
                  广告
                  不感兴趣
                  开通SVIP免广告
                  编译运行。可以看到,箭支和光芒随着武将的射箭动作而射出,并且以稳定的速度向左飞行。


                  IP属地:中国香港10楼2020-03-23 05:16
                  回复
                    三、通过异步调用锁定镜头
                    下一个问题是,在箭支飞行过程中,我们希望镜头能一直锁定箭支的位置。
                    奥汀在magic点cpp中提供了一系列将镜头锁定指定物件的函数。不过,这些函数写得多少有一些……匪夷所思。为了演示异步调用,我们按如下方式,自己写一个将镜头锁定物件的函数——

                    这个函数的逻辑稍微比直接将摄像机锁在物件上多了三步。首先,它多了一个参数yOffset,意在说明欲锁定的摄像机目标点不是物件的 (x, y),而是 (x, y + yOffset);回忆一下上一章中我们提到,想要将摄像机对准物件,摄像机的y坐标应该是物件的y坐标减去120左右,因此yOffset我们应当填入-120。
                    其次,我们希望如果物件正在飞向画面中心,则不急着移动摄像机,而是等一会儿,等待物件飞到画面中心再开始锁定。因此,我们判断物件的方向:如果物件朝右飞(dir == 0)且物件在摄像机右侧(dx > 0),或者物件朝左飞(dir == 128)且物件在摄像机左侧(dx < 0),则调整摄像机位置,否则什么都不做,让摄像机等待物件飞过来。
                    最后,为了防止出现弓箭射失,导致类似于五岳华斩卡死的现象,我们加了一个条件,如果超出战场范围则自动退出循环,解除锁定。


                    IP属地:中国香港本楼含有高级字体11楼2020-03-23 05:23
                    回复
                      完成这个函数后,接下来要做的,就是在主函数中异步调用这个函数:

                      编译运行。可以看到,镜头会自动锁定箭支,并在飞出底线后解除锁定;但是天空会在飞到一半时就变白,说明主函数在asynccall后仍然继续执行。


                      IP属地:中国香港本楼含有高级字体12楼2020-03-23 05:43
                      收起回复
                        借用时序图的格式,下图显示了本节异步调用的示意过程。其中,橙色方块部分表示代码执行,从上到下表示时间的先后顺序。


                        IP属地:中国香港13楼2020-03-23 05:47
                        回复
                          四、打击目标标记
                          在第四章中,我们手动使用KillForce函数和DoHarmToMajor函数对敌方士兵和主将进行杀伤。事实上,在群2武将技中这是比较少见的做法;更多情况下是,我们将物件指定杀伤的目标,当物件碰到(刮到)敌方士兵或主将时,由EXE来判定进行杀伤。
                          回顾上一章中我们提到的标记(Flags)。我们特意提到了这样一类标记,即打击目标标记。打击目标标记的作用是:告诉EXE,当物件坐标与战场上哪一类单位重合时(即,刮到哪一类单位时),对该单位造成杀伤——
                          OF_ENEMYGENERAL = 0x01000000 ;對敵方的武將有效
                          OF_ENEMYFORCE = 0x02000000 ;對敵方的小兵有效
                          OF_MYGENERAL =0x04000000 ;對我方的武將有效
                          OF_MYFORCE = 0x08000000 ;對我方的小兵有效
                          OF_ATTACKENEMY = 0x03000000 ;只對敵方有效
                          OF_ATTACKMY = 0x0c000000 ;只對我方有效
                          OF_ATTACKALL =0x0F000000 ;對敵我雙方都有效
                          实际上,打击目标标记只有4个:OF_ENEMYGENERAL、OF_ENEMYFORCE、OF_MYGENERAL、OF_MYFORCE。其它的标记只是这4个标记的叠加(相加)。例如,OF_ATTACKENEMY实际上是OF_ENEMYGENERAL和OF_ENEMYFORCE的叠加, OF_ATTACKMY则是OF_MYGENERAL和OF_MYFORCE的叠加;OF_ATTACKALL则是所有4个标记的叠加。
                          在这些标记中,主将标记和士兵标记又有所不同。如果打上了杀伤士兵的标记,意味着该物件只要擦到敌方(或我方、敌我)士兵,就会直接导致士兵死亡;但是如果打上了杀伤主将的标记,并不会自动对武将造成伤害。对主将造成伤害需要经过回调函数进行。
                          设置标记的方法和第五章中一样:要么,我们在Things点ini中直接设置指定的武将技物件的标记,要么,我们使用代码进行设置。由于我们射出的箭支物件是和落月弓共用的,因此,我们采取代码设置的办法,避免影响落月弓武将技的逻辑。出于演示的目的,我们暂时将打击目标标记设置为OF_ATTACKENEMY,以展示“擦到即死”的效果。我们将以下代码放在主函数中,asynccall所在行的后面一行:

                          不过,仅仅是这样还不够——因为箭支太高了,会从士兵的头上飞过去,这样是打不到士兵的。因此我们临时稍作调整,把之前调整箭支位置时的z轴偏移从80改为0:


                          IP属地:中国香港本楼含有高级字体14楼2020-03-23 05:51
                          回复
                            编译运行。可以看到,小兵“擦到即死”,但主将不会受到任何伤害。


                            IP属地:中国香港15楼2020-03-23 05:55
                            回复
                              2026-01-16 08:08:14
                              广告
                              不感兴趣
                              开通SVIP免广告
                              五、回调函数
                              回调函数是这样一类函数:它在被设置了打击目标标记的物件击中目标时被调用。
                              在上面的例子中,我们的箭支被设置为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
                              回复