三国群英2吧 关注:45,924贴子:1,015,531

【脚本基础教程】第九章(上):必杀技(大喝)

只看楼主收藏回复

视频来自:百度贴吧


IP属地:美国1楼2020-04-12 08:52回复
    本章我们介绍一些“奇技淫巧”:必杀技和士兵技的创建。细心的读者在经过之前的章节后,可能已经注意到,武将技的杀伤可以在武将技结束之后继续执行;这意味着武将技的杀伤效果并不是和“武将技”本身捆绑的。既然如此,我们当然可以完全摆脱武将技,在武将技以外的地方把我们的“魔法效果”放出来。这就涉及到了本章的主题,也就是必杀技和士兵技;它们都是在战场上“自动施放”的“技能”。
    为了达成本章介绍的内容,我们需要带有1.05+“脚本支持”相关功能的EXE。因此,如果您的EXE不带有此功能,您可能会希望使用1.05+修改内容说明当中的相应补丁。
    此外,需要说明的是,本章的内容主要以启发性为主。必杀技和士兵技能做到的事情远比本章提供的例子要丰富得多,尤其是结合内存操作之后更是如虎添翼。在本章中,笔者暂时避免涉及过多的内存操作,只在必要时提供相关的代码;不过笔者也会对一些潜在的可以利用内存操作的地方进行提示。


    IP属地:美国本楼含有高级字体2楼2020-04-12 08:58
    回复
      2026-01-17 16:43:56
      广告
      不感兴趣
      开通SVIP免广告
      一、在Things ini中指定信息
      我们早在第四章的第一节就介绍了Things ini中的许多内容。不过,Things ini中还有一些内容是我们没有注意到的;我们先来介绍这些内容。
      首先,在一般的动画序列中,我们会看到一些特别的记号:
      Death =#5, L1F10001, L1F10002, L1F10003, L1F10004, #128, L1F10006, #5, L1F10007,L1F10008, L1F10009, L1F10010, #128, L1F10010, #9999, @%OM_DEATHSTOP, SCROVER
      Wait =#60, m018FireA1, m018FireA1, @%OM_REMOVE,SCROVER
      SCROVER本身表示动画的结束(亦即,不会回到首帧继续播放)。有一类特别的记号以 @% 开头,这一类表示物件的内部信息,亦即,当动画播放到这里时,自动向EXE发送特定的信息。例如,在主将的死亡序列中,OM_DEATHSTOP向EXE发送“死亡结束”的信息,告诉EXE执行主将死亡的后续动作;在武将技物件中的OM_REMOVE则告知EXE销毁当前物件。
      这个例子说明,我们可以在Things ini中向EXE发送信息,让EXE执行一些特别的动作。类似的例子还有:
      Defense = #8, L1D10002, $Defense, SCROVER
      Pain =#4, L1H10002, $Hit01, L1H10002, SCROVER
      在主将物件中,我们能看到以上以$开头的内容,它表示播放指定的声音


      IP属地:美国本楼含有高级字体3楼2020-04-12 09:03
      回复
        接下来是我们的重头戏:
        Wait = #11,m006mfss0001, m006mfss0002, m006mfss0003, m006mfss0004, m006mfss0005,m006mfss0006, #16, !6003, m006mfss0007, #11,m006mfss0008, m006mfss0009, m006mfss0010, m006mfss0011, m006mfss0012,@%OM_REMOVE, SCROVER
        上面这段出自这个物件:
        [OBJECT]
        Name = 向上射弓箭的藍小兵
        Sequence= 16001
        Type = %TYPE_FORCE
        Space = 0,0,0,0
        Flags = OF_BIGSHAPE
        Process = %MagicObjectProcess
        以感叹号 ! 开头的信息是什么意思呢?6003这个数字看起来很像一个调用代码。事实上,它确实是一个调用代码;!6003 表示动画运行到这里时,调用指定调用代码的函数
        我们立即想到:如果我们能在主将或士兵物件中增加这样的以感叹号开头的内容,我们事实上就可以放出“武将技”。(当然,这样做的前提是,进行了1.05+中脚本支持部分的修改。)我们来实际操作一下:
        [OBJECT]
        Name = 主將L8 (主體) - 劍
        Sequence= 628
        Type = %TYPE_MAJOR
        Space = 8,8,64,0
        Flags = OF_MAN, OF_BIGSHAPE
        Process = %ForceMajorObjectProcess
        Directory = \MAJOR\BODY
        First =#1, NULSHAPE
        Wait = L8W10001, #16, L8W10002, #8, L8W10003, #8, L8W10004, #16, L8W10003,#8, L8W10002, #8
        Walk = #4, L8R10001, L8R10002, L8R10003, L8R10004, L8R10005, L8R10006,L8R10007, L8R10008, L8R10009
        Attack = #3, L8A10002, !55501, L8A10003,L8A10004, L8A10005, L8A10006, L8A10007, @%OM_ATTACK, L8A10008, L8A10009, L8A10010,L8A10011, SCROVER
        Attack2 = #3, L8A60002, !55501, L8A60003, L8A60004, L8A60005, L8A60006,L8A60007, @%OM_ATTACK, L8A60008, L8A60009, L8A60010, L8A60011, SCROVER
        Attack3 = #3, L8A50002, L8A50003,L8A50004, L8A50005, L8A50006, L8A50007, L8A50008, L8A50009, L8A50010, L8A50011,SCROVER
        Attack4 = #3, L8A30002, L8A30003,L8A30004, L8A30005, L8A30006, L8A30007, L8A30008, L8A30009, L8A30010, L8A30011,L8A20004, L8A20005, L8A20007, L8A50004, L8A50005, L8A50007, L8A30004, L8A30005,L8A30007, L8A20004, L8A20005, L8A20007, L8A50004, L8A50005, L8A50007, L8A20004,L8A20005, L8A20007, L8A50004, L8A50005, L8A50007, L8A20004, L8A20005, L8A20007,L8A20004, L8A20005, L8A20007, L8A30002, L8A30003, L8A30004, #12, L8A30005, #3,L8A30006, L8A30007, L8A30008, L8A30009, L8A30010, L8A30011, SCROVER
        Attack5 = #3, L8A30002, !55501, L8A30003, L8A30004, L8A30005, L8A30006,L8A30007, L8A30008, L8A30009, L8A30010, L8A30011, SCROVER
        Attack6 = #3, L8A20002, !55501, L8A20003, L8A20004, L8A20005, L8A20006,L8A20007, @%OM_ATTACK, L8A20008, L8A20009, L8A20010, L8A20011, SCROVER
        Attack7 = #3, L8A30002, !55501, L8A30003, L8A30004, L8A30005, L8A30006,L8A30007, @%OM_ATTACK, L8A30008, L8A30009, L8A30010, L8A30011, SCROVER
        Attack8 = #3, L8A50002, !55501, L8A50003, L8A50004, L8A50005, L8A50006, L8A50007,@%OM_ATTACK, L8A50008, L8A50009, L8A50010, L8A50011, SCROVER
        Spell1 =#64, L8L10001, SCROVER
        Spell2 =#64, L8L10002, SCROVER
        Shot1 = #3, L8S10002, L8S10003,L8S10004, L8S10005, L8S10006, L8S10007, L8S10008, L8S10009, L8S10010, L8S10011,SCROVER
        Shot2 = #3, L8A30002, L8A30003,L8A30004, L8A30005, L8A30006, L8A30007, L8A30008, L8A30009, L8A30010, L8A30011,SCROVER
        Shot3 = #3, L8S10002, L8S10003,L8S10004, L8S10005, L8S10006, L8S10007, L8S10006, L8S10007, L8S10006, L8S10007,L8S10006, L8S10007, L8S10008, L8S10009, L8S10010, L8S10011, SCROVER
        Defense = #8, L8D10002, $Defense,SCROVER
        Pain = #4, L8H10002, $Hit01, L8H10002, SCROVER
        Death = #5, L8F10001, L8F10002, L8F10003, L8F10004, #128, L8F10006, #5,L8F10007, L8F10008, L8F10009, L8F10010, #128, L8F10010, #9999, @%OM_DEATHSTOP,SCROVER
        Win =#5, L8FS0001, L8FS0002, L8FS0003, L8FS0004, #128, L8FS0006, #5, L8FS0007,L8FS0008, L8FS0009, L8FS0010, #128, L8FS0010, #9999, @%OM_DEATHSTOP, SCROVER
        在这里,我们为所有正常攻击的序列都指定了脚本的执行,调用代码为55501. 我们将必杀技的施放放在攻击序列中,是因为必杀技施放时,通常都是旁边有敌对目标的时候,因此放在攻击序列中比较合理;如果放在等待或移动序列中则有空放的可能。
        (当然,做得更精细一点的话,我们可以为不同的序列指定不同的入口函数,然后判断周围是否存在敌对目标。目前我们暂时从简。)
        这样指定之后,任意时刻主将进行攻击,播放到动画序列中的相应位置时,EXE会自动触发55501号脚本函数,此时也正是必杀技发动的时机。我们接下来就将编写这个调用代码为55501的函数,也即必杀技的实际内容。


        IP属地:美国本楼含有高级字体4楼2020-04-12 09:10
        回复
          二、将武将技作为必杀技移植
          我们接下来将编写我们的必杀技的具体内容。出于演示的目的,笔者使用一个较简单的、我们自己编写的武将技直接移植为必杀技,也即第四章中的“大喝”。
          将武将技移植为必杀技时有什么需要改动的地方呢?首先,创建物件、执行系统函数等内容,不管函数在哪里触发,结果都是一致的,代码的执行并不因调用方的变化而有所不同。其次,打击目标标记看起来是一个问题;但是,如果我们查看EXE的反汇编代码的话,可以发现,脚本中创建的物件的打击目标判定所属方时,和脚本的调用方的关联物件的所属方是一致的。通过Things ini添加调用代码调用脚本时,脚本的关联物件自然是被添加调用代码的物件;因此打击目标也不成问题了。
          有问题的地方在于预设变量。在通过施放武将技调用脚本时,预设变量中会填好攻击方、防守方的主将及施放武将技的所属方,还有武将技的伤害;用我们的方式触发脚本时,这些变量都还没有被设置,甚至,当必杀技的触发发生在对方施放武将技的过程中时,预设变量的值和预想中甚至是相反的。因此,这是需要注意的首要之处。
          另一个需要注意的地方是武将技的标准流程。我们已经很久没怎么改过武将技的标准流程了,但是读者应该还记得这套标准流程的作用:天空变黑,摄像机对准武将,禁止双方施放武将技……这些都需要删去,因为施放必杀技的时候是不应该做这些动作的。
          摄像机的操作也是需要完整地删去的,通常而言,我们不希望施放必杀技时强制将摄像机对准必杀技的施放者——当然,主要原因是避免搞乱可能的正在进行的武将技。
          最后,一个容易被忽略的地方是播放声音:通常在武将技中都是用PlaySound1无源地播放声音的,但是由于我们不再移动摄像机,因此需要改成有源的。


          IP属地:美国本楼含有高级字体5楼2020-04-12 09:17
          回复
            我们先来实际地移植一下“大喝”必杀技。首先,先把武将技的部分复制过来,改个名字,分别把原来的Roar()和Magic112()两个函数改名为RoarGenSkill()和GenSkill(),入口函数的调用代码也改为55501. 紧接着,我们开始删东西(红色的部分表示删除):


            注意到我们“主将把武器一招“这个动作保留了下来;施放必杀技时,完全可以让主将做一些动作(尽管这样会打断当前的攻击动作)。


            IP属地:美国6楼2020-04-12 09:28
            回复
              接下来,我们来重点解决预设变量的问题。我们主要用到的预设变量有:
              1. intvAttackerMajor;
              2. intvIsLeft;
              3. intvMagicAttackValue;
              4. intvDefenderMajor.
              首先是intvAttackerMajor。之前已经提到,脚本存在一个关联物件:当我们在Things ini中指定!xxxx触发脚本时,该脚本的关联物件就是Things ini中定义的物件本身。我们可以用GetScriptLinkedObject系统函数得到它:
              int GetScriptLinkedObject ();
              得到的物件就是我们需要的 intvAttackerMajor了。
              其次,我们需要得到施放必杀技的所属方。我们可以直接使用GetForceSide系统函数得到主将或士兵物件的所属方(1=左,0=右);我们直接对得到的攻击方武将应用该函数,即可得到所属方。
              接下来是intvMagicAttackValue。这个最简单,直接替换成一个常数即可;毕竟对于必杀技并没有相应的ini文件指定对主将的伤害。
              上面三个属性可以由如下代码直接得出:
              int attackerMajor = GetScriptLinkedObject();
              int isLeft = GetForceSide(attackerMajor);
              int magicAttackValue = 10; // 或者别的伤害值
              最后,比较麻烦的是intvDefenderMajor。我们并没有一个很好的方法来直接获得对方主将的物件;事实上,在笔者所知的范围内,这一操作只能通过内存操作来完成。一种取巧的方法是,如果场上任意一方施放过武将技,可以逐一判断intvDefenderMajor和intvAttackerMajor是否和已知的必杀技施放者相等,另一个自然是目标主将;但是这样终归不是很尽善尽美。
              这里,笔者不加解释地给出相关内存操作的代码:


              IP属地:美国本楼含有高级字体7楼2020-04-12 09:33
              回复
                我们逐一将各个预设变量换成我们自己的变量。由于这个武将技原来只有一个主函数,因此这样就算结束了。如果还有其它函数的话,需要用各种方法把需要的值传进去(例如为函数新增参数;或者当涉及回调函数时,使用物件额外参数)。
                最后,我们还需要将无源的PlaySound1改成有源的PlaySound:
                // 大喝一声
                PlaySound(attackerMajor, "Roar01", 255);
                移植完的代码如下(改写原代码之处作标红处理):



                IP属地:美国8楼2020-04-12 09:38
                回复
                  2026-01-17 16:37:56
                  广告
                  不感兴趣
                  开通SVIP免广告
                  编译运行。


                  IP属地:美国9楼2020-04-12 09:47
                  回复
                    三、发动条件和获得武将指针
                    在上面的例子中,我们没有对必杀技的发动加上任何限制:因此,武将一旦发动攻击,就会伴随一声大喝。这当然是不合理的;在上面的测试中,83武力的刘备直接把98武力的夏侯硬生生喝死了。在多数情况下,我们都希望为必杀技加上一些限制条件。
                    首先,我们希望限制必杀技的发动概率——比如10%。(注意,10%是一个并不算很低的概率:武将平均攻击10次就会发动一次。)实现起来并不困难:每当入口函数被调用(武将攻击)时,我们制造一个0-9999之间的随机数,然后判断这个数是否低于10000的10%,也就是1000;如果低于1000则执行必杀技的主函数,否则什么也不做。如下:


                    IP属地:美国本楼含有高级字体10楼2020-04-12 09:53
                    收起回复
                      另一个很自然的想法是,根据武将的属性来判断是否施放必杀技。不过,在不进行内存操作的情况下,我们能获取的武将属性很有限。我们可以获得以下属性:
                      1. 武将的姓名,使用GetGeneralName系统函数得到。其定义如下:
                      string GetGeneralName(int general);
                      2. 武将的等级,使用GetMajorLevel系统函数得到。其定义如下:
                      int GetMajorLevel(int isLeft);
                      3. 武将的“类型”;这个实际上是开战前两边喊话时用到的,由武智最高值决定,大于90为类型0,大于75为类型1,否则为类型2. 使用GetGeneralType系统函数得到:
                      int GetGeneralType(int general);
                      在上面的定义中,和很久以前我们介绍的Prompt函数一样,笔者暂时用int型来指定general参数的类型。
                      在这里我们使用了“武将”一词。它的含义实际上是武将指针;因此,准确地说,genreal参数的类型应该是General*。不过,指针类型从底层上来说是整型,和int型之间可以显式地互相转换,因此我们暂时都使用int型变量。
                      虽然我们暂时使用了int类型,但笔者仍然需要说明主将(主将物件)和武将(武将指针)的区别。在本教程中,笔者一直有意区分了这两种用法:“主将”(Major)指的是战场上的主将物件,是主将身体+马匹+武器的组合,也就是我们一直使用的 intvAttackerMajor / intvDefenderMajor ;而“武将”(General)在作为参数、变量等出现的时候,指的是武将指针,也就是具体储存武将属性的结构,其中保存了武将的武力、智力、等级、经验值等一系列信息。主将物件不等于武将指针;读者可以这么理解,主将物件指的是战场上的图形,而武将指针指的是这个图形物件背后的武将本身。
                      获取武将指针并不困难,甚至比获取主将物件还简单一些。在战场上的任何时候,我们都可以使用intvLeftGeneral和intvRightGeneral两个预设变量,获取战场上左方和右方的武将指针。因此,如果我们希望“大喝”必杀技只能由刘备发出,我们可以按下面的方法添加条件:


                      IP属地:美国本楼含有高级字体11楼2020-04-12 10:00
                      回复
                        (注释全部换成了繁体,这是因为我们用到了Big5编码的字符串"劉備"。因此,cpp文件本身也必须以Big5编码保存,此时无法兼容简体字。)
                        这样就完成了。必杀技的其它条件是类似的。
                        (注:如果您使用内存操作的话,不妨将储存武将指针的general变量直接指定为General*类型。此时,您可以直接使用->运算符,直接获取武将的武力、智力、HPMP、经验值、武器、马匹、书籍甚至武将技等内容;这样可以极大地丰富必杀技发动条件依赖的属性的可能性。)


                        IP属地:美国12楼2020-04-12 10:05
                        回复
                          必杀技“大喝”的完整代码






                          IP属地:美国本楼含有高级字体13楼2020-04-12 10:21
                          回复
                            在结束必杀技部分前,我们需要说明两点:
                            1. 为了方便起见,我们目前只在主将L8-剑的Things ini定义中加入了必杀技的调用代码。在现实中,您可能希望所有主将造型的武将、敌我双方的武将都可以执行必杀技,此时,需要在Things ini中,将所有主将主体物件定义的攻击动画序列中,都加上同样的调用代码!55501。
                            2. 在上面的例子中,一个动画序列中只指定了一个调用代码,这意味着所有必杀技都有同样的入口。当然,您可以在动画序列中指定多个调用代码,或者您希望不同的必杀技有着不同的触发时机,此时也可以制造出多个入口;但是,总体而言,必杀技的入口是和发动时机相关的,而与具体的必杀技是无关的。这和武将技入口函数的逻辑有很大的区别。因此,多个必杀技的发动条件也往往会混在同一个入口函数中,需要根据不同的条件来判断施放哪一个必杀技。具体的做法往往因实际MOD制作的需求而异。


                            IP属地:美国14楼2020-04-12 10:27
                            回复
                              2026-01-17 16:31:56
                              广告
                              不感兴趣
                              开通SVIP免广告
                              附录:一些可能的内存操作
                              之前已经提过,通过内存操作,我们可以实现许多功能。笔者在此列举一些可能的做法,以供启迪。
                              在获得武将指针之后,我们使用General* 类型储存这一指针:
                              General* general;
                              if (isLeft) general = intvLeftGeneral;
                              else general = intvRightGeneral;
                              这样,我们就可以使用->运算符直接获取武将的属性了。我们可以获取(和设置)的一些武将属性如下表(经过节选;详见脚本编译器目录下config/STRUCT_TABLE点txt文件中的GENERAL一节):

                              例如,我们可以根据武将的初始武力判断是否施放必杀技。此时我们可以这样书写条件:
                              if (general->wStrSrc >= 95) { /* ... */ }
                              我们还可以根据书籍的编号来判断是否施放必杀技,例如判断是否携带遁甲天书:
                              if (general->wItemHold == 55) { /* ... */ }
                              我们甚至可以遍历武将技列表:

                              士兵技的部分见本章的下半部分。


                              IP属地:美国本楼含有高级字体15楼2020-04-12 10:32
                              回复