MCBBS Wiki欢迎您共同参与编辑!在参与编辑之前请先阅读Wiki方针。
如果在编辑的过程中遇到了什么问题,可以去讨论板提问。
为了您能够无阻碍地参与编辑 未验证/绑定过邮箱的用户,请尽快绑定/验证。
MCBBS Wiki GitHub群组已上线!
您可以在回声洞中发表吐槽!
服务器状态监控。点击进入
本站由MCBBS用户自行搭建,与MCBBS及东银河系漫游指南(北京)科技有限公司没有从属关系。点此了解 MCBBS Wiki 不是什么>>
用户:MashKJo/1.12.2模组开发教程/2.基础概念:修订间差异
(创建页面,内容为“这一节,会讲述一些非常重要的前置概念。 == ResourceLocation == 这是一个位于<code>net.minecraft.util</code>包下的一个类。这个类的用途的其中之一,顾名思义,就是用来表示文件在assets文件夹中的路径。在开发环境中,assets文件夹在<code>src/main/resources</code>路径下;而在打包好的.jar文件的中,assets文件夹点进去就能看到。 ResourceLocation总共有3个构造器,我…”) |
(→注册表: // Edit via Wikiplus) |
||
第70行: | 第70行: | ||
所以我们需要用Forge提供的注册表,我们不需要显式指定数字ID。另外它接管了原版的注册表,且是基于Forge的事件系统的,事件系统会在后面讲到。 |
所以我们需要用Forge提供的注册表,我们不需要显式指定数字ID。另外它接管了原版的注册表,且是基于Forge的事件系统的,事件系统会在后面讲到。 |
||
所有注册项的类都实现了 |
所有注册项的类都实现了IForgeRegistryEntry<V>这一接口,这个接口主要规定了关于注册名的一些规范,实际上这个接口有个静态内部类:Impl<T> implements IForgeRegistryEntry<T>,所以实际上所有注册项的类是继承了这个类,这个类提供了IForgeRegistryEntry<V>的默认实现。也就是说,理论上你可以通过T extends IForgeRegistryEntry.Impl<T>来设定:可以用Forge的注册表系统给你的某个自定义的游戏元素进行注册,不过一般没必要这么做。 |
||
使用原版的注册表时,没有“注册名”这一说——但是我们用的是Forge的注册表,所以必须在注册对象前,设定该对象的注册名:通过setRegistryName这个方法设定。这个方法可以接受一个ResourceLocation,但也可以接受这样两种形参列表:<code>(String name)</code>和<code>(String modid, String name)</code>,用法可以类比ResourceLocation的那两个构造方法,笔者这里就不细说了。如果你尝试注册未设定注册名的注册项对象(即注册名为null),则肯定会崩游戏,抛NullPointerException。 |
使用原版的注册表时,没有“注册名”这一说——但是我们用的是Forge的注册表,所以必须在注册对象前,设定该对象的注册名:通过setRegistryName这个方法设定。这个方法可以接受一个ResourceLocation,但也可以接受这样两种形参列表:<code>(String name)</code>和<code>(String modid, String name)</code>,用法可以类比ResourceLocation的那两个构造方法,笔者这里就不细说了。如果你尝试注册未设定注册名的注册项对象(即注册名为null),则肯定会崩游戏,抛NullPointerException。 |
||
=== @ObjectHolder === |
=== @ObjectHolder === |
||
利用<code>@GameRegistry.ObjectHolder</code>这个注解,我们可以自动给某个类型为<T extends |
利用<code>@GameRegistry.ObjectHolder</code>这个注解,我们可以自动给某个类型为<T extends IForgeRegistryEntry<T>>的变量赋值。 |
||
那么具体怎么用呢?看个例子就知道了: |
那么具体怎么用呢?看个例子就知道了: |
2024年12月5日 (四) 16:17的版本
这一节,会讲述一些非常重要的前置概念。
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的事件系统的,事件系统会在后面讲到。
所有注册项的类都实现了IForgeRegistryEntry<V>这一接口,这个接口主要规定了关于注册名的一些规范,实际上这个接口有个静态内部类:Impl<T> implements IForgeRegistryEntry<T>,所以实际上所有注册项的类是继承了这个类,这个类提供了IForgeRegistryEntry<V>的默认实现。也就是说,理论上你可以通过T extends IForgeRegistryEntry.Impl<T>来设定:可以用Forge的注册表系统给你的某个自定义的游戏元素进行注册,不过一般没必要这么做。
使用原版的注册表时,没有“注册名”这一说——但是我们用的是Forge的注册表,所以必须在注册对象前,设定该对象的注册名:通过setRegistryName这个方法设定。这个方法可以接受一个ResourceLocation,但也可以接受这样两种形参列表:(String name)
和(String modid, String name)
,用法可以类比ResourceLocation的那两个构造方法,笔者这里就不细说了。如果你尝试注册未设定注册名的注册项对象(即注册名为null),则肯定会崩游戏,抛NullPointerException。
@ObjectHolder
利用@GameRegistry.ObjectHolder
这个注解,我们可以自动给某个类型为<T extends IForgeRegistryEntry<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)
最后,注意:能使用注册表注册的游戏元素一定遵循享元设计模式,但是遵循享元设计模式的游戏元素,不一定是通过注册表注册。
注释与外部链接
- ↑ 注意:实体(Entity)并不遵循享元模式,实际上世界中一个实体就代表了一个独特的Entity对象。只是Forge提供了一个工具类:EntityEntry,用来代表实体的种类,因此我们可以通过注册表注册实体。