A block family is a group, or family (hence the name) of blocks. What this means is there are an arbitrary number of blocks that are grouped together because they are similar. For example, there is a RomanColumn family which consists of a cap block, a base block and a middle block. These blocks are separate blocks with different textures, and they can also have distinct shapes (although RomanColumns are all cubes), but they share a *.block file. A block family is created with the "rotation" tag in v1 block families and the "family" tag in v2 block families. For more information about v1 block families and v2 block families, look here. This gist aims to compare and contrast the block family versions and add some documentation as well.
What I'm referring to as old block families are the current block families in the master Terasology repo as of the time of writing (Friday, December 1 2017). Example of old block families.
New block families are the block families that are in the branch newBlockFamiliesof the main Terasology repoat the time of writing. These changes were made mostly by @pollend and they improve the usability and simplicity of block families. Example of new block families.
Old block families had some glaring limitations. Each block family had two parts:
- The Factory
- The Family
The factory essentailly gathers all the parts of the family. The parts are the variables that are passed in from the engine when it creates the block family. One of the main problems with this is the limitation of what is passed in from the engine. There was also problem where extending UpdatesWithNeighboursFamily
and implementing getBlockForNeighborUpdate
did not work correctly all the time and it had to be called manually. The factories tended to be very complex because of the hacks needed to get around the limitations of the factory/family code pattern.
New block families simplify a lot of making a family. Here's a short list:
- Only one file needed
- No factory
- Dependency Injection
- Less boilerplate code
Let's go through the list one by one. First, now we only need one file. That's right, everything you need to make a family can be contained in one file. Factories were unnecessary if you think about it. The function of the factory was to collect all of the pieces of the family. All that work is now done in the constructor of the family. Dependency Injection is one of the most amazing things about the new blok family scheme. The constructor for a block with a cube shape using AbstractBlockFamily is AbstractBlockFamily(BlockFamilyDefinition definition, BlockBuilderHelper blockBuilder)
. But what if you need the WorldProvider
to see a block in another location or BlockEntityRegistry
or literally any other dependency you need (within the scope of the engine)? The @In
tag is your friend. To get the WorldProvider
and BlockEntityRegistry
, just define the following as instance variables:
@In
WorldProvider worldProvider;
@In
BlockEntityRegistry blockRegistry;
The engine will automatically put the world provider into the worldProvider
variable and the same for blockRegistry
. Less overall boilerplate code is needed with the new scheme. After you choose which classes to extend and implement to make your family and override or inherit the necessary methods, you're prety much set.
One con of the new block family system is after a while, there will be lots of block families and so there could be some tricky inheritance involved.
NOTE: There will not be any docs in this gist about the old block families. Below are docs and important notes about the new block families. From now on, when I say block famil[y][ies], I am referring to the new block family scheme.
Here are some gotchas that happen commonly when making a block family for the first time:
- Remember to register your block family with
@RegisterBlockFamily("your_family")
this annotation goes above your class header - Register your block sections
@BlockSections({"your_block1", "your_block2", "your_block3", "etc..."})
this annoatation also goes above the class header - After you create your block URI, remember to set it with
this.setBlockUri(blockUri)
in your constructor - After you create a new block, set the Uri and the BlockFamily like this:
your_block.setUri(put your unique block Uri here)
your_block.setBlockFamily(this)
import gnu.trove.map.TByteObjectMap;
import gnu.trove.map.hash.TByteObjectHashMap;
import org.terasology.math.Side;
import org.terasology.math.SideBitFlag;
import org.terasology.math.geom.Vector3i;
import org.terasology.naming.Name;
import org.terasology.registry.In;
import org.terasology.world.WorldProvider;
import org.terasology.world.block.Block;
import org.terasology.world.block.BlockBuilderHelper;
import org.terasology.world.block.BlockUri;
import org.terasology.world.block.family.AbstractBlockFamily;
import org.terasology.world.block.family.BlockFamily;
import org.terasology.world.block.family.BlockSections;
import org.terasology.world.block.family.RegisterBlockFamily;
import org.terasology.world.block.family.UpdatesWithNeighboursFamily;
import org.terasology.world.block.loader.BlockFamilyDefinition;
import org.terasology.world.block.shapes.BlockShape;
@RegisterBlockFamily("genericfamily") // Registers the block family
@BlockSections({"block1", "block2", "block3", "block4"}) // Registers the block sections (for the block file)
public class GenericFamily extends AbstractBlockFamily implements UpdatesWithNeighboursFamily {
@In
WorldProvider worldProvider; // Gets us the world provider. Note the @In tag
private TByteObjectMap<Block> blocks; // The map to keep our blocks in
BlockUri blockUri; // BlockUri global
// This constructor is only used is you have a non-cube shape
public GenericFamily(BlockFamilyDefinition definition, BlockShape shape, BlockBuilderHelper blockBuilder) {
super(definition, shape, blockBuilder);
}
public GenericFamily(BlockFamilyDefinition definition, BlockBuilderHelper blockBuilder) {
super(definition, blockBuilder);
blocks = new TByteObjectHashMap<Block>(); // Instantiate our block map
blockUri = new BlockUri(definition.getUrn()); / Get us a unique block URI
addConnection(Side.Front, "block1", definition, blockBuilder); // Add a new block
addConnection(SideBitFlag.getSide(Side.LEFT), "block2", definition, blockBuilder); // Add another new block
addConnection(SideBitFlag.getSide(Side.RIGHT), "block3", definition, blockBuilder); // Yet another new block
addConnection(SideBitFlag.getSides(Side.TOP, Side.BACK), "block4", definition, blockBuilder); // Last new block
this.setBlockUri(blockUri); // Set our URI. This is important
this.setCategory(definition.getCategories()); // Set out categories as passed down from the engine. Not so important
}
private void addConnection(Byte bitFlag, String section, BlockFamilyDefinition definition, BlockBuilderHelper blockBuilder) {
blocks.put(bitFlag, addBlock(definition, blockBuilder, section, blockUri, bitFlag)); // Put the block into the map
}
private Block addBlock(BlockFamilyDefinition definition, BlockBuilderHelper blockBuilder, String section, BlockUri blockUri, byte sides) {
Block newBlock = blockBuilder.constructSimpleBlock(definition, section); // Instantiate the block to add
newBlock.setUri(new BlockUri(blockUri, new Name(String.valueOf(sides)))); // Set the URI for this specific block. The URI must be unique, this is important
newBlock.setBlockFamily(this); // Set the block family. I feel like this should be obvious. This is also important
return newBlock;
}
@Override
// Just returns the URI
public BlockUri getURI() {
return blockUri;
}
@Override
// Returns the display name
public String getDisplayName() {
return "Generic Block";
}
@Override
// This method is called when the player places a block of this type
public Block getBlockForPlacement(Vector3i location, Side attachmentSide, Side direction) {
// Your block placement logic here
}
@Override
// The archetype is the "standard" block of the family
public Block getArchetypeBlock() {
return blocks.get((byte) 0);
}
@Override
// Returns the block for a given URI
public Block getBlockFor(BlockUri blockUri) {
for (Block block : blocks.valueCollection()) {
if (block.getURI().equals(blockUri)) {
return block;
}
}
return null;
}
@Override
// List of blocks from the map
public Iterable<Block> getBlocks() {
return blocks.valueCollection();
}
@Override
// This method is called when a neighbor of one of the blocks of this type is changed
public Block getBlockForNeighborUpdate(Vector3i location, Block oldBlock) {
// Your block update logic here
}
}
*.block files are where you define certain aspects of the block/block family. As mentioned in the beginning of this gist, the old way to define a family in the block file was using the "rotation" tag. The new way is to use the "family" tag. In both new and old block files, there are sections. A section might look like this:
"top": {
"shape": "TorchGrounded"
},
This section says that when the torch is on top of something, it should change it's shape to "TorchGrounded". In the predefined block families, both old an new, there are sections already defined for you to use. In the boilerplate java above, we define our own sections for example "block1". These user defined sections can be used just lime regular sections. See this page on block attributes. Any attribute that can be set in the ouer JSON can also be set within the section. Any attributes set in the section will override that of the parent. For example if you have a block file:
...
"tiles" : {
"topBottom" : "default_top_and_bottom",
"sides" : "default_side"
},
...
"some_block": {
"tiles" : {
"topBottom" : "some_texture",
"sides" : "some_other_texture"
},
"shape": "engine:cube"
},
...
When the block placed is a some_block
, the textures/tiles will change to be what is in the some_block
section.
New block files are very much the same as the old ones, with one small difference. If you registered your block family as "genericblock", then in your block file you have to have "family": "genericblock"
instead of "rotation": "genericblock"
to let the engine know that this file is for the block family that you made in your java files.