用户:MashKJo/1.12.2模组开发教程/2.基础概念

MashKJo留言 | 贡献2024年12月3日 (二) 15:33的版本 (创建页面,内容为“这一节,会讲述一些非常重要的前置概念。 == ResourceLocation == 这是一个位于<code>net.minecraft.util</code>包下的一个类。这个类的用途的其中之一,顾名思义,就是用来表示文件在assets文件夹中的路径。在开发环境中,assets文件夹在<code>src/main/resources</code>路径下;而在打包好的.jar文件的中,assets文件夹点进去就能看到。 ResourceLocation总共有3个构造器,我…”)
(差异) ←上一版本 | 最后版本 (差异) | 下一版本→ (差异)

这一节,会讲述一些非常重要的前置概念。

ResourceLocation

这是一个位于net.minecraft.util包下的一个类。这个类的用途的其中之一,顾名思义,就是用来表示文件在assets文件夹中的路径。在开发环境中,assets文件夹在src/main/resources路径下;而在打包好的.jar文件的中,assets文件夹点进去就能看到。

ResourceLocation总共有3个构造器,我们用到的有2个:public ResourceLocation(String resourceName)public ResourceLocation(String resourceDomain, String resourcePath)。通常推荐使用后者。

resourceDomain是什么意思?举个例子,"minecraft"就是一个resourceDomain,"buildcraft"同样也是。所以明白了吗?resourceDomain实质上就是某个模组的modid。我们当然一般是要用自己的Mod的modid。

resourcePath又是什么意思呢?再举个例子,假如我这样new了一个ResourceLocation:

ResourceLocation location = new ResourceLocation("minecraft", "textures/items/apple.png");
Minecraft.getMinecraft().getTextureManager().bindTexture(location);

Minecraft就会尝试去原版和各个模组的.jar包中找路径为assets/minecraft/textures/items/apple.png的文件,代码中的TextureManager#bindTexture的作用是把图片绑定到当前渲染器,那么,苹果的材质文件就会被绑定。当然,这段代码看不懂没关系。笔者只是想说明ResourceLocation对象的使用机制:一般情况下,它实际上能指代一个位于某个.jar包中路径为"assets/" + resourceDomain + "/" + resourcePath的文件。

但是也不是所有时候都是如此,比如在设定物品的模型文件的时候,resourcePath就不需要打那么多了,只需要new ResourceLocation(modid, itemid)就可以了,比如new ResourceLocation("minecraft", "apple")。所以,笔者的建议是,如果是你自定义的渲染操作,你按照“路径为"assets/" + resourceDomain + "/" + resourcePath”的规则,是没问题的;但是如果你要用Minecraft或Forge内置的某些方法,其中要传入一个ResourceLocation对象,那你就要小心了,还是点进去看看Minecraft或Forge的代码到底是怎么处理这个ResourceLocation的吧。

那么,那个只有一个resourceName的构造方法是怎么回事呢?再举个例子,我可以这样:

ResourceLocation location = new ResourceLocation("minecraft:apple");

所以实质上,Minecraft会根据传入的resourceName中的英文冒号,自动分离出resourceDomain和resourcePath的。

另外,ResourceLocation对象还经常被用来当作某个游戏元素的唯一标识符。这个时候,它实际上就没有指代一个具体文件的作用了。

客户端与服务端

看到这个二级标题,想必有的读者觉得自己没必要看这部分内容了——这也要特意强调?

然而这部分确实很重要。

如果我随便问一个MC玩家:Minecraft的客户端是什么,服务端又是什么?

他/她的回答可能是这样:拿启动器启动的是客户端,拿开服工具的start.bat启动的是服务端。

唔,这样的回答不能算错,但不够全面——至少比认为单人模式 = 客户端, 多人模式 = 服务端的理解要好到不知道哪里去了。

物理端和逻辑端

实际上,上面那个回答,只是粗略地说出了物理客户端和物理服务端之间的区别。

而实际上,除了物理端,还有个概念,名为“逻辑端”。

逻辑端,实际上是线程层面的概念。逻辑客户端和逻辑服务端各分属两个线程组,其中逻辑客户端(Render Thread)主要用于处理和视觉、听觉有关的效果,还负责处理玩家的键鼠输入等;而逻辑服务端(Server Thread)则主要用于处理游戏逻辑的进行和游戏数据的更新。

在你游玩服务器的时候,实际上是在用你启动的物理客户端的逻辑客户端线程,连接物理服务器的逻辑服务端线程。而你处于单人模式时呢?请回忆一下,你玩模组的时候,肯定有崩游戏的时候,崩游戏的那一刹那前,是不是会有一行字:“正在关闭内置服务端”?没错,处于单机存档时,逻辑服务端也存在。也就是说,你的单人游戏,实际上相当于一个IP地址为localhost的本地服务器。

那么,逻辑客户端和逻辑服务端需不需要进行交互呢?当然是需要的。它们之间需要进行网络通信,以同步数据——否则,如果是单人模式还好,在你玩服务器的时候,你的电脑和运行服务器的设备可能距离十万八千里这么远,请问你的电脑凭什么什么都不做就能获取十万八千里外,另一台设备的服务端中的数据?所幸,Minecraft已经包办了绝大多数需要双端通信的地方。不过还有一些地方,需要我们自己完成通信。

@SideOnly

这个注解,可以用于修饰类、字段、方法、构造器。它的唯一一个参数类型是枚举类:Side。Side类有三个实例:CLIENT、SERVER和BUKKIT。这里我们主要把目光放在前两个身上。

举个例子,如果你给某个方法加上了@SideOnly(Side.CLIENT),那么这个方法只能在物理客户端被调用,如果尝试在物理服务端调用,会直接抛NoSuchMethodException。通常,这用来修饰那些负责渲染、播放音效的类、方法。

所以,这个注解是针对物理端的。

那么@SideOnly(Side.SERVER)呢?和你想的是一样的,但是——在正常情况下,不要用这个修饰任何东西。前文说了,@SideOnly(Side.CLIENT)用于修饰那些负责渲染、播放音效的类、方法,而又说了,“逻辑客户端(Render Thread)主要用于处理和视觉、听觉有关的效果,还负责处理玩家的键鼠输入等”。因为物理服务端没有逻辑客户端线程,所以@SideOnly(Side.CLIENT)实际上也起到了“把代码的执行限定在逻辑客户端”的作用。可问题是,物理客户端也会有逻辑服务端,比如有个萌新Modder觉得“逻辑服务端掌管的是游戏逻辑的进行”,给某个负责游戏逻辑进行的方法打了@SideOnly(Side.SERVER),那糟了,装了你的模组就没法进行单人游戏了——因为两个物理端都有逻辑服务端。

可能会有读者觉得,笔者讲的太抽象了。没事,等读者有了一定的开发经验,就懂了。反正记住:什么情况下,都别用@SideOnly(Side.SERVER)。我们是在写模组,不是在写服务器插件。

那么这个注解,意义何在呢?答案是减少性能开销,比如,你在物理服务端调用渲染代码也没卵用,毕竟物理服务端是以命令行的形式体现的,你渲染给谁看呢?可能又有人问了:要减少性能开销,那么针对两个物理端分别写不同的代码不就好了?这也太麻烦了。所以,这就是@SideOnly的存在的意义——让同一个模组在不同的物理端上运行时,共享同一套代码。

注册表

注册表是什么?那要先从“注册项”这个概念说起。

Minecraft原版中有着各种游戏元素,如物品、方块、状态效果、实体等等,这些游戏要素的很多行为(包括方块的“长按左键挖掘”这种通用行为,以及实体的“靠近玩家会尝试爆炸”这种仅在苦力怕这种生物上会体现的特殊行为),显然,是必须要在游戏初始化时完成设置的——不然游戏初始化有什么意义?干脆就在遇到某个游戏要素的时候读取相关逻辑不就好了?——显然,这种设计是很不好的。

所以在这种情况下,显然就需要一种东西来统筹这些游戏元素的逻辑的初始化,被统筹的游戏元素都是注册项,而统筹者则是注册表。一般而言,注册项会在FMLPreInitialization阶段被注册。想想看:如果没有注册表,就拿物品(Item)这一注册项举例,MC如何能获取到你想要添加到游戏中的所有物品呢?难不成还要用黑科技读取你的项目中的所有继承了Item类的类,再用Class#newInstance手动构造实例?或者自动检索所有Item的实例?这根本不可行。所以注册表应运而生。

Minecraft原版就有一套注册表系统,不过它的实现太过于丑陋,也根本就存在很多问题——如,你会发现使用它注册注册项时,你还需要显式指定每个实例的数字ID。那么请问你如何保证你的模组的注册项的数字ID不会和其他模组的发生冲突呢?实际上Minecraft在1.13也弃用了这种注册模式。

所以我们需要用Forge提供的注册表,我们不需要显式指定数字ID。另外它接管了原版的注册表,且是基于Forge的事件系统的,事件系统会在后面讲到。

所有注册项的类都实现了IForgeRegistry<V>这一接口,这个接口主要规定了关于注册名的一些规范,实际上这个接口有个静态内部类:Impl<T> implements IForgeRegistry<T>,所以实际上所有注册项的类是继承了这个类,这个类提供了IForgeRegistry<V>的默认实现。也就是说,理论上你可以通过T extends IForgeRegistry.Impl<T>来设定:可以用Forge的注册表系统给你的某个自定义的游戏元素进行注册,不过一般没必要这么做。

使用原版的注册表时,没有“注册名”这一说——但是我们用的是Forge的注册表,所以必须在注册对象前,设定该对象的注册名:通过setRegistryName这个方法设定。这个方法可以接受一个ResourceLocation,但也可以接受这样两种形参列表:(String name)(String modid, String name),用法可以类比ResourceLocation的那两个构造方法,笔者这里就不细说了。如果你尝试注册未设定注册名的注册项对象(即注册名为null),则肯定会崩游戏,抛NullPointerException。

@ObjectHolder

利用@GameRegistry.ObjectHolder这个注解,我们可以自动给某个类型为<T extends IForgeRegistry<T>>的变量赋值。

那么具体怎么用呢?看个例子就知道了:

@ObjectHolder("minecraft:grass")
public static final Block GRASS;

这样的话,这个静态字段就会自动被赋值Blocks.GRASS所对应的对象,相当于操作了GRASS = Blocks.GRASS

另外,你还可以在这个字段所在的类上打这个注解,作用是指定默认的作用域(Domain),如,打上@ObjectHolder("minecraft"),那么下面的只需写成@ObjectHolder("grass")即可。

享元设计模式

注册项基本上都有一个特点:遵循了“享元”的设计模式。简而言之,这个设计模式的核心思想是:重复利用实例。拿Item这一注册项举例:所有玩家背包里的苹果,无论其数量有多少,在物品栏的哪一格,其对应的Item对象都是同一个:Items.APPLE;再拿Block举例:每个Minecraft存档中都有数不清的方块,所有草方块,不管它们的坐标是什么,对应的Block对象都是同一个:Blocks.GRASS。那么,这样做有什么意义呢?

意义就是:节省内存开销啊。如果不重复利用实例,你确定你的电脑有那么大内存去分配给数量爆炸的对象吗?

提一句,既然要重复利用实例,那么显然我们需要缓存这些注册项的对象。你可以在net.minecraft.init包下找到一些类:Items、Blocks、PotionTypes等等,这些类中有许多的静态字段,都是对这些需要重复利用的对象的引用,需要的时候直接拿来用即可。不过也有例外——比如,没有Potions这个类,实际上Potion的注册是在Potion类本身中完成的,它注册的时候根本就没有保有对对象的引用,而是直接new的。

那又有人要问了:那么Minecraft在处理性质不同、但是代表的对象相同的事物的逻辑的时候,如何区分呢?对于这个问题,不同种类的注册项的处理都不同。对于Item来说,Minecraft会随建随用一个ItemStack对象;对于Block来说,有BlockPos这个类可以用。这些在后面会具体讲到。

根据Forge的官方文档,Minecraft和Forge内建的注册项的种类有如下这些:

  • 物品(Item)
  • 方块(Block)
  • 状态效果(Potion)
  • 药水类型(PotionType)
  • 声音(SoundEvent)
  • 附魔(Enchantment)
  • 合成配方(IRecipe)
  • 村民职业(VillagerProfession)
  • 实体类型(EntityEntry)[1]
  • 生物群系(Biome)

最后,注意:能使用注册表注册的游戏元素一定遵循享元设计模式,但是遵循享元设计模式的游戏元素,不一定是通过注册表注册。

注释与外部链接

  1. 注意:实体(Entity)并不遵循享元模式,实际上世界中一个实体就代表了一个独特的Entity对象。只是Forge提供了一个工具类:EntityEntry,用来代表实体的种类,因此我们可以通过注册表注册实体。