Skip to content

Block state in practice

Each block owns a state definition. Add a property and its default in CreateBlockStateDefinition; the definition builder constructs the legal combinations when BlockRegistry initializes:

public static BooleanProperty OpenProperty => BlockStateProperties.Open;
public static Property<BlockFace> FacingProperty => BlockStateProperties.HorizontalFacing;
protected override void CreateBlockStateDefinition(BlockStateDefinition.Builder builder)
{
builder.Add(FacingProperty, BlockFace.North)
.Add(OpenProperty, false);
}

BooleanProperty, IntegerProperty, and EnumProperty<T> define the valid values and their order. Prefer shared properties from BlockStateProperties when a concept is already represented there.

BlockState is immutable. SetValue returns the registered state with that property changed; Cycle advances it to the next declared value:

BlockState facingNorth = gate.DefaultBlockState;
BlockState openNorth = facingNorth.SetValue(FenceGateBlock.OpenProperty, true);
BlockState openEast = openNorth.SetValue(FenceGateBlock.FacingProperty, BlockFace.East);
BlockFace facing = openEast.GetValue(FenceGateBlock.FacingProperty);
bool isOpen = openEast.GetValue(FenceGateBlock.OpenProperty);

Every generated combination is a registered flyweight with a compact ushort ID. Avoid creating state objects for individual coordinates; the state belongs to the block definition and the world stores a state ID per voxel.

The state model is being adopted incrementally. Some blocks declare canonical BlockState properties while the active world API still reads and writes the packed BlockStateData shape. Those blocks provide explicit adapters, for example:

BlockStateData packed = world.GetBlockState(pos);
BlockState canonical = gate.FromLegacyStateData(packed);
BlockState changed = canonical.SetValue(FenceGateBlock.OpenProperty, true);
world.SetBlockState(pos, gate.ToLegacyStateData(changed));

Keep both directions complete when adding a property to a block on this bridge. Check the packed representation, write path, and save/load round trip before adding a field. Do not keep a second mutable copy of the property on the shared block instance: there is one registered block object but many block positions.

State changes often affect more than a visual flag. Check collision and selection shapes, face textures, light properties, redstone output, neighbor notifications, and mesh invalidation. Add assertions for defaults and legal transitions in SystemValidation.Voxel.cs; check state serialization in the persistence suite when storage changes.

Related: adding a block, block behavior, and voxel storage.