Back to Main

Voxel Eras Modding Tutorial - How to make a simple block

This tutorial assumes you already set up your mod. If you didn’t, look at this tutorial here.

Creating a new file

We are going to start by creating a new file for this block. Create a file called block_name.rhai (replace block_name by your actual block name) inside the scripts folder (not mandatory but again, it’s for organization purposes).

Inside, you can add this function:

fn define(mod_info) {
    // Mod content here!
}

I like having this function always at the top for again organization purposes (also, all the game base content does this).

Go into your entrypoint script, and add this inside setup:

fn setup(mod_info) {
    import "mod_folder_name/scripts/path/to/block_name" as block_name;
    
    block_name::define(mod_info);
}

This is how you will register any elements into the game (blocks, items, entities,…). Launch the game and enable the mod to see if it works properly. In case the game crashes or you have an error means you probably gave the wrong path when importing.

Adding a Voxel Builder

Now, we can go and add our block/voxel. To do this, we need to make a new “blockstate” (similar to Minecraft) using a VoxelBuilder inside our define function. If you need multiple blockstates, you just add other VoxelBuilders (we’ll see when making a more advanced block) :

mod_info.add(
    VoxelBuilder(
        "BlockName:ModName",
        "Your Block Name",
        "mod_folder_name/scripts/path/to/block_name.rhai"
    )
    .model(SolidVoxelModel("mod_folder_name/assets/textures/path/to/Texture.png"))
);

This is very simple. The VoxelBuilder takes in three arguments: a string identifier, the block display name, and the script path. I believe the script path is needed because you can add logic to these blocks, which we will cover when doing the advanced block.

Next you have .model(): this indicates what model to use. There’s a lot of voxel models : CrossVoxelModel for plant-like blocks, TransparentVoxelModel for transparent blocks,… Since we are doing a simple block, we use SolidVoxelModel :

NOTE: Block face textures needs to be 16 by 16 pixels.

Adding a Construct Builder

Our block type is added, but if you start the game, you’ll notice that you can’t find your block anywhere. This is because you only registered the block type, and since there is no custom world generation available yet, the only way to have your block into the world is by adding it into the building menu.

To do this, we will register a new ConstructBuilder, similar to how we did with VoxelBuilder :

mod_info.add(
    ConstructBuilder(
        "ConstructName:ModName",
        "Your Construct Name",
        "Enter the construct description here.",
        "Category Name",
        "IconId:ModName",
        construct::ROT_NONE
    )
    .variant(
        ConstructVariantBuilder(voxel::ROT_UP)
            .place(
                0, 0, 0,
                WeakVoxelVariant("BlockName:ModName", voxel::ROT_UP)
            )
    )
    .priority(0)
);

ConstructBuilder takes a bunch more arguments. Since we only have one “blockstate”, "ConstructName" and "BlockName" can be the same. Also notice construct::ROT_NONE: that mean that the block is never rotated, which is good since the block has only one texture anyway. More advanced blocks can use options such as construct::ROT_CARDINAL, construct::ROT_COMPASS or construct::ROT_FULL, each of them giving different results.

Next, we register a variant into this ConstructBuilder with a ConstructVariantBuilder. It takes one argument of type VoxelRotation. Because again, this block only has one texture and can’t be rotated, we just use voxel::ROT_UP. We use the place place function to tell that this variant is going to place a "BlockName:ModName" voxel rotated voxel::ROT_UP with offset 0, 0, 0. This is cool because it means you can actually make multiblocks constructs fairly easily by chaining place functions like so :

.variant(
    ConstructVariantBuilder(voxel::ROT_UP)
        .place(
            0, 0, 0,
            WeakVoxelVariant("BlockName:ModName", voxel::ROT_UP)
        )
        .place(
            0, 1, 0,
            WeakVoxelVariant("BlockName:ModName", voxel::ROT_UP)
        )
        .place(
            0, 1, 1,
            WeakVoxelVariant("BlockName:ModName", voxel::ROT_UP)
        )
)

The priority function here will tell the construct menu where to place this construct relative to the others in the category.

Again, if you launch the game and you did everything correctly, you should be able to see the construct available in the menu. Look for a purple question mark icon (we will fix that afterwards). The block is probably grayed out even though the “Build Cost” is saying “Free!”.

To fix this, we need to add a cost. For this tutorial, I’m just going to use stone as a placeholder :

mod_info.add(
    ConstructBuilder(
        "ConstructName:ModName",
        "Your Construct Name",
        "Enter the construct description here.",
        "Category Name",
        "IconId:ModName",
        construct::ROT_NONE
    )
    .variant(
        ConstructVariantBuilder(voxel::ROT_UP)
            .place(
                0, 0, 0,
                WeakVoxelVariant("BlockName:ModName", voxel::ROT_UP)
            )
    )
    .priority(0)
    .cost(WeakItemStack("Stone:FactorEras", 1))
);

Boot up the game again and see that you can now build your block if you have at least one stone in your inventory. You can chain these to make the construct request more than one building material :

// Example
.cost(WeakItemStack("Stone:FactorEras", 1))
.cost(WeakItemStack("BronzePlate:VoxelEras", 5))

Adding a custom texture for the icon

To fix the default error icon, you can register your own :

mod_info.add(TextureBuilder(
    "IconId:ModName",
    "mod_folder/assets/items/path/to/Texture.png"
));

Now, you should be able to see in the construct menu that it uses the correct texture.

Making the block breakable and dropping items

Breaking the block

You can place the block, but can’t break it. Here’s how to fix it:

mod_info.add(
    VoxelBuilder(
        "BlockName:ModName",
        "Your Block Name",
        "mod_folder_name/scripts/path/to/block_name.rhai"
    )
    .model(SolidVoxelModel("mod_folder_name/assets/textures/path/to/Texture.png"))
    .does_break()
    .break_rate(voxel::BREAK_RATE_COMPONENT)
);

.does_break() indicate that this block is breakable and .break_rate(break_speed: float) indicate how quick it breaks. There’s a bunch of available constants already : BREAK_RATE_COMPONENT, BREAK_RATE_FRAGILE, BREAK_RATE_HARD, BREAK_RATE_MACHINE, BREAK_RATE_INVENTORY, BREAK_RATE_ORE, BREAK_RATE_PLANT, BREAK_RATE_SOFT or you can set break_speed yourself : 1. is 1 second, 0.5 is 2 seconds, and 2 is 0.5 seconds of time.

Drop items on break

You probably want to get your items back when breaking your block, which is fair. Since we added .does_break(), we can listen to the on_break event. Inside the script, add this function:

fn on_break(voxel, location, target, entity, game_state) {
	if location == target {
		game_state.drop(WeakItemStack("Stone:FactorEras", 1), location);
	}

	event::ACCEPT
}

Now, when breaking the block, a stone will drop on the ground. Call game_state.drop to drop other items if your construct require more than one ingredient. In later tutorials, we will also be able to drop block inventories.

Tags & Pipette

Two other elements you can add into your voxel builder are tags and telling the game which construct to select when this voxel is pipette.

Tags

To add a tag, nothing simpler than:

mod_info.add(
    VoxelBuilder(
        "BlockName:ModName",
        "Your Block Name",
        "mod_folder_name/scripts/path/to/block_name.rhai"
    )
    .model(SolidVoxelModel("mod_folder_name/assets/textures/path/to/Texture.png"))
    .does_break()
    .break_rate(voxel::BREAK_RATE_COMPONENT)
    .tag("Solid:VoxelEras")
);

Here I’m using the "Solid:VoxelEras" (because all the other blocks in the base game uses it). You can check if a VoxelPrefab has a tag by doing prefab.has_tag("Tag:ModName");

Pipette

Instead of going into the construct menu everytime, you have the ability to select an existing block in the world and pipette (default: middle mouse click). To be able to do this with our custom block, we use the pipette function:

mod_info.add(
    VoxelBuilder(
        "BlockName:ModName",
        "Your Block Name",
        "mod_folder_name/scripts/path/to/block_name.rhai"
    )
    .model(SolidVoxelModel("mod_folder_name/assets/textures/path/to/Texture.png"))
    .does_break()
    .break_rate(voxel::BREAK_RATE_COMPONENT)
    .pipette("ConstructName:ModName")
);

Conclusion

And there you go! A whole block added into the game. Next steps could be adding a custom item or an advanced block (COMING SOON).