如何查找自己需要的类型
在本章的学习中,我们已经接触并查阅了部分源代码。本节将系统地介绍查阅原版源代码的流程,以帮助建立自主查阅和理解代码的能力。
原版内容加载逻辑
原版中所有被注册的内容,其加载类基本位于mindustry.content包下,此包的所有内容类型我们都已经看过了,此处列表如下:
| 文件/文件夹名称 | 内容 |
|---|---|
Blocks.java | 原版所有方块,包括地板和覆盖层(Overlay) |
Bullets.java | 已废止 |
ErekirTechTree.java | Erekir星的科技树 |
Fx.java | 原版大部分特效 |
Items.java | 原版所有物品 |
Liquids.java | 原版所有流体 |
Loadouts.java | 四个在星球上发射的起始贴图的蓝图 |
Planets.java | 原版所有星球 |
SectorPresets.java | 原版所有区块预设 |
SerpuloTechTree.java | Serpulo星的科技树 |
StatusEffects.java | 原版所有状态效果 |
TeamEntries.java | 暂未使用 |
TechTree.java | 科技树的工具类,不是内容加载类 |
UnitTypes.java | 原版所有单位,但无人机和导弹在对应的方块或单位里定义 |
Weathers.java | 原版所有天气 |
../ai/UnitCommand.java | 原版所有单位命令 |
../ai/UnitStance.java | 原版所有单位姿态 |
v7之后,所有内容加载类的字段和方法都是静态的,在原版中由Vars.content.createBaseContent()统一调用,随后各模组的loadContent()方法被执行。
loadContent()执行后,内容加载并未完成。之后会依次执行所有内容的 createIcons()(原版内容的此方法在编译期执行)、init()、postInit()、loadIcon() 和 load() 方法,最后才执行Mod#init()。
根据方块找到合适的class
这里我们特指的内容类型是方块,因为其他类型的子类通常不需要特别查找。
要确定一个方块对应的类,首先需要获取方块的name属性。在桌面版或某些修改版中,name通常直接显示在方块的统计信息面板上,位于本地化名称的正下方。例如,初代核心的name是core-shard。接下来,在Blocks.java文件中搜索这个name,即可定位到其对应的类型。
如果在移动原版中,name未在游戏内直接显示,则可以在源代码文件夹/core/assets/bundle/bundle_zh_CN.properties文件中搜索该方块的当前中文译名,然后从对应条目的键名中获取其name。
Javadoc
Javadoc 是指一种特殊的注释标准,即以*开头的多行注释,有着给类、方法、字段或注解充当文档的功能。Block BulletType UnitType等字段较多的类均有较为详细的Javadoc。本教程之前大部分的字段介绍都是直接由DeepSeek翻译的Javadoc,是想向读者证明,现阶段AI翻译的Javadoc已经达到可堪一用的程度了。
并不是所有字段都有Javadoc,这种情况是由两种原因引起的:一是Anuke犯懒没写;二是这个字段的名称已经完全表明字段的用途了,例如GenericCrafter的ignoreLiquidFullness,即字面意思是否忽略流体充满。另一方面,你也要利用IDEA提供的“查找用法”功能,当你不知道某一字段用处的时候,可以看看原版内容是如何设置这一字段的。
根据类型查找所需的贴图
使用某些类型,特别是贴图导数非常多的类型,会让我们分不清要添加哪些贴图。
原版中所有能绘制的贴图可以用一个TextureRegion来表示,而这种贴图都需要在load()期从Core.atlas.find()中掏出来,所以我们可以直接观察内容的load()方法来研究需要什么名称的贴图,如果方块有drawer,那么也可以对drawer作同样处理。例如:
@Override
public void load(){
super.load();
laser = Core.atlas.find(name + "-beam", Core.atlas.find("power-beam"));
laserEnd = Core.atlas.find(name + "-beam-end", Core.atlas.find("power-beam-end"));
}这段摘自LongPowerNode.java的代码展示了如何加载贴图:该方块会尝试加载名为name + "-beam"的贴图作为激光中段;find()方法的第二个参数表示,如果找不到第一个参数指定的贴图,则会**回滚(Fallback)**到power-beam这张贴图。如果power-beam也找不到,则会回滚到error贴图(即ohno);如果连error贴图也找不到,则表明Core.atlas已完全损坏,游戏将崩溃。
仅靠手动加载有时不够便捷,原版还提供了一个功能强大的注解(Annotation)@Load。Java 注解(Annotation)是 JDK5.0 引入的一种注释机制。此处 Anuke 使用的并非基础的“运行期注解”,而是功能更复杂的“编译期注解”,它具备**元编程(Meta-programing)**能力,可以自动生成代码。@Load注解处理器是原版注解处理器中相对简单的一个,目前无需深究其原理,因为注解处理器是原版源代码中较为复杂的部分。
这里只需了解@Load各个参数的含义即可。如果只有一个参数,则该参数即为value,代表要加载的贴图“名称”,其中可以包含插值(Interpolation):@代表方块的名称,#、#1、#2等代表循环的数字;如果有多个参数,还可能包含fallback参数,用于指定回滚的目标贴图名称;length与#搭配使用,指示其循环长度;lengths与#1、#2等搭配使用,所有数字均从1开始计数。
比如:
//把“select-arrow-small”贴图加载到selectArrowRegion中
public @Load("select-arrow-small") TextureRegion selectArrowRegion;
//把“$name-glow”贴图加载到glow中
public @Load("@-glow") TextureRegion glow;
//把“$namw-launch-arrow”贴图加载到arrowRegion中,如果找不到就回滚到“launch-arrow”
public @Load(value = "@-launch-arrow", fallback = "launch-arrow") TextureRegion arrowRegion;
//把“$name-1”、“$name-2”、“$name-3”加载到regions[]中
public @Load(value = "@-#", length = 3) TextureRegion[] regions;
//把“$name-1-1” “$name-1-2” “$name-1-3” “$name-1-4” “$name-2-1” “$name-2-2” “$name-2-3” “$name-2-4” ………… “$name-7-4”加载到regions[][]中
public @Load(value = "@-#1-#2", lengths = {7, 4}) TextureRegion[][] regions;
//把“$name-bottom-1” “$name-bottom-2” …… “$name-bottom-5”加载到botRegions[]中,哪个找不到就用“duct-bottom-$i”代替
@Load(value = "@-bottom-#", length = 5, fallback = "duct-bottom-#") TextureRegion[] botRegions;你可以用Content的练习一下:
@Override
public void loadIcon(){
fullIcon =
Core.atlas.find(fullOverride == null ? "" : fullOverride,
Core.atlas.find(getContentType().name() + "-" + name + "-full",
Core.atlas.find(name + "-full",
Core.atlas.find(name,
Core.atlas.find(getContentType().name() + "-" + name,
Core.atlas.find(name + "1"))))));
uiIcon = Core.atlas.find(getContentType().name() + "-" + name + "-ui", fullIcon);
}请思考:物品的本体贴图到底有多少种命名方式?Mindustry每次大更新都没有模组负担,为什么Anuke还留着他们?
原版Bundle格式
关于语言的获取代码位于UnlockableContent中:
public UnlockableContent(String name){
super(name);
this.localizedName = Core.bundle.get(getContentType() + "." + this.name + ".name", this.name);
this.description = Core.bundle.getOrNull(getContentType() + "." + this.name + ".description");
this.details = Core.bundle.getOrNull(getContentType() + "." + this.name + ".details");
this.unlocked = Core.settings != null && Core.settings.getBool(this.name + "-unlocked", false);
}其中getContentType()就是上节提到的ContentType若干种。另外需要知道的是,在内容的name前面自动加modName是Vars.content.transformName()方法的功能。此时我们回顾一下各种内容的bundle格式就是极为合适的:
此外,前面也讲到一些不属于UnlockableContent的东西,他们的bundle是自己加载的。
command.tutorialUnitCommand = 演示单位命令
stance.tutorialUnitStance = 演示单位姿态
ability.turorialunitability = 演示单位能力
techtree.tutorial = 演示科技树并且我们会发现,这些内容的name并不再是连字符命名的,command和stance是小驼峰命名,而ability 干脆是类名全小写化,正如它的代码一样:
public String localized(){
return Core.bundle.get(getBundle());
}
public String getBundle(){
var type = getClass();
return "ability." + (type.isAnonymousClass() ? type.getSuperclass() : type).getSimpleName().replace("Ability", "").toLowerCase();
}其他我暂时不知道在哪里讲合适的东西
本节是一些原版比较偏难怪的功能,未来这些内容可能会分散在本章或后几章中。
一个莫斯科大学的数学教授跳槽到了哈佛,刚一抵达就被要求教数学分析,于是他跑去问其它教授:“这门课我该教些什么?”其他人告诉他:“教点极限、连续性、可微性,再加点不定积分就行了。”第二天,他又跑过来问其他教授,“那我第二堂课该教些什么呢?”
Env位掩码系统
在方块、单位等处中你均可以看到三个变量:
envRequired:要求当前地图环境符合该变量的所有环境需求;envEnabled:要求当前地图环境符合该变量的某个环境需求;envDisabled:要求当前地图不符合该变量的任何环境需求。
并且行星也有一个字段defaultEnv,默认值为Env.terrestrial | Env.spores | Env.groundOil | Env.groundWater | Env.oxygen。
但是这些字段的值都是int类型的,意味着他们只是数字而已,事实也是如此,defaultEnv的值为0b01110101,也就是117。这说明,这里采用了某种机制在数字中隐藏了信息,实际上,这里使用的方法是位掩码(Bitmask),通过把二进制数的某位设置为0或1来对标记进行设置。比如说,原版中所有env是这样定义的:
public class Env{
public static final int
//处在星球上
terrestrial = 1,
//在太空中,没有大气层
space = 1 << 1,
//在水下,首先要在星球上
underwater = 1 << 2,
//有孢子
spores = 1 << 3,
//环境就像火焰山
scorching = 1 << 4,
//有石油
groundOil = 1 << 5,
//有地下水
groundWater = 1 << 6,
//大气层中有氧气
oxygen = 1 << 7,
//所有环境,用来位掩码运算
any = 0xffffffff,
//没有环境
none = 0;
}具体来说,位掩码是这样操作的:
- 首先我们要获得当前地图环境的
env:Vars.state.rules.env(假设为默认值:0b01110101) - 然后对一个需要判断在当前地图环境下能否工作的方块或单位:
envRequired要么是空的,要么与env作“或”(Or)操作,结果为envRequired,说明要求的环境都是存在的;envEnabled与env作“与”(And)操作,结果不为空,说明要求的环境至少存在一个;envDisabled与env作“与”(And)操作,结果为空,说明禁止的环境都不存在。
原版只用了int的后八位,模组还有56位的发挥空间。
像这样的位掩码机制原版在存档机制中使用较多,毕竟存档是一个 的地方。此外,还有一种数据结构叫作BitSet(在Arc为arc.struct.Bits),设计出来专门就是存放大量0/1数据的。
Interp
与统计学上的同名概念或lerp不同,Arc(源自LibGDX)中的插值Interp是一种**补间(Tweening)**函数,通常定义为从[0,1]区间映射到[0,1]区间的函数,用于在两个值之间实现平滑过渡或创建视觉效果。在Java中,它以Lambda表达式的形式存在,位于arc.math.Interp类中,该类也提供了一些实用的补间函数。
插值函数的存在是必要的,因为人眼对匀速运动的感知并不自然,而非线性的补间函数可以使变化看起来更平滑。原版(同样源自LibGDX)提供的一些Interp示例可见于LibGDX wiki上的相关图示