引言
本章主要介绍 创建新的 子弹类型(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;
@Override
public 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() {
}
@Override
public void load() {
super.load();
orbTR = Core.atlas.find(textureName);
}
}
你可以将 power-orb.png 放到在 assets/sprites文件夹 下的任何位置(可以放在bullets子文件夹下)
能量球 PowerOrb
先为能量球写个需求列表:
- 随着飞行的距离增加伤害
- 在能量球周围显示能量脉冲的粒子效果——这个效果将会在之后介绍
通过覆写 BulletType#update() 方法,我们可以在执行原来的更新内容后——即调用父类的 update() 方法,追加我们的内容。与此同时,我们还需要渲染加载的贴图。覆写的两个方法的代码如下:
public float dmgIncrease = 2f / 60f;
@Override
public void update(Bullet b) {
super.update(b);
b.damage += dmgIncrease;
}
@Override
public void draw(Bullet b) {
super.draw(b);
Draw.rect(orbTR,b.x,b.y);
}
接下来会省略很多步骤,需要您慢慢理解。
public class ModBullets implements ContentList {
public static PowerOrb powerOrb;
@Override
public 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:
// 获得物品 Item
Vars.content.items();
// 获得子弹类型 BulletType
Vars.content.bullets();
// 获得方块 Block
Vars.content.blocks();
// 获得液体 Liquid
Vars.content.liquids();
// 获得所有的 Content
Vars.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;
}
@Override
public void load() {
super.load();
// 加载对应的贴图
bodyTR = Core.atlas.find(name + "-body");
eyeBallTR = Core.atlas.find(name + "-eye-ball");
eyeTR = Core.atlas.find(name + "-eye");
}
// 这里需要覆写这个方法,因为Turret类默认会加上底座
@Override
public TextureRegion[] icons() {
return new TextureRegion[]{region};
}
public class EyeBuild extends ItemTurretBuild {
// 无需调用父类,进行自定义绘制
@Override
public 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;
// 两次发射间的CD
reloadTime = 60f;
// 攻击范围
range = 240f;
// 后坐力大小
recoilAmount = 3f;
}};
从此,您在以后的路上将会遇到琳琅满目的字段与方法,本教程无法为您一一说明。并且因存在着可能的版本差异,本教程只能为您提供最基础的入门,您必须学会通过IDEA的各种便捷功能查阅相关的API。
从此处以后,以下的提示将不会再出现,您必须时刻牢记且熟练这些方法。
如何查看一个类的定义?
如何查看一个类的父类们或子类们?
如何查看一个字段/方法在哪里被使用了?
如何查找我想要,但我不知道的东西?
现在我们已经能够正常发射子弹了,这得益于 ItemTurret类及其父类 已经为我们提供了很多有用的功能,如果您想要从零实现也是可行的——这样可以更好地控制炮台的行为。
作为一个 Java mod,接下来就要讲述一些进阶的内容了。
TurretBuild类(ItemTurret类的父类) 实现了 ControlBlock接口,我们可以像其他炮台一样控制该炮台的旋转与子弹发射。
提出以下需求:
- 黑眼珠需要随着玩家的鼠标移动而移动
-
眼珠旋转
代码如下,每行代码都有对应的解释:
public class EyeBuild extends ItemTurretBuild {
// 眼珠运动的2D矢量,长度为3
public Vec2 movement = new Vec2(3, 0);
@Override
public 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;
@Override
public void load() {
// 省略已有的
hotBodyTR = Core.atlas.find(name + "-body-hot");
}
下方是 EyeBuild类 中是关于”如何根据过热程度修改 充能贴图 绘制时的alpha值”的代码。
@Override
public void draw() {
float anchorX = x;
float anchorY = y;
Drawf.shadow(anchorX, anchorY - 12f, 40f);
Draw.rect(bodyTR, anchorX, anchorY);
//以下为新增的内容
// 将炮台的heat字段设置为alpha值,heat是从[1f,0f]的float
Draw.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.