Блоки-сутності — це спосіб зберігати додаткові дані для блока, які не є частиною стану блока: вміст інвентарю, спеціальна назва тощо. 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
6
2
3
4
5
6
Реєстрація 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
12
2
3
4
5
6
7
8
9
10
11
12
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
17
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
Використання BaseEntityBlock як батьківського класу означає, що нам також потрібно реалізувати метод createCodec, який досить простий.
На відміну від блоків, які є одинарними, нова блок-сутність створюється для кожного екземпляра блока. Це робиться за допомогою методу createBlockEntity, який приймає позицію та BlockState і повертає BlockEntity або null, якщо його не має бути.
Не забудьте зареєструвати блок у класі ModBlocks, як у посібнику створення вашого першого блока:
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
10
2
3
4
5
6
7
8
9
10
Метод 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.displayClientMessage(Component.literal("You've clicked the block for the " + counterBlockEntity.getClicks() + "th time."), true);
return InteractionResult.SUCCESS;
}1
2
3
4
5
6
7
8
9
10
11
12
2
3
4
5
6
7
8
9
10
11
12
Оскільки BlockEntity не передається в метод, ми використовуємо level.getBlockEntity(pos), і якщо BlockEntity недійсний, повертаємося з методу.

Збереження і завантаження даних
Тепер, коли у нас є функціональний блок, ми повинні зробити так, щоб лічильник не обнулявся між перезавантаженнями гри. Це робиться шляхом серіалізації в NBT під час збереження гри та десеріалізації під час завантаження.
Збереження в NBT виконується через ValueInputs і ValueOutput. Ці подання відповідають за зберігання помилок кодування/декодування та відстеження реєстрів протягом усього процесу серіалізації.
Ви можете читати з ValueInput за допомогою методу read, передаючи Codec для потрібного типу. Так само ви можете писати в ValueOutput за допомогою методу store, передаючи кодек для типу та значення.
Існують також методи для примітивів, наприклад getInt, getShort, getBoolean тощо для читання та putInt, putShort, putBoolean тощо для запису. Представлення також надає методи для роботи зі списками, типами, що допускають null-значення, і вкладеними об’єктами.
Серіалізація виконується за допомогою методу saveAdditional:
java
@Override
protected void saveAdditional(ValueOutput output) {
output.putInt("clicks", clicks);
super.saveAdditional(output);
}1
2
3
4
5
6
7
2
3
4
5
6
7
Тут ми додаємо поля, які мають бути збережені в переданому 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
7
2
3
4
5
6
7
Тепер, якщо ми збережемо та перезавантажимо гру, блок лічильника має продовжуватися з того місця, на якому він зупинився під час збереження.
У той час як saveAdditional і loadAdditional керують збереженням і завантаженням на носій і з носія, все ще є проблема:
- Сервер знає правильне значення
clicks. - Клієнт не отримує правильне значення під час завантаження чанку.
Щоб виправити це, ми перевизначаємо getUpdateTag:
java
@Override
public CompoundTag getUpdateTag(HolderLookup.Provider registryLookup) {
return saveWithoutMetadata(registryLookup);
}1
2
3
4
5
2
3
4
5
Тепер, коли гравець увійде або переміститься в чанк, де існує блок, він одразу побачить правильне значення лічильника.
Тикери
Інтерфейс 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
6
2
3
4
5
6
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
4
2
3
4
Не забудьте серіалізувати та десеріалізувати це поле!
Тепер ми можемо використовувати 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

