🇺🇦 Українська (Ukrainian - Ukraine)
🇺🇦 Українська (Ukrainian - Ukraine)
Зовнішній вигляд
🇺🇦 Українська (Ukrainian - Ukraine)
🇺🇦 Українська (Ukrainian - Ukraine)
Зовнішній вигляд
Ця сторінка написана для версії:
1.21.4
Ця сторінка написана для версії:
1.21.4
Блоки-сутності — це спосіб зберігати додаткові дані для блоку, які не є частиною стану блоку: вміст інвентарю, спеціальна назва тощо. Minecraft використовує блоки-сутності для блоків, як-от скрині, печі й командні блоки.
Як приклад, ми створимо блок, який підраховує, скільки разів було натиснуто ПКМ.
Щоб змусити Minecraft розпізнавати та завантажувати нові блоки-сутності, нам потрібно створити тип блоку-сутності. Це робиться шляхом розширення класу BlockEntity
і реєстрації його в новому класі ModBlockEntities
.
public class CounterBlockEntity extends BlockEntity {
public CounterBlockEntity(BlockPos pos, BlockState state) {
super(ModBlockEntities.COUNTER_BLOCK_ENTITY, pos, state);
}
}
Реєстрація BlockEntity
дає BlockEntityType
, подібний до COUNTER_BLOCK_ENTITY
, який ми використовували вище:
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());
}
TIP
Зверніть увагу, що конструктор CounterBlockEntity
приймає два параметри, а конструктор BlockEntity
приймає три: BlockEntityType
, BlockPos
і BlockState
. Якби ми не жорстко закодували BlockEntityType
, клас ModBlockEntities
не компілювався б! Це тому, що BlockEntityFactory
, який є функціональним інтерфейсом, описує функцію, яка приймає лише два параметри, як і наш конструктор.
Далі, щоб фактично використовувати блок-сутність нам потрібен блок, який реалізує BlockEntityProvider
. Нумо створімо один і назвемо його CounterBlock
.
TIP
Є два способи підійти до цього:
BlockWithEntity
, і реалізувати метод createBlockEntity
(і метод getRenderType
, оскільки BlockWithEntity
робить його невидимим за замовчуванням)BlockEntityProvider
і замінити метод createBlockEntity
У цьому прикладі ми використаємо перший підхід, оскільки BlockWithEntity
також надає кілька крутих штук.
public class CounterBlock extends BlockWithEntity {
public CounterBlock(Settings settings) {
super(settings);
}
@Override
protected MapCodec<? extends BlockWithEntity> getCodec() {
return createCodec(CounterBlock::new);
}
@Override
protected BlockRenderType getRenderType(BlockState state) {
return BlockRenderType.MODEL;
}
@Nullable
@Override
public BlockEntity createBlockEntity(BlockPos pos, BlockState state) {
return new CounterBlockEntity(pos, state);
}
}
Використання BlockWithEntity
як батьківського класу означає, що нам також потрібно реалізувати метод createCodec
, який досить простий.
На відміну від блоків, які є одинарними, нова блок-сутність створюється для кожного екземпляра блоку. Це робиться за допомогою методу createBlockEntity
, який приймає позицію та BlockState
і повертає BlockEntity
або null
, якщо його не має бути.
Не забудьте зареєструвати блок у класі ModBlocks
, як у посібнику Створення вашого першого блоку:
public static final RegistryKey<Block> COUNTER_BLOCK_KEY = RegistryKey.of(
RegistryKeys.BLOCK,
Identifier.of(FabricDocsReference.MOD_ID, "counter_block")
);
public static final Block COUNTER_BLOCK = register(
new CounterBlock(AbstractBlock.Settings.create().registryKey(COUNTER_BLOCK_KEY)), COUNTER_BLOCK_KEY, true
);
Тепер, коли у нас є блок-сутність, ми можемо використовувати його для збереження кількості натискань ПКМ по блоку. Ми зробимо це, додавши поле clicks
до класу CounterBlockEntity
:
private int clicks = 0;
public int getClicks() {
return clicks;
}
public void incrementClicks() {
clicks++;
markDirty();
}
Метод markDirty
, який використовується в incrementClicks
, повідомляє грі, що дані цієї сутності було оновлено; це буде корисно, коли ми додамо методи серіалізації лічильника та завантаження його назад із файлу збереження.
Далі нам потрібно збільшувати це поле кожного разу, коли по блоку натискають ПКМ. Це робиться шляхом перевизначення методу onUse
в класі CounterBlock
:
@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;
}
Оскільки BlockEntity
не передається в метод, ми використовуємо world.getBlockEntity(pos)
, і якщо BlockEntity
недійсний, повертаємося з методу.
Тепер, коли у нас є функціональний блок, ми повинні зробити так, щоб лічильник не обнулявся між перезавантаженнями гри. Це робиться шляхом серіалізації в NBT під час збереження гри та десеріалізації під час завантаження.
Серіалізація виконується за допомогою методу writeNbt
:
@Override
protected void writeNbt(NbtCompound nbt, RegistryWrapper.WrapperLookup registryLookup) {
nbt.putInt("clicks", clicks);
super.writeNbt(nbt, registryLookup);
}
Тут ми додаємо поля, які слід зберегти в переданому NbtCompound
: у випадку блоку лічильника це поле clicks
.
Читання відбувається подібно, але замість збереження в NbtCompound
ви отримуєте значення, які ви зберегли раніше, і зберігаєте їх у полях BlockEntity:
@Override
protected void readNbt(NbtCompound nbt, RegistryWrapper.WrapperLookup registryLookup) {
super.readNbt(nbt, registryLookup);
clicks = nbt.getInt("clicks");
}
Тепер, якщо ми збережемо та перезавантажимо гру, блок лічильника має продовжуватися з того місця, на якому він зупинився під час збереження.
Інтерфейс BlockEntityProvider
також визначає метод під назвою getTicker
, який можна використовувати для запуску коду кожного тика для кожного екземпляра блоку. Ми можемо реалізувати це, створивши статичний метод, який використовуватиметься як BlockEntityTicker
:
Метод getTicker
також має перевірити, чи переданий BlockEntityType
збігається з тим, який ми використовуємо, і якщо це так, повертати функцію, яка буде викликатися кожного такту. На щастя, є допоміжна функція, яка виконує перевірку в BlockWithEntity
:
@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);
}
CounterBlockEntity::tick
— це посилання на статичний метод tick
, який ми повинні створити в класі CounterBlockEntity
. Структурувати його таким чином не обов’язково, але це гарна практика, щоб код був чистим і організованим.
Скажімо, ми хочемо зробити так, щоб лічильник можна було збільшувати лише один раз кожні 10 тактів (2 рази на секунду). Ми можемо зробити це, додавши поле ticksSinceLast
до класу CounterBlockEntity
і збільшуючи його кожного такту:
public static void tick(World world, BlockPos blockPos, BlockState blockState, CounterBlockEntity entity) {
entity.ticksSinceLast++;
}
Не забудьте серіалізувати та десеріалізувати це поле!
Тепер ми можемо використовувати ticksSinceLast
, щоб перевірити, чи можна збільшити лічильник у incrementClicks
:
if (ticksSinceLast < 10) return;
ticksSinceLast = 0;
TIP
Якщо блок-сутність не працює, спробуйте перевірити реєстраційний код! Він має передати блоки, дійсні для цієї сутності, у BlockEntityType.Builder
, інакше він видасть попередження на консолі:
[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: