Сущности блока
Сущности блока - это способ хранения дополнительных данных для блока, которые не являются частью состояния блока: содержимое инвентаря, пользовательское имя и так далее. В Minecraft используются блочные сущности для таких блоков, как сундуки, печи и командные блоки.
В качестве примера мы создадим блок, который будет подсчитывать, сколько раз на нем щелкнули ПКМ.
Создание сущности блока
Чтобы Minecraft распознал и загрузил новые сущности блока, нам нужно создать тип сущности блока. Это делается путем расширения класса BlockEntity и регистрации его в новом классе ModBlockEntities.
java
public class CounterBlockEntity extends BlockEntity {
public CounterBlockEntity(BlockPos pos, BlockState state) {
super(ModBlockEntities.COUNTER_BLOCK_ENTITY, pos, state);
}
}1
2
3
4
5
2
3
4
5
Регистрация BlockEntity дает BlockEntityType, подобный COUNTER_BLOCK_ENTITY, который мы использовали выше:
java
public static final BlockEntityType<CounterBlockEntity> COUNTER_BLOCK_ENTITY =
register("counter", CounterBlockEntity::new, ModBlocks.COUNTER_BLOCK);
private static <T extends BlockEntity> BlockEntityType<T> register(
String name,
FabricBlockEntityTypeBuilder.Factory<? extends T> entityFactory,
Block... blocks
) {
Identifier id = Identifier.fromNamespaceAndPath(ExampleMod.MOD_ID, name);
return Registry.register(BuiltInRegistries.BLOCK_ENTITY_TYPE, id, FabricBlockEntityTypeBuilder.<T>create(entityFactory, blocks).build());
}1
2
3
4
5
6
7
8
9
10
11
2
3
4
5
6
7
8
9
10
11
TIP
Обратите внимание, что конструктор CounterBlockEntity принимает два параметра, а конструктор BlockEntity - три: BlockEntityType, BlockPos и BlockState. Если бы мы жёстко не закодировали BlockEntityType, класс ModBlockEntities не скомпилировался бы! Это происходит потому, что BlockEntityFactory, является функциональным интерфейсом, описывает функцию, которая принимает только два параметра, как и наш конструктор.
Создание блока
Далее, чтобы действительно использовать сущность блока, нам нужен блок, реализующий EntityBlock. Давайте создадим такой блок и назовем его CounterBlock.
TIP
К этому можно подойти двумя способами:
- создать блок, который расширяет
BaseEntityBlockи реализует методcreateBlockEntity - создать блок, реализующий
EntityBlockсамостоятельно и переопределить методcreateBlockEntity
В этом примере мы будем использовать первый подход, поскольку BaseEntityBlock также предоставляет несколько хороших утилит.
java
public class CounterBlock extends BaseEntityBlock {
public CounterBlock(Properties settings) {
super(settings);
}
@Override
protected MapCodec<? extends BaseEntityBlock> codec() {
return simpleCodec(CounterBlock::new);
}
@Nullable
@Override
public BlockEntity newBlockEntity(BlockPos pos, BlockState state) {
return new CounterBlockEntity(pos, state);
}
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Использование BaseEntityBlock в качестве родительского класса означает, что нам также необходимо реализовать метод createCodec, что довольно просто.
В отличие от блоков, которые являются сингл тонами, для каждого экземпляра блока создается новая сущность блока. Это делается с помощью метода createBlockEntity, который принимает позицию и BlockState, и возвращает BlockEntity, или null, если его не должно быть.
Не забудьте зарегистрировать блок в классе ModBlocks, как в [Создайте свой первый блок].(../blocks/first-block) guide:
java
public static final Block COUNTER_BLOCK = register(
"counter_block",
CounterBlock::new,
BlockBehaviour.Properties.of(),
true
);1
2
3
4
5
6
2
3
4
5
6
Использование сущности блока
Теперь, когда у нас есть сущность блока, мы можем использовать её для хранения кол-ва раз, когда на блоке щёлкали ПКМ. Для этого мы добавим поле clicks в класс CounterBlockEntity:
java
private int clicks = 0;
public int getClicks() {
return clicks;
}
public void incrementClicks() {
clicks++;
setChanged();
}1
2
3
4
5
6
7
8
9
2
3
4
5
6
7
8
9
Метод setChanged, используемый в incrementClicks, сообщает игре, что данные этой сущности были обновлены; это может быть полезно, когда мы добавим методы для сериализации счётчика и загрузки его обратно из файла сохранения.
Далее нам нужно увеличивать это поле каждый раз, когда на блоке щелкают ПКМ. Это делается путём переопределения метода useWithoutItem в классе CounterBlock:
java
@Override
protected InteractionResult useWithoutItem(BlockState state, Level level, BlockPos pos, Player player, BlockHitResult hit) {
if (!(level.getBlockEntity(pos) instanceof CounterBlockEntity counterBlockEntity)) {
return super.useWithoutItem(state, level, pos, player, hit);
}
counterBlockEntity.incrementClicks();
player.sendOverlayMessage(Component.literal("You've clicked the block for the " + counterBlockEntity.getClicks() + "th time."));
return InteractionResult.SUCCESS;
}1
2
3
4
5
6
7
8
9
10
11
2
3
4
5
6
7
8
9
10
11
Поскольку BlockEntity не передается в метод, мы используем level.getBlockEntity(pos), и если BlockEntity не является действительным, возвращаемся из метода.

Сохранение и загрузка данных
Теперь, когда у нас есть функциональный блок, мы должны сделать так, чтобы счетчик не сбрасывался между перезапусками игры. Это делается путем сериализации в NBT при сохранении игры и десериализации при ее загрузке.
Сохранение в NBT осуществляется с помощью ValueInput и ValueOutput. Эти классы отвечают за сохранение ошибок с кодирования/декодирования, а также за отслеживание реестров во время процесса сериализации.
Вы можете читать данные из ValueInput, используя метод read, передавая Codec для нужного типа. Аналогично, вы можете записывать данные в ValueOutput, используя метод store, передавая Codec для типа и само значение.
Также есть методы для примитивных типов, такие как getInt, getShort, getBoolean и т. д. для записи и putInt, putShort, putBoolean и т. д. для записи. View также предоставляет методы для работы со списками, nullable-типами и вложенными объектами.
Сериализация выполняется с помощью метода saveAdditional:
java
@Override
protected void saveAdditional(ValueOutput output) {
output.putInt("clicks", clicks);
super.saveAdditional(output);
}1
2
3
4
5
6
2
3
4
5
6
Здесь мы добавляем поля, которые должны быть сохранены в переданный ValueOutput: в случае блока-счётчика это поле clicks.
Чтение происходит аналогично: вы получаете ранее сохранённые значения из ValueInput и сохраняете их в поля BlockEntity:
java
@Override
protected void loadAdditional(ValueInput input) {
super.loadAdditional(input);
clicks = input.getIntOr("clicks", 0);
}1
2
3
4
5
6
2
3
4
5
6
Теперь, если мы сохраним и перезагрузим игру, блок счетчика должен продолжиться с того места, на котором он остановился при сохранении.
Хотя saveAdditional и loadAdditional отвечают за сохранение и загрузку на диск и с диска, остаётся проблема:
- Сервер знает правильное значение
clicks. - Клиент не получает правильное значение при загрузке чанка.
Чтобы исправить это, мы переопределяем getUpdateTag:
java
@Override
public CompoundTag getUpdateTag(HolderLookup.Provider registryLookup) {
return saveWithoutMetadata(registryLookup);
}1
2
3
4
2
3
4
Теперь, когда игрок входит в игру или перемещается в чанк, где есть блок, он сразу же увидит правильное значение счетчика.
Тики
Интерфейс EntityBlock также определяет метод getTicker, который можно использовать для выполнения кода каждый тик для каждого экземпляра блока. Мы можем реализовать это, создав статический метод, который будет использоваться в качестве BlockEntityTicker:
Метод getTicker также должен проверить, совпадает ли переданный BlockEntityType с тем, который мы используем, и если да, то вернуть функцию, которая будет вызываться каждый тик. К счастью, существует служебная функция BaseEntityBlock, которая выполняет эту проверку:
java
@Nullable
@Override
public <T extends BlockEntity> BlockEntityTicker<T> getTicker(Level level, BlockState state, BlockEntityType<T> type) {
return createTickerHelper(type, ModBlockEntities.COUNTER_BLOCK_ENTITY, CounterBlockEntity::tick);
}1
2
3
4
5
2
3
4
5
CounterBlockEntity::tick - это ссылка на статический метод tick, который мы должны создать в классе CounterBlockEntity. Структурировать его таким образом не обязательно, но это хорошая практика, чтобы сохранить код чистым и организованным.
Допустим, мы хотим сделать так, чтобы счетчик увеличивался только раз в 10 тиков (2 раза в секунду). Мы можем сделать это, добавив поле ticksSinceLast в класс CounterBlockEntity и увеличивая его с каждым тиком:
java
public static void tick(Level level, BlockPos blockPos, BlockState blockState, CounterBlockEntity entity) {
entity.ticksSinceLast++;
}1
2
3
2
3
Не забудьте сериализовать и десериализовать это поле!
Теперь мы можем использовать ticksSinceLast, чтобы проверить, можно ли увеличить счетчик в incrementClicks:
java
if (ticksSinceLast < 10) return;
ticksSinceLast = 0;1
2
2
TIP
Если блок-сущность не отображается, попробуйте проверить регистрационный код! Он должен передать в BlockEntityType.Builder блоки, которые действительны для этой сущности, иначе он выдаст предупреждение в консоли:
log
[13:27:55] [Server thread/WARN] (Minecraft) Block entity example-mod:counter @ BlockPos{x=-29, y=125, z=18} state Block{example-mod:counter_block} invalid for ticking:1

