Skip to content

物品与流体

“万事开头难”

Mindustry的游戏内容大致可以划分为若干个板块,从作为材料的物品液体,到进行加工的工厂,再到消耗产品的功能性方块炮塔,以及用材料生产的单位,这些基本的游戏要素构成了游戏的核心玩法。

要入门mod开发,自然从最简单的制作一个物品开始,然后制作一个同样基础的流体。

创建一个Item

在Mindustry中,物品被封装为一个类型mindustry.type.Item,而创建一个Item实际上就是创建一个该类型的对象:

java
new Item("tutorial-item", Color.red);
kotlin
Item("tutorial-item", Color.red)
javascript
const tutorialItem = extend(Item, "tutorial-item", Color.red, {})
json
// content/items.tutorial-item.json
{
  
}

其中,第二个参数传入的颜色会影响此物品在分类器中的显示色。Item还有一个不带颜色的构造方法,不过此构造方法仅供 Mindustry 内部使用。

只需要在Mod主类的loadContent()入口方法中创建这个Item对象,我们就能够在游戏中找到这个物品了:

java
public class TutorialMod extends Mod{
  @Override
  public void loadContent(){
    new Item("tutorial-item", Color.red);
  }
}
kotlin
class TutorialMod: Mod(){
  override fun loadContent(){
    Item("tutorial-item", Color.red)
  }
}

接着,打开游戏的核心数据库,你就能看到你的物品被添加到了“物品”这一类当中:

第一个物品

如你所见,物品此时还没有贴图,也没有名字,因此会显示为错误贴图(oh no)和一段内部名称作为名字,为了给这个物品提供贴图和名称,就需要做一些Java代码以外的工作了。

为物品赋予名称和描述

严厉声明

我们不能让物品在游戏内以内部名称显示,所以就需要给物品命名,这就需要将物品的名字写进mod的语言文件当中(即我们在第一章第三节 mod文件结构中所讲到的bundles目录中的bundle文件)。

物品的本地化名称,描述和细节文本分别被表示为语言文件中的几个固定格式的键值对:

  • item.[mod内部名称]-[物品名称].name:物品的本地化名称
  • item.[mod内部名称]-[物品名称].description:物品的描述文本
  • item.[mod内部名称]-[物品名称].details:物品的细节文本

其中mod内部名称填写你在mod.json中所写的name,而物品名称即在你创建物品对象时,在构造方法中写下的那个字符串。

例如,对于我们刚刚创建的那个物品,其名称为tutorial-item,我们例子中的演示mod内部名称为tutorial-mod,那么在bundle中的键值对键名就应当填写为tutorial-mod-tutorial-item,例如我们将如下信息填写到bundle_zh_CN.propertiesbundle.properties当中:

properties
item.tutorial-mod-tutorial-item.name = 演示物
item.tutorial-mod-tutorial-item.description = Hello World!(为什么在这里还要Hello World?)
item.tutorial-mod-tutorial-item.details = 你看不见我看不见我看不见我
properties
item.tutorial-mod-tutorial-item.name = Tutorial Item
item.tutorial-mod-tutorial-item.description = Hello World!(Why)
item.tutorial-mod-tutorial-item.details = Shhhhhh

打开这个物品的详细信息:

命名物品

实际模组开发中,写出贴合实际功能和原版风格的bundle是一门学问,有待深入研究。

为物品分配贴图

物品没有贴图当然也是不行的,这就需要我们在mod的sprites目录中给物品提供它的贴图。

这一步很简单,我们只需要为物品绘制一张贴图,并把这张图片命名为你构造方法中写下的那个字符串,然后将它按照第一章第三节 mod文件结构中所讲的那样放入到sprites目录中即可。

需要注意的一点是,提供给物品的贴图尺寸必须是32x32,大于这个尺寸的贴图将会导致物品在流速显示页面上显示错误。此外它必须是png格式,位深为4,是一张彩色的图片,如果不遵守可能会在稀奇古怪的设备上出现奇怪的问题。

我们将这样一张图片按先前创建物品时提供的名称,命名为tutorial-item.png,并放进sprites目录里:

与语言文件中的键名称不同,物品贴图命名不需要在命名前附加mod名称。

(贴图来自笔者已暂停开发的mod)

crystal_FEX.png

重新构建并进入游戏,就可以看到物品成功的被分配了贴图:

分配贴图的物品

物品的属性

正如你在详情页看到的那样,这个物品的所有基础属性都是0%,如果物品应该具有这些属性,那么就应该在创建物品时为他们设置这些值(其实不必在创建时,但是通常这样会更利于维护)。

物品中的属性和作用均如下所示,其中大部分属性都在一些工厂识别材料时使用:

  • explosiveness:爆炸性,这个值会影响物品在容器和传送带上的效果,如果爆炸性较高,容器和传送带被破坏时会引发爆炸,强度取决于易爆性大小,还能影响携带此物品的容器和单位死亡时的爆炸伤害。
  • flammability:燃烧性,会影响物品在容器和传送带上的效果,如果物品可燃性较高,那么火焰会引起容器燃烧。
  • radioactivity:放射性,这个值通常只用于筛选工厂消耗的材料,例如RTG发电机。
  • charge:带电性,同放射性,作为方块消耗的识别项,但还能影响携带此物品的容器和单位死亡时的产生的爆炸。
  • hardness:硬度,当有一个矿物地板被采掘生产这个物品时,决定此矿物地板的硬度,即影响哪些钻头可以采掘此物品,并影响钻探速度。
  • lowPriority:影响对应矿物地板的效果,该值影响钻头的采掘优先级,如果钻头覆盖了多种矿物,则会忽略掉这个值为true的地板。
  • buildable:虽然字面义上叫“能否建造方块”,但实际上控制的是能否进入设置incinerateNonBuildable(销毁不可建造物品)的核心,Erekir上的所有行星都设置了此项。
  • cost:当此物品参与方块的建造时,用于计算建造方块需要的时间,此值越大,消耗时间越长。
  • healthScaling:此物品在方块未设定默认生命值时,在计算方块生命值时作为额外生命值参加计算。

我们可以使用这样的语法来在创建物品时就地为它们分配属性:

java
new Item("tutorial-item"){{
  hardness = 3;
  explosiveness = 0f;
  flammability = 0f;
  radioactivity = 0.4f;
  cost = 1.25f;
}};
kotlin
Item("tutorial-item").apply{
  hardness = 3
  explosiveness = 0f
  flammability = 0f
  radioactivity = 0.4f
  cost = 1.25f
}

现在,再次看看它的详情:

分配属性

注意

上述的代码中javakotlin的程序实际上并不等价,在java的就地分配属性中其实创建了一个匿名类,即new Type(...){...}表达式,然后在匿名类中仅定义了一个初始化块{...}来完成的属性分配,从而形成了new Type(...){ {...} }这样的形式,而kotlin则是实际的就地分配属性。

这并不重要,但是如果你很在意这一份开销的话,也可以把java声明拆开写。

创建一个Liquid

在Mindustry中,流体被封装为Liquid。虽然叫“液体”,但这样的命名是源自v7前游戏没有原生的气体,而在v7中Anuke简单地把气体实现为不会产生水坑的液体,所以Liquid类的正确译名应当是流体。

java
new Liquid("tutorial-liquid", Color.blue);
kotlin
Liquid("tutorial-liquid", Color.blue)

分配贴图不再陈述。

properties
liquid.tutorial-mod-tutorial-liquid.name = 演示液体
liquid.tutorial-mod-tutorial-liquid.description = 流体不做任何处理默认是液体。
liquid.tutorial-mod-tutorial-liquid.details = 上善似水。水善利万物而有静,居众之所恶,故几于道矣。
properties
liquid.tutorial-mod-tutorial-liquid.name = Tutorial Liquid
liquid.tutorial-mod-tutorial-liquid.description = He who is not gas is liquid
liquid.tutorial-mod-tutorial-liquid.details = And God said, "Let there be an expanse between the waters to separate water from water."

流体的属性

1. 基础类型与外观

这些字段定义了流体的基本类别和视觉呈现。

  • public boolean gas = false;

    • 是否为气体。这是最基础的分类。
    • true: 该流体是气体。它不会在地面上形成液体水坑(puddles),通常会向上飘散或具有特殊行为。
    • false: 该流体是液体。它会在地面上形成水坑并流动。
  • public Color color;

    • 基础颜色。这是流体在管道中流动、或作为液体水坑存在时的默认颜色
  • public Color gasColor = Color.lightGray.cpy();

    • 气体颜色。当 gas = true 时,流体会使用这个颜色进行渲染,而不是 color。通常气体颜色比液体颜色更浅、更半透明。
  • public @Nullable Color barColor;

    • 状态条颜色可为空Nullable)。在游戏UI中(例如显示储液罐容量、单位液体库存的条状图)所使用的颜色。如果为 null,则很可能回退使用 color
  • public Color lightColor = Color.clear.cpy();

    • 发光颜色。用于绘制该流体发出的光效。
    • 特别注意: 这个颜色的 Alpha通道(透明度) 值决定了发光的亮度Color.clear 表示完全不发光。
  • public boolean hidden;

    • 是否隐藏。如果为 true,该流体将在大多数UI(如选择器、数据库)中被隐藏,可能用于一些开发者流体或特殊场景流体。

2. 物理与化学属性

这些字段模拟了流体的“真实”物化性质,并直接关联到游戏玩法。

  • public float flammability;

    • 易燃性(0-1)
    • 0: 完全不可燃。
    • > 0: 暴露在高温(如岩浆、激光)下有可能着火
    • >= 0.5: 非常易燃(例如油)。
  • public float temperature = 0.5f;

    • 基础温度(0-1)。一个相对的标度。
    • 0.5: 环境温度(例如水)。
    • < 0.5: 低温流体(例如冷冻液)。
    • > 0.5: 高温流体(例如熔融金属、岩浆)。高温流体可能点燃可燃物或伤害单位。
  • public float heatCapacity = 0.5f;

    • 热容量。表示流体储存热量能力的大小。
    • 值越高,作为冷却剂的效果越好(因为它能吸收更多热量而自身温度上升较慢)。注释提到水(0.4)是相当不错的冷却剂。
  • public float viscosity = 0.5f;

    • 粘度。表示流体的粘稠度,直接影响其流动速度
    • 0.5: 类似水的粘度(相对粘稠,流动速度中等)。
    • 1.0: 类似焦油的粘度(非常粘稠,流动极慢)。
  • public float explosiveness;

    • 爆炸性(0-1)。表示流体受热时发生爆炸的倾向和威力。
    • 0: 不爆炸。
    • > 0: 受热时可能爆炸。
    • 1: 像核弹一样爆炸(例如硝化甘油)。
  • public float boilPoint = 2f;

    • 沸点。流体蒸发的温度阈值。注意注释提到这不完全是现实中的沸腾。当流体温度 >= 此值时,会转变为气体状态并触发 vaporEffect

3. 游戏内交互与行为

这些字段控制流体如何与游戏世界中的其他元素互动。

  • public boolean blockReactive = true;

    • 是否与方块反应。如果为 true,该流体会与特定方块发生特殊反应,用于瘤液
  • public boolean coolant = true;

    • 是否可作为冷却剂。如果为 false,即使 heatCapacity 很高,也无法被放入需要冷却剂的设备(如散热器)中。
  • public boolean moveThroughBlocks = false;

    • 是否可穿透方块。如果为 true,该流体的水坑可以穿过方块流动(例如像水一样渗过墙壁),用于瘤液
  • public boolean incinerable = true;

    • 是否可被焚化。如果为 true,该流体可以在焚化炉(Incinerator) 中被销毁,用于瘤液
  • public boolean capPuddles = true;

    • 是否限制水坑大小。如果为 true,流体的水坑面积会有一个上限,防止单种流体无限蔓延。

有些字段没有列出,涉及到了后文才提到的内容。

整理并列表

本小节仅作建议与参考,只是提供一种接近原版的、相对工整的形式,项目结构的具体组织还须根据实际情况调整。

通常我们制作mod不会只声明一个物品,更不可能只创建物品,我们上文中直接将ItemLiquid创建在了Mod主类的loadContent()中了,但是实际工作中则不应该这么做,而应当将我们的物品,以及所有其他内容都分好类,然后放在不同的类/文件当中,再在loadContent()中调用加载函数以进行加载。

与此同时,我们还需要保存各个物品的变量,以方便在我们定义方块生产消耗等情况时引用这个物品。

这就引出了整理物品以及所有其他游戏内容的需要,通常来说,我们会将各个类型的内容按照一定的规则来进行分类,然后将它们集中的定义在多个类型中。

例如,我们创建一个类型(kotlin则是单例)ModItems来存储mod的所有物品,ModLiquids来存储mod的所有物品,然后在一个load()方法中进行统一创建:

java
public class ModItems{
  public static Item item1, 
      item2, 
      item3, 
      item4,
      //...
      itemN;
  
  public static void load(){
    item1 = new Item("item1"){{
      //...
    }};
    item2 = new Item("item2"){{
      //...
    }};
    item3 = new Item("item3"){{
      //...
    }};
    item4 = new Item("item4"){{
      //...
    }};
    //...
  }
}
kotlin
object ModItems{
  lateinit var item1
  lateinit var item2
  lateinit var item3
  lateinit var item4
  //...
  lateinit var itemN

  fun load() {
    item1 = Item("item1").apply {
      //...
    }
    item2 = Item("item2").apply {
      //...
    }
    item3 = Item("item3").apply {
      //...
    }
    item4 = Item("item4").apply {
      //...
    }
    //...
  }
}

ModLiquids类也类似。

接着在Mod主类的loadContent()方法中调用load()方法即可:

java
public class TutorialMod extends Mod{
  @Override
  public void loadContent(){
    ModItems.load();
    ModLiquids.load();
  }
}
kotlin
class TutorialMod: Mod(){
  override fun loadContent(){
    ModItems.load()
    ModLiquids.load()
  }
}

不只是物品和流体,我们之后会创建的所有类型的内容,都应当做好分类以便管理和维护。本教程将以此范式继续展开讲解。

思考题

试想一下如果我们不在loadContent()阶段创建物品会怎么样呢?另外我们今后还会在loadContent()阶段创建我们的工厂等,那么调用各类列表的load()的顺序应当如何确定呢?