引言

正如 注册第一个物品的章节的引言 所说,为了减少内存占用,我们需要将那些 可以在 不同实体(Entity) 间共享的 相同的部分 抽象(abstract) 出来,这就是 享元模式(Flyweight Pattern)
在方块中,我们不难发现,方块 (Block) 本体包含着最终在游戏中呈现 方块实体(Tile Entity)元信息(meta data) ——例如 尺寸(size,如1x1, 2x2, 3x3等)、最大生命值(max health)建筑分类(category)建造消耗(requiremens) 等信息,而 方块实体(Tile Entity) 可能包含一些相互独立的信息——例如 当前生命值(health)制造进度(progress)逻辑块的代码(code) 等。
您可能会依然对这个概念感到疑惑,不过从现在开始,普冷姆将进行一些本文中使用到的名词进行命名,希望您能在随后的示例和代码中理解这些。

命名

方块 (Block)——代表所有在游戏中可以看见的方块的享元
方块实体 (Tile Entity 或 Building)——代表所有在游戏中存储独立信息的”方块”

第一个方块 铥墙 Thulium Wall

类似与 注册第一个物品 ,我们可以,也最好创建一个新的 ContentList,用以存放所有我们 mod 的方块。别忘了在你的 mod 的主类中的 void initContent( ) 中实例化本类,并调用它的 void load( ) 方法!

  1. public class ModBlocks implements ContentList {
  2. public static Wall thuliumWall;
  3. @Override
  4. public void load() {
  5. thuliumWall = new Wall("thulium-wall");
  6. }
  7. }

然后你就可以为它添加 贴图本地化文件 了。
不过我们还需要修改一下它的字段,这样我们才能建造栏里找到它,并建造它。
这里我们需要使用 匿名类(Anonymous Class) ,并且在匿名类的构造方法里加入额外的代码,用以修改字段或调用方法。
更多关于匿名类的信息和语法,请参考:https://www.runoob.com/java/java-anonymous-class.html

  1. @Override
  2. public void load() {
  3. thuliumWall = new Wall("thulium-wall") {{
  4. requirements(Category.defense, BuildVisibility.shown, new ItemStack[]{});
  5. health = 2500;
  6. size = 2;
  7. buildCostMultiplier = 2f;
  8. }};
  9. }

如代码所示,我们将铥墙的尺寸设置为2x2,生命值为2500,建造花费时间倍率改为2.0。
具体 Block类 相关的API可以参考官方wiki:https://mindustrygame.github.io/wiki/modding/5-types/
或者使用 IDEA 直接查看 Block类 的定义。

快捷键提示:将光标移至类名上,按住ctrl键,并鼠标左键点击,即可跳转进该类的定义。对于方法和字段,该操作也是同理。

接下来,您就可以经过 打包 ,在游戏中看到自己的方块了。
image.png

进阶

当然,本教程是 Java mod,而非 Json mod 或 JavaScript mod,所以,我们接下来就进入进阶的部分。如果您在浏览这部分之后,还依然抱有疑惑,那么只能给您一个忠告:多练 多写 多想。

整理代码结构

在创建了 ModBlock 类后,我们的 mod 主类已经变成了如下:

  1. public class TutorialMod extends Mod {
  2. public static ModItems modItems;
  3. public static ModBlocks modBlocks;
  4. public TutorialMod() {
  5. Log.info("Loaded TutorialMod constructor.");
  6. }
  7. @Override
  8. public void loadContent() {
  9. Log.info("Loading some tutorial content.");
  10. modItems = new ModItems();
  11. modItems.load();
  12. modBlocks = new ModBlocks();
  13. modBlocks.load();
  14. }
  15. }

您能够很明显地发现,void loadContent() 方法会随着我们添加的 ContentList 对象变多,而变得更长、更臃肿,所以我们需要使用 数组 (Arrary) 对这些代码进行整理,以便之后更易于 拓展 (extension)
修改之后,我们的类变成了这样:

  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. public TutorialMod() {
  9. Log.info("Loaded TutorialMod constructor.");
  10. }
  11. @Override
  12. public void loadContent() {
  13. Log.info("Loading some tutorial content.");
  14. for (ContentList content : modContents) {
  15. content.load();
  16. }
  17. }
  18. }

后记:这里的顺序请勿更改,详情请参见此处
我们创建了一个 ContentList 类型的数组 用以存放 mod 中所有的 ContentList 对象。前文 曾提到 :子类对象 可以赋值给 父类引用,所以我们可以将不同的 ContentList 的子类存入 ContentList 类型的数组 中去,然后只调用它们共同的父类的方法 void load()。
这里使用了 for-each 循环 (也叫 增强 for 循环),更多相关信息请参考:https://www.runoob.com/java/java-loop.html
更多有关数组的信息,请参考:https://www.runoob.com/java/java-array.html
Java的数组引用是 协变的 (Covariant) ,所以请当心,您可能因为往 父类数组引用 中插入 子类对象 而在运行时报错,但在编译期没有任何提示,正如下所示:

Number[] num = new Integer[10]; num[0] = 2.1f; //可通过编译,运行时报错

继承 方块实体 Building

我们将在 tutorial 包下创建一个子包 blocks,再在 blocks 包 中创建一个类 ThuliumWall——因此 ThuliumWall 的全限定名为 tutorial.blocks.ThuliumWall
注:一般来说,我们有IDEA,无需手动写出 import 语句和记忆全限定名,但当您需要使用 反射 (reflection) 时,这是必须的。
我们使 ThuliumWall 继承 Wall 类;并在 ThuliumWall 类 定义的内部定义一个 内部类 (Inner Class),名为 ThuliumBuild 并继承 WallBuild 类。最终代码如下:

  1. public class ThuliumWall extends Wall {
  2. public ThuliumWall(String name) {
  3. super(name);
  4. }
  5. public class ThuliumBuild extends WallBuild {
  6. }
  7. }

注:Building 的子类的命名规范:XXX + Build
接下来,我们需要先规划好我们的需求:

  1. 让铥墙的贴图根据 不同的受伤程度 进行改变——基于剩余生命值

    Kotlin提示:这里必须使用内部类,而Kotlin的内部类默认为静态类,请您记得加上 inner 修饰符—— 例如 open inner class ThuliumBuild : WallBuild()

    加载自定义贴图

    首先,我们需要准备4张贴图——具体数量基于您的设计,放入 assets/sprites/blocks 下:

    thulium-wall-1.png thulium-wall-0.png thulium-wall-2.png thulium-wall-1.png thulium-wall-3.png thulium-wall-2.png thulium-wall-4.png thulium-wall-3.png

名称以 1、2、3 这样结尾的好处是,我们可以通过循环便捷地加载它们。
然后我们需要覆写 Block 类的 void load() 方法,以便在加载贴图的时候,可以加载我们自定义使用的额外贴图。这里,我们依然需要调用父类的 void load() 方法,加载其他必要的贴图。

快捷键提示:将光标放在类定义块中,按下ctrl+o,弹出快捷覆写/实现窗口,选择您想要覆写/实现的方法,然后按下Enter,即可自动生成方法覆写/实现。您可以在覆写/实现窗口中直接输入字符,进行搜索。

前文 中我们提到过,可以使用 Core.atlas.find(String) 方法搜索已经加载成功的贴图资源。最终代码如下,此处省略了 ThuliumBuild 类的定义:

  1. public class ThuliumWall extends Wall {
  2. public TextureRegion[] states;
  3. public int stateNumber;
  4. public ThuliumWall(String name) {
  5. super(name);
  6. }
  7. @Override
  8. public void load() {
  9. super.load(); //调用父类的load()方法
  10. states = new TextureRegion[stateNumber];
  11. for (int i = 0; i < stateNumber; i++) {
  12. // name 是 Block 的注册名
  13. states[i] = Core.atlas.find(name + "-" + i);
  14. }
  15. }
  16. }

我们设置一个 stateNumber 用于在创建 ThuliuWall 对象时,设置需要加载的额外贴图的数量。

自定义渲染(绘制)

我们需要覆写 Building 的 void draw() 方法,但是这次我们不用调用父类的 void draw(),将绘制全权交给我们自己的方块。
因为,我们的 ThuliumBuild类 是 ThuliumWall 的 内部类 (inner class) ,所以可以访问 外部类 (outer class) 的字段和方法。
我们需要使用 Draw类 下各种方法进行渲染,例如,这里我们使用了 Draw#rect(TextureRegion,float,float) ,您可以通过IDEA查看这个方法更详细的内容,仅在此处不多讲解,您可以先自行尝试。

  1. public class ThuliumBuild extends WallBuild {
  2. @Override
  3. public void draw() {
  4. // 将已损失生命值的百分比映射到 [0,stateNumber] 之间
  5. // 访问了外部类的 stateNumber 字段
  6. int curIndex = (int) (lostHealthPct() * stateNumber);
  7. // 加上这行代码,防止 curIndex 意外地超过数组的长度
  8. curIndex = Math.min(curIndex, stateNumber - 1);
  9. // 绘制一块矩形贴图,位于世界坐标的x y(这里用的是当前方块实体的xy)
  10. Draw.rect(states[curIndex], x, y);
  11. // 绘制队伍的角标
  12. this.drawTeamTop();
  13. }
  14. // 获取已损失生命值的百分比
  15. public float lostHealthPct() {
  16. return 1f - health / maxHealth;
  17. }
  18. }

然后别忘了在 ModBlocks#load() 里修改 ThuliumWall 匿名类的定义!修改后的如下:

  1. thuliumWall = new ThuliumWall("thulium-wall") {{
  2. requirements(Category.defense, BuildVisibility.shown, new ItemStack[]{});
  3. health = 2500;
  4. size = 2;
  5. buildCostMultiplier = 2f;
  6. stateNumber = 4; // 一共有4张贴图,所以设置成4
  7. }};

以上,我们就能让这个铥墙根据当前的血量来改变自己显示的贴图了。
DamagedThuliumWalls.png

代码复用 Code Reuse

现在,您已经学会了如何制作一个可以改变自己渲染状态的铥墙,但是为了一个”铥墙”去造了一个类,未免有点小题大做了——或者我们更多地重复利用我们刚刚写好的类?
这就是 代码复用(Code reuse) —— 我们应当重复利用现有的代码,而非进行简单的复制、粘贴,这样更利用组织、管理代码和节约开发时间。

现在您可以将 ThuliumWall 类,重命名为 StatedWall 类——别忘了同时修改它的 Building 哦。
传送门:如何利用IDEA快捷重命名
这样,我们就可以只修改 贴图 和 部分”属性”——“属性”是指游戏内的属性,例如 方块的生命值、我们定义的状态数量 等,不需要为以后可能会出现的每一种墙编写一个类 (去造出更多的方块) 了。
现在去试试吧!

打包 Jar 与 测试 Test

接下来,确保以上代码和操作都无误且没有疏漏后,可以如 前文 一样执行 Gradle 的 Jar Task,等待 jar 打包完毕,并导入到游戏中测试。
检查您的方块是否正常地被加载到游戏中;贴图是否正确——而不是 “oh no”;是否显示了本地化名称;是否有会根据当前血量显示不一样的状态。

尾声

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

版权声明

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