Minecraft Modding: Making Hand-Held Light Source (Flashlight / Lantern / Torch)

Background


You can download a simple mod I made using the ideas presented here: Jabelar's Moving Light Source Mod.

Note: This tutorial was designed for 1.8. However, converting back to 1.7.10 should be fairly easy and the overall concept should still work for such earlier versions.

The lighting system in Minecraft is fairly limited in terms of customizing light sources. For example, making colored lights requires some serious coding and performance hit. Making moving lights isn't straightforward either; however, in this tutorial I give an approach that works pretty well.

Check out my video of what you can achieve (when torch is held it gives off light, that follows player pretty well):


The basic idea is that I am placing invisible, light-emitting blocks dynamically as I move around. They have a tile entity associated with them in order to check if the player moves too far away, in which case it deletes itself.

There are a lot of additional tricks required. For example, I generally place the block at head height, but in the case where it blocked by an existing block (for example when moving through tall grass) I look for other nearby locations.

There may still be some bugs, as there are so many cases where a block (invisible or otherwise) might interfere with other stuff (like redstone transmission, fluid flow, etc.). Let me know if you find any such bugs! But overall my testing shows that it works pretty decently.

Create A Custom Block For the Moving Light Source


Here is an example of the custom block:


public class BlockMovingLightSource extends Block implements ITileEntityProvider
{
    public static List<Item> lightSourceList = new ArrayList<Item>() {
        {
            add(Item.getItemFromBlock(Blocks.torch));
            add(Item.getItemFromBlock(Blocks.redstone_torch));
            add(Item.getItemFromBlock(Blocks.redstone_lamp));
            add(Item.getItemFromBlock(Blocks.redstone_block));
            add(Item.getItemFromBlock(Blocks.redstone_ore));
            add(Items.redstone);
            add(Item.getItemFromBlock(Blocks.redstone_wire));
            add(Item.getItemFromBlock(Blocks.glowstone));
            add(Items.glowstone_dust);
            add(Item.getItemFromBlock(Blocks.lava));
            add(Items.lava_bucket  );
            add(Item.getItemFromBlock(Blocks.lit_redstone_lamp));
            add(Item.getItemFromBlock(Blocks.beacon));
            add(Item.getItemFromBlock(Blocks.sea_lantern));
            add(Item.getItemFromBlock(Blocks.end_portal));
            add(Item.getItemFromBlock(Blocks.end_portal_frame));
        }
    };

    public BlockMovingLightSource()
    {
        super(Material.air );
        setUnlocalizedName("movinglightsource");
        setDefaultState(blockState.getBaseState());
        setTickRandomly(false);
        setLightLevel(1.0F);
        setBlockBounds(0.5F, 0.5F, 0.5F, 0.5F, 0.5F, 0.5F);
    }
    
    public static boolean isLightEmittingItem(Item parItem)
    {
        return lightSourceList.contains(parItem);
    }

    @Override
    public AxisAlignedBB getCollisionBoundingBox(World worldIn, BlockPos pos, IBlockState state)
    {
        return null;
    }

    @Override
    public boolean isOpaqueCube()
    {
        return false;
    }

    @Override
    public boolean isFullCube()
    {
        return false;
    }

    @Override
    public boolean canPlaceBlockAt(World worldIn, BlockPos pos)
    {
        return true;
    }

    @Override
    public IBlockState onBlockPlaced(
          World worldIn, 
          BlockPos pos, 
          EnumFacing facing, 
          float hitX, 
          float hitY, 
          float hitZ, 
          int meta, 
          EntityLivingBase placer)
    {
        return getDefaultState();
    }

    @Override
    public void onBlockAdded(World worldIn, BlockPos pos, IBlockState state)
    {
        return;
    }
    @Override
    public void onNeighborBlockChange(
          World worldIn, 
          BlockPos pos, 
          IBlockState state, 
          Block neighborBlock)
    {
        return;
    }

    @Override
    public IBlockState getStateFromMeta(int meta)
    {
        return getDefaultState();
    }

    @Override
    public int getMetaFromState(IBlockState state)
    {
        return 0;
    }

    @Override
    @SideOnly(Side.CLIENT)
    public EnumWorldBlockLayer getBlockLayer()
    {
        return EnumWorldBlockLayer.CUTOUT;
    }

    @Override
    public void onFallenUpon(
          World worldIn, 
          BlockPos pos, 
          Entity entityIn, 
          float fallDistance)
    {
        return;
    }

    @Override
    public void onLanded(World worldIn, Entity entityIn)
    {
        return;
    }

    @Override
    protected BlockState createBlockState()
    {
        return new BlockState(this);
    }

    @Override
    public TileEntity createNewTileEntity(World worldIn, int meta)
    {
        return new TileEntityMovingLightSource();
    }
}

It should be fairly self-explanatory. The class extends Block and implement ITileEntityProvider (we'll create the tile entity class below).

I create a list of all items I'd like to emit light when held. You can edit this list how you like, maybe adding custom lanterns and flashlights.

The key line in the constructor is where I set the light value to 1.0F. So it will be a light-emitting block.

To try to make the block so it doesn't interfere with other things in the game, we:

  1. Set the material to Material.air.
  2. We make the bounding box have no width, height or depth -- it is just a single point at 0.5, 0.5, 0.5.
  3. We return null for the getCollisionBoundingBox() method.
  4. We return false for the isOpaqueCube() and isFullCube() methods.
  5. We @Override the onFallenUpon(), onLanded() and onNeighborBlockChanged() methods.
We also aren't using metadata -- I have any data I need to monitor in the tile entity. It is probably possible to implement metadata instead, possibly with random ticking enabled, to achieve same effect without tile entities. But since there will only be a couple of these placed at any time, tile entities are convenient and shouldn't cause performance issues.

I also make sure the block can be transparent without any texture, but returning the EnumWorldBlockFacing.CUTOUT value.

Create The Tile Entity Class

Here is the tile entity class:


public class TileEntityMovingLightSource extends TileEntity implements IUpdatePlayerListBox
{
    public EntityPlayer thePlayer;
    
    public TileEntityMovingLightSource()
    {
        // after constructing the tile entity instance, remember to call 
        // the setPlayer() method.

    }
    
    /**
     * This controls whether the tile entity gets replaced whenever the block state 
     * is changed. Normally only want this when block actually is replaced.
     */
    @Override
    public boolean shouldRefresh(
          World world, 
          BlockPos pos, 
          IBlockState oldState, 
          IBlockState newSate)
    {
        return (oldState.getBlock() != newSate.getBlock());
    }

    @Override
    public void update()
    {
        // check if player has moved away from the tile entity
        EntityPlayer thePlayer = worldObj.getClosestPlayer(
              getPos().getX()+0.5D, 
              getPos().getY()+0.5D, 
              getPos().getZ()+0.5D, 
              2.0D);
        if (thePlayer == null)
        {
            if (worldObj.getBlockState(getPos()).getBlock() == 
                  MovingLightSource.blockMovingLightSource)
            {
                worldObj.setBlockToAir(getPos());
            }
        }
        else if (thePlayer.getCurrentEquippedItem().getItem() != null 
              && !BlockMovingLightSource.isLightEmittingItem(
                    thePlayer.getCurrentEquippedItem().getItem()))
        {
            if (worldObj.getBlockState(getPos()).getBlock() == 
                  MovingLightSource.blockMovingLightSource)
            {
                worldObj.setBlockToAir(getPos());
            }            
        }
    }  
    
    public void setPlayer(EntityPlayer parPlayer)
    {
        thePlayer = parPlayer;
    }
}

Again, I hope this is fairly self explanatory. The main thing the tile entity is doing is checking to see whether the player is still very close. If not, it deletes the block. Also, even if the player is still close if he doesn't have a torch in his hand then it deletes the block.

Note: MovingLightSource is the name of my mod's main class. You'd replace that with the name of your mod's main class.

Tip: This code example only checks for holding a torch, but you can add checks for other items held such as redstone torch, glowstone, bucket of lava, etc. Or maybe you might make a custom lantern or flashlight item.

Warning: Notice the comment in the constructor. It reminds you that it is important to remember to set the player field after creating the block, otherwise the whole system won't work.

Place The Block From A PlayerTickEvent Handling Method

Now the idea is that every player tick you need to check if the player is holding an item that should emit light. If there is, then place one of the blocks.

I'm assuming you already know how to handle events, but in case you don't I have a comprehensive tutorial here: Jabelar's Event Handling Tutorial

Here is my example method for handling the PlayerTickEvent:


@SubscribeEvent(priority=EventPriority.NORMAL, receiveCanceled=true)
public void onEvent(PlayerTickEvent event)
{        
    if (event.phase == TickEvent.Phase.START && !event.player.worldObj.isRemote)
    {
        if (event.player.getCurrentEquippedItem() != null)
        {
            if (BlockMovingLightSource.isLightEmittingItem(
                  event.player.getCurrentEquippedItem().getItem()))
            {
                int blockX = MathHelper.floor_double(event.player.posX);
                int blockY = MathHelper.floor_double(event.player.posY-0.2D - 
                      event.player.getYOffset());
                int blockZ = MathHelper.floor_double(event.player.posZ);
                // place light at head level
                BlockPos blockLocation = new BlockPos(blockX, blockY, blockZ).up();
                if (event.player.worldObj.getBlockState(blockLocation).getBlock() == 
                      Blocks.air)
                {
                    event.player.worldObj.setBlockState(
                          blockLocation, 
                          MovingLightSource.blockMovingLightSource.getDefaultState());
                }
                else if (event.player.worldObj.getBlockState(
                      blockLocation.add(
                            event.player.getLookVec().xCoord, 
                            event.player.getLookVec().yCoord, 
                            event.player.getLookVec().zCoord)).getBlock() == Blocks.air)
                {
                    event.player.worldObj.setBlockState(
                          blockLocation.add(
                                event.player.getLookVec().xCoord, 
                                event.player.getLookVec().yCoord, 
                                event.player.getLookVec().zCoord), 
                                MovingLightSource.blockMovingLightSource.getDefaultState());
                }
            }
        }
    }
}

So, again it is pretty straight-forward code. You check if the player is holding an item, if he is then check if it is an item that emits light and if it is try to place a block at the player's head. However, if the head location is blocked (like if the player is walking through tall grass) try to place the block just slightly ahead (in direction player is looking).

Overall, this placement seems to work pretty well in most situations. For example, walking through tall grass will work as long as you're not surrounded by it (in which case it kinda makes sense that the light wouldn't extend). Or if you go in water, as long as your head is above it will work, but as soon as you're under water it won't. But you might want to play around with this and try additional locations.

Create Your Block State JSON Files


I'm assuming that you already know a bit about blocks in 1.8, but you also need some resources for the block state models placed in the proper resource asset packages.

Here is my block state JSON, which needs to have same name as the unlocalized name of the block (which I set in the constructor of the Block class, so in my example file name should be movinglightsource.json):

{
    "variants": {
        "normal": { "model": "movinglightsource:movinglightsource" }
    }
}

Here is the block model JSON, which needs to be named to match the model listed in the block state JSON above (movinglightsource.json);

{      

    "textures": 
    {
        "pole": "blocks/log_big_oak"
    },
   "elements":[  
      {  
         "__comment":"basically make it have no surface area so it is invisible", 
         "from":[ 0.5, 0.5, 0.5 ],
         "to":[ 0.5, 0.5, 0.5 ],
         "faces":{  
            "down":{ "uv": [ 0, 0, 15, 15 ], "texture":"#pole" },
            "up":{ "uv": [ 0, 0, 15, 15 ], "texture": "#pole" },
            "north":{ "uv": [ 0, 0, 15, 15 ], "texture":"#pole" },
            "south":{ "uv": [ 0, 0, 15, 15 ], "texture":"#pole" },
            "west":{ "uv": [ 0, 0, 15, 15 ], "texture":"#pole" },
            "east":{ "uv": [ 0, 0, 15, 15 ], "texture":"#pole" }
         }
      }
   ]
}

Note that I am creating a model with 0 height, width, depth. This helps ensure that nothing is rendered (i.e. it is invisible). The JSON can probably be simplified since all the faces have same value, but oh well it works as is.

And lastly, the item rendering model JSON. It can have any name but I think it makes most sense to name it the same as the block's unlocalized name (movinglightsource.json):

{
    "parent": "movinglightsource:block/movinglightsource",
    "display": {
        "thirdperson": {
            "rotation": [ 10, -45, 170 ],
            "translation": [ 0, 1.5, -2.75 ],
            "scale": [ 0.375, 0.375, 0.375 ]
        }
    }
}

Note that the parent field must match the name of the block model JSON.

Don't Forget To Register Everything!


Okay, make sure you register everything:

  1. Register the block in your common proxy.
  2. Register the tile entity in your common proxy.
  3. Register the event handler in your common proxy.
  4. Register the block renderer in your client proxy. Note that I don't think this really does anything, because we didn't put the block on any creative tab, and also didn't make the block drop anything. But still probably a good idea.
That's it!

Further Thoughts And Areas For Improvement


There may cases where the block interferes with something. I tried to make it so it doesn't, but try testing with fluids, multi-player, projectiles, placing other blocks, etc. I wouldn't be surprised if there is still some bugs in this implementation -- please write a comment if you find one.

I published a simple mod based on the ideas discussed above, try it out: Jabelar's Moving Light Source Mod.

Happy modding!

3 comments:

  1. Using this tutorial, I built a Glowstone Golem that lights up the area around it. Funny, until now I never realized that sheep move toward light sources when it's dark.

    ReplyDelete
    Replies
    1. A Glowstone Golem seems like a really cool idea. Glad it worked well for you.

      Delete
  2. I just noticed that in 1.11.2, when you are holding a torch in your hand, you are emitting light. Still not sure whether it is only some kind of client rendering tricks, or it actually does affect block lighting.
    I'm wondering if there's a more elegant way to implement light source hand-held items...

    ReplyDelete