前言
之前一直就很想做forge开发,但是没有时间
趁着春节假期,打算好好学一下
主要基于北山_Besson的教程
复习
java,两年半没写了,还是生疏了很多,基础的东西还好,一些关键的语法已经忘了
final和static
类(实例)、抽象类(模板)、接口(方法要求)
lambda表达式,(参数) -> {语句块}
设计模式
单例模式、工厂模式:简单工厂、工厂方法、抽象工厂
策略模式、状态模式、观察者模式(事件监听器)
配置
jdk17,win+path添加环境变量
forge的mdk
Downloads for Minecraft Forge for Minecraft 1.20.1
给IDEA换源:gradle/wrapper/gradle-wrapper.properties
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https://mirrors.cloud.tencent.com/gradle/gradle-8.8-bin.zip
networkTimeout=10000
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
加载反混淆映射:gradle.properties
将mapping_channel改为parchment
去羊皮纸官网Getting Started查询对应的映射版本号
把mapping_version改为2023.09.03-1.20.1
添加反混淆的仓库:settings.gradle
(添加 maven{url='https://maven.parchmentmc.org'} )
pluginManagement {
repositories {
gradlePluginPortal()
maven {
name = 'MinecraftForge'
url = 'https://maven.minecraftforge.net/'
}
maven { url = 'https://maven.parchmentmc.org' }
}
}
plugins {
id 'org.gradle.toolchains.foojay-resolver-convention' version '0.7.0'
}
在build.gradle中添加反混淆插件:
(添加 id 'org.parchmentmc.librarian.forgegradle' version '1.+' )
plugins {
id 'eclipse'
id 'idea'
id 'maven-publish'
id 'net.minecraftforge.gradle' version '[6.0,6.2)'
id 'org.parchmentmc.librarian.forgegradle' version '1.+'
}
然后把下面minecraft{}中的mapping channels改一下:
mappings channel: 'parchment', version: '2023.09.03-1.20.1'
最后调整一下maven构建的线程数,设置->构建、执行、部署->构建工具->maven
线程计数改为10C(十线程)
然后在maven->运行程序,勾选 "将IDE构建/运行委托给maven"
点旁边的大象,有个刷新按钮,重新构建一下项目
然后打开下面的forgegradle runs,双击runClient运行
源码阅读
阅读反编译、反混淆后的MC源代码
在外部库中,找net开头的有mapped_parchment的库

数据文件在这个库:

解读所有源码的工程量相当的大,只需要看几个重要的,比如Items和Blocks,看一下他们的注册流程(双击shift,可以快速搜索类查看代码)
我们阅读一下实例代码中,变量BLOCKS到变量EXAMPLE_TAB之间的内容
DeferredRegister:延迟注册器,避免一些并发注册的问题,让mod中需要注册的内容在forge注册之后进行注册
RegistryObject:注册完之后会变成一个实际的对象
IEventBus:事件总线,把DeferredRegister注册到总线上时会调用事件总线的注册方法。
可以发现,它就是定义了三个延迟注册器,分别注册Block、Item、CreativeModeTab
然后分别调用三个延迟注册器,注册了对应的方块、方块物品、物品、创造模式物品栏
读完后可以删掉这些示例内容,因为我们后续做物品、方块、物品栏注册会分到不同的文件中去写,这样看起来就会规整许多
开始
一般开始编写mod需要有一个有趣的idea,当然,如果只是为了练习,随便取什么名字都可以
想好我们的mod名称,我们可以右键src/main/java/com/example/examplemod/ExampleMod,进行重构->重命名,改为新的名字,同时右键使用重构,修改ExampleMod中的MOD_ID的值
我这里改为了TestMod,MOD_ID为test_mod
可能后面还需要看看gradle.properties中最下面的部分有没有同步更改,最下面第二个变量表示MOD在列表中显示的名称。
注册物品
首先MC中物品和方块是不一样的,方块更像是特殊的物品,它既能够带在背包中,作为合成材料,也具有模型,能够放置出来。而物品一般只能作为掉落物和合成材料。
方块一般需要同时注册Blocks和Items,而物品就简单一些,只用注册Items。
首先新建item文件夹,然后新建类ModItems

模仿实例中注册物品的过程,先建立延迟注册器ITEMS,然后注册物品。
最后写register方法,让我们的TestMod类中来调用这个register方法,完成所有物品的注册。
代码如下:item/ModItems.java
java
public class ModItems {
public static final DeferredRegister<Item> ITEMS = DeferredRegister.create(ForgeRegistries.ITEMS, TestMod.MOD_ID);
public static final RegistryObject<Item> CANDY_STICK =
ITEMS.register("candy_stick", () -> new Item(new Item.Properties()));
public static final RegistryObject<Item> CANDY_STAFF =
ITEMS.register("candy_staff", () -> new Item(new Item.Properties()));
public static void register(IEventBus eventBus) {
ITEMS.register(eventBus);
}
}
这里可能会令人感到疑惑,ITEMS都是用的register方法,为啥会有不同的效果。
因为如果它检测到传入的参数时IEventBus时,它会走另一条逻辑,把自己注册了的内容打包给eventBus注册。源代码如下:

而普通物品的注册逻辑是这样的:
java
public <I extends T> RegistryObject<I> register(String name, Supplier<? extends I> sup) {
if (this.seenRegisterEvent) {
throw new IllegalStateException("Cannot register new entries to DeferredRegister after RegisterEvent has been fired.");
} else {
Objects.requireNonNull(name);
Objects.requireNonNull(sup);
ResourceLocation key = new ResourceLocation(this.modid, name);
if (this.registryKey != null) {
RegistryObject<I> ret = this.optionalRegistry ? RegistryObject.createOptional(key, this.registryKey, this.modid) : RegistryObject.create(key, this.registryKey, this.modid);
if (this.entries.putIfAbsent(ret, sup) != null) {
throw new IllegalArgumentException("Duplicate registration " + name);
} else {
return ret;
}
} else {
throw new IllegalStateException("Could not create RegistryObject in DeferredRegister");
}
}
}
可以看到,里面通过ResourceLocation来定位对应物品的资源(图标、材质模型、音效)
然后调用RegistryObject的create方法,最后把创建的RegistryObject返回出来
接下来我们需要准备物品的资源
resources资源(models与textures)

textures包含物品的纹理,也就是显示的图标
models就是物品的模型,lang就是语言文件,blockstates就是方块的状态文件。

首先将绘制的物品图标放在resources/assets/test_mod/textures/item中
然后在models/item中新建各个物品的json文件,名称必须与我们注册的物品名称相同
以candy_staff.json为例:
java
{
"parent": "minecraft:item/generated",
"textures": {
"layer0": "test_mod:item/candy_staff"
}
}
item/generated一般表示2D物品渲染,layer0就是基础层(可以定义layer1、layer2进行叠加渲染)
(item/handheld表示手持工具的渲染,item/handheld_rod表示手持杆状工具的渲染)
在models中,item主要管手持物(一三人称、左右手)、掉落物、物品栏、物品展示框的渲染
而block主要管方块的渲染,以candy_clock.json为例
java
{
"parent": "block/cube_all",
"textures": {
"all": "test_mod:block/candy_block"
}
}
这里用cube_all是因为该方块所有的面都相同,都使用同一个纹理
还有很多种方块渲染模式,AI一下就能知道
注册方块
其实操作也是差不多的,只不过需要在注册时同时注册方块的物品,否则,如果只有方块,没有对应物品会出一些奇怪的问题。
这里写了一个registerBlock函数,来一起注册方块和方块物品。
java
public class ModBlocks {
public static final DeferredRegister<Block> BLOCKS
= DeferredRegister.create(ForgeRegistries.BLOCKS, TestMod.MOD_ID);
public static final RegistryObject<Block> CANDY_BLOCK
= registerBlock("candy_block", () -> new Block(BlockBehaviour.Properties.of().strength(1.0f, 1.0f)));
private static <T extends Block> void registerBlockItems(String name, RegistryObject<T> block) {
ModItems.ITEMS.register(name, () -> new BlockItem(block.get(), new Item.Properties()));
}
private static <T extends Block> RegistryObject<T> registerBlock(String name, Supplier<T> block) {
RegistryObject<T> blocks = BLOCKS.register(name, block);
registerBlockItems(name, blocks);
return blocks;
}
public static void register(IEventBus eventBus) {
BLOCKS.register(eventBus);
}
}
然后我们依然需要去准备方块的资源,定义方块的模型
除此之外,我们还需要在resources中设置方块的blockstate,这里本来是交给一些比较复杂的方块的(比如有朝向、有开关两种状态的方块)
不过这里只是改变模型的渲染效果,无法改变碰撞箱。
代码:resources/assets/test_mod/blockstates/candy_block.json
java
{
"variants": {
"": {
"model": "test_mod:block/candy_block"
}
}
}
注册创造模式物品栏
也是类似的,先创建延迟注册器,但创建物品栏的时候差别较大
这里采用的是建造者模式来构建CreativeModeTab,icon就是物品栏的图标,title就是物品栏的名称,displayItems就是物品栏中进行展示的物品,最后进行一个.build完成建造。
java
public class ModCreativeModeTabs {
public static final DeferredRegister<CreativeModeTab> CREATIVE_MODE_TABS
= DeferredRegister.create(Registries.CREATIVE_MODE_TAB, TestMod.MOD_ID);
public static final RegistryObject<CreativeModeTab> TEST_TAB
= CREATIVE_MODE_TABS.register("test_tab", () -> CreativeModeTab.builder()
.icon(() -> new ItemStack(ModItems.CANDY_STICK.get()))
.title(Component.translatable("itemGroup.test_tab"))
.displayItems((parameters, output) -> {
output.accept(ModItems.CANDY_STICK.get());
output.accept(ModItems.CANDY_STAFF.get());
output.accept(ModBlocks.CANDY_BLOCK.get());
})
.build()
);
public static void register(IEventBus eventBus) {
CREATIVE_MODE_TABS.register(eventBus);
}
}
注意到这里有个Component.translatable("itemGroup.test_tab"),这里其实是为了方便后续文本翻译,在lang文件中使用该名称查询对应物品栏的翻译。
resources资源(lang)

在lang文件夹下新建en_us.json,里面根据你注册时的物品名称与命名空间来定位物品
一般物品就是 "item.mod名称.物品名称",方块就是 "block.mod名称.物品名称"
java
{
"item.test_mod.candy_stick": "Candy Stick",
"item.test_mod.candy_staff": "Candy Staff",
"block.test_mod.candy_block": "Candy Block",
"itemGroup.test_tab": "Test Tab"
}
运行结果
由于我们还没定义糖块的texture,它无法显示正常的纹理。
