(この記事は一連の解説記事の一つになります)
先頭記事:入門編
前の記事:8. 鉱石の追加と生成
次の記事:99. Modの出力
木の追加
前記事では鉱石の追加について学びました。次は木を追加し、ワールドに生成させてみましょう。だいぶ複雑になってきますので、一緒に頑張っていきましょう。
原木の追加
まず木を構成する原木ブロックを追加します。2. ブロックの追加を参考にブロックを追加していきますが、ところどころ異なる点があるので順にみていきましょう。
//...
public class BlockList {
public static Block ExampleLog = new LogBlock(
MaterialColor.CYAN,
Block.Properties.create(Material.WOOD, MaterialColor.BLUE)
.hardnessAndResistance(2.0F)
.sound(SoundType.WOOD)
.lightValue(6))
.setRegistryName(new ResourceLocation(ExampleMod.MOD_ID, "example_log"));
@SubscribeEvent
public static void registerBlocks(RegistryEvent.Register<Block> event) {
event.getRegistry().registerAll(
ExampleLog
);
}
@SubscribeEvent
public static void registerBlockItems(RegistryEvent.Register<Item> event) {
event.getRegistry().registerAll(
new BlockItem(ExampleLog, new Item.Properties().group(ExampleItemGroup.DEFAULT))
.setRegistryName(new ResourceLocation(ExampleMod.MOD_ID, "example_log"))
);
}
}
基本的には同じですが、単なるBlock
クラスではなく、LogBlock
クラスでつくります。
これはBlock
クラスのサブクラスRotatedPillarBlock
のサブクラスです。RotatedPillarBlock
はBlock
に回転に関するものを追加したものです。そしてLogBlock
ではさらにマップ上での表示に関するものを追加しているようです。
コンストラクタの引数は1番目に一つ増えて、MaterialColor
クラスでマップ上で表示する色を渡します。Block.Properties.create()
の方でも引数が増えて色を渡していますが、これは非垂直設置時のマップ上の色で、先程のほうが垂直設置時の色になるようです。これらは適当に選びましょう。
例のごとくresources
の設定をしていきますが、原木ブロックは面によってテクスチャが異なるため、この設定を以下に示します。これらはminecraftの原木ブロックを参考にしているので特別なことをしない限りこれでよいです。
\src\main\resources
├ assets
│ └ example_mod
│ ├ blockstates
│ │ └ example_log.json
│ ├ lang
│ │ └ en_us.json
│ │ └ ja_jp.json
│ ├ models
│ │ ├ block
│ │ │ └ example_log.json
│ │ └ item
│ │ └ example_log.json
│ └ textures
│ ├ blocks
│ │ ├ example_log.png
│ │ └ example_log_top.png
│ └ items
└ data
└ example_mod
└ loot_tables
└ blocks
└ example_log.json
{
"variants": {
"axis=y": { "model": "example_mod:block/example_log" },
"axis=z": { "model": "example_mod:block/example_log", "x": 90 },
"axis=x": { "model": "example_mod:block/example_log", "x": 90, "y": 90 }
}
}
このように書くことで、おそらくLogBlock
(厳密にはRotatedPillarBlock
)クラスが持つメンバ変数axis
(面の法線方向)がx,y,zのいずれかによって条件分岐するようになっています。(公式ドキュメントにもそれっぽいことが書いてありますね。)
"x":90``"y":90
などの記述は回転角です。要するに、垂直設置状態を基準に原木を倒した表示になるよう回転させています。
{
"parent": "block/cube_column",
"textures": {
"end": "example_mod:blocks/example_log_top",
"side": "example_mod:blocks/example_log"
}
}
parent
にはblock/cube_column
を指定します。これによって立方体型で上下面と側面で区別したテクスチャの適用ができます。それぞれのテクスチャファイルへのパスを指定しましょう。
{
"parent": "example_mod:block/example_log"
}
これは今まで通りblock
のモデルファイルを引き継げばよいです。
{
"block.example_mod.example_log": "Example Log"
}
{
"block.example_mod.example_log": "例原木"
}
言語ファイルも今まで通りです。
{
"type": "minecraft:block",
"pools": [
{
"rolls": 1,
"entries": [
{
"type": "minecraft:item",
"name": "example_mod:example_log"
}
]
}
]
}
いつも通り。
もう一つ原木ブロックに関してやっておくことがあります。
ブロックの追加自体はここまでで完了していますが、あとで木を構成していく段階で必要になるので、ここで行っておきましょう。
今追加した原木ブロックをminecraft:logs
のタグに追加します。
6. レシピの追加でも軽く触れましたが、「タグ」とはすなわち共通因子を持つオブジェクトたちをまとめたグループです。minecraftには原木に相当するものをまとめたタグがあり、木に関する処理をする際にこれを使います。そのため、この原木ブロックもこのタグに含めておくと無駄な実装をしなくて済みます。
\src\main\resources
├ assets
└ data
├ example_mod
└ minecraft
└ tags
└ blocks
└ logs.json
自分のプロジェクトフォルダ内に\src\main\resources\data\minecraft\tags\blocks
フォルダを作り、そこにlogs.json
を配置します。この名前は必ず同じにしてください。
{
"replace": false,
"values": [
"example_mod:example_log"
]
}
replace
にfalse
を与えることにより、同名のminecraft:logs
にこのファイルでの記述が統合されます。values
の中でブロックを指定しましょう。
デバッグモードでブロックにカーソルを合わせた際、赤下線を引いた部分のように#minecraft:logs
と表示されていれば完了です。
葉の追加
次に木を構成する葉ブロックを追加します。重ねてになりますが、ベースは2. ブロックの追加を参考に、順にみていきましょう。
//...
public class BlockList {
public static Block ExampleLeaves = new LeavesBlock(
Block.Properties.create(Material.LEAVES)
.hardnessAndResistance(0.2F)
.tickRandomly()
.sound(SoundType.PLANT)
.lightValue(8))
.setRegistryName(new ResourceLocation(ExampleMod.MOD_ID, "example_leaves"));
@SubscribeEvent
public static void registerBlocks(RegistryEvent.Register<Block> event) {
event.getRegistry().registerAll(
ExampleLeaves
);
}
@SubscribeEvent
public static void registerBlockItems(RegistryEvent.Register<Item> event) {
event.getRegistry().registerAll(
new BlockItem(ExampleLeaves, new Item.Properties().group(ExampleItemGroup.DEFAULT))
.setRegistryName(new ResourceLocation(ExampleMod.MOD_ID, "example_leaves"))
);
}
}
葉ブロックはLeavesBlock
でつくります。その他はいつも通りです。
resources
の設定をしていきます。
\src\main\resources
├ assets
│ └ example_mod
│ ├ blockstates
│ │ └ example_leaves.json
│ ├ lang
│ │ └ en_us.json
│ │ └ ja_jp.json
│ ├ models
│ │ ├ block
│ │ │ └ example_leaves.json
│ │ └ item
│ │ └ example_leaves.json
│ └ textures
│ ├ blocks
│ │ └ example_leaves.png
│ └ items
└ data
└ example_mod
└ loot_tables
└ blocks
└ example_leaves.json
{
"variants": {
"": { "model": "example_mod:block/example_leaves" }
}
}
{
"parent": "block/leaves",
"textures": {
"all": "example_mod:blocks/example_leaves"
}
}
parent
にはblock/leaves
を指定しましょう。(グラフィック設定にもよりますが)半透明なテクスチャが適用できます。
{
"parent": "example_mod:block/example_leaves"
}
{
"block.example_mod.example_leaves": "Example Leaves"
}
{
"block.example_mod.example_leaves": "例の葉"
}
これらはいつも通り。
{
"type": "minecraft:block",
"pools": [
{
"rolls": 1,
"entries": [
{
"type": "minecraft:alternatives",
"children": [
{
"type": "minecraft:item",
"conditions": [
{
"condition": "minecraft:alternative",
"terms": [
{
"condition": "minecraft:match_tool",
"predicate": {
"item": "minecraft:shears"
}
},
{
"condition": "minecraft:match_tool",
"predicate": {
"enchantments": [
{
"enchantment": "minecraft:silk_touch",
"levels": {
"min": 1
}
}
]
}
}
]
}
],
"name": "example_mod:example_leaves"
},
{
"type": "minecraft:item",
"conditions": [
{
"condition": "minecraft:survives_explosion"
},
{
"condition": "minecraft:table_bonus",
"enchantment": "minecraft:fortune",
"chances": [
0.05,
0.0625,
0.083333336,
0.1
]
}
],
"name": "example_mod:example_sapling"
}
]
}
]
}
]
}
loot_table
ファイルは今まで結構異なります。(今まではさぼってきましたが)葉ブロックは複数のドロップをもちエンチャントやツールにも依存するのできちんと実装するとこのようになります。
詳しくは参考ページに説明を譲りますが、簡単に説明します。
これは「ブロックからのドロップであり、数は1つ、children
のどれかを返す。children
は2つあって、まずハサミあるいはシルクタッチⅠのツールで壊した場合に葉ブロックを返すもの。次にそれ以外の場合のランダム確率(幸運エンチャで確率アップ)で苗を返すもの。」という風に書かれています。ここで先に出てしまい順番が前後しますが、次の項で苗の追加を行います。
これはminecraftのOrkの葉を参考にしています。各自色々と見て好きなように書きましょう。
最後に、MaterialColor
の設定をします。これは葉や草がバイオームに応じて色が変わる効果をもたらすものです。ここは面倒な場合無視してもよいです。無視する場合全バイオームで同じ色が表示されるのでその色でテクスチャを用意しましょう。
//...
public class BlockList {
//...
@SubscribeEvent
public static void registerBlockColors(ColorHandlerEvent.Block event) {
event.getBlockColors().register((p_210229_0_, p_210229_1_, p_210229_2_, p_210229_3_) -> {
return p_210229_1_ != null && p_210229_2_ != null ? BiomeColors.getFoliageColor(p_210229_1_, p_210229_2_) : FoliageColors.getDefault();
}, ExampleLeaves);
}
@SubscribeEvent
public static void registerBlockItemColors(ColorHandlerEvent.Item event) {
event.getItemColors().register((p_210235_1_, p_210235_2_) -> {
BlockState blockstate = ((BlockItem)p_210235_1_.getItem()).getBlock().getDefaultState();
return event.getBlockColors().getColor(blockstate, (IEnviromentBlockReader)null, (BlockPos)null, p_210235_2_);
}, ExampleLeaves);
}
}
宣言と登録をしているBlockList.java
で同時にこれも登録してしまいます。ColorHandlerEvent
というものがあるので、これをつかってregister()
することで反映されます。ブロックとアイテムそれぞれについて行います。変数などが意味不明な文字の羅列になっていますが、これは難読化されたminecraftのコードからそのまま引っ張ってきただけなので変えてもよいです。
ブロックについては、BiomeColors.getFoliageColor()
によってそのブロックが存在するバイオームに応じた色が取得され、登録されます。
アイテムについては、デフォルトの色が取得され、登録されます。
両者ともに、register()
の第一引数に色(ラムダ式で取得してます)、第二引数に色を設定するオブジェクトを渡します。
この辺りは難しいことをやろうとしない限り深く理解しなくてよいです。
実際の色については、参考ページに詳しく書いてあります。基本的にはグレースケールの葉ブロックテクスチャを用意して色を載せているようですが、今回色味がかったファイルを用意してみた場合実際のブロックもその色味になったので、おそらく内部では加算してるのかな、と思っています。
TreeFeatureクラスとTreeクラスの追加
これらは違いを言葉で説明するのが難しいですが、ともに木を管理するクラスです。Tree
クラスは木自体を管理するクラスで、この後で追加する苗に関連して必要になります。一方で、TreeFeature
クラスは木の生成に関することのみを管理しており、Tree
クラスから該当するTreeFeature
を取得することもできます。
\src\main\java\jp\koteko\example_mod\
├ blocks
│ └ trees
│ └ ExampleTree.java
├ items
├ lists
├ world
│ └ features
│ └ ExampleTreeFeature.java
└ ExampleMod.java
(ファイル配置はいろいろ参考にして合わせているつもりですが、悩んだら割と適当です。)
package jp.koteko.example_mod.world.features;
import jp.koteko.example_mod.lists.BlockList;
import net.minecraft.world.gen.feature.NoFeatureConfig;
import net.minecraft.world.gen.feature.TreeFeature;
import net.minecraftforge.common.IPlantable;
public class ExampleTreeFeature extends TreeFeature {
public ExampleTreeFeature() {
super(NoFeatureConfig::deserialize, false, 4, BlockList.ExampleLog.getDefaultState(), BlockList.ExampleLeaves.getDefaultState(), false);
setSapling((IPlantable) BlockList.ExampleSapling);
}
}
TreeFeature
クラスを継承してExampleTreeFeature
クラスを作ります。継承元のコンストラクタに渡す引数は順に以下の通りです。
第一引数はNoFeatureConfig
のインスタンスで多分特に有効に使われません。
第二引数は何らかの通知の可否を決定するboolean
値で、他の木々でfalseだったため真似しました。何かわかったら追記します。
第三引数は木の最小の高さを決めるint
値です。
第四引数は幹となるブロックのBlockState
です。
第五引数は葉となるブロックのBlockState
です。
第六引数は木にツタを伸ばすかどうかを決めるboolean
値です。
また、setSapling()
は苗の設定をしており、ここに苗のインスタンスを渡します。苗は次の項で実装します。
package jp.koteko.example_mod.blocks.trees;
import jp.koteko.example_mod.world.features.ExampleTreeFeature;
import net.minecraft.block.trees.Tree;
import net.minecraft.world.gen.feature.AbstractTreeFeature;
import net.minecraft.world.gen.feature.NoFeatureConfig;
import javax.annotation.Nullable;
import java.util.Random;
public class ExampleTree extends Tree {
@Nullable
protected AbstractTreeFeature<NoFeatureConfig> getTreeFeature(Random random) {
return new ExampleTreeFeature();
}
}
抽象クラスTree
を継承してExampleTree
クラスを作ります。getTreeFeature
というメソッドを定義します。先ほど定義したExampleTreeFeature
のインスタンスを返すようにしましょう。
引数で乱数を受け取って使っていませんが、これはオークの木などが確率で通常種と巨大種に分かれる場合などに使われています。
苗の追加
次に苗を追加していきます。
まず苗のクラスを用意します。
\src\main\java\jp\koteko\example_mod\
├ blocks
│ ├ trees
│ └ ExampleSapling.java
├ items
├ lists
├ world
└ ExampleMod.java
package jp.koteko.example_mod.blocks;
import net.minecraft.block.Block;
import net.minecraft.block.BlockState;
import net.minecraft.block.BushBlock;
import net.minecraft.block.IGrowable;
import net.minecraft.block.trees.Tree;
import net.minecraft.state.IntegerProperty;
import net.minecraft.state.StateContainer;
import net.minecraft.state.properties.BlockStateProperties;
import net.minecraft.util.math.BlockPos;
import net.minecraft.util.math.shapes.ISelectionContext;
import net.minecraft.util.math.shapes.VoxelShape;
import net.minecraft.world.IBlockReader;
import net.minecraft.world.IWorld;
import net.minecraft.world.World;
import java.util.Random;
public class BlockExampleSapling extends BushBlock implements IGrowable {
public static final IntegerProperty STAGE = BlockStateProperties.STAGE_0_1;
protected static final VoxelShape SHAPE = Block.makeCuboidShape(2.0D, 0.0D, 2.0D, 14.0D, 12.0D, 14.0D);
private final Tree tree;
public BlockExampleSapling(Tree p_i48337_1_, Block.Properties properties) {
super(properties);
this.tree = p_i48337_1_;
this.setDefaultState(this.stateContainer.getBaseState().with(STAGE, Integer.valueOf(0)));
}
public VoxelShape getShape(BlockState state, IBlockReader worldIn, BlockPos pos, ISelectionContext context) {
return SHAPE;
}
public void tick(BlockState state, World worldIn, BlockPos pos, Random random) {
super.tick(state, worldIn, pos, random);
if (!worldIn.isAreaLoaded(pos, 1)) return; // Forge: prevent loading unloaded chunks when checking neighbor's light
if (worldIn.getLight(pos.up()) >= 9 && random.nextInt(7) == 0) {
this.grow(worldIn, pos, state, random);
}
}
public void grow(IWorld worldIn, BlockPos pos, BlockState state, Random rand) {
if (state.get(STAGE) == 0) {
worldIn.setBlockState(pos, state.cycle(STAGE), 4);
} else {
if (!net.minecraftforge.event.ForgeEventFactory.saplingGrowTree(worldIn, rand, pos)) return;
this.tree.spawn(worldIn, pos, state, rand);
}
}
/**
* Whether this IGrowable can grow
*/
public boolean canGrow(IBlockReader worldIn, BlockPos pos, BlockState state, boolean isClient) {
return true;
}
public boolean canUseBonemeal(World worldIn, Random rand, BlockPos pos, BlockState state) {
return (double)worldIn.rand.nextFloat() < 0.45D;
}
public void grow(World worldIn, Random rand, BlockPos pos, BlockState state) {
this.grow(worldIn, pos, state, rand);
}
protected void fillStateContainer(StateContainer.Builder<Block, BlockState> builder) {
builder.add(STAGE);
}
}
このコードについてですが、(ほぼ)すべてminecraftの苗のコードと同じです。じゃあ何故わざわざ作ったか、というと、minecraftのSapling
クラスのコンストラクタがprotected
で、modのコード内から直接使えなかったためです。minecraft側がこれをどう扱っているかは理解する時間が不足していたためここでは考えないことにしますが、おそらくそちらに合わせた形でのもう少しうまい実装の仕方があると思います。
パッケージ名、クラス名、コンストラクタの3つのみ変更して使います。
//...
public class BlockList {
public static Block ExampleSapling = new BlockExampleSapling(
new ExampleTree(),
Block.Properties.create(Material.PLANTS)
.doesNotBlockMovement()
.tickRandomly()
.hardnessAndResistance(0.0F)
.sound(SoundType.PLANT))
.setRegistryName(new ResourceLocation(ExampleMod.MOD_ID, "example_sapling"));
@SubscribeEvent
public static void registerBlocks(RegistryEvent.Register<Block> event) {
event.getRegistry().registerAll(
ExampleSapling
);
}
@SubscribeEvent
public static void registerBlockItems(RegistryEvent.Register<Item> event) {
event.getRegistry().registerAll(
new BlockItem(ExampleSapling, new Item.Properties().group(ExampleItemGroup.DEFAULT))
.setRegistryName(new ResourceLocation(ExampleMod.MOD_ID, "example_sapling"))
);
}
}
先程用意した苗のクラスでブロックを追加します。
第一引数は対応する木を渡すので、先ほど作ったExampleTree
クラスのインスタンスを渡します。
いつも通りresources
の設定をしていきます。特に変わらないものは解説を省略します。
\src\main\resources
├ assets
│ └ example_mod
│ ├ blockstates
│ │ └ example_sapling.json
│ ├ lang
│ │ └ en_us.json
│ │ └ ja_jp.json
│ ├ models
│ │ ├ block
│ │ │ └ example_sapling.json
│ │ └ item
│ │ └ example_sapling.json
│ └ textures
│ ├ blocks
│ │ └ example_sapling.png
│ └ items
└ data
└ example_mod
└ loot_tables
└ blocks
└ example_sapling.json
{
"variants": {
"": { "model": "example_mod:block/example_sapling" }
}
}
{
"parent": "block/cross",
"textures": {
"cross": "example_mod:blocks/example_sapling"
}
}
parent
にはblock/cross
を指定します。これによって平面が交差したような形でテクスチャの適用ができます(実際に形を見れば言ってる意味が分かると思います)。
{
"parent": "item/generated",
"textures": {
"layer0": "example_mod:blocks/example_sapling"
}
}
block
のモデルファイルを引き継ぐのではなく、item/generated
で指定します。
{
"block.example_mod.example_sapling": "Example Sapling"
}
{
"block.example_mod.example_sapling": "例の苗"
}
{
"type": "minecraft:block",
"pools": [
{
"rolls": 1,
"entries": [
{
"type": "minecraft:item",
"name": "example_mod:example_sapling"
}
]
}
]
}
ゲームを起動して確認してみましょう。
苗が追加され、骨粉で木が育つことが確認できると思います。
また、木の幹を除去すると、葉ブロックが自然消滅をはじめ、消える際に確率で苗をドロップすることが確認できると思います。
木の生成
ここまで来たらあと少しです。ここまでで実装した木をワールド生成時に自動で生成されるようにしましょう。
これは8. 鉱石の追加と生成とほぼ同じことをするのでそちらも参考にしてください。
\src\main\java\jp\koteko\example_mod\
├ blocks
├ items
├ lists
├ world
│ ├ WorldGenOres.java
│ └ WorldGenTrees.java
└ ExampleMod.java
WorldGenTrees.java
を配置します。
package jp.koteko.example_mod.world;
import jp.koteko.example_mod.world.features.ExampleTreeFeature;
import net.minecraft.world.biome.Biome;
import net.minecraft.world.gen.GenerationStage;
import net.minecraft.world.gen.feature.Feature;
import net.minecraft.world.gen.feature.IFeatureConfig;
import net.minecraft.world.gen.feature.NoFeatureConfig;
import net.minecraft.world.gen.placement.AtSurfaceWithExtraConfig;
import net.minecraft.world.gen.placement.Placement;
import net.minecraftforge.registries.ForgeRegistries;
public class WorldGenTrees {
public static void setup() {
addTreeToOverworld(new ExampleTreeFeature());
}
private static void addTreeToOverworld(Feature<NoFeatureConfig> featureIn) {
for(Biome biome : ForgeRegistries.BIOMES) {
if (!biome.getCategory().equals(Biome.Category.NETHER) && !biome.getCategory().equals(Biome.Category.THEEND)) {
biome.addFeature(
GenerationStage.Decoration.VEGETAL_DECORATION,
Biome.createDecoratedFeature(
featureIn,
IFeatureConfig.NO_FEATURE_CONFIG,
Placement.COUNT_EXTRA_HEIGHTMAP,
new AtSurfaceWithExtraConfig(2, 0.1F, 1)
)
);
}
}
}
}
AtSurfaceWithExtraConfig
の引数は、1チャンク当たりの抽選回数、追加抽選を行う可能性、追加抽選を行う場合の追加回数です。この例の場合、「1チャンク当たり2か所抽選を行い、10%の確率でさらに1回抽選を行う」です。
最後に、今定義したWorldGenOres.setup()
をメインファイル内のsetup
中で呼びます。
//...
public class ExampleMod
{
//...
private void setup(final FMLCommonSetupEvent event)
{
WorldGenTrees.setup();
}
//...
}
ゲームを起動して新たにワールドを生成します。
なんとなく光らせておいたので夜だと一層目立ちます。
木の追加と生成ができました!
参考
1.14.3 Tags help - Modder Support - Forge Forums
バイオーム - Minecraft Wiki
ルートテーブル - Minecraft Wiki
地図アイテムフォーマット - Minecraft Wiki