概要
村や要塞のような構造物を追加するための解説とサンプルコードです。
既存の構造物生成に関する解説
構造物生成は、
1.未生成チャンクに配置する構造物の基点と構成をあらかじめ決めておく
2.チャンク生成時に構造物の一部がそのチャンク範囲と重なっていたら構造物の構成ブロックを配置する
構造物の基点と構成パーツ(家や通路等)の決定も実はチャンクの生成がトリガーとなっています。
例としてはChunkProviderGenerateクラスのprovideChunkメソッド内の、
this.caveGenerator.generate(this, this.worldObj, par1, par2, abyte); this.ravineGenerator.generate(this, this.worldObj, par1, par2, abyte); if (this.mapFeaturesEnabled) { this.mineshaftGenerator.generate(this, this.worldObj, par1, par2, abyte); this.villageGenerator.generate(this, this.worldObj, par1, par2, abyte); this.strongholdGenerator.generate(this, this.worldObj, par1, par2, abyte); this.scatteredFeatureGenerator.generate(this, this.worldObj, par1, par2, abyte); }
があります。
xxxGeneratorは名前のとおり、洞窟や渓谷や村や要塞の生成を担当するクラスのインスタンスです。 なお、generateメソッドは実際にブロックを配置するメソッドではなく、上に書いた構造物の基点と構成パーツの決定を行うメソッドです。
generateメソッドが呼ばれるとそのチャンクから距離8チャンク以内のチャンクのchunkX,chunkZが構造物の基点になるかどうかをcanSpawnStructureAtCoordsメソッドで判定し、構造物の基点になる場合はgetStructureStartメソッドで構造物の構成を決定します。 ここで確定した構造物の基点と構成パーツの情報はStructureStartクラスが保持し、ワールドNBTに保存されます。
チャンク生成はChunkProviderGenerateクラスのpopulateメソッドで行われますが、
if (this.mapFeaturesEnabled) { this.mineshaftGenerator.generateStructuresInChunk(this.worldObj, this.rand, par2, par3); flag = this.villageGenerator.generateStructuresInChunk(this.worldObj, this.rand, par2, par3); this.strongholdGenerator.generateStructuresInChunk(this.worldObj, this.rand, par2, par3); this.scatteredFeatureGenerator.generateStructuresInChunk(this.worldObj, this.rand, par2, par3); }
で構造物の構成パーツのうち、そのチャンクと位置が重なっているパーツのブロックを配置します。
独自構造物の追加方法
独自構造物を追加するには、
1.チャンク生成イベントをフックする
2.追加構造物を表すMapGenStructureのサブクラスを追加する
3.構造物の基点を表し、追加構造物の構成パーツの決定を行うStructureStartのサブクラスを追加する
4.構造物の構成パーツを表し、実際にブロックを設置するStructureComponentのサブクラスを追加する
の手順が必要です。
また、追加構造物・構成パーツはMapGenStructureIOに登録しておく必要があります。
独自構造物追加のサンプルコード
Forge 1.6.4-9.11.1.933で確認しました。 https://github.com/dewfalse/sampledungeon にも同じサンプルコードを置いています。
- SampleDungeon.java
MODのエントリポイント。 構造物生成のイベント登録と、追加構造物・追加構造物のパーツを登録しています。
package sampledungeon; import net.minecraft.world.gen.structure.MapGenStructureIO; import net.minecraftforge.common.MinecraftForge; import cpw.mods.fml.common.Mod; import cpw.mods.fml.common.Mod.EventHandler; import cpw.mods.fml.common.event.FMLInitializationEvent; @Mod(modid = SampleDungeon.modid, name = SampleDungeon.modid, version = "1.0") public class SampleDungeon { public static final String modid = "sampledungeon"; @EventHandler public void init(FMLInitializationEvent event) { // チャンク生成時に追加構造物の生成が行われるようにフック MinecraftForge.EVENT_BUS.register(new SampleDungeonEventHandler()); // 構造物・構成パーツは名前をMapGenStructureIOに登録しなければならない MapGenStructureIO.func_143034_b(StructureSampleDungeonStart.class, "SampleDungeon"); MapGenStructureIO.func_143031_a(ComponentSampleDungeon1.class, "SD1"); MapGenStructureIO.func_143031_a(ComponentSampleDungeon2.class, "SD2"); MapGenStructureIO.func_143031_a(ComponentSampleDungeon3.class, "SD3"); MapGenStructureIO.func_143031_a(ComponentSampleDungeon4.class, "SD4"); } }
- SampleDungeonEventHandler.java
村や要塞等と同じタイミングで追加構造物の生成が行われるようにしています。
package sampledungeon; import net.minecraftforge.event.ForgeSubscribe; import net.minecraftforge.event.terraingen.InitNoiseGensEvent; import net.minecraftforge.event.terraingen.PopulateChunkEvent; public class SampleDungeonEventHandler { MapGenSampleDungeon mapGenSampleDungeon = new MapGenSampleDungeon(); // コンストラクタ相当(getModdedMapGen or InitMapGenEvent)相当 @ForgeSubscribe public void onInitNoiseGensEvent(InitNoiseGensEvent event) { } // generateStructuresInChunk相当 // 要塞や村より前のタイミングならこちら // ここで生成すると、要塞や村・溶岩溜まり等の後で生成される地形要素に潰される可能性がある // その代わり村や要塞を避けるような判定は不要 @ForgeSubscribe public void onPopulateChunkEvent(PopulateChunkEvent.Pre event) { // 通常世界(Overworld)にサンプルダンジョンを生成したいのでディメンションIDで通常世界かどうか判断する if(event.world.provider.dimensionId == 0) { // 8チャンク以内に追加構造物生成に適したチャンクがあるかを調べ、ある場合は生成する追加構造物の構成パーツを決定する mapGenSampleDungeon.generate(event.chunkProvider, event.world, event.chunkX, event.chunkZ, null); //追加構造物の一部が このチャンク範囲に重複するかどうかを調べ、重複する場合は追加構造物のブロックをチャンク内に設置する mapGenSampleDungeon.generateStructuresInChunk(event.world, event.rand, event.chunkX, event.chunkZ); } } // generateStructuresInChunk相当 // 要塞や村より後のタイミングならこちら // event.typeにより順番が決まる&呼ばれなかったりするので注意 // ここで生成するなら要塞等を潰さないように注意 @ForgeSubscribe public void onPopulateChunkEvent(PopulateChunkEvent.Populate event) { } // generateStructuresInChunk相当 // 完全にチャンクの要素が決定された後のタイミングならこちら // ここで生成するなら要塞等を潰さないように注意 @ForgeSubscribe public void onPopulateChunkEvent(PopulateChunkEvent.Post event) { } }
- MapGenSampleDungeon.java
追加構造物の管理クラス。追加構造物の基点となるチャンク座標はここで決定されます。
package sampledungeon; import net.minecraft.world.gen.structure.MapGenStructure; import net.minecraft.world.gen.structure.StructureStart; public class MapGenSampleDungeon extends MapGenStructure { @Override public String func_143025_a() { // 構造物名 return "SampleDungeon"; } @Override protected boolean canSpawnStructureAtCoords(int i, int j) { // ここではチャンク座標が0,0の場所に構造物を生成するものとする return i == 0 && j == 0; } @Override protected StructureStart getStructureStart(int i, int j) { return new StructureSampleDungeonStart(this.worldObj, this.rand, i, j); } }
- StructureSampleDungeonStart.java
追加構造物の基点を表すクラス。コンストラクタで追加構造物を構成するパーツリストを決定しています。
package sampledungeon; import java.util.List; import java.util.Random; import net.minecraft.world.World; import net.minecraft.world.gen.structure.StructureComponent; import net.minecraft.world.gen.structure.StructureStart; public class StructureSampleDungeonStart extends StructureStart { public StructureSampleDungeonStart() {} public StructureSampleDungeonStart(World par1World, Random par2Random, int par3, int par4) { super(par3, par4); // 構造物の構成パーツを決定する // 基点はComponentSampleDungeon1 ComponentSampleDungeon1 componentSampleDungeon1 = new ComponentSampleDungeon1(0, par2Random, (par3 << 4) + 2, (par4 << 4) + 2); this.components.add(componentSampleDungeon1); // 次のパーツを得る componentSampleDungeon1.buildComponent(componentSampleDungeon1, components, par2Random); // 次のパーツが決定していないパーツは一時的にstructureComponentsに保持されるので、空になるまで次のパーツの決定を続ける List<StructureComponent> list = componentSampleDungeon1.structureComponents; while(!list.isEmpty()) { int k = par2Random.nextInt(list.size()); StructureComponent structurecomponent = list.remove(k); structurecomponent.buildComponent(componentSampleDungeon1, this.components, par2Random); } // 構造物全体の占有範囲を更新する this.updateBoundingBox(); } }
- ComponentSampleDungeon1.java
追加構造物を構成するパーツその1。石で構成される縦長豆腐。追加構造物は必ずこのパーツから開始します。(StructureSampleDungeonStartのコンストラクタ参照)
なお、このパーツには必ずComponentSampleDungeon2のパーツが斜め横に接続します。(buildComponentメソッド参照)
package sampledungeon; import java.util.ArrayList; import java.util.List; import java.util.Random; import net.minecraft.block.Block; import net.minecraft.nbt.NBTTagCompound; import net.minecraft.world.World; import net.minecraft.world.gen.structure.StructureBoundingBox; import net.minecraft.world.gen.structure.StructureComponent; public class ComponentSampleDungeon1 extends StructureComponent { // 構成パーツリストを記憶するためのリスト public List<StructureComponent> structureComponents = new ArrayList(); public ComponentSampleDungeon1() {} public ComponentSampleDungeon1(int par1, Random par2Random, int par3, int par4) { // 東西南北の方向をランダムに決める this.coordBaseMode = par2Random.nextInt(4); switch(this.coordBaseMode) { case 0: case 1: case 2: case 3: default: // 占有範囲を設定(このサンプルではどの方角を向いてても同じ) // (x,y,z) = (par3, 64, par4)の地点から4x10x4ブロックが占有範囲 this.boundingBox = new StructureBoundingBox(par3, 64, par4, par3 + 4, 74, par4 + 4); break; } } @Override public void buildComponent(StructureComponent par1StructureComponent, List par2List, Random par3Random) { // 次のパーツはComponentSampleDungeon2を斜めに接続 StructureComponent structureComponent = new ComponentSampleDungeon2(0, par3Random, this.boundingBox.maxX + 1, this.boundingBox.maxZ + 1, 0); ((ComponentSampleDungeon1)par1StructureComponent).structureComponents.add(structureComponent); par2List.add(structureComponent); } @Override protected void func_143012_a(NBTTagCompound nbttagcompound) { // TODO Auto-generated method stub } @Override protected void func_143011_b(NBTTagCompound nbttagcompound) { // TODO Auto-generated method stub } @Override public boolean addComponentParts(World world, Random random, StructureBoundingBox structureboundingbox) { // 建設予定範囲内に液体があった場合は建設中止 if(this.isLiquidInStructureBoundingBox(world, structureboundingbox)) { return false; } // 占有範囲(structureboundingbox)内の指定範囲を指定ブロック&メタデータで埋める // 占有範囲内の指定範囲は占有範囲原点を基準として(0,0,0)-(4,10,4)の範囲 this.fillWithMetadataBlocks(world, structureboundingbox, 0, 0, 0, 4, 10, 4, Block.stone.blockID, 0, 0, 0, false); // 占有範囲(structureboundingbox)内の指定範囲を空気ブロックで埋める // 占有範囲内の指定範囲は占有範囲原点を基準として(1,1,1)-(3,9,3)の範囲 // 要するに中をくりぬいてるってことです this.fillWithAir(world, structureboundingbox, 1, 1, 1, 3, 9, 3); // 占有範囲(structureboundingbox)内の指定範囲を置き換える // 占有範囲内の指定範囲は占有範囲原点を基準として(0,1,2), (0,2,2), (0,3,2)の位置を空気ブロックに置き換えている // 入り口っぽく壁に穴を空けている。coordBaseModeでランダムな方向になってることを確認できるようにするためです this.placeBlockAtCurrentPosition(world, 0, 0, 0, 1, 2, structureboundingbox); this.placeBlockAtCurrentPosition(world, 0, 0, 0, 2, 2, structureboundingbox); this.placeBlockAtCurrentPosition(world, 0, 0, 0, 3, 2, structureboundingbox); return true; } }
- ComponentSampleDungeon2.java
追加構造物を構成するパーツその2。羊毛で構成される縦長豆腐。
羊毛の色を変化させながら同じパーツが斜め横に連続して接続し、15個接続するとパーツ3かパーツ4どちらかにランダムに接続します。(buildComponentメソッド参照)
package sampledungeon; import java.util.List; import java.util.Random; import net.minecraft.block.Block; import net.minecraft.nbt.NBTTagCompound; import net.minecraft.world.World; import net.minecraft.world.gen.structure.StructureBoundingBox; import net.minecraft.world.gen.structure.StructureComponent; public class ComponentSampleDungeon2 extends StructureComponent { // 構成ブロック(羊毛)の色 int color = 0; public ComponentSampleDungeon2() {} public ComponentSampleDungeon2(int par1, Random par2Random, int par3, int par4, int par5) { this.coordBaseMode = par2Random.nextInt(4); switch(this.coordBaseMode) { case 0: case 1: case 2: case 3: default: this.boundingBox = new StructureBoundingBox(par3, 64, par4, par3 + 4, 74, par4 + 4); break; } this.color = par5; } @Override protected void func_143012_a(NBTTagCompound nbttagcompound) { // TODO Auto-generated method stub } @Override protected void func_143011_b(NBTTagCompound nbttagcompound) { // TODO Auto-generated method stub } @Override public void buildComponent(StructureComponent par1StructureComponent, List par2List, Random par3Random) { // 構成ブロックである羊毛の色を変えながらパーツをつなげる if(this.color < 15) { StructureComponent structureComponent1 = new ComponentSampleDungeon2(0, par3Random, this.boundingBox.maxX + 1, this.boundingBox.maxZ + 1, color + 1); ((ComponentSampleDungeon1)par1StructureComponent).structureComponents.add(structureComponent1); par2List.add(structureComponent1); } else { // 羊毛の色を変えながらパーツをつなげて、最後の色まで到達したら終端のパーツをつなげる // 若干ランダムっぽさを演出するためパーツ3とパーツ4をランダムに選択 if(par3Random.nextBoolean()) { StructureComponent structureComponent = new ComponentSampleDungeon3(0, par3Random, this.boundingBox.minX, this.boundingBox.maxZ + 1); ((ComponentSampleDungeon1)par1StructureComponent).structureComponents.add(structureComponent); par2List.add(structureComponent); } else { StructureComponent structureComponent = new ComponentSampleDungeon4(0, par3Random, this.boundingBox.maxX + 1, this.boundingBox.minZ); ((ComponentSampleDungeon1)par1StructureComponent).structureComponents.add(structureComponent); par2List.add(structureComponent); } } } @Override public boolean addComponentParts(World world, Random random, StructureBoundingBox structureboundingbox) { if(this.isLiquidInStructureBoundingBox(world, structureboundingbox)) { return false; } // 指定の色の羊毛ブロックで範囲を埋める this.fillWithMetadataBlocks(world, structureboundingbox, 0, 0, 0, 4, 10, 4, Block.cloth.blockID, this.color, 0, 0, false); this.fillWithAir(world, structureboundingbox, 1, 1, 1, 3, 9, 3); this.placeBlockAtCurrentPosition(world, 0, 0, 0, 1, 2, structureboundingbox); this.placeBlockAtCurrentPosition(world, 0, 0, 0, 2, 2, structureboundingbox); this.placeBlockAtCurrentPosition(world, 0, 0, 0, 3, 2, structureboundingbox); return true; } }
- ComponentSampleDungeon3.java
追加構造物を構成するパーツその3。鉄ブロックで構成される縦長豆腐。 この追加構造物の終端は必ずこのパーツかパーツその4になります。
package sampledungeon; import java.util.List; import java.util.Random; import net.minecraft.block.Block; import net.minecraft.nbt.NBTTagCompound; import net.minecraft.world.World; import net.minecraft.world.gen.structure.StructureBoundingBox; import net.minecraft.world.gen.structure.StructureComponent; public class ComponentSampleDungeon3 extends StructureComponent { public ComponentSampleDungeon3() {} public ComponentSampleDungeon3(int par1, Random par2Random, int par3, int par4) { this.coordBaseMode = par2Random.nextInt(4); switch(this.coordBaseMode) { case 0: case 1: case 2: case 3: default: this.boundingBox = new StructureBoundingBox(par3, 64, par4, par3 + 4, 74, par4 + 4); break; } } @Override protected void func_143012_a(NBTTagCompound nbttagcompound) { // TODO Auto-generated method stub } @Override protected void func_143011_b(NBTTagCompound nbttagcompound) { // TODO Auto-generated method stub } @Override public void buildComponent(StructureComponent par1StructureComponent, List par2List, Random par3Random) { } @Override public boolean addComponentParts(World world, Random random, StructureBoundingBox structureboundingbox) { if(this.isLiquidInStructureBoundingBox(world, structureboundingbox)) { return false; } // 鉄ブロックで範囲を埋める this.fillWithMetadataBlocks(world, structureboundingbox, 0, 0, 0, 4, 10, 4, Block.blockIron.blockID, 0, 0, 0, false); this.fillWithAir(world, structureboundingbox, 1, 1, 1, 3, 9, 3); this.placeBlockAtCurrentPosition(world, 0, 0, 0, 1, 2, structureboundingbox); this.placeBlockAtCurrentPosition(world, 0, 0, 0, 2, 2, structureboundingbox); this.placeBlockAtCurrentPosition(world, 0, 0, 0, 3, 2, structureboundingbox); return true; } }
- ComponentSampleDungeon4 .java
追加構造物を構成するパーツその4。ダイヤブロックで構成される縦長豆腐。 この追加構造物の終端は必ずこのパーツかパーツその3になります。
package sampledungeon; import java.util.Random; import net.minecraft.block.Block; import net.minecraft.nbt.NBTTagCompound; import net.minecraft.world.World; import net.minecraft.world.gen.structure.StructureBoundingBox; import net.minecraft.world.gen.structure.StructureComponent; public class ComponentSampleDungeon4 extends StructureComponent { public ComponentSampleDungeon4() {} public ComponentSampleDungeon4(int par1, Random par2Random, int par3, int par4) { this.coordBaseMode = par2Random.nextInt(4); switch(this.coordBaseMode) { case 0: case 1: case 2: case 3: default: this.boundingBox = new StructureBoundingBox(par3, 64, par4, par3 + 4, 74, par4 + 4); break; } } @Override protected void func_143012_a(NBTTagCompound nbttagcompound) { // TODO Auto-generated method stub } @Override protected void func_143011_b(NBTTagCompound nbttagcompound) { // TODO Auto-generated method stub } @Override public boolean addComponentParts(World world, Random random, StructureBoundingBox structureboundingbox) { if(this.isLiquidInStructureBoundingBox(world, structureboundingbox)) { return false; } // ダイヤブロックで範囲を埋める this.fillWithMetadataBlocks(world, structureboundingbox, 0, 0, 0, 4, 10, 4, Block.blockDiamond.blockID, 0, 0, 0, false); this.fillWithAir(world, structureboundingbox, 1, 1, 1, 3, 9, 3); this.placeBlockAtCurrentPosition(world, 0, 0, 0, 1, 2, structureboundingbox); this.placeBlockAtCurrentPosition(world, 0, 0, 0, 2, 2, structureboundingbox); this.placeBlockAtCurrentPosition(world, 0, 0, 0, 3, 2, structureboundingbox); return true; } }