Перейти к основному содержанию

Изменяем регион

Игровой мир состоит из чанков — составляющих региона, в них распологаются блоки и существа, образующие окружение. Количество регионов неограничено, они привязаны к измерению. Специально для связки регионов друг с другом создан источник блоков, для разработчика лишь важно определить с каким измерением он будет взаимодействовать, а обработкой займется специальный обработчик. Рассмотрим возможности окружения, изменения и получения свойств, спавн существ и размещение блоков.

Система координат

Трехмерный игровой мир использует в качестве величин метры, где каждый метр равен одному блоку. Эта система применима к любому объекту игрового окружения, исключая разве что метрики с абсолютной (относительной) системой координат и модели. Измерения в блоках производятся относительно сторон света игрового мира, где существуют ширина (x, на север сзади и юг спереди), долгота (z, на запад слева и восток справа), а также высота (y).

Выбираем источник

Под понятием региона в документации будет рассматриваться именно источник блоков, так как фактически именно он служит доступом к игровому миру. Прежде всего, необходимо понять по какому критерию регион будет использоваться. Здесь есть несколько вариантов:

  1. По измерению (основной для большинства действий)

    Эти регионы общие для всего движка и проектов в целом, они кешируются и подходят практически всегда. Любые изменения тут же будут переданы остальным игрокам (клиентам), а также сохранены в самом мире.

    BlockSource.getDefaultForDimension(EDimension.NORMAL)

    Если измерение неизвестно, можно запросить его для определенного существа.

    Callback.addCallback("EntityAdded", function(entity) {
    const region = BlockSource.getDefaultForActor(entity);
    ...
    });
  2. Для генерации и ее событий

    Созданы специально для правки блоков и тайлов во время генерации мира, на этот момент это единственные доступные регионы для использования. Применимы лишь к калбекам генерации и не должны использоваться вне событий генератора. Они неоднократно будут рассмотрены в серии статей о измерениях и структурах.

    BlockSource.getCurrentWorldGenRegion()
  3. Клиентские

    Клиент со своей стороны не может использовать серверные и любые другие регионы (в них попросту не будет блоков, а их изменение ничем не закончится), так что для получения информации о окружении созданы клиентские регионы. Правки окружения естественно повлияют только на клиента, а вот получение информации вполне актуально.

    BlockSource.getCurrentClientRegion()

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

Что он умеет

Давайте на практике рассмотрим доступные нам возможности, парочки нелепых примеров для каждого случая будет достаточно. Прежде всего, в качестве региона рассмотрим калбек измерения игроком измерения в мире:

Callback.addCallback("PlayerChangedDimension", function(playerUid, currentId, lastId) {
const region = BlockSource.getDefaultForDimension(currentId);
const position = Entity.getPosition(playerUid);
...
});

Он вызывается сразу же после подключения к миру, измерение определяется самим событием. Уже успели устать от ItemUse? Давайте сделаем что-нибудь поинтереснее, используя другой калбек.

А вот и блок

Любой блок в мире определяется на основе координат, идентификатора, вариации и описания состояний. C технической точки зрения, даже воздух является блоком, заполняя все пустые ячейки конструктора из чанков. Для описания идентификаторов в мире используйте VanillaTileID, они актуальны только для мира, не инвентаря.

Начнем с метода установки/замены блока, старый блок будет заменен новым:

region.setBlock(x, y, z, id, data?)

Здесь все просто — указываем координаты, идентификатор блока и опционально вариацию. Но откуда взять координаты... А идентификатор? Можно использовать координаты событий (вроде нажатия по блоку), либо получить координаты игровых объектов или существ. В нашем случае событие передает ключ игрока, положение которого можно использовать в качестве ориентира. Вы любите башенки?

region.setBlock(position.x - 1, position.y + 3, position.z - 1, VanillaTileID.dirt);
region.setBlock(position.x - 1, position.y + 3, position.z, VanillaTileID.dirt);
region.setBlock(position.x - 1, position.y + 3, position.z + 1, VanillaTileID.dirt);
region.setBlock(position.x, position.y + 3, position.z - 1, VanillaTileID.dirt);
region.setBlock(position.x, position.y + 3, position.z, VanillaTileID.dirt);
region.setBlock(position.x, position.y + 3, position.z + 1, VanillaTileID.dirt);
region.setBlock(position.x + 1, position.y + 3, position.z - 1, VanillaTileID.dirt);
region.setBlock(position.x + 1, position.y + 3, position.z, VanillaTileID.dirt);
region.setBlock(position.x + 1, position.y + 3, position.z + 1, VanillaTileID.dirt);
region.setBlock(position.x, position.y + 2, position.z, VanillaTileID.dirt);

Этот скрипт вызовет появление одной такой над вашим персонажем. Небольшая платформа с выпуклостью внизу, просто запустите и посмотрите как работает этот код! Мы сдвинули постройку на несколько блоков вверх, ориентируясь на положение игрока в мире. Цепочек же из блоков может быть сколько угодно, главное чтобы чанки были загружены.

Давайте упростим это!
for (let x = -1; x <= 1; x++) {
for (let z = -1; z <= 1; z++) {
region.setBlock(position.x + x, position.y + 3, position.z + z, VanillaTileID.dirt);
}
}
region.setBlock(position.x, position.y + 2, position.z, VanillaTileID.dirt);

Мы использовали небольшой вложенный цикл, чтобы создать повторение блоков по двум осям. Таким образом над игроком появилась такая же платформа 3x1x3 блока, обрамленная "куполом" снизу.

Помимо установки блоков, куда чаще нам придется проверять какой блок уже находится на координатах. Сделать это еще проще, получив идентификатор:

region.getBlockId(x, y, z)

Результатом выполнения метода вернется идентификатор, который можно использовать для проверок. Давайте не будем останавливаться на одном if и сделаем цикличную замену блоков под игроком:

const radius = Math.round(4 + Math.random() * 8);
for (let x = -radius; x <= radius; x++) {
for (let z = -radius; z <= radius; z++) {
for (let y = -radius; y <= radius; y++) {
if (x * x + y * y + z * z <= radius * radius) {
if (region.getBlockId(position.x + x, position.y + y, position.z + z) != VanillaTileID.air) {
region.setBlock(position.x + x, position.y + y, position.z + z, VanillaTileID.green_glazed_terracotta);
break;
}
}
}
}
}

Рандомизируя радиус от игрока, мы использовали формулу окружности, заменяя первый найденный блок поверхности (не являющийся воздухом). Слишком резкий переход к более сложным алгоритмам?

Разберемся с алгоритмом

Давайте рассмотрим этот код пошагово, чтобы быстрее прояснить алгоритм действий:

  1. Рассчитаем целый, случайный радиус от 4 до 12 блоков:

    const radius = Math.round(4 + Math.random() * 8);
  2. Пройдем по всем трем осям, высота будет последней специально для следующих шагов:

    for (let x = -radius; x <= radius; x++) {
    for (let z = -radius; z <= radius; z++) {
    for (let y = -radius; y <= radius; y++) {
    ...
    }
    }
    }
  3. Применим формулу окружности (x * x + y * y + z * z) к координатам текущего шага цикла, убедившись что ее значение не выходит из полученного диаметра (удвоенного радиуса):

    if (x * x + y * y + z * z <= radius * radius) {
    ...
    }
  4. Полученный блок (за счет формулы) части окружности не должен быть воздухом, тогда можем поставить терракоту:

    if (region.getBlockId(position.x + x, position.y + y, position.z + z) != VanillaTileID.air) {
    region.setBlock(position.x + x, position.y + y, position.z + z, VanillaTileID.green_glazed_terracotta);
    ...
    }
  5. Блок поверхности был заменен, а значит цикл по этому x и z (помните, как мы использовали высоту последней, завершится только она) можно закончить, переходя к следующему:

    break;

Поэкспериментируйте с этим кодом, к примеру, проигнорировав последний шаг. В таком случае, вместо одного блока поверхности, будут заменены все в радиусе сферы вокруг игрока.

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

region.setExtraBlock(x, y, z, id, data?)

Однако, экстра блоком может быть что угодно. Как вам видится установка факела внутри стекла?

region.setBlock(position.x + 2, position.y, position.z, VanillaTileID.glass);
region.setExtraBlock(position.x + 2, position.y, position.z, VanillaTileID.torch);

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

Блокстейты

Помимо обычного идентификатора и вариации, каждый блок описывает состояния. Состояния формируют визуальную часть блока в мире на основе поворота, переключателей и прочих свойств. В каждом случае блок можно представить в виде блокстейта — связки идентификатора, и, вариации или состояний. Состояния используются в блоках древесины (для описания поворота), дверях (помимо поворота, открыта ли дверь), якорях возрождения (уровень заряда) и многих других блоках.

Получить блокстейт по координатам можно с помощью метода:

region.getBlock(x, y, z)
region.getExtraBlock(x, y, z)

Он содержит неизменяемый идентификатор и подготовленную вариацию, а также предоставляет методы для обработки состояний. Мне кажется, неплохим примером будет связать создание топора и его использование на блоках древесины. Каким образом? Топор обтесывает древесину, это нужно реализовать:

Callback.addCallback("ItemUse", function(coords, item, block, isExternal, playerUid) {
const logId = (function() {
switch (block.id) {
case VanillaTileID.log:
switch (block.data) {
case 0: return VanillaTileID.stripped_oak_log;
case 1: return VanillaTileID.stripped_spruce_log;
case 2: return VanillaTileID.stripped_birch_log;
case 3: return VanillaTileID.stripped_jungle_log;
}
break;
case VanillaTileID.log2:
switch (block.data) {
case 0: return VanillaTileID.stripped_acacia_log;
case 1: return VanillaTileID.stripped_dark_oak_log;
}
break;
case VanillaTileID.crimson_stem:
return VanillaTileID.stripped_crimson_stem;
case VanillaTileID.warped_stem:
return VanillaTileID.stripped_warped_stem;
}
return 0;
})();
if (logId != 0) {
const region = BlockSource.getDefaultForActor(playerUid);
const axis = region.getBlock(coords.x, coords.y, coords.z).getState(EBlockStates.PILLAR_AXIS);
if (logId == VanillaTileID.stripped_crimson_stem || logId == VanillaTileID.stripped_warped_stem) {
region.setBlock(coords.x, coords.y, coords.z, logId, axis);
} else {
const block = new BlockState(logId, { pillar_axis: axis });
region.setBlock(coords.x, coords.y, coords.y, logId, block);
}
ToolLib.breakCarriedTool(1, playerUid);
World.playSound(coords.x + 0.5, coords.y + 0.5, coords.z + 0.5, "hit.wood", 0.5, 0.8);
}
});

Использование блокстейтов необходимо только для обычной древесины, в случае адской вариация служит показателем поворота блока. Здесь нас интересуют несколько функций и класс BlockState. Функция getBlock, о которой было сказано ранее, возвращает необходимый нам класс блокстейта. В частности, он содержит следующие методы и данные:

// получая блокстейт, у нас есть вся информация об этом блоке
const block = region.getBlock(coords.x, coords.y, coords.z);
// в случае если этот блок содержит состояние поворота
if (block.hasState(EBlockStates.PILLAR_AXIS)) {
// получим значение этого состояния, вернется -1 если его нет
// (но мы уже убедились что это состояние есть, так что все в порядке)
const state = block.getState(EBlockStates.PILLAR_AXIS);
// добавим/заменим состояние, в данном случае блок будет поворачиваться
// по каждой оси, пока оси не закончатся, либо начнет сначала
const newBlock = block.addState(EBlockStates.PILLAR_AXIS, (state + 1) % 6);
// установим новосозданный блокстейт на координаты, старый не будет затронут
region.setBlock(coords.x, coords.y, coords.z, newBlock);

// в случае если блок содержит состояние открытия (двери, люки)
} else if (block.hasState(EBlockStates.open_bit)) {
// получим список всех состояний, вернется объект с их списком
const states = block.getNamedStatesScriptable();
// возьмем направление из списка состояний, его может и не быть
const direction = states.direction || 0;
// применим сразу несколько состояний к новому блокстейту,
// основываясь на старом, напомню, что он не затронется
const newBlock = block.addStates({
open_bit: Math.round(Math.random()),
direction: (state + 1) % 4
});
// существуют блокстейты, неприменимые друг к другу
if (newBlock.isValidState()) {
// отлично, наша дверь или люк повернутся по следующей оси
// и получат случайное состояние открытости
region.setBlock(coords.x, coords.y, coords.z, newBlock);
}
}

Помимо представленных методов, есть и более базовые свойства, такие как id и data. По этой причине, любой блокстейт может быть установлен на координаты, практически всегда сохраняя все свойства блока. Не забывайте, что полученный блок привязан к идентификационному пространству одного мира, свойство runtimeId генерируется единожды и не может быть использовано в других мирах. Вернемся к установке блоков.

region.setBlock(x, y, z, state)
region.setExtraBlock(x, y, z, state)

Как и в случае с установкой блоков по идентификатору, экстра блоки имеют синтаксис обычных. Сам класс блокстейтов может принимать на вход runtimeId, связку идентификатора и вариации, либо идентификатора и списка состояний. Каждый случай специфичен для своей ситуации, но самое главное что блокстейты применимы для экстра блоков:

const block = new BlockState(VanillaTileID.flowing_water, 0);
// добавление стейта сотрет переданную в констуктор вариацию
block.addState(EBlockStates.LIQUID_DEPTH, 1);
region.setExtraBlock(coords.x, coords.y, coords.z, block);

Но почему мы не можем использовать вариацию и состояния сразу? Фактически, состояния это все та же вариация, просто созданная на основе предоставленных данных. Ручное изменение состояний для тайлов (вроде печек, варочных стоек) лишь сбивает с толку, от того что печка загорится (хотя она неактивна) никому не будет лучше, также как и от внезапного появления бутыльков с зельями. Просто знайте, что такое возможно, но лучше рассмотрите внутреигровые тайлы.

Список свойств для блоков

Пока находится в разработке, но как только мы закончим его, здесь обязательно появится ссылка с описанием каждого из них. А пока, воспользуйтесь получением состояний, используя метод getNamedStatesScriptable. Актуальный список доступных состояний можно найти в EBlockStates.

Существа

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

region.spawnEntity(x, y, z, type)

Достаточно определить координаты и... Тип существа? Есть несколько вариантов его описания, рассмотрите этот код:

// используем числовой идентификатор для призыва существа
region.spawnEntity(position.x, position.y, position.z, EEntityType.CREEPER);
// помимо числового, пакеты поведения описывают именной идентификатор
region.spawnEntity(position.x, position.y, position.z, "creeper");
// неймспейс (пространство имен) игры используется по умолчанию
region.spawnEntity(position.x, position.y, position.z, "minecraft:creeper");

Каждый метод здесь равносилен друг другу. Используя пространства имен (а также именные идентификаторы), мы дополнительно можем указать еще и события (состояния, они указываются между <> и разделяются запятыми) для призыва:

// крипер будет заряжен (как в результате удара молнии) как только появится, рассмотрите
// пакеты поведения для получения подробностей о использовании событий или команду /summon
region.spawnEntity(position.x, position.y, position.z, "minecraft:creeper:<become_charged>");

Метод возвращает уникальный идентификатор призванного моба, используемый для обработки существ. Ну а сейчас продолжим работать с регионом и рассмотрим остальные возможности.

С точки зрения игры, существами на самом деле является куда больше чем может показаться. Что такое падающий блок? А выкинутый из инвентаря предмет? Картины, сферы опыта и многое, многое другое — такие же мобы как и все остальное. Учитывая, что некоторые из существ содержат уникальные данные, которые нельзя просто изменить, существует несколько вспомогательных методов для призыва специфических существ.

Начать хотелось бы со сфер опыта, определите лишь их количество:

region.spawnExpOrbs(x, y, z, amount)

Сфер опыта может выпадать сколько угодно, к примеру после убийства дракона края игрок получает 12000 опыта. Для снижения нагрузки, сферы объединяются друг с другом, формируя более плотные с большим количеством опыта. В качестве примера реализуем разбитие пузырька опыта, но прежде создайте бросающийся предмет:

Item.registerThrowableFunction("diamond_bullet", function(projectile, item, target) {
const region = BlockSource.getDefaultForActor(projectile);
region.spawnExpOrbs(target.x, target.y, target.z, Math.floor(3 + Math.random() * 8));
});

После "разбития" алмаза о блок или существо выпадут сферы опыта в месте попадания броска. Случайное количество опыта от 3 до 11 равносильно обычным пузырькам опыта, функционал в несколько строк готов.

Куда более интересной возможностью является призыв выпавших предметов (дропа), список аргументов здесь схож с остальными способами описания предмета (в том числе событиями):

region.spawnDroppedItem(x, y, z, id, count, data, extra?)

Возьмите в руку игрока любой предмет. Представьте, что он находится не у вас в инвентаре, а лежит перед вами. А теперь, воплотим плод воображения в реальность:

const item = Entity.getCarriedItem(playerUid);
region.spawnDroppedItem(position.x, position.y, position.z, item.id, item.count, item.data, item.extra || null);

После призыва предмет может быть изменен с помощью Entity.setDroppedItem, используя уникальный идентификатор существа. Воспользуйтесь для этого возвращаемым результатом метода или событиями.

Ну а помимо калбеков EntityAdded или EntityAddedLocal, всегда можно получить список мобов между двумя точками в мире. Результат будет возвращен в виде массива и отфильтрован по типу существа:

region.listEntitiesInAABB(x1, y1, z1, x2, y2, z2, entityType?, blacklist?)

Последнее свойство которого отвечает за то, должен ли фильтр быть инвертирован (искаться будут все мобы кроме заданного типа). Если не использовать аргументы фильтра, а лишь указать координаты, будут возвращены все существа между этими точками. Удалим всех мобов вокруг игрока во время смены измерения, исключая самого игрока:

const entities = region.listEntitiesInAABB(
position.x - 16, position.y - 8, position.z - 16,
position.x + 16, position.y + 8, position.z + 16,
EEntityType.PLAYER, true
);
entities.forEach(function(entity) {
Entity.remove(entity);
});
Что будет если удалить игрока

Экстренное завершение игры. Почему это происходит? Отрисовка напрямую использует существо камеры, если его не будет, то и миру будет нечего отрисовывать. Если у вас задействуется изменение камеры, опасайтесь нанесению урона используемому существу. Результат его убийства приведет к удалению, а удаление влечет за собой вылет. Может быть стоило бы нанести игроку урон для смерти, либо запросить отключение клиента от сервера. По крайней мере, теперь у нас есть понятие как быстро избавиться от игрока.

Разрушение блоков

Немаловажно рассмотреть разрушение блоков отдельно, так помимо уничтожения блока в мире, другим модам тоже нужно узнать об этом. Для чего это нужно? Здесь может быть все что угодно, начиная достижениями и заканчивая изменением выпадаемых из блоков предметов (дропа).

В большинстве случаев будет достаточно обычной функции разрушения блока, ее синтаксис сводится к следующему методу:

region.breakBlock(x, y, z, isDropAllowed, actor?, item?)

Последняя пара аргументов задействуется для вызова события разрушения блока игроком, калбека DestroyBlock. Помимо него, этот метод в любом случае вызовет BreakBlock. К примеру, разрушим блоки (возможно травы) под игроком, используя зачарование шелкового касания:

const item = {
id: VanillaItemID.diamond_pickaxe,
count: 1,
data: 0,
extra: new ItemExtraData()
};
item.extra.addEnchant(EEnchantment.SILK_TOUCH, 1);
for (let dx = -8; dx < 8; dx++) {
for (let dz = -8; dz < 8; dz++) {
region.breakBlock(position.x + dx, position.y - 1, position.z + dz, true, item);
}
}

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

region.breakBlockForJsResult(x, y, z, actor?, item?)

Такая реализация будет означать отсутствие дропа по умолчанию (можно впринципе не обрабатывать его, посмотрите на следующий метод), мы можем обработать после событий выбрасываемые предметы как угодно. Синтаксис дропа представлен в виде массива из предметов, в общем случае:

[
{
id: VanillaItem/BlockID.something,
count: 1,
data: 0
},
...
]

А также количеству опыта, который должен выпасть из предмета. Представим, что блоки должны удваивать свой дроп в случае их разрушения алмазной киркой, увеличим количество предметов и опыт в два раза:

Callback.addCallback("DestroyBlock", function(coords, block, playerUid) {
if (playerUid && playerUid != -1) {
const item = Entity.getCarriedItem(playerUid);
if (item.id != VanillaItemID.diamond_pickaxe) {
return;
}
const region = BlockSource.getDefaultForActor(playerUid);
if (region != null) {
Game.prevent();
const result = region.breakBlockForJsResult(x, y, z, playerUid, item);
result.items.forEach(function(entry) {
region.spawnDroppedItem(coords.x, coords.y, coords.z, entry.id, entry.count * 2, entry.data, entry.extra);
});
region.spawnExpOrbs(coords.x, coords.y, coords.z, result.experience * 2);
}
}
});

Возможно, вашему алгоритму необходимо разрушить коллосальное количество блоков. Или обработка событий просто не нужна, блоки были разрушены черной дырой. Это вполне реальные случаи, для этого создан отдельный метод:

region.destroyBlock(x, y, z, drop?)

Минимальное количество аргументов и полное отсутствие событий. Последний аргумент определяет нужно ли обрабатывать дроп, в противном случае предметов выброшено не будет (поведение по умолчанию). Отключите частицы для получения наилучших результатов, для примера разрушим целый чанк:

const particles = region.getDestroyParticlesEnabled();
if (particles) {
region.setDestroyParticlesEnabled(false);
}
const x = position.x - position.x % 16;
const z = position.z - position.z % 16;
for (let dx = 0; dx < 16; dx++) {
for (let dy = 0; dy < 256; dy++) {
for (let dz = 0; dz < 16; dz++) {
region.destroyBlock(x + dx, dy, z + dz);
}
}
}
if (particles) {
region.setDestroyParticlesEnabled(true);
}

Высчитывая начало чанка, в котором находится игрок, мы можем разрушить его полностью. Не забудьте вернуть отображение частиц — как уже было сказано, многие регионы общие для всех модов. Кстати, порядок осей формирует название алгоритма, использующегося для цикличных действий с регионом. В данном случае это XYZ, а к примеру, внутреигровые структуры задействуют алгоритм XZY. Мы еще рассмотрим предназначение этих принципов в следующих статьях.

Дополнительные возможности

Сюда попали не менее полезные методы, но куда более специфичные, и использующиеся чуть реже остальных. Здесь затронуты термины, не объясненные в документации, но их можно легко найти в сети.

Начнем с чего-то яркого, хотя и болезненного для игроков. Конечно же, речь идет о взрывах:

region.explode(x, y, z, power, fire)

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

Почему взрыв не отнесен к разрушению блоков? Дело в том, что здесь используются другие принципы уничтожения блоков. Для взрыва задейстован калбек Explosion, а разрушения для отдельных блоков обрабатываются в ограниченном объеме. Ну и сам по себе, он просто нужен куда более реже.

Следующий метод лично для меня стал открытием, хотя способов его применения в игре целая масса. У вас когда-нибудь возникала потребность быстро найти поверхность для призыва существ, генерации строения или просто определения точки возрождения игрока? Хотите использовать для этого цикличные проверки блока на воздух? Есть вариант получше:

region.clip(x1, y1, z1, x2, y2, z2, mode, output)

Посредством быстрого "столкновения" с игровым миром, функция определит ближайший цельный блок от первой координаты. Аргумент output задействуется для нахождения места столкновения с этим блоком, предоставляя помимо координат сторону, с которой произошло столкновение (для особо прошаренных, можно определить нормаль и рассчитать его границы). Это отличный способ для поиска поверхности среди блоков воздуха, значительно сокращающая время на обработку. Как вам идея сделать неровный рельеф (особенно на холмистых участках) идеально плоским?

const particles = region.getDestroyParticlesEnabled();
if (particles) {
region.setDestroyParticlesEnabled(false);
}
const x1 = position.x - 16;
const z1 = position.z - 8;
const x2 = position.x + 16;
const z2 = position.z + 8;
let collision = [];
for (let y = 0; y < 12; y++) {
region.clip(x1, position.y + y, z1, x2, position.y + y, z2, 0, collision);
while (Math.abs(collision[0] - x2) >= 0.0001 || Math.abs(collision[2] - z2) >= 0.0001) {
region.destroyBlock(collision[0], collision[1], collision[2]);
region.clip(x1, position.y + y, z1, x2, position.y + y, z2, 0, collision);
}
}
if (particles) {
region.setDestroyParticlesEnabled(true);
}

Метод помещает координаты найденного соприкосновения с блоком в массив, используемый для определения следующего места разрушения. Небольшая погрешность откидывается, позволяя определить остались ли еще блоки в пространстве. Участок 16x12x8 вокруг игрока будет разрушен, причем убедиться что этот алгоритм работает быстрее цикличной проверки блоков несложно — чуть видоизмените код из разрушения блоков и все готово.

Непроверенная информация

Мне неизвестно за что отвечает аргумент mode, здесь я могу лишь предположить, что он влияет на режим поиска блоков относительно двух координат. К примеру, по умолчанию, столкновение обрабатывается начиная с первой точки (mode = 0). Тогда, вероятно, что следующий режим (mode = 1) установит начальную точку второй, а еще следующий (mode = 2) начнет с центра между двумя точками. Точно известно, что один или несколько режимов позволяют искать не только блоки, но еще и существ. Этот вопрос разрешится после дополнительных тестов.

И хотелось бы остановиться на обновляемых блоках. Если вкратце, это блоки, находящиеся не дальше границы симуляции (в радиусе чанков симуляции, дальше этого расстояния печки приостанавливают свою работу, а из листвы перестают выпадать ростки). На самом деле, помимо этой области, существуют еще и так называемые обновляемые области. Они создаются с помощью команды /tickingarea в любом месте измерения. Мы же, можем настроить определенный блок:

region.addToTickingQueue(x, y, z, block?, delay, mode?)

Задавая периодичность его обновлений в тиках, этот блок всегда будет выполнять свою работу. Конечно, если измерение с ним сейчас загружено хотя бы одним игроком. Передавая еще и свойство блокстейта (он должен быть получен по блоку на координатах), обновляться будет именно полученный блок.

На этом можно закончить основной список методов региона, хотя конечно же, мы рассмотрели не все, но практически каждая из отсутствующих здесь возможностей рассмотрена в отдельных статьях документации.