Skip to content

Commit

Permalink
Block Entities article (#238)
Browse files Browse the repository at this point in the history
  • Loading branch information
natri0 authored Dec 30, 2024
1 parent 4767872 commit 4871192
Show file tree
Hide file tree
Showing 12 changed files with 298 additions and 0 deletions.
4 changes: 4 additions & 0 deletions .vitepress/sidebars/develop.ts
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,10 @@ export default [
text: "develop.blocks.blockstates",
link: "/develop/blocks/blockstates",
},
{
text: "develop.blocks.block-entities",
link: "/develop/blocks/block-entities",
}
],
},
{
Expand Down
112 changes: 112 additions & 0 deletions develop/blocks/block-entities.md
Original file line number Diff line number Diff line change
@@ -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:
```

:::
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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.
Expand All @@ -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());
});
}

Expand Down
Original file line number Diff line number Diff line change
@@ -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<? 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);
}

// :::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 <T extends BlockEntity> BlockEntityTicker<T> getTicker(World world, BlockState state, BlockEntityType<T> type) {
return validateTicker(type, ModBlockEntities.COUNTER_BLOCK_ENTITY, CounterBlockEntity::tick);
}

// :::3

// :::1
}
// :::1
Original file line number Diff line number Diff line change
Expand Up @@ -9,19 +9,26 @@

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<EngineBlockEntity> ENGINE_BLOCK_ENTITY =
register("engine", EngineBlockEntity::new, ModBlocks.ENGINE_BLOCK);

// :::1
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,
BlockEntityType.BlockEntityFactory<? extends T> entityFactory,
Block... blocks) {
Identifier id = Identifier.of(FabricDocsReference.MOD_ID, name);
return Registry.register(Registries.BLOCK_ENTITY_TYPE, id, BlockEntityType.Builder.<T>create(entityFactory, blocks).build());
}

// :::1

public static void initialize() {
}
}
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"variants": {
"": {
"model": "fabric-docs-reference:block/counter_block"
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"parent": "block/cube_all",
"textures": {
"all": "fabric-docs-reference:block/counter_block"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"parent": "fabric-docs-reference:block/counter_block"
}
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions sidebar_translations.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down

0 comments on commit 4871192

Please sign in to comment.