用户:MashKJo/1.12.2模组开发笔记/实现新维度

要实现一个新维度,在代码层面,通常分为四个步骤:

  • 注册该维度
  • 写出该维度的WorldProvider
  • 写出该维度的BiomeProvider
  • 写出该维度的ChunkGenerator

注册维度

要注册一个维度,需要一个类型是“维度类型”(net.minecraft.world.DimensionType)的对象,DimensionType类是一个枚举类,这个枚举类默认有三个对象:OVERWORLD、NETHER和THE_END。

public class MyDim
{
    public static int dimID;
    public static DimensionType dimType;
    
    public void registerDimension()
    {
        dimType = DimensionType.register(dimName, dimSuffix, dimID, MyDimWorldProvider.class, isKeepLoaded);
        DimensionManager.registerDimension(dimID, dimType);
    }
}

要构造一个DimensionType对象,需要如下几个参数:

  • dimName是一个字符串,一般由小写字母和下划线组成,其意义显而易见。
  • dimSuffix可能会让人有点摸不着头脑——“维度后缀”?实际上,它是用来指定存档在保存维度的村庄等建筑的数据时,使用的文件后缀名。它同样是一个字符串。
  • dimID——这个参数别说模组开发者了,即使是会自己排查模组冲突的普通玩家也对它有一定的了解——毕竟不同模组添加的维度的ID如果发生冲突,肯定是会崩游戏的。这个参数当然是一个整型量,一般就是int类型。笔者这里建议不要把维度ID硬编码,而应该设定为从模组的配置文件里读取。
  • MyDimWorldProvider.class用于指定该维度的WorldProvider类。
  • isKeepLoaded是一个布尔值,意思是“是否持续加载该维度”,出于性能上的考量,一般都定为false。

最后,调用DimensionManager类的静态方法static void registerDimension(int id, DimensionType type),以实现新维度的注册。

看到这里,读者可能已经发现不对劲的地方了:这里的设计有问题吧?要知道如果不借助一些黑科技的话,枚举类是无法在除了它内部以外的地方产生新对象的。但我们的目标不是要添加新维度吗?那自然也需要一个全新的DimensionType才对啊。

答案是:不是“这里的设计有问题”,而是DimensionType是一个原版类,MC原版压根就没有对这块进行设计,因为MC的底层逻辑是不包括“为模组考虑”,即“提供很好的扩展性”的。不过这也无可厚非——因为维度这东西和物品、方块不同,MC从1.0版本到1.12版本,维度只有三个,站在原版的角度看,也确实没必要为新维度的添加专门造轮子。

那么读者又会问了:既然原版的DimensionType类可扩展性几乎为0,那为什么我们在添加新维度的时候还要用到这个类呢?因为Forge出手了。经过Forge的一番patch后,这个类获得了一个名为register的静态方法:

public static DimensionType register(String name, String suffix, int id, Class<? extends WorldProvider> provider, boolean keepLoaded)
{
    String enum_name = name.replace(" ", "_").toLowerCase();
    DimensionType ret = net.minecraftforge.common.util.EnumHelper.addEnum(DimensionType.class, enum_name, ENUM_ARGS,id, name, suffix, provider);
    return ret.setLoadSpawn(keepLoaded);
}

这个方法调用了Forge添加的一个类——EnumHelper,来实现在某个枚举类内部以外的地方添加该枚举类的对象。如果细看addEnum这个方法的话,你会发现这个方法用到了Java的反射机制——果然是黑科技。

DimensionManager是一个完完全全的Forge类,且该类中,实现了对原版三个维度的注册——也就是说,这个类实际上不能体现原版中维度的注册机制——甚至退一万步说,原版中到底有没有“维度的注册”这个概念都不好说。

注意:不要将维度类型和世界类型搞混淆。

WorldProvider

要实现一个新维度,必须要为该维度提供对应的WorldProvider。所有维度的WorldProvider类都是抽象类net.minecraft.world.WorldProvider的子类。

原版提供了三种WorldProvider:WorldProviderSurface、WorldProviderHell以及WorldProviderEnd。如果你只是想做个换皮维度的话,倒是可以考虑继承这三种WorldProvider的其中之一[1]

当然,我们最好还是自己写一个WorldProvider。首先新建一个类,让其继承WorldProvider类。

public class MyDimWorldProvider extends WorldProvider
{
    @Override
    public DimensionType getDimensionType()
    {
        return MyDim.dimType;
    }
    
    @Override
    public IChunkGenerator createChunkGenerator()
    {
        return new MyDimChunkGenerator(this.world);
    }
}

首先,WorldProvider虽然为一个抽象类,但它其实只有一个抽象方法待子类实现:DimensionType getDimensionType()。这里我们只需要返回我们之前新建的DimensionType对象即可。

IChunkGenerator createChunkGenerator()这个方法虽然在WorldProvider类中不是抽象方法,但它也必须被覆写!因为它提供的是该维度的区块生成类——它决定了这个维度的世界生成。如果你不覆写该方法,那么等你进入维度后,你会发现这个维度的地面在Y = 255处,且天空的光照分布十分不正常,TPS会非常非常低。

那么可能这个时候就会有人问了:我看过了主世界的WorldProvider,没看到它里面有指定主世界的ChunkGenerator啊?原因是:主世界有两个不同的ChunkGenerator:ChunkGeneratorOverworld和ChunkGeneratorFlat,后者用于生成超平坦主世界,前者用于生成正常的主世界。到底选择哪个ChunkGenerator生成主世界,取决于玩家在创建世界时所选的世界类型(WorldType),这些逻辑是被写在了其他地方的。由此也可以看出,虽然在很多时候,MC中一个World实例就可以近似代表一个维度,但WorldType和DimensionType是不一样的。

下界和末地的WorldProvider倒是直接给出了对应的ChunkGenerator,可见,这两个维度的世界生成并不受WorldType影响。

单独为你的新维度创建新的WorldType是完全没有必要的,因为一来,新的WorldProvider不刚需新的WorldType,你完全可以直接在你的WorldProvider类里指定ChunkGenerator;二来,WorldType中所能自定义的其他数据也不太常用,比如:云的高度、Y坐标等于多少时虚空迷雾最浓……

如果你真想给自己的新维度创建一个全新的WorldType,也不是很难,只需覆写若干非常简单的方法即可,这里就不展开论述了。

回到正题上来:在你自己的WorldProvider类中,你也可以自定义很多数据,如“该维度是否为露天世界”“该维度的虚空迷雾的颜色”等等,只需覆写若干方法即可。但要注意:你在你的WorldProvider类中提供的这些数据仅仅是给MC原版的一些机制或者其他模组传递了一些信息而已,它并不会体现在具体的世界生成中。比如笔者把主世界的“是否为露天世界”从true改为false,难道主世界顶部就会自动生成遮天蔽日的方块?MC可没这么智能。实际上这些数据仅仅是你手动暴露给MC原版的一些机制和其他模组的、关于你的新维度的一些信息而已。比如小地图模组可能会根据这一参数来决定生成的小地图的每一个像素点到底要不要取该像素点对应的(x, z)坐标的最高非空气方块的颜色。如果你填的这一参数和实际的世界生成情况不符,那小地图就会出错,就这么简单。新维度的绝大部分的世界生成特性都只体现在你的ChunkGenerator类中。

另外,WorldProvider类还有一个方法值得覆写:String getSaveFolder()。这用于指定存档文件夹中用于保存该新维度数据的文件夹名称。如果你不覆写该方法,则存储新维度数据的文件夹名默认为"DIM" + dimID",那么如果有玩家游玩了你的模组,进入过你的模组添加的新维度,后续TA又加了一些添加了新维度的模组,你的模组的维度的ID和某个另外的维度冲突了,TA修改了你的模组的维度的ID,那就糟了——由于维度数据存储文件夹变更,你的维度将会重新生成。因此最好覆写该方法。

另外,“新维度应该有哪些生物群系”也是在WorldProvider中规定的。你的新维度的WorldProvider类应该从WorldProvider类继承了一个类型为net.minecraft.world.biome.BiomeProvider的变量:biomeProvider。有关该变量的处理会在下一节讲到。

BiomeProvider

ChunkGenerator

注释与外部链接

  1. 事实上,MCreator就是这么干的——用MCreator创建新维度的时候,你只有这三种WorldProvider可以选。因为自定义世界生成牵扯到复杂的算法,这东西是没法给出一个通用的程式的。