现在,我们将在接下来的几周和几个月内展示很多令人兴奋的内容。但是在今天,受上一次开发日志之后提出的一些问题的启发,我将为mod制作者们写一篇脚本技术方面的文章,并且激励他们。这篇文章会着眼于“什么会导致性能问题,和怎样避免制作出坏脚本” Now, we have a lot of exciting stuff to show off in the weeks and months to come, but for today, inspired by some questions that were asked after the last dev diary, I’m going to be writing about the technical side of scripting for modders and aspiring modders, specifically with an eye on what can cause performance problems and how to avoid making bad scripts.
是什么导致了性能问题呢? 每次您运行检查或执行效果时,这将占用非常少量的计算机算力。除了一些应该谨慎使用的例外(稍后我将介绍这些例外),这样完全没问题,而且做任何事情都需要这样做。但是当经常对大量对象重复检查时,问题就会发生。在实践中,这通常意味着,人口系统(pop)是其原因,尽管在银河系的所有行星上运行某种东西也是一个相当糟糕的想法。 作为第一步,在可能的情况下,控制脚本的运行时间是一个好主意。要做到这一点,最好的方法就是设置触发事件的位置,并尽可能使用 (或者从决策中触发事件等),而不是通过平均时间检查,或者更糟糕的,将事件设置成每天尝试并触发。如果事件需要一定的随机性,也可以通过触发隐藏的事件,比如说每年一次脉冲,然后用随机延迟触发实际的事件(例如,查看事件 )。 当然,并不是每一件事都是事件(event)。不幸的是,在Stellaris中,很多任务都是用pop系统来完成的!在职业文件中的职业权重和其他触发器,在过去已经发现会出现问题。作为一个长者,我给你们传授一点人生经验:即使你可以做出什么超级炫酷的事情,对于职业做出任何过于复杂的脚本pyromania(本意为纵火狂) 。 例如,如果你要使职业权重依赖于星球上没有pop处于失业状态(使用planet = { any_owned_pop = { is_unemployed = yes } }),那么你将定期检查恒星上的每一个pop,然后检查行星上的其他所有pop,也即是。这在游戏后期肯定会引发问题 What causes performance issues? Every time you run a check or execute an effect, this will take a very tiny amount of your computer’s processing power. With a few exceptions that should be used sparingly (I’ll get to those later), this is totally fine and is needed to do anything at all. It is when the check is repeated often, over lots of objects, that problems happen. In practice, this usually means pops are the cause, though running something across all planets in the galaxy is also a pretty bad idea. As a first step, when possible, it is a good idea to control when your script is run. The best way to do this is by setting where events are fired and using on_actions (or firing events from decisions and the like) wherever possible, instead of mean time to happen or, even worse, just setting an event to try and fire every day. If a degree of randomness is needed, one could also fire a hidden event via, say, a yearly pulse and then firing the actual event you want with a random delay (for an example, check out event action.220). Of course, not everything is events, and unfortunately, in Stellaris, a lot of stuff is done with pops! Job weights and other triggers in the jobs files, in particular, have been shown to cause issues in the past. As a rule of thumb, even if you can do super cool stuff, it is a bad idea to do any too complicated script pyromania on jobs. For example, if you were to make a job weight dependent on there being no other pops on the planet that are unemployed (using planet = { any_owned_pop = { is_unemployed = yes } }), then you are doing a regular check on every pop on the planet that then checks every other pop on the planet, i.e. pops squared. Once you reach the late game, this is pretty much guaranteed to cause issues.
所以我们能做些什么呢? 避免嵌套循环、确保事件被正确激发、并且尽可能避免pop系统,这些都能给你一些帮助,但是我们可以做得更好。这里是我对优化脚本的建议列表: 始终使用最适合的作用域 比如说,你想检查当前国家的联邦领导者的一些情况。理论上,你可以这么做 any_country = { is_in_federation_with = root is_federation_leader = yes <my_triggers_here> = yes } 这样的代码会遍历游戏中的所有国家,检查它们是否和你在同一个联邦,包括太空阿米巴和卑鄙的帕沙提(谁会想让他们加入联邦呢?)。那是一群绝对无关紧要的东西。 因此,更好的检查方法可以这样做: any_federation_ally = { is_federation_leader = yes <my_triggers_here> = yes } 在代码术语中,这代表游戏从当前国家先遍历联邦,然后获取其成员列表,不含当前国家,并检查触发器。所以,显然这会减少检测次数。 但是最好的版本应该是: federation.leader = { <my_triggers_here> = yes } 这一版代码直接检查联邦,并且转到代码中的领导者,这是最少的脚本到代码转换,并且不需要针对任何国家的检查触发器。这也恰好是可读性最好的。(可读性与更好的性能通常非常相关) 在这一实例中,游戏检查的国家数从第一版的50个,到第二版的5个,再到第三版的1个——这对优化确实不错! 运用类似的逻辑,不要使用检查银河系中的所有物体的脚本总是更好的(尤其是,所有的pop或所有的行星),而是使用过滤后的列表,比如any_planet_within_border instead of any_planet = { solar_system.owner = { is_same_value = prevprev } }(你可能会笑,但是我已经想到了)。事实上,人们几乎总是可以检查any_owned_fleet而不是any_owned_ship So what can be done? Avoiding nested loops, making sure your events are fired appropriately and avoiding pops when possible will get you some way, but we can do better than that. Here is my list of advice for optimising scripts: Always use the most appropriate scope Say you want to check something on the leader of the current country’s federation. One could, theoretically, do it this way: {Code A} This'll run through all countries in the game and see whether they are in the same federation as you, including the space amoeba country and the vile Pasharti (and who would want them in a federation?). That’s a bunch that are definitely irrelevant. So a better check would be to do it this way: {Code B} In code terms, this means that the game going from the country to the federation and then grabbing a list of its members, excluding the current country, and checking the triggers against them. So, that’s obviously going to be fewer checks. However, the best version would be this: {Code C} That version would go straight to the federation and from there straight to its leader in the code, with as little as possible script to code conversion needed and no need to check triggers against any countries to get there. It also happens to be the most readable (readability and better performance very often correlate…). So in this case, the game would check around 50 countries first the first version, 5 for the second and 1 for the third - not bad for some optimisations! Using a similar logic, it is always better to use something that isn’t checking all objects in the galaxy (esp. all pops or all planets) if at all possible but rather a filtered list, e.g. any_planet_within_border instead of any_planet = { solar_system.owner = { is_same_value = prevprev } } (you laugh, but I’ve seen it). And, indeed, one can almost always check any_owned_fleet instead of any_owned_ship. Another important improvement we added in 2.6 was any_owned_species, which can replace many any_owned_pop checks (specifically the ones that check for traits and so on of the pop) and mean that way, way fewer objects have to be checked (in a xenophobic empire, it could be single figures for any_owned_species and thousands for any_owned_pop).
有时你可以完全避开作用域 在类似的情况下,如果您可以在不使用作用域的情况下检查某些内容,这总是会更好。因此,如果人们想要检查一颗行星上是否有两个以上的pop作为矿工工作,可以通过两种方式做到这一点: count_owned_pop = { count > 2 limit = { has_job = miner } } num_assigned_jobs = { job = miner value >= 2 } 前者将检查行星上的每个pop,看看它是否有挖矿任务,然后查看是否有大于2的数字。后者将检查游戏已经计算出的缓存的矿工数,检查其是否大于2,这样做要快得多。 *Sometimes you can avoid scopes completely* On a similar note, if you can check something without doing things with scopes, that’s always going to be better. So, if one wants to check whether a planet has more than two pops working as miners, one could do this two ways: {Code A} {Code B} The former will check each pop on the planet and see whether it has the miner job, and then see whether the number that do is higher than 2. The latter will check a cached number that the game has already calculated and see if it is more than 2, which is much quicker to do.
有些东西开销就是昂贵 并不是每一项检查或效果都是相等的。检查一个标志(flag)或值(value)通常相当简单,更改它通常也不会太复杂。然而,如果游戏必须重新计算一些东西,那么它将花费更长的时间,因为它不仅只是查找已经知道的数字。创建新程序的成本也更高,这既是因为它在做一些有点复杂的事情(的效果就是,我不是在开玩笑,这超过600行C++代码……),又是因为一旦完成这项工作,它可能必须重新计算各种值。要知道哪些触发器和效果将是不好的,这可能有点棘手。但通常情况下,这些情况是您应该注意的: 要创建新作用域的任何内容,例如create_country、create_species、modify_species。 需要您计算或重新计算寻路的任何内容(例如,can_access_system触发器、创建新超时空通道,特别是创建一个新的系统)。 任何计算pop的内容(例如,在行星上更改pop的工作) Some things are just expensive Not every check or effect is equal. Checking a flag or a value is generally pretty simple, and changing it is usually not much more complicated. If, however, the game has to recalculate stuff, then it will take longer, because it’s not just looking up a number it already knows. Creating new stuff is also more expensive, both because it’s doing something somewhat complicated (the create_species effect is, I kid you not, more than 600 lines of C++ code...), and because it’ll probably have to recalculate all sorts of values once this is done. It can be a bit tricky to know which triggers and effects are going to be bad, but as a rule, these cases are what you should look out for: Anything where you are creating a new scope e.g. create_country, create_species, modify_species Anything that needs you to calculate or recalculate pathfinding (e.g. can_access_system trigger, creating new hyperlanes, especially creating new systems) Anything that calculates pops (changing around pop jobs on a planet, for instance)
如果必须做的话…… 有时候,坏的操作必须做。在这些情况下,最好还是精准地使用不那么大的东西。当游戏检查触发器,如事件时,它通常会停止在第一个返回值为false的检查点(我被告知这叫做“短路评估”),所以您要做以下类似的事情: trigger = { has_country_flag = flag_that_narrows_things_down_loads <something really horrible here> } 我最近做了些类似于难民pop效应的事情。在以前,这有些疯狂(完整内容参见01_scripted_triggers_refugees.txt)总之,它将对一系列变化检测8次。 any_relation = { is_country_type = default has_communications = prev #relations include countries that have made first contact but not established comms NOT = { has_policy_flag = refugees_not_allowed } prevprev = { #this ensures Pop scope, as root will not always be pop scope OR = { has_citizenship_type = { type = citizenship_full country = prev } has_citizenship_type = { type = citizenship_caste_system country = prev } AND = { has_citizenship_type = { type = citizenship_limited country = prev } has_citizenship_type = { type = citizenship_caste_system_limited country = prev } prev = { has_policy_flag = refugees_allowed } } } } any_owned_planet = { is_under_colonization = no is_controlled_by = owner has_orbital_bombardment = no } } 它的变化只是最后一个any_owned_planet:它会尝试为这一pop找到一个很适合(really good)的宜居星球,然后是一个较好的(pretty good),然后是一个不错的(pretty decent),然后最终定居于任何旧星球。显然,这是相当低效的,因为欢迎难民的国家名单在你检查的8次中每一次都不会改变。我避免这样做的方法是在任何检查之前设置一个标志(flag),这同时让脚本更加宜读,就像这样: every_relation = { limit = { has_any_habitability = yes #bare minimum for being a refugee destination } set_country_flag = valid_refugee_destination_for_@event_target:refugee_pop } 然后,检查就必然是:“这一国家是否有标志。如果有,那么这一国家有一颗足够好的星球”: has_good_habitability_and_housing = { has_country_flag = valid_refugee_destination_for_@event_target:refugee_pop any_owned_planet = { habitability = { who = event_target:refugee_pop value >= 0.7 } free_housing >= 1 is_under_colonization = no is_controlled_by = owner has_orbital_bombardment = no } } 另外,我们也可以在触发器中用if限制和else(或者可能的话,用switch——这样的性能最优),从而缩小检查范围。这同时也可以使你的脚本更加可读。最近我处理物种权利的文件,并且重做了允许触发器for sanity’s sake: #Before custom_tooltip = { fail_text = MACHINE_SPECIES_NOT_MACHINE OR = { has_trait = trait_mechanical has_trait = trait_machine_unit from = { has_valid_civic = civic_machine_assimilator } } } custom_tooltip = { fail_text = ASSIMILATOR_SPECIES_NOT_CYBORG OR = { NOT = { from = { has_valid_civic = civic_machine_assimilator } } AND = { OR = { has_trait = trait_cybernetic has_trait = trait_machine_unit has_trait = trait_mechanical } from = { has_valid_civic = civic_machine_assimilator } } } } #After if = { limit = { from = { NOT = { has_valid_civic = civic_machine_assimilator } } } custom_tooltip = { fail_text = MACHINE_SPECIES_NOT_MACHINE OR = { has_trait = trait_mechanical has_trait = trait_machine_unit } } } else = { custom_tooltip = { fail_text = ASSIMILATOR_SPECIES_NOT_CYBORG OR = { has_trait = trait_cybernetic has_trait = trait_machine_unit has_trait = trait_mechanical } } } 第二个版本更有效率。对于物种是否有机械体特性,或者国家是否有同化斗士的国策,它只用检查一次而不是两次。 If it must be done... Sometimes, bad things must be done. In these cases, it is best to still use the not so great things with precision. When the game is checking triggers e.g. for an event, it’ll generally stop checking them at the first point something returns false (I’m told this is called “short-circuit evaluation”), so you’ll want to do something like this: {Code A} I recently did something like this to the refugee pop effect. It was previously a little bit insane (see 01_scripted_triggers_refugees.txt for the full horror). In total, it would check a variation of the following up to eight times: {Code B} Where it varied was simply the last any_owned_planet: It would try and find a relation with a really good planet for the pop to live on, then a pretty good, then a pretty decent, and then finally settle for just any old planet. Which is, obviously, pretty inefficient, since the list of countries that welcome refugees does not change between each of the 8 times you check it. My way of avoiding this - and making the script way more readable, whilst I was at it - was to set a flag before any of the checks, like this: {Code C} The checks then simply had to be “does the country have the flag, if yes, does it have a good enough planet”: {Code D} One can also similarly use if-limits and elses (and, even better, switches when possible - those are the best for performance) in triggers to narrow down the checks down and make things far more readable whilst you are at it. I recently went through the species rights files and redid the allow triggers for sanity’s sake: {Code E1} {Code E2} The second version will be more efficient, since it is only checking e.g. whether the species has the mechanical trait or whether the country has the assimilator civic once instead of twice, and also, the triggers in the second custom tooltip aren’t obscenely weird anymore. (I also removed all the NANDs, because they broke my brain).
新年快乐 我写了这篇开发日志,就不能不告诉你“新年快乐”bug。简单地说,我们在一个相当大的星系上玩开发版MP,并达到了相当晚的时间。并且由于我们都在远程工作,以各种不同的电脑和互联网连接速度运行,性能可能有点慢,但大部分还是可以接受的。然后,突然间,我们注意到了巨大的延时尖峰——大约20 s或者更多——在1月1日。这些尖峰如此引人注目,以至于每次我们的游戏都冻结了,于是我们就开始祝福其他人新年快乐! 这种延时的爆发恰好与几个大帝国决定机飞,并开始同化他们的帝国相吻合。现在,同化属于应该在脚本中完成的事情,事后看来或许不应该那么做……甚至通过在每个1月1日为每个同化国家触发一个事件来工作。这个事件反过来为他们的每个行星都触发了一个事件,而这一被触发的事件有选择了行星上的每个pop,并至少对每个pop使用了一次modify_species,有时能多达四次。这加起来是十分重要的性能开销。 在尝试了各种解决方案后,我们发现最好的修复办法是,首先检查在这一国家作用域内的every_owned_species,检查这一物种是否应该被同化。如果应该,那么用modify_species来创造将被同化的物种,设置一个物种标志,用以指向正在被同化的物种。然后,不是为每一个被同化的pop创建一个新物种,脚本被重写为:查找已经被创造的pop应该被同化的物种,并简单地对其使用change_species。这一结果仍然是不可读的脚本(我会节省你们的眼神,不把它放在这里),但是在我的测试中,这样做减少了超过50%的每年的tick数。因为我们尽可能少地运行了复杂的效果(modify_species)。 Happy New Year I can’t write this dev diary without telling you about the “Happy New Year” bug. Basically, we were playing dev MP on a reasonably large galaxy and reached reasonably late into the game, and since we were all working remotely on wildly varying computers and internet connection speeds, the performance was perhaps a tad sluggish, but still acceptable for the most part. Then, suddenly, we noticed huge lag spikes - 20 seconds and more - on the 1st of January. So noticeable were these spikes that we began wishing each other a Happy New Year each time the game froze! It just so happened that the onset of this lag coincided with several large empires deciding to become synthetic and starting to assimilating their empires. Now, assimilation falls into the category of things that are done in script that maybe, in hindsight, should probably not have been done that way… and works by firing an event for each assimilating country every 1st of January. This event in turn fired an event for each of their planets that selected a bunch of pops on the planet and used modify_species on each of them at least once, but sometimes up to four times. This added up to a fairly significant performance hog! After trying various solutions, it turned out the best way to fix this was to first go through every_owned_species from the country scope, check whether this species should be assimilated, and if so use modify_species to create the species it would assimilate to, setting a species flag that pointed to the species it was being assimilated from. Then, instead of creating a new species for every pop that was assimilated, the scripts were rewritten to find the already-created species that the pop should become, and simply use change_species on it. The result is still unreadable script (I will spare your eyes and not post it here), but in my tests it reduced the yearly tick by over 50%, thanks to the complicated effect (modify_species) being run as seldom as possible.
现在就是这些了!我想对大多数人来说,这是一个比平时更枯燥的开发日志,但是我希望它仍然很有趣。(括弧笑)最后,我想你们中比较勇的那些可能会把我叫出来,给我看在一些并不符合我之前讲述那些原则的脚本。请随便这么做,因为对于这个职业来说,没有什么比让一些相当糟糕的事情变得不那么糟糕更令人满意了 That’s it for me, for now! I’m guessing this was a bit of a drier dev diary than usual for most of you, but hopefully it was interesting nonetheless. :) As a final note, I suspect that the more intrepid among you could call me out on bits of script in the base game that don’t quite live up to these guidelines. Please, feel free to do so, because there are few things more satisfying in this profession than taking something really horrible and making something less horrible out of it!