Bloco-Entidades 26.2
Aprenda a criar bloco-entidades para seus blocos personalizados.
Bloco-entidades são um jeito de armazenar dados adicionais para um bloco, isso não sendo parte do Bloco de Estado: inventário de conteúdo, nome personalizado etc. Minecraft utiliza bloco-entidades para blocos como baús, fornalhas e blocos de comando.
Como exemplo, nós iremos criar um bloco que conta quantas vezes ele foi ativo.
Criando o Bloco-Entidade
Para fazer o Minecraft reconhecer e carregar o novo Bloco-entidade nós iremos criar um tipo de bloco-entidade. Isso é feito com extensão de classe BlockEntity e registrando como uma nova 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
2
3
4
5
Registrando uma BlockEntity concede a BlockEntityType como o COUNTER_BLOCK_ENTITY nós usamos anteriormente:
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 = ExampleMod.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
2
3
4
5
6
7
8
9
10
11
TIP
Note como a construção do CounterBlockEntity tem dois parâmetros, mas a construção de BlockEntity tem três: o BlockEntityType, o BlockPos, e BlockState. Se nós não fizermos uma codificação rígida em BlocoEntityType, a classe ModBlockEntities não iria compilar! Isso porque o FabricBlockEntityTypeBuilder.Factory, cujo é a interface funcional, descreve uma função que leva dois parâmetros, que nem a nossa construção.
Criando o Bloco
A seguir, para realmente usar o bloco-entidade, nós precisamos de um bloco que implemente o 'EntityBlock'. Vamos criar um e chamá-lo de 'CounterBlock'.
TIP
Existem duas formas de fazer isso:
- criar um bloco que estende
BaseEntityBlocke implementa o métodonewBlockEntity - criar um bloco que implementa
EntityBlocksozinho e substitui o métodonewBlockEntity
Vamos usar o primeiro método nesse exemplo, desde que BaseEntityBlock também nos dá boas utilidades.
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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Usando BaseEntityBlock como a classe-pai significa que também temos que implementar o método createCodec que é bastante fácil.
Diferente de blocos, que são singletons, uma nova entidade de bloco é criada para cada instância de bloco. Isso é feito com o método newBlockEntity, que toma a posição e o BlockState, e retorna a BlockEntity, or null se lá não há algum.
Não se esqueça de adicionar a chave a ModBlockItemIds, assim como no guia Criando Seu Primeiro Bloco:
java
public static final BlockItemId COUNTER_BLOCK = create(
"counter_block"
);1
2
3
2
3
java
public static final Block COUNTER_BLOCK = register(
ModBlockItemIds.COUNTER_BLOCK,
CounterBlock::new,
BlockBehaviour.Properties.of()
);1
2
3
4
5
2
3
4
5
Usando o Bloco-Entidade
Agora que nós temos uma bloco-entidade, nós podemos usar para armazenar o número de vezes que o bloco foi utilizado. Para fazermos isso iremos adicionar o campo clicks a classe CounterBlockEntity:
java
private int clicks = 0;
public int getClicks() {
return this.clicks;
}
public void incrementClicks() {
this.clicks++;
this.setChanged();
}1
2
3
4
5
6
7
8
9
2
3
4
5
6
7
8
9
O método setChanged, utilizado em icrementClicks diz ao jogo que a informação da entidade foi atualizada; isso será útil quando nós adicionarmos métodos para serializar o contador e carregar de volta dos arquivos salvos.
A seguir, nós precisamos incrementar esse campo toda vez que o bloco é utilizado. Isso pode ser feito sobrescrevendo o método useWithoutItem na 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();
if (level.isClientSide()) {
player.sendOverlayMessage(Component.literal("You've clicked the block for the " + counterBlockEntity.getClicks() + "th time."));
}
return InteractionResult.SUCCESS;
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
2
3
4
5
6
7
8
9
10
11
12
13
14
Desde que BlockEntity não é passada para o método, nós usamos level.getBlockEntity(pos), e se o BlockEntity não é válido, retorna para o método.

Salvando e Carregando Dados
Agora que temos um bloco funcional, nós podemos fazer que o contador não reinicie entre reinícios de jogo. Isso será feito serializando isso entre NBT quando o jogo salva, e deserializando quando é carregado.
NBT é salvo pelos ValueInputs and ValueOutputs. Essas views são responsáveis por armazenar erros da codificação/decodificação e acompanhar os registros pelo processo de serialização.
Você pode ler de um ValueInput usando o método read e passando um Codecpara o tipo desejado. Do mesmo modo, você pode escrever um ValueOutput usando o método store, passando um Codec para o tipo e o valor.
Também há métodos para primitivos, como getInt, getShort, getBoolean etc. para leitura e putInt, putShort, putBoolean etc. para escrita. O View também fornece métodos para trabalhar com listas, tipos anuláveis e objetos aninhados.
A serialização é feita com o método saveAdditional:
java
@Override
protected void saveAdditional(ValueOutput output) {
output.putInt("clicks", this.clicks);
super.saveAdditional(output);
}1
2
3
4
5
6
2
3
4
5
6
Aqui, nós adicionamos os campos que devem ser salvos no ValueOutput passado: no caso de um bloco contador, esse será o campo clicks.
Para ler é parecido, você pega os valores do ValueInput que você salvou anteriormente e os salva nos campos do BlockEntity:
java
@Override
protected void loadAdditional(ValueInput input) {
super.loadAdditional(input);
this.clicks = input.getIntOr("clicks", 0);
}1
2
3
4
5
6
2
3
4
5
6
Agora, se nós salvarmos e recarregarmos o jogo, o Bloco Contador deve continuar de onde foi deixado quando foi salvo.
Enquanto saveAdditional and loadAdditional lidam com o salvamento e o carregamento de dados no disco, ainda há um problema:
- O servidor sabe o valor correto de
clicks. - O client não recebe o valor quando baixando um chunk.
Para consertar isso, nós sobrescrevemos getUpdateTag:
java
@Override
public CompoundTag getUpdateTag(HolderLookup.Provider registryLookup) {
return saveWithoutMetadata(registryLookup);
}1
2
3
4
2
3
4
Agora, quando um jogador entra ou se move em um chunk onde o bloco existe, ele irá ver o valor correto.
Sincronizando Dados
Enquanto novos jogadores que estão carregando no bloco verão a contagem correta, o contador não atualizará para os outros jogadores que estão vendo a interação. Esse fenômeno é chamado dessincronização, e ele ocorre quando o servidor atualiza o seu estado, mas o cliente não.
Para resolver isso, nós podemos usar os pacotes de atualização de entidade de bloco. Sobrescreva o método getUpdatePacket e retorne um pacote contendo os dados do bloco do nosso getUpdateTag.
java
@Override
public Packet<ClientGamePacketListener> getUpdatePacket() {
return ClientboundBlockEntityDataPacket.create(this);
}1
2
3
4
2
3
4
Então, sobrescreva setChanged para transmitir os dados sempre que a entidade de bloco mudar.
java
@Override
public void setChanged() {
super.setChanged();
if (level == null) return;
BlockState state = getBlockState();
level.sendBlockUpdated(worldPosition, state, state, Block.UPDATE_ALL);
}1
2
3
4
5
6
7
8
9
2
3
4
5
6
7
8
9
Agora outros jogadores também devem conseguir ver o contador mudar.
Tickers
A interface EntityBlock também define um método chamado getTicker, cujo pode ser usando para rodar um código todo tick para cada instância do bloco. Nós podemos implementar isso criando o método estátistica que vai ser usado como BlockEntityTicker:
O método getTicker também deve checar se passado o BlockEntityType é o mesmo ao que nós estamos usando, e se é, retornar para a função que vai ser chamada todo tick. Felizmente, há uma função utilitária que realiza a verificação em 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
2
3
4
5
CounterBlockEntity::tick é uma referência ao método estatística tick que nós devemos criar na classe CounterBlockEntity. Estruturar assim não é necessário, mas é uma boa prática para manter o código limpo e organizado.
Vamos dizer que nós queremos fazer o contador aumentar apenas a cada 10 ticks (2 vezes por segundo). Nós podemos fazer isso adicionando o campo ticksSinceLasta classe CounterBlockEntity, aumentando-o a cada tick:
java
public static void tick(Level level, BlockPos blockPos, BlockState blockState, CounterBlockEntity entity) {
entity.ticksSinceLast++;
}1
2
3
2
3
Não se esqueça de serializar e desserializar esse campo!
Agora nós podemos usar ticksSinceLast para checar se o contador pode ser aumentado em incrementClicks:
java
if (this.ticksSinceLast < 10) return;
this.ticksSinceLast = 0;1
2
2
TIP
Se o bloco-entidade não parece responder ao tick, cheque o código de registo! Isso deve passar os blocos válidos para essa entidade em BlockEntityType.Builder, caso contrário irá dar um aviso no 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


