rimworld吧 关注:277,965贴子:5,145,265

泰南行为学:实战篇

只看楼主收藏回复

上面我们介绍完了小人的行为触发的代码,这里我就抛砖引玉,介绍我写的mod。目前这个mod的任务很简单,但还没人做,就是如何让小人在着火的时候不解除征召,依然像没着火一样战斗,并且什么时候灭火完全由玩家决定。本mod教程仅涉及C#补丁,xml相关的我将不再论述。下面是截图(我录制的gif在被吃了,也许明天能放出来)






IP属地:广东1楼2025-12-26 22:00回复
    什么,你问我mod呢?我都把mod怎么制作交给你了你为什么不能自己做一个呢?
    咕咕咕,其实只是我还没想好要怎么完成它,也没有在正常游玩里测试过,感觉着火没有任何影响还不如流血,是不是应该加个约束条件如近战10级以上或着火十次以上才可以无视原版逻辑,或只是原版逻辑改进一下


    IP属地:广东2楼2025-12-26 22:06
    回复
      2026-01-21 16:53:28
      广告
      不感兴趣
      开通SVIP免广告
      目前关于火焰的两个补丁我看吧友主要提到是Stop drop and roll和yayo's animation这两个,这两个补丁对于着火后的行为都没有完全的控制,你也不想和机械族刚枪的时候不小心被点火然后直接被biu死吧。而且更难绷的是着火后小人会往敌人方向跑,连草履虫都知道趋利避害。下面介绍的教程就是为了修改这些问题。如果已经有相关的mod了大家就当普通的教程看吧


      IP属地:广东3楼2025-12-26 22:16
      收起回复
        首先是通过VS安装,吧里sincerajin大佬已经给出了使用rider进行开发的步骤。我将使用vs2022进行开发。
        首先按装CSharp(C#)

        选择.net 项目和9.0或更高版本



        然后使用negut包管理工具下载Krafs.Rimworld.Ref和Harmony Lib(你不用就可以不下)
        然后修改配置,主要修改语言版本和目标版本,下面是修改前,截图时我忘记加Harmony了
        修改后

        然后理论上我们就可以自由的编辑我们的代码了


        IP属地:广东4楼2025-12-26 22:31
        回复
          忘记说了,修改配置要在项目的根目录下找 项目名.csproj用文本编辑器打开修改


          IP属地:广东5楼2025-12-26 22:33
          回复
            首先讲一下Harmony的使用,Harmony主要是通过CSharp的特性如反射等进行补丁。基本的使用可以去看B站Azzy_H大佬录制的harmony patch制作。我简单复述一下,首先要有一个静态类在启动时运行补丁,这玩意几乎是写死的,直接抄就好

            每一个类都要指定你这个补丁的补的是哪个类,以及类里面的方法是什么,下面就是补丁补的是FloatMenuOptionProvider_DraftedTend类的GetOptionsFor,后面用字符串的原因是,因为有的方法是私有的,直接写nameof(类.方法名)会报错


            再然后是Prefix,前补丁,这里的逻辑是我复制某个后补丁的,一般不这样写,前补丁有两件事要注意。第一件事是该补丁的返回值决定后面的函数是否运行,返回True则运行,False则不运行,包括其他的前补丁和方法本体。而第二件事就是由于上面提到返回False会使得后面的方法不运行,则会有更高的概率冲突或者出bug,所以使用要慎重。

            后补丁Postfix我用的更多,主要就是用来修改返回值,好处是影响小,出bug和冲突的概率较低。坏处是用这个就必须原方法运行一遍,一方面原方法的影响可能不止返回值,另一方面如果只是修改其中部分逻辑,需要运行一遍原方法,那么最坏就相当于原方法跑了两边,这可能会造成性能上的影响

            还有就是注入值,写在补丁的参数中,可以访问
            主要是两下划线的值
            __instance,获取类的实例,想于python的self
            __args 获取原方法的参数,官网上好像说用object[]装箱,拆箱使用。好像可以直接写变量名获取参数,但我老是报错,不知道为什么
            __result 返回值,前补丁中这个是默认值,直接var __result =你想返回的值就可以修改原函数的返回值。
            三下划线___field,用来访问私有属性(Csharp这玩意好像叫字段,但我还是习惯叫属性),比如有个私有属性叫pawn,你直接访问编译器就会报错,所以要在参数中写___pawn来间接访问


            IP属地:广东6楼2025-12-26 23:08
            收起回复
              修正一下上面错误
              Postfix最坏就相当于原方法跑了两遍
              __instance,获取类的实例,相当于python的self
              另外在说一下迭代器,也就是IEnumerable<Class>,这玩意比较特殊,好像学校里也没怎么介绍。IEnumerable<Class>返回的是迭代器,这个迭代器就是返回值,它像一口井,你每调用一次就返回一次值,就像是每次打一小桶水而非一大桶水,用这个实现了函数的分端调用。那对Harmony有什么影响呢?主要是后补丁,由它返回的是一个迭代器,形成这个迭代器的函数就还没有执行,所以你在调用这个返回值之前的处理相当于前补丁,在其中调用yield return IEnumerable<Class>相当于分段运行这个函数,运行完了就相当于正常的后补丁,当然这样返回值就是每次yield return IEnumerable<Class>的返回值,而非__result


              IP属地:广东7楼2025-12-26 23:22
              收起回复
                今天先简单说一下Stop drop and roll,明天我再说我的Patch设计时的流程和解决方案。由于是别人的mod,我不会放上源代码。
                Stop drop and roll我在前面提到过,他的逻辑很简单,Pawn会乱跑找水?那就前补丁禁了JobGiver_JumpInWater.TryGiveJob()
                Pawn不会自己灭火?那就直接前补丁返回JobDefOf.ExtinguishSelf。这里JobGiver_ExtinguishSelf我们可以看见泰南在这里放了一个随机数作为判断条件,所以即使运行到这里也不一定会灭火,大概是只有Pawn
                的概率小人会想起来灭火

                除此之外还有通过前补丁禁止FireUtility.TryAttachFire()来防止Pawn着火
                首先看到这里你应该注意到了pawn.jobs,点进去看后发现它有一个任务队列,其次这个任务队列可以使用StopAll()这个方法来全部删除。
                其次这个方法倒是提醒我Pawn有个record系统,可以把这个系统的数值作为条件,比如TimesOnFire被火烧的次数大于100才能征召时无视火焰


                IP属地:广东8楼2025-12-26 23:48
                收起回复
                  2026-01-21 16:47:28
                  广告
                  不感兴趣
                  开通SVIP免广告
                  但Stop drop and roll没有从代码上解决Pawn乱跑的问题,它只是是用百分比百强制灭火替代了泰南的小巧思,并掩盖了Pawn乱跑的行为。从效果上看,和我们希望被点燃了依然可以正常指挥Pawn还是不一样的。至于为什么会掩盖乱跑的行为请看后面分析。


                  IP属地:广东10楼2025-12-26 23:59
                  回复
                    既然Stop drop and roll通过禁止某几个行为从而解决了小人乱跑的问题,那么理论上我们也可以和他一样,通过禁止Pawn在着火时和普通征召时不同的行为,从而实现实际Pawn不论是否着火行为都相同的目的。
                    首先我们先将JobGiver_JumpInWater给禁了,然后进游戏看看,会发现此时小人的行为提示为闲逛中

                    再在xml里面搜索,两个里大概率是GotoWander对应的 闲逛中



                    然后就可以搜索JobDef.GotoWander找到JobGiver_Wander.TryGiveJob()给禁了.


                    IP属地:广东11楼2025-12-27 10:36
                    回复
                      理论上应该就已经解决了
                      但在禁了之后进入游戏,点燃Pawn,我们会惊奇的发现Pawn无法移动或做出任何动作了,仔细观察会发现下面显示观察目标,并在下面有一行走动中,说明Job阻塞了,真奇怪,明明我们没有改变任务的优先级,但为什么任务会阻塞呢?

                      这时我们就想到了上面在FireUtility.TryAttachFire()中的StopAll()方法,调用这个方法清空Job,但显然这个方法不太好,兼容性不高,所以我们最好另找寻方法


                      IP属地:广东12楼2025-12-27 10:44
                      回复
                        这里再说一下JobDriver的创建,在Pawn.Pawn_JobTracker.StartJob()中创建并保存
                        同上步骤, 观察目标 对应的是JobDefOf.Wait_Combat,最后找到JobGiver_Orders,但是我们不能禁了它的TryGiveJob(),因为在未着火的征召时期也可以看见 观察目标 这句话.而且其实禁了会报错,有泰南的一个托底的错误(大概就是这个Job行为不应该被调用,但是被调用了)

                        所以我们往下看,既然Giver不行那就看看Driver可不可以解决这个问题
                        一眼就看见了expiryInterval这个单词,设置的话应该会让Job定期过期,应该可以解决卡死的问题.

                        后补丁设置这个值为30,阻塞消失了!

                        此贴完结,撒花


                        IP属地:广东13楼2025-12-27 11:08
                        回复
                          开玩笑的,并未完结
                          后续测试发现泰南瞄准好像是使用了JobDefOf.Wait_Combat,设置定值时间会导致射击瞄准时间重置,楼主测试时用速射机枪一梭子都射不出去,所以设定expiryInterval不是个好方法.同时由于可以移动,楼主还发现在瞄准射击特定的物体后无法移动,同样出现了Job队列阻塞.

                          说明不是单个Job导致的问题,要往上看,前面的贴子我已经详细的介绍过思维树,这里我就不介绍了


                          IP属地:广东14楼2025-12-27 11:14
                          回复
                            其实楼主首先搜了isBurning()这个函数的使用,尽管泰南在设置属性时用了OnFire这个词,但是提供了火焰相关的isBurning(),我自然而然就认为判断是否着火会用isBurning().搜这个方法的被使用没发现什么有用的东西
                            然后去搜思考树,Fire和Burning,一眼看见BurningResponse子树

                            分析一下,首先时判断是否Pawn燃烧,如果燃烧则检测是否会使用工具(估计排除动物,话说回来,九莲我记得好像是算动物,那她是不是就不会找水),没有找到水则尝试灭火,但上面提到过一定灭,没有灭火就随即乱跑.这里的Deadly好像是指区域的危险度,Deadly可能是怕殖民者跑太远征召回来时间太长设定的
                            理论上禁了或者换了JobGiver_RunRandom就可以防止乱跑,但是我选择尝试直接在征召时将ThinkNode_ConditionalBurning永远返回False,让Pawn在征召时无视火焰



                            IP属地:广东15楼2025-12-27 11:32
                            收起回复
                              2026-01-21 16:41:28
                              广告
                              不感兴趣
                              开通SVIP免广告
                              这里再提一嘴Debug.我知道大伙都会直接复制Log去给ai,要ai分析出了什么问题.但是如果你的问题非常广泛,比如涉及很多行为,又比如像上面样你根本不知道哪里有问题,那你可能要打很多log给ai分析.但ai的上下文是有限的,所以要尽量压缩Log的长度.我建议的是没有必要就将log放到一个变量里面,最后再打印.
                              这是改之前的log,打出来有有1MB(1024KB)文本txt大小,ai根本读不完

                              下面是改之后的方法,打出来只有30KB的大小

                              这主要是因为每一次打印都会同时打印调用的函数链,在相同的函数里打多就会重复占据空间
                              普通白字是Log.Message()
                              黄子是Log.Warning()
                              红字好像是Log.Error()
                              三个函数打印mod信息


                              IP属地:广东16楼2025-12-27 11:46
                              回复