Stellaris开发日志#240 | 2/3 3.3版本号的脚本系统改进

牧游社 牧有汉化翻译
Stellaris Dev Diary #240 - Scripting Improvements in 3.3
Caligula Caesar, Stellaris Technical Scripter
Hello and welcome to another modding-based dev diary - as has become tradition in the weeks before releases in recent times. I fear that we may soon run out of ways to revolutionise the script system, but for now, there's some pretty cool improvements we have to show off which will be making their debut in 3.3 (you can try them out early in the open beta).
大家吼啊,欢迎阅读另一篇主要关注Mod开发的开发日志——在更新发布前几周发布与Mod开发相关的开发日志,已经在最近成为了我们的传统。我担心我们可能很快就会穷尽改进脚本系统的方法,但现在我们还是有一些好康的改进能给大家看看的,它们将在3.3版本号中首次亮相(你也可以在开放测试中试用)。
Script Values
脚本化数值
This story starts with weight fields. With which I mean something that looks a bit like this:
首先从权重方面开始。这个系统差不多是下面这个样子:
Code:
weight = {
base = 1
modifier = {
factor = 2
some_trigger = yes
}
}
We realised that the code underlying this script structure was not consistent: there were a number of distinct code implementations that varied in ways that were not readily obvious to the end user, the scripter. For instance, in certain ones, you could input "factor" or "add", in others, "factor" or "weight". Then there were the downright problematic cases: sometimes, when you set the base to 0, the game would read 1, and in one case (ai personalities) "factor" would actually count as "add"!
我们发现这个脚本结构的底层代码并不统一:有许多不同的代码实现方式,对终端用户(脚本编写者)而言很难发现问题。例如,在某些脚本中你输入“factor”和“add”,在其它脚本中你则是输入“factor”和“weight”。然后是出现问题的情况:有时,当你将基数设为0时,游戏会读为1,在一种情况下(ai人格),“factor”实际上会按照“add”处理!
The solution here was to remove all the variations and consolidate them into one code implementation to rule them all. It would have to be made to incorporate the idiosyncrasies (except for the mentioned issues/bugs) of the existing versions (i.e. not break vast swathes of script), but on the other hand, having one system would allow for us to roll out improvements that could be used everywhere in the game.
这里的解决方案是删除所有的变量,并将它们整合到一个代码实现方式中。它必须结合现有版本的特性(除了提到的问题/错误)(即不破坏当前的大量脚本),但另一方面,这个系统将允许我们推出可以在游戏中随处使用的改进。
Despite a few hitches at the start (I may or may not have accidentally had every anomaly capable of spawning on every planet, at one point), this proved quite achievable, so now we no longer need to worry about these fields working differently in different places. Basically the only variance left is whether their default value is 1 or 0.
尽管一开始出现了一些问题(我不能保证每一个异常现象都能在任何一个星球上生成),但事实证明,这是完全可以实现的,所以现在我们不再需要担心这些东西在不同地方的工作方式不同。基本上,唯一剩下的差异是它们的默认值是1还是0。
This done, a few more things could be added to the system. For instance, why just have "factor", "add" and "weight"? There are a lot of other mathematical operations out there. So we added subtraction, division, modulo, min, max, abs, and rounding (with round, floor, ceiling and round_to). We also made it no longer necessary to enclose these in "modifier = {}", if they were meant to always apply rather than be triggered.
这样一来,系统中还可以添加更多的东西。例如,为什么我们只有“factor”、“add”和“weight”?还有很多其他的数学运算。所以我们添加了减法、除法、模、最小值、最大值、绝对值和舍入(包括四舍五入、向下取整、向上取整)。如果它们总是要被使用而不是被触发的话,我们也不再需要将它们括在“modifier={}”中。
But that was just the start. Back in 3.1, we added the ability to use "trigger:<trigger>" in lieu of a number in places such as this, to allow some more complicated maths (so it would take the result of the trigger e.g. num_pops could return 32 pops instead of an absolute number). The code behind this wasn't quite ideal, though. Basically, whenever the game wanted to calculate what "trigger:num_pops" meant, it would take the string "trigger:num_pops", see if it started with "trigger:", if yes then shave that off, and then try and make a trigger from the remainder (and log an error if it failed). Unfortunately, this wouldn't happen during startup, but rather whenever the game encountered this in script - for example, if it was needed to calculate a tooltip, it would be doing this every frame. Which made it annoying to debug and more potentially costly performance-wise than it needed to be.
但这只是开始。回到3.1版本号中,我们添加了使用“trigger:<trigger>”来代替这种数字的功能,以允许进行一些更复杂的数学运算(因此它将使用触发器的结果,例如num_pops可以返回“32 pops”,而不只是32)。不过,这背后的代码并不十分理想。基本上,每当游戏想要计算“trigger:num_pops”是什么意思时,它都会使用字符串“trigger:num_pops”,查看它是否以“trigger:”开头,如果是,则将其删除,然后尝试从剩余部分中创建一个触发器(如果失败,则记录错误)。不幸的是,这不会发生在启动期间,而是发生在当游戏在脚本中每次遇到这种情况时——例如,如果需要计算提示栏中的数据,它会在每一帧都这样做。这使得调试变得很烦人,而且在性能方面的潜在消耗比实际需要的要高。
This could be done better. So, for 3.3, we made a container called a "CVariableValue", which could contain several objects:
这是可以被改进的地方。所以,在3.3版本号中,我们做了一个叫“CVariableValue”的容器,它可以容纳多个对象。
- An integer or fixed point (basically just a normal number)
- 一个整数或小数点位置固定的数(一个普通的数字)
- A scope/event target - so you could refer to "owner.trigger:num_pops"
- 一个作用域或事件对象-可以参考“owner.trigger:num_pops”
- A trigger
- 一个触发器
- A modifier definition
- 一个修正项定义
- A variable string
- 一个可变的字符串
- A script value*
- 一个脚本化数值*
* I'll get back to this later.
* 我之后会回来讲这个地方
Basically, whenever the game would read a script value, it'd work out what it is on startup. This means that whenever the actual value is needed, it would not have to chop up the string and work out what is wanted, but it could simply give the value or call the trigger that is specified. Coincidentally, this system also made it vastly easier to roll out the ability to use "trigger:<trigger>" in various places, so if there are more places where they'd be desirable, we really don't have that many excuses not to provide them there (uh oh).
基本上,每当游戏读取脚本化数值时,它都会在启动时计算出脚本化数值。这意味着,无论何时需要实际值,它都不必切碎字符串并计算出所需内容,而是可以简单地给出值或调用指定的触发器。巧合的是,这个系统也使得在不同的地方推广使用“trigger:<trigger>”变得非常容易,所以如果有更多的地方需要它们的话,我们真的没有那么多借口不提供它们(迫真)。
The modders amongst you will have noticed that there's a few extra things we made possible along the way, there. Firstly, a quick win was to let you call "modifier:<modifier>" in the same way as you'd call "trigger:<trigger>". Basically, if a pop had +20% citizen happiness modifiers applying to it, and you used "modifier: pop_citizen_happiness", you'd get 0.2. The other thing we added was script values.
Mod作者可能注意到了,我们在这个过程中可能还做了点额外的东西。首先要简单说的是,现在调用“modifier:<modifier>”和调用“trigger:<trigger>”方式相同。大体上,如果要对一个人口使用一个+20%幸福度修正,并且你使用了“modifier: pop_citizen_happiness”,你会获得0.2。此外,我们添加进去的另一个内容是脚本化数值。
The idea for these came from newer PDS games, the Content Designers of which would taunt us with their games' superior script maths capabilities. Basically, the gist of what made them powerful was being able to substitute a value for a key which would run a series of calculations on demand. So "my_script_value" could be 57 + ( 24 * num_pops ) / num_colonies, or something like that. With the already-mentioned changes, we were almost there, so we added a new thing (named after script_values and capable of many of the things they are capable of in our newer games, but actually sharing very little code, so the exact workings probably vary a bit).
这些新想法来自于比Stellaris更晚的PDS游戏,那些游戏的内容设计师会用他们游戏中更优越的脚本数学性能来嘲讽我们。基本上,让它们的游戏性能强大的关键就是“让数值代替键值”,这样就能根据需要进行一系列的计算。所以“my_script_value”可以是57+(24*num_pops)/num_colonies,或者是类似的东西。通过前面提到的修改,我们几乎做到了,所以我们添加了一个新东西(命名为script_value,能够在我们的新游戏中实现许多功能,但实际上相同的代码很少,因此实际的工作方式可能会有一点不同)。
These "script values" would basically be a weight field like that mentioned at the start of this section, which would be defined by key in a script_values directory, e.g.
这些“script values”实际上是权重字段,就如本段开始所说的,由script_values目录中的键值定义的。下面是示例。
Code:
leader_cost = {
base = 2
modifier = {
subtract = 6
num_owned_leaders > 5
}
modifier = {
add = trigger:num_owned_leaders
num_owned_leaders > 5
}
mult = 50
}
Then we could refer to it anywhere in the game via "value:leader_cost", and it would calculate the value on demand. We are already finding this very useful in improving the game's scripts - not only is it easier to get correct values with this, but we can also radically cut down on copy-pasted scripts in weight fields (job weights, I’m coming for you!). Conveniently, since script values are read in a similar way to scripted values and triggers, we can feed in parameters, e.g. value:my_value|PARAMETER|50| would have the game use the script value my_value where any instance of "$PARAMETER$" would be substituted with 50.
然后我们可以在游戏的任何地方使用“value:leader_cost”,它将根据需要计算数值。我们已经发现这在改进游戏脚本方面非常有效——不仅更容易获得正确数值,同时能从根本上减少权重字段中复制粘贴的脚本(岗位权重,说你呢!)。方便的是,因为脚本化数值读取方式和预设数值及触发器的读取方式类似,我们能直接输入参数。比如value:my_value|PARAMETER|50|将让游戏使用脚本值my_value,并将所有的“$PARAMETER$”替换为50。
Even with all these changes, there were still a couple more we could make to the scripting language. The first was adding complex_trigger_modifiers to script_values and weight fields. Basically, these allow you to use the value of triggers too complicated to use with "trigger:<trigger>". An example would be this:
即便已经有了这些改动,我们还是能对脚本语言做进一步的改进。第一个就是将complex_trigger_modifiers增加到script_values和权重中。大体上,这会允许你使用复杂到用不了“trigger:<trigger>”的触发器值。以下是例子:
Code:
complex_trigger_modifier = { #fewer worlds => more menace from destroying one
trigger = check_galaxy_setup_value
parameters = { setting = habitable_worlds_scale }
mode = divide
}
This works with the same triggers that work with export_trigger_value_to_variable. We also added a few triggers to these: notably, all count_x script list triggers (e.g. count_owned_planet), and the "distance" trigger.
这对于能对export_trigger_value_to_variable生效的触发器都可以生效。我们也在以下方面增加了几个触发器:特别是所有的count_x脚本菜单触发器(例如count_owned_planet),以及“距离”触发器。
A comprehensive guide on all you can do with script values is attached to this post (and in common/script_values). To be honest, it's hard to overstate the amount of things this new system system enables us to potentially do. For instance, in the example above, we scaled leader costs based on how many leaders you own. We also scaled Unity boosts with the Autochthon Monument based on how many ascension perks you have unlocked with this method. The list goes on and will continue to grow with each update we release.
如何使用这些脚本化数值的全面指南在文后的附件(还有common/script_values)中。说实话,这个新系统能让我们干的事简直说不完。比方说,在上边的例子中,我们将领袖花费与你有多少领袖挂钩。我们也可以让先驱者纪念碑提供的凝聚力加成和你解锁了多少个飞升天赋相关。相关例子还有不少,随着我们的不断更新也会越来越多。
Mod Overwriting
Mod覆写
Script values isn't the only thing I can talk about today. Modders have long been a bit bemused by the different ways elements of the game handle overwriting. Specifically, by the way it varies. Unfortunately, they will probably continue doing so for a while yet, but since a bit of progress was made here, I felt it would be interesting to people to know why this sort of issue occurs.
我今天能说的可不止是脚本化数值。Mod制作者们经常被这游戏各种各样处理覆写的方式搞得有点晕头转向,而且覆写方式实在有点多。不幸的是,这段时间内这一点恐怕没法改变了,但是我们既然已经做出了一些进展,我觉得和大伙儿说说为什么会有这种问题还是挺有意思的。
Basically, when modders overwrite the vanilla files, they can either overwrite the entire file (which always works), or they can overwrite individual entries within the file, for example the "miner" job. When the game encounters a second entry that matches the key of an existing one, various things can happen:
大体上,当Mod制作者们覆写原版游戏时,他们要么覆写整个文件(总是能够生效),要么只覆写文件中的个别条目,比方说“矿工”岗位。当游戏遇到了第二个符合现有键值的条目时,可能会发生许多不同的事情:
- It replaces the existing one perfectly (last read is used)
- 完美取代了原先的条目(只采用最后读取的那个)。
- It replaces the existing one, but imperfectly, e.g. if you individually overwrite jobs, you can no longer refer to them in modifiers (not ideal)
- 取代了原先的条目,但是不太完美,比方说你重写了个别岗位,你就不能在修正项中指向它们了(不太理想)。
- The second entry is ignored (first read is used)
- 第二个条目被无视(采用了首次读取的)
- Both the first and the second entries are kept (duplication - not ideal)
- 第一次读取和第二次读取的条目都被保留了(重复——并不理想)
- It appends the information inside to the existing entry (special case for on_actions)
- 在已有条目后追加了新条目的信息(on_actions的特殊情况)。
So, why are there these various behaviours? Basically, it is largely a matter of how the database is read in the C++ code.
那么,为什么会有这么多不同的行为呢?主要还是因为C++代码中数据库的读取方式。
When the game encounters script files, as a rule, it will read the object (e.g. miner = { }) and store that in a matching database (e.g. the job type database), which the game will use in various ways when it needs to do stuff with that matching object type. In the case of many of the oldest objects in the game (stuff that has existed largely in its current form since before release, e.g. technologies and ethics), they would be coded in as an object in a custom-made database. Since the code for reading this database would be written afresh (or copied) each time a new object was defined, both the order in which it would read files (A to Z or Z to A) and the way it would treat duplicates could vary, which is not ideal. In some cases, this made sense: for example, in on_actions, there is a legitimate cause for special handling - basically, the intention there is that modders can use on_actions without having to worry about Vanilla contents. This is also the case for heavily code-dependent databases, such as diplomatic actions, where one cannot simply add an entry and expect the code to know what to do with it.
当游戏遇到脚本文件时,按照规则,它会读取对象(比如miner={ })并将其存储在对应的数据库中(比如岗位类型数据库),而对于匹配的对象类型,游戏会用多种方式使用数据库。对于游戏中许多的最老的对象而言(那些从发售前到现在基本没怎么变的东西,比如科技和思潮),它们是作为特制数据库中的对象编码出来的。因为读取这个数据库的代码每次定义新对象的时候都要重新写一遍(或者复制过去),它读取文件的顺序(A到Z或者Z到A)和它对待复制品的方式也会有所不同,这就不太好了。在某些情况下,这说得通:比如在on_actions中,对于这种特殊处理方式确实有合适的原因——原因大致是Mod开发者们可以在完全不用担心原版内容的情况下用on_actions。对于一样特别依赖于代码的数据库来说也是如此,比如外交行动,这时候就没法加一行代码然后指望它还能知道怎么处理这个。
But for most cases, this is simply a matter of tech debt: nowadays, we have better ways of coding in databases. When we add a new object, we now (since a couple of years) add it as a TGameDatabaseObject in a TSingleObjectGameDatabase. The standard code for the TSingleObjectGameDatabase handles the reading of objects and need not be copy-pasted, and most importantly for modders, it handles overwriting by deleting the existing object and replacing it with the new one.
但是在大多数情况下,这就是一个简单的“技术债”问题(译注:指开发团队早期选择了一个短期内易于实现的方案,但从长远来看,这种方案会带来消极的影响)。当下我们有了更好的数据库中的编程方法。当我们增加一个新的对象时,我们(从几年前开始)会把它作为TSingleObjectGameDatabase中的TGameDatabaseObject添加进去。TSingleObjectGameDatabase中的标准代码可以处理对象的读取,也不需要复制粘贴了,而且对于Mod制作者来说十分重要的是,它可以通过删除原有对象再用新增对象代替的方式处理覆写问题。
This usually works well for modders, but there were some high-profile cases where it didn't: in the cases of jobs, districts, planet classes and a few others, modifiers would be broken, i.e. a modifier adding miner jobs to a planet would end up doing nothing. Basically, what would happen is, the job would create modifiers, which would be a) added to the modifier database and b) stored in the game's job definition (not the script file, but rather what the program gets from reading the script file) - which allows the game to attach an effect to that modifier (i.e. grant x number of this job). Then the job would be deleted and a new one would be made. It too would create modifiers in the same way. But now the modifier list would have two entries with the same key. Then, when the game encounters such a modifier when it is used in a script file, it will look through the list, find the first one which matches, and assume that was the one intended. Unfortunately, the job itself thinks that the second modifier applies to it. As a result, the modifier - for the intents and purposes of a modder - becomes unusable.
通常来说,这对于Mod制作者能正常生效,但无法应对一些经常出现的情况:对于岗位、区划、行星类型以及其他一些情况来说,修正项会损坏,也就是说,一个给行星增加矿工岗位的修正向最后什么也不会增加。大致上发生的情况就是,这个岗位会创造修正项,这会是:a)增加到修正项的数据库中,以及b)储存在游戏的岗位定义当中(不是脚本文件,而是程序从脚本文件中得到的东西)——这会允许游戏为这个修正项增加效果(比方说加X个这个岗位)。然后岗位就会被删除,新的岗位就会创造出来。它也会以同样方式创造出修正项。但是现在,修正列表中会有两个有着相同键值的条目。之后,游戏遇到一个在脚本文件中这么用的修正项时,就会过一遍列表,找到第一个匹配的,并且认为这就是需要的那个。不幸的是,岗位本身觉得是用的是第二个修正项。最后的结果就是,对于Mod制作者的目的而言,这个修正项就发挥不了作用。
I can report good news on this front, though - we fixed that issue. These objects can now be overwritten safely, or at least, this particular cause is not a reason for their overwriting to break - the modifiers will now function properly. (Why the caution? Well, basically, if adding an entry to one database alters another database, then overwriting it can cause issues unless carefully handled; luckily, this is fairly rare aside from the example of modifiers). This will hopefully be quite useful to modders, since jobs and districts are some of the objects that the are probably most likely to want to alter.
不过,这方面我可以说一些好消息——我们已经修复了这个问题。这些对象现在可以安全地覆写了,或者说至少这个特定的原因不再会导致覆写不管用了——这些修正项现在可以正常工作。(为什么这么小心呢?好吧,因为如果往一个数据库中增加条目会改变另一个数据库,覆写还是会导致问题,除非处理得非常谨慎;幸好,除了这个修正项的例子之外,这样的情况非常少见)。希望这个改动对于Mod制作者来说会很有用,因为岗位和区划大概是他们最想改动的东西吧。
As a final note on TGameDatabaseObjects, since the way they are all read uses the same lines of code, we added a small gift for modders: the error log message warning about overwrites will now specify exactly which one is used, removing some of the ambiguity in overwriting. So if you see this error message, you can be fairly confident of how the game is behaving:
最后还要说一下TGameDatabaseObjects,因为它们读取的方式使用了同样的几行代码,我们为Mod制作者们增加了一个小礼物:覆写的错误记录警报信息会特别指出实际上用了哪一个条目,这样就能去掉一些覆写时的不确定性了。如果你看到了这条错误信息,你就能比较确定游戏的运行方式了。
[14:03:02][game_singleobjectdatabase.h:147]: Object with key: miner already exists, using the one at file: common/pop_jobs/03_worker_jobs.txt line: 319
As a side note, we've extended the 3.3 Unity Open beta feedback period until Monday, February 7th. We will be leaving the Open beta branch available until 3.3 releases so those of you who are currently playing on the open beta can continue your games until 3.3 releases. And if you haven't yet, please leave your feedback on the 3.3 Unity Open beta here!
还有件事,我们把3.3版本号凝聚力改动的开放测试反馈环节延长到了2月7日星期一。我们也会把开放测试一直开到3.3版本号发布为止,这样你们还没玩完的话就可以继续玩到3.3版本号发布了。如果你还没留下反馈的话,请务必到这里提供你对于3.3版本号凝聚力改动的开放测试的反馈!
Don't miss the next episode of Dev clash 2022 on Monday, February 7th, starting at 1500 CET.
别错过了星期一,2月7号的2022年的开发者大战,欧洲中部时间15:00开始!
http://twitch.tv/paradoxinteractive
That's all for this week! Eladrin will be back next week to share his thoughts on the open beta and, of course, the dev clash!
这周就到这里了!Eladrin下周会回来说说他对于开放测试当然还有开发者大战的感想!
翻译:枸杞泡阔落 AntiAccess
校对:一水战阿部熊 三等文官猹中堂
欢迎关注UP主和主播小牧Phenix!
欢迎关注牧游社微信公众号和知乎专栏!微信公众号改版为信息流,欢迎【置顶订阅】不迷路,即时获得推送消息!
B站在关注分组中设置为【特别关注】,将会在私信内及时收到视频和专栏投稿的推送!
欢迎加入牧有汉化,致力于为玩家社群提供优质内容!组员急切募集中!测试群组822400145!
本作品英文原文著作权属Paradox interactive AB所有,中文译文著作权属牧有汉化所有。