废话

之前就有想尝试过mod制作,结果在网上看了N多教程还是不知道从何下手。后来想到解铃还须系铃人,无奈去看英文的文档,觉得会详细一些。
所以本篇参考了 wiki上的一篇教程。但是即便是非常详细的教程,对于第一次尝试的人来说还是有很多容易踩坑的地方,所以本篇记录踩坑过程。

核心内容

1 认识mod结构

1.1 mod结构

  • Mod根目录
    • Abount(存放mod的简介和mod的标题图片)
    • Defs(数据定义)
    • Assemblies(mod代码打包后的库文件)
    • Languages(翻译的核心目录)
    • Textures(纹理,mod中对象的素材)
    • Sounds(声音)
    • Patches(针对其他mod的补丁)
    • Source(mod的源码,一般没有作者好心给出源码,不过我们可以反编译就是了,很少有加密的mod)

2 认识开发工具

2.1 开发工具

visual studio:用于编写mod核心代码
vscode:用于编辑xml数据定义
dsnpy:用于反编译原游戏或其他mod的源代码来学习新姿势
下载地址请直接百度, 要学会善用搜索引擎

本篇使用以上落后ide开发,目前有更好用的开发工具jetbrain rider, 体积更轻便,运行更快,并且可以完成上面3个工具的功能,你有什么理由拒绝最新的工具呢?后面会找时间写一篇教程介绍如何使用rider开发mod

2.2 visual studio开发环境

百度下载,安装时勾选.net桌面开发,否则无法创建C#程序集,也就是无法打包dll出来。剩下的一路下一步。
在这里插入图片描述

2.3 程序集构建

我们在vs上新建一个类库(.net framework),并把名字改成命名空间SR
wiki的教程提醒我们使用.net framework 3.5的框架版本,我尝试之后发现原游戏的版本已经高于3.5,于是我反编译其他mod看了一下其他作者使用的版本,选择了相同4.6.1
在这里插入图片描述

2.4 修改编译输出路径

在properties中点生成,把dll输出路径改到我们的mod路径E:\steam\steamapps\common\RimWorld\Mods\TestGun\Assemblies里
在这里插入图片描述
接下来点击高级,设置内部编译器错误报告为无,这样做是为了避免我们写出一些小bug导致整个游戏崩溃。

2.5 添加引用库

点击引用,选择原游戏下RimWorld\RimWorldWin64_Data\Managed路径下UnityEngine,UnityEngine.CoreModule和Assembly-CSharp.dll三个库文件。

右键选择引用文件,在属性中取消“复制到本地”,否则编译时会多复制一堆引用库文件,而这些文件已经被原游戏添加过一次了。

下图有部分内容是错的,实际不需要引用这么多库文件,只需要上面提到的3个,以及自带的部分。
在这里插入图片描述

2.6 编写代码及编译

代码编写过程略。
右键方案点击build即可打包编译。

3 案例

此部分暂时只做了解,能看懂多少算多少。

3.1 mod结构构建

为了区分哪些变量是游戏中的关键词,我更换了一下命名,我们的mod叫做TestGun(测试枪).

在这里插入图片描述
按照上面的教程,构建一套mod文件结构。并丢到rimworld的mod文件夹下,在这个文件夹下的mod会被游戏识别,并且可以随时发布。
在这里插入图片描述

3.2 修改简介

然后进入About目录,拖进去一张图片改名叫Preview.png,再创建一个txt文件,然后改成About.xml,如果你看不到.xml的后缀,在上方点击查看–选项,取消隐藏已知文件类型的扩展名
在这里插入图片描述
在这里插入图片描述
然后打开About.xml
复制以下代码,分别是这个mod的名字,作者,版本和描述

<?xml version="1.0" encoding="utf-8"?>
<ModMetaData>
  <name>TestGun</name>
  <author>Shadowrabbit</author>
  <supportedVersions>
    <li>1.1</li>
  </supportedVersions>
  <description>测试创造mod物品
  </description>
</ModMetaData>

完成了这一步,就可以在游戏中看到我们的mod
在这里插入图片描述这里非常推荐一种mod命名规则:
[{作者名缩写}{支持的版本号}]{mod名称}
例如:[SR1.2]Test Gun
这种规范可以让人一眼了解这个mod是否适用于当前版本

3.3 创建数据定义

接下来在Defs那个文件夹内创建一个ThingDefs的文件夹,然后在ThingDefs内创建一个xml文件叫做RangedWeapon_TestGun
在这里插入图片描述
这是原游戏的命名规则,不遵守也可以正常运行,但我还是推荐认真一点命名。
接下来在xml中更改一下格式。

<?xml version="1.0" encoding="utf-8"?>
<Defs>
  
</Defs>

然后我们要去“抄”一下原游戏中某个枪的xml数据,这样我们才能知道默认都有哪些数据能修改。
我们在vscode中添加一个目录E:\steam\steamapps\common\RimWorld\Data\Core,这是我的游戏目录仅供参考,要填自己电脑安装的目录
在这里插入图片描述
core是原游戏的核心,可以发现它的结构和我们mod的定义是完全一样的。打开Defs可以找到所有的物品数据定义,但是太多了反而不知道从哪看起。我们在core中搜索Revolver(手枪),把手枪的数据复制过来
在这里插入图片描述
在这里插入图片描述
为了方便观察,我加了一些翻译注释

<?xml version="1.0" encoding="utf-8"?>
<!-- 子弹的数据 -->
<Defs>
<!-- 继承了哪个物品 -->
  <ThingDef ParentName="BaseBullet">
    <!-- 代码中物品的名字 -->
    <defName>Bullet_Revolver</defName>
    <!-- 游戏中物品显示的名字 -->
    <label>revolver bullet</label>
    <graphicData>
      <!-- 使用的纹理贴图 -->
      <texPath>Things/Projectile/Bullet_Small</texPath>
      <!-- 使用哪个图形类的代码 -->
      <graphicClass>Graphic_Single</graphicClass>
    </graphicData>
    <projectile>
    <!-- 子弹类型 -->
      <damageDef>Bullet</damageDef>
      <!-- 子弹基础伤害 -->
      <damageAmountBase>12</damageAmountBase>
      <!-- 取消射击的反应速度 -->
      <stoppingPower>1</stoppingPower>
      <!-- 子弹的速度 -->
      <speed>55</speed>
    </projectile>
  </ThingDef>
  <!-- 枪的数据 -->
  <ThingDef ParentName="BaseHumanMakeableGun">
    <defName>Gun_Revolver</defName>
    <label>revolver</label>
    <!-- 描述 -->
    <description>An ancient pattern double-action revolver. It's not very powerful, but has a decent range for a pistol and is quick on the draw.</description>
    <graphicData>
      <texPath>Things/Item/Equipment/WeaponRanged/Revolver</texPath>
      <graphicClass>Graphic_Single</graphicClass>
    </graphicData>
    <!-- UI缩放 -->
    <uiIconScale>1.4</uiIconScale>
    <soundInteract>Interact_Revolver</soundInteract>
    <!-- 个人猜测是艺术系统的标签 -->
    <thingSetMakerTags><li>RewardStandardQualitySuper</li></thingSetMakerTags>
    <!-- 基础属性 -->
    <statBases>
    <!-- 制作的工作量 -->
      <WorkToMake>4000</WorkToMake>
      <!-- 物品大小 -->
      <Mass>1.4</Mass>
      <!-- 贴脸的命中率 -->
      <AccuracyTouch>0.80</AccuracyTouch>
      <!-- 短距离命中率 -->
      <AccuracyShort>0.75</AccuracyShort>
      <!-- 中距离命中率 -->
      <AccuracyMedium>0.45</AccuracyMedium>
      <!-- 远距离命中率 -->
      <AccuracyLong>0.35</AccuracyLong>
      <RangedWeapon_Cooldown>1.6</RangedWeapon_Cooldown>
    </statBases>
    <!-- 武器标签 -->
    <weaponTags>
      <li>SimpleGun</li>
      <li>Revolver</li>
    </weaponTags>
    <!-- 消耗列表 -->
    <costList>
      <Steel>30</Steel>
      <ComponentIndustrial>2</ComponentIndustrial>
    </costList>
    <recipeMaker>
    <!-- 技能需求 -->
      <skillRequirements>
        <Crafting>3</Crafting>
      </skillRequirements>
    </recipeMaker>
    <!-- 动作 -->
    <verbs>
      <li>
      <!-- 射击 -->
        <verbClass>Verb_Shoot</verbClass>
        <!-- 是否有标准命令 -->
        <hasStandardCommand>true</hasStandardCommand>
        <!-- 子弹类型 -->
        <defaultProjectile>Bullet_Revolver</defaultProjectile>
        <!-- 预热时间 -->
        <warmupTime>0.3</warmupTime>
        <!-- 射击距离 -->
        <range>25.9</range>
        <!-- 射击时使用的音效 -->
        <soundCast>Shot_Revolver</soundCast>
        <!-- 中弹时的音效 -->
        <soundCastTail>GunTail_Light</soundCastTail>
        <!-- 枪口光效的缩放 -->
        <muzzleFlashScale>9</muzzleFlashScale>
      </li>
    </verbs>
    <tools>
      <li>
        <label>grip</label>
        <capacities>
          <li>Blunt</li>
        </capacities>
        <power>9</power>
        <cooldownTime>2</cooldownTime>
      </li>
      <li>
        <label>barrel</label>
        <capacities>
          <li>Blunt</li>
          <li>Poke</li>
        </capacities>
        <power>9</power>
        <cooldownTime>2</cooldownTime>
      </li>
    </tools>
  </ThingDef>
</Defs>

到这里为止就可以使用xml修改枪的各种参数,以及发射什么样的子弹(需要去core里再查查都有什么子弹,然后填在defaultProjectile里),还可以更换枪的贴图和音效。更多的物品参数需要查查wiki,然后翻译+自己理解,还有多看看core中的xml代码。
我们本次要制作的是带有瘟疫效果的枪,我们需要创建一种新的子弹,所以我们要在子弹的def里加几个新参数与C#代码交互
在子弹的xml数据中添加

<ThingDef Class="SR.ThingDef_TestBullet" ParentName="BaseBullet">

和三个自定义的新属性

	<!-- 触发瘟疫的概率 -->
    <AddHediffChance>0.5</AddHediffChance>
    <!-- Hediff这个词是游戏里独一无二的名字,字典里查不到,他的含义是某个部位上的状态,比如感染,仿生部位,瘟疫Plague是原版游戏其中的一种 -->
    <HediffToAdd>Plague</HediffToAdd>
    <!-- 该物品在C#中使用的逻辑类 -->
    <thingClass>SR.Projectile_PlagueBullet</thingClass>

然后修改子弹的命名避免和原版一样,修改后如下

<?xml version="1.0" encoding="utf-8"?>
<!-- 子弹的数据 -->
<Defs>
<!--该物品在C#中使用的数据类 继承了哪个物品 -->
  <ThingDef Class="SR.ThingDef_TestBullet" ParentName="BaseBullet">
    <!-- 代码中物品的名字 -->
    <defName>Bullet_TestBullet</defName>
    <!-- 游戏中物品显示的名字 -->
    <label>测试子弹</label>
    <graphicData>
      <!-- 使用的纹理贴图 -->
      <texPath>Things/Projectile/Bullet_Small</texPath>
      <!-- 使用哪个图形类的代码 -->
      <graphicClass>Graphic_Single</graphicClass>
    </graphicData>
    <projectile>
    <!-- 子弹类型 -->
      <damageDef>Bullet</damageDef>
      <!-- 子弹基础伤害 -->
      <damageAmountBase>12</damageAmountBase>
      <!-- 取消射击的反应速度 -->
      <stoppingPower>1</stoppingPower>
      <!-- 子弹的速度 -->
      <speed>55</speed>
    </projectile>
    <!-- 触发瘟疫的概率 -->
    <addHediffChance>0.5</addHediffChance>
    <!-- Hediff这个词是游戏里独一无二的名字,字典里查不到,他的含义是某个部位上的状态,比如感染,仿生部位,瘟疫Plague是原版游戏其中的一种,我们定义的这个变量意义是储存附加给中弹生物的hediff类型 -->
    <hediffToAdd>Plague</hediffToAdd>
    <!-- 该物品在C#中使用的逻辑类 -->
    <thingClass>SR.Projectile_TestBullet</thingClass>
  </ThingDef>
  <!-- 枪的数据 -->
  <ThingDef ParentName="BaseHumanMakeableGun">
    <defName>SR_Gun_TestGun</defName>
    <label>影兔的测试枪</label>
    <!-- 描述 -->
    <description>影兔测试瘟疫效果的枪</description>
    <graphicData>
      <texPath>Things/Item/Equipment/WeaponRanged/Revolver</texPath>
      <graphicClass>Graphic_Single</graphicClass>
    </graphicData>
    <!-- UI缩放 -->
    <uiIconScale>1.4</uiIconScale>
    <!-- 交互的音效 -->
    <soundInteract>Interact_Revolver</soundInteract>
    <!-- 个人猜测是艺术系统的标签 -->
    <thingSetMakerTags><li>RewardStandardQualitySuper</li></thingSetMakerTags>
    <!-- 基础属性 -->
    <statBases>
    <!-- 制作的工作量 -->
      <WorkToMake>4000</WorkToMake>
      <!-- 物品大小 -->
      <Mass>1.4</Mass>
      <!-- 贴脸的命中率 -->
      <AccuracyTouch>0.80</AccuracyTouch>
      <!-- 短距离命中率 -->
      <AccuracyShort>0.75</AccuracyShort>
      <!-- 中距离命中率 -->
      <AccuracyMedium>0.45</AccuracyMedium>
      <!-- 远距离命中率 -->
      <AccuracyLong>0.35</AccuracyLong>
      <RangedWeapon_Cooldown>1.6</RangedWeapon_Cooldown>
    </statBases>
    <!-- 武器标签 -->
    <weaponTags>
      <li>SimpleGun</li>
      <li>Revolver</li>
    </weaponTags>
    <!-- 消耗列表 -->
    <costList>
      <Steel>30</Steel>
      <ComponentIndustrial>2</ComponentIndustrial>
    </costList>
    <recipeMaker>
    <!-- 技能需求 -->
      <skillRequirements>
        <Crafting>3</Crafting>
      </skillRequirements>
    </recipeMaker>
    <!-- 动作 -->
    <verbs>
      <li>
      <!-- 射击 -->
        <verbClass>Verb_Shoot</verbClass>
        <!-- 是否有标准命令 -->
        <hasStandardCommand>true</hasStandardCommand>
        <!-- 子弹类型 -->
        <defaultProjectile>Bullet_TestBullet</defaultProjectile>
        <!-- 预热时间 -->
        <warmupTime>0.3</warmupTime>
        <!-- 射击距离 -->
        <range>25.9</range>
        <!-- 射击时使用的音效 -->
        <soundCast>Shot_Revolver</soundCast>
        <!-- 中弹时的音效 -->
        <soundCastTail>GunTail_Light</soundCastTail>
        <!-- 枪口光效的缩放 -->
        <muzzleFlashScale>9</muzzleFlashScale>
      </li>
    </verbs>
    <tools>
      <li>
        <label>grip</label>
        <capacities>
          <li>Blunt</li>
        </capacities>
        <power>9</power>
        <cooldownTime>2</cooldownTime>
      </li>
      <li>
        <label>barrel</label>
        <capacities>
          <li>Blunt</li>
          <li>Poke</li>
        </capacities>
        <power>9</power>
        <cooldownTime>2</cooldownTime>
      </li>
    </tools>
  </ThingDef>
</Defs>

为了避免与其他mod命名冲突,我使用SR作为代码的命名空间,SR.XX表示C#类,SR_XX表示xml数据,这也是原游戏默认的命名规范,有两处SR.XX的数据是与C#进行交互的,命名必须一致。
至此xml部分就结束了。

3.4 编写C#代码

using RimWorld;
using Verse;
namespace SR
{
    public class Projectile_TestBullet:Bullet
    {

    }
}

using RimWorld;
using Verse;

namespace SR
{
    public class ThingDef_TestBullet:ThingDef
    {
    }
}

我们把新添加的字段写入数据类中,名字要与xml中的一致,不需要给新变量赋默认值,因为会被xml中的数据覆盖,所以addHediffChance运行时为我们在xml中设置的0.5

using RimWorld;
using Verse;

namespace SR
{
    public class ThingDef_TestBullet:ThingDef
    {
        public float addHediffChance; //默认值会被xml覆盖
        public HediffDef hediffToAdd;
    }
}

题外话,说一个wiki上关于老版本的问题,如果HediffDef类型的数据有默认值的话,在1.0版本之后是会报错的,因为这个时候HediffDefOf还没有初始化。不过假如你就是想留默认值也有解决方法,有一个回调函数,我们在回调的时候重新赋值即可。

public override void ResolveReferences()
{
    base.ResolveReferences();
    hediffToAdd = HediffDefOf.Plague;
}

之后是逻辑类,先定义一个变量存它的数据

#region data
public ThingDef_TestBullet ThingDef_TestBullet
{
    get
    {
        //底层通过名字读取了我们定义的ThingDef_TestBullet这个xml格式的新数据,并存放到了this.def中,我们将this.def拆箱拿到我们定义好的ThingDef_TestBullet格式数据
        return this.def as ThingDef_TestBullet
    }
}
#endregion

我们需要知道原版子弹击中目标会执行什么流程,打开dnspy反编译Assembly-CSharp.dll,原游戏所有代码都在Assembly-CSharp.dll中,然后搜索bullet子弹,可以看到子弹的源码里只有一个可以重载的方法impact,根据英文的意思知道他是中弹后执行的逻辑,里面写了什么我们暂时不需要关心,我们在测试子弹的类中继承子弹这个类,然后重写impact方法,在原逻辑执行完毕后添加我们关于瘟疫的设定
在这里插入图片描述
关于添加瘟疫的设定,wiki上给出了代码,我详细解读一下贴出来

using RimWorld;
using Verse;
namespace SR
{
    public class Projectile_TestBullet : Bullet
    {
        #region data
        public ThingDef_TestBullet ThingDef_TestBullet
        {
            get
            {
                //底层通过名字读取了我们定义的ThingDef_TestBullet这个xml格式的新数据,并存放到了this.def中,我们将this.def拆箱拿到我们定义好的ThingDef_TestBullet格式数据
                return this.def as ThingDef_TestBullet;
            }
        }
        #endregion
        protected override void Impact(Thing hitThing)
        {
            //子弹的影响,底层实现了伤害 击杀之类的方法,感兴趣的话可以用dnspy反编译Assembly-Csharp.dll研究里面到底写了什么
            base.Impact(hitThing);
            //绝大多数mod报错都是因为没判断好非空,写注释和判断非空是好习惯
            //大佬在这里用了一个语法糖hitThing is Pawn hitPawn
            //如果hitThing可以被拆箱为Pawn的话 这个值返回true并且会声明一个变量hitPawn=hitThing as Pawn
            //否则返回false hitPawn是null
            if (ThingDef_TestBullet != null && hitThing != null && hitThing is Pawn hitPawn)
            {
                var rand = Rand.Value; //这个方法封装了一个返回0%-100%随机数的函数
                //触发瘟疫
                if (rand <= ThingDef_TestBullet.addHediffChance)
                {
                    //在屏幕左上角显示提示,translate方法用于翻译不同语言之后再说,MessageTypeDefOf要设置一种事件
                    Messages.Message("{0}使用测试枪导致{1}感染瘟疫".Translate(this.launcher.Label,hitPawn.Label),MessageTypeDefOf.NeutralEvent);
                    //判断一下目标是否已经触发了瘟疫效果
                    var plagueOnPawn = hitPawn.health?.hediffSet?.GetFirstHediffOfDef(ThingDef_TestBullet.hediffToAdd);
                    //我们为本次触发的瘟疫随机生成一个严重程度
                    var randomSeverity = Rand.Range(0.15f, 0.30f);
                    //已经触发瘟疫
                    if (plagueOnPawn != null)
                    {
                        //严重程度叠加,超过100%会即死
                        plagueOnPawn.Severity += randomSeverity;
                    }
                    else
                    {
                        //我们调用HediffMaker.MakeHediff生成一个新的hediff状态,类型就是我们之前设置过的HediffDefOf.Plague瘟疫类型
                        Hediff hediff = HediffMaker.MakeHediff(ThingDef_TestBullet.hediffToAdd, hitPawn);
                        //设置这个状态的严重程度
                        hediff.Severity = randomSeverity;
                        //把状态添加到被击中的目标身上
                        hitPawn.health.AddHediff(hediff);
                    }
                }
                //本次没有触发
                else
                {
                    //这个方法可以在某个位置(这里是被击中目标的身旁)弹出一小行字,比如未击中,击中头部之类的,也是可以
                    MoteMaker.ThrowText(hitThing.PositionHeld.ToVector3(), hitThing.MapHeld, "{0}未触发瘟疫".Translate(hitPawn.Label), 12f);
                }
            }
        }
    }
}

至此新武器的mod就制作完毕了,我们选择打包生成新的dll,根据之前设置的目录,会打包在Assemblies中。

3.5 测试

很遗憾我们的新武器虽然已经有了数据,但是没有任何方法可以在游戏中自然生成,我们需要借助开发者模式来测试我们的新枪支,首先打开开发者模式(记得加载我们的mod),随便建个新地图,然后在上方选择open debug actions menu
在这里插入图片描述
在里面找到spawn weapon – SR_Gun_TestGun,然后把它丢在地上,让我们的角色捡起来,然后对着自己的小人开一枪试试,刚好触发了瘟疫效果
在这里插入图片描述
未触发的log也正常显示
在这里插入图片描述
叠加到100%瘟疫会直接死亡,也是正常的。至此新武器的制作就完成了。

3.6 本地化

本地化就是指翻译成各种语言,我们把之前message中的内容用一个变量代替,让这个变量在各种语言下显示不同的文字就可以了。

Messages.Message("{0}使用测试枪导致{1}感染瘟疫".Translate(this.launcher.Label,hitPawn.Label),MessageTypeDefOf.NeutralEvent);

我们更改为

Messages.Message("SR_Message_TestBullet_Success".Translate(this.launcher.Label,hitPawn.Label),MessageTypeDefOf.NeutralEvent);

之后在Languages中创建ChineseSimplified目录,然后在ChineseSimplified目录下创建Keyed目录,再在Keyed目录下创建一个xml文件,我们命名为SR_TestGun_Keys.xml(名称可以随便取,但还是遵循规范).复制默认的语言数据结构

<?xml version="1.0" encoding="utf-8" ?>
<LanguageData>
</LanguageData>

之后添加两个新的key,对应我们上一步设置的key名字

<?xml version="1.0" encoding="utf-8" ?>
<LanguageData>
  <SR_Message_TestBullet_Success></SR_Message_TestBullet_Success>
  <SR_Mote_TestBullet_Fail></SR_Mote_TestBullet_Fail>
</LanguageData>

再把中文的翻译写进去

<?xml version="1.0" encoding="utf-8" ?>
<LanguageData>
  <SR_Message_TestBullet_Success>{0}使用测试枪导致{1}感染瘟疫</SR_Message_TestBullet_Success>
  <SR_Mote_TestBullet_Fail>{0}未触发瘟疫</SR_Mote_TestBullet_Fail>
</LanguageData>

至此中文版本就完成了,如果要加入英文版的话,就在Luaguages下创建English目录等等,然后创建相同的文件,在值里填入英文对应的文本即可,游戏会根据用户选择的语言自动选择语言数据加载。
源码下载

如果这篇文章对你有帮助,点赞收藏支持一下呗!

Logo

开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!

更多推荐