引言

本章主要介绍如何通过现有的原版代码创建工厂方块,并使用 组件化的(Composite) 渲染控制。
组件化在 上一章 “进阶:组件化” 中有提及,如果你还未理解什么是组件化,可以先去阅读上一章的内容。

复制-粘贴 Copy-Paste

原版(Vanilla) 为我们提供了很多已有的代码,作为可参照的示例,在您开始真正学习 modding 之前,您必须要能熟练地使用各种工具和利用任何现有的资源。
原版提供的所有方块的注册表:https://github.com/Anuken/Mindustry/blob/master/core/src/mindustry/content/Blocks.java

快捷键提示:除ctrl+左键外,您还可以通过快速双击shift键,弹出快速搜索窗口,勾选”include non-project items”,并输入您想要搜索的名称,正如下图所示:

DualShiftSearching.png
您可以很轻松地在里面找到想要的代码作为示范,之后的工作只需要 复制粘贴(Copy-paste) 即可——但请不要忘记,还需结合自己的代码,稍加修改。

  1. siliconSmelter = new GenericCrafter("silicon-smelter"){{
  2. requirements(Category.crafting, with(Items.copper, 30, Items.lead, 25));
  3. craftEffect = Fx.smeltsmoke;
  4. outputItem = new ItemStack(Items.silicon, 1);
  5. craftTime = 40f;
  6. size = 2;
  7. hasPower = true;
  8. hasLiquids = false;
  9. drawer = new DrawSmelter(Color.valueOf("ffef99"));
  10. ambientSound = Sounds.smelter;
  11. ambientSoundVolume = 0.07f;
  12. consumes.items(with(Items.coal, 1, Items.sand, 2));
  13. consumes.power(0.50f);
  14. }}

大部分 可注册类型(物品,方块,子弹类型等) 都有对应的注册表,您可以在 Mindustry 官方仓库的这里找到他们:https://github.com/Anuken/Mindustry/tree/master/core/src/mindustry/content
或者在IDEA的 类继承层次结构(Hierarchy) 窗口里查询所有 ContentList类子类(Subclass) ——因为原版中所有的注册表都继承了这个类。

快捷键提示:将光标移至类名上,按下ctrl+H键,可以弹出 类继承层次结构(Hierarchy) 窗口。结果如下:

Hierarchy.png

快捷键提示:使用Ctrl+Shift+F,可以弹出 在文件中查找(Find In Files) 窗口。

当您想找某样对象——可能是类,可能是字符串,也可能是个方法,甚至是您猜出来的名称!您可以通过 “Find In Files” 窗口进行 全局查找,这个功能异常强大!—— 可能需要搜索很久……
具体使用方法请您自行使用搜索引擎进行查找,本教程不进行讲述。—— 使用搜索引擎进行搜索,也是对您能否掌握IDEA的”搜索”的考验。示例的窗口截图如下:
FindInFiles.png

注册一个复杂的物品

在注册工厂前,普冷姆还需要注册一个新的物品,当然,您可以不必那么做。
代码如下,同时也要添加对应的 本地化文件贴图 ,这里使用了在第一个方块中使用过的匿名类,额外修改了一些物品的原信息。
这里我们注册了一个新的物品——铥块 Thulium Bar

  1. public class ModItems implements ContentList {
  2. public static Item thulium;
  3. public static Item thuliumBar;
  4. @Override
  5. public void load() {
  6. thulium = new Item("thulium", Color.valueOf("#f7ae08")) {{
  7. hardness = 0;
  8. radioactivity = 0.25f;
  9. cost = 3.5f;
  10. }};
  11. thuliumBar = new Item("thulium-bar", Color.valueOf("#ce7508")) {{
  12. radioactivity = 0.25f;
  13. }};
  14. }
  15. }

这里修改了一些您可能不理解的元信息,对于某些 字段/方法,光看名称是很难对它的作用和意义有个较准确的判断和认识的。我们可以使用IDEA的 “Find Usages” 功能,让IDEA帮我们搜索、罗列出项目中所有对于该 字段/方法 的使用。如下所示:
RClickFindUsage.png

快捷键提示:将光标移至类名/字段名/方法名上——1. 右键名称,选择 “Find Usages”;2. 直接按下Alt+F7,无需右键

通过这个,我们可以结合其他的方法调用和字段引用,根据上下文判断该字段的含义与作用。
这里,我们可以得到这个简易结论 —— radioactivity 字段可以用于为 RTG发电机(RTG Generator) 用于判断物品的 发电效率(Efficiency)

第一个工厂

注册:

通过复制原版的工厂并加以修改,我们可以得到如下的工厂的代码,注释中有更多对代码的解释。

  1. ancientAltar = new GenericCrafter("ancient-altar") {{
  2. // 该方块在什么建筑栏,建造它需要消耗什么材料
  3. requirements(Category.crafting, new ItemStack[]{
  4. new ItemStack(ModItems.thulium, 30),
  5. new ItemStack(Items.copper, 30),
  6. new ItemStack(Items.lead, 25)
  7. });
  8. // 输出什么物品,这里设定为我们mod里的另一种物品
  9. outputItem = new ItemStack(ModItems.thuliumBar, 1);
  10. // 设置每次生产需要花费的时间
  11. craftTime = 80f;
  12. // 方块的尺寸是 3x3 的
  13. size = 3;
  14. // 该方块是否能存储物品,这对消耗物品进行生产的工厂来说是必须的
  15. hasItems = true;
  16. // 需要消耗多种物品
  17. consumes.items(
  18. new ItemStack(Items.coal, 1),
  19. new ItemStack(Items.sand, 2)
  20. );
  21. // 只需消耗一种物品
  22. consumes.item(ModItems.thulium, 6);
  23. // 该方块是否能存储液体,这对消耗液体进行生产的工厂来说是必须的
  24. hasLiquids = true;
  25. // 设定需要消耗的液体种类与容积
  26. consumes.liquid(Liquids.water, 10f);
  27. // 该方块是否需要用电,这对用电进行生产的工厂来说是必须的
  28. hasPower = true;
  29. // 设定需要消耗多少电力
  30. consumes.power(0.5f);
  31. }};

普冷姆对于该方块的需求如下:

  1. 只消耗一种物品进行生产
  2. 每次消耗 mod 物品 铥(Thulium) 6个
  3. 每次生产1个 mod 物品 铥块(Thulium Bar)
  4. 不消耗液体
  5. 不需要电力
  6. 占地 3x3
  7. 每次生产花费 大约1.33秒——即 80 Ticks
  8. 建造消耗:30个 铥(thulium),30个 铜(copper) 和 25个 铅(lead)

最终,我们可以得到如下代码,您可以根据您的需求自由选择、组合这些消耗与输出。不过先请您尝试理解上述需求与下方代码之间的关系。

  1. ancientAltar = new GenericCrafter("ancient-altar") {{
  2. // 需求 8
  3. requirements(Category.crafting, new ItemStack[]{
  4. new ItemStack(ModItems.thulium, 30),
  5. new ItemStack(Items.copper, 30),
  6. new ItemStack(Items.lead, 25)
  7. });
  8. // 需求 3
  9. outputItem = new ItemStack(ModItems.thuliumBar, 1);
  10. // 需求 7
  11. craftTime = 80f;
  12. // 需求 6
  13. size = 3;
  14. hasItems = true;
  15. // 需求 2
  16. consumes.item(ModItems.thulium, 6);
  17. }};

加载顺序:

请您特别注意,在这里我们引用了

new ItemStack(ModItems.thulium, 30)

请您确保在 方块的注册表(ModBlocks类) 执行 void load() 时,这个物品 铥(thulium) 的 静态字段(static field) 已经被 赋值(Assgin) 后再 访问(Access)
物品(Item)方块(Block) 之间的关系来说,顺序如下:

  1. public class TutorialMod extends Mod {
  2. public static ModItems modItems;
  3. public static ModBlocks modBlocks;
  4. public static final ContentList[] modContents = {
  5. modItems = new ModItems(),// 注意这里的顺序!
  6. modBlocks = new ModBlocks(),// 注意这里的顺序!
  7. };
  8. // 省略不必要的代码
  9. }

之后遇到 液体(Liquid)方块(Block),或 子弹类型(BulletType) 方块,或 子弹类型 单位类型(UnitType) 时,您还需要去处理 加载顺序
如果遇到 相互引用 的复杂情况,您需要独立地创建对象 —— 但依然要保证,在 Mod#loadContent( )方法 的执行期间 —— 防止加载出错,或为其他 mod 添加了内容。

效果:

然后添加 贴图本地化文件,使用的贴图如下:

ancient-altar.png ancient-altar.png

然后我们就可以在 打包 后,在游戏内看到效果了。
AncientAltarCrafting.png

自定义渲染组件 Drawer

这里普冷姆使用了一个 非 常规大小 的贴图,正常贴图的大小要求如下:
贴图长 = 32 x 方块尺寸
贴图宽= 32 x 方块尺寸
即:贴图大小 = (32, 32) x 方块尺寸
例如,此处我将方块尺寸设置为 3,那么应该使用 96 x 96 大小的贴图。

您能够发现,这个贴图的位置非常靠近下方。这是因为 方块实体(Building) 的 x,y 是在 3x3 方块的正中心,而 Draw#rect 等方法对于贴图的 对齐(Alignment) 也是在正中央的。
因此,我们需要自定义渲染。不过这次,原版为我们提供了 组件化的(Composite) 的渲染控制—— DrawBlock类。组件化在 上一章 “进阶:组件化” 中有提及,如果你还未理解什么是组件化,可以去阅读那章。
创建 AncientAltarDrawer类 继承 DrawBlock类,您可以放在任何您喜欢的 包(Package) 里,这里我新建了 tutorial.draw包,用以存放所有的渲染组件。
我们需要覆写 void draw(GenericCrafter.GenericCrafterBuild) 方法。

背景信息 Background

Mindustry 的游戏画面是纯2D的,坐标系如下:
Coordinate256.png

调整渲染

因此,我们需要增加最终的y坐标的值,最终代码如下:

  1. public class AncientAltarDrawer extends DrawBlock {
  2. @Override
  3. public void draw(GenericCrafter.GenericCrafterBuild build) {
  4. Draw.rect(build.block.region,
  5. build.x,
  6. // 向上偏移一点距离,这个数字是来源于贴图,是硬编码
  7. build.y + 22.5f
  8. );
  9. }
  10. }

别忘了为 远古祭坛(AncientAltar) 方块添加这个渲染组件!

  1. ancientAltar = new GenericCrafter("ancient-altar") {{
  2. requirements(Category.crafting, new ItemStack[]{
  3. new ItemStack(ModItems.thulium, 30),
  4. new ItemStack(Items.copper, 30),
  5. new ItemStack(Items.lead, 25)
  6. });
  7. // 设置渲染组件
  8. drawer = new AncientAltarDrawer();
  9. outputItem = new ItemStack(ModItems.thuliumBar, 1);
  10. craftTime = 80f;
  11. size = 3;
  12. hasItems = true;
  13. consumes.item(ModItems.thulium, 6);
  14. }};

这里我们直接new了一个对象——即实例化 AncientAltarDrawer类,而不是使用静态字段来存储——像之前用于为铥墙传递生命值用的组件,是因为 这个对象 与 贴图/该方块 耦合(Coupling),我们无法 复用(Reuse) 该对象。后文会介绍如何更好地 解耦合(Reducing Coupling),和 复用(Reuse) 该AncientAltarDrawer类。
接下来,就能在游戏中看到效果啦!
AncientAltarCrafting2.png

更好的渲染

目前我们在游戏中能看到这个方块因为并非符合 原版(Vanilla) 的要求,阴影显得非常突兀——因为是方形的,接下来,我们需要自己控制阴影的渲染。
首先,我们可以通过 Block#hasShadow 字段来开关阴影自动渲染——这个字段初始化值为 true。
所以我们需要设置为 false,以此来关闭阴影。

hasShadow = false;

我们可以使用 Drawf类 下的各种方法来快捷地进行各种渲染——例如 绘制线段、绘制光亮、绘制阴影、绘制液体、绘制圆形方形、绘制激光等。
这里我们使用 Drawf#shadow(float, float,float) 方法进行绘制地面上的垂直阴影。
最终代码如下:

  1. public class AncientAltarDrawer extends DrawBlock {
  2. @Override
  3. public void draw(GenericCrafter.GenericCrafterBuild build) {
  4. // 渲染阴影
  5. Drawf.shadow(build.x, build.y, 40f);
  6. // 用于设置渲染的z坐标——层
  7. Draw.z(Layer.blockOver);
  8. // 渲染方块本身的贴图
  9. Draw.rect(build.block.region, build.x, build.y + 22.5f);
  10. // 因为设置了一些参数,我们需要重置 Draw类
  11. Draw.reset();
  12. }
  13. }

这里我们使用了 Draw#z(float) 方法用于设置渲染的 层(Layer) ,层的数值越小,渲染越下层——即会被 上层(数值大) 的覆盖住。
而方块的渲染会默认将 z坐标(z-Index) 设置为 Layer.block —— 数值比 Layer.blockOver 小。
别忘了为 远古祭坛(AncientAltar) 这个方块设置 hasShadow = false !

  1. ancientAltar = new GenericCrafter("ancient-altar") {{
  2. requirements(Category.crafting, new ItemStack[]{
  3. new ItemStack(ModItems.thulium, 30),
  4. new ItemStack(Items.copper, 30),
  5. new ItemStack(Items.lead, 25)
  6. });
  7. drawer = new AncientAltarDrawer();
  8. outputItem = new ItemStack(ModItems.thuliumBar, 1);
  9. craftTime = 80f;
  10. size = 3;
  11. hasItems = true;
  12. // 新增
  13. hasShadow = false;
  14. consumes.item(ModItems.thulium, 6);
  15. }};

最终效果如下:
AncientAltarCrafting5.png
当然,您可以根据喜好的不同,选择不一样的渲染方式——比如您可以不渲染阴影,或者渲染其他形状的阴影。这些内容,您需要自行查看 IDEA显示的源代码 或 Mindustry 的 GitHub 仓库,进行摸索和测试,本教程并不能做到面面俱到。

解耦 Reducing Coupling

您可以发现,这个 AncientAltarDrawer类 类似于一开始的 ThuliumWall类,所有代码都是围绕着我们要制作的特殊方块来编写的,这样会造成代码 耦合(Coupling) ,不利于代码的 复用(Reuse) ,所以我们要提取出被 硬编码(Hardcode) 的部分,让其可以被外部修改。
普冷姆将 AncientAltarDrawer类 重命名为 OffsetCrafterDrawer类,并把代码修改成如下:

  1. public class OffsetCrafterDrawer extends DrawBlock {
  2. public float shadowRadius;
  3. public float xOffset;
  4. public float yOffset;
  5. public OffsetCrafterDrawer(float shadowRadius, float xOffset, float yOffset) {
  6. this.shadowRadius = shadowRadius;
  7. this.xOffset = xOffset;
  8. this.yOffset = yOffset;
  9. }
  10. public OffsetCrafterDrawer() {
  11. }
  12. @Override
  13. public void draw(GenericCrafter.GenericCrafterBuild build) {
  14. Drawf.shadow(build.x, build.y, shadowRadius);
  15. Draw.z(Layer.blockOver);
  16. Draw.rect(build.block.region, build.x + xOffset, build.y + yOffset);
  17. Draw.reset();
  18. }
  19. }

这样一来,我们就可以这样传递参数对渲染进行控制了:

drawer = new OffsetCrafterDrawer(40f, 0f, 22.5f);

最终的 远古祭坛(AncientAltar) 方块的代码如下:

  1. ancientAltar = new GenericCrafter("ancient-altar") {{
  2. requirements(Category.crafting, new ItemStack[]{
  3. new ItemStack(ModItems.thulium, 30),
  4. new ItemStack(Items.copper, 30),
  5. new ItemStack(Items.lead, 25)
  6. });
  7. // 修改后
  8. drawer = new OffsetCrafterDrawer(40f, 0f, 22.5f);
  9. outputItem = new ItemStack(ModItems.thuliumBar, 1);
  10. craftTime = 80f;
  11. size = 3;
  12. hasItems = true;
  13. hasShadow = false;
  14. consumes.item(ModItems.thulium, 6);
  15. }};

这样,您就可以把这个类,通过实例化的时候在构造函数中传入不同的参数,以此能够在既可以精确控制某个特定方块的渲染的情况下,又可以不多新建类,更加具有泛用性。

打包 Jar 与 测试 Test

接下来,确保以上代码和操作都无误且没有疏漏后,可以如 前文 一样执行 Gradle 的 Jar Task,等待 jar 打包完毕,并导入到游戏中测试。
检查您的方块是否正常地被加载到游戏中;贴图是否正确——而不是 “oh no”;是否显示了本地化名称;工厂是否能够正常运行和生产;是否能正确渲染阴影;是否能够正确地在有一定偏移位置的情况下渲染贴图。

尾声

本章带您走进了 Mindustry Java Mod 的最关键部分——自定义渲染,之后还会介绍更多的关于 Mindustry Modding 的内容。
如果您喜欢本教程,可以给 普冷姆的 Mod —— Cyber IO 点上一个 Star 哦qwq
开源项目地址 https://github.com/liplum/CyberIO

版权声明

在本 教学mod 中,普冷姆以非商用形式使用了来自 饥荒 (Don’t Starve) 的图像资源,如若侵权请书面告知。对于引用的图片,All copyright Klei reserved.