Skip to content

Java 语法初步与 ContentParser 解析

这一节不是让你“学会 Java”,而是让你看懂源码,并理解 JSON 是如何被解析成游戏内容的。只要你能读懂类、字段与方法,就能推断出 JSON 该怎么写。

Java 语法速通(只讲必要部分)

Java 里的“内容定义”通常是一个类,字段就是配置项。比如:

java
public class Drill extends Block{
    public int tier = 1;
    public float drillTime = 300f;
}

public 表示公开字段,int/float 是类型,右侧是默认值。JSON 里只要写同名字段即可覆盖默认值。注意 Java 的 float 常带 f 后缀,表示这是浮点数而不是整数。

extends 表示继承。Drill extends Block 的意思是“钻头拥有 Block 的所有字段”,所以你在 JSON 里看到的很多字段其实来自父类,比如 sizehealthrequirementssuper(name) 是调用父类构造器,确保父类先完成初始化。源码里出现 @Override 时,代表子类重写了父类方法;abstract 表示抽象类不能直接实例化,必须用具体子类。

方法是类里的“行为”,例如:

java
public void updateTile(){
    //每帧逻辑
}

构造器名字与类名相同,常用于创建时设定基础值:

java
public Drill(String name){
    super(name);
}

你在 core/src/mindustry/content/*.java 里看到的写法,经常是“匿名内部类”初始化:

java
new GenericCrafter("x"){ {
    craftTime = 60f;
}}

这一对大括号的含义就是“创建对象后马上设置字段”。JSON 本质上就是在做这件事。

源码里还会出现大量“箭头函数”写法,例如 u -> true() -> {}。这是 Java 的 Lambda 表达式,表示“把一段逻辑当作参数传入”。当你看到这些写法时,就应该意识到这是函数类型字段,JSON 通常无法配置,只能用 JS/Java 处理。

数组与列表也经常出现。Java 里 ItemStack[] 是数组,Seq<ItemStack> 是列表。JSON 里通常用数组表示,比如 "requirements": ["copper/10", "lead/10"]ObjectMap 一类的映射通常写成“键值对对象”。

你还会看到 @Nullable 注解或字段默认值为 null。这类字段通常是“可选项”,JSON 可以不写;但一旦写,就必须符合类型。例如 Effect 字段允许写原版名称字符串或自定义对象,如果字段为 null,游戏就会走默认行为。理解“可选项”能减少不必要的配置,也能避免写错类型导致报错。

源码里常见的“容器构造”也值得一看,例如:

java
requirements = ItemStack.with(Items.copper, 50, Items.lead, 30);
consumes.add(new ConsumeItems(new ItemStack(Items.graphite, 2)));

它们在 JSON 中对应 "requirements": ["copper/50", "lead/30"]consumes.items 的写法。看到这些语句时,你就能反推 JSON 的结构。

如果你看到 staticfinalprivate 这些关键字,可以简单理解为“静态”“不可变”“私有”,它们通常不需要也不应该在 JSON 里配置。你只需要关注公开字段与构造器里对字段的默认赋值。

this 指向当前对象,this.field 只是更明确地引用字段;super.fieldsuper.method() 则是访问父类成员。读源码时不必被这些写法吓到,它们只是告诉你“值来自当前类还是父类”。JSON 里不需要写这些关键字,只需要写字段本身。

把 Java 写法翻译成 JSON 是最实用的练习。例如 Java 中常见的写法:

java
new GenericCrafter("silicon-smelter"){{
    craftTime = 60f;
    outputItem = new ItemStack(Items.silicon, 1);
    consumes.power(0.5f);
}}

对应的 JSON 大致是:

json
{
	"type": "GenericCrafter",
	"name": "silicon-smelter",
	"craftTime": 60,
	"outputItem": "silicon/1",
	"consumes": {
		"power": 0.5
	}
}

只要你能把“匿名内部类里的字段赋值”翻译成 JSON 键值对,就已经掌握了 70% 的阅读技巧。

再注意 Java 里常见的 Items.copperLiquids.waterBlocks.mechanicalDrill 这类写法,它们都是静态字段引用。换到 JSON,你要写的是内部名 copperwatermechanical-drill。这一点是“源码到 JSON”最常见的转换。

同理,单位武器的写法也可以直接翻译:

java
weapons.add(new Weapon(){{
    x = 4f; y = 1f;
    reload = 30f;
    bullet = new BasicBulletType(3f, 12);
}});

对应 JSON 的结构就是:

json
{
	"weapons": [
		{
			"x": 4,
			"y": 1,
			"reload": 30,
			"bullet": {
				"type": "BasicBulletType",
				"speed": 3,
				"damage": 12
			}
		}
	]
}

这类“从 Java 还原 JSON”的练习做多了,就会形成直觉。

ContentParser 在做什么

ContentParser 是 JSON 解析器,它把 JSON 字段逐个写进 Java 对象。它遵循“字段同名映射”的基本规则:JSON 的键名必须和 Java 字段名一致,类型也要对应。如果类型不一致,游戏会直接报错。

解析器还会根据文件夹推断内容类型:content/blocks 会被当成方块,content/units 会被当成单位。文件名就是内部名,所以文件夹与文件名写错,经常会导致“找不到内容”或“类型不匹配”的报错。

type 是最关键的字段之一。方块的 type 决定 Java 类(如 GenericCrafterDrillPowerNode),单位的 type 决定构造器(如 flying/mech/legs/naval/payload/missile/tank/hover/tether/crawl),而子类对象(子弹、能力、特效等)也依赖 type 指定具体子类。你可以把它理解成“告诉解析器该用哪个类来建对象”。

从源码角度看,解析器的大致流程类似这样(伪代码):

java
var type = json.getString("type", defaultType);
var block = makeBlock(type, name);
readFields(block, json);

因此当你写错 type,解析器甚至无法创建对象,后续字段都会失效。

type 的作用不仅体现在方块与单位上,drawerpartsBulletTypeAbility 等复杂对象也依赖它来决定具体子类。比如 DrawTurretDrawMultiRegionPartHaloPart 都是靠 type 识别的。如果你忘了写 type 或写错大小写,解析器就会回退到默认类型,表现与预期完全不同。

解析顺序也很重要。以方块为例,解析器会先读取 bundle 文案,然后解析 consumes(因为它是特殊语法),接着再把剩余字段逐个写入对象。单位则会先处理 requirements(用于挂载工厂与重构厂),再处理 AI 与控制器字段,最后写入剩余字段。理解这个顺序可以帮助你避免“字段被覆盖”或“写了不生效”的情况。

解析完成后,类的 init()load() 会在后续流程中执行,这一步可能继续改字段或加载贴图。比如某些方块在 init() 里会根据 size 自动计算 itemCapacity,这意味着你在 JSON 里写的值可能被逻辑覆盖。看懂这些“后置逻辑”,才能解释“写了却不生效”的现象。

如果 JSON 没有写某个字段,解析器就会保留 Java 默认值。这也是为什么“极简 JSON”能跑起来:默认值已经把大部分东西填好了。但当你使用 template 或覆盖原版时,默认值可能来自模板而不是原版,所以在关键字段上最好显式写出,避免被继承值影响。

解析器使用反射写字段,这意味着父类的 public 字段同样可写。比如 Block 里定义的 sizehealthrequirementscategory 都可以直接写在任意方块的 JSON 里。只要字段是 public 并且类型可解析,就能被写入。

常见的 JSON 简写

ContentParser 内置了多种简写语法。例如 "copper/10" 会被解析为 ItemStack"water/0.1" 会被解析为 LiquidStack,载荷堆栈则可以用 "block-or-unit/amount" 的形式表示。简写很方便,但只能表达“名称 + 数量”,一旦需要额外参数(例如 boosteroptional),就必须改用对象形式。

EffectBulletTypeDrawPart 等复杂对象也有简写规则。例如 Effect 可以直接写原版 Fx 名字,或者写一个对象并用 type 指定子类。BulletType 可以写字符串引用原版子弹,也可以写对象并指定 type 创建自定义子弹。理解这些规则后,你会发现 JSON 其实比模板更灵活。

颜色与向量也有常见写法。颜色可以写成十六进制字符串(如 ffffff25C9AB80),解析器会自动识别透明度;向量类字段往往支持数组或对象形式,例如 x/y[x, y]。这些细节虽然不影响逻辑,但能显著影响表现,尤其是子弹与特效参数。

读源码时还要注意“时间单位”。Java 里很多逻辑都是以“刻”为单位(Time.delta 是每帧增量),所以你在源码里看到的 reload = 60fcraftTime = 120f 实际对应 1 秒和 2 秒。把这个换算带回 JSON,有助于你把数值写得更直观。

容器类型也有对应关系:Seq 与数组一一对应,ObjectSet 通常对应“去重数组”,ObjectMap 则对应 JSON 对象的键值结构。看到这类类型时先想“它在 JSON 里长什么样”,再决定是否用数组或对象写法。

另外,一些字段本质上是整数(例如 sizetier),虽然 JSON 支持小数,但写成整数更稳妥,避免因为隐式转换造成困惑。

consumes 不是普通字段

consumes 是特殊语法,它不会直接写进字段,而是被解析成“消耗器”。常见的键包括 itemsliquidspoweritemFlammableitemExplosivecoolant 等。你在 JSON 里写的消耗器最终会变成 Consume* 对象,决定方块的效率与输入逻辑。也就是说,consumes 更像“配方逻辑”,而不是单纯的“字段赋值”。

不同消耗器还有不同的写法限制。itemsliquids 支持数组或对象形式,coolantliquid 如果需要 optionalboosterupdate 之类参数,就必须写对象;powerpowerBuffered 则决定是持续耗电还是从缓冲中抽取。理解这些差异,可以避免“写了不生效”的常见坑。

类似的“特殊解析”还有 requirementsresearchrequirements 本质是 ItemStack[],解析器允许 "copper/10" 这种简写,也允许对象写法;research 则是一个带 parentobjectives 的对象,里面会出现 SectorCompleteOnSector 等目标类型。只要目标复杂,就应该回到源码或 Java 教程核对字段名。

现实中的写法可以直接参考成熟模组。“饱和火力 3.3.0”的“离子钻头”在 consumes.liquid 里同时使用 optionalbooster,这类对象写法是 ContentParser 明确支持的。看到类似配置时,你就能反推出“为什么需要对象而不是简写”。

requirements 是“建造成本”,consumes 是“运行成本”,两者用途完全不同。很多新人会把运行消耗写进 requirements,导致方块只在建造时消耗一次,运行时不耗料。理解这条分界线,会让你的配方更符合玩家预期。

覆盖与模板

如果 JSON 文件名与原版内容内部名相同,且不写 type,解析器会认为你是在“覆盖原版”。单位还有一个 template 字段,可以用现成单位作为模板,再覆盖少量字段。这些机制能减少重复配置,但也更容易出错,建议熟悉后再使用。

模板继承的另一个常见陷阱是“忘记重写关键字段”。比如你用模板继承了某个单位,但忘了改 weaponsabilities,就会得到一个“外形改了但行为没变”的单位。research 字段也是类似:它本质上是一个对象,里面包含 parentobjectives,后者往往涉及 SectorComplete 等复杂目标,这些结构在 JSON 里能写,但写错一个键就会整个解锁链失效。

常见报错的读法

No content found with name 'xxx' 通常是内部名拼写错误或内容解析失败;Unknown consumption type 表示 consumes 里写了不支持的键;Invalid unit type 说明单位 type 拼写有误;Class not found 则意味着 type 指向了不存在的类或类名不完整。遇到这些报错时,先检查拼写,再检查解析器支持的类型,再看原版有没有对应内容。

还有一些更“类型相关”的报错,比如 Expected objectCannot parse,通常意味着你把对象写成了字符串,或把数组写成了对象。遇到这类问题时,回到类定义看字段类型是最直接的解决方案。另外,报错通常会带文件名和行号,建议先定位那一行,而不是盲目翻全文件。

字段名大小写也会导致报错。JSON 的键名必须和 Java 字段名完全一致,reloadTimereloadtime 就会被当成两个不同字段。内容内部名同样区分大小写,连字符也不能漏掉。遇到 No content found 这类问题时,优先检查拼写。

还有一种“不会报错但看起来不对”的情况:你写了字段,但 UI 没有对应显示。这通常是因为 setStats()setBars() 没有为该字段添加展示,或者展示逻辑基于其他条件。遇到这种问题时,应该回到类定义看 UI 逻辑,而不是继续调数值。

小结

只要能看懂 Java 类与字段,就能写对 JSON。ContentParser 决定了“哪些写法被允许”,遇到问题时先看报错,再查解析器,效率会比盲猜高得多。建议多做“源码到 JSON”的逆向练习,熟悉之后写配置会非常顺手。把源码当成说明书,就不会被旧教程误导,学习曲线也会更平滑,后续进阶到 JS/Java 也会更轻松。等你形成自己的“字段词典”,写 JSON 会像在写一份更短的 Java,这时你已经具备独立读源码的能力,也更容易快速定位问题,改数值会更快,整体效率会更高也更稳定、更省心、更顺畅。