Skip to content

Instantly share code, notes, and snippets.

@Steampunkery
Last active December 2, 2017 17:54
Show Gist options
  • Save Steampunkery/caf36d1791616901a178e72e25d8318d to your computer and use it in GitHub Desktop.
Save Steampunkery/caf36d1791616901a178e72e25d8318d to your computer and use it in GitHub Desktop.
A compare & contrast of the new and old BlockFamily systems of Terasology

What is a BlockFamily?

A block family is a group of blocks that are defined together. 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.

Definition

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 and their Limitations

Old block families had some glaring limitations. Each block family had two parts:

  1. The Factory
  2. 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.

Why New Block Families are Awesome

New block families simplify a lot of making a family. Here's a short list:

  1. Only one file needed
  2. No factory
  3. Dependency Injection
  4. 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.

New Block Family Notes

Here are some gotchas that happen commonly when making a block family for the first time:

  1. Remember to register your block family with @RegisterBlockFamily("your_family") this annotation goes above your class header
  2. Register your block sections @BlockSections({"your_block1", "your_block2", "your_block3", "etc..."}) this annoatation also goes above the class header
  3. After you create your block URI, remember to set it with this.setBlockUri(blockUri) in your constructor
  4. 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)

A short example of a block family using AbstractBlockFamily

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

*.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

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.

Sources and Credit

  • @pollend for writing the Signalling which has a great implementation of block families
  • Boilerplate code in the gist is modified from this RomanColumn family
  • Credit again to @pollend for writing this gist that I linked at the top of this gist. It's a very lucid explanation of block families.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment