三国群英2吧 关注:43,925贴子:999,194

【脚本基础教程】第七章:物理引擎(地火 / 地焰涌 / 地焰星燃)

只看楼主收藏回复

视频来自:百度贴吧


IP属地:智利1楼2020-03-29 11:58回复
    到目前为止,我们还没有涉足原版武将技中最大的一块:“指向性”杀兵技。例如,每次发射龙炮时,炮弹的落点都会集中在人群附近。因此,在这一章中,我们将以一个“指向性”的杀兵技作为框架。
    在知识点上,除了介绍如何把武将技送进人堆以外,本章我们主要介绍物理引擎相关的内容,以及球坐标相关的函数。我们将了解如何使用速度、重力和阻力,以及无重力标记的使用。
    这将是一个“足够复杂”的武将技;事实上,它也是本教程中最复杂的武将技案例。不过,读者也不用太担心:我们会一点一点地把这个武将技完成,并且,这一过程需要的新知识并不是太多。
    我们先准备本章所需的素材。读者可以到资源文件里下载到相应的图形文件;除此之外,笔者还准备了一个Things ini片段(见All-Things点ini),读者可以将该片段中的内容直接复制粘贴到自己的Things ini中。
    本章的武将技是一个三级武将技系列:地火、地焰涌、地焰星燃。我们先在Magic ini中配置它们,如下:
    [MAGIC]
    SEQUENCE = 115
    NAME =地火
    MP =26
    POWER =100
    ATTACK =8
    SCRIPT =804
    ATTRIB =全體,主將
    TITLE =
    NOTE =地火,L1
    ACTIVE =敵方全軍
    [MAGIC]
    SEQUENCE = 116
    NAME =地焰涌
    MP =37
    POWER =100
    ATTACK =8
    SCRIPT =805
    ATTRIB =全體,主將
    TITLE =
    NOTE =地火,L2
    ACTIVE =敵方全軍
    [MAGIC]
    SEQUENCE = 117
    NAME =地焰星燃
    MP =46
    POWER =100
    ATTACK =8
    SCRIPT =806
    ATTRIB =全體,主將
    TITLE =
    NOTE =地火,L3
    ACTIVE =敵方全軍
    在实际开始之前,我们先通过一楼的视频了解一下我们的大致目标。在这个武将技中,伴随着地面的抖动,地面将会裂开若干(1/2/3)道裂缝,从裂缝中喷出一束高火苗;一团团岩浆火球从地面的裂缝中喷涌而出,落向四周的地面,给周围的士兵和路过的主将造成杀伤。火球落地后,残余的岩浆还将持续存在一小会儿,烫死任何试图踏入其中的士兵。
    读者可能注意到,这个武将技和龙炮有不少异曲同工之妙;事实上,该武将技中飞溅的火球的效果的确参考了龙炮。除此之外,地震的效果参考了大地狂啸。


    IP属地:智利本楼含有高级字体3楼2020-03-29 12:16
    回复
      一、批量载入图形
      在原版武将技中,大部分的武将技的开头都会在标准流程之外加上一条BatchLoadShape,意为批量载入图形。在目前的机器上,是否使用该功能已经没有明显区别了;不过我们还是介绍一下,同时把我们的武将技基本框架搭起来。
      BatchLoadShape函数用于一次性将指定目录下的SHP文件载入到内存中。其定义如下:
      void BatchLoadShape(string path);
      通常情况下,我们见到的是带通配符的形式,例如:
      BatchLoadShape("magic\\115\\*");
      BatchLoadShape("magic\\033\\*");
      在这里,我们使用了双反斜杠"\\"来表达一个反斜杠的含义,这是因为反斜杠是转义字符。上面的代码中,我们载入了我们将使用的地火系武将技的图形物件,以及龙炮系武将技的图形物件。
      武将技的框架搭建如下——没什么特别的地方,除了主函数多出来了一个参数,表示这是第几级武将技:1/2/3分别表示地火、地焰涌、地焰星燃。


      IP属地:智利本楼含有高级字体4楼2020-03-29 12:21
      回复
        二、寻找武将技的打击位置
        对于“指向性”杀兵技,寻找武将技的打击位置有多种方法,但一般而言,它的第一步总是从寻找人堆开始的。
        在三国2脚本中,寻找人堆通常由GetMostImportantSoldier函数实现:它直接返回某一方的所有士兵中最“关键”的那一位。
        int GetMostImportantSoldier (int isLeft);
        其中isLeft表示是哪一方的士兵,1表示红方(左方/电脑方),0表示蓝方(右方/玩家方)。
        怎么样才算“关键”呢?EXE会计算每一个士兵的“关键度”:士兵身边的友军士兵越多,这个士兵就越关键。更具体地,在士兵周围的7×7的矩形范围内,各单位友军士兵对该士兵的“关键度”的贡献如下:

        因此,作为我们的武将技的第一步,我们也先找到人堆中最“关键”的士兵。我们将在该士兵的脚底下展开地面裂缝,喷出火苗和岩浆;这样,飞溅的岩浆火球将充分地打击该士兵周围的人堆。
        //得到地火喷发点(人群密集处)
        int centerSoldier = GetMostImportantSoldier(!intvIsLeft);
        int centerX = GetObjectScreenX(centerSoldier);
        int centerY = GetObjectScreenY(centerSoldier);
        在上面的代码中,我们使用 !intvIsLeft(等价于intvIsLeft ^ 1,奥汀比较喜欢后面这种写法)作为参数,指定获得对方人堆里的士兵;如果得不到这样的士兵,就直接指定对方主将为目标。然后,将该目标的X/Y坐标记录下来。
        另一种方法是得到随机的士兵:
        int GetRandomSoldierHandleFromAlive (int isLeft);
        这种方法看起来比较“儿戏”,毕竟随机一个的话,指不定就得到人堆边缘的了。不过,配合下面的系统函数,可以反复挑选随机士兵,并选出周围士兵数“足够”多的那个:
        int GetForceCountInRect (int isLeft, int x0, int y0, int width, int height);
        该函数的含义有一些微妙:它的意思是找出以战场坐标 (x0, y0) 为中心,坐标在 [x0-width,x0+width], [y0-width, y0+width] 矩形范围内的士兵数量。我们可以通过该函数检查得到的随机士兵周围的人数。
        同样,配合上面的函数,可以获得给定矩形范围内所有士兵的物件:
        int GetNthForceInRect (int isLeft, int x0, int y0, int width, int height, int n);
        其中n表示该范围内第n个士兵(从第0个开始数到第N-1个,N为区域内士兵个数;从矩形区域左上角开始一列一列竖着扫。当n超出范围时,会返回最后一个士兵)。


        IP属地:智利本楼含有高级字体6楼2020-03-29 12:28
        收起回复
          在我们的武将技中,为简单起见,只指定人堆当中就足够了。事实上,有相当一部分原版的多级武将技就是这么处理的;例如,冰岚刃舞就是连续三次瞄准人堆。不过,我们还希望喷出的火焰不要出现在对方主将的马下,否则这样都没被烧死似乎说不过去;因此,我们还加入了一些特判的代码。
          我们将在“地火”武将技中大量使用随机函数。该函数很简单,生成一个[a, b]之间的随机整数(取值可能性是均匀的):
          int Rand (int a, int b);
          下面的代码中,我们首先获得了人堆中的士兵。为防止意外,如果没有获得士兵(centerSoldier == 0),则先指定主将为目标。但是,接下来继续时,如果发现果真要指向主将,则调整centerX和centerY的值:首先随机决定x/y坐标变化量的符号(当Rand(0, 1) == 1时为正,否则为负),紧接着把坐标变化量也随机出来(Rand(100, 200)和Rand(50, 130)),让目标点离主将脚下远一点。
          最后,我们将摄像机平移(MoveCamera)到目标位置点,准备下一步制造地裂和火焰喷发。
          下面的代码填入我们预留的武将技主体部分:


          IP属地:智利7楼2020-03-29 12:32
          回复
            编译运行,可以看到摄像机指向了对方人堆中的士兵,即GetMostImportantSoldier函数得到的士兵。


            IP属地:智利8楼2020-03-29 12:36
            回复
              三、实例推进:制造地裂效果
              我们将在上一节得到的 (centerX, centerY) 位置处制造地裂效果。在素材中,笔者为地裂效果准备了4组素材,每组2个物件。我们将用到的物件在Things ini中的定义如下:
              [OBJECT]
              Name = 地裂01
              Sequence=11501
              Type = %TYPE_FORCE
              Space = 0,0,0,0
              Flags = OF_DARKLIGHT
              Process= %MagicObjectProcess
              Directory= \magic\115
              Wait = #9999, leak01
              [OBJECT]
              Name = 地裂岩漿01
              Sequence=11511
              Type = %TYPE_FORCE
              Space = 0,0,0,0
              Flags = OF_WHITELIGHT
              Process= %MagicObjectProcess
              Directory= \magic\115
              Wait = #9999, leaklava01
              以及类似格式的地裂02-04、地裂岩浆02-04。“地裂”一组的编号为11501-11504,“地裂岩浆”一组的编号为11511-11514,它们是一一对应的。
              (如下分别是“地裂01”和“地裂岩浆01”两个物件的图形。这里,由于裂缝本身是黑色的,不能给背景填入黑色后用WHITELIGHT去除背景;但是素材中又存在“半透明”的黑色部分,而SHP文件是不支持任何半透明像素的。因此,笔者将裂缝素材反白(反相),然后用DARKLIGHT(减色)标记将白色的部分“减去”——任何颜色减去白色都等于黑色,而任何颜色减去黑色都不变。这样就得到了边缘半透明的黑色裂缝了。
              下边的“地裂岩浆”则使用了正常的WHITELIGHT标记去除黑色背景的方法。它的所有色彩都在上边的“地裂”的白色部分以内;因此,当两个物件叠在一起时,橙红色的岩浆图案将被盖在黑色的裂缝图案之上,且黑色的裂缝加上岩浆颜色仍等于岩浆原本的颜色。)



              IP属地:智利本楼含有高级字体9楼2020-03-29 12:41
              回复
                我们来着手生成这些物件。我们先随机决定使用1-4中的哪一组地裂效果,然后,在上一节中得到的目标点处,生成对应的物件leak和leaklava:
                // 制造地裂和地裂里的岩浆
                int leakNo = Rand(1, 4);
                int leak = CreateObjectRaw(centerX, centerY, 0, 0, 11500 + leakNo);
                int leaklava = CreateObjectRaw(centerX, centerY, 0, 0, 11510 + leakNo);
                紧接着,我们需要制造一个竖直的、喷涌而出的火焰。我们使用的是八面火的火焰,物件编号为19002:

                int fire = CreateObjectRaw(centerX, centerY, 0, 0, 19002);
                这个火焰有点太小了。我们期望看到的是一个“主火焰”,但八面火的火焰本身并不是很大。因此,我们还需要用SetObjectScale调整其大小,使之达到原来的1.5倍高度:
                SetObjectScale(fire, 0x10000 * 3 / 2, 0x10000 * 3 / 2);
                这样就大概像回事了。
                最后,当火焰喷出时,其上的士兵自然是活不成的。因此我们还要对fire物件增加打击目标标记和回调函数:
                //主火焰有杀伤
                SetObjectFlags(fire, OF_ATTACKENEMY);
                SetCallbackProcedure(fire, 11501);
                回调函数的编写暂时比较简单。我们分两种情况判断,一种是主将,一种是士兵:对于主将,我们用DoHarmToMajor造成伤害,然后用magic cpp自带的HitGeneral()制造击中主将的效果,11002是Things ini自带的“橙色”魔法击中主将的爆炸效果的编号;对于士兵,我们直接调用FireMan函数让士兵着火而死。
                这里的一个小细节是,击中之后,我们将删除物件打击主将的标记,因为我们希望火焰只对主将造成一次伤害。注意到我们之前指定的是OF_ATTACKENEMY标记,它是OF_ENEMYGENERAL和OF_ENEMYFORCE的组合;因此清除OF_ENEMYGENERAL后,OF_ENEMYFORCE标记仍然存在,并不影响该火焰继续打击士兵。总的来说,这部分的内容和上一章中的回调函数差不多,而且省去了销毁物件和制造中毒效果的部分。


                IP属地:智利10楼2020-03-29 12:46
                回复
                  目前的整体代码如下(入口函数不再贴上,红字代表新增部分):



                  IP属地:智利11楼2020-03-29 12:50
                  回复
                    四、设置球坐标下的速度
                    下一个目标是从地裂处喷出岩浆火球(熔岩弹)。
                    在上一章中,我们已经使用过SetObjectSpeed_Cylind函数设置物件的速度。但是,对于我们的需求而言,更直觉的做法是,确定初速度和抛出的角度;如果使用SetObjectSpeed_Cylind,还需要经过换算。
                    我们使用SetObjectSpeed_Sphere函数来直接指定抛出的角度和初速度。该系统函数的定义如下:
                    void SetObjectSpeed_Sphere (int object, int theta, int phi, int speed);
                    其中theta又可视作dir。它的意思是,给定 theta (θ) 和 phi (ϕ) 两个角度,确定一个方向,并将物件在该方向上以speed速度抛出。

                    上图显示了球坐标下指定抛出速度的示意图。其中,原点是抛出点,红色的OA粗线表示抛出的方向;为了指定抛出的方向,我们需要两个角度 θ 和 ϕ :其中,参数里先指定的 θ (theta) 是地面上转过的角度,也即通常我们指定dir时所指的角度;后指定的 ϕ (phi) 则是OA方向和地面的夹角。
                    数学上,这样得到的速度向量在x/y/z轴上的三个分量分别为

                    所有角度的取值都是0=0°,64=90°,128=180°,以此类推。因此,theta一般取[0, 256)(也即0度到360度),而phi一般取[-64, 64](也即-90度到90度;为负数时,自然就是向斜下方抛射了)。
                    (注:球坐标系的定义有许多差别不大的版本,并且两个角度的希腊字母也可能不一样。这里笔者按照自己习惯的方式使用希腊字母,但具体指的是哪两个角度是明确的。)


                    IP属地:智利本楼含有高级字体13楼2020-03-29 12:59
                    回复
                      类似地,我们也可以用SetCoordinateByReference_Sphere函数来根据参考物件调整物件的位置,该函数的逻辑也是球坐标的逻辑:
                      void SetCoordinateByReference_Sphere (int object, int referenceObject, int theta, int phi, int radius, int zOffset);
                      该函数表示以referenceObject为参考原点,将object物件移动到由theta, phi指定的、距参考原点距离为radius像素的位置;紧接着,再在z轴方向上移动zOffset像素(这可能有一些奇怪,但是该函数确实是这么定义的)。
                      我们来着手抛出火球。我们编写一个ThrowLava函数专门用来做这件事;它有五个参数,分别表示抛出火球的原点(即喷火的中心点)x/y坐标、抛出火球的水平面方向dir(即theta)、抛出火球方向与水平面的夹角phi,以及火球的初始速度initSpeed ——
                      void ThrowLava (int centerX, int centerY, int dir, int theta, int initSpeed) { /* ... */ }
                      我们使用的火球素材沿用了龙炮系中的素材,即43004号。这个素材很小:

                      我们直接创建这个物件,然后,利用SetObjectSpeed_Sphere函数,将抛出的参数设置好并抛出:
                      //创建熔岩弹
                      int lavaSplit = CreateObjectRaw(centerX, centerY, 0, 0, 43004);
                      //按照给定的三维方向和初速度掷出
                      SetObjectSpeed_Sphere(lavaSplit, dir, phi, initSpeed);


                      IP属地:智利本楼含有高级字体14楼2020-03-29 13:15
                      回复
                        不过,仅仅是这么小的一颗熔岩,效果多少差了一些。我们希望抛出熔岩火球时能带有一道“尾迹”;幸运的是,龙炮系武将技中,当炮弹落地时,会向周围六个方向飞溅出火星,该火星正好就带有类似的尾迹。因此我们可以仿照现有的解决方案。
                        我们回顾一下龙炮系武将技中的做法:

                        可以看到,每个火球实际上是6个小火球连在一起;这样可以形成“拖尾”的效果。同时,越往后的小火球,其不透明度越低(越透明)。因此我们也可以如法炮制:


                        IP属地:智利15楼2020-03-29 13:20
                        回复
                          接下来需要做的,就是在主函数中确定参数并调用这个函数了。这里,我们以6 Tick为间隔,以随机方向、随机角度和随机初速度向外抛射火球,连续抛出24次:

                          这里的参数已经调好。水平面上的方向可以取[0, 255]之间的任意随机数,亦即火球可能向四面八方任意方向抛出;phi值,也即抛出方向和水平面的夹角,在40-60之间,大致为55°-85°;初速度在10和15像素每Tick之间。
                          编译运行。可以看到,我们成功地抛出了多个小球;小球在落地之后,会以匀速向四面八方滚动。


                          IP属地:智利16楼2020-03-29 13:24
                          回复
                            很新颖


                            IP属地:广东来自Android客户端17楼2020-03-29 14:20
                            回复
                              五、重力
                              在三国2脚本中,我们无法像指定速度一样指定加速度。物件的位置坐标有三个维度,速度也可以是任意方向、任意长度的向量;但是加速度却不是这样。事实上,三国2脚本中只有两个加速度维度:重力和阻力
                              我们先从重力开始。默认情况下,三国2脚本中的所有物件都会受到重力作用,除非为物件指定OF_NOGRAVITY的标记。这一标记的用途相当广泛,以至于奥汀特意设计了一个专门的系统函数来设置和清除无重力标记:
                              void SetObjectHasGravity (int object, int hasGravity);
                              其中hasGravity表示物件是否受重力影响,为1则受重力影响,为0则不受。
                              在上一节中,我们已经看到了物件受到重力影响的结果:虽然我们总是向斜上方抛射火球,但是火球仍然在空中划出了一道抛物线,最终坠落到地面上。重力即是物件在竖直方向(z轴指向下的方向)上的加速度。
                              我们还注意到,当火球落地时,垂直方向的速度立即归0,火球开始“无摩擦”地向四面八方滚去。这是因为群2的z坐标不能为负数。因此,一旦物件触地,坐标为0,无论原先竖直方向上的速度是多少,竖直方向速度都必须归0;但水平方向上的速度仍然保持不变,因此物件会继续“无摩擦”地往外滚。此外,落地时也没有反弹的效果。


                              IP属地:智利本楼含有高级字体18楼2020-03-29 14:36
                              回复