Block Entitäten sind ein Weg für Blöcke zusätzliche Daten, die nicht Teil des Blockzustands sind, zu speichern: Inventarinhalte, benutzerdefinierter Name und so weiter. Minecraft nutzt Block Entitäten für Blöcke, wie Kisten, Öfen und Befehlsblöcke.
Als Beispiel werden wir einen Block erstellen, der zählt, wie oft er mit der rechten Maustaste angeklickt wurde.
Erstellen der Block Entität
Damit Minecraft die neuen Block Entitäten erkennt und lädt, müssen wir einen Block Entität Typen erstellen. Das machen wir, indem wir die BlockEntity Klasse erweitern und in einer neuen ModBlockEntities Klasse registrieren.
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
Wenn eine BlockEntity registriert wird, gibt es einen BlockEntityType zurück, wie bei dem COUNTER_BLOCK_ENTITY, welches wir oben benutzt haben:
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
Man beachte, dass der Konstruktor von der CounterBlockEntity zwei Parameter benötigt, der Konstruktor von der BlockEntity jedoch drei: den BlockEntityType, BlockPos und den BlockState. Wenn wir den BlockEntityType nicht hart kodiert hätten, würde die Klasse ModBlockEntities nicht kompiliert werden! Das liegt daran, dass die BlockEntityFactory, die ein funktionales Interface ist, eine Funktion beschreibt, die nur zwei Parameter benötigt, genau wie unser Konstruktor.
Erstellen des Blocks
Um als Nächstes die Blockentität zu nutzen, brauchen wir einen Block, der EntityBlock implementiert. Lass uns einen erstellen und diesen CounterBlock nennen.
TIP
Es gibt zwei Wege, um dies zu erreichen:
- Einen Block erstellen, der von
BaseEntityBlockerbt und die MethodecreateBlockEntityimplementiert - Einen Block erstellen, der
EntityBlockimplementiert und die MethodecreateBlockEntityüberschreibt
Wir werden in diesem Beispiel den ersten Weg nutzen, da BaseEntityBlock ein paar nützliche Hilfsfunktionen bietet.
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
Die Verwendung von BaseEntityBlock als übergeordnete Klasse bedeutet, dass wir auch die Methode createCodec implementieren müssen, was relativ einfach ist.
Im Gegensatz zu Blöcken, die Singletons sind, wird für jede Instanz des Blocks eine neue Blockentität erstellt. Dies geschieht mit der Methode createBlockEntity, die die Position und den BlockState entgegennimmt und ein BlockEntity zurückgibt, oder null, wenn es keins geben sollte.
Vergiss nicht, den Block in der Klasse ModBlocks zu registrieren, genau wie in der Anleitung Deinen ersten Block erstellen:
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
Nutzen der Block Entität
Jetzt, da wir eine Blockentität haben, können wir sie verwenden, um die Anzahl der Rechtsklicks auf den Block zu speichern. Dafür werden wir der Klasse CounterBlockEntity ein Feld clicks hinzufügen:
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
Die Methode setChanged, die in incrementClicks verwendet wird, teilt dem Spiel mit, dass die Daten dieser Entität aktualisiert wurden; dies wird nützlich sein, wenn wir die Methoden hinzufügen, um den Zähler zu serialisieren und ihn aus der Speicherdatei zurückzuladen.
Als Nächstes müssen wir dieses Feld jedes Mal erhöhen, wenn der Block mit der rechten Maustaste angeklickt wird. Dies geschieht indem die Methode useWithoutItem in der Klasse CounterBlock überschrieben wird:
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
Da die BlockEntity nicht an die Methode übergeben wird, verwenden wir level.getBlockEntity(pos), und wenn die BlockEntity nicht gültig ist, kehren wir aus der Methode zurück.

Speichern und Laden von Daten
Da wir nun einen funktionierenden Block haben, sollten wir dafür sorgen, dass der Zähler zwischen den Neustarts des Spiels nicht zurückgesetzt wird. Dies geschieht durch Serialisierung in NBT, wenn das Spiel speichert, und Deserialisierung, wenn es geladen wird.
Das Speichern in NBT erfolgt über ValueInputs und ValueOutputs. Diese Views sind für die Speicherung von Fehlern bei der Kodierung/Dekodierung und die Verfolgung von Registrierungen während des gesamten Serialisierungsprozesses verantwortlich.
Du kannst aus einem ValueInput mit der Methode read lesen, indem du einen Codec für den gewünschten Typ übergibst. Ebenso kannst du in einen ValueOutput schreiben, indem du die Methode store verwendest und einen Codec für den Typ sowie den Wert übergibst.
Es gibt auch Methoden für primitive Datentypen, wie z. B. getInt, getShort, getBoolean usw. zum Lesen und putInt, putShort, putBoolean usw. zum Schreiben. Die View bietet auch Methoden für das Arbeiten mit Listen, nullbaren Typen und verschachtelten Objekten.
Die Serialisierung erfolgt mit der Methode 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
Hier fügen wir die Felder hinzu, die in dem übergebenen ValueOutput gespeichert werden sollen: Im Fall des Zählerblocks ist es das Feld clicks.
Das Lesen funktioniert ähnlich, indem du die zuvor gespeicherten Werte aus dem ValueInput abrufst und in den Feldern der BlockEntity speicherst:
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
Wenn wir nun speichern und das Spiel neu laden, sollte der Zählerblock dort weitermachen, wo er beim Speichern aufgehört hat.
Obwohl saveAdditional und loadAdditional das Speichern und Laden auf und von der Festplatte regeln, gibt es noch ein Problem:
- Der Server weiß den korrekten
clicksWert. - Der Client erhält nicht den korrekten Wert, wenn der Chunk geladen wird.
Um dies zu beheben, überschreiben wir getUpdateTag:
java
@Override
public CompoundTag getUpdateTag(HolderLookup.Provider registryLookup) {
return saveWithoutMetadata(registryLookup);
}1
2
3
4
5
2
3
4
5
Wenn sich nun ein Spieler anmeldet oder in einen Chunk geht, in dem der Block vorhanden ist, sieht er sofort den korrekten Zählerwert.
Ticker
Das Interface EntityBlock definiert auch eine Methode namens getTicker, mit der für jede Instanz des Blocks bei jedem Tick Code ausgeführt werden kann. Wir können das implementieren, indem wir eine statische Methode erstellen, die als BlockEntityTicker verwendet wird:
Die Methode getTicker sollte auch prüfen, ob der übergebene BlockEntityType derselbe ist wie der, den wir verwenden, und wenn ja, die Funktion zurückgeben, die bei jedem Tick aufgerufen wird. Glücklicherweise gibt es eine Hilfsfunktion, die diese Prüfung in BaseEntityBlock durchführt:
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 ist ein Verweis auf die statische Methode tick, die wir in der Klasse CounterBlockEntity erstellen sollten. Eine solche Strukturierung ist nicht erforderlich, aber es ist eine gute Praxis, um den Code sauber und übersichtlich zu halten.
Nehmen wir an, wir wollen, dass der Zähler nur alle 10 Ticks (2 Mal pro Sekunde) erhöht werden kann. Wir können dies tun, indem wir der Klasse CounterBlockEntity ein Feld ticksSinceLast hinzufügen und es bei jedem Tick erhöhen:
java
public static void tick(Level level, BlockPos blockPos, BlockState blockState, CounterBlockEntity entity) {
entity.ticksSinceLast++;
}1
2
3
4
2
3
4
Vergiss nicht, dieses Feld zu serialisieren und zu deserialisieren!
Jetzt können wir ticksSinceLast verwenden, um zu prüfen, ob der Zähler in incrementClicks erhöht werden kann:
java
if (ticksSinceLast < 10) return;
ticksSinceLast = 0;1
2
2
TIP
Wenn die Blockentität nicht zu ticken scheint, überprüfe den Registrierungscode! Es sollte die Blöcke, die für diese Entität gültig sind, an den BlockEntityType.Builder, übergeben, sonst wird eine Warnung in der Konsole ausgegeben:
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

