가이드
모드 정리하기
모드는 한 번 작성하고 몇 년 동안 관리합니다. 관리에 가장 도움이 되는 것은 언어가 아니라, 모든 것이 어디에 있느냐입니다. 이 가이드는 CraftScript와 Kotlin 각각에 하나씩 구조를 제안하고 그 이유를 설명합니다.
네 가지 규칙
- 파일 하나에 책임 하나. 광석용 파일 하나, 조합법용 파일 하나, 명령어마다 파일 하나. 파일을 설명하는 데 “그리고”가 필요하다면, 아마 파일 두 개일 것입니다.
- 크기가 아니라 주제로 묶습니다.
content/,events/,commands/,ui/,lib/같은 폴더. 모드를 연 사람은 코드를 읽지 않고도 어디를 봐야 할지 압니다. - 데이터와 동작을 분리합니다. 존재하는 것(아이템, 블록, 조합법)은 속성 목록입니다. 일어나는 일(이벤트, 명령어)은 코드입니다. 둘을 섞으면 숫자 하나를 바꾸려 해도 로직을 읽어야 합니다.
- 연결만 하는 진입점.
main.craft나 모드 클래스는 어떤 부분이 있는지만 말합니다. 게임 규칙은 담지 않습니다.
이렇게 하면 모드의 밸런스를 바꿀 때는 content/recipes.craft를 수정하고, 명령어를 고칠 때는 commands/를 열면 됩니다. 이벤트의 버그가 블록 이백 줄 사이에 숨어 있지도 않습니다.
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의 작동 방식
import "content/items"는 그 파일을 한 번 실행합니다. 거기서 선언한 아이템, 블록, 이벤트, 명령어는main.craft에 있는 것처럼 모드의 일부가 됩니다.import { give_rubies } from "/lib/rewards"는 특정 이름을 가져옵니다./로 시작하는 경로는 모드 폴더 기준이므로 어느 하위 폴더에서든 똑같이 작동합니다./가 없으면 가져오는 파일 기준의 상대 경로입니다..craft확장자는 생략할 수 있습니다.import "lib/rewards" as rewards는 모듈 전체를 하나의 이름으로 가져옵니다(rewards.give_rubies(p, 1)).as가 없으면 이름은 파일 이름이므로, 같은 이름의 파일 두 개를 가져오면 서로 덮어씁니다. 다른 이름을 쓰거나as를 사용하세요.- 어떤 경로도 모드 폴더 밖으로 나갈 수 없으며, 순환 import는 위치를 알려 주는 오류를 냅니다.
공유 로직
두 파일에서 쓰는 함수는 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. 파일마다register함수를 가진object하나. - 모드 클래스는 연결만 합니다. 여러 부분이 공유하는 것(설정,
ContentBuilder)을 만들어 각 부분에 전달합니다. 어떤 부분도 스스로 무언가를 찾아오지 않으므로, 각 부분은 그 파일만 읽어도 이해할 수 있습니다. - id를 상수로, 그것을 등록하는 object 안에(
Items.RUBY). 오타는 컴파일 오류가 됩니다. - 저장 데이터는
storage/에: 각 키를Visits.of(player),Visits.add(player)같은 함수를 가진 object에 둡니다. 설정은Config클래스에 두고, 필요한 곳에 전달합니다.
Java에서
구조는 같고, object 대신 클래스를 씁니다. API는 Crafting.INSTANCE를 통해 사용하며, 리스너는 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: 모든 것을 시험해 보는 명령어.
몹 파일에는 그 몹의 선언만 들어갑니다. 이것이 드레이크입니다:
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로 옮깁니다. |
| 이벤트 하나가 서로 다른 세 가지 일을 합니다 | 같은 이벤트에 리스너 세 개를, 각각 주제에 맞는 파일에 둡니다. |