Friday, 7 February 2020

Blocks [1.14.4+]

Key concepts relating to blocks:

  • The minecraft world is composed of a three-dimensional grid of blocks.  The world grid is only 256 blocks high but extends indefinitely along the two horizontal axes.
  • The blocks are grouped into 'Chunks', which comprise a cuboid of 16x256x16 blocks.
  • Chunks are then grouped into regions; each region is comprised of 32 x 32 chunks.  Regions are stored on disk, one per file.
  • The point of all this grouping is firstly to reduce storage space (Sections which are completely empty are not saved to disk) and secondly to allow chunks to be created and loaded into memory individually as necessary, without having to store areas where the player hasn't explored.
  • The coordinate system used in minecraft is shown below:

See the Direction enum class for a number of constants and utility functions related to the six directions (eg getOpposite(), rotateX(), getFrontOffsetX(), etc). 

At a minimum, each grid location [x,y,z] contains the following information:
  1. An BlockState, which specifies the type of block at that location (eg a BlockDoor) as well as its current state (eg OPEN or CLOSED).
  2. An integer "SkyLight" value from 0 to 15, which specifies the amount of light this block is receiving from the sky.
  3. An integer "BlockLight" value from 0 to 15, which specifies the amount of light this block is receiving from nearby blocks.
These data are not stored in the Block class.   They are stored in an array inside BlockStateContainer objects, inside Chunk objects.  They are accessed through various World methods such as
BlockState blockAtXYZ = world.getBlockState( {x,y,z});
int lightFromSky = world.getLightFor(SKY, {x,y,z});
int lightFromBlocks = world.getLightFor(BLOCK, {x,y,z});

The most important aspect for the modder to understand is the BlockState.  An BlockState is comprised of two components, the Block and its Properties (its state)
  1. The Block class and derived classes do not store unique information about every location in the world, they only store information related to the Block type.  The Block classes hence consist mostly of methods to determine the behaviour of the block and how it is drawn on the screen. These are generally unique but not always (for example: different types of wood share the same LogBlock).  The vanilla Block instances can be found in the Blocks class; for example Blocks.DIRT, Blocks.SAND, Blocks.WATER.
  2. Each Block may also have one or more Properties which describe the different states that the block can be in. For example- LadderBlock has a property called FACING which has four allowable values (NORTH, SOUTH, EAST, or WEST) depending on which direction it is facing.  Some Blocks have multiple properties, such as BedBlock which has
  •   HORIZONTAL_FACING (in HorizontalBlock), with possible values NORTH, SOUTH, EAST, WEST;
  •   PART, with possible values HEAD or FOOT to define which part of the bed this is;
  •   OCCUPIED, possible values TRUE or FALSE;
The combination of a Block and its current state is represented by a BlockState.  For example, one possible state of BlockBed is
block_bed [facing=east, part=head, occupied=false];
The state can be manipulated using
1.   BlockState.get(property) to get the value of a particular property, eg 
    bedFacingDirection = currentBedState.get(FACING); to return EAST.
2.   BlockState.with(property, new value) to return a new state with one of the properties changed to the new target value, eg 
    BlockState newBedState = oldBedState.with(FACING, NORTH);

If you want to set the state to a particular value, you need to start from the base(default) state, for example
BlockState bedFacingNorth = Blocks.bed.getBaseState().with(FACING, NORTH);
It's a bit verbose but that's how vanilla does it.

A number of other data structures can be used to store more information about a particular grid location:
  • TileEntity, which stores custom information such as the text on signs, the list of items in a chest, etc.  Most grid locations do not have a corresponding TileEntity; each chunk contains a list of TileEntities, with an entry for each TileEntity and its [x,y,z] location within the chunk.  The TileEntity at a particular location is accessed using
    TileEntity tileEntityAtXYZ = world.getTileEntity({x,y,z});
  • Entity, used for some special "blocks" such falling blocks (sand, gravel), paintings, item frames.

Further details

Vanilla use a wide range of BlockStateProperties (see the `BlockStateProperties` class) but the basic types are
  • EnumProperty such as  PISTON_TYPE (normal or sticky) for PistonHeadBlock
  • DirectionProperty such as FACING (N, S, E, W) for DoorBlock
  • IntegerProperty such as AGE (0 – 7) for CropsBlock (eg wheat growth stage)
  • BoolProperty – such as OCCUPIED (true/false) for BedBlock.
The vanilla code contains helpers to create your own properties with custom names and values; see the vanilla blocks mentioned above for inspiration.  

For each given Block, a StateContainer instance is used to define the valid states that the corresponding Block can have.  The StateContainer can be accessed using Block.getStateContainer().  For example, the StateContainer for BedBlock contains three properties
-        FACING, with possible values NORTH, SOUTH, EAST, WEST.
-        PART, with possible values HEAD or FOOT.
-        OCCUPIED, possible values TRUE or FALSE.
The StateContainer for your custom Block is created during Block construction by overriding Block.fillStateContainer().  You need to be careful to set the base (default) state in your block's constructor - see BedBlock for an example.

Unlike previous versions of minecraft, you're not restricted to a certain number of bits for the state information.

During the game initialisation phases, a unique instance is created for each block, see Block.registerBlocks(), and the Blocks class.  This means that you can test for equality using the == operator instead of having to use .equals(), for example
boolean thisBlockIsDirt = (blockAtCursor == Blocks.DIRT);
Likewise, there is a unique instance for each BlockState, allowing for example
boolean stateHasChanged = (newBlockState != currentBlockState);
instead of
boolean stateHasChanged = !newBlockState.equals(currentBlockState);

No comments:

Post a Comment