Приветствую моддер, я не нашел в интернете подробного гайда по данной теме, поэтому эта статья - способ обобщить мой опыт и поделиться им с другими. В основном статья предназначена для начинающих, но будет не лишним и для тех кто хочет напомнить себе работу со структурами при моддинге. Позже я буду делать её более подробной, либо писать дополнения к этой статье.
Стек: Minecraft 1.20.1, Forge 47.0.+, Litematica, IntelliJ IDEA.
Подготовка
Прежде чем переходить к написанию кода начнем с подготовки самого строения. И для этого есть несколько методов:
-
Метод 1: Structure Block. Хорошо для начала.
Структурный блок подходит для небольших структур, всё из-за главной проблемы: Ограничения по размеру сохранения до 64x256x64 блоков, где первое число - это ширина (ось X), второе - высота (ось Y), и третье - глубина (ось Z).
-
**Метод 2: Litematica ** ( на котором мы остановимся). Что это? Специальный инструмент для строительства, редактирования, создания и экспорта схематик в формате
.litematic
или ванильномnbt
.Рабочий процесс: 1. Устанавливаем Litematica (и MaLiLib) как обычный мод. 2. Строим в креативном мире нашу мега-структуру. 3. С помощью инструментов Litematica выделяем ее и сохраняем в формате
.litematic
. 4. Ключевой шаг: Экспорт, необходимо зайти в меню сохраненных схем, выбрать нужную и в нижней панели выбрать ванильный формат (nbt
).Финальный шаг: Копируем полученный .nbt файл из папки
chematics
в корне самой игры, вставляем в ресурсы мода по путиsrc/main/resources/data/modid/structures/my_test_example.nbt
.
Начало
Для того, чтобы наша структура работала и генерировалась в мире, нам понадобится подключить Json файлы, настроить (об этом потом) их содержимое к нашему nbt
файлу и зарегистрировать в java класс - так происходит работа с привычным и в какой-то степени классическим Jigsaw-подходом.
Json файлы необходимые в этом случае:
template_pool.json
с одним элементом (single_pool_element), который указывает на наш файл .nbt иstructure.json
иstructure_set.json
Последние два файла содержат информацию о структуре (биом для генерации, частота генерации, уникальный номер, расположение к рельефу и т.д). Jigsaw отличный выбор для небольших структур, но есть и минусы, особенно с большими структурами. В моем случае minecraft даже не смог сгенерировать строение. Анализ проблемы: Этот метод метод был разработан не для размещения одного гигантского объекта, а для композиции структуры из множества мелких и средних частей. Например, деревня состоит из 10-20 небольших.nbt
(дома, дороги, колодцы), и система эффективно справляется с их последовательным размещением. Когда Jigsaw-система решает разместить элемент из пула (например, черезminecraft:single_pool_element)
, она загружает весь указанный.nbt
файл в память, чтобы вставить его в мир. Отсутствие внятных ошибок: Если вы ошиблись, игра в 99% случаев просто промолчит. В логах не будет ошибки "файл не найден". Команда /locate будет говорить, что такой структуры не существует, и вы будете часами искать опечатку. Это крайне болезненный процесс отладки.
Решение: Программная генерация через свой класс Structure
Вместо того чтобы полагаться на JSON-конфигурацию для загрузки .nbt
, мы напишем свой собственный Java-класс, который будет управлять процессом генерации. Это дает нам полный контроль.
Шаг 1: Создаем свой класс Structure
Создайте файл
MyGiantStructure.java
, который наследуется от net.minecraft.world.level.levelgen.structure.Structure.
public class MyGiantStructure extends Structure {
// Кодек для сериализации/десериализации нашей структуры
public static final Codec<MyGiantStructure> CODEC = simpleCodec(MyGiantStructure::new);
public MyGiantStructure(StructureSettings settings) {
super(settings);
}
// Это сердце нашего метода!
@Override
public Optional<GenerationStub> findGenerationPoint(GenerationContext context) {
// Логика поиска подходящего места для спавна.
// Для простоты можно просто выбрать центр чанка.
BlockPos centerOfChunk = context.chunkPos().getMiddleBlockPosition(0);
// Возвращаем точку генерации
return Optional.of(new GenerationStub(centerOfChunk, (builder) -> {
// Здесь мы будем вызывать саму генерацию
this.generate(builder, context);
}));
}
// Метод, который будет размещать нашу структуру в мире
private void generate(StructurePiecesBuilder builder, GenerationContext context) {
BlockPos pos = builder.getCenter(); // Получаем позицию из GenerationStub
ResourceLocation location = new ResourceLocation(MyMod.MODID, "my_giant_castle");
// Добавляем "кусок" нашей структуры. Так как она одна, кусок будет один.
builder.addPiece(new MyGiantStructurePiece(context.structureTemplateManager(), location, pos));
}
@Override
public StructureType<?> type() {
return ModStructureTypes.MY_GIANT_STRUCTURE.get(); // Ссылка на наш зарегистрированный тип
}
}
Шаг 2: Создаем класс "куска" структуры (StructurePiece).
Создайте MyGiantStructurePiece.java, который наследуется от TemplateStructurePiece. Этот класс будет отвечать за реальное размещение блоков из .nbt.
Логика здесь довольно стандартна, она просто загружает .nbt и размещает его.
public class MyGiantStructurePiece extends TemplateStructurePiece {
public MyGiantStructurePiece(StructureTemplateManager manager, ResourceLocation location, BlockPos pos) {
super(ModStructurePieceTypes.MY_GIANT_PIECE.get(), 0, manager, location, location.toString(), new StructurePlaceSettings(), pos);
}
// Конструктор для загрузки из NBT
public MyGiantStructurePiece(ServerLevel level, CompoundTag tag) {
super(ModStructurePieceTypes.MY_GIANT_PIECE.get(), tag, level.getServer().getStructureManager(), (location) -> new StructurePlaceSettings());
}
@Override
protected void handleDataMarker(String function, BlockPos pos, ServerLevelAccessor level, RandomSource random, BoundingBox sbox) {
// Можно оставить пустым, если у вас нет дата-блоков
}
}
Шаг 3: Регистрация всего этого в Forge.
Покажите, как теперь выглядит регистрация StructureType (он ссылается на кодек нашего нового класса).
Добавьте регистрацию StructurePieceType.
// В классе ModStructureTypes.java
public static final RegistryObject<StructureType<MyGiantStructure>> MY_GIANT_STRUCTURE =
STRUCTURE_TYPES.register("my_giant_structure", () -> () -> MyGiantStructure.CODEC);
// В отдельном классе ModStructurePieceTypes.java
public static final DeferredRegister<StructurePieceType> PIECE_TYPES = ...
public static final RegistryObject<StructurePieceType> MY_GIANT_PIECE =
PIECE_TYPES.register("my_giant_piece", () -> MyGiantStructurePiece::new);
Конфигурация в JSON
Теперь нам хватит двух файлов. template_pool.json
- удаляем, он больше не нужен.
structure.json
: Становится тривиальным. Его единственная задача — указать на наш зарегистрированный тип структуры и задать биомы/категорию спавна.
"type": "mymod:my_giant_structure",
"biomes": "#minecraft:is_overworld",
"step": "surface_structures",
"spawn_overrides": {}
}
structure_set.json
: Остается таким же, он все еще отвечает за частоту и расстояние между структурами.
Финал
Основная настройка структуры подходит к концу, и чтобы подытожить и помочь я продемонстрирую как выглядят каркасы java файлов.
*MyGiantStructure.java
. Этот класс решает, где разместить структуру, и инициирует процесс строительства.
// src/main/java/com/yourname/mymod/world/structure/MyGiantStructure.java
package com.yourname.mymod.world.structure;
import com.mojang.serialization.Codec;
import com.yourname.mymod.MyMod; // Замените на ваш главный класс мода
import com.yourname.mymod.world.ModStructureTypes; // Замените на ваш класс регистрации типов структур
import net.minecraft.core.BlockPos;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.world.level.levelgen.structure.Structure;
import net.minecraft.world.level.levelgen.structure.StructureType;
import net.minecraft.world.level.levelgen.structure.pieces.StructurePiecesBuilder;
import java.util.Optional;
/**
* Главный класс нашей структуры. Он выступает в роли "архитектора".
* Его задача - найти подходящее место для генерации и дать команду на начало "строительства".
*/
public class MyGiantStructure extends Structure {
/**
* Codec - это механизм сериализации/десериализации от Mojang.
* Он нужен, чтобы Minecraft мог сохранять и загружать информацию о нашей структуре.
* simpleCodec использует конструктор класса для создания экземпляра.
*/
public static final Codec<MyGiantStructure> CODEC = simpleCodec(MyGiantStructure::new);
/**
* Конструктор. Принимает настройки (биомы, шаг генерации и т.д.),
* которые обычно загружаются из structure.json.
*/
public MyGiantStructure(StructureSettings settings) {
super(settings);
}
/**
* Это сердце нашего класса. Метод вызывается для каждого чанка, чтобы определить,
* можно ли здесь начать генерацию нашей структуры.
* @return Optional.of(...) если место подходит, Optional.empty() если нет.
*/
@Override
public Optional<GenerationStub> findGenerationPoint(GenerationContext context) {
// Находим подходящую точку для старта. Для простоты возьмем центр чанка.
// Вы можете добавить сюда сложную логику, например, проверку высоты ландшафта.
BlockPos startPos = new BlockPos(context.chunkPos().getMinBlockX(), 90, context.chunkPos().getMinBlockZ());
// Возвращаем "заглушку" для генерации. Это "обещание" построить структуру.
// Сама генерация происходит в лямбда-функции.
return Optional.of(new GenerationStub(startPos, (piecesBuilder) -> {
this.generatePieces(piecesBuilder, context, startPos);
}));
}
/**
* Этот метод создает и добавляет "куски" (pieces) нашей структуры в мир.
* В нашем случае кусок всего один.
*/
private void generatePieces(StructurePiecesBuilder piecesBuilder, GenerationContext context, BlockPos startPos) {
// Указываем путь к нашему .nbt файлу.
// Он должен лежать в `src/main/resources/data/mymod/structures/my_giant_castle.nbt`
ResourceLocation location = new ResourceLocation(MyMod.MODID, "my_giant_castle");
// Создаем и добавляем единственный "кусок" нашей структуры, передавая ему все необходимые данные.
piecesBuilder.addPiece(new MyGiantStructurePiece(
context.structureTemplateManager(),
location,
startPos
));
}
/**
* Возвращает зарегистрированный тип этой структуры.
* Это нужно, чтобы Minecraft мог идентифицировать её.
*/
@Override
public StructureType<?> type() {
// ModStructureTypes.MY_GIANT_STRUCTURE - это RegistryObject, который вы создадите в отдельном классе.
return ModStructureTypes.MY_GIANT_STRUCTURE.get();
}
}
*MyGiantStructurePiece.java
. Этот класс — "строительная бригада". Он берет конкретный .nbt файл и размещает его блоки в мире.
// src/main/java/com/yourname/mymod/world/structure/MyGiantStructurePiece.java
package com.yourname.mymod.world.structure;
import com.yourname.mymod.world.ModStructurePieceTypes; // Замените на ваш класс регистрации типов кусков
import net.minecraft.core.BlockPos;
import net.minecraft.nbt.CompoundTag;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.util.RandomSource;
import net.minecraft.world.level.ServerLevelAccessor;
import net.minecraft.world.level.levelgen.structure.BoundingBox;
import net.minecraft.world.level.levelgen.structure.TemplateStructurePiece;
import net.minecraft.world.level.levelgen.structure.templatesystem.StructurePlaceSettings;
import net.minecraft.world.level.levelgen.structure.templatesystem.StructureTemplateManager;
/**
* Класс-"кусок" нашей структуры. Он отвечает за фактическое размещение блоков из .nbt файла.
* Наследуется от TemplateStructurePiece, который содержит всю логику для работы с шаблонами.
*/
public class MyGiantStructurePiece extends TemplateStructurePiece {
/**
* Конструктор, который мы вызываем при первоначальной генерации мира.
* @param templateManager Менеджер для загрузки .nbt файлов.
* @param location Путь к нашему .nbt файлу.
* @param pos Позиция, где будет размещена структура.
*/
public MyGiantStructurePiece(StructureTemplateManager templateManager, ResourceLocation location, BlockPos pos) {
// Вызываем конструктор родительского класса, передавая все необходимые параметры.
super(
ModStructurePieceTypes.MY_GIANT_PIECE.get(), // Зарегистрированный тип этого "куска".
0, // genDepth, глубина генерации (для Jigsaw).
templateManager,
location, // ResourceLocation нашего .nbt
location.toString(), // Имя шаблона, можно использовать то же самое.
new StructurePlaceSettings(), // Настройки размещения (поворот, зеркалирование и т.д.).
pos // Позиция.
);
}
/**
* Второй конструктор. Он необходим для загрузки структуры из сохраненного мира.
* Minecraft не генерирует структуры заново при загрузке, а считывает их из NBT-данных чанка.
* @param serverLevel Мир
* @param tag NBT-данные этого "куска"
*/
public MyGiantStructurePiece(net.minecraft.server.level.ServerLevel serverLevel, CompoundTag tag) {
super(ModStructurePieceTypes.MY_GIANT_PIECE.get(), tag, serverLevel.getServer().getStructureManager(), (location) -> new StructurePlaceSettings());
}
/**
* Этот метод вызывается для обработки специальных "блоков данных" (Data Structure Blocks) в вашем .nbt.
* Они позволяют выполнять команды или спавнить сущностей.
* Если вы их не используете, можно оставить этот метод пустым.
*/
@Override
protected void handleDataMarker(String function, BlockPos pos, ServerLevelAccessor level, RandomSource random, BoundingBox sbox) {
// Пример:
// if (function.equals("spawn_zombie")) {
// level.addFreshEntity(new Zombie(level.getLevel()));
// }
}
}
Не забудьте добавить "регистрацию" в главный файл mods.toml
:
Если бы у вас был замок и, скажем, маленькая хижина (small_hut), ваш mods.toml выглядел бы так:
# ... (остальная часть файла)
[[features]]
type = "minecraft:structure"
id = "mymod:my_giant_castle"
[[features]]
type = "minecraft:structure"
id = "mymod:small_hut"
Без этого блока Forge при запуске просто не знает, что ему нужно заглянуть в папку data/mymod/worldgen/structure
и поискать там файлы для загрузки. Он проигнорирует их.
Заключение
Путь от идеи до первого сгенерированного замка оказался куда длиннее и извилистее, чем я ожидал. Сначала я уперся в стену производительности стандартного Jigsaw-метода. Потом, написав, как мне казалось, идеальный код, я часами искал проблему, которая крылась не в сложной логике Java, а в одной-единственной забытой строчке в mods.toml.
Этот опыт научил меня двум вещам. Во-первых, для нестандартных задач нужны нестандартные подходы. Программная генерация через собственный класс Structure — это именно такой подход, дающий гибкость и производительность там, где пасуют стандартные конфигурации. Во-вторых, самый важный навык моддера — это не столько умение писать код, сколько умение его отлаживать и методично искать причину, когда что-то идет не так.
Надеюсь, мой опыт сэкономит вам несколько часов (или даже дней) отладки и покажет, что даже самые большие и сложные задачи решаемы, если подходить к ним системно.