diff --git a/.vitepress/sidebars/develop.ts b/.vitepress/sidebars/develop.ts index 462c7ae13..e906ad1be 100644 --- a/.vitepress/sidebars/develop.ts +++ b/.vitepress/sidebars/develop.ts @@ -103,6 +103,10 @@ export default [ text: "develop.blocks.blockstates", link: "/develop/blocks/blockstates", }, + { + text: "develop.blocks.block-entities", + link: "/develop/blocks/block-entities", + } ], }, { diff --git a/develop/blocks/block-entities.md b/develop/blocks/block-entities.md new file mode 100644 index 000000000..3660655e9 --- /dev/null +++ b/develop/blocks/block-entities.md @@ -0,0 +1,112 @@ +--- +title: Block Entities +description: Learn how to create block entities for your custom blocks. +authors: + - natri0 +--- + +# Block Entities {#block-entities} + +Block entities are a way to store additional data for a block, that is not part of the block state: inventory contents, custom name and so on. +Minecraft uses block entities for blocks like chests, furnaces, and command blocks. + +As an example, we will create a block that counts how many times it has been right-clicked. + +## Creating the Block Entity {#creating-the-block-entity} + +To make Minecraft recognize and load the new block entities, we need to create a block entity type. This is done by extending the `BlockEntity` class and registering it in a new `ModBlockEntities` class. + +@[code transcludeWith=:::1](@/reference/latest/src/main/java/com/example/docs/block/entity/custom/CounterBlockEntity.java) + +Registering a `BlockEntity` yields a `BlockEntityType` like the `COUNTER_BLOCK_ENTITY` we've used above: + +@[code transcludeWith=:::1](@/reference/latest/src/main/java/com/example/docs/block/entity/ModBlockEntities.java) + +::: tip +Note how the constructor of the `CounterBlockEntity` takes two parameters, but the `BlockEntity` constructor takes three: the `BlockEntityType`, the `BlockPos`, and the `BlockState`. +If we didn't hard-code the `BlockEntityType`, the `ModBlockEntities` class wouldn't compile! This is because the `BlockEntityFactory`, which is a functional interface, describes a function that only takes two parameters, just like our constructor. +::: + +## Creating the Block {#creating-the-block} + +Next, to actually use the block entity, we need a block that implements `BlockEntityProvider`. Let's create one and call it `CounterBlock`. + +::: tip +There's two ways to approach this: + +- create a block that extends `BlockWithEntity` and implement the `createBlockEntity` method (_and_ the `getRenderType` method, since `BlockWithEntity` makes it invisible by default) +- create a block that implements `BlockEntityProvider` by itself and override the `createBlockEntity` method + +We'll use the first approach in this example, since `BlockWithEntity` also provides some nice utilities. +::: + +@[code transcludeWith=:::1](@/reference/latest/src/main/java/com/example/docs/block/custom/CounterBlock.java) + +Using `BlockWithEntity` as the parent class means we also need to implement the `createCodec` method, which is rather easy. + +Unlike blocks, which are singletons, a new block entity is created for every instance of the block. This is done with the `createBlockEntity` method, which takes the position and `BlockState`, and returns a `BlockEntity`, or `null` if there shouldn't be one. + +Don't forget to register the block in the `ModBlocks` class, just like in the [Creating Your First Block](../blocks/first-block) guide: + +@[code transcludeWith=:::5](@/reference/latest/src/main/java/com/example/docs/block/ModBlocks.java) + +## Using the Block Entity {#using-the-block-entity} + +Now that we have a block entity, we can use it to store the number of times the block has been right-clicked. We'll do this by adding a `clicks` field to the `CounterBlockEntity` class: + +@[code transcludeWith=:::2](@/reference/latest/src/main/java/com/example/docs/block/entity/custom/CounterBlockEntity.java) + +The `markDirty` method, used in `incrementClicks`, tells the game that this entity's data has been updated; this will be useful when we add the methods to serialize the counter and load it back from the save file. + +Next, we need to increment this field every time the block is right-clicked. This is done by overriding the `onUse` method in the `CounterBlock` class: + +@[code transcludeWith=:::2](@/reference/latest/src/main/java/com/example/docs/block/custom/CounterBlock.java) + +Since the `BlockEntity` is not passed into the method, we use `world.getBlockEntity(pos)`, and if the `BlockEntity` is not valid, return from the method. + +!["You've clicked the block for the 6th time" message on screen after right-clicking](/assets/develop/blocks/block_entities_1.png) + +## Saving and Loading Data {#saving-loading} + +Now that we have a functional block, we should make it so that the counter doesn't reset between game restarts. This is done by serializing it into NBT when the game saves, and deserializing when it's loading. + +Serialization is done with the `writeNbt` method: + +@[code transcludeWith=:::3](@/reference/latest/src/main/java/com/example/docs/block/entity/custom/CounterBlockEntity.java) + +Here, we add the fields that should be saved into the passed `NbtCompound`: in the case of the counter block, that's the `clicks` field. + +Reading is similar, but instead of saving to the `NbtCompound` you get the values you saved previously, and save them in the BlockEntity's fields: + +@[code transcludeWith=:::4](@/reference/latest/src/main/java/com/example/docs/block/entity/custom/CounterBlockEntity.java) + +Now, if we save and reload the game, the counter block should continue from where it left off when saved. + +## Tickers {#tickers} + +The `BlockEntityProvider` interface also defines a method called `getTicker`, which can be used to run code every tick for each instance of the block. We can implement that by creating a static method that will be used as the `BlockEntityTicker`: + +The `getTicker` method should also check if the passed `BlockEntityType` is the same as the one we're using, and if it is, return the function that will be called every tick. Thankfully, there is a utility function that does the check in `BlockWithEntity`: + +@[code transcludeWith=:::3](@/reference/latest/src/main/java/com/example/docs/block/custom/CounterBlock.java) + +`CounterBlockEntity::tick` is a reference to the static method `tick` we should create in the `CounterBlockEntity` class. Structuring it like this is not required, but it's a good practice to keep the code clean and organized. + +Let's say we want to make it so that the counter can only be incremented once every 10 ticks (2 times a second). We can do this by adding a `ticksSinceLast` field to the `CounterBlockEntity` class, and increasing it every tick: + +@[code transcludeWith=:::5](@/reference/latest/src/main/java/com/example/docs/block/entity/custom/CounterBlockEntity.java) + +Don't forget to serialize and deserialize this field! + +Now we can use `ticksSinceLast` to check if the counter can be increased in `incrementClicks`: + +@[code transcludeWith=:::6](@/reference/latest/src/main/java/com/example/docs/block/entity/custom/CounterBlockEntity.java) + +::: tip +If the block entity does not seem to tick, try checking the registration code! It should pass the blocks that are valid for this entity into the `BlockEntityType.Builder`, or else it will give a warning in the console: + +```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: +``` + +::: diff --git a/public/assets/develop/blocks/block_entities_1.png b/public/assets/develop/blocks/block_entities_1.png new file mode 100644 index 000000000..3a7174bdb Binary files /dev/null and b/public/assets/develop/blocks/block_entities_1.png differ diff --git a/reference/latest/src/main/java/com/example/docs/block/ModBlocks.java b/reference/latest/src/main/java/com/example/docs/block/ModBlocks.java index 53e54746c..de1c61733 100644 --- a/reference/latest/src/main/java/com/example/docs/block/ModBlocks.java +++ b/reference/latest/src/main/java/com/example/docs/block/ModBlocks.java @@ -13,6 +13,7 @@ import net.fabricmc.fabric.api.itemgroup.v1.ItemGroupEvents; import com.example.docs.FabricDocsReference; +import com.example.docs.block.custom.CounterBlock; import com.example.docs.block.custom.EngineBlock; import com.example.docs.block.custom.PrismarineLampBlock; import com.example.docs.item.ModItems; @@ -49,6 +50,12 @@ public class ModBlocks { new EngineBlock(AbstractBlock.Settings.create()), "engine", true ); + // :::5 + public static final Block COUNTER_BLOCK = register( + new CounterBlock(AbstractBlock.Settings.create()), "counter_block", true + ); + // :::5 + // :::1 public static Block register(Block block, String name, boolean shouldRegisterItem) { // Register the block and its item. @@ -75,6 +82,7 @@ public static void initialize() { ItemGroupEvents.modifyEntriesEvent(ModItems.CUSTOM_ITEM_GROUP_KEY).register((itemGroup) -> { itemGroup.add(ModBlocks.CONDENSED_OAK_LOG.asItem()); itemGroup.add(ModBlocks.PRISMARINE_LAMP.asItem()); + itemGroup.add(ModBlocks.COUNTER_BLOCK.asItem()); }); } diff --git a/reference/latest/src/main/java/com/example/docs/block/custom/CounterBlock.java b/reference/latest/src/main/java/com/example/docs/block/custom/CounterBlock.java new file mode 100644 index 000000000..440d35a37 --- /dev/null +++ b/reference/latest/src/main/java/com/example/docs/block/custom/CounterBlock.java @@ -0,0 +1,72 @@ +package com.example.docs.block.custom; + +import com.mojang.serialization.MapCodec; +import org.jetbrains.annotations.Nullable; + +import net.minecraft.block.BlockRenderType; +import net.minecraft.block.BlockState; +import net.minecraft.block.BlockWithEntity; +import net.minecraft.block.entity.BlockEntity; +import net.minecraft.block.entity.BlockEntityTicker; +import net.minecraft.block.entity.BlockEntityType; +import net.minecraft.entity.player.PlayerEntity; +import net.minecraft.text.Text; +import net.minecraft.util.ActionResult; +import net.minecraft.util.hit.BlockHitResult; +import net.minecraft.util.math.BlockPos; +import net.minecraft.world.World; + +import com.example.docs.block.entity.ModBlockEntities; +import com.example.docs.block.entity.custom.CounterBlockEntity; + +// :::1 +public class CounterBlock extends BlockWithEntity { + public CounterBlock(Settings settings) { + super(settings); + } + + @Override + protected MapCodec 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); + } + + // :::1 + + // :::2 + @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; + } + + // :::2 + + // :::3 + @Nullable + @Override + public BlockEntityTicker getTicker(World world, BlockState state, BlockEntityType type) { + return validateTicker(type, ModBlockEntities.COUNTER_BLOCK_ENTITY, CounterBlockEntity::tick); + } + + // :::3 + + // :::1 +} +// :::1 diff --git a/reference/latest/src/main/java/com/example/docs/block/entity/ModBlockEntities.java b/reference/latest/src/main/java/com/example/docs/block/entity/ModBlockEntities.java index dd996036f..8d38b4289 100644 --- a/reference/latest/src/main/java/com/example/docs/block/entity/ModBlockEntities.java +++ b/reference/latest/src/main/java/com/example/docs/block/entity/ModBlockEntities.java @@ -9,12 +9,17 @@ import com.example.docs.FabricDocsReference; import com.example.docs.block.ModBlocks; +import com.example.docs.block.entity.custom.CounterBlockEntity; import com.example.docs.block.entity.custom.EngineBlockEntity; public class ModBlockEntities { public static final BlockEntityType ENGINE_BLOCK_ENTITY = register("engine", EngineBlockEntity::new, ModBlocks.ENGINE_BLOCK); + // :::1 + public static final BlockEntityType COUNTER_BLOCK_ENTITY = + register("counter", CounterBlockEntity::new, ModBlocks.COUNTER_BLOCK); + private static BlockEntityType register(String name, BlockEntityType.BlockEntityFactory entityFactory, Block... blocks) { @@ -22,6 +27,8 @@ private static BlockEntityType register(String name, return Registry.register(Registries.BLOCK_ENTITY_TYPE, id, BlockEntityType.Builder.create(entityFactory, blocks).build()); } + // :::1 + public static void initialize() { } } diff --git a/reference/latest/src/main/java/com/example/docs/block/entity/custom/CounterBlockEntity.java b/reference/latest/src/main/java/com/example/docs/block/entity/custom/CounterBlockEntity.java new file mode 100644 index 000000000..856d0b185 --- /dev/null +++ b/reference/latest/src/main/java/com/example/docs/block/entity/custom/CounterBlockEntity.java @@ -0,0 +1,78 @@ +package com.example.docs.block.entity.custom; + +import net.minecraft.block.BlockState; +import net.minecraft.block.entity.BlockEntity; +import net.minecraft.nbt.NbtCompound; +import net.minecraft.registry.RegistryWrapper; +import net.minecraft.util.math.BlockPos; +import net.minecraft.world.World; + +import com.example.docs.block.entity.ModBlockEntities; + +// :::1 +public class CounterBlockEntity extends BlockEntity { + // :::1 + + // :::2 + private int clicks = 0; + // :::2 + + private int ticksSinceLast = 0; + + // :::1 + public CounterBlockEntity(BlockPos pos, BlockState state) { + super(ModBlockEntities.COUNTER_BLOCK_ENTITY, pos, state); + } + + // :::1 + + // :::2 + public int getClicks() { + return clicks; + } + + public void incrementClicks() { + // :::2 + + // :::6 + if (ticksSinceLast < 10) return; + ticksSinceLast = 0; + // :::6 + + // :::2 + clicks++; + markDirty(); + } + + // :::2 + + // :::3 + @Override + protected void writeNbt(NbtCompound nbt, RegistryWrapper.WrapperLookup registryLookup) { + nbt.putInt("clicks", clicks); + + super.writeNbt(nbt, registryLookup); + } + + // :::3 + + // :::4 + @Override + protected void readNbt(NbtCompound nbt, RegistryWrapper.WrapperLookup registryLookup) { + super.readNbt(nbt, registryLookup); + + clicks = nbt.getInt("clicks"); + } + + // :::4 + + // :::5 + public static void tick(World world, BlockPos blockPos, BlockState blockState, CounterBlockEntity entity) { + entity.ticksSinceLast++; + } + + // :::5 + + // :::1 +} +// :::1 diff --git a/reference/latest/src/main/resources/assets/fabric-docs-reference/blockstates/counter_block.json b/reference/latest/src/main/resources/assets/fabric-docs-reference/blockstates/counter_block.json new file mode 100644 index 000000000..965ea3a82 --- /dev/null +++ b/reference/latest/src/main/resources/assets/fabric-docs-reference/blockstates/counter_block.json @@ -0,0 +1,7 @@ +{ + "variants": { + "": { + "model": "fabric-docs-reference:block/counter_block" + } + } +} diff --git a/reference/latest/src/main/resources/assets/fabric-docs-reference/models/block/counter_block.json b/reference/latest/src/main/resources/assets/fabric-docs-reference/models/block/counter_block.json new file mode 100644 index 000000000..17c0a9f57 --- /dev/null +++ b/reference/latest/src/main/resources/assets/fabric-docs-reference/models/block/counter_block.json @@ -0,0 +1,6 @@ +{ + "parent": "block/cube_all", + "textures": { + "all": "fabric-docs-reference:block/counter_block" + } +} \ No newline at end of file diff --git a/reference/latest/src/main/resources/assets/fabric-docs-reference/models/item/counter_block.json b/reference/latest/src/main/resources/assets/fabric-docs-reference/models/item/counter_block.json new file mode 100644 index 000000000..d02572e91 --- /dev/null +++ b/reference/latest/src/main/resources/assets/fabric-docs-reference/models/item/counter_block.json @@ -0,0 +1,3 @@ +{ + "parent": "fabric-docs-reference:block/counter_block" +} \ No newline at end of file diff --git a/reference/latest/src/main/resources/assets/fabric-docs-reference/textures/block/counter_block.png b/reference/latest/src/main/resources/assets/fabric-docs-reference/textures/block/counter_block.png new file mode 100644 index 000000000..5d1ed0de5 Binary files /dev/null and b/reference/latest/src/main/resources/assets/fabric-docs-reference/textures/block/counter_block.png differ diff --git a/sidebar_translations.json b/sidebar_translations.json index bdc18a1de..7445a19cd 100644 --- a/sidebar_translations.json +++ b/sidebar_translations.json @@ -33,6 +33,7 @@ "develop.blocks": "Blocks", "develop.blocks.first-block": "Creating Your First Block", "develop.blocks.blockstates": "Block States", + "develop.blocks.block-entities": "Block Entities", "develop.entities": "Entities", "develop.entities.effects": "Status Effects", "develop.entities.damage-types": "Damage Types",