引言
本章主要介绍 创建新的 子弹类型(BulletType) 与新的 炮台(Turret) 方块。
子弹类型(BulletType) 是一种 享元(Flyweight) 和 原型(Prototype) 的结合。
子弹(Bullet) 是游戏中的 实体(Entity),具有 位置信息(x,y字段)、速度、子弹类型(type字段)、额外信息(data字段)等。
子弹类型(BulletType) 是游戏中的 可注册类型,包含一个 子弹(Bullet)实体 的元信息,可以决定子弹的属性 (伤害,速度等),可以决定 子弹(Bullet)实体 如何渲染 和 如何更新 等等。
- 通过 子弹类型(BulletType) 可以在游戏中生成它所对应的 子弹(Bullet)实体 ——一个或多个,当你连续发射的时候,连续射击出来的子弹的 type 字段都引用了同一个 子弹类型(BulletType)。所以 子弹类型(BulletType) 是 子弹(Bullet)实体 的 享元(Flyweight)。
- 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数组 中实例化出此类。
代码如下:
package tutorial;import mindustry.ctype.ContentList;import mindustry.entities.bullet.BulletType;public class ModBullets implements ContentList {// 这个只是演示,后续会删除此行代码public static BulletType thuliumBullet;@Overridepublic void load() {// 这个只是演示,后续会删除此行代码thuliumBullet = new BulletType();}}
我们曾提到,因为静态字段的特殊性,您必须在静态字段被 赋值 (或初始化) 之后 访问(Access),所以您需要保证在 在 ModBlocks类 里 炮台方块(Turret) 对象被实例化前,ModBullets类 里的 所有所需 子弹类型(BulletType) 都已经被实例化,且赋值给相应的静态字段。
从此处起,除特殊情况外,本教程将不再提示您 添加 和 加载顺序,请您别忘记。
public class TutorialMod extends Mod {public static ModItems modItems;public static ModBullets modBullets;public static ModBlocks modBlocks;public static final ContentList[] modContents = {modItems = new ModItems(),modBullets = new ModBullets(),modBlocks = new ModBlocks(),};// 省略不重要的部分}
新的子弹类型 New Bullet Type
为了能更好地控制我们专属的 子弹类型(BulletType) 的更新与渲染,我们需要继承 BulletType类。
在 tutoiral.bullets包 下创建 PowerOrb类,覆写父类的 void load() 加载独特的贴图,代码如下:
public class PowerOrb extends BulletType {public TextureRegion orbTR;public String textureName = "";public PowerOrb(float speed, float damage) {super(speed, damage);}public PowerOrb() {}@Overridepublic void load() {super.load();orbTR = Core.atlas.find(textureName);}}
你可以将 power-orb.png 放到在 assets/sprites文件夹 下的任何位置(可以放在bullets子文件夹下)
能量球 PowerOrb
先为能量球写个需求列表:
- 随着飞行的距离增加伤害
- 在能量球周围显示能量脉冲的粒子效果——这个效果将会在之后介绍
通过覆写 BulletType#update() 方法,我们可以在执行原来的更新内容后——即调用父类的 update() 方法,追加我们的内容。与此同时,我们还需要渲染加载的贴图。覆写的两个方法的代码如下:
public float dmgIncrease = 2f / 60f;@Overridepublic void update(Bullet b) {super.update(b);b.damage += dmgIncrease;}@Overridepublic void draw(Bullet b) {super.draw(b);Draw.rect(orbTR,b.x,b.y);}
接下来会省略很多步骤,需要您慢慢理解。
public class ModBullets implements ContentList {public static PowerOrb powerOrb;@Overridepublic void load() {// 参数1:速度。参数2:伤害powerOrb = new PowerOrb(3.5f, 20f){{// 将 "power-orb" 添加当前的ModID作为前缀,变成 "tutorial-mod-power-orb"textureName = Vars.content.transformName("power-orb");// 子弹最大持续时长lifetime = 110;// 不使用任何粒子效果hitEffect = Fx.none;despawnEffect = Fx.none;shootEffect = Fx.none;smokeEffect = Fx.none;}};powerOrb.textureName = Vars.content.transformName("power-orb");((ItemTurret) Blocks.duo).ammoTypes.put(Items.copper, powerOrb);}}
下面这行代码非常特殊:
((ItemTurret) Blocks.duo).ammoTypes.put(Items.copper, powerOrb);
首先我们知道 Blocks类 里使用大量 静态字段 存放了 所有原版的 方块(Block) 的,其次在我们的mod进行 Mod#loadContent() 方法时,原版的所有 Content 都已经注册完毕了。因此,我们可以在这里访问 duo(双管炮),为它修改使用 铜(copper) 时发射出来的子弹的 子弹类型(BulletType)。
效果如下:
可注册类型 Content
我们在 上一节中覆写的 void load() 方法 源自于 Content(mindustry.ctype.Content)类,这就是 前文 中一直提到的 “可注册类型“,通过 IDEA的 类继承层次结构(Hierarchy) 窗口 我们可以发现 方块(Block),物品(Item),星球(Plant),天气(Weather),单位类型(UnitType)等,都是继承于 Content类 的。
在 Content类 的构造方法中,会调用”注册该 可注册类型”相关的方法,所以,您只需要实例化它们的对象,就会 Mindustry 就会自动为您注册该类型。它会为每个类型,添加您的 Mod ID 作为前缀,并检查是否存在命名冲突。
访问所有 Content
您可以通过下列几个方法,查询本游戏中所有已经注册过的Content:
// 获得物品 ItemVars.content.items();// 获得子弹类型 BulletTypeVars.content.bullets();// 获得方块 BlockVars.content.blocks();// 获得液体 LiquidVars.content.liquids();// 获得所有的 ContentVars.content.getContentMap();
您也可以通过以下代码,将 任何名称 转换为 添加了当前ModID为前缀的名称。
Vars.content.transformName("这里填写需要被转换的名称");
替换原版内容
我们在 上一节中修改了原版炮台 双管炮(duo) 的子弹,当然,这里只是为了测试,随后您可以删除本行代码。
但这也为您带来了一种思路——您可以以 调试 或 替换原版内容 为目的,修改原版的方块、物品等 Content 的一些字段,以此来改变某些行为。
眼球炮塔 EyeTurret
先上效果图
这是使用到的资源文件:
用于放置时的图标 eye-turret.png
身体 eye-turret-body.png
眼球 eye-turret-eye-ball.png
黑眼珠 eye-turret-eye.png
下面是 眼球炮塔(EyeTurret) 的代码:
// 继承 ItemTurret,消耗物品作为弹药public class EyeTurret extends ItemTurret {// 身体贴图public TextureRegion bodyTR;// 眼球贴图public TextureRegion eyeBallTR;// 黑眼珠贴图public TextureRegion eyeTR;public EyeTurret(String name) {super(name);// 不渲染阴影hasShadow = false;}@Overridepublic void load() {super.load();// 加载对应的贴图bodyTR = Core.atlas.find(name + "-body");eyeBallTR = Core.atlas.find(name + "-eye-ball");eyeTR = Core.atlas.find(name + "-eye");}// 这里需要覆写这个方法,因为Turret类默认会加上底座@Overridepublic TextureRegion[] icons() {return new TextureRegion[]{region};}public class EyeBuild extends ItemTurretBuild {// 无需调用父类,进行自定义绘制@Overridepublic void draw() {// 如何绘制基于您的设计float anchorX = x;float anchorY = y;// 绘制阴影Drawf.shadow(anchorX, anchorY - 12f, 40f);Draw.rect(bodyTR, anchorX, anchorY);Draw.z(Layer.turret + 1);float eyeBallX = anchorX;float eyeBallY = anchorY + 24.5f;Draw.rect(eyeBallTR, eyeBallX, eyeBallY);Draw.rect(eyeTR, eyeBallX, eyeBallY);}}}
这里需要提及的是:TextureRegion[] icons()方法 被 Turret类 覆写了,它会在最终生成图标的时候,将炮台底座的贴图给加上。所以我们需要覆写它,只使用我们提供的 TextureRegiond对象 拼成最终图标。
注意:这里传入的 TextureRegiond对象 必须完整的,不能使用被处理过大小的——比如是从一张 精灵表(Sprite Sheet) 里切割出来。
注册方块相关代码如下:
eyeTurret = new EyeTurret("eye-turret") {{requirements(Category.turret, new ItemStack[]{new ItemStack(ModItems.thuliumBar, 5)});health = 1200;// 设定弹药和其对应的子弹类型ammo(ModItems.thulium, ModBullets.powerOrb);// 弹药仓储量maxAmmo = 20;size = 3;// 两次发射间的CDreloadTime = 60f;// 攻击范围range = 240f;// 后坐力大小recoilAmount = 3f;}};
从此,您在以后的路上将会遇到琳琅满目的字段与方法,本教程无法为您一一说明。并且因存在着可能的版本差异,本教程只能为您提供最基础的入门,您必须学会通过IDEA的各种便捷功能查阅相关的API。
从此处以后,以下的提示将不会再出现,您必须时刻牢记且熟练这些方法。
如何查看一个类的定义?
如何查看一个类的父类们或子类们?
如何查看一个字段/方法在哪里被使用了?
如何查找我想要,但我不知道的东西?
现在我们已经能够正常发射子弹了,这得益于 ItemTurret类及其父类 已经为我们提供了很多有用的功能,如果您想要从零实现也是可行的——这样可以更好地控制炮台的行为。
作为一个 Java mod,接下来就要讲述一些进阶的内容了。
TurretBuild类(ItemTurret类的父类) 实现了 ControlBlock接口,我们可以像其他炮台一样控制该炮台的旋转与子弹发射。
提出以下需求:
- 黑眼珠需要随着玩家的鼠标移动而移动
-
眼珠旋转
代码如下,每行代码都有对应的解释:
public class EyeBuild extends ItemTurretBuild {// 眼珠运动的2D矢量,长度为3public Vec2 movement = new Vec2(3, 0);@Overridepublic void draw() {float anchorX = x;float anchorY = y;Drawf.shadow(anchorX, anchorY - 12f, 40f);Draw.rect(bodyTR, anchorX, anchorY);Draw.z(Layer.turret + 1);// 这里以后为发生变动的内容// 考虑后坐力,来自原版代码tr2.trns(rotation, -recoil);// 整个眼球的位置float eyeBallX = anchorX + tr2.x;float eyeBallY = anchorY + 28f + tr2.y;// 绘制眼球Draw.rect(eyeBallTR, eyeBallX, eyeBallY);// 判断是否被玩家操控了if (isControlled()) {// 获取玩家单位Unit player = unit();// 得到玩家的 鼠标/手指 指向的世界坐标float aimX = player.aimX;float aimY = player.aimY;// 为眼珠运动矢量设置角度movement.setAngle(// 获得以(x,y)为起点,(aimX,aimY)到(x,y)矢量的角度Angles.angle(x, y, aimX, aimY));} else {// 如果不是玩家控制时,眼珠随炮台的发射角度而旋转movement.setAngle(rotation);}// 绘制黑眼珠,考虑了眼珠转动Draw.rect(eyeTR,eyeBallX + movement.x,eyeBallY + movement.y);}}
需要多说的是,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)一书 等。
最终效果如下:
充能
我们需要额外加载一张用于充能时绘制的贴图,作为 叠加层(Overlay) 覆盖在表面:
充能贴图 eye-turret-body-hot.png
下方是 EyeTurret类 中加载充能时绘制的贴图。
public TextureRegion hotBodyTR;@Overridepublic void load() {// 省略已有的hotBodyTR = Core.atlas.find(name + "-body-hot");}
下方是 EyeBuild类 中是关于”如何根据过热程度修改 充能贴图 绘制时的alpha值”的代码。
@Overridepublic void draw() {float anchorX = x;float anchorY = y;Drawf.shadow(anchorX, anchorY - 12f, 40f);Draw.rect(bodyTR, anchorX, anchorY);//以下为新增的内容// 将炮台的heat字段设置为alpha值,heat是从[1f,0f]的floatDraw.alpha(heat);// 渲染充能的贴图Draw.rect(hotBodyTR, anchorX, anchorY);// 清空颜色的设置Draw.color();//以上为新增的内容Draw.z(Layer.turret + 1);tr2.trns(rotation, -recoil);float eyeBallX = anchorX + tr2.x;float eyeBallY = anchorY + 28f + tr2.y;Draw.rect(eyeBallTR, eyeBallX, eyeBallY);if (isControlled()) {Unit player = unit();float aimX = player.aimX;float aimY = player.aimY;movement.setAngle(Angles.angle(x, y, aimX, aimY));} else {movement.setAngle(rotation);}Draw.rect(eyeTR,eyeBallX + movement.x,eyeBallY + movement.y);}
打包 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

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