stellaris吧 关注:263,938贴子:6,439,339

Stellaris开发日志 #182 脚本编写的可能风险以及如何避免他们

只看楼主收藏回复

Stellaris Dev Diary #182 : The Perils of Scripting and How to Avoid Them
对于玩家没什么用


IP属地:重庆1楼2020-09-04 13:39回复
    现在,我们将在接下来的几周和几个月内展示很多令人兴奋的内容。但是在今天,受上一次开发日志之后提出的一些问题的启发,我将为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.


    IP属地:重庆3楼2020-09-04 14:01
    收起回复
      又是这种鬼东西


      IP属地:广东来自iPhone客户端5楼2020-09-04 16:18
      回复
        第一段被锑度吞了,虽然这段没什么卵用


        IP属地:重庆6楼2020-09-04 16:43
        回复
          是什么导致了性能问题呢?
          每次您运行检查或执行效果时,这将占用非常少量的计算机算力。除了一些应该谨慎使用的例外(稍后我将介绍这些例外),这样完全没问题,而且做任何事情都需要这样做。但是当经常对大量对象重复检查时,问题就会发生。在实践中,这通常意味着,人口系统(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.


          IP属地:重庆8楼2020-09-04 17:15
          收起回复


            IP属地:广东来自Android客户端9楼2020-09-04 17:24
            收起回复
              所以我们能做些什么呢?
              避免嵌套循环、确保事件被正确激发、并且尽可能避免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).


              IP属地:重庆10楼2020-09-04 17:42
              回复
                有时你可以完全避开作用域
                在类似的情况下,如果您可以在不使用作用域的情况下检查某些内容,这总是会更好。因此,如果人们想要检查一颗行星上是否有两个以上的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.


                IP属地:重庆本楼含有高级字体11楼2020-09-04 17:48
                回复
                  摸了摸了,晚上再来


                  IP属地:重庆12楼2020-09-04 17:49
                  回复
                    锑度不支持Markdown,辣鸡


                    IP属地:重庆13楼2020-09-04 17:50
                    收起回复
                      有些东西开销就是昂贵
                      并不是每一项检查或效果都是相等的。检查一个标志(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)


                      IP属地:重庆14楼2020-09-04 17:56
                      回复
                        如果必须做的话……
                        有时候,坏的操作必须做。在这些情况下,最好还是精准地使用不那么大的东西。当游戏检查触发器,如事件时,它通常会停止在第一个返回值为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).


                        IP属地:重庆15楼2020-09-04 22:32
                        回复
                          emmm,脚本代码还是去P社论坛的开发日志看吧


                          IP属地:重庆16楼2020-09-04 22:33
                          回复
                            好活,支持翻译


                            IP属地:广西17楼2020-09-04 22:36
                            回复