Saturday 20 July 2013

Blocks


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 cube of 16x16x16 blocks.
  • Chunks are then grouped into regions; each region is comprised of 32 x 32x16 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 EnumFacing 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 IBlockState, 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 ExtendedBlockStorage objects, inside Chunk objects.  They are accessed through various World methods such as
IBlockState blockAtXYZ = world.getBlockState( {x,y,z});
int lightFromSky = world.getLightFor(SKY, {x,y,z});
int lightFromBlocks = world.getLightFor(BLOCK, {x,y,z});

An IBlockState 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 BlockOldLog instance).  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- BlockLadder has a property called FACING which has four allowable values (NORTH, SOUTH, EAST, or WEST) depending on which direction it is facing.  BlockOldLog has a property called VARIANT with allowable values OAK, SPRUCE, BIRCH, JUNGLE.  Some Blocks have multiple properties, such as BlockBed which has
  •   FACING, 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 an IBlockState.  For example, one possible state of BlockBed is
block_bed [facing=east, part=head, occupied=false];
The state can be manipulated using
1.    IBlockState.getValue(property) to get the value of a particular property, eg 
    bedFacingDirection = currentBedState.getValue(FACING); to return EAST.
2.   IBlockState.withProperty(property, new value) to return a new state with one of the properties changed to the new target value, eg 
    IBlockState newBedState = oldBedState.withProperty(FACING, NORTH);

A number of other data structures can be used to store more information about a particular block 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

The main IProperties used by vanilla are
  • PropertyEnum such as VARIANT (OAK, SPRUCE, BIRCH, JUNGLE) for BlockOldLogs
  • PropertyDirection such as FACING (N, S, E, W) for BlockDoor
  • PropertyInteger such as AGE (0 – 7) for BlockCrops (eg wheat growth stage)
  • PropertyBool – such as OCCUPIED (true/false) for BlockBed.
The vanilla code contains helpers to create your own properties with custom names and values.  

For each given Block, a BlockState instance is used to define the valid states that the corresponding Block can have.  (The name is misleading because BlockState does not implement IBlockState!).  The BlockState can be accessed using Block.getBlockState().  For example, the BlockState for BlockBed 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 BlockState for your Block is created during Block construction by overriding Block.createBlockState().

As for versions 1.7 and earlier, minecraft only uses a total of 16 bits for IBlockState - 12 bits for a blockID plus 4 bits for the state information (previously called metadata)).  This means that a block must have no more than 16 unique states.  Although some vanilla blocks have more than 16 combinations of properties (for example BlockDoor has 64), at most 16 of these are actually used.  In the example of BlockBed, although there are 16 possible combinations of values, only 12 are used– see the table below.

PART
HEAD
FOOT

OCCUPIED
TRUE
FALSE
TRUE
FALSE
FACING
NORTH
14
10
not used
2
SOUTH
12
8
not used
0
EAST
15
11
not used
3
WEST
13
9
not used
1

Block.getMetaFromState() and Block.getStateFromMeta() are used to interconvert between the IBlockState value and the corresponding integer metadata value (0-15 inclusive).  When implementing Blocks with Properties, you must be careful to only return metadata in the range 0 – 15, otherwise you will overwrite the mappings for other blocks.  Likewise, you must make sure that your getMetaFromState() and getStateFromMeta() match each other.

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 IBlockState, allowing for example
boolean stateHasChanged = (newIBlockState != currentIBlockState);
instead of
boolean stateHasChanged = !newIBlockState.equals(currentIBlockState);