12 KiB
title = "Tile Entities"
date = "2016-11-27 12:32:42 -0400"
In Minecraft, the Block
class is used to represent a type of block, not a single block in the world. The Block
instance has the properties for every single instance of your block that exists. If we want to have data that is unique to an instance of a block in the world, we need to use a TileEntity
.
A common myth that exists in the world of modding is that tile entities are bad, especially for performance. This is not true. Tile entities can be bad for performance if they're implemented poorly, just like anything else, but they are not bad just by virtue of existing.
There are two varieties of tile entities: ticking and non-ticking. Ticking tile entities, as the name implies, are updated (or ticked) every single game tick (usually 20 times per second). Tick tile entities are the more performance intensive kind because they're updated so frequently and as such, need to be written carefully. Non-ticking tile entities on the other hand don't tick at all, they exist simply for storing data. In this tutorial we'll be making a non-ticking tile entity. Ticking tile entities we'll get to later.
Helper Stuff
Before we create the tile entity, we'll add some more code to our mod that will make it easier to add more tile entities in the future.
Firstly, we'll create a BlockTileEntity
class in our block
package.
package net.shadowfacts.tutorial.block;
import net.minecraft.block.material.Material;
import net.minecraft.block.state.IBlockState;
import net.minecraft.tileentity.TileEntity;
import net.minecraft.util.math.BlockPos;
import net.minecraft.world.IBlockAccess;
import net.minecraft.world.World;
import javax.annotation.Nullable;
public abstract class BlockTileEntity<TE extends TileEntity> extends BlockBase {
public BlockTileEntity(Material material, String name) {
super(material, name);
}
public abstract Class<TE> getTileEntityClass();
public TE getTileEntity(IBlockAccess world, BlockPos pos) {
return (TE)world.getTileEntity(pos);
}
@Override
public boolean hasTileEntity(IBlockState state) {
return true;
}
@Nullable
@Override
public abstract TE createTileEntity(World world, IBlockState state);
}
Our BlockTileEntity
class wil provide a couple of things:
- It will extend
BlockBase
so we still have access to all our existing helpers. - It will have a generic parameter
TE
which will be the type of our tile entity class. This will be used to create a simple helper method to reduce the numebr of casts necessary to obtain the instance of our tile entity for a specific position in the world and to ensure that theTileEntity
we create is of the correct type for our block instance. - It will override the
hasTileEntity(IBlockState)
method from Minecraft'sBlock
class to returntrue
. This will tell Minecraft that our block has a tile entity associated with it that needs to be created. - It will have two abstract methods:
getTileEntityClass
: From here, we'll return theClass
that our tile entity is so it can easily be registered when our block is registered.createTileEntity
: This is a more specific version of theBlock
class'createTileEntity
. This method will be called by Minecraft whenever it needs to create a new instance of our tile entity, like when our block has been placed.
The Block
Now that we've got the necessary helpers out of the way it's time to create the block.
We'll create a new class called BlockCounter
in the block.counter
package of our mod. This class will be block class that extends BlockTileEntity
. (This class will have some errors because we haven't created the tile entity class itself yet.)
package net.shadowfacts.tutorial.block.counter;
import net.minecraft.block.material.Material;
import net.minecraft.block.state.IBlockState;
import net.minecraft.entity.player.EntityPlayer;
import net.minecraft.item.ItemStack;
import net.minecraft.util.EnumFacing;
import net.minecraft.util.EnumHand;
import net.minecraft.util.math.BlockPos;
import net.minecraft.util.text.TextComponentString;
import net.minecraft.world.World;
import net.shadowfacts.tutorial.block.BlockTileEntity;
import javax.annotation.Nullable;
public class BlockCounter extends BlockTileEntity<TileEntityCounter> {
public BlockCounter() {
super(Material.ROCK, "counter");
}
@Override
public boolean onBlockActivated(World world, BlockPos pos, IBlockState state, EntityPlayer player, EnumHand hand, EnumFacing side, float hitX, float hitY, float hitZ) {
if (!world.isRemote) {
TileEntityCounter tile = getTileEntity(world, pos);
if (side == EnumFacing.DOWN) {
tile.decrementCount();
} else if (side == EnumFacing.UP) {
tile.incrementCount();
}
player.addChatMessage(new TextComponentString("Count: " + tile.getCount()));
}
return true;
}
@Override
public Class<TileEntityCounter> getTileEntityClass() {
return TileEntityCounter.class;
}
@Nullable
@Override
public TileEntityCounter createTileEntity(World world, IBlockState state) {
return new TileEntityCounter();
}
}
Our block class will extend BlockTileEntity
and have a generic parameter of TileEntityCounter
because that's the type of tile entity that belongs to this block.
In the constructor, we'll simply call the super constructor with the material ROCK
and the name "counter"
.
In the getTileEntityClass
method, we'll return TileEntityCounter.class
(this wil cause an error, but don't worry, we haven't created this class yet). This will allow our ModBlocks
class to automatically register our TileEntityCounter.class
to the name tutorial:counter
.
In the createTileEntity
class, we'll simply return a new instance of our TileEntityCounter
class.
Lastly, and most importantly, in the onBlockActivated
method, which is called when our block is right-clicked, we'll do a number of things:
- Check that we're operating on the server*.
- Retrieve the
TileEntityCounter
instance. - If the player hit the bottom side:
- Decrement the counter.
- Or if the player hit the top side:
- Increment the counter.
- Send a chat message to the player with the current value of the counter.
- Retrieve the
- Return true
Sides
As I mentioned above, before we modify the counter, we check that we're on the server. We need to do this because the Minecraft client and the server are completely separated and some methods are called on both.
In a multiplayer scenario, there are multiple clients connect to one server. In this case, the distinction between client and server is fairly obvious, but in a single player scenario, things get more complicated. In a multiplayer scenario, the server that everybody's connecting to is referred to as the physical server and all of the individual clients are the physical clients.
In a single player world, the client and the server are still decoupled, even though they are running on the same computer (and even in the same JVM, just on different threads). In singleplayer, the client connects to a local, private server that functions very similarly to a physical server. In this case, the server thread is referred to as the logical server and the client thread as the logical client because both logical sides are running on the same physical side.
The World.isRemote
field is used to check which logical side we're operating on (be it logical or physical). The field is true
for the physical client in a multiplayer scenario and for the logical client in a single-player scenario. The reverse is also true. The field is false
for the physical server in a multiplayer scenario and for the logical server in the single-player scenario. So by checking !world.isRemote
, we ensure that the code inside the if
statement will only be run on the server (be it logical or physical).
If you want to know more about sides in Minecraft and how they work, you can see here for the official Forge documentation.
The Tile Entity
Now that our block is finished, we can finally create the tile entity itself.
We'll create a new class called TileEntityCounter
which will also reside in the block.counter
package of our mod (this is my preferred package structure, however, many people also prefer to have all the tile entity classes reside in a separete tileentity
package and the block classes reside in the block
package).
package net.shadowfacts.tutorial.block.counter;
import net.minecraft.nbt.NBTTagCompound;
import net.minecraft.tileentity.TileEntity;
public class TileEntityCounter extends TileEntity {
private int count;
@Override
public NBTTagCompound writeToNBT(NBTTagCompound compound) {
compound.setInteger("count", count);
return super.writeToNBT(compound);
}
@Override
public void readFromNBT(NBTTagCompound compound) {
count = compound.getInteger("count");
super.readFromNBT(compound);
}
public int getCount() {
return count;
}
public void incrementCount() {
count++;
markDirty();
}
public void decrementCount() {
count--;
markDirty();
}
}
Our TileEntityCounter
class is fairly simple. It will:
- Extend Minecraft's
TileEntity
class so Minecraft knows what to do with it. - Have a private
int count
field which will store the value of the counter. - Override the
writeToNBT
andreadFromNBT
methods so Minecraft is able to properly save and load it from the disk. - Provide
getCount
,incrementCount
, anddecrementCount
methods for accessing and modifying the value of the field.
Additionally, in the incrementCount
and decrementCount
methods, we call the markDirty
method from the Vanilla TilEntity
class. This method call tells Mincraft that our TE has changed since it was last saved to disk and therefore must be re-saved.
The NBT (Named Binary Tag) Format
NBT is a format for storing all types of data into a key/value tree structure that can easily be serialized to bytes and saved to the disk. You can read more about the internal structure of the NBT format here. You can look at the NBTTagCompound
class in Minecraft to see all the types of things that can be stored. Vanilla code is also a good example of how to store more complex things in NBT.
In this case, we'll store our count
integer field with the count
key in the NBTTagCompound
in the writeToNBT
method and read it back from the tag compound in the readFromNBT
method.
Registration
We'll need to add our counter to our ModBlocks
class and register the block, item block, and item model.
In the register
method, we also make a call to GameRegistry.registerTileEntity
with our counter's TileEntity
class and the counter's registry name (tutorial:counter
). This is required so that when the world is loaded from disk, Minecraft knows which TileEntity
goes with our block and can re-created and load it from NBT.
// ...
public class ModBlocks {
// ...
public static BlockCounter counter = new BlockCounter();
public static void register(IForgeRegistry<Block> registry) {
registry.registerAll(
// ...
counter
);
GameRegistry.registerTileEntity(counter.getTileEntityClass(), counter.getRegistryName().toString());
}
public static void registerItemBlocks(IForgeRegistry<Item> registry) {
registry.registerAll(
// ...
counter.createItemBlock()
);
}
public static void registerModels() {
// ...
counter.registerItemModel();
}
}
Finished
Now that we've got everything done, we can run Minecraft, grab one of our counters from our creative tab, place it, and see how the counter changes when the top and bottom of the block are right-clicked.