指南
组织模组
模组写一次,却要维护好几年。对维护帮助最大的不是语言,而是每样东西放在哪里。本指南分别为 CraftScript 和 Kotlin 推荐一种结构,并解释原因。
四条规则
- 每个文件只负责一件事。矿石一个文件,配方一个文件,每条命令各一个文件。如果描述一个文件需要用到“和”字,它多半应该是两个文件。
- 按主题分组,而不是按大小。使用
content/、events/、commands/、ui/和lib/这样的文件夹。任何人打开模组,不读代码也知道该去哪里找。 - 数据与行为分开。存在的东西(物品、方块、配方)是一组属性。发生的事情(事件、命令)是代码。把它们混在一起,改一个数字就得去读逻辑。
- 入口只负责连接各部分。
main.craft或模组类只说明有哪些部分,不包含游戏规则。
这样一来,重新平衡模组只需编辑 content/recipes.craft;修复命令只需打开 commands/;事件中的 bug 也不会藏在两百行方块定义之间。
CraftScript 中
mods/rubies/
├── main.craft the mod and the list of its parts
├── settings.craft settings for the server owner
├── content/ data: what exists
│ ├── items.craft
│ ├── ores.craft
│ ├── world.craft
│ ├── dimensions.craft
│ ├── creatures.craft
│ ├── sounds.craft
│ ├── particles.craft
│ └── recipes.craft
├── events/ behaviour: what happens
│ ├── mining.craft
│ ├── hunting.craft
│ └── welcome.craft
├── ui/ forms: what the player sees
│ └── shop.craft
├── commands/
│ ├── realm.craft
│ ├── rubies.craft
│ └── shop.craft
├── lib/ shared logic
│ ├── rewards.craft
│ └── storage.craft saved data: the keys, in one place
├── sounds/
│ └── ruby/chime.ogg
├── structures/
│ └── ruby_shrine.nbt
└── textures/
├── item/ruby.png
├── block/ruby_ore.png, ruby_log.png, ruby_leaves.png, ruby_flower.png
├── entity/ruby_golem.png
└── particle/ruby_spark_0.png, ruby_spark_1.pngmods/rubies/
只声明模组并列出它的各个部分。读一遍就知道有什么、在哪里。
mod "rubies" { name: "Rubies", version: "1.0.0", author: "you" }
// What exists
import "content/items"
import "content/ores"
import "content/world"
import "content/dimensions"
import "content/creatures"
import "content/sounds"
import "content/particles"
import "content/recipes"
// What happens
import "events/mining"
import "events/hunting"
import "events/welcome"
import "commands/realm"
import "commands/rubies"
import "commands/shop"导入的工作方式
import "content/items"会运行该文件一次。它声明的物品、方块、事件和命令成为模组的一部分,就像写在main.craft中一样。import { give_rubies } from "/lib/rewards"导入指定的名称。以/开头的路径从模组文件夹开始解析,所以在任何子文件夹中效果都一样;不带/则相对于导入它的文件。.craft扩展名可省略。import "lib/rewards" as rewards以一个名称导入整个模块(rewards.give_rubies(p, 1))。不带as时,名称就是文件名,因此两个同名的导入文件会互相覆盖:请使用不同的名称或as。- 任何路径都不能离开模组文件夹,循环导入会报错并指出位置。
共享逻辑
被两个文件使用的函数放在 lib/ 中。如果逻辑有自己的状态(经济系统、任务队列),就用带方法的类,同样放在 lib/ 中;见 CraftScript。只被一个文件使用的变量留在该文件中,但要记住它会在重启后丢失:需要长期保留的内容应放进存档数据。
设置与存档数据各占一个文件
有两样东西模组的其他部分会用到,但不应了解其内部细节:
- 设置(根目录下的
settings.craft):一个包含所有默认值的config(…)。其他文件导入SETTINGS并读取所需的值;没有任何文件把平衡数值写死。 - 存档数据(
lib/storage.craft):键以及几个小函数,比如visits(p)和add_visit(p)。统计访问次数的代码调用add_visit(player),并不知道rubies.visits这个键的存在。哪天它要改,只需改一处。
不要把这个文件命名为 config.craft:不带 as 导入时,它的名称会遮蔽 config 函数。
Kotlin 中
rubies/
├── build.gradle.kts
├── settings.gradle.kts
└── src/main/
├── kotlin/com/example/rubies/
│ ├── RubiesMod.kt entry point: only wires the parts
│ ├── RubiesConfig.kt player settings
│ ├── content/
│ │ ├── Items.kt
│ │ ├── Blocks.kt
│ │ ├── Creatures.kt
│ │ ├── Sounds.kt
│ │ ├── Particles.kt
│ │ └── Recipes.kt
│ ├── storage/
│ │ └── Visits.kt saved data: the keys, in one place
│ ├── events/
│ │ ├── MiningEvents.kt
│ │ ├── CombatEvents.kt
│ │ └── WelcomeEvents.kt
│ ├── ui/
│ │ └── ShopForm.kt forms: what the player sees
│ └── commands/
│ ├── RubyCommand.kt
│ └── ShopCommand.kt
└── resources/
├── crafting.mod.json
└── assets/rubies/
├── sounds/ruby/chime.ogg
└── textures/
├── item/ruby.png
├── entity/ruby_golem.png
└── particle/ruby_spark_0.png, ruby_spark_1.pngsrc/main/kotlin/com/example/rubies/
入口类本身不做任何事:它创建共享的部分并调用每个部分。
package com.example.rubies
class RubiesMod : CraftingMod {
override fun onLoad(ctx: ModContext) {
val config = Crafting.configs.load(RubiesConfig())
val content = ContentBuilder(ctx.metadata.id, Crafting.registries)
Items.register(content)
Blocks.register(content)
Creatures.register(content)
Sounds.register(content)
Particles.register(content)
Recipes.register(content)
MiningEvents.register(config)
CombatEvents.register(config)
WelcomeEvents.register()
RubyCommand.register()
ShopCommand.register()
}
}为便于阅读,省略了每个文件的 import 行。
- 按主题分包:
content、events、commands。每个文件一个object,带一个register函数。 - 模组类只负责连接各部分:它创建多个部分共享的东西(配置、
ContentBuilder)并传给它们。没有哪个部分自己去获取东西,所以只读它自己的文件就能理解它。 - id 作为常量,放在注册它们的 object 中(
Items.RUBY)。拼写错误会变成编译错误。 - 存档数据放在
storage/:每个键放在一个 object 中,配有Visits.of(player)和Visits.add(player)这样的函数。设置放在Config类中,传给需要它的部分。
Java 中
结构相同,只是用类代替 object。通过 Crafting.INSTANCE 使用 API,监听器返回 Unit.INSTANCE:
package com.example.rubies;
public final class RubiesMod implements CraftingMod {
@Override
public void onLoad(ModContext ctx) {
ContentBuilder content = new ContentBuilder(ctx.getMetadata().getId(), Crafting.INSTANCE.getRegistries());
Items.register(content);
WelcomeEvents.register();
}
}package com.example.rubies.events;
public final class WelcomeEvents {
public static void register() {
Crafting.INSTANCE.getEvents().subscribe(PlayerJoinEvent.class, EventPriority.NORMAL, false, null, e -> {
e.getPlayer().send("&aWelcome, " + e.getPlayer().getName() + "!");
return Unit.INSTANCE;
});
}
}完整示例:Frostlands
Frostlands 是一个按部分组织的 CraftScript 模组,用到了本指南介绍的大部分内容。它的 main.craft 只列出各个部分:
mod "frostlands" { name: "Frostlands", version: "1.0.0" }
// One file per part of the mod. Each part only declares what it is about.
import "content/materials" // frost crystals: their ore, the recipes made from them
import "content/gear" // armor, blade, wand, the frozen effect and the frost bolt
import "content/blocks" // the gate, a lamp, an ice chest, a frost pad
import "world/frostlands" // the frozen dimension
import "creatures/drake" // tame, ride and fly a frost drake
import "creatures/wisp" // wisps that shoot frost bolts at the undead
import "creatures/yak" // yaks to breed
import "controls/keys" // dash and roar keys
import "events/welcome" // a first greeting
import "commands/kit" // /frostkit for trying it all outcontent/materials:一种水晶、它的矿石,以及用它制作的配方。content/gear:护甲、一把冰冻之刃、一根带冷却、发射模组投射物的法杖,以及一个模组效果。content/blocks:一座传送门、一盏可由点击或红石点亮的灯、一个冰箱子和一个跳跃垫。world/frostlands:一个维度。creatures/drake:一个带有自己模型和平滑动画的生物;可以驯服、骑乘和飞行,按键还会咆哮。creatures/wisp:会射击亡灵的幽灵火焰。creatures/yak:可以繁殖的牦牛。controls/keys:冲刺和咆哮按键。events/welcome:初次问候。commands/kit:用来试玩所有内容的命令。
生物文件只声明那一个生物。这是 drake:
creature "drake" {
body: "minecraft:bee"
model: "entity/drake"
texture: "entity/drake"
size: [1.4, 1.1]
health: 30
speed: 0.4
tame_item: "frostlands:frost_crystal"
rideable: true
behaviour: [{ follow_owner: 1.4, distance: 10 }, { fly_around: 1 }, { look_at_players: 8 }, "look_around"]
spawns: [{ biomes: ["minecraft:ice_spikes"], weight: 3, group: [1, 1] }]
}这些属性的详细说明见生物。
何时拆分
| 信号 | 怎么做 |
|---|---|
| 一个文件超过大约 150 行 | 找出它混杂的主题,把它们分开。 |
| 你把同一个函数复制到了两个文件中 | 把它移到 lib/ 并导入。 |
| 同一个平衡数值出现在多处 | 在 settings.craft(或 Kotlin 中的 Config 类)中设为一项设置;如果不应被修改,就设为常量。 |
| 你在多个文件中写了同一个数据键 | 把它移到 lib/storage.craft,配上读取和保存两个函数。 |
| 一个事件做了三件不同的事 | 为同一事件写三个监听器,各自放在所属主题的文件中。 |