十五、世界生成器
如果你仔细观察,会发现有个叫saves
的文件夹,这个文件夹是用来存放存档的,即我们平常说的世界。
显然,服务器承担了创建世界、加载世界的任务。Bukkit 中也有专门生成世界的生成器,所以我们可以继承这些类写一个世界生成器。
世界生成器有很多种,但在这之前,我们需要了解一下生成的原理。
0.原理
Minecraft 地形生成是分为两部分:Generation
和Population
。Generation
部分负责生成最基本的地形,Population
部分是负责在这个地形上加装饰(花、草、树等等)。
区块是世界中长和宽为
16
16
16,高为
256
256
256 的部分。两个部分都是以区块作为最基本的单位,Generation
再使用柏林噪声(Perlin noise)算法生成基础地形。
柏林噪声算法有很多版本,从 1980 年始的原始算法,到如今的改进算法,各种各样呢。柏林噪声实质上是一个函数。省流就是,传入之间相差不大的参数,最后返回一个随机数,
public double perlin(double x,double y,double z);
如上,传入三个参数,最后返回一个 [ 0 , 1 ] [0,1] [0,1] 区间的一个浮点数,即柏林噪声值。传入相同的参数,最后得到的柏林噪声值也相同,这也就是为什么 Minecraft 中相同的种子总是生成相同的地形。
除此之外还有分形噪声,分形噪声将不同频率的噪声函数合并为一个更为复杂的噪声函数。
但这个函数图像是粗略的,我们可以将某个噪声值进行放大再生成,得到的地形就精细的得多了。
柏林噪声算法不是这一章的重要部分,如果你有兴趣,可以自行阅读其他文章。
1.装饰
在世界开始加载时会触发一个WorldInit
事件,我们可以监听这一事件,然后来完成一次“点缀”。
public class DiamondGenerator implements Listener {
@EventHandler
public void onWorldInit(WorldInitEvent e) {
e.getWorld().getPopulators().add(new DiamondPopulator());
}
}
前面说过,Population
就是给原本的地形上加点装饰,当然加的装饰不止一种,我们可以自己添加一种,重点在于实现DiamondPopulator
类。
public class DiamondPopulator extends BlockPopulator {
@Override
public void populate(World world, Random random, Chunk source) {
}
}
首先判断我们需要添加什么,比如随处可见的钻石块,假设我们一个区块需要5个钻石块,对于每一个钻石块而言,只需确定它的横纵坐标,遍历一遍所有 y y y 轴坐标判断是否符合我们要的条件即可。
int count = 5;
for(int i = 0; i <= 5; i++) {
//随机确定x,z坐标
int x = random.nextInt(16);
int z = random.nextInt(16);
//遍历我们所要的y坐标
for(int y = 128; y >= 0; y--) {
//我们的钻石块要在天空上,即空气方块上
if(source.getBlock(x, y, z).getType() == Material.AIR) {
world.getBlockAt(x, y, z).setType(Material.DIAMOND_BLOCK);
}
}
}
运用上面的例子你就可以加一些小型建筑来丰富你的插件。
只要你能够理解上面的Populator
就差不多行了,但差不多达不到完美。实际我们一般很少很少会用到世界生成器,而且生成器较为复杂,需要一些基础。
2.地形
2.1.简单区块地形生成
前面说过,Generation
以区块作为基本单位生成,所以我们可以用ChunkGenerator
来生成一个最最简单的地形,比如说钻石大陆。
在你的主类中重写:
@Override
public ChunkGenerator getDefaultWorldGenerator(String worldName, String id) {
return new DiamondGenerator();
}
老规矩,重在实现DiamondGenerator
:
public class DiamondGenerator extends ChunkGenerator {
@Override
public ChunkData generateChunkData(World world, Random random, int x, int z, BiomeGrid biome) {
ChunkData chunkData = createChunkData(world);
chunkData.setRegion(0, 0, 0, 16, 1, 16, Material.BEDROCK);
chunkData.setRegion(0, 1, 0, 16, 16, 16, Material.DIAMOND_BLOCK);
return chunkData;
}
}
setRegion
方法是用于填充方块,传入两个三维坐标,然后对选中的部分进行填充。
如上,对于每个区块,我们只需将它填充为钻石块就行了。对于每个区块的最底层,我们将它填充为基岩就好了。
我们只是用一个小区块,然后用它拼合成一个大世界。这是最简单的地形生成。
2.2.噪声地形生成
接着就来到了本章的重头戏,对于噪声算法,我们提到了柏林(Perlin)噪声,但还有一个 2.0 版本——Simplex,是对 Perlin算法进行的优化。
private SimplexOctaveGenerator simplex;
@Override
public ChunkData generateChunkData(World world, Random random, int x, int z, BiomeGrid biome) {
ChunkData chunkData = createChunkData(world);
if(simplex == null) {
//传入种子
simplex = new SimplexOctaveGenerator(world.getSeed(), 1);
simplex.setScale(0.001D);
}
return chunkData;
}
SimplexOctaveGenerator
就是我们说的分形噪声。同时也可以用setScale
调整噪声函数的频率,可以调的小一些,地形更平坦,或者调大一些,地形更陡峭,甚至完全混乱。
这次我们不需要随机取一个方块的横纵坐标了,我们只需改变一个方块的高度既可,因为最终形成的地形是跌宕起伏的,不可能有个大窟窿吧。
for(int i = 0; i < 16; i++) {
for(int j = 0; j < 16; j++) {
//设置x,z坐标
int xx = x * 16 + i;
int zz = z * 16 + j;
//获取噪声值
double noise = simplex.noise(xx, zz, 0.3D, 0.4D);
//将噪声值放大
int y = (int) (noise * 35D + 150D);
//底层基岩
chunkData.setBlock(x, 0, z, Material.BEDROCK);
//1~y层全为钻石块
for(int k = 1; k <= y; k++) {
chunkData.setBlock(x, y, z, Material.DIAMOND_BLOCK);
}
}
}
以上就是Generation
部分,至于Populartion
部分,我们可以用getDefaultPopulators
方法:
@Override
public List<BlockPopulator> getDefaultPopulators(World world) {
return ImmutableList.of(....);//你的装饰生成器
}
3.结束了?
编者一开始想重点讲噪声算法,但是我觉得由于低龄化太过严重,噪声算法过于深奥,不利理解,并且现在随便搜搜都一大堆,也就偷个懒了。原定分为上下稿,现在硬是压缩成一稿,属实不易。
基本上所有要讲的都讲完了,并且还额外扩充了一些。
当然现在不会草草了结这个系列的,应粉丝要求,还有实战呢(恼)。
上一篇:我的世界Bukkit服务器插件开发教程(十四)消息和命令补全器
下一篇:我的世界Bukkit服务器插件开发教程(实战一)起床战争