引言

本章主要介绍 创建新的 子弹类型(BulletType) 与新的 炮台(Turret) 方块。
子弹类型(BulletType) 是一种 享元(Flyweight) 原型(Prototype) 的结合。
子弹(Bullet) 是游戏中的 实体(Entity),具有 位置信息(x,y字段)速度子弹类型(type字段)额外信息(data字段)等。
子弹类型(BulletType) 是游戏中的 可注册类型,包含一个 子弹(Bullet)实体 的元信息,可以决定子弹的属性 (伤害,速度等),可以决定 子弹(Bullet)实体 如何渲染 和 如何更新 等等。

  1. 通过 子弹类型(BulletType) 可以在游戏中生成它所对应的 子弹(Bullet)实体 ——一个或多个,当你连续发射的时候,连续射击出来的子弹的 type 字段都引用了同一个 子弹类型(BulletType)。所以 子弹类型(BulletType)子弹(Bullet)实体 的 享元(Flyweight)
  2. BulletType类 实现了 Cloneable接口,你可以通过 BulletType#copy() 方法用同一个 BulletType对象 复制出多个相同设置的 BulletType对象。通过这个,你就可以基于已有的 BulletType对象,进行修改了。所以,子弹类型(BulletType)原型(Prototype)

您可能对于 原型模式(Prototype Pattern) 还比较陌生,我们将会在后续介绍 单位类型(UnitType) 能力(Ability) 时,再更详细地解读。

子弹类型 BulletType

经过前几章节的学习,我们很容易就能举一反三出以下代码,用以注册Mod中的 子弹类型(BulletType)
注:BulletType类 有很多实用的 子类(Subclass),您可以通过IDEA的 类继承层次结构(Hierarchy) 窗口来查找它们,建议您直接使用/继承 Mindustry 提供的子类,这样可以剩下很多时间。
这里我们直接继承 BulletType类,注册表 ModBullets类 代码如下,记得在 TutorialMod类 的ContentList数组 中实例化出此类。
代码如下:

  1. package tutorial;
  2. import mindustry.ctype.ContentList;
  3. import mindustry.entities.bullet.BulletType;
  4. public class ModBullets implements ContentList {
  5. // 这个只是演示,后续会删除此行代码
  6. public static BulletType thuliumBullet;
  7. @Override
  8. public void load() {
  9. // 这个只是演示,后续会删除此行代码
  10. thuliumBullet = new BulletType();
  11. }
  12. }

我们曾提到,因为静态字段的特殊性,您必须在静态字段被 赋值 (或初始化) 之后 访问(Access),所以您需要保证在 在 ModBlocks类炮台方块(Turret) 对象被实例化前,ModBullets类 里的 所有所需 子弹类型(BulletType) 都已经被实例化,且赋值给相应的静态字段。
从此处起,除特殊情况外,本教程将不再提示您 添加加载顺序,请您别忘记。

  1. public class TutorialMod extends Mod {
  2. public static ModItems modItems;
  3. public static ModBullets modBullets;
  4. public static ModBlocks modBlocks;
  5. public static final ContentList[] modContents = {
  6. modItems = new ModItems(),
  7. modBullets = new ModBullets(),
  8. modBlocks = new ModBlocks(),
  9. };
  10. // 省略不重要的部分
  11. }

新的子弹类型 New Bullet Type

为了能更好地控制我们专属的 子弹类型(BulletType) 的更新与渲染,我们需要继承 BulletType类。
tutoiral.bullets包 下创建 PowerOrb类,覆写父类的 void load() 加载独特的贴图,代码如下:

  1. public class PowerOrb extends BulletType {
  2. public TextureRegion orbTR;
  3. public String textureName = "";
  4. public PowerOrb(float speed, float damage) {
  5. super(speed, damage);
  6. }
  7. public PowerOrb() {
  8. }
  9. @Override
  10. public void load() {
  11. super.load();
  12. orbTR = Core.atlas.find(textureName);
  13. }
  14. }

你可以将 power-orb.png 放到在 assets/sprites文件夹 下的任何位置(可以放在bullets子文件夹下)

能量球 PowerOrb

先为能量球写个需求列表:

  1. 随着飞行的距离增加伤害
  2. 在能量球周围显示能量脉冲的粒子效果——这个效果将会在之后介绍

通过覆写 BulletType#update() 方法,我们可以在执行原来的更新内容后——即调用父类的 update() 方法,追加我们的内容。与此同时,我们还需要渲染加载的贴图。覆写的两个方法的代码如下:

  1. public float dmgIncrease = 2f / 60f;
  2. @Override
  3. public void update(Bullet b) {
  4. super.update(b);
  5. b.damage += dmgIncrease;
  6. }
  7. @Override
  8. public void draw(Bullet b) {
  9. super.draw(b);
  10. Draw.rect(orbTR,b.x,b.y);
  11. }

接下来会省略很多步骤,需要您慢慢理解。

  1. public class ModBullets implements ContentList {
  2. public static PowerOrb powerOrb;
  3. @Override
  4. public void load() {
  5. // 参数1:速度。参数2:伤害
  6. powerOrb = new PowerOrb(3.5f, 20f){{
  7. // 将 "power-orb" 添加当前的ModID作为前缀,变成 "tutorial-mod-power-orb"
  8. textureName = Vars.content.transformName("power-orb");
  9. // 子弹最大持续时长
  10. lifetime = 110;
  11. // 不使用任何粒子效果
  12. hitEffect = Fx.none;
  13. despawnEffect = Fx.none;
  14. shootEffect = Fx.none;
  15. smokeEffect = Fx.none;
  16. }};
  17. powerOrb.textureName = Vars.content.transformName("power-orb");
  18. ((ItemTurret) Blocks.duo).ammoTypes.put(Items.copper, powerOrb);
  19. }
  20. }

下面这行代码非常特殊:

((ItemTurret) Blocks.duo).ammoTypes.put(Items.copper, powerOrb);

首先我们知道 Blocks类 里使用大量 静态字段 存放了 所有原版的 方块(Block) 的,其次在我们的mod进行 Mod#loadContent() 方法时,原版的所有 Content 都已经注册完毕了。因此,我们可以在这里访问 duo(双管炮),为它修改使用 铜(copper) 时发射出来的子弹的 子弹类型(BulletType)
效果如下:
PowerOrb.png

可注册类型 Content

我们在 上一节中覆写的 void load() 方法 源自于 Content(mindustry.ctype.Content)类,这就是 前文 中一直提到的 “可注册类型“,通过 IDEA的 类继承层次结构(Hierarchy) 窗口 我们可以发现 方块(Block)物品(Item)星球(Plant)天气(Weather)单位类型(UnitType)等,都是继承于 Content类 的。
在 Content类 的构造方法中,会调用”注册该 可注册类型”相关的方法,所以,您只需要实例化它们的对象,就会 Mindustry 就会自动为您注册该类型。它会为每个类型,添加您的 Mod ID 作为前缀,并检查是否存在命名冲突。

访问所有 Content

您可以通过下列几个方法,查询本游戏中所有已经注册过的Content:

  1. // 获得物品 Item
  2. Vars.content.items();
  3. // 获得子弹类型 BulletType
  4. Vars.content.bullets();
  5. // 获得方块 Block
  6. Vars.content.blocks();
  7. // 获得液体 Liquid
  8. Vars.content.liquids();
  9. // 获得所有的 Content
  10. Vars.content.getContentMap();

您也可以通过以下代码,将 任何名称 转换为 添加了当前ModID为前缀的名称。

  1. Vars.content.transformName("这里填写需要被转换的名称");

替换原版内容

我们在 上一节中修改了原版炮台 双管炮(duo) 的子弹,当然,这里只是为了测试,随后您可以删除本行代码。
但这也为您带来了一种思路——您可以以 调试替换原版内容 为目的,修改原版的方块、物品等 Content 的一些字段,以此来改变某些行为。

眼球炮塔 EyeTurret

先上效果图
EyeTurret1.png
这是使用到的资源文件:

eye-turret.png 用于放置时的图标 eye-turret.png eye-turret-base.png 身体 eye-turret-body.png eye-turret-eye-ball.png 眼球 eye-turret-eye-ball.png eye-turret-eye.png 黑眼珠 eye-turret-eye.png

下面是 眼球炮塔(EyeTurret) 的代码:

  1. // 继承 ItemTurret,消耗物品作为弹药
  2. public class EyeTurret extends ItemTurret {
  3. // 身体贴图
  4. public TextureRegion bodyTR;
  5. // 眼球贴图
  6. public TextureRegion eyeBallTR;
  7. // 黑眼珠贴图
  8. public TextureRegion eyeTR;
  9. public EyeTurret(String name) {
  10. super(name);
  11. // 不渲染阴影
  12. hasShadow = false;
  13. }
  14. @Override
  15. public void load() {
  16. super.load();
  17. // 加载对应的贴图
  18. bodyTR = Core.atlas.find(name + "-body");
  19. eyeBallTR = Core.atlas.find(name + "-eye-ball");
  20. eyeTR = Core.atlas.find(name + "-eye");
  21. }
  22. // 这里需要覆写这个方法,因为Turret类默认会加上底座
  23. @Override
  24. public TextureRegion[] icons() {
  25. return new TextureRegion[]{region};
  26. }
  27. public class EyeBuild extends ItemTurretBuild {
  28. // 无需调用父类,进行自定义绘制
  29. @Override
  30. public void draw() {
  31. // 如何绘制基于您的设计
  32. float anchorX = x;
  33. float anchorY = y;
  34. // 绘制阴影
  35. Drawf.shadow(anchorX, anchorY - 12f, 40f);
  36. Draw.rect(bodyTR, anchorX, anchorY);
  37. Draw.z(Layer.turret + 1);
  38. float eyeBallX = anchorX;
  39. float eyeBallY = anchorY + 24.5f;
  40. Draw.rect(eyeBallTR, eyeBallX, eyeBallY);
  41. Draw.rect(eyeTR, eyeBallX, eyeBallY);
  42. }
  43. }
  44. }

这里需要提及的是:TextureRegion[] icons()方法 被 Turret类 覆写了,它会在最终生成图标的时候,将炮台底座的贴图给加上。所以我们需要覆写它,只使用我们提供的 TextureRegiond对象 拼成最终图标。
注意:这里传入的 TextureRegiond对象 必须完整的,不能使用被处理过大小的——比如是从一张 精灵表(Sprite Sheet) 里切割出来。
注册方块相关代码如下:

  1. eyeTurret = new EyeTurret("eye-turret") {{
  2. requirements(Category.turret, new ItemStack[]{
  3. new ItemStack(ModItems.thuliumBar, 5)
  4. });
  5. health = 1200;
  6. // 设定弹药和其对应的子弹类型
  7. ammo(
  8. ModItems.thulium, ModBullets.powerOrb
  9. );
  10. // 弹药仓储量
  11. maxAmmo = 20;
  12. size = 3;
  13. // 两次发射间的CD
  14. reloadTime = 60f;
  15. // 攻击范围
  16. range = 240f;
  17. // 后坐力大小
  18. recoilAmount = 3f;
  19. }};

从此,您在以后的路上将会遇到琳琅满目的字段与方法,本教程无法为您一一说明。并且因存在着可能的版本差异,本教程只能为您提供最基础的入门,您必须学会通过IDEA的各种便捷功能查阅相关的API。
从此处以后,以下的提示将不会再出现,您必须时刻牢记且熟练这些方法。
如何查看一个类的定义?
如何查看一个类的父类们或子类们?
如何查看一个字段/方法在哪里被使用了?
如何查找我想要,但我不知道的东西?
现在我们已经能够正常发射子弹了,这得益于 ItemTurret类及其父类 已经为我们提供了很多有用的功能,如果您想要从零实现也是可行的——这样可以更好地控制炮台的行为。
作为一个 Java mod,接下来就要讲述一些进阶的内容了。
TurretBuild类(ItemTurret类的父类) 实现了 ControlBlock接口,我们可以像其他炮台一样控制该炮台的旋转与子弹发射。
提出以下需求:

  1. 黑眼珠需要随着玩家的鼠标移动而移动
  2. 发射子弹时,身上会闪出红色充能

    眼珠旋转

    代码如下,每行代码都有对应的解释:

    1. public class EyeBuild extends ItemTurretBuild {
    2. // 眼珠运动的2D矢量,长度为3
    3. public Vec2 movement = new Vec2(3, 0);
    4. @Override
    5. public void draw() {
    6. float anchorX = x;
    7. float anchorY = y;
    8. Drawf.shadow(anchorX, anchorY - 12f, 40f);
    9. Draw.rect(bodyTR, anchorX, anchorY);
    10. Draw.z(Layer.turret + 1);
    11. // 这里以后为发生变动的内容
    12. // 考虑后坐力,来自原版代码
    13. tr2.trns(rotation, -recoil);
    14. // 整个眼球的位置
    15. float eyeBallX = anchorX + tr2.x;
    16. float eyeBallY = anchorY + 28f + tr2.y;
    17. // 绘制眼球
    18. Draw.rect(eyeBallTR, eyeBallX, eyeBallY);
    19. // 判断是否被玩家操控了
    20. if (isControlled()) {
    21. // 获取玩家单位
    22. Unit player = unit();
    23. // 得到玩家的 鼠标/手指 指向的世界坐标
    24. float aimX = player.aimX;
    25. float aimY = player.aimY;
    26. // 为眼珠运动矢量设置角度
    27. movement.setAngle(
    28. // 获得以(x,y)为起点,(aimX,aimY)到(x,y)矢量的角度
    29. Angles.angle(x, y, aimX, aimY)
    30. );
    31. } else {
    32. // 如果不是玩家控制时,眼珠随炮台的发射角度而旋转
    33. movement.setAngle(rotation);
    34. }
    35. // 绘制黑眼珠,考虑了眼珠转动
    36. Draw.rect(eyeTR,
    37. eyeBallX + movement.x,
    38. eyeBallY + movement.y
    39. );
    40. }
    41. }

    需要多说的是,Arc游戏引擎 为我们提供了很多实用的数学运算库——例如 Mathf类,Angles类,Interp接口/类,Mat类 和 Rand类 等;还为我们提供了各种数学结构——例如 Vec2类(二维矢量),Vec3类(三维矢量)。您可以合理使用这些数学库,使您的动画更加生动、有趣、有质感。
    单位(Unit) aimX字段aimY字段 是同步的,这表示:玩家在本地客户端的鼠标位置,在服务端是可以获得到的,并且是几乎一致 (双端同步的 Synchronized) ——这关乎您的mod是否能正常 在服务器上运行多人局域网联机

在这里我将 movement字段 声明在了 EyeBuild类 里,这代表:每个 眼球炮塔的方块实体(Building) 都会在内存中占用至少一个 Vec2对象 的大小。如果您对于 性能 和 内存占用 要求比较高的话,可以学习原版的做法——将 movement字段 声明在了 EyeTurret类 里,这将会让所有的 眼球炮塔的方块实体 都使用同一个 Vec2对象,会节省内存。但这是基于 渲染(Render)更新(Update) 是在同一个线程内进行的——是单线程的游戏,否则,大量使用这种”技巧”将带来 线程不安全数据不一致性
关于更多 Java线程 相关的知识,请参考:https://www.runoob.com/java/java-multithreading.html
《Java并发编程实战》(Java Concurrency in Practice)一书 等。
最终效果如下:
EyeTurret1.gif

充能

我们需要额外加载一张用于充能时绘制的贴图,作为 叠加层(Overlay) 覆盖在表面:

eye-turret-body-hot.png 充能贴图 eye-turret-body-hot.png

下方是 EyeTurret类 中加载充能时绘制的贴图。

  1. public TextureRegion hotBodyTR;
  2. @Override
  3. public void load() {
  4. // 省略已有的
  5. hotBodyTR = Core.atlas.find(name + "-body-hot");
  6. }

下方是 EyeBuild类 中是关于”如何根据过热程度修改 充能贴图 绘制时的alpha值”的代码。

  1. @Override
  2. public void draw() {
  3. float anchorX = x;
  4. float anchorY = y;
  5. Drawf.shadow(anchorX, anchorY - 12f, 40f);
  6. Draw.rect(bodyTR, anchorX, anchorY);
  7. //以下为新增的内容
  8. // 将炮台的heat字段设置为alpha值,heat是从[1f,0f]的float
  9. Draw.alpha(heat);
  10. // 渲染充能的贴图
  11. Draw.rect(hotBodyTR, anchorX, anchorY);
  12. // 清空颜色的设置
  13. Draw.color();
  14. //以上为新增的内容
  15. Draw.z(Layer.turret + 1);
  16. tr2.trns(rotation, -recoil);
  17. float eyeBallX = anchorX + tr2.x;
  18. float eyeBallY = anchorY + 28f + tr2.y;
  19. Draw.rect(eyeBallTR, eyeBallX, eyeBallY);
  20. if (isControlled()) {
  21. Unit player = unit();
  22. float aimX = player.aimX;
  23. float aimY = player.aimY;
  24. movement.setAngle(
  25. Angles.angle(x, y, aimX, aimY)
  26. );
  27. } else {
  28. movement.setAngle(rotation);
  29. }
  30. Draw.rect(eyeTR,
  31. eyeBallX + movement.x,
  32. eyeBallY + movement.y
  33. );
  34. }

最终效果如下:
EyeTurret2.gif

打包 Jar 与 测试 Test

接下来,确保以上代码和操作都无误且没有疏漏后,可以如 前文 一样执行 Gradle 的 Jar Task,等待 jar 打包完毕,并导入到游戏中测试。
检查您的方块是否正常地被加载到游戏中;贴图是否正确——而不是 “oh no”;是否显示了本地化名称;工厂是否能够正常运行和生产;是否能正确渲染阴影;是否能够正确地在有一定偏移位置的情况下渲染贴图;炮台是否能正确地消耗弹药和射出子弹;是否能正确地让眼珠根据鼠标的位置转动;是否能够正确地渲染 过热/充能 时的 叠加层(Overlay)

尾声

本章带您了解了 Mindustry Java Mod 的最常见的游戏内容——炮台和子弹,之后还会介绍更多的关于 Mindustry Modding 的内容。
如果您喜欢本教程,可以给 普冷姆的 Mod —— Cyber IO 点上一个 Star 哦qwq
开源项目地址 https://github.com/liplum/CyberIO

版权声明

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

彩蛋 Bonus

EyeTurretBonus.gif