BlockStates

Revision as of 22:10, 30 July 2021 by ChampionAsh5357 (talk | contribs) (Update to 1.17)

Introduction of States

The block state system abstracts out the details of the block's properties from the other behaviors of the block.

Each property of a block is described by an instance of Property<?>. Examples of block properties include instruments (Property<NoteBlockInstrument>), direction (Property<Direction>), poweredness (Property<Boolean>), etc. Each property has the value of the type T parametrized by Property<T>.

A unique triple can be constructed from the Block, the set of Property<?>, and the set of values for those properties. This unique triple is called a BlockState. For example, a stone button which is facing east and is powered or held down is represented by minecraft:stone_button[facing=east,powered=true].

Proper Usage of Block States

BlockState is a flexible and powerful system, but it also has limitations. BlockStates are immutable, and all combinations of their properties are generated on startup of the game. This means that having a BlockState with many properties and possible values will slow down the loading of the game, and befuddle anyone trying to make sense of your block logic.

Not all blocks and situations require the usage of BlockState; only the most basic properties of a block should be put into one, and any other situation is better off with having a BlockEntity or being a separate Block. Always consider if you actually need to use a state for your purposes.

Tip

A good rule of thumb is: if it has a different name, it should be a separate block.

An example is making chair blocks: the direction of the chair should be a property, while the different types of wood should be separated into different blocks. An "Oak Chair" facing east (oak_chair[facing=east]) is different from a "Spruce Chair" facing west (spruce_chair[facing=west]).

Implementing Block States

In your Block class, create or reference static final Property<?> objects for every property that your Block has. You are free to make your own Property<?> implementations, but the means to do that are left as an exercise to the reader. The vanilla code provides several convenience implementations:

  • IntegerProperty
    • Implements Property<Integer>. Defines a property that holds an integer value.
    • Created by calling IntegerProperty.create(String propertyName, int minimum, int maximum).
  • BooleanProperty
    • Implements Property<Boolean>. Defines a property that holds a true or false value.
    • Created by calling BooleanProperty.create(String propertyName).
  • EnumProperty<E extends Enum<E>>
    • Implements Property<E>. Defines a property that can take on the values of an Enum class.
    • Created by calling EnumProperty.create(String propertyName, Class<E> enumClass).
    • It is also possible to use only a subset of the Enum values (e.g. RailShapes that can only ascend and not turn). See the overloads of EnumProperty.create.
  • DirectionProperty
    • This is a convenience implementation of EnumProperty<Direction>
    • Several convenience predicates are also provided. For example, to get a property that represents the cardinal directions, call DirectionProperty.create("<name>", Direction.Plane.HORIZONTAL); to get the X directions, DirectionProperty.create("<name>", Direction.Axis.X)

The class BlockStateProperties contains shared vanilla properties which should be used or referenced whenever possible, in place of creating your own properties.

When you have your desired Property<> objects, override Block#createBlockStateDefinition(StateDefinition$Builder) in your Block class. In that method, call StateDefinition$Builder#add(...); with the parameters as every Property<?> you wish the block to have.

Every block will also have a "default" state that is automatically chosen for you. You can change this "default" state by calling the Block#registerDefaultState(BlockState) method from your constructor. When your block is placed it will become this "default" state. An example from DoorBlock:

this.registerDefaultState(
    this.stateDefinition.any()
        .setValue(FACING, Direction.NORTH)
        .setValue(OPEN, false)
        .setValue(HINGE, DoorHingeSide.LEFT)
        .setValue(POWERED, false)
        .setValue(HALF, DoubleBlockHalf.LOWER)
);

If you wish to change what BlockState is used when placing your block, you can overwrite Block#getStateForPlacement(BlockPlaceContext). This can be used to, for example, set the direction of your block depending on where the player is standing when they place it.

Because BlockStates are immutable, and all combinations of their properties are generated on startup of the game, calling BlockState#setValue(Property<T>, T) will simply go to the Block's StateHolder and request the BlockState with the set of values you want.

Because all possible BlockStates are generated at startup, you are free and encouraged to use the reference equality operator (==) to check if two BlockStates are equal.

Using BlockStates

You can get the value of a property by calling BlockState#getValue(Property<?>), passing it the property you want to get the value of. If you want to get a BlockState with a different set of values, simply call BlockState#setValue(Property<T>, T) with the property and its value.

You can get and place BlockStates in the level using Level#setBlockAndUpdate(BlockPos, BlockState) and Level#getBlockState(BlockState). If you are placing a Block, call Block#defaultBlockState() to get the "default" state, and use subsequent calls to BlockState#setValue(Property<T>, T) as stated above to achieve the desired state.