引言

上一篇 中,我们注册了第一个方块——铥墙 (Thulium Wall)。接下来,我们将通过 组件设计模式 (Composite Pattern) 对铥墙的一些功能进行适当地修改。
在原版中,多次用到了这种设计模式,例如:单位类型(UnitType) 上的 能力(Ability)方块(Block) 上的 消耗(Comsume)方块实体(Building) 中的 物品模块(ItemModule) 等等。我们在之后还会多次看到这个 设计模式(Design Pattern) 的身影。
组件模式旨在:减少继承树的深度,增加代码 复用性(Reusability)可拓展性(Extensibility)灵活性(Flexibility) 等,将 面对对象(OOP) 中的经典 抽象(Abstract) —— 是一个(is-a),变为 有一个(has-a)

组件 Component

首先,我们需要为等待实现的功能写一个需求表:

  1. 铥墙之间能够相互传递生命值,直到大家的生命值都变成平均值——这样可以防止前线被单点击破.

我们上一章已经将 ThuliumWall类 更名为 StatedWall类 了,为了实现上述需求,也不建议您再次新建 ThuliumWall类 去继承 StatedWall类,这样依然会让 功能的管理 成为负担。所以,我们这里将采用 组件(Component) 的形式,将”平摊生命值”变为一种组件——以供其他方块使用,例如你可以让其他墙也有这种”平摊生命值’的能力!

背景信息 Background

先介绍一些基本的 Background,如果您已经了解过,可以跳过这部分。

  1. 游戏内的显示 帧率(fps) 决定了游戏运行的 频率(tps),你可以默认每秒钟有60tick。
  2. 方块(Block) 默认是不开启更新的,您需要将 update 设置为 true。

首先我们在 tutorial.components包 (若不存在就创建) 下创建 ComponentBase 类,并添加 void onUpdate(Build),代码如下:

  1. package tutorial.components;
  2. import mindustry.gen.Building;
  3. public abstract class ComponentBase<Build extends Building> {
  4. public void onUpdate(Build b) {
  5. }
  6. }

其中,用到了 抽象类(Abstract class)泛型(Generic),下面我们来简单介绍一下:

  1. 抽象类:该类无法被实例化,必须声明子类继承它,且子类必须实现所有的 抽象方法(Abstract Method)
  2. 泛型:您可以把泛型认为是一种会被替换的文本,你可以在不必指定确定要使用的类时编写代码。此处使用 可以确保 Build 这个 泛型参数(Generic Argument) 一定是继承了 Building类

关于抽象类的信息,请参考:https://www.runoob.com/java/java-abstraction.html
关于泛型的信息,请参考:https://www.runoob.com/java/java-generics.html

修改 StatedWall 类

现在我们已经有了 组件基类(ComponentBase) 了,接下来就是修改 StatedWall类,使它能够应用我们的各种组件。
为了能让我们的 方块实体(Building) 能够在每次游戏 更新(update) 的时候可以执行特殊的代码,我们需要让它覆写来自 Building类 的 void updateTile() 方法,并在里面写上我们需要执行的代码。
请注意:任何写在 void updateTile() 里的代码,都会在游戏每次 更新(update) 的时候被调用,如果执行内容比较耗时,可能会导致游戏卡顿,请尽量把复杂的任务拆分成可以分步骤执行的一个或多个任务。并且, 更新(update) 一般会在一秒内调用大约60次,请将任何 持续性的动作的速度 减缓60倍——即除以60,以防止在半秒钟不到就执行/运动完,导致 玩家失去判断能力 或 没有足够的时间用以显示动画。
我们将新增如下代码:

在StatedWall类里加入如下: public ComponentBase component; 在StatedWall类里加入如下: @Override
public void updateTile() {
if (component != null) {
component.onUpdate(this);
}
}

将在 方块实体 的 void updateTile() 里调用组件的 void onUpdate(Build) 方法,也请不要忘记,我们需要判断 component 字段是否为空,因为:

  1. 引用类型的字段的默认值为 null
  2. 可能外部没有为本方块添加组件

最终的 StatedWall类 代码如下:

  1. public class StatedWall extends Wall {
  2. public TextureRegion[] states;
  3. public int stateNumber;
  4. public ComponentBase<StatedWallBuild> component;
  5. public StatedWall(String name) {
  6. super(name);
  7. }
  8. @Override
  9. public void load() {
  10. super.load();
  11. states = new TextureRegion[stateNumber];
  12. for (int i = 0; i < stateNumber; i++) {
  13. states[i] = Core.atlas.find(name + "-" + i);
  14. }
  15. }
  16. public class StatedWallBuild extends WallBuild {
  17. @Override
  18. public void updateTile() {
  19. if (component != null) {
  20. component.onUpdate(this);
  21. }
  22. }
  23. @Override
  24. public void draw() {
  25. int curIndex = (int) (lostHealthPct() * stateNumber);
  26. curIndex = Math.max(curIndex, stateNumber - 1);
  27. Draw.rect(states[curIndex], x, y);
  28. this.drawTeamTop();
  29. }
  30. public float lostHealthPct() {
  31. return 1f - health / maxHealth;
  32. }
  33. }
  34. }

编写组件 SharingHealth

接下来,我们将在 tutorial.components包 下创建 SharingHealth类——或者放在任何您喜欢的地方,输入类名,IDEA会自动提示您引用它
使 SharingHealth类 继承 ComponentBase类,在类定义中,覆写父类的 void onUpdate(Build) 方法,代码如下:

  1. package tutorial.components;
  2. import tutorial.blocks.StatedWall;
  3. public class SharingHealth extends ComponentBase<StatedWall.StatedWallBuild>{
  4. @Override
  5. public void onUpdate(StatedWall.StatedWallBuild b) {
  6. }
  7. }

可以看到,我们将原版的 泛型参数(Generic Argument) Build 全部替换为了 StatedWall.StatedWallBuild 类。
要实现传递血量,我们需要使用如下几个方法和字段:

字段 Building#health 它代表一个 Building 对象当前的生命值

字段 Building#proximity 它代表一个 Building 对象相邻近的(紧贴的)其他方块实体

最终代码如下,我会在注释中解释每行代码的作用:

  1. public class SharingHealth extends ComponentBase<StatedWall.StatedWallBuild> {
  2. @Override
  3. public void onUpdate(StatedWall.StatedWallBuild b) {
  4. // b 是在 StatedWall#updateTile() 里被传入进来的
  5. // 遍历邻近的方块实体,设为other
  6. for (Building other : b.proximity) {
  7. // 如果other的享元方块 和 b的享元方块 不一样,则跳过
  8. if (other.block != b.block)
  9. continue;
  10. // 获得b的生命值
  11. float thisH = b.health;
  12. // 获取other的生命值
  13. float otherH = other.health;
  14. // 如果 b的生命值 大于 other的生命值 且
  15. // b的生命值 大于 1 —— 防止因为生命值传递而死
  16. // other的生命值 小于 其最大生命值
  17. if (thisH > otherH && thisH > 1 && otherH < other.maxHealth){
  18. // 传递生命值
  19. other.health += 1;
  20. b.health -= 1;
  21. }
  22. }
  23. }
  24. }

接下来,我们需要将这个类的对象 赋值给 铥墙方块对象。在 ModBlocks.java 里的代码变成了如下:

  1. public class ModBlocks implements ContentList {
  2. public static StatedWall thuliumWall;
  3. //新增的
  4. public static SharingHealth sharingHealth = new SharingHealth();
  5. @Override
  6. public void load() {
  7. thuliumWall = new StatedWall("thulium-wall") {{
  8. requirements(Category.defense, BuildVisibility.shown, new ItemStack[]{
  9. });
  10. health = 2500;
  11. size = 2;
  12. buildCostMultiplier = 2f;
  13. stateNumber = 4;
  14. update = true;
  15. //新增的
  16. component = sharingHealth;
  17. }};
  18. }
  19. }

接下来,您就能经过 打包 后,就能在游戏中看到效果了。
ThuliumWallSharingHealth2.gif

组件容器

目前我们的 StatedWall类 只支持同时存在一个 组件(Component),当我们需要让一个Building有多个组件时,我们可以使用 Java 提供的 容器/集合类(Collecion) 来进行存储。
所以我们可以使用 ArrayList 类,用以存放所有组件。ArrayList类 是一个底层使用数组存储数据,但是可以在添加元素的时候动态扩展长度 (即数组能存放的最大元素数量) 的 集合(Collection) 此前我们已经使用过数组了。
关于容器类的信息,请参考:https://www.runoob.com/java/java-collections.html
修改后的代码如下,代码内容并不难,请您仔细理解:

  1. public class StatedWall extends Wall {
  2. public TextureRegion[] states;
  3. public int stateNumber;
  4. // 修改后
  5. public ArrayList<ComponentBase<StatedWallBuild>> components =
  6. new ArrayList<>();
  7. public StatedWall(String name) {
  8. super(name);
  9. }
  10. @Override
  11. public void load() {
  12. super.load();
  13. states = new TextureRegion[stateNumber];
  14. for (int i = 0; i < stateNumber; i++) {
  15. states[i] = Core.atlas.find(name + "-" + i);
  16. }
  17. }
  18. public class StatedWallBuild extends WallBuild {
  19. @Override
  20. public void updateTile() {
  21. // 修改后
  22. for (ComponentBase<StatedWallBuild> component : components) {
  23. component.onUpdate(this);
  24. }
  25. }
  26. @Override
  27. public void draw() {
  28. int curIndex = (int) (lostHealthPct() * stateNumber);
  29. curIndex = Math.min(curIndex, stateNumber - 1);
  30. Draw.rect(states[curIndex], x, y);
  31. this.drawTeamTop();
  32. }
  33. public float lostHealthPct() {
  34. return 1f - health / maxHealth;
  35. }
  36. }
  37. }
  1. public class ModBlocks implements ContentList {
  2. public static StatedWall thuliumWall;
  3. public static SharingHealth sharingHealth = new SharingHealth();
  4. @Override
  5. public void load() {
  6. thuliumWall = new StatedWall("thulium-wall") {{
  7. requirements(Category.defense, BuildVisibility.shown, new ItemStack[]{
  8. });
  9. health = 2500;
  10. size = 2;
  11. buildCostMultiplier = 2f;
  12. stateNumber = 4;
  13. update = true;
  14. // 修改后
  15. components.add(sharingHealth);
  16. }};
  17. }
  18. }

以上,您就完成了本章教程的学习。请您务必多加练习,多多尝试使用这种设计模式来编写代码,祝您和您的 mod 大获成功。

打包 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.