艾约bright 发表于 2022-6-2 20:04:27

利用xs脚本实现战役的跨关卡联动

本帖最后由 艾约bright 于 2022-6-4 00:31 编辑

战役制作中一直缺少跨关卡联动的功能,如果说玩家上一关的选择能够影响到下一关的发展,那么能极大地丰富战役的可玩性。现在,决定版中利用xs脚本的读写文件功能,能够间接地实现这一想法。


要实现跨关卡联动,就必然要有一个文件用来记录之前关卡的状态。首先给出遗朝网站上对xs脚本读写文件功能的介绍,参考:.xs scripting in Age of Empires II: Definitive Edition - Forgotten Empires

所以xsCreateFile这个函数是用来创建文件的,xsOpenFile这个函数是用来打开并读取文件内容的,xsWriteString、xsWriteInt等一系列函数是向文件写值的,文件后缀名是.xsdat。下面对这几个函数进行一些解释,因为它们和常见编程语言中的用法存在差异:

[*]xsCreateFile:创建文件的函数无法指定路径和文件名称,xs脚本会自动将文件创建在固定位置。根据场景的不同,创建位置也有差别。如果是随机地图或者在地编里运行,则会创建在C:\Users\用户名\Games\Age of Empires 2 DE\STEAMID\profile这个文件夹下,并且文件名等于场景名;如果打包成了战役,则会创建在resources/_common/campaign文件夹下,并且文件名等于.aoe2campaign文件的名称。而且后者还可再细分为几种情形,如果是一个单独的.aoe2campaign文件,没有打包成模组形式,那么生成的.xsdat文件在C:\Users\用户名\Games\Age of Empires 2 DE\STEAMID\resources\common\campaign文件夹下,否则会在mods/local或者mods/subscribed对应的路径下。除此之外,这个函数有一个参数append,是用来控制写入模式的,如果设为true就会接着已有内容写,如果设为false就会创建一个新文件
[*]xsOpenFile:打开文件后默认为只读模式,是无法写入的。也就是说,如果要写入文件就必须先xsCreateFile。这个函数的参数为文件名(不含.xsdat后缀),但这个文件名其实隐含了一个路径,如果你特别指定,那它就会去C:\Users\用户名\Games\Age of Empires 2 DE\STEAMID\profile下面找
[*]xsWriteString:这一系列的函数就是用来写入值的,注意必须xsCreateFile后才能写入,xsOpenFile是不行的


介绍完了这几个函数后,我们开始构建一个简单的应用场景。比如说我们的战役有两关,第一关中设置了一个成就任务,如果完成了,第二关会给一个额外的奖励。那么我们需要在第一关结束时进行结算,如果完成了任务就写入一个值(比如1),否则就写入默认值(比如0),然后在第二关读取这个文件,判断玩家是否完成了成就任务。
这里最需要注意的是文件路径问题,我们需要使用相对路径来为xsOpenFile函数指定读取文件的位置,否则它就跑去profile下面找了。如上所述,当在一个打包的模组战役里运行的时候,生成的.xsdat文件是在mods/local或subscribed/模组名/resources/_common/campaign下的,那么根据profile文件的位置,我们可以写出以下的相对路径:../mods/local或subscribed/模组名/resources/_common/campaign/.xsdat文件名。这里需要注意的是,subscribed里的模组文件夹前面有个数字编号,需要预先上传一下模组,获取这个编号后再来定义路径。

我们定义三个函数,分别执行状态的初始化、更新和读取:
void initValue()
{
    xsCreateFile(false);
    xsWriteInt(0);
    xsCloseFile();
}

void changeValue()
{
    xsCreateFile(false);
    xsWriteInt(1);
    xsCloseFile();
    xsChatData("Value has been written");
}

void checkValue()
{
    bool flag = xsOpenFile("../mods/local/xs_test/resources/_common/campaign/xs_test");
    if (flag == false) {
      xsOpenFile("../mods/subscribed/72551_xs_test/resources/_common/campaign/xs_test");
    }
    int val = xsReadInt();
    xsCloseFile();
    if(val == 1){
      xsChatData("Read your record!");
    }
    else{
      xsChatData("No record!");
    }
}

在第一个场景中,我们首先调用initValue函数来初始化val,如果达成了任务,就调用changeValue函数将val修改为1;在第二个场景中,我们调用checkValue函数,来读取并检测val的值,判断玩家是否完成了任务。这里考虑到玩家可能使用本地模组或者订阅模组,我们写了两个路径来判断(不考虑玩家下载下来后又自己改名等特殊情况)。


大家可以在模组中订阅“xs_test”这个测试样例,来体验一下此功能。在第一个场景中等待20秒,即视为完成了成就任务,此时进入第二关,左上角会提示“Read your record!”;如果没有完成任务,第二关的左上角则会提示“No record!”;重进第一关则会刷新状态。


以上是一个简单例子的展示。在实际应用中,大家可以在文件中读写更加复杂的数据。例如,文件中定义6个数字分别作为6个场景的状态码,然后根据实际情况进行初始化、更新和读取,即可实现多关卡联动。可以用来实现库曼5进阶版的多线剧情,你在第一关选择了帮助匈牙利,那么后续关卡也是匈牙利作为你的盟友等等。


补充介绍一下关于file offset相关的操作:
在较复杂的场景设置中,可能需要用到多个数字来记录状态。例如,第一关有两个成就任务,在第二关中分别对应两个奖励;除此之外我们还想将第二关的剩余资源继承到第三关。这个时候,按照一般思路,我们首先需要2个int来记录是否完成了第一关的两个成就任务,然后还需要4个float来记录4种资源。但是这里存在一个问题,第三关的时候我们并不需要读取第一关成就任务的完成情况,只要读第二关的剩余资源就可以了。此时就需要设置文件读取时的offset,来控制从特定位置开始读取了。



首先说明一下,文件的offset是从0开始计算的,每个int和float都是4个字节,所以说每读了一个int或者float,offset都会+4(你调用read系列函数的时候,读完会自动加)。主要有三个函数:

[*]xsGetFilePosition():获取当前读取位置的offset
[*]xsSetFilePosition(int n):设置当前读取位置的offset,这个是绝对值,比如第一个int占据0-3字节,你设成4,就从第二个数字开始读了
[*]xsOffsetFilePosition(int n):和上面函数的区别是,它是将读取位置的offset向前移动n个字节,所以是一个相对值


那么我们的做法如下所述:首先涉及到多关卡多状态联动的情境,在写入文件的时候我推荐是先把里面的值全部读出来,然后修改相应字节位数的值,再重新创建文件写入。因为它默认的追加写入模式会写在最后,你没法控制,重新创建再写是最方便的。所以我建议大家首先列一个要用到的状态序列,比如这里是X=(int, int, float, float, float, float),开始时进行一次全0初始化,这样每一位是对应的什么含义就很清楚了。在第二关结束时我们将第二关剩余资源写入,那么过程就是首先把X的值都读出来,然后保持前两个int不变,修改后面4个float值(分别填入肉木金石的资源),再重新创建文件并写入。在第三关开场我们打开文件后,利用xsSetFilePosition或者xsOffsetFilePosition函数,将读取的位置移动到8,即跳过0-3和4-7这两个共占据8字节的int。这样我们连续执行4次xsReadFloat,读取到的就是刚才存储的4种资源余量了,于是我们就可以利用这些值来修改资源达到效果。

简单概括一下,file offset的相关操作可以让你从任意位置开始读取写入的变量,这样你就可以控制和划分不同关卡、不同状态的变量范围,从而实现复杂的多关卡联动了。

GM_Hong 发表于 2022-6-2 20:49:37

RPG作者欣喜

春田一九零三 发表于 2022-6-2 23:13:25

其实这个功能最大的帮助是RPG。
因为现在关卡可以扩展,然后只要玩家的状态记录下来
那么每个关卡可以按场景进行设置,而玩家可以反复进入
整个RPG场景会完全模块化
不再限制于1张地图,而维护性也会变得很方便

love371482 发表于 2022-6-3 06:45:08

就看啥时候可以大规模实用化了。

cly806 发表于 2022-6-3 16:32:42

本帖最后由 cly806 于 2022-6-3 16:53 编辑

思路非常好,最根本的问题我测试了一下,我的目的是把第一关的四种资源加到第二关去,但是这个第一关的数怎么转移到第二关去,目前测试是没能成功的。另外官方的教程最好测试后再来看,比如随机函数那里官方取值5-10但是结果永远是0-5的,官方的东西未必准确。而且很多时候并不靠谱


目前看只能通过读取是否完成对应分支(可以是多个分支,不一定两个),然后新关卡获取一定数额的奖励,但是旧关卡和新关卡之前的数据依然是独立的,没有办法继承过来,这个是需要去摸索的点

艾约bright 发表于 2022-6-3 18:22:52

cly806 发表于 2022-6-3 16:32
思路非常好,最根本的问题我测试了一下,我的目的是把第一关的四种资源加到第二关去,但是这个第一关的数怎 ...

按我的理解,应该是把上一关的资源用writefloat写入文件,然后下一关readfloat读出来并加上去。但是问题在于,它这个文件不能自定义名称,都是用的那一个,这种需要存的东西多了会乱,要规划好每一个状态的位置,利用offset去做读取

cly806 发表于 2022-6-3 22:54:25

艾约bright 发表于 2022-6-3 18:22
按我的理解,应该是把上一关的资源用writefloat写入文件,然后下一关readfloat读出来并加上去。但是问题 ...

所以我说这个只能通过设置分支来判断玩家完成了哪条线的既定任务或者有没有完成关卡内成就,然后第二关给予指定的加成,这种跨关联动当然是一种很大的进步,但是各关间的数据独立性使得我即使使用全局变量打头(第二张图),关卡之间的属性也不能做到最简单的继承关系,属实是可惜,这种局限性大大限制了这个方法的使用,如果能攻破这层,将是颠覆级的发现

艾约bright 发表于 2022-6-3 22:59:51

cly806 发表于 2022-6-3 22:54
所以我说这个只能通过设置分支来判断玩家完成了哪条线的既定任务或者有没有完成关卡内成就,然后第二关给 ...

全局变量就跟其他编程语言一样,只是能让你在不同函数里调用,但是每个场景开始都会读一遍xs文件并将其初始化,所以这个没法在不同场景起到全局作用。
各关之间的数据,理论上只要你愿意可以全部存起来,给到下一关读取,是可以继承的,就是麻烦点。

cly806 发表于 2022-6-3 23:08:22

艾约bright 发表于 2022-6-3 22:59
全局变量就跟其他编程语言一样,只是能让你在不同函数里调用,但是每个场景开始都会读一遍xs文件并将其初 ...

你就做个第一关用剩下的木肉金石加到第二关去这个简单的继承关系探索一下,主要是我想不出来用什么函数存储起来给新关调用,你看看能不能实践出来,我知道理论上一定行的。主要是摸不到门道,这方面你探究比我深,应该更容易找到方法

艾约bright 发表于 2022-6-3 23:28:27

可以做到的,就把第一关资源存到文件第二关读嘛

cly806 发表于 2022-6-4 00:03:01

艾约bright 发表于 2022-6-3 23:28
可以做到的,就把第一关资源存到文件第二关读嘛

经此探究之后,基本上划时代的成就完成了,这个发现非常的好,有了一个小突破口以后做各种效果见微知著,迎刃而解,你能在主贴里详细讲讲xsSetFilePosition()和xsOffsetFilePosition()的具体用法吗,UGC的英文解释什么字节位移的看的我头大,非常感谢供稿,以后专栏会详细叙述这一点的

cly806 发表于 2022-6-4 10:44:57

本帖最后由 cly806 于 2022-6-4 10:48 编辑

艾约bright 发表于 2022-6-3 23:28
可以做到的,就把第一关资源存到文件第二关读嘛
关于offsetfileposition,他有两个参数,后面有一个可选的布尔值参数,默认TRUE,向前移位,你也可以改成false那么就是向后移位了


状态序列的读取貌似是数组形式,你怎么读取X中的数值呢

艾约bright 发表于 2022-6-4 12:54:15

cly806 发表于 2022-6-4 10:44
关于offsetfileposition,他有两个参数,后面有一个可选的布尔值参数,默认TRUE,向前移位,你也可以改成f ...

我只是为了叙述方便把它描述成数组。实质上就是几个连续的数字,按顺序存在文件里。你要读相应位置的数就把offset移到那里,1个数对应4字节。需要记住每一个数字代表什么含义

cly806 发表于 2022-6-4 16:24:22

本帖最后由 cly806 于 2022-6-4 16:25 编辑

艾约bright 发表于 2022-6-4 12:54
我只是为了叙述方便把它描述成数组。实质上就是几个连续的数字,按顺序存在文件里。你要读相应位置的数就 ...
大概懂了,你罗列了5个数你要从第二个数开始读取,位移量就是4,然后每读取1个数就要配一个xsRead..函数

就是说你要读取第一个字符是从0开始然后第二个字符就是4开始,第三个字符就是从8开始,第N个字符就是位移位从4(N-1)开始,对吧,这应该是xs第一次用到位操作吧

艾约bright 发表于 2022-6-4 16:34:19

cly806 发表于 2022-6-4 16:24
大概懂了,你罗列了5个数你要从第二个数开始读取,位移量就是4,然后每读取1个数就要配一个xsRead..函数

...

没错,就是这个意思,所以我说按现在这套思路,你要是多关卡联动加上联动资源较多的话,会导致数字个数很多,也就会比较乱,要提前规划好各位置代表的意思。

cly806 发表于 2022-6-4 16:51:52

艾约bright 发表于 2022-6-4 16:34
没错,就是这个意思,所以我说按现在这套思路,你要是多关卡联动加上联动资源较多的话,会导致数字个数很 ...

配合我在专栏中首次全面详细叙述的随机函数作为继承比例,可以给玩家继承的时候每次都不同,这样的继承更加刺激也一定不会有相同的时候,更增加了游戏中的不确定性

troytroytroy 发表于 2022-7-22 02:12:49

DE總算達到多年前etp1.5的效果了{:164:}

xing470646171 发表于 2023-7-31 11:21:02

{:149:}啊实打实大苏打倒萨

gwh6662 发表于 2023-8-2 14:56:33

比较麻烦的一点就是只能控制读取的位置没法控制写入的位置,只能如你所说的先全部读取出来修改再重新写入
页: [1]
查看完整版本: 利用xs脚本实现战役的跨关卡联动