I blocchi-entità sono un modo per memorizzare dati aggiuntivi per un blocco, che non siano parte dello stato del blocco: contenuti dell'inventario, nome personalizzato e così via. Minecraft usa i blocchi-entità per blocchi come bauli, fornaci, e blocchi dei comandi.
Come esempio, creeremo un blocco che conta quante volte esso è stato cliccato con il tasto destro.
Creare il Blocco-Entità
Per fare in modo che Minecraft riconosca e carichi i nuovi blocchi-entità, dobbiamo creare un tipo di blocco-entità. Questo si fa estendendo la classe BlockEntity e registrandola in una nuova classe 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
La registrazione di un BlockEntity produce un BlockEntityType come il COUNTER_BLOCK_ENTITY che abbiamo usato sopra:
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
Nota come il costruttore del CounterBlockEntity accetti due parametri, ma il costruttore del BlockEntity ne accetti tre: il BlockEntityType, la BlockPos, e lo BlockState. Se non fissassimo nel codice il BlockEntityType, la classe ModBlockEntities non compilerebbe! Questo perché la BlockEntityFactory, che è un'interfaccia funzionale, descrive una funzione che accetta solo due parametri, proprio come il nostro costruttore.
Creare il Blocco
Dopo di che, per usare effettivamente il blocco-entità, ci serve un blocco che implementi EntityBlock. Creiamone uno e chiamiamolo CounterBlock.
TIP
Ci sono due modi per approcciarsi a questo:
- creare un blocco che estenda
BaseEntityBlocke implementi il metodocreateBlockEntity - creare un blocco che implementi
EntityBlockper sé e faccia override del metodocreateBlockEntity
Useremo il primo approccio in questo esempio, poiché BaseEntityBlock fornisce anche alcune utilità comode.
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
Usare BaseEntityBlock come classe genitore significa che dobbiamo anche implementare il metodo createCodec, il che è piuttosto semplice.
A differenza dei blocchi, che sono dei singleton, viene creato un nuovo blocco-entità per ogni istanza del blocco. Questo viene fatto con il metodo createBlockEntity, che accetta la posizione e il BlockState, e restituisce un BlockEntity, o null se non ce ne dovrebbe essere uno.
Non dimenticare di registrare il blocco nella classe ModBlocks, proprio come nella guida Creare il Tuo Primo Blocco:
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
Usare il Blocco-Entità
Ora che abbiamo un blocco-entità, possiamo usarlo per memorizzare il numero di clic con il tasto destro sul blocco. Faremo questo aggiungendo un attributo clicks alla classe CounterBlockEntity:
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
Il metodo setChanged, usato in incrementClicks, informa il gioco che i dati dell'entità sono stati aggiornati; questo sarà utile quando aggiungeremo i metodi per serializzare il contatore e ricaricarlo dal file di salvataggio.
Il prossimo passaggio è incrementare questo attributo ogni volta che il blocco viene cliccato con il tasto destro. Questo si fa facendo override del metodo useWithoutItem nella classe CounterBlock:
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
Poiché il BlockEntity non viene passato nel metodo, usiamo level.getBlockEntity(pos), e se il BlockEntity non è valido, usciamo dal metodo.

Salvare e Caricare i Dati
Ora che abbiamo un blocco funzionante, dovremmo anche fare in modo che il contatore non si resetti dopo un riavvio del gioco. Questo si fa serializzandolo in NBT quando si salva il gioco, e deserializzandolo durante il caricamento.
Il salvataggio a NBT viene fatto tramite ValueInput e ValueOutput. Queste View sono responsabili di memorizzare errori dall'encoding/decoding, e di tener conto delle registry in tutto il processo di serializzazione.
Puoi leggere da una ValueInput con il metodo read, passando un Codec per il tipo desiderato. Allo stesso modo, puoi scrivere a una ValueOutput usando il metodo store, passando un Codec per il tipo, e il valore.
Ci sono anche metodi per i primitivi, come getInt, getShort, getBoolean... per la lettura e putInt, putShort, putBoolean... per la scrittura. La View fornisce anche metodi per lavorare con liste, tipi nullabili, e oggetti annidati.
La serializzazione avviene con il metodo 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
Qui, aggiungiamo gli attributi che dovrebbero essere salvati al ValueOutput passato: nel caso del blocco contatore, l'attributo clicks.
La lettura è simile, si ottengono i valori salvati precendentemente dal ValueInput, e salvarli negli attributi del BlockEntity:
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
Ora, salvando e ricaricando il gioco, il blocco contatore dovrebbe riprendere da dove è stato salvato.
Anche se saveAdditional e loadAdditional gestiscono il salvataggio e il caricamento al disco, c'è ancora un problema:
- Il server conosce il valore corretto di
clicks. - Il client non riceve il valore corretto quando carica un chunk.
Per risolvere questo, facciamo override di getUpdateTag:
java
@Override
public CompoundTag getUpdateTag(HolderLookup.Provider registryLookup) {
return saveWithoutMetadata(registryLookup);
}1
2
3
4
5
2
3
4
5
Ora, quando un giocatore accede o si muove in un chunk in cui il blocco esiste, vedrà subito il valore corretto del contatore.
Ticker
L'interfaccia EntityBlock definisce anche un metodo chiamato getTicker, che può essere usato per eseguire del codice ogni tick per ogni istanza del blocco. Possiamo implementarlo creando un metodo statico che verrà usato come BlockEntityTicker:
Il metodo getTicker dovrebbe anche controllare che il BlockEntityType passato sia quello che stiamo usando; se lo è, restituirà la funzione da chiamare a ogni tick. Vi è una funzione di utilità che fa il controllo in BaseEntityBlock:
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 è un riferimento al metodo statico tick che dobbiamo creare nella classe CounterBlockEntity. Non è necessario seguire questa struttura, ma buona pratica per mantenere del codice pulito e organizzato.
Diciamo di voler fare in modo che il contatore possa essere incrementato soltanto ogni 10 tick (2 volte al secondo). Possiamo fare ciò aggiungendo un attributo ticksSinceLast alla classe CounterBlockEntity, e incrementandolo a ogni tick:
java
public static void tick(Level level, BlockPos blockPos, BlockState blockState, CounterBlockEntity entity) {
entity.ticksSinceLast++;
}1
2
3
4
2
3
4
Non dimenticare di serializzare e deserializzare questo attributo!
Ora possiamo usare ticksSinceLast per controllare se il contatore può essere incrementato in incrementClicks:
java
if (ticksSinceLast < 10) return;
ticksSinceLast = 0;1
2
2
TIP
Se sembra che il blocco-entità non faccia tick, prova a controllare il codice della registrazione! Dovrebbe passare i blocchi validi per questa entità nel BlockEntityType.Builder, o altrimenti scriverà un avviso nella console:
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

