跳到主要内容

第三章 基础概念

ℹ️提示

本章涉及的内容多数人早已有所了解,你可以快速阅读本章内容。

这是一个教你怎么写一个对标 Minecraft1.21并使用NeoForge模组加载器的模组的教程。

⚠️警告

本教程只适用于使用NeoForge模组加载器的 Minecraft1.21版本,如果你在写别的版本的模组,参考本教程可能会导致你的代码出错或出现未经验证的问题

ℹ️使用须知

在本教程中,我们只讨论如何用Java写模组,不会讨论有关MCreator这类模组制作器的使用。如果你不会Java,请先学习Java。

游戏概念

在Minecraft中,所有元素都具有明确的功能划分。通过决定不同的元素应具有怎样的特性,我们可以快速地按照Minecraft的逻辑添加我们需要的东西。

  • 了解这些逻辑有助于减少不必要的思考并养成良好的编写习惯,还会显著提升代码规范性。

让我们来了解一下不同元素的概念与关系,顺便熟悉一下它们的类名:

在这张思维导图中,我们把Minecraft世界中的不同内容划分成三大板块:

  • 定义(Definitions):作为定义内容,这些对象在单个Minecraft程序中实际上只有一个实例

    例如,你有两把原版铁镐物品(Item),正作为物品堆(ItemStack, 一种小容器)保留在你的物品栏中,即使它们的附魔不同、耐久度不同、命名也不同,但这两个铁镐在使用时所执行的代码,都指向同一个继承了Item类的实例,也就是PickaxeItem类1,这表明了它们都是镐。这一点很重要!(McJty如是说)

    请注意,“只有一个实例”并不是说所有的镐都共用一个PickaxeItem实例,而是所有木镐共用一个PickaxeItem实例、所有石镐共用一个PickaxeItem实例、所有铁镐共用一个PickaxeItem实例等。物品类并非单例,而是给每一种单独的物品一个实例。

    思考一下:既然所有的同类物品都指向同一个物品实例,它们的变量就是共享的。既然如此,我们是如何区分不同铁镐的名称和耐久度等内容的呢?不妨仔细看看思维导图寻找答案。

    有想法了吗?继续阅读获得更多信息吧!

    除了物品,每一种方块(静态的方块,如木板)、每一个方块实体类型定义,以至于每一种实体类型的定义,都指向它们唯一的实例。比如所有告示牌都共用名为sign的方块实体类型,这一类型作为构造器&工厂持有SignBlockEntity类的new方法入口2,在使用时能直接提供一个新的SignBlockEntity对象。

  • 物品栏(Inventory):无论是玩家自己还是其它生物的物品栏,亦或是营火等可以存物品的方块,它们所持有的每一组物品都是以ItemStack,也就是物品栈的形式记录在容器中。每一个物品栈,无论容纳了像斧这样只能堆叠一个的物品,还是能堆叠多个的物品,都是不同的实例

    ItemStack作为存储某一种类特定属性物品的容器,在玩家使用物品操作时,负责将上游提供的记录了目标点等参数的useContext传递给对应Item实例内的方法,并将其产生的交互结果,也就是InteractionResult(可以决定玩家是否做出其它动作)返回给上游。这一逻辑也辅助着远程玩家的挥手动作等行为的同步与计算,大大节省了网络开支。

  • 世界内(World):我们在定义中提到,世界上每一种方块都只有一个实例,也就说明我们只要更改某种方块对应的实例,整个世界的这种方块都会跟着改变。那么,如果这是一个红石灯,我们又如何区分它是亮还是灭呢?红石灯可没有方块实体帮它存储数据。

    实际上,世界中的每个方块都是方块状态(BlockState),它可以更详细地表示某一个方块具有什么特性。Block与BlockState的关系和Item与ItemStack的关系比较接近,所以你可以将BlockState理解为一个方块的外包装

    当你放下某种材质的台阶时,台阶方块对应的类实例就会被调用。它会判断你放在哪里、哪个平面上以及此刻的你面朝哪里等数据,随后从自身获取一个默认BlockState,并设置其属性(你也可以使用调试棒直观地更改它们),随后,该BlockState被Level(世界)放置到坐标上。有关方块放置的相关事件我们会在方块章节再次提到。

    实体的概念和方块略有差异。对于一个实体,

用户端

处理业务的时候,你肯定了解过服务端客户端客户端负责处理用户的渲染、交互等显示信息和发出指令的部分,通常运行在使用者的电脑上;服务端则负责处理更新、响应这类数据,一般存在于远程服务器上,也可能在使用者的电脑上。然而,具体来了解Minecraft的用户端机制,会发现Minecraft实际上有两种具体的端类型,分别是物理端组以及逻辑端组。这与标准的双端逻辑略有差异,并有可能会影响到后续的模组开发,需要划分明白。接下来,我会基本按照NeoForge文档的描述,分为物理端组逻辑端组两种类型进行介绍,并对它们的关系进行阐述。

物理端组

当你用启动器运行一个Minecraft实例,你其实是启动(boot up)了一个物理客户端。“物理“表示这是一个客户端上的程序。这特别意味着包括所有的渲染功能在内的客户端功能,都可以在这里使用,并且可以根据需要使用

物理服务端,也被称为专有(dedicated)服务端,是在运行Minecraft服务器Jar时启动的。虽然Minecraft服务器带有基本的GUI功能,但它不具有任何客户端的功能。由此表明,服务器实例中缺少客户端具有的类。在物理服务器上调用这些类将导致类不存在错误(NoClassDefFoundError,是指编译时存在该类,但运行时无法找到),并会导致崩溃,因此在开发时务必要谨慎对待

逻辑端组

逻辑端主要是Minecraft的内部程序结构

逻辑服务端执行游戏逻辑的端。像时间和天气变化、实体的游戏刻(game tick)更新和实体生成等操作都在服务端上运行。所有类型的数据,包括物品栏内容数据,也都是服务器该处理的

逻辑客户端负责控制并渲染所有要在游戏中显示的内容。Minecraft将所有客户端代码保存在一个独立的net.minecraft.client包中,并在一个名为渲染线程(Render Thread)的单独分配的线程中运行,而其它所有逻辑内容都被规定为客户端和服务端共有的代码。

总结

物理端组代表的是实际上负责调度和运行Minecraft逻辑的进程。你运行一个客户端或一个服务端包的时候,启动的程序体就是物理端程序体

逻辑端组代表的是运行Minecraft游戏逻辑的那部分程序,只负责游戏内逻辑,并在物理端提供的对外界运行环境不敏感的框架内与渲染线程或数据同步机制交互。

关系

物理端可以调度运行逻辑端,并给逻辑端营造一个无需考虑系统类型的环境,确保在不更改逻辑端代码的情况下适配多种主机。

需要注意的是,在某些情况下,某个端不一定存在。这里是相关情况的例子:

  • 当你启动Minecraft客户端的一段时间内,只存在物理客户端,此时物理客户端正在启动逻辑客户端。
  • 从客户端Minecraft窗体开始渲染开始,只存在物理客户端和逻辑客户端,没有任何服务端可用。
  • 当你加载一个单人游戏存档时,物理客户端开始启动一个逻辑服务端,之后,主机上同时存在物理客户端逻辑客户端逻辑服务端,客户端和服务端采用类似localhost(本地连接)的方式传递信息。
  • 当你加入一个多人游戏后,你的主机上只存在物理客户端逻辑客户端,不同的是你可以和远程服务端进行通信了。
  • 当你启动一个服务端jar的一段时间内,只存在物理服务端,此时物理服务端正在启动逻辑服务端。如果你没有在启动前添加-noGUI参数,物理服务端还将启用一个简易的监控窗口,监视并控制服务器功能。
  • 服务端启动完成后,服务器主机上只存在物理服务端逻辑服务端

事件(Events)

事件系统是NeoForge的一大优秀功能,协助着从模组内容开始加载到玩家登入、右键、摔落、离开等一系列事件的触发和运行,功能众多,并有效解决了在一般情况下必须要修改源码(Mixin)才能实现某些功能的问题。

事件总线(Event Bus)

在NeoForge架构中,事件系统是采用总线模式设计的,这个概念是广泛存在于生活中的。

什么是总线

所谓总线,就是说所有的相关事件发生时都会提交给一个共同的通道,并触发对应该事件的监听器。NeoForge提供两种总线:模组总线(Mod Bus)和NeoForge总线。

在FML(Fancy Mod Loader)加载模组的阶段,FML会扫描模组代码中的注解,并将收集到的事件注解自动添加到总线。你也可以将事件监听方法或类直接注册到总线上。

模组总线-Mod Bus

首先注意一点,NeoForge在模组开始加载时会给每个可加载模组生成并分配一个模组总线,并传给该模组入口方法。值得注意的是,多个模组总线是并发的,也就是同一时刻会有多个模组监听自己的模组总线并执行初始化任务。这种设计使得NeoForge可以更快地加载大量模组。


MakerTechno于2025年6月2日起稿并于6月11日凌晨暂时中止

MakerTechno修正于2025年6月28日,并添加了标识性头

MakerTechno于2025年7月份恢复编写,7月3日搁置(实际上是去打ATM10了)

MakerTechno于2025年8月3日恢复编写

MakerTechno改写于2025年8月4日,更改了本站教程的协议类型

Copyright © 2025 MakerTechno. 保留所有权利。

在明确注明原文出处(包括作者名与原始链接)的前提下,允许非商业性地引用本作品片段。引用内容不得超过原文的 20%,不得歪曲原意或用于误导性语境。整篇转载或复制使用需获得作者授权。本网站所有教程不允许商用,也不会授予商用授权。

Footnotes

  1. PickaxeItem的父类链:(FeatureElement, ItemLike, IItemExtension) -> Item -> TieredItem -> DiggerItem -> PickaxeItem

  2. SignBlockEntity的父类链:([IAttachmentHolder -> AttachmentHolder], IBlockEntityExtension) -> BlockEntity -> SignBlockEntity