三国群英2吧 关注:45,916贴子:1,015,454

【脚本基础教程】第四章:图形与战场(武将技:大喝)

取消只看楼主收藏回复

一楼


IP属地:美国1楼2020-03-14 11:19回复
    本章介绍三国2的图形系统和战场结构,并按照武将技的标准流程,实际地制作一个成型的武将技“大喝”。
    具体的效果,可以参考@hopecolor2 兄的帖子:https://tieba.baidu.com/p/6530856317
    (既然已经有现成的了,我就懒得再录了。感谢@hopecolor2 兄的分享)


    IP属地:美国本楼含有高级字体2楼2020-03-14 11:22
    回复
      2026-01-15 06:10:35
      广告
      不感兴趣
      开通SVIP免广告
      一、图形物件和Things.ini
      所谓图形物件,就是指大地图或战场上的一个图形。对于战场而言,一个主将就是一个图形物件(注),一个士兵也是一个图形物件;施放武将技时,炎龙的火龙、龙炮的炮车、生死门周围的电光等都是典型的图形物件,它们都属于武将技物件。
      事实上,我们对图形物件并不陌生:Things点ini定义了几乎所有图形物件的原型,我们实际看到的图形物件,往往都是通过Things.ini进行设置和调整的。
      在此,我们区分图形物件(实例)和图形物件原型:当我们说到“物件”的时候,指的是一个实际的物件,例如主将、士兵或者炎龙系武将技中的一个火龙;当我们说“物件原型”或者“物件定义”的时候,指的是Things点ini中定义的一个物件的原型,比如,下面就是蓝方朴刀兵步兵的物件原型:
      [OBJECT]
      Name = 朴刀兵
      Sequence= 100
      Type = %TYPE_FORCE
      Space = 8,8,64,0
      Flags = OF_MAN, OF_BIGSHAPE
      Process = %ForceSoldierObjectProcess
      Directory = \Force\
      Wait = MAW10001, #16, MAW10002, #8, MAW10003, #8, MAW10004, #16, MAW10003,#8, MAW10002, #8
      Walk = #4, MAR10001, MAR10002, MAR10003, MAR10004, MAR10005, MaR10006,MAR10007, MAR10008, MAR10009, MAR10010, MAR10011, MAR10012
      Attack = #3, MAA10002, MAA10003, MAA10004, MAA10005, MAA10006, MAA10007,@%OM_ATTACK, MAA10008, MAA10009, MAA10010, MAA10011, SCROVER
      Win =#3, MAA20002, MAA20003, MAA20004, MAA20005, MAA20006, MAA20007, @%OM_ATTACK,MAA20008, MAA20009, MAA20010, MAA20011, SCROVER
      Defense = #8, MAD10002, SCROVER
      Death = #5, MAF10001, MAF10002, MAF10003, MAF10004, #128, MAF10006, #5,MAF10007, MAF10008, MAF10009, MAF10010, #128, MAF10010, #9999, @%OM_DEATHSTOP,SCROVER
      Pain = #5, MAFS0001, MAFS0002, MAFS0003, MAFS0004, #128, MAFS0006, #5,MAFS0007, MAFS0008, MAFS0009, MAFS0010, #128, MAFS0010, #9999, @%OM_DEATHSTOP,SCROVER
      (在台湾,面向对象中的“对象”(Object)通常翻译为“物件”。在本文中,“物件”专指图形物件。具体的物件和物件原型之间的关系,有一些类似于对象与类之间的关系。)


      IP属地:美国本楼含有高级字体4楼2020-03-14 11:26
      回复
        一个图形物件有哪些要素呢?主要有下面一些属性:
        (1) 显示位置。在战场上时,为X/Y/Z三维坐标(即“屏幕坐标”,详见本章第二节);
        (2) 该物件可能的所有动画序列;
        (3) 目前在哪个动画序列里,在第几帧;
        (4) 缩放比例和透明度;
        (5) 物件的处理方式(Process),例如上面的朴刀兵定义中,指定生成的图形物件为ForceSoldierObjectProcess,亦即在EXE中,该物件按士兵的方式处理对待;
        (6) 标记(Flags),是物件的一系列特殊属性,例如是否受重力影响、武将技物件的打击目标(敌方全军等)、混合模式(混色/加色/减色)等等;
        (7) 物理引擎相关属性,即速度和阻力。
        除此之外,在大地图上,还有点击区域的属性space;我们目前不去涉及它。
        在Things点ini中,已经预先指定了生成物件时的一些属性。例如,在上面的朴刀兵原型中,已预先指定了物件处理方式(Process)、标记(Flags)和动画序列的具体信息。


        IP属地:美国本楼含有高级字体5楼2020-03-14 11:33
        回复
          我们对Things点ini中设置的内容具体介绍如下:
          Sequence: 物件原型独特的编号,用来在创建物件时指定。
          Type: 见后文。
          Flags: 即标记,指定生成物件时带有的初始标记。
          Process: 物件的处理方式。处理方式最接近于“物件类型”的概念。原版中,共设置了21种不同的处理方式;其中最常见的是ForceMajorObjectProcess(主将)、MajorHorseProcess(主将马匹)、MajorGeneralWeaponProcess(主将武器)、ForceSoldierObjectProcess(士兵)、ForceArrowObjectProcess(士兵射出的箭支)、ForceHorseProcess(骑兵死亡后的跑马),以及,本教程中最经常使用的MagicObjectProcess(武将技物件)。
          Directory: SHP文件的储存目录。例如此处为\Force\,意思是SHP文件在Shape\Force\目录下。
          Wait/Walk/Attack/...: 预设的动画序列,将各帧的SHP文件的文件名用逗号隔开。其中,#X代表从下一帧开始,每过X Tick播放一帧。默认情况下,动画序列是循环播放的,除非遇到SCROVER。
          在动画序列中,除了具体的图形文件名、帧率和SCROVER以外,我们还会看到很多额外的信息,例如@%OM_ATTACK。@%OM_ATTACK等属于物件的动作,只在武将/士兵/武器/马匹/箭支上出现,由EXE定义并处理,通常表示动画序列中“发动攻击的那一刻”之类的关键节点,我们不会过多涉及;其它的内容我们将在第九章第一节中介绍。Space我们也不会涉及——Space有实际的作用,但对武将技物件没有;对于其他物件,大多数情况我们只需照抄即可。
          最后,有必要说一下这个Type。看起来这个叫类型的东西比较唬人;但是实际上它是Things点ini里物件原型定义的类型,而不是具体的物件的类型(毕竟处理方式更接近具体物件类型的概念)。它总共有四类,即%TYPE_BASE、%TYPE_TROOP、%TYPE_FORCE和%TYPE_MAJOR。有什么区别呢?当指定%TYPE_BASE时,只会读取Wait动画序列;指定%TYPE_TROOP时,读取Wait/Walk/Win/Lose序列;指定%TYPE_FORCE时,读取Wait/Walk/Attack/Defense/Pain/Death/Win序列;指定%TYPE_MAJOR时则会读取所有序列(除了Lose,但Lose的编号其实和Death是一样的)。Type属性只在这一步发挥作用;因此,完全不妨将武将技物件指定为%TYPE_FORCE,只为了能多读取一个DEATH序列。(事实上,奥汀也是这么干的。)
          (注:主将的情况比较复杂。实际上,它是一个嵌套结构,武器和马匹是主将的子物件。因此,在操作主将时,只需对主将物件进行操作即可,被改变的属性会同时被应用到武器和马匹上。可惜的是,在三国2脚本中无法通过系统函数构造这样的嵌套结构。)


          IP属地:美国本楼含有高级字体6楼2020-03-14 11:47
          回复
            二、屏幕坐标与战场坐标
            这一节介绍坐标系。
            一个具体的物件,在战场上的具体坐标被称为屏幕坐标。这个名字有一些误导性;实际上,它并不是物件在屏幕上的具体位置,而是在战场这个“沙盒”内相对于战场原点的相对位置。
            群2的坐标属于右手系,坐标在战场的最左下方的角落,以向右为x轴正方向,以指向背景的方向为y轴正方向,以指向天空的方向为z轴正方向。如下图所示:

            (读者应该能观察到Y轴和地面纹理的走向是重合的。)
            每个战场上的物件都有X/Y/Z三维屏幕坐标。坐标要么是一个正整数(注),要么为0,通常情况下不能为负数。为简捷起见,我们称坐标的单位为“像素”。战场的长度为6816像素,Y轴上的宽度为1008像素。


            IP属地:美国本楼含有高级字体7楼2020-03-14 11:52
            收起回复
              除屏幕坐标外,另一个坐标是战场坐标。战场坐标只有两个维度,即向右的X轴和指向背景方向的Y轴;1点战场坐标就是一,它是一名主将或一个小兵所占据的位置。战场坐标同样以左下角为原点,长宽为71*14。在小地图上看得最明显:

              战场XY坐标轴和屏幕XY坐标轴是完全重合的,都以战场左下角为原点。它们之间的数量关系为:X轴上一格战场坐标等于96像素,Y轴上一格战场坐标等于72像素。因此,ScreenX = BattleX * 96, ScreenY = BattleY * 72。
              战场和屏幕坐标可以通过以下系统函数进行互相转换。实际编程中,建议尽量使用转换函数,以增强代码可读性——并且还不用去记忆数字。
              int BattleXToScreenX (int battleX); //战场X转屏幕X
              int BattleYToScreenY (int battleY); //战场Y转屏幕Y
              int ScreenXToBattleX (int screenX); //屏幕X转战场X
              int ScreenYToBattleY (int screenY); // 屏幕Y转战场Y
              同样地,三国2脚本也提供了下面四个系统函数以直接获取战场的长宽(分别在屏幕坐标和战场坐标下):
              int GetBattleWidthInScreenX (); // 返回6816
              int GetBattleHeightInScreenY (); // 返回1008
              int GetBattleWidth (); // 返回71
              int GetBattleHeight (); // 返回14
              (在附带的magic_inst.txt中,注册称为“战场宽度”和“战场高度”。这个“宽度”和“高度”指的是在小地图上的宽度和高度,也即X轴和Y轴上的长度。为统一起见,本文一律称为战场长度(X)和战场宽度(Y)。)


              IP属地:美国本楼含有高级字体8楼2020-03-14 11:57
              回复
                我们可以直接使用下列函数得到物件的屏幕和战场坐标:
                // 得到物件屏幕坐标
                int GetObjectScreenX (int object);
                int GetObjectScreenY (int object);
                int GetObjectScreenZ (int object);
                // 得到物件战场坐标
                int GetObjectBattleX (int object);
                int GetObjectBattleY (int object);
                同时,我们可以直接使用SetObjectCoordinate函数改变任意物件的屏幕坐标。函数定义如下:
                int SetObjectCoordinate (int object, int x, int y, int z);
                默认情况下,当不特别指明是战场坐标时,我们指的都是屏幕坐标。


                (注:底层上,屏幕坐标并不是严格的整数——它是有小数部分的;更具体地,它是一个int型的值,高2位作为整数部分,低2位剩余的部分作为小数部分。因此,当X坐标为100时,底层储存的实际上是 (100 << 16) = 6553600. 不过,在使用系统函数进行操作时,传入的参数和得到的值仍然是100,因此这只是一个底层上的“无关紧要”的细节。除非必要,否则建议总是使用系统函数而非内存操作来改变物件的坐标。)


                IP属地:美国本楼含有高级字体9楼2020-03-14 12:01
                回复
                  2026-01-15 06:04:35
                  广告
                  不感兴趣
                  开通SVIP免广告
                  最后一个值得关注的属性是方向(dir):它表示了物件正在运动的方向,同时,对于主将和士兵物件而言,它也表示了主将或士兵当前的朝向。和高中数学一样,0度方向指向X轴正方向(正右),随着逆时针角度的增加而增加;不同的是,群2中以64为90度(π/2),以128表示180度(π)。如下图所示,夏侯惇的方向为0,出击中的刘备的方向则为128。


                  IP属地:美国本楼含有高级字体10楼2020-03-14 12:05
                  回复
                    三、武将技的标准流程
                    读者可能会问:既然上一章中的例子只是“有点像”武将技,那怎么才能“完全像”呢?
                    回忆一下放武将技时都会经历哪些过程。首先,天空会变黑;其次,镜头会转向放武将技的武将,武将会做一个动作,在短暂时间内无敌;接着,武将技被放出,武将继续砍人、赶路、站定,该干嘛干嘛;最后,武将技结束,天空变白。
                    以上这套被笔者称为武将技的“标准流程”。大部分奥汀的官方武将技都会经历类似的过程——即使这个武将技最后只是换来一口锅。接下来,我们将试着“煞有介事”地把我们的Hello World也改写成遵照这套标准流程。


                    IP属地:美国本楼含有高级字体11楼2020-03-14 12:10
                    回复
                      做的事情虽然看起来很多,但是并不复杂,非常有条理。我们来一行一行看:
                      (1) DisablePlayMagic(); 一行调用系统函数,禁止双方放武将技
                      (2) DownBrightness(12, 5); 一行,调用了magic点cpp中已经被奥汀定义好的DownBrightness函数,让天空慢慢变黑。为什么可以慢慢变黑?当调用该函数时,代码会直接跳转到DownBrightness内部执行,而这个函数中有Delay语句,因此,执行这一行的过程就是天空慢慢变黑的过程。天空黑透之后DownBrightness函数返回,继续执行。
                      (12表示天空变黑的目标亮度,初始为16。5表示每5 Tick天空变黑1个单位,因此天空变黑需要 (16 - 12) * 5 = 20 个Tick。)
                      (3) 天空变黑后,SetOverwhelming(intvAttackerMajor, 1); 设置施放武将技的武将为无敌状态intvAttackMajor是一个预设变量(intv变量),我们用它来获取正在施放武将技的主将的物件。
                      (4) 获取主将的屏幕X/Y坐标,并设置摄像机的位置(SetViewCamera),将摄像机对准主将
                      接下来的一段代码是武将的动作:
                      (5) AddAttackCounter让主将定身(停下当前的工作);参数2表示2个“定身时间单位”,一个单位等于40 Tick。
                      (6) 15 Tick后,用SetObjectAnimate指定主将物件播放动画序列OAF_SPELL1,也就是Things点ini中定义的SPELL1序列——武将“把手一招”的动作。
                      (7) 20 Tick后,SetOverwhelming(intvAttackerMajor, 0); 解除武将无敌状态(第二个参数为0)。
                      武将技的准备工作到此结束,执行武将技的主体——Hello World。
                      Hello World结束之后是武将技的结束工作。只有两件事:
                      (8) 让天空慢慢变白RaiseBrightness),速度和变黑的速度相同;
                      (9) 武将技彻底结束,允许双方操作和施放武将技EnablePlayMagic)。
                      以上的流程中,(1)-(4)、(8)-(9)的部分,几乎对于每个武将技都是不变的。(5)-(7)的部分,不同武将技会有所不同,主要是武将的动作不同,比如施放落日弓时,武将是不会停下来的,动作也是拉弓的动作;解除武将无敌状态的时机也因武将技而异。
                      除了上面的流程之外,还有一个流程是调用BatchLoadShape函数,即批量读取图形文件。由于Hello World的例子中没有创建任何图形物件,因此我们跳过了这一步。
                      最后,注意到我们将所有的武将技内容全部移到了一个单独的函数中,并另外安排了一个“入口函数” Magic111. 由于原版武将技中有很多2/3/4/5级武将技,它们有不同的入口函数,却有着类似的执行过程,因此原版的做法是用参数指定施放的武将技是第几级,然后在不同武将技的入口函数中调用该函数时,指定不同的等级参数。只有一级的武将技(如狂雷天牢等)也遵照了同样的格式。
                      读者可以试着编译运行一下。一切顺利的话,我们将看到一个完整的、“煞有介事”的武将技施放过程。


                      IP属地:美国本楼含有高级字体12楼2020-03-14 12:14
                      回复


                        IP属地:美国13楼2020-03-14 12:54
                        回复
                          四、图形物件的创建
                          接下来的几节中,我们将创造一个实际的武将技——“大喝”。它是虎咆的超级弱化版,随着武将的一声大喝,武将的周围扩散出一圈气波,将周围的8个小兵震飞,如果敌将在范围内的话,会造成15点伤害。我们将一步一步地完成这个武将技。
                          首先,为了制造出气波的效果,我们需要实际地制造出气波的图形物件。我们用到的原型是Things点ini中定义的18002号物件“扩张的光气环”,原版定义如下:
                          [OBJECT]
                          Name =擴張的光氣環
                          Sequence= 18002
                          Type = %TYPE_FORCE
                          Space = 0,0,0,0
                          Flags = OF_NOGRAVITY, OF_MIXER, OF_WHITELIGHT
                          Process = %MagicObjectProcess
                          Directory = \MAGIC\008
                          Wait = #9999, m008circle
                          它长这个样子:

                          在三国2脚本中,有两个系统函数被用于创建图形物件。它们分别是:
                          int CreateObjectRaw (int x, int y, int z, int nDir, int dwSequence);
                          int CreateObjectByReference (int referenceObjectHandle, int dwSequence,int nDir, int zOffset);
                          CreateObjectRaw函数被用于直接在指定位置生成图形物件,需要指定X/Y/Z坐标、方向和物件原型的Sequence。在我们这个例子中,X/Y坐标和主将的坐标相同,Z轴坐标则比主将Z坐标(为0,即地面)高出40点;方向无关紧要,因而可以填0;dwSequence则是Things点ini中的Sequence。因此,使用该函数创建光气环的代码如下:
                          int x = GetObjectScreenX(intvAttackerMajor);
                          int y = GetObjectScreenY(intvAttackerMajor);
                          int z = GetObjectScreenZ(intvAttackerMajor);
                          int circle = CreateObjectRaw(x, y, z + 40, 0, 18002);
                          CreateObjectRaw的返回值是一个物件的句柄(Handle),它是int型的。通常我们直接将物件句柄称之为“物件”,因为所有的系统函数都是使用句柄来操作物件的;因此,我们可以认为,CreateObjectRaw函数返回被创建好的物件。
                          指定x/y/z三维坐标未免有一些麻烦。我们可以用CreateObjectByReference直接根据指定物件的坐标创建新的物件。上面的代码可以简化为:
                          int circle = CreateObjectByReference(intvAttackerMajor, 18002, 0, 40);
                          其中,第一个参数指定参考物件,新物件的位置与参考物件相同;第二个参数是物件sequence;第三个参数是方向;第四个参数是新物件相对于参考物件的z坐标的差值。


                          IP属地:美国本楼含有高级字体14楼2020-03-14 12:59
                          回复
                            我们来实际测试一下结果。创建一个新的112号武将技,使用调用编号801:
                            [MAGIC]
                            SEQUENCE = 112
                            NAME =大喝
                            MP =18
                            POWER =100
                            ATTACK =15
                            SCRIPT =801
                            ATTRIB =全體,主將
                            TITLE =
                            NOTE =
                            ACTIVE =敵方全軍
                            武将技代码如下:
                            (想了想,还是在文本编辑器里面截图好一点。)


                            IP属地:美国15楼2020-03-14 13:03
                            收起回复
                              2026-01-15 05:58:35
                              广告
                              不感兴趣
                              开通SVIP免广告
                              编译运行。观察到主将将武器一招后,周围出现了一个白色的光圈——这就是我们创建的物件。


                              IP属地:美国16楼2020-03-14 13:07
                              回复