Блоки-сутності — це спосіб зберігати додаткові дані для блоку, які не є частиною стану блоку: вміст інвентарю, спеціальна назва тощо. 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.of(FabricDocsReference.MOD_ID, name);
return Registry.register(Registries.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, який є функціональним інтерфейсом, описує функцію, яка приймає лише два параметри, як і наш конструктор.
Створення блоку
Далі, щоб фактично використовувати блок-сутність нам потрібен блок, який реалізує BlockEntityProvider. Нумо створімо один і назвемо його CounterBlock.
TIP
Є два способи підійти до цього:
- створити блок, який розширює
BlockWithEntityі реалізувати методcreateBlockEntity - створити блок, який сам по собі реалізує
BlockEntityProviderі замінити методcreateBlockEntity
У цьому прикладі ми використаємо перший підхід, оскільки BlockWithEntity також надає кілька крутих штук.
java
public class CounterBlock extends BlockWithEntity {
public CounterBlock(Settings settings) {
super(settings);
}
@Override
protected MapCodec<? extends BlockWithEntity> getCodec() {
return createCodec(CounterBlock::new);
}
@Nullable
@Override
public BlockEntity createBlockEntity(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
Використання BlockWithEntity як батьківського класу означає, що нам також потрібно реалізувати метод createCodec, який досить простий.
На відміну від блоків, які є одинарними, нова блок-сутність створюється для кожного екземпляра блоку. Це робиться за допомогою методу createBlockEntity, який приймає позицію та BlockState і повертає BlockEntity або null, якщо його не має бути.
Не забудьте зареєструвати блок у класі ModBlocks, як у посібнику Створення вашого першого блоку:
java
public static final Block COUNTER_BLOCK = register(
"counter_block",
CounterBlock::new,
AbstractBlock.Settings.create(),
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++;
markDirty();
}1
2
3
4
5
6
7
8
9
10
2
3
4
5
6
7
8
9
10
Метод markDirty, який використовується в incrementClicks, повідомляє грі, що дані цієї сутності було оновлено; це буде корисно, коли ми додамо методи серіалізації лічильника та завантаження його назад із файлу збереження.
Далі нам потрібно збільшувати це поле кожного разу, коли по блоку натискають ПКМ. Це робиться шляхом перевизначення методу onUse в класі CounterBlock:
java
@Override
protected ActionResult onUse(BlockState state, World world, BlockPos pos, PlayerEntity player, BlockHitResult hit) {
if (!(world.getBlockEntity(pos) instanceof CounterBlockEntity counterBlockEntity)) {
return super.onUse(state, world, pos, player, hit);
}
counterBlockEntity.incrementClicks();
player.sendMessage(Text.literal("You've clicked the block for the " + counterBlockEntity.getClicks() + "th time."), true);
return ActionResult.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 не передається в метод, ми використовуємо world.getBlockEntity(pos), і якщо BlockEntity недійсний, повертаємося з методу.

Збереження і завантаження даних
Тепер, коли у нас є функціональний блок, ми повинні зробити так, щоб лічильник не обнулявся між перезавантаженнями гри. Це робиться шляхом серіалізації в NBT під час збереження гри та десеріалізації під час завантаження.
Серіалізація виконується за допомогою методу writeNbt:
java
@Override
protected void writeData(WriteView writeView) {
writeView.putInt("clicks", clicks);
super.writeData(writeView);
}1
2
3
4
5
6
7
2
3
4
5
6
7
Тут ми додаємо поля, які слід зберегти в переданому NbtCompound: у випадку блоку лічильника це поле clicks.
Читання відбувається подібно, але замість збереження в NbtCompound ви отримуєте значення, які ви зберегли раніше, і зберігаєте їх у полях BlockEntity:
java
@Override
protected void readData(ReadView readView) {
super.readData(readView);
clicks = readView.getInt("clicks", 0);
}1
2
3
4
5
6
7
2
3
4
5
6
7
Тепер, якщо ми збережемо та перезавантажимо гру, блок лічильника має продовжуватися з того місця, на якому він зупинився під час збереження.
У той час як writeNbt і readNbt керують збереженням і завантаженням на диск і з диска, все ще є проблема:
- Сервер знає правильне значення «натискань».
- Клієнт не отримує правильне значення під час завантаження блоку.
Щоб виправити це, ми замінюємо toInitialChunkDataNbt:
java
@Override
public NbtCompound toInitialChunkDataNbt(RegistryWrapper.WrapperLookup registryLookup) {
return createNbt(registryLookup);
}1
2
3
4
5
2
3
4
5
Тепер, коли гравець увійде або переміститься в блок, де існує блок, він одразу побачить правильне значення лічильника.
Тикери
Інтерфейс BlockEntityProvider також визначає метод під назвою getTicker, який можна використовувати для запуску коду кожного тика для кожного екземпляра блоку. Ми можемо реалізувати це, створивши статичний метод, який використовуватиметься як BlockEntityTicker:
Метод getTicker також має перевірити, чи переданий BlockEntityType збігається з тим, який ми використовуємо, і якщо це так, повертати функцію, яка буде викликатися кожного такту. На щастя, є допоміжна функція, яка виконує перевірку в BlockWithEntity:
java
@Nullable
@Override
public <T extends BlockEntity> BlockEntityTicker<T> getTicker(World world, BlockState state, BlockEntityType<T> type) {
return validateTicker(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(World world, 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, інакше він видасть попередження на консолі:
text
[13:27:55] [Server thread/WARN] (Minecraft) Block entity fabric-docs-reference:counter @ BlockPos{x=-29, y=125, z=18} state Block{fabric-docs-reference:counter_block} invalid for ticking:1

