Growing block
The crystal sprout grows one stage every 5 seconds, from stage 0 to stage 3. If you use a fully grown sprout, you get 3 crystals and it goes back to stage 0.
You use three things.
| What | What it does |
|---|---|
Block state property stage | Which stage the sprout is in. Each stage has a different shape, brightness, and size |
| Block entity | Data saved per block (growth) and code that runs every tick |
| Block behavior | What happens when a player uses the block |
The sprout class
Create a new file, CrystalSprout.java.
final class CrystalSprout {
static final BlockProperty.IntegerProperty STAGE = BlockProperty.integer("stage", 0, 3);
static final int RIPE = 3;
/** Ticks per stage: 100 ticks are 5 seconds. */
static final int TICKS_PER_STAGE = 100;
static final int CRYSTALS_PER_HARVEST = 3;
private CrystalSprout() {}
static void declare(ContentRegistrar content) {
content.block(
"crystal_sprout",
BlockDefinition.builder()
.strength(0.5f, 0.5f)
.sound(BlockSound.AMETHYST)
.noOcclusion()
.shape(state -> BlockShape.box(3, 0, 3, 13, 4 + 3 * state.get(STAGE), 13))
.lightLevel(state -> 2 + 3 * state.get(STAGE))
.properties(STAGE)
.behavior(new SproutBehavior())
.blockEntity(BlockEntityDefinition.builder()
.ticker(CrystalSprout::grow)
// The client draws the growth bar from this data (CrystalGardenClient).
.syncToClients()
.build())
.item(ItemDefinition.builder()
.creativeTab(CreativeTab.NATURAL_BLOCKS)
.build())
.build());
}
.properties(STAGE): putsstage=0..3in the block state..shape(state -> …),.lightLevel(state -> …): the sprout gets bigger and brighter as the stage rises. The game computes these once per state when it builds the block..noOcclusion(): the sprout is not a full cube, so it must not hide the faces of neighboring blocks..blockEntity(...): attaches a block entity to every one of these blocks.syncToClients()sends the data to the client. You use it in Client code to draw the growth display.
Grow every tick
/** Runs every server tick for every sprout. */
static void grow(BlockEntityView sprout) {
WorldView world = sprout.world();
int stage = world.stateAt(sprout.pos()).get(STAGE);
if (stage >= RIPE) {
return;
}
DataTree data = sprout.data();
int growth = data.getInt("growth", 0) + 1;
if (growth >= TICKS_PER_STAGE) {
growth = 0;
world.setProperty(sprout.pos(), STAGE, stage + 1);
}
data.putInt("growth", growth);
// markChanged() saves the data and, with syncToClients(), sends it to clients. Once a second is
// enough for a growth bar, and sending every tick would cost network traffic for nothing.
if (growth % 20 == 0) {
sprout.markChanged();
}
}
The ticker runs on the server every tick (20 times a second). A fully grown sprout returns right away and does no work.
With hundreds of sprouts, this method is called thousands of times a second. markChanged() marks the data for saving and also sends it to clients, so call it only once a second (growth % 20 == 0). A stage change is announced separately by setProperty, so the display does not lag.
Harvest on use
/** Using a ripe sprout harvests it; using a growing one tells how far along it is. */
static final class SproutBehavior implements BlockBehavior {
@Override
public InteractionResult onUse(BlockUse use) {
WorldView world = use.world();
if (world.isClientSide()) {
return InteractionResult.SUCCESS;
}
int stage = world.stateAt(use.pos()).get(STAGE);
if (stage >= RIPE) {
harvest(world, use.pos(), use.player());
} else {
int growth = world.blockEntityAt(use.pos())
.map(entity -> entity.data().getInt("growth", 0))
.orElse(0);
use.player()
.sendMessage("Crystal sprout: stage " + stage + "/" + RIPE + ", "
+ growth * 100 / TICKS_PER_STAGE + "% to the next stage");
}
return InteractionResult.SUCCESS;
}
}
}
onUse is called on both the client and the server. Change game state only on the server (return right away if isClientSide()). You write harvest together with the harvest records on the next page. For now, you can add only the code below and move on.
static void harvest(WorldView world, BlockPos pos, PlayerView player) {
world.setProperty(pos, STAGE, 0);
player.give(CrystalGardenMod.CRYSTAL, CRYSTALS_PER_HARVEST);
}
Connect it to the entrypoint
static final ResourceId CRYSTAL_SPROUT = ResourceId.of(MOD_ID, "crystal_sprout");
// inside onInitializeCommon, after the crystal block
CrystalSprout.declare(mod.content());
Look for each stage
You write the block state file by hand so that each stage uses a different model. It borrows the vanilla amethyst bud models.
{
"variants": {
"stage=0": {"model": "minecraft:block/small_amethyst_bud"},
"stage=1": {"model": "minecraft:block/medium_amethyst_bud"},
"stage=2": {"model": "minecraft:block/large_amethyst_bud"},
"stage=3": {"model": "minecraft:block/amethyst_cluster"}
}
}
{"model": {"type": "minecraft:model", "model": "minecraft:item/small_amethyst_bud"}}
Leave the rest (the drop, the English name, the pickaxe tag) to polyResources. The block state and item files you wrote by hand are used as they are.
polyResources {
// …crystal, crystal_block…
block("crystal_sprout") {
texture = "minecraft:block/budding_amethyst"
minedWith = "pickaxe"
}
}
Add "block.crystal_garden.crystal_sprout": "크리스털 새싹" to ko_kr.json.
Check
Start the game with runClient and place a crystal sprout from the Natural Blocks tab. It grows bigger and brighter every 5 seconds. After it is fully grown, use it by hand and 3 crystals come into your inventory. If you use it while it is growing, it tells you what percent it has grown.
Other block state and block entity features (inventories, menus, data versions) are in Blocks and Block entities and menus.
Next step
Record the harvests in Stored data and commands.