Когда Structure Block уже не справляется: Продвинутая генерация огромных строений в Minecraft

от автора

Приветствую моддер, я не нашел в интернете подробного гайда по данной теме, поэтому эта статья — способ обобщить мой опыт и поделиться им с другими. В основном статья предназначена для начинающих, но будет не лишним и для тех кто хочет напомнить себе работу со структурами при моддинге. Позже я буду делать её более подробной, либо писать дополнения к этой статье.

  • Стек: 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 — это именно такой подход, дающий гибкость и производительность там, где пасуют стандартные конфигурации. Во-вторых, самый важный навык моддера — это не столько умение писать код, сколько умение его отлаживать и методично искать причину, когда что-то идет не так.
Надеюсь, мой опыт сэкономит вам несколько часов (или даже дней) отладки и покажет, что даже самые большие и сложные задачи решаемы, если подходить к ним системно.


ссылка на оригинал статьи https://habr.com/ru/articles/937976/