初心者modderの備忘録

マイクラのmodを作りたくて、初めて見たのですが難しくて忘れそうなので自分用の備忘録も兼ねてブログにしようと思います

7日目 nbtデータを使った構造物の追加

今回は、nbtデータを使って構造物を追加します。
基本的なコードは通常の構造物の追加とあまり変わりません。

以前の記事参照
2日目 構造物の生成 - 初心者modderの備忘録

二つに分けて書いていきます。

①構造物の保存
Javaの記述

①構造物の保存
まず、構造物を保存します。
IDEのrunClienetの実行などからマイクラを開いてクリエイティブモードで世界を生成します (バージョンが合っていれば通常のラウンチャーからの起動でも大丈夫です)。
余計なものが入るのを避けたい場合はスーパーフラットワールドが良いでしょう。

生成したら、ストラクチャーブロックをコマンドで入手します。
/give @p structure_block

f:id:json_fileman:20200423233425p:plain




次に、保存したい構造物を建築します。
今回は、レンガブロックの上に適当に木を生やしたものを作成しました。

f:id:json_fileman:20200423233542p:plain

ストラクチャーブロックの範囲に収まる構造物にする必要があります。
もし、より大きな構造物を追加したい場合は、少し複雑ですが後述の方法で追加できます。

建築できたら、構造物を保存します。
2つのストラクチャーブロックを構造物が収まる直方体の空間の対角となる頂点にそれぞれ設置します。

f:id:json_fileman:20200423234113p:plain

設置したストラクチャーブロックを右クリックで開き、左下のDataと表示されているボタンを3回クリックしてCornerモードにします。
上部の空欄に任意を名前を入力してDoneをクリックします。

f:id:json_fileman:20200423234508p:plain


同様に、対角においたもう一つのストラクチャーブロックに対しても、Cornerモードで名前をつけます。
このとき反対側においたストラクチャーブロックと同じ名前をつけます。



続いて、新たにストラクチャーブロックを設置し、開いてDataを1回クリックしてSaveモードにします。
上部の空欄に先程つけた名前を正確に入力し、DETECTをクリックします。

f:id:json_fileman:20200423234814p:plain


ちゃんと検知されると下図のようにストラクチャーブロックで囲んだ範囲が枠線で表示されます。

f:id:json_fileman:20200423234953p:plain

この状態で、もう一度3個目においたストラクチャーブロックをクリックし、大文字のSAVEをクリックします。


f:id:json_fileman:20200423235456p:plain


うまくセーブされていれば、
\forge-1.15.2-31.1.0-mdk\run\saves\New World\generated\minecraft\structures
に名前をつけた構造物のnbtファイルが保存されています (Forgeで起動した場合。World名はそのとき使ったWorld名)。


f:id:json_fileman:20200424000659p:plain




Javaの記述
構造物が保存出来たら続いて、Javaでコードを書いていきます。

public class TestStructure extends Feature<NoFeatureConfig>
{
	private final PlacementSettings SETTING = new PlacementSettings().setMirror(Mirror.NONE).setRotation(Rotation.NONE).setIgnoreEntities(true);

	public TestStructure(Function<Dynamic<?>, ? extends NoFeatureConfig> configFactoryIn)
	{
		super(configFactoryIn);
	}

	@Override
	public boolean place(IWorld worldIn, ChunkGenerator<? extends GenerationSettings> generator, Random rand, BlockPos pos, NoFeatureConfig config)
	{
		if(rand.nextInt(1000)!=0)
		{
			return false;
		}else
		{
			TemplateManager manager = worldIn.getWorld().getServer().getWorld(DimensionType.OVERWORLD).getStructureTemplateManager();
			Template template = manager.getTemplate(new ResourceLocation(TestMod.MODID, "test"));

			template.addBlocksToWorld(worldIn, pos, SETTING);

			return true;
		}
	}
}

Featureクラスを継承した、TestStructureクラスを作成しました。
placeメソッド内で引数のIWorldから辿って、TemplateManagerを取得します。
取得したTemplateManagerを使ってTemplateのファイル場所を設定して渡します。
先程保存したnbtファイルをコピーして、
resources/data/MODID/structures
に保存します。

その後、TemplateのaddBlocksToWorldメソッドを使って生成させます。
第三引数は設置の設定で、3行目で宣言しています。
設置するときに回転させるか、反転させるかなどを設定したものを渡します。


続いて、構造物の登録を行ないます。

@Mod.EventBusSubscriber(bus=Mod.EventBusSubscriber.Bus.MOD)
public class ModStructures
{
	public static final Feature<NoFeatureConfig> TEST_STRUCTURE = new TestStructure(NoFeatureConfig::deserialize);

	@SubscribeEvent
	public static void registerStructures(final RegistryEvent.Register<Feature<?>> event)
	{
		TEST_STRUCTURE.setRegistryName("structure_test");
		event.getRegistry().registerAll(TEST_STRUCTURE);

		addStructure();
	}

	private static void addStructure()
	{
		ForgeRegistries.BIOMES.forEach(biome ->
		{
			biome.addFeature(Decoration.SURFACE_STRUCTURES,
					TEST_STRUCTURE
					.func_225566_b_(IFeatureConfig.NO_FEATURE_CONFIG)
					.func_227228_a_(TestFrequency.TEST_FREQ
					.func_227446_a_(new NoPlacementConfig())));
		});
	}
}


この辺りは、アイテムやブロックの登録と同様に、@SubscribeEventをつけたメソッド内で登録を行ないます。
今回は、登録用のクラスとして ModStructuresというクラスを作成しました。

また構造物は、登録しただけでは自然に生成されないので各バイオームにaddFeatureメソッドを用いて自然生成を登録します。
今回は登録している構造物が1つですが、構造物が多くなるとコードが長くなるので、addStructureメソッド内で処理を記述し、それを@SubscribeEventn内で呼び出すようにしています。

addFeatureの中の.func_227228_a_にはPlacementを渡します。PlacementではFeatureをどのように生成させるかが記述されています。バニラのConfiguredPlacementを使いたい時は、Placementクラスで宣言されているものを呼び出して使うことも出来ます (上記の過去記事参照)。

今回はテスト用で、シンプルな設置がしたかったので自作のPlacementを作成しました。

public class TestFrequency extends Placement<NoPlacementConfig>
{
	public static final Placement<NoPlacementConfig> TEST_FREQ = new TestFrequency(NoPlacementConfig::deserialize);

	public TestFrequency(Function<Dynamic<?>, ? extends NoPlacementConfig> configFactoryIn)
	{
		super(configFactoryIn);
	}

	@Override
	public Stream<BlockPos> getPositions(IWorld worldIn, ChunkGenerator<? extends GenerationSettings> generatorIn,
			Random random, NoPlacementConfig configIn, BlockPos pos)
	{
		int x = pos.getX();
		int z = pos.getZ();
		int y = worldIn.getHeight(Heightmap.Type.WORLD_SURFACE, x, z);

		return Stream.of(new BlockPos(x, y, z));
	}
}

Placementは登録しないまま使うので、自分のクラスの中で宣言を行ないました。Placementsクラス等を作成して、そこでまとめて宣言してもいいと思います。



ここまで行なって起動すると、


f:id:json_fileman:20200423233720p:plain


図のように、登録した構造物が地表に生成されています。


②大きな構造物の登録

バニラの方法とは違うので、あまり良い方法ではないかもしれませんが一応一つの方法として書いておきます。

例として、6チャンク×6チャンクの大きさの構造物を生成するとします。


f:id:json_fileman:20200424005116p:plain



昔のゲームのマップチップのような考え方で登録します。
なので、まず1つのチップの大きさを何チャンク×何チャンクにするかを決めます。
例では2×2チャンクを1チップとしています。


f:id:json_fileman:20200424005504p:plain


さらに1つの構造物が入る範囲をregionとして、
1regionが何チップ×何チップかを決めます。
例では3×3チップを1regionとしました。



f:id:json_fileman:20200424005859p:plain




そして、worldのseed値、regionのX座標、Z座標を使うことで、1region毎に、固有の乱数を与えて、生成判定やマップチップの配置を行ないます。
こうすることで、1reagionがチャンクの読み込み範囲超えるような大きな構造物であってもクラッシュせず、次にチャンクが読み込まれるタイミングで生成されます。


かなり雑ですが、実際の実施例とコードを載せておきます。
実例では2チャンク×5チップの10チャンク四方からなる構造物を作成しています。
チップ毎につながりを持たせれば、ネザー要塞や森の洋館みたいな構造物も作れるのではないかと考えています。


f:id:json_fileman:20200424010755p:plain


今回はこれで終わります。




ExampleStructure.class

public class ExampleStructure extends Feature<NoFeatureConfig>
{

	private static final int sizeX = 5;
	private static final int sizeZ = 5;

	public static final int chipX = 2;
	public static final int chipZ = 2;

	private static final int region_sizeX = sizeX * chipX;
	private static final int region_sizeZ = sizeZ * chipZ;

	private boolean isLoaded = false;
	private TemplateManager manager;


	private final PlacementSettings SETTING = new PlacementSettings().setMirror(Mirror.NONE).setRotation(Rotation.NONE).setIgnoreEntities(true);

	public ExampleStructure(Function<Dynamic<?>, ? extends NoFeatureConfig> configFactoryIn)
	{
		super(configFactoryIn);
	}

	@Override
	public boolean place(IWorld worldIn, ChunkGenerator<? extends GenerationSettings> generator, Random rand, BlockPos pos, NoFeatureConfig config)
	{
		System.out.println("regionX ="+ pos.getX() / (sizeX*16) + "posX is" + pos.getX());
		System.out.println("regionZ ="+ pos.getZ() / (sizeZ*16) + "posZ is" + pos.getZ());
		System.out.println("isPlace ="+ isPlace(worldIn.getSeed(), pos.getX(), pos.getZ(), rand));

		if(!isLoaded)
		{
			manager = worldIn.getWorld().getServer().getWorld(DimensionType.OVERWORLD).getStructureTemplateManager();

			Arrays.asList(mapChip.values())
			.forEach(map -> map.setTemplate(manager));

			isLoaded = true;
		}


		if(!isPlace(worldIn.getSeed(), pos.getX(), pos.getZ(), rand))
			return false;
		else
		{
			generate(worldIn, rand, pos, manager);
			return true;
		}
	}

	private boolean isPlace(long seed, int posX, int posZ, Random rand)
	{
		int regionX = posX / (region_sizeX*16);
		int regionZ = posZ / (region_sizeZ*16);

		if(regionX == 0 && regionZ == 0)
		{
			return false;
		}else if(regionX == 0 && regionZ == 1)
		{
			rand.setSeed(seed+regionZ);
		}else if(regionX == 0)
		{
			rand.setSeed(seed*regionZ);
		}else if(regionZ == 0)
		{
			rand.setSeed(seed*regionX);
		}else
		{
			rand.setSeed(seed*regionX);
			rand.setSeed(rand.nextLong()*regionZ);
		}

		return rand.nextInt(5) == 0;
	}

	private void generate(IWorld world, Random rand, BlockPos pos, TemplateManager manager)
	{
		int x = pos.getX() >= 0? (pos.getX() / 16) % region_sizeX : (pos.getX() / 16) % region_sizeX + region_sizeX - 1;
		int z = pos.getZ() >= 0? (pos.getZ() / 16) % region_sizeZ : (pos.getZ() / 16) % region_sizeZ + region_sizeZ - 1;

		long seed = world.getSeed();

		int regionX = pos.getX() / (region_sizeX*16);
		int regionZ = pos.getZ() / (region_sizeZ*16);

		if(regionX == 0 && regionZ == 0)
		{

		}else if(regionX == 0 && regionZ == 1)
		{
			rand.setSeed(seed+regionZ);
		}else if(regionX == 0)
		{
			rand.setSeed(seed*regionZ);
		}else if(regionZ == 0)
		{
			rand.setSeed(seed*regionX);
		}else
		{
			rand.setSeed(seed*regionX);
			rand.setSeed(rand.nextLong()*regionZ);
		}

		mapChip map[][] = new mapChip[sizeX][sizeZ];

		for(int i=0; i < sizeX; i++)
		{
			for(int j=0; j < sizeZ; j++)
			{
				map[i][j] = setMap(i, j, rand, map);
			}
		}

		map[x/chipX][z/chipZ].template[x%chipX][z%chipZ].addBlocksToWorld(world, pos, SETTING);

		System.out.println(map[x/chipX][z/chipZ].toString());


		System.out.println(x/chipX);

	}

	private mapChip setMap(int i, int j, Random rand, mapChip[][] map)
	{
		mapChip chip;
		int Xmax = sizeX -1;
		int Zmax = sizeZ -1;

		if((i == 0 && j == 0) || (i == Xmax && j == 0) || (i == 0 && j == Zmax) || (i == Xmax && j == Zmax))
		{
			chip = mapChip.MAP_A;
		}else if(j == 0 || j == Zmax)
		{
			chip = rand.nextBoolean()? mapChip.MAP_B : mapChip.MAP_C;
		}else if(i == 0 || i == Xmax)
		{
			chip = rand.nextBoolean()? mapChip.MAP_D : mapChip.MAP_E;
		}else
		{
			if(map[i][j-1].canJointVir && map[i-1][j].canJointHor)
			{
				chip = mapChip.MAP_F;
			}else
			{
				chip = rand.nextBoolean()? mapChip.MAP_G : mapChip.MAP_H;
			}
		}

		return chip;

	}

	private enum mapChip
	{
		MAP_A(true, true, "map_a"),
		MAP_B(false,true, "map_b"),
		MAP_C(true, true, "map_c"),
		MAP_D(true, false, "map_d"),
		MAP_E(true, true, "map_e"),
		MAP_F(true, true, "map_f"),
		MAP_G(true, true, "map_g"),
		MAP_H(true, false, "map_h");

		private final boolean canJointVir;
		private  final boolean canJointHor;
		private final String name;
		private Template[][] template = new Template[chipX][chipZ];

		private mapChip(boolean Virtical, boolean Horazontal, String name)
		{
			this.canJointVir = Virtical;
			this.canJointHor = Horazontal;
			this.name = name;
		}

		private void setTemplate(TemplateManager manager)
		{

			for(int i=0; i < chipX; i++)
			{
				for(int j=0; j < chipZ; j++)
				{
					this.template[i][j] = manager.getTemplate(new ResourceLocation(TestMod.MODID, name+code(i,j)));
				}
			}
		}

		private String code(int i, int j)
		{
			if(i == 0 && j == 0)
			{
				return "a";
			}else if(i == 1 && j == 0)
			{
				return "b";
			}else if(i == 0 && j == 1)
			{
				return "c";
			}else
			{
				return "d";
			}
		}
	}

}