初心者modderの備忘録

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

11日目 WorldGeneratorの利用

前回から少し間が開きましたたが、今回は追加したブロック等を自然生成させるためのWorldGeneratorの使い方を調べて見ました。

基本的には
親クラスWorldGeneratorを継承したクラスと
インターフェースIWorldGeneratorを実装したクラスの
合計2つのクラスを作ります。

まずこの時点でややこしかったのに加えて、サンプルコードとかを見るとforとifがいっぱい書いてあってさらに難しくてちょっと手を出さないでいたのですが、実際にコードを書いてみると思ったよりは難しくなかったので、忘れないうちにここに記録しておこうと思います。

まず、極めて単純なコードを書いていきます。
WorldGeneratorの方から。

WorldGenSample.java

package test6mod;

import java.util.Random;

import net.minecraft.init.Blocks;
import net.minecraft.util.math.BlockPos;
import net.minecraft.world.World;
import net.minecraft.world.gen.feature.WorldGenerator;

public class WorldGenSample extends WorldGenerator {

	@Override
	public boolean generate(World world, Random rand, BlockPos pos)
	{
			world.setBlockState(pos, Blocks.DIAMOND_BLOCK.getDefaultState());
		return true;
	}

}

ダイヤモンドブロックを与えられた座標に1つ置きます。


続いてIWorldGeneratorの方を。

IWorldGenSample.java

package test6mod;

import java.util.Random;

import net.minecraft.util.math.BlockPos;
import net.minecraft.world.World;
import net.minecraft.world.chunk.IChunkProvider;
import net.minecraft.world.gen.IChunkGenerator;
import net.minecraft.world.gen.feature.WorldGenerator;
import net.minecraftforge.fml.common.IWorldGenerator;

public class IWorldGenSample implements IWorldGenerator {

	public WorldGenerator gen = new WorldGenSample();

	@Override
	public void generate(Random random, int chunkX, int chunkZ, World world, IChunkGenerator chunkGenerator,IChunkProvider chunkProvider) 
	{
		switch (world.provider.getDimension()) 
		{
		case -1:
			break;

		case 0:
			runGenerator(gen, world, random, chunkX, chunkZ);
			break;

		case 1:

		}
	}

	private void runGenerator(WorldGenerator gen, World world, Random rand, int chunkX, int chunkZ) 
	{
		int x = chunkX * 16 + 8;
		int y = 80;
		int z = chunkZ * 16 + 8;
	
		gen.generate(world, rand, new BlockPos(x, y, z));
	}
}


こっちの方では、まず後で"WorldGenSampleのgenerate()"を使うために、インスタンスを生成します。
続いてオーバーライドした"IWorldGenSampleのgenerate()"の中身ですが、ディメンションによって場合分けをしておきます。
それぞれ-1がネザー、0がオーバーワールド、1がエンドですね。自作のディメンションも使う場合にはここに自作のディメンションIDも追加します。

今回はオーバーワールドにのみ作用させるので、case 0にのみrunGenerator()を記述しました。


次にrunGeneratorの中身でWorldGenSampleのgenerate()をどこで使うかを書いておきます。
IWorldGeneratorでは与えられるのが座標ではなくチャンクなので、16を掛けて座標に変換してからgenerateに座標を渡します。
また+8の意味は、生成するチャンクの端っこではなく中央からgenerateを起こしてほしいためです。ちなみにこれがないと、よほど軽い処理の場合を除いて後述するカスケードという現象が頻発します。


最後に、これをメインのクラスや登録用のクラスで登録します。

Main.java

package test6mod;

import net.minecraftforge.fml.common.Mod;
import net.minecraftforge.fml.common.event.FMLInitializationEvent;
import net.minecraftforge.fml.common.event.FMLPostInitializationEvent;
import net.minecraftforge.fml.common.event.FMLPreInitializationEvent;
import net.minecraftforge.fml.common.registry.GameRegistry;

@Mod(modid = Main.modId, name = Main.name, version = Main.version)
public class Main {

	public static final String modId = "testmod";
	public static final String name = "testmod";
	public static final String version = "1.0.0";

	@Mod.Instance(modId)
	public static Main instance;


	@Mod.EventHandler
	public void preInit(FMLPreInitializationEvent event)
	{
		GameRegistry.registerWorldGenerator(new IWorldGenSample(), 0);

    	if(event.getSide().isClient()) {
    		
    	}
	}


	@Mod.EventHandler
	public void init(FMLInitializationEvent event) {

	}

	@Mod.EventHandler
	public void postInit(FMLPostInitializationEvent event) {

	}
}

これで起動してみます。

f:id:json_fileman:20190119230634p:plain

乱数をまったく使ってないので、y=80のところに規則正しくダイヤモンドブロックが生成されました。
実際この程度単純であれば、WorldGeneratorクラスは使わずともIWorldGeneratorだけで記述できますが、もし複雑なものを作る場合は分けておいた方がわかりやすいかもしれないですね。
また、WorldGeneratorは世界の生成時以外でも、たとえば成長による木の生成にも使えるので使い回せるといった利点もあるかもしれません。

すこしだけ、生成条件と生成するものを変えてみます。

WorldGenSample.java

public class WorldGenSample extends WorldGenerator {

	@Override
	public boolean generate(World world, Random rand, BlockPos pos)
	{
		for(int i=-4; i<=4;i++)
		{
			for(int j=-4; j<=4; j++)
			{
				if(i%2==0 && j%2==0)
					world.setBlockState(pos.add(i, 0, j), Blocks.DIAMOND_BLOCK.getDefaultState());
				else
					world.setBlockState(pos.add(i, 0, j), Blocks.GOLD_BLOCK.getDefaultState());
			}
		}
			
		return true;
	}

}


IWorldGenSample.java

public class IWorldGenSample implements IWorldGenerator {

//省略

	private void runGenerator(WorldGenerator gen, World world, Random rand, int chunkX, int chunkZ) 
	{
		int x = chunkX * 16 +8+rand.nextInt(16);
		int z = chunkZ * 16 +8+rand.nextInt(16);
		
		int y = world.getHeight();
		boolean foundGround = false;
		
		while(!foundGround && y-- >= 0)
		{
			Block block = world.getBlockState(new BlockPos(x,y,z)).getBlock();
			foundGround = block == world.getBiome(new BlockPos(x, y, z)).topBlock.getBlock();
		}
		gen.generate(world, rand, new BlockPos(x, y+1, z));
	}
}

地表を見つけて、そこに金とダイヤでできた変なもの生成します。
実行した結果です。

f:id:json_fileman:20190119235945p:plain


ちなみに、このときのコンソールがこちらなんですが、

f:id:json_fileman:20190120000041p:plain


Preparing spawn area: XX%
っていうのが連続して表示されていますね。
これが普通の状態です。



一方、チャンク×16+8の+8をなくした結果がこちらです。

f:id:json_fileman:20190120000408p:plain

Preparing spawn area: XX%同士の間に
causing cascading worldgen lag
っていうエラーメッセージが表示されています。

これでも一応起動されるのですが、場合によってはかなり起動が遅くなります。
この現象は、現在生成中の構造物がまだ生成されていないチャンクに生成されようとしている場合に発生します。
ここの+8がないと、チャンクの隅っこから生成を始めることになるので未生成チャンクに影響を及ぼす可能性が大きくなり、発生しやすくなります。
なので、特にこだわる理由がない場合は+8しておくのが無難な気がします。

ただ、検証はしていませんがそもそもWorldGeneratorで16×16を大きく超えるような生成物を作ろうとすると避けられないかもしれないですね。

今回はこの辺で終わります。

次回は次元の追加でもやろうと思います。


Main.java
WorldGenSample.java
IWorldGenSample.java