Skip to content

工厂

游戏的灵魂

作为一个工厂+塔防+RTS游戏,Mindustry的第一大灵魂玩法当之无愧是“工厂”。工厂的作用可以概括为“转换”,即把输入的物品、流体、电力、热量等资源耗费一定时间转换成新的物品或流体。本节将创建一个通用工厂,设置其输入与输出,并介绍方块的共性,然后介绍工厂的一些子类型以及分离机等功能相似的方块。

创建一个GenericCrafter

像物品和流体一样,所有方块都被封装成了一个类型mindustry.world.Block子类(Subclass)。不同方块有着不同的功能,可以设置不同的属性,这就需要方块拥有不同的类型。因此,在创建一个方块的时候,需要根据需要的功能选择合适的类型。要创建一个通用工厂,需要的类型为mindustry.world.blocks.production.GenericCrafter

java
new GenericCrafter("tutorial-crafter");
kotlin
GenericCrafter("tutorial-crafter")

和上节一样,你可以选择简单地在主类的loadContent()里创建这个对象,也可以选择选择新建一个类来存放所有的工厂或所有的方块。

方块的共性

如果只是创建了这个对象,而不做任何设置,那么在核心数据库和建造栏中均不会见到这个方块,因为方块默认是隐藏的。所以,你需要设置一些属性来让方块“现形”。

对于一个方块来说,最基本的属性有以下几个:

  • health:建筑的生命值;
  • size:建筑的尺寸,也就是边长。注意,Mindustry原生不支持非正方形建筑;

还有四个属性也是基本属性,但设置它们并不推荐直接给对应字段赋值,而是应该使用requirements方法。这个方法有很多版本的重载,根据形参的名称和类型可以得知各个重载版本中参数的含义。

  • alwaysUnlock(在参数列表中叫unlocked):顾名思义,即为在战役模式中是否始终解锁;
  • category:方块所属的类别,原版有十大类别,分别对应建造栏中的十个图标,从上到下分别为turret(炮塔)、production(生产)、distribution(物流)、liquid(液体)、power(电力)、defense(防御)、crafting(制造)、units(单位)、effect(特殊)、logic(逻辑);
  • requirements(在参数列表叫stacks):建造方块所需的物品。参照原版可知,有一个工厂方法ItemStack.with专门用来生成一个ItemStack[]供这个方法使用。注意,建造时间是直接通过建造耗费物品量来计算的,也可以通过buildCostMultiplier间接控制,具体设置方法见于下方代码;
  • buildVisibility:建筑可视性,即为某一方块是否在建造栏或核心数据库中可见,默认为BuildVisibility.hidden(隐藏状态),需要设置成BuildVisibility.shown才能默认显示。其余的BuildVisibility会在几节之后提到。

如你所见,如果不设置requirements(),那么方块默认就是处于隐藏状态。所以,你需要使用任意一个requirements()方法来设置buildVisibility。例如,可以使用这样的代码:

java
new GenericCrafter("tutorial-crafter"){{
  health = 100f;
  size = 2;
  //使用此重载,Anuke会贴心地帮你设置成BuildVisibility.shown
  requirements(Category.crafting, ItemStack.with(Items.copper,50,Items.lead,50));
}};
kotlin
GenericCrafter("tutorial-crafter").apply{
  health = 100f
  size = 2
    //使用此重载,Anuke会贴心地帮你设置成BuildVisibility.shown
  requirements(Category.crafting, ItemStack.with(Items.copper,50,Items.lead,50))
}

从这里开始,你将会需要在代码当中 引用(Refer to) 原版的内容。原版的内容的引用大部分都存放在mindustry.content这个包下对应的类中,例如mindustry.content.Items存放了所有物品的引用,mindustry.content.Blocks存放了所有的方块的引用。在这些存放内容的类中,变量名起到标识的作用,通常与内容的内部名称或英文名是保持一致的,例如铜(Copper)是mindustry.content.Items.copper,双管(Duo)是mindustry.content.Blocks.duo。引用本模组的内容也是类似的,如果你的内容声明在loadContents方法中,你需要用一个变量把它的引用存起来,然后直接引用变量即可。如果你仿照原版的组织架构,那么引用时也和原版类似。关于引用其他模组的内容,详见本章最后一节。

值得注意的是,方块的建造时间并不是手动设置的,而是由建造需求决定的,计算公式为需求物品的cost总和再乘以buildCostMultiplier。如果你觉得某个方块的建造时间过长,但又不想更改物品的cost时,可以设置buildCostMultiplier为一个比1小的数来降低建造时间。 直接设置buildCost不会起效,因为计算buildCost在创建对象之后很久之后才会发生

方块也是物品、流体、电力的容器,这三种物质分别存储在建筑的物品槽(ItemModule)、流体槽(LiquidModule)和电力槽(PowerModule)中。你需要分别使用hasItems hasLiquids hasPower去标记建筑需要这三种槽。你还可以设置物品和流体的容量,分别为itemCapacity liquidCapacity

接下来同样需要给方块分配名称和贴图:

properties
block.tutorial-mod-tutorial-crafter.name = 演示工厂
block.tutorial-mod-tutorial-crafter.description = 工厂,也叫冶炼厂、压缩机、混合器、编织器、离心机、提取器、抽水机、培养器
block.tutorial-mod-tutorial-crafter.details = 工厂游戏没有工厂叫什么工厂游戏
properties
block.tutorial-mod-tutorial-crafter.name = Turorial Crafter
block.tutorial-mod-tutorial-crafter.description = Crafter may also be called smelter, compressor, mixer, weaver, centrifuger, extractor and cultivator.
block.tutorial-mod-tutorial-crafter.details = Without factory you cannot spell "factory game"

至于贴图,你需要根据方块的尺寸而定,一般每一个地图格子为32px*32px(px为像素的意思)。如3x3的方块应使用96px*px大小的贴图。

声明消耗器(Consume)

接下来,你可以为工厂添加输入(Input)。在 Mindustry 中,大部分输入都被抽象成了一个**消耗器(Consumer)**对象。

Anuke已经封装好了常用的消耗器的声明,这些方法的功能正如其字面义:

java
//每次生产消耗1个铜
consumeItem(Items.copper,1);
//每次生产消耗1个铜和2个铅
consumeItems(ItemStack.with(Items.copper,1,Items.lead,2));
//生产时每“刻”(见下)消耗1单位水
consumeLiquid(Liquids.water,1f);
//生产时每刻消耗1单位水和2单位矿渣
consumeLiquids(LiquidStack.with(Liquids.water,1f,Liquids.slag,2f));
//生产时每刻消耗1单位电力
consumePower(1f);

值得注意的是,流体和电量的消耗都是以 刻(Tick) 为单位的,而1s = 60tick,例如,consumePower(60f)实际上每秒消耗3600电力。因此,应当注意此处的单位换算问题,避免出现消耗速率意外扩大60倍的问题。

以上只是消耗器字面上的用法。事实上,原版中火力发电机的燃料消耗、炮塔的冷却剂等也是消耗器的功能。这些“不正常”的消耗器并没有什么不同,也可以应用在工厂中。

java
//像火力发电机一样,消耗高燃烧性的物品,额外提升的工作效率由物品的燃烧性决定
consume(new ConsumeItemFlammable());
//像火力发电机一样,遇到高爆炸性的物品会炸坏方块
consume(new ConsumeItemExplode());
//像炮塔一样,使用冷却剂提高工作效率
consume(new ConsumeCoolant(1f));

Anuke没有封装这些特殊消耗器的“快速通道”,所以你需要用基本方法consume()来**注册(Register)**消耗器。查阅源代码可知,consumeItem()等方法也是对consume()方法的包装。本教程后续章节将会继续深入了解消耗器的相关机制,届时你可以创建自己的消耗器。

最后仍要强调的是,一个类型为GenericCrafter的方块有且仅有一个配方,所有的消耗器都是这个配方的输入,自相矛盾的消耗器会导致方块无法正常工作。

输出项

接下来,你可以为工厂添加输出项。GenericCrafter的输出是由outputItem outputItems outputLiquid outputLiquids声明的。

java
//每次生产输出1个铜
outputItem = new ItemStack(Items.copper, 1);
//或
//每次生产输出1个铜和2个本模组物品
outputItems = ItemStack.with(Items.copper, 1, ModItems.item1 ,2);

//生产时每刻输出1单位水和2单位矿渣
outputLiquids = new LiquidStack(Liquids.water, 1f);
//或
//生产时每刻输出1单位水和1单位本模组流体
outputItem = LiquidStack.with(Liquids.water, 1f, ModItems.liquid1, 2f);

//同时设置`outputItem`和`outputItems`会无视`outputItem`,流体同理。

如果想要让工厂输出电力,可以让工厂消耗负的电量,但是不推荐这么做,因为兼容性较差。但如果想同时输出物品和发电则不得不这样做。

java
//不推荐
//生产时每刻消耗-1单位电力,即输出1单位电力
consumePower(-1f);

至于输出热量,则需要调整方块的类型才能做到,见于下文。

其他字段

除了输入和输出,生产时间craftTime也是工厂的要素。这个字段也是以刻为单位的。

对于输出多种流体的工厂(如电解器),可以用liquidOutputDirections控制各种流体的输出面,设置为-1表示任意方向。Mindustry中的方向与数学上任意角的定义是一致的,都是以x轴正方向开始,沿逆时针旋转,这样可以得到右0上1左2下3的四个方向。这个字段的顺序需要和outputLiquids保持一致。此外,你还需要控制是否忽略部分流体已经充满,dumpExtraLiquidtrue时表示“只要有一种流体还没满就继续工作”,而ignoreLiquidFullness表示无论流体输出什么状态均无视。

关于updateEffect craftEffect等特效和ambientSound destroySound breakSound placeSound等音效,见于本章第五节。

声明绘制器(Drawer)

在v7版本之前,原版工厂仅有少数几种绘制模式,且绘制内容受工厂类型限制。v7版本更新后,绘制系统发生了显著变化:一方面,所有无需热量需求且无属性增益的工厂被统一归入 GenericCrafter 类型;另一方面,绘制逻辑被抽离为独立的绘制器(Drawer)组件。这一改动增强了JSON模组在绘制行为上的自定义能力,同时优化了Java端的工厂架构,提升了绘制模式的复用性。本节将首先介绍如何使用与组合原版中已有的绘制器。

最基本的drawer是DrawDefault,可以绘制一张名称与本工厂相同的贴图。使用方式如下:

java
drawer = new DrarDefault();

Drawer可以对绘制过程进行一定拓展,在mindustry.world.draw包中还有许多drawer可供使用。但是一个方块只有一个drawer字段,大部分drawer只能做一件事,需要配合使用。这时,你可以使用DrawMulti来对drawer进行组合:

java
drawer = new DrawMulti(
                new DrawRegion("-bottom"),
                new DrawLiquidTile(Liquids.water, 2f),
                new DrawBubbles(Color.valueOf("7693e3")),
                new DrawRegion(),
                new DrawLiquidOutputs(),
                new DrawGlowRegion()
                );

一个DrawMulti中可以包括多个drawer,这些drawer将会按照声明顺序从先到后,按照从下到上叠加绘制,最终获得较佳的效果。

neoplastic-reactor

有的drawer会加载贴图。例如DrawerDefault需要一张与方块内部名相同的贴图,DrawRegion的有参构造器版本会需要一张内部名+特定后缀的贴图,DrawGlowRegion需要一张-glow为后缀的贴图。各个drawer所加载的贴图要求都会呈现在其load()方法。

原版所有的drawer如下表:

名称效果所需贴图后缀使用例
DrawArcSmelt绘制半径周期变化的圆,和一些向四周飞出的、充当火焰的短线电弧硅炉
DrawBlock是抽象类,不能直接使用所有方块
DrawBlurSpin低速时绘制一个旋转贴图,高速时绘制动态模糊版的旋转贴图suffixsuffix-blur涡轮冷凝器
DrawBubbles绘制一些泡泡电解器
DrawCells绘制流体层和一些在流体里放大缩小的粒子middle瘤变反应堆中心瘤液处
DrawCircles绘制一些会放大缩小的粒子合金坩埚和瘤变反应堆四周
DrawCrucibleFlame绘制向内运动并缩小的粒子碳化物坩埚的中心
DrawCultivator绘制在随机位置出现的逐渐扩大的八边形培养机
DrawDefault绘制贴图无后缀大部分方块
DrawFade绘制有周期性变化颜色的遮罩层top塑钢压缩机
DrawFlame绘制工作时周期性变化大小的灯top硅冶炼厂
DrawFrames绘制随工作进度周期性或线性变换的贴图framei
DrawGlowRegion绘制亮度随工作进度线性变化的贴图glow热解发生器
DrawHeatInput绘制亮度随输入热量线性变化的贴图heat所有带热量输入的方块
DrawHeatOutput绘制亮度随输出热量线性变化的贴图heat glow top1 top2所有带热量输出的方块
DrawHeatRegion绘制亮度随热量线性变化并周期变化的贴图glow所有热量工厂
DrawLiquidOutputs按输出流体面(liquidOutputDirections)绘制贴图liquid-output电解器
DrawLiquidRegion绘制透明度随指定或当前存在流体量变化的贴图liquid孢子压缩机
DrawLiquidTile绘制透明度随指定或当前存在流体量变化的纯色层冷冻液混合器
DrawMulti组合多个Drawer基本所有方块
DrawMultiWeave绘制两个旋转的、会发光的梭子weave weave-glow相织布合成机
DrawParticles绘制粒子特效大气收集器
DrawPistons绘制一圈周期收缩的活塞piston热解发生器
DrawPlasma绘制旋转的、透明度随预热程度线性变化的多张贴图plasma-i冲击反应堆
DrawPower绘制颜色随电量线性变化的贴图power或(power-empty和power-full)电池
DrawPulseShape绘制一个逐渐扩大的菱方形或圆形再生投影仪
DrawPumpLiquid绘制与抽取流体同色的贴图liquid
DrawRegion绘制一张贴图
DrawShape绘制一个多边形再生投影仪
DrawSideRegion绘制一个与朝向有关(右上/左下)的贴图top1 top2
DrawSoftParticles绘制柔软的粒子特效通量反应堆
DrawSpikes(未知)相织布合成机
DrawTurret绘制炮塔的part视情况而定所有很帅的炮塔
DrawWarmupRegion绘制随预热程度变化的贴图topRTG发电机
DrawWeave绘制一个旋转的的梭子weave相织布编织器

此外有几点须知:

  • 大部分drawer为了增加动态效果,会通过正弦函数对绘制的参数增加一个周期性变化的值,这种现象称为 “律动”(Pulse) ,而正弦函数y=Asinωx中的A、ω分别叫作振幅(Magnificance)和频率(Scale),缩写分别为Mag和Scl;
  • 上提到的所有变化中的自变量都不是“量”,而是“量”与“容量”的比值(这种处理叫 归一化(Normalized) ),介于0-1之间(有时会超过1),比如DrawLiquidTile的自变量是流体量与流体容量的比值。

一些特殊的工厂子类型

并非所有工厂都是GenericCrafter。正如开头所说,不同的功能需要不同的类型,属性增益工厂的类型是mindustry.world.blocks.production.AttributeCrafter,有热量需求的工厂的类型是mindustry.world.blocks.production.HeatCrafter,热量产生器为mindustry.world.blocks.heat.HeatProducer(不在production包内)。

这三个类型都是GenericCrafter的子类,也就是说,上文所提的一切字段在这两个类型中都是可用的;相应地,这三个类也有一些新增的属性:

对于AttributeCrafter

  • atrribute:使此工厂获得增益或减益的属性(Attribute),原版中的attribute包括heat spores water oil light sand steam等。属性并不一定是地形带来的,天气和环境也可以带来属性,如孢子迷雾天气会增加spores属性;
  • baseEfficiency:无增益效果时的基础效率;
  • boostScale:增益效果的倍率;
  • maxBoost:增益效果的最大值,为增量;
  • minEfficiency:允许放置方块的最低增益效果,-1为无需增益也可放置;
  • displayEfficiencyScale:在显示工厂效率时的一个乘数;
  • displayEfficiency:是否在方块的 **建造栏(Placement Fragment)**显示工厂效率,即屏幕右下角显示建筑血量的部分;
  • scaleLiquidConsumption:是否在更改效率的同时改变流体消耗速率。

对于HeatCrafter

  • heatRequirement:所需热量;
  • overheatScale:当获取热量超出所需热量时,多出的热量将以多大的比例提高效率,默认是按原倍数增长;
  • maxEfficiency:由热量增益产生的最大效率。

对于HeatProducer

  • heatOutput:热量输出;
  • warmupRate:预热速度。

创建一个Seperator

分离器的类型是mindustry.world.blocks.production.Seperator,它不是工厂,但是运行逻辑类似工厂。

java
new Seperator("tutorial-seperator");
kotlin
Seperator("tutorial-seperator")

和工厂类似的是,你可以设置drawercraftTime。而分离器的产出及各产物概率是在results字段中。此时ItemStack中的amount不再表示数量,而是此物品所占的比例:

java
results = with(
                Items.copper, 5,
                Items.lead, 3,
                Items.graphite, 2,
                Items.titanium, 2
            );

总比例是5+3+2+2 = 12,则铜占其中的5/12。

分离器的机制有两点值得讨论:

  • 分离器的结果是如何在服务器和各个玩家之间保持同步的? 分离器的结果并不是真随机,而是伪随机。简单来说,伪随机代表一种确定的随机。其确定性体现在,如果生成随机数的种子确定,则生成出来的随机数列也是确定。例如,如果种子都是1,那么第一次生成必定出现4,第3次生成必定出现9等。其随机性体现在,仅凭几次的输出无法推算出种子值,且生成的数列在各种意义上分布都是较为平均的。
  • 大分离器是如何输出钍等物品而不输出废料的,以及是如何计算物品总容量的? 实际上分离器根本没有使用方块的separateItemCapacity属性,而是在init()时获取第一个物品消耗项的物品,并在之后的计算过程中减去此物品计算总共的物品容量。这就导致使用ContentPatch是无法修改这一点的。

关于载荷系统中的工厂,见于第八节。

加载顺序

在方块中使用模组内的物品或流体时,应确保这些内容先于方块本身加载,否则可能引发 NullPointerException 异常或游戏内显示异常。为此,只需确保物品或流体的注册代码在方块的注册代码之前执行即可。