本篇教程由作者设定使用 CC BY-NC-ND 协议。

看前注意,本教程主体为1.18.X版本,如果没有指示,默认为1.18.X的教程。

例子:

版本通用(在哪个版本都可以使用)

XX独有功能(只有XX版本才可以用的)



(这个教程会持续更新)

如果你想制作mod但是不懂java?不知道怎么配环境?寻找了网上的教程的但是不知道说的是什么?这个教程能详细的告诉你怎么制作mod

本教程分为


游戏开发前(学了或许对开发模组有帮助)

JAVA篇(学习java的基本知识)版本通用

Kotlin前篇(学习Kotlin的基本知识)版本通用←它在施工


游戏开发中(模组的核心)

模组构建篇(必须学会,不然我顺着网线教你!!!)版本通用

新手篇(是个新手就能理解,不怎么需要java的基础)

基础篇(内容稍微有一点难,需要一点java的基础)

应用篇(十分的困难,需要理解java的写法)

模组篇(如果写模组的前置和联动的话值得看)版本通用

mixin篇(想修改minecraft的内容的)版本通用

Kotlin后篇(如何将Kotlin应用到游戏开发上)版本通用←它在施工


游戏开发后(发表模组)

发表模组篇(做完的模组如何发表)版本通用

游戏崩溃篇(教你如何处理游戏崩溃)版本通用

自述文件篇(想介绍你的模组的人的可以学这个)版本通用


需要的东西

IDEA

forge[MDK](不能直接下载,需要点击MDK)

Blockbench (建模用)

构建模组

IDEA

按照指示下载即可(把所有的勾了!)

开始构建模组

如果把一下的做了你的屏幕应该会出现这两个东西

InterlliJ IDEA(IDEA版本)

forge-(游戏版本)-(forge版本)-mdk.zip

如果有就说明你成功了。如果没有,那就是哪里出错了。

把zip的压缩给解开放在(给你的模组起名字)文件夹(之后把(给你的模组起名字)文件夹为模组文件夹),打开IDEA把模组文件夹放进去。

环境变量(如果出现因为java版本不对遇到的问题建议看,否则可以跳过)

打开IDEA之后右键模组文件夹,打开【打开模块设置】,点击SDK的右边。下载JDK,下载最新的JDK。然后右键{此电脑},点击属性。

点击高级系统设置,点击环境变量。

需要你设置的有

Path

JAVA_HOME

第一个需要把你下载的JDK的bin当做路径,第二个就是java路径

比如说

(Path)C:\.gradle\jdks\adoptium-17-x64-hotspot-windows\jdk-17.0.11+9\bin

(JAVA_HOME)C:\.gradle\jdks\adoptium-17-x64-hotspot-windows\jdk-17.0.11+9

设置模组

打开IDEA把进行这样的操作

src/main/java/com/example/examplemod/examplemod.java(保留examplemod.java然后把包括com在内的删掉改成)

src/main/java/(你的域名)/examplemod.java(然后把examplemod.java的名称改成你的模组名称)

这个可留可不留,如果你是新手的话建议留

      src/main/resources/META-INF/mods.toml(修改成以下的格式)

# 这是一个示例 mods.toml 文件。它包含与加载模组相关的数据。
# 有几个强制字段 (#mandatory),以及许多可选字段 (#optional)。
# 总体格式是标准的 TOML 格式,v0.5.0。
# 请注意,此文件中有几个 TOML 列表。
# 在此处查找有关 toml 格式的更多信息:https://github.com/toml-lang/toml
# 要加载的模组加载器类型的名称 - 对于常规的 FML @Mod 模组,应该是 javafml
modLoader = "javafml" #mandatory
# 与该模组加载器匹配的版本范围 - 对于常规的 FML @Mod,它将是 Forge 的版本
loaderVersion = "[40,)" #mandatory 这通常会被 Forge 每个 Minecraft 版本提升一次。请参阅我们的下载页面以获取版本列表。
# 模组的许可证。这是强制性元数据,可更轻松地理解您的再分发属性。
# 请在 https://choosealicense.com/ 上查看您的选项。All rights reserved 是默认的版权声明,因此在此处默认为此。
license = "All rights reserved"
# 当此模组出现问题时,可用于引导人们的 URL
issueTrackerURL = "(如果模组出现了问题,可以在这给链接报告的东西,可不加" #optional
# 一系列模组 - 允许的数量由各个模组加载器确定
[[mods]] #mandatory
# 模组的 modid
modId = "(模组名)" #mandatory
# 模组的版本号 - 此处可以使用一些众所周知的 ${} 变量,或者直接硬编码
# ${file.jarVersion} 将替换从模组的 JAR 文件元数据中读取的 Implementation-Version 的值
# 有关如何在构建过程中完全自动填充此内容的相关 build.gradle 脚本,请参阅。
version = "(你的模组版本)" #mandatory
# 模组的显示名称
displayName = "(模组名(可以写汉字))" #mandatory
# 用于查询此模组的更新的 URL。请参阅 JSON 更新规范 https://mcforge.readthedocs.io/en/latest/gettingstarted/autoupdate/
updateJSONURL = "(更新的时候的链接,可以不加)" #optional
# 此模组的“主页”URL,在模组 UI 中显示
displayURL = "(模组的主页链接,可以不加)" #optional
# 用于显示的包含在模组 JAR 根目录中的标志的文件名
logoFile = "(模组的图像)" #optional
# 在模组 UI 中显示的文本字段
credits = "(制作团队,可以不加)" #optional
# 在模组 UI 中显示的文本字段
authors = "(作者)" #optional
# 模组的描述文本(多行!)(#mandatory)
description = '''(简介)'''
# 一个依赖项 - 使用 . 来指示特定 modid 的依赖关系。依赖项是可选的。
#从这里往下写这个模组的依赖,forge和minecraft是必须的!!!
[[dependencies.changedplus]] #optional
# 依赖项的 modid
modId = "forge" #mandatory
# 此依赖项是否必须存在 - 如果不是,则必须指定以下顺序
mandatory = true #mandatory
# 依赖项的版本范围
versionRange = "[40,)" #mandatory
# 依赖项的排序关系 - 如果关系不是强制性的,则必须在之后指定
ordering = "NONE"
# 此依赖项应用的侧面 - BOTH、CLIENT 或 SERVER
side = "BOTH"
# 这是另一个依赖项
[[dependencies.changedplus]]
modId = "minecraft"
mandatory = true
# 此版本范围声明了当前 Minecraft 版本的最低版本,但不包括下一个主要版本
versionRange = "[1.18.2,1.19)"
ordering = "NONE"
side = "BOTH"

如果是新手的话这么设置可以,但是你想加模组前置之类的看后续的应用篇。

(模组的图像指的是{.png}把你想放进去的图像放进src/main/resources里)

右击java,新建,目录,写你的域名(啥都可以,可以是你的B站主页,github,curseforge等等,如果没有写个[com.你的名字.你的模组名称])

如何打包模组?

以我的模组为例子

buildscript {
    repositories {
        // 这些仓库仅用于 Gradle 插件,将任何其他仓库放在下面的 repository 块中
        maven { url = 'https://maven.minecraftforge.net' }
        mavenCentral()
    }
    dependencies {
        classpath group: 'net.minecraftforge.gradle', name: 'ForgeGradle', version: '5.1.+', changing: true
    }
}

// 只编辑此行以下的内容,上面的代码添加并启用了 Forge 设置所需的内容。
plugins {
    id 'eclipse'
    id 'maven-publish'
}
apply plugin: 'net.minecraftforge.gradle'


version = '0.0.0' //你的模组版本
group = 'github.com.gengyoubo.changedplus' //域名
archivesBaseName = 'changedplus'//模组名字,没有别的了

// Mojang 在 1.18+ 版本中向最终用户提供 Java 17,因此您的模组应该针对 Java 17。
java.toolchain.languageVersion = JavaLanguageVersion.of(17)

println "Java: ${System.getProperty 'java.version'}, JVM: ${System.getProperty 'java.vm.version'} (${System.getProperty 'java.vendor'}), Arch: ${System.getProperty 'os.arch'}"
minecraft {
    // 映射可以随时更改,并且必须采用以下格式。
    // 渠道:版本:
    // official   MCVersion             Mojang 映射文件中的官方字段/方法名称
    // parchment  YYYY.MM.DD-MCVersion  基于官方的开放社区参数名称和 javadocs
    //
    // 在使用 'official' 或 'parchment' 映射时,您必须注意 Mojang 许可证。
    // 请在此处查看更多信息:https://github.com/MinecraftForge/MCPConfig/blob/master/Mojang.md
    //
    // Parchment 是由 ParchmentMC 维护的非官方项目,与 MinecraftForge 分开
    // 使用它们的映射需要进行其他设置:https://github.com/ParchmentMC/Parchment/wiki/Getting-Started
    //
    // 使用非默认映射时请自行承担风险。它们可能不总是有效。
    // 更改映射后,只需重新运行设置任务即可更新您的工作区。
    mappings channel: 'official', version: '1.18.2'

    // accessTransformer = file('src/main/resources/META-INF/accesstransformer.cfg') // 目前,此位置无法从默认值更改。

    // 默认的运行配置。
    // 可以根据需要进行调整、删除或复制。
    runs {
        client {
            workingDirectory project.file('run')

            // 用户开发环境中推荐的日志数据
            // 标记可以按需添加/删除,以逗号分隔。
            // "SCAN": 用于扫描模组。
            // "REGISTRIES": 用于触发注册表事件。
            // "REGISTRYDUMP": 用于获取所有注册表的内容。
            property 'forge.logging.markers', 'REGISTRIES'

            // 控制台的推荐日志级别
            // 您可以在这里设置各种级别。
            // 请阅读:https://stackoverflow.com/questions/2031163/when-to-use-the-different-log-levels
            property 'forge.logging.console.level', 'debug'

            // 逗号分隔的命名空间列表,用于加载游戏测试。空白 = 所有命名空间。
            property 'forge.enabledGameTestNamespaces', 'examplemod'

            mods {
                examplemod {
                    source sourceSets.main
                }
            }
        }

        server {
            workingDirectory project.file('run')

            property 'forge.logging.markers', 'REGISTRIES'

            property 'forge.logging.console.level', 'debug'

            // 逗号分隔的命名空间列表,用于加载游戏测试。空白 = 所有命名空间。
            property 'forge.enabledGameTestNamespaces', 'examplemod'

            mods {
                examplemod {
                    source sourceSets.main
                }
            }
        }

        // 此运行配置启动 GameTestServer 并运行所有已注册的游戏测试,然后退出。
        // 默认情况下,当没有提供游戏测试时,服务器将崩溃。
        // 游戏测试系统默认也启用了其他运行配置下的 /test 命令。
        gameTestServer {
            workingDirectory project.file('run')

            // 用户开发环境中推荐的日志数据
            // 标记可以按需添加/删除,以逗号分隔。
            // "SCAN": 用于扫描模组。
            // "REGISTRIES": 用于触发注册表事件。
            // "REGISTRYDUMP": 用于获取所有注册表的内容。
            property 'forge.logging.markers', 'REGISTRIES'

            // 控制台的推荐日志级别
            // 您可以在这里设置各种级别。
            // 请阅读:https://stackoverflow.com/questions/2031163/when-to-use-the-different-log-levels
            property 'forge.logging.console.level', 'debug'

            // 逗号分隔的命名空间列表,用于加载游戏测试。空白 = 所有命名空间。
            property 'forge.enabledGameTestNamespaces', 'examplemod'

            mods {
                examplemod {
                    source sourceSets.main
                }
            }
        }

        data {
            workingDirectory project.file('run')

            property 'forge.logging.markers', 'REGISTRIES'

            property 'forge.logging.console.level', 'debug'

            // 指定数据生成的模组 id、输出结果的资源和查找现有资源的位置。
            args '--mod', 'examplemod', '--all', '--output', file('src/generated/resources/'), '--existing', file('src/main/resources/')

            mods {
                examplemod {
                    source sourceSets.main
                }
            }
        }
    }
}

// 包含由数据生成器生成的资源。
sourceSets.main.resources { srcDir 'src/generated/resources' }

repositories {
    // 将依赖项的仓库放在这里
    // ForgeGradle 会自动为您添加 Forge maven 和 Maven Central

    // 如果在 ./libs 中有模组 jar 依赖项,您可以将它们声明为仓库,如下所示:
    // flatDir {
    //     dir 'libs'
    // }
}

dependencies {
    // 指定要使用的 Minecraft 版本。如果此组不是 'net.minecraft',则假定该依赖项是 ForgeGradle 的 'patcher' 依赖项,并将应用其补丁。
    // 用户开发时间是一个特殊名称,并且将对其进行各种转换。
    minecraft 'net.minecraftforge:forge:1.18.2-40.2.0'

    // 真实模组非混淆依赖项示例 - 这些将被重映射为当前映射
    // compileOnly fg.deobf("mezz.jei:jei-${mc_version}:${jei_version}:api") // 将 JEI API 添加为编译时依赖项
    // runtimeOnly fg.deobf("mezz.jei:jei-${mc_version}:${jei_version}") // 将完整的 JEI 模组添加为运行时依赖项
    // implementation fg.deobf("com.tterrag.registrate:Registrate:MC${mc_version}-${registrate_version}") // 将 Registrate 添加为依赖项

    // 使用 ./libs 中的模组 jar 的示例
    // implementation fg.deobf("blank:coolmod-${mc_version}:${coolmod_version}")

    // 更多信息...
    // http://www.gradle.org/docs/current/userguide/artifact_dependencies_tutorial.html
    // http://www.gradle.org/docs/current/userguide/dependency_management.html
}

// 示例以将属性添加到清单以供运行时读取。
jar {
    manifest {
        attributes([
                "Specification-Title"     : "examplemod",
                "Specification-Vendor"    : "examplemodsareus",
                "Specification-Version"   : "1", // 我们是我们自己的版本 1
                "Implementation-Title"    : project.name,
                "Implementation-Version"  : project.jar.archiveVersion,
                "Implementation-Vendor"   : "examplemodsareus",
                "Implementation-Timestamp": new Date().format("yyyy-MM-dd'T'HH:mm:ssZ")
        ])
    }
}

// 示例配置以使用 maven-publish 插件进行发布
// 这是重新混淆 jar 文件的首选方法
jar.finalizedBy('reobfJar')
// 但是,如果您处于多项目构建中,开发时间需要未混淆的 jar 文件,因此您可以在发布时延迟混淆,方法是
// publish.dependsOn('reobfJar')

publishing {
    publications {
        mavenJava(MavenPublication) {
            artifact jar
        }
    }
    repositories {
        maven {
            url "file://${project.projectDir}/mcmodsrepo"
        }
    }
}

tasks.withType(JavaCompile).configureEach {
    options.encoding = 'UTF-8' // 使用 UTF-8 字符集进行 Java 编译
}

然后点击大象(🐘),点击Tasks,build,build

如果是新手的话这么设置可以,但是你想加模组前置之类和mixin的看后续的应用篇。

新手篇

如果你能正常的启动我的世界的话,恭喜你!你有资格创造你的模组,接下来才是刚开始!!!

写进入新的世界的时候的信息

如图,我的例子

package github.com.gengyoubo.changedplus;

import net.minecraft.Util;
import net.minecraft.network.chat.TextComponent;
import net.minecraft.world.entity.player.Player;
import net.minecraft.world.level.Level;
import net.minecraftforge.event.entity.player.PlayerEvent;
import net.minecraftforge.eventbus.api.SubscribeEvent;
import net.minecraftforge.fml.common.Mod;

@Mod("changedplus")
@Mod.EventBusSubscriber
//public 公用的函数
//class 属性
//Main 函数的名字,这里是文件的名字,对应了Main.java
public class Main  {

    @SubscribeEvent
    //static 静态函数
    //void 从这里开始调用
    public static void playerJoinWorld(PlayerEvent.PlayerLoggedInEvent event){
        Player player = event.getPlayer();
        Level level =player.level;
        //从这里写进入世界的时候的信息
        //player.sendMessage(new TextComponent("信息"), Util.NIL_UUID);
        player.sendMessage(new TextComponent("欢迎来到胶兽的世界!"+player.getDisplayName().getString()+",你准备好被兽化了吗?"), Util.NIL_UUID);
        //如果想添加玩家的名称时,需要在"的外面写+player.getDisplayName().getString()+
    }
}

注意,将文本直接写进代码里的叫做硬编码。我不怎么推荐将文本直接写入文本,这个时候就要用到本地化键了。(在基础篇会写)

写一个物品

想要制作一个物品需要

编写物品属性

注册物品

注册物品属性

建模

我来一个一个教

编写物品属性

例子

package github.com.gengyoubo.changedplus.item;

import net.minecraft.world.item.Item;

// 单个物品函数
public class Latex extends Item {
    public Latex()
    {
        super(new Item.Properties().tab(ChangedPlusTab.getInstance()));
    }

}

你需要创建这样的格式

src.main.java.你的域名.item.物品名.java

注册物品(属性)

你可以一个一个注册,但是你要是觉得麻烦可以制造一个注册表

package github.com.gengyoubo.changedplus.item;

import net.minecraft.world.item.Item;
import net.minecraftforge.event.RegistryEvent;
import net.minecraftforge.eventbus.api.SubscribeEvent;
import net.minecraftforge.fml.common.Mod;

// 物品注册表
@Mod.EventBusSubscriber(modid = "changedplus", bus = Mod.EventBusSubscriber.Bus.MOD)
public class ModEventSubscriber {
    // 注册物品
    @SubscribeEvent
    public static void onRegisterItems(final RegistryEvent.Register<Item> event) {
        registerItem(event, new Latex_sword(), "latex_sword");
        registerItem(event, new Latex(), "latex");
        registerItem(event, new latex_stick(), "latex_stick");
        registerItem(event, new long_latex_stick(), "long_latex_stick");
        // 以registerItem的格式注册物品↑
    }
    //一键注册物品的属性
    private static void registerItem(RegistryEvent.Register<Item> event, Item item, String name) {
        item.setRegistryName("changedplus", name);
        event.getRegistry().register(item);
    }
}

太便利了,这个直接套公式呀!!!

文件路径和上面同理

但是这种只支持物品的注册,但是不适应与物品以外的。详细请看注册事件

建模

如何将贴图导入至你做的模组 - [Forge]Minecraft Forge - MC百科|最大的Minecraft中文MOD百科

配方

现在的版本有这些配方可以让你使用

合成

熔炼

酿造

高炉熔炼

烟熏烹饪

切割

篝火烹饪

锻造

接下来我将介绍它的位置和如何写

合成

所有配方(酿造除外)的位置都统一在

src/main/resources/data/你的模组ID/recipes/

而酿造的位置在

src/main/java/模组域名/recipes/brewing/

配方写法

合成

合成有有序合成和无序合成。有序合成是顺序是固定的不能改。无序合成就是顺序无所谓,在哪里都可以。

有序合成

{
  "type": "minecraft:crafting_shaped",
  "pattern": [
    "XXX",
    "XXX",
    "XXX"
  ],
  "key": {
    "X": {
      "item": "minecraft:oak_wood"
    }
  },
  "result": {
    "item": "minecraft:deepslate_gold_ore",
    "count": 1
  }
}

"key": {
   "X": {
     "item": "minecraft:oak_wood"
   }

}

需要注意的是如果是模组的物品时候,需要用模组ID:物品ID来填写。

key里头最大可以写9种不同的物品(字母也行,数字也行)。

"result": {
   "item": "minecraft:deepslate_gold_ore",
   "count": 1
 }

"count": 1里写合成出来的时候会产生多少的成品。

无序合成

{
 "type": "minecraft:crafting_shapeless",
 "ingredients": [
   {
     "item": "minecraft:oak_wood"
   }
 ],
 "result": {
   "item": "minecraft:deepslate_gold_ore",
   "count": 1
 }
}

 "ingredients": [
   {
     "item": "minecraft:oak_wood"
   }
 ]

ingredients里可以用"item": "模组ID:物品ID"的格式,可以设置9种物品。

熔炼

{
  "type": "minecraft:smelting",
  "experience": 1,
  "cookingtime": 200,
  "ingredient": {
    "item": "minecraft:waxed_weathered_cut_copper"
  },
  "result": "minecraft:deepslate_gold_ore"
}

"experience": 1 获得1点经验值(真抠)。

"cookingtime": 200 燃烧200刻(ticks)。1秒等于20刻,所以200刻等于10秒。

酿造

package net.mcreator.g.recipes.brewing;



@Mod.EventBusSubscriber(bus = Mod.EventBusSubscriber.Bus.MOD)
public class PpBrewingRecipe implements IBrewingRecipe {

    @SubscribeEvent
    public static void init(FMLCommonSetupEvent event) {
        event.enqueueWork(() -> BrewingRecipeRegistry.addRecipe(new PpBrewingRecipe()));
    }

    @Override
    public boolean isInput(ItemStack input) {
        return Ingredient.of(new ItemStack(Blocks.STRIPPED_BIRCH_LOG)).test(input);
    }

    @Override
    public boolean isIngredient(ItemStack ingredient) {
        return Ingredient.of(new ItemStack(Blocks.OAK_PLANKS)).test(ingredient);
    }

    @Override
    public ItemStack getOutput(ItemStack input, ItemStack ingredient) {
        if (isInput(input) && isIngredient(ingredient)) {
            return new ItemStack(Blocks.CUT_SANDSTONE);
        }
        return ItemStack.EMPTY;
    }

}

如何制作并且维护你的mod?(forge版)(1.18.X,1.19.X)-第1张图片

(虽然在实际演示里使用了ItemStack(Block.something),但是也可以使用ItemStack(Item.something),尤其是中间的代码必须是药水类。

至于如何把ItemStack(Block.something)或者是ItemStack(Item.something)改成ItemStack(注册器.someting)会在基础篇介绍。

需要这些包

import net.minecraftforge.fml.event.lifecycle.FMLCommonSetupEvent;
import net.minecraftforge.fml.common.Mod;
import net.minecraftforge.eventbus.api.SubscribeEvent;
import net.minecraftforge.common.brewing.IBrewingRecipe;
import net.minecraftforge.common.brewing.BrewingRecipeRegistry;
import net.minecraft.world.level.block.Blocks;
import net.minecraft.world.item.crafting.Ingredient;
import net.minecraft.world.item.ItemStack;

高炉熔炼

{
  "type": "minecraft:blasting",
  "experience": 1,
  "cookingtime": 200,
  "ingredient": {
    "item": "minecraft:"
  },
  "result": "minecraft:"
}

和熔炉同理。

烟熏烹饪

{
  "type": "minecraft:smoking",
  "experience": 1,
  "cookingtime": 200,
  "ingredient": {
    "item": "minecraft:"
  },
  "result": "minecraft:"
}

和熔炉同理。

切割

{
  "type": "minecraft:stonecutting",
  "count": 1,
  "ingredient": {
    "item": "minecraft:"
  },
  "result": "minecraft:"
}

和熔炼的构造基本一样。

篝火烹饪

{
  "type": "minecraft:campfire_cooking",
  "experience": 1,
  "cookingtime": 200,
  "ingredient": {
    "item": "minecraft:"
  },
  "result": "minecraft:"
}

和熔炉同理。

锻造

{
  "type": "minecraft:smithing",
  "base": {
    "item": "minecraft:"
  },
  "addition": {
    "item": "minecraft:"
  },
  "result": {
    "item": "minecraft:"
  }
}

如何制作并且维护你的mod?(forge版)(1.18.X,1.19.X)-第2张图片

左边是base

中间是addition

右边是result

矿物标签

Q:矿物词典(矿物标签)是什么?

A:矿物词典(矿物标签)是为了统一相同物品却不一样模组的东西,比如说原版的铁矿和别的铁矿想要应用到配方时就需要矿物词典(矿物标签)来进行合成。虽然写了“矿物”,但是除了矿物还可以统一其他的东西,比如说箱子,原木等等。

Q:矿物词典和新出的Tags(矿物标签)有什么区别

在1.13之前的版本,forge提供了"矿物词典"让我们使用,在1.13和更高的版本里矿物词典变成了"Tags(矿物标签)"

想要使用矿物词典的时候需要专门注册一个矿物词典类,然后只能局限于物品的合成或者是熔炼。

OreDictionary.registerOre("oreCopper", new ItemStack(ModItems.copper_ore));
{
  "type": "minecraft:crafting_shaped",
  "pattern": [
    "###",
    "###",
    "###"
  ],
  "key": {
    "#": {
      "item": "oreDict:copperIngot"
    }
  },
  "result": {
    "item": "your_mod:copper_block",
    "count": 1
  }
}

在Tags(矿物标签)里,只需要写json就可以实现类似矿物词典的功能,并且不仅能应用于合成,并且还可以用于实体(Entities)、流体(Fluids)等多种类型的游戏元素。但是我并没有逝过注册实体和流体,毕竟谁有人用实体或者是流体来注册矿物词典?

{
  "replace": false,
  "values": [
    "your_mod:copper_ore",
    "other_mod:copper_ore"
  ]
}
{
  "type": "minecraft:crafting_shaped",
  "pattern": [
    "###",
    "###",
    "###"
  ],
  "key": {
    "#": {
      "tag": "forge:ingots/copper"
    }
  },
  "result": {
    "item": "your_mod:copper_block",
    "count": 1
  }
}

也许你已经发现了区别了,前者是"item": "oreDict:copperIngot",后者是 "tag": "forge:ingots/copper"(oreDict,ingots可以自己命名)。

所以当没有作用的时候优先排查是否是"tag": "forge:ingots/copper"的格式。

在后续里将使用矿物标签来代表新版的矿物词典

位置

首先需要想一个矿物标签的名字,如果实在是想不出来的话,就用ores代替吧

首先它的位置在

src/main/resources/data/forge/tags/items/ores/something.json

然后something里你需要替换,比如说你要填diamond的矿物词典的话就写diamond.json就行。

然后diamond.json的基本格式为

{
  "replace": false,
  "values": [
    "模组ID:物品 ID"
  ]}

"replace": false   一般设置为 false,表示不会替换已存在的 Tag。(一般都会省略)

"values": [
   "模组ID:物品 ID"
 ]}

列出所有应被归类为该 Tag 的物品 ID。如果这个 Tag 是从其他模组借用的,确保相应的物品已经正确添加到 values 列表中。

当然,values里可不是只有一个的,如果有很多个同样的物品的话就在模组ID的下面里在添加一个吧。

配方

在输入里需要把"item": "your_mod:something"改成"tag": "forge:ores/something"

第二的something里需要把它改成.json的前面的名字。

基础篇

基础篇里主要讲的是比较难,但是必须要会的地方!!!

事件

事件在英语里称event。有注册事件(Registey Event)和监听事件(Listener Event)

这些事件通常都放在init(初始化)文件夹里,但是有些事件可以手动注册也可以自动注册(比如某些注册事件和所有的监听事件),但是有些需要手动注册(物品或者是方块(实体)的注册事件)。虽然物品也可以自动注册,但是难以引用。所以我推荐用手动注册。

本教程为小教程,分为非订阅事件和订阅事件。

非订阅事件

非订阅事件就是需要手动注册的事件。在模组的主文件夹中是

public Minecraft_Science() {
    IEventBus bus = FMLJavaModLoadingContext.get().getModEventBus();
    ModLoadingContext.get().registerConfig(ModConfig.Type.COMMON, Minecraft_Science_Config.CONFIG);

    // 注册其他内容
    ModEventSubscriber_Block.REGISTRY.register(bus);
    ModEventSubscriber_BlockItem.REGISTRY.register(bus);
    MCSEFeatures.REGISTRY.register(bus);
    MCSEBlockEntities.REGISTRY.register(bus);

    // 注册客户端事件
    bus.addListener(MCSEScreens::clientSetup);
}

首先我们需要知道bus是什么意思?bus也就是event bus 就是事件总线,用于手动注册。

下一个行的配置文件先不用管

手动注册事件的格式通常为

(init的文件).REGISTRY.register(bus)

这里可以是物品,方块等。通常用于注册字段。

在新手篇中我们写了物品的具体信息。但是如何手动注册呢?

物品

public static final DeferredRegister<Item> REGISTRY = DeferredRegister.create(ForgeRegistries.ITEMS, "mcse");
public static final RegistryObject<Item> Lowercase_a = REGISTRY.register("lowercase_a", github.com.gengyoubo.Minecraft_Science.Item.Alphabet.Lowercase.Lowercase_a::new);
private static void registerItem(RegistryEvent.Register<Item> event, Item item, String name) {
    item.setRegistryName("mcse", name);
    event.getRegistry().register(item);
}

首先REGISTRY是不能改的,然后上面写的mcse可以改成你的模组ID,然后需要赋值的字段名可以写你要创造的物品名。记住,写""里面的东西是一定要是小写。(我吃了不少亏)

github.com.gengyoubo.Minecraft_Science.Item.Alphabet.Lowercase.Lowercase_a是物品信息的路径。

方块

public static final DeferredRegister<Block> REGISTRY = DeferredRegister.create(ForgeRegistries.BLOCKS, "mcse");
public static final RegistryObject<Block> WORD_ORE = REGISTRY.register("word_ore", Word_Ore::new);

方块物品

public static final DeferredRegister<Item> REGISTRY = DeferredRegister.create(ForgeRegistries.ITEMS, "mcse");
public static final RegistryObject<Item> WORD_ORE = block(ModEventSubscriber_Block.WORD_ORE, GENERIC_TAB.getInstance());
private static RegistryObject<Item> block(RegistryObject<Block> block, CreativeModeTab tab) {
    return REGISTRY.register(block.getId().getPath(), () -> new BlockItem(block.get(), new Item.Properties().tab(tab)));
}

需要注意的是引用是需要去init的方块去引用

方块实体

public static final DeferredRegister<BlockEntityType<?>> REGISTRY = DeferredRegister.create(ForgeRegistries.BLOCK_ENTITIES, "mcse");
public static final RegistryObject<BlockEntityType<?>> SCRT = register("scrt", ModEventSubscriber_Block.SCRT, SingleChemicalReactionTableBlockEntity::new);
private static RegistryObject<BlockEntityType<?>> register(String registryname, RegistryObject<Block> block, BlockEntityType.BlockEntitySupplier<?> supplier) {
    return REGISTRY.register(registryname, () -> BlockEntityType.Builder.of(supplier, block.get()).build(null));
}

订阅事件

订阅事件就是大部分的不需要手动注册了,通常用于监听事件和某些注册事件

@SubscribeEvent

@SubscribeEvent 是 Minecraft Forge 提供的一个注解,用于标记需要处理事件的方法。它通常与 Forge 的事件总线(Event Bus)一起使用,用于监听和响应各种游戏事件。

  • 方块事件:BlockEvent

  • 实体事件:EntityEvent

  • 玩家事件:PlayerEvent

  • 世界事件:WorldEvent

  • 物品事件:ItemEvent

  • GUI 事件:GuiEvent

  • 输入事件:InputEvent

  • 生命周期开启事件(也可以叫初始化事件):LifecycleEvent

  • 网络相关事件:NetworkEvent

  • 渲染相关事件:RenderEvent

  • 其他的事件:(ServerTickEvent,ClientTickEvent)

  • 自定义事件:(下面会讲)

等,最基本的就是这些。但是并非所有的都可以用自动注册


字段非静态方法静态方法
自动注册需要手动注册可以被@SubscribeEvent注解,但是需要手动注册可以被@SubscribeEvent注解,并且可以被自动注册

并且有一些事件可以被取消(比如说死亡事件,物品燃烧事件等)

本地化键

前言

你在游玩的时候有没有用过自动汉化更新模组,它的原理就是进入游戏的时候把汉化包(带有本地化键的资源包)下载出来。然后你只需要加载这个整合包就好,其实这些都是背后的汉化大佬的帮助才能游玩汉化过的模组。

在本地化键里,你不仅要学会如何写java,你还会写json。但是不要担心,你只需要会写文本路径就好。

格式

在本地化键里有两种,一个是有路径的本地化键,一个是没有路径的本地化键。

有路径的是这么写的

"item.changedplus.latex_sword": "胶剑"

这个是指定的,不能随便改

没有路径的是这么写的

"message.welcome": "欢迎来到胶兽的世界! %s, 你准备好被兽化了吗?",

这个只要按照规定去写,就不会出现大问题

还有比较特殊的

"item.changedplus.latex_sword.tooltip": "当前的攻击力: %s",

这时候有人会想“啊?这里哪里特殊了?这不是和有路径的一模一样吗?”但是你先别急,你看代码。

//部分片段
@Override
public void appendHoverText(ItemStack stack, @Nullable Level world, List<Component> tooltip, TooltipFlag flag) {
    super.appendHoverText(stack, world, tooltip, flag);
    CompoundTag tag = stack.getTag();
    int currentAttackDamage = BASE_ATTACK_DAMAGE;
    if (tag != null && tag.contains("CurrentAttackDamage")) {
        currentAttackDamage = tag.getInt("CurrentAttackDamage");
    }
    tooltip.add(new TranslatableComponent("item.changedplus.latex_sword.tooltip",5 + currentAttackDamage));
}
//部分片段

这个和前两个有什么不一样的?

前半部分的item.changedplus.latex_sword是固定的。

后半部分的tooltip是可以改的。

十分的复杂,但是为什么没在应用篇就是因为这个还不算复杂,只要知道写法就能用,从这里正式的开始如何写本地化键

有路径的本地化键

有路径的基本格式是这样的

(总类).(模组ID).(名称).(自定义)

总类:模组的总类比如说物品就是item,方块就是block,成就就是advancements,还有创造模式选项卡itemGroup

模组ID:基于你的文件夹的名称

名称:物品的名称,方块的名称等等(如果没有可以不用写,比如说创造模式选项卡,成就等)

自定义:这里有一点难,和其他的不一样。如果是写物品的详细信息的话,它就是这种格式

"item.changedplus.long_latex_stick.detailed_info": "给生物施加击退5和1分钟的缓慢5"

如果是成就的话它就是这种格式

"advancements.changedplus.root.title": "欢迎来到changedplus"

不知道?那么看下一个

特殊的有路径的本地化键

给物品详细信息的物品的本地化键(这里只是介绍一下,在应用篇里会教你如何写)

"item.changedplus.long_latex_stick.detailed_info": "给生物施加击退5和1分钟的缓慢5"

成就(如何写成就请看新手篇)

我们来复习一下,在成就里有根成就root和子成就child,它们的写法是

advancements.changedplus.root/child.title/description

title就是成就的标题

description就是成就的副标题

没有路径的本地化键

(自定义).(自定义).(自定义).(自定义).......

就像这样,你可以自己起名字

写好的本地化键需要放哪里?

确认一下你的格式是否是json格式然后放进

src/main/resources/assets/lang

这里需要两个json,需要按照下面的起名字,缺一个都不行

一个是英语en.us.json(英语的本地化键)

一个是中文zh.cn.json(中文的本地化键)

都有哪个国家的本地化键

(引用语言 - 中文 Minecraft Wiki

南非荷兰语af_za

阿拉伯语ar_sa

阿斯图里亚斯语ast_es

阿塞拜疆语az_az

巴什基尔语ba_ru

巴伐利亚语bar

白俄罗斯语be_by

保加利亚语bg_bg

布列塔尼语br_fr

布拉邦特语brb

波斯尼亚语bs_ba

加泰罗尼亚语ca_es

捷克语cs_cz

威尔士语cy_gb

丹麦语da_dk

奥地利德语de_at

瑞士德语de_ch

德语de_de

希腊语el_gr

澳大利亚英语en_au

加拿大英语en_ca

英式英语en_gb

新西兰英语en_nz

海盗英语en_pt

颠倒英语en_ud

美式英语en_us

纯粹英语enp

莎士比亚风格英语enws

世界语eo_uy

阿根廷西班牙语es_ar

智利西班牙语es_cl

厄瓜多尔西班牙语es_ec

西班牙语es_es

墨西哥西班牙语es_mx

乌拉圭西班牙语es_uy

委内瑞拉西班牙语es_ve

安达卢西亚语esan

爱沙尼亚语et_ee

巴斯克语eu_es

波斯语fa_ir

芬兰语fi_fi

菲律宾语fil_ph

法罗语fo_fo

加拿大法语fr_ca

法语fr_fr

东法兰克语fra_de

弗留利语fur_it

弗里斯兰语fy_nl

爱尔兰语ga_ie

苏格兰盖尔语gd_gb

加利西亚语gl_es

夏威夷语haw_us

希伯来语he_il

印地语hi_in

克罗地亚语hr_hr

匈牙利语hu_hu

亚美尼亚语hy_am

印尼语id_id

伊博语ig_ng

伊多语io_en

冰岛语is_is

斯拉夫共通语isv

意大利语it_it

日语ja_jp

逻辑语jbo_en

格鲁吉亚语ka_ge

哈萨克语kk_kz

卡纳达语kn_in

韩语/朝鲜语ko_kr

科隆语/利普里安语ksh

康沃尔语kw_gb

拉丁语la_la

卢森堡语lb_lu

林堡语li_li

伦巴第语lmo

老挝语lo_la

小猫皮钦语lol_us

立陶宛语lt_lt

拉脱维亚语lv_lv

文言文lzh

马其顿语mk_mk

蒙古语mn_mn

马来语ms_my

马耳他语mt_mt

纳瓦特尔语nah

低地德语nds_de

弗拉芒语nl_be

荷兰语nl_nl

挪威尼诺斯克语nn_no

巴克摩挪威语no_no

奥克语oc_fr

艾尔夫达伦语ovd

波兰语pl_pl

巴西葡萄牙语pt_br

葡萄牙语pt_pt

昆雅语qya_aa

罗马尼亚语ro_ro

俄语(革命前)rpr

俄语ru_ru

卢森尼亚语ry_ua

雅库特语sah_sah

北萨米语se_no

斯洛伐克语sk_sk

斯洛文尼亚语sl_si

索马里语so_so

阿尔巴尼亚语sq_al

塞尔维亚语(拉丁字母)sr_cs

塞尔维亚语(西里尔字母)sr_sp

瑞典语sv_se

上萨克森德语sxu

西里西亚语szl

泰米尔语ta_in

泰语th_th

他加禄语tl_ph

克林贡语tlh_aa

道本语tok

土耳其语tr_tr

鞑靼语tt_ru

乌克兰语uk_ua

瓦伦西亚语val_es

威尼斯语vec_it

越南语vi_vn

维奥沙语vp_vl

意第绪语yi_de

约鲁巴语yo_ng

简体中文zh_cn

繁体中文(香港)zh_hk

繁体中文(台湾)zh_tw

马来语(爪夷文)zlm_arab

配置文件

public class Config {

    public static class Common {
        public Common(ForgeConfigSpec.Builder builder){
        }

    }
    public static class Client {
        public Client(ForgeConfigSpec.Builder builder){
        }
    }
    public static class Server {
        public Server(ForgeConfigSpec.Builder builder){
        }
    }
    private final Pair<Common, ForgeConfigSpec> commonPair;
    private final Pair<Client, ForgeConfigSpec> clientPair;
    private final Pair<Server, ForgeConfigSpec> serverPair;
    public final Common common;
    public final Client client;
    public final Server server;

    public Config(ModLoadingContext context) {
        commonPair = new ForgeConfigSpec.Builder()
                .configure(Common::new);
        clientPair = new ForgeConfigSpec.Builder()
                .configure(Client::new);
        serverPair = new ForgeConfigSpec.Builder()
                .configure(Server::new);

        context.registerConfig(ModConfig.Type.COMMON, commonPair.getRight());
        context.registerConfig(ModConfig.Type.CLIENT, clientPair.getRight());
        context.registerConfig(ModConfig.Type.SERVER, serverPair.getRight());
        common = commonPair.getLeft();
        client = clientPair.getLeft();
        server = serverPair.getLeft();
    }
}

你是不是在想我在说什么,其实配置文件格式被分为通用,客户端和服务端。这种格式甚至在高版本都可以用。

然后这个当然要注册了,它的注册方法是。

//例子
@Mod("la")
public class La {
    public static Config config;

    public La(){
        config = new Config(ModLoadingContext.get());
    }
}

特别简单,而且容易。我其实要讲的也就是关于配置文件的语法而已了。

这里需要注意的是pair的包是org.apache.commons.lang3.tuple.Pair;

配置文件本身的语法

首先定义字段时必须要是包装类型的。什么?你不知道我在说什么?

比如说int和Integer的特性都是上限为2^31-1。但是一个是基本类型,另外一个是包装类型。额,你不知道?

  • 基本类型:默认为0,Unicode值为'\u0000',布尔值为false。

  • 包装类型:默认为null。

你可以参考一下这个表

如何制作并且维护你的mod?(forge版)(1.18.X,1.19.X)-第3张图片

但是有例外。

如何制作并且维护你的mod?(forge版)(1.18.X,1.19.X)-第4张图片如果是聪明的你一定能发现这个Void和void有点相似。

如何制作并且维护你的mod?(forge版)(1.18.X,1.19.X)-第5张图片但是并不常见......。

然后定义字段时。

public final ForgeConfigSpec.ConfigValue<Integer> Example;
那么这里必须是包装类,但是是所有封装类吗?并不是。必须是基本类型与包装类型是对应的才行。

如何制作并且维护你的mod?(forge版)(1.18.X,1.19.X)-第6张图片如何制作并且维护你的mod?(forge版)(1.18.X,1.19.X)-第7张图片如何制作并且维护你的mod?(forge版)(1.18.X,1.19.X)-第8张图片

ForgeConfigSpec.ConfigValue<List<String>> exampleList = builder
    .comment("This is a list of strings.")
    .defineList("example.list", Arrays.asList("a", "b", "c"), obj -> obj instanceof String);

如何制作并且维护你的mod?(forge版)(1.18.X,1.19.X)-第9张图片如何制作并且维护你的mod?(forge版)(1.18.X,1.19.X)-第10张图片如何制作并且维护你的mod?(forge版)(1.18.X,1.19.X)-第11张图片如何制作并且维护你的mod?(forge版)(1.18.X,1.19.X)-第12张图片如何制作并且维护你的mod?(forge版)(1.18.X,1.19.X)-第13张图片

引用配置文件的语法

引用时,使用

模组主文件夹.config.(在common/server/client中选一个).字段.get()

就行。

应用篇

你要是学到了这里,那就说明你可以制作简单的模组了,恭喜你,但是想要制作高质量的模组需要更难的知识。在应用篇里,你会接触到特别复杂的函数(比如说只用特定的种族的生物可以使用某某),加油吧!

种族识别

前言

为什么会有种族识别?这个真的会用吗?你是否是这么想的?我也以为这个不需要,但是制造某某专属武器之类的东西就需要种族识别函数,比如说如果你是人类的话武器的攻击力才10点,但是胶兽使用武器的话攻击力就是100点,看起来很难,其实一点也不简单废话文学(确信,首先需要做支持胶兽的附属模组,然后需要写判断,然后还要写攻击力。对了,攻击力是无法直接修改的,需要依靠动态dmg来修改,动态dmg的话后面会讲.

条件函数

例子:如果持有武器的人是胶兽,返回A。如果不是,返回B

public void check(LivingEntity entity) {
        LatexVariant<?> variant = LatexVariant.getEntityVariant(entity);
        if (variant != null) {
        info("Entity variant exists for entity: {}", entity.getName().getString());
        } else {
        info("Entity variant exists for entity: {}", entity.getName().getString());         
        }
    }

如果需要引用函数,需要用event.

动态dmg

前言

有一个人想修改武器的攻击力,但是修改不了,为什么?因为物品的基本信息包括攻击力已经被注册了,不能再修改了,那该怎么办?于是他就想,我不去修改武器的攻击力,只要写攻击力增加或者是减少了多少,这不就变相的修改了武器的攻击力了吗?这就是动态dmg!

在这里我会教你如何写动态dmg。

但是写好的动态dmg不能直接用,需要有个接受动态dmg的函数。

动态dmg

需要的代码

武器本身的攻击力和提升的攻击力

识别函数

事件监听函数

NBT函数

武器本身的攻击力和提升的攻击力

必须是静态final,所以实际应该是这么写的

public class Latex_sword extends SwordItem {
    private static final int BASE_ATTACK_DAMAGE = 10; // 基础攻击力
    private static final int ADDITIONAL_ATTACK_DAMAGE = 25; // 额外攻击力

BASE_ATTACK_DAMAGE会在武器信息里调用,ADDITIONAL_ATTACK_DAMAGE会在之后的函数调用。

识别函数

在种族识别中,我教你如何写识别函数,把它利用起来

@SubscribeEvent
public static void onEntityHurt(LivingHurtEvent event) {
    if (event.getSource().getDirectEntity() instanceof LivingEntity) {
        LivingEntity attacker = (LivingEntity) event.getSource().getDirectEntity();
        Item item = attacker.getMainHandItem().getItem();
        //下面是种族识别函数
        if (item instanceof Latex_sword) {
            LatexVariant<?> variant = LatexVariant.getEntityVariant(attacker);
            if (variant != null) {
                // 如果攻击者是latex player或者是NPC latex,增加攻击力
                event.setAmount(event.getAmount() + ADDITIONAL_ATTACK_DAMAGE);
            }
        }
    }
}

事件监听函数

实时刷新当前的攻击力

@SubscribeEvent
public static void onPlayerTick(TickEvent.PlayerTickEvent event) {
    if (event.phase == TickEvent.Phase.END) {
        ItemStack stack = event.player.getMainHandItem();
        if (stack.getItem() instanceof Latex_sword) {
            updateAttackDamage(stack, event.player);
        }
    }
}

NBT函数

给武器添加伤害的幅度

private static void updateAttackDamage(ItemStack stack, LivingEntity entity) {
    int currentAttackDamage = calculateCurrentAttackDamage(entity);

    CompoundTag tag = stack.getOrCreateTag();
    tag.putInt("CurrentAttackDamage", currentAttackDamage);
    stack.setTag(tag);
}

条件函数

引用了识别函数

private static int calculateCurrentAttackDamage(LivingEntity entity) {
    int currentAttackDamage = BASE_ATTACK_DAMAGE;
    if (LatexVariant.getEntityVariant(entity) != null) {
        currentAttackDamage += ADDITIONAL_ATTACK_DAMAGE;
    }
    return currentAttackDamage;
}

JAVA篇

这个教程是一个如何写java的教程

理论篇

这里学习java的知识,但是内容复杂,建议从实战篇开始学!

第零章

在java里有5种的类

Class类(属性)也叫C类,大部分模组的java文件就是C类。

Interface类(接口)也叫I类,不怎么用。

Record类(记录)也叫R类。
Enum类(枚举)也叫E类。

@interface类(注解)也叫@类。
C类

C类的基本写法是这样的

public class C {
}

I类

I类的基本写法是这样的

public interface I {
}

R类

R类的基本写法是这样的

public record R() {
}

E类

E类的基本写法是这样的

public enum E {
}

@类

@类的基本写法是这样的

public @interface AT {
}

第一章

第一节(基本概念1)

在学习JAVA之前一定要了解这些概念。

对象:对象是类的一个实例,有状态和行为。

类:类是一个模板,它描述一类对象的行为和状态。

方法:方法就是行为,一个类可以有很多方法。

实例变量:每个对象都有独特的实例变量,对象的状态由这些实例变量的值决定。

用图描述就是这样

类----对象------方法
 |          |--------方法
 |          |--------方法
 |          |--------实例变量
 |          |--------实例变量
 |          
 |------对象

Java 所有的组成部分都需要名字。类名、变量名以及方法名都被称为标识符。

关于 Java 标识符,有以下几点需要注意:

所有的标识符都应该以字母(A-Z 或者 a-z),美元符($)、或者下划线(_)开始

Apple

首字符之后可以是字母(A-Z 或者 a-z),美元符($)、下划线(_)或数字的任何字符组合

Apple

关键字不能用作标识符

public

标识符是大小写敏感的

Apple

apple

ApPlE

aPpLe

Java可以使用修饰符来修饰类中方法和属性。主要有两类修饰符:

访问控制修饰符 : default, public , protected, private

非访问控制修饰符 : final, abstract, static, synchronized

Java 中主要有如下几种类型的变量

局部变量

类变量(静态变量)

成员变量(非静态变量)

数组是储存在堆上的对象,可以保存多个同类型变量

这个会在后面详细说明

Java 5.0引入了枚举,枚举限制变量只能是预先设定好的值。使用枚举可以减少代码中的 bug。

第一节(JAVA关键字)


如何制作并且维护你的mod?(forge版)(1.18.X,1.19.X)-第14张图片

如何制作并且维护你的mod?(forge版)(1.18.X,1.19.X)-第15张图片

如何制作并且维护你的mod?(forge版)(1.18.X,1.19.X)-第16张图片引用:https://www.runoob.com/java/java-basic-syntax.html

第一节(注释)

注释是很重要的东西,可以说明这个方法是如何运作的。有三种方法.

/* *
    *  这是第一种
    */
 
 
 
 //这是第三种·

第一节(基本概念2)

在 Java 中,一个类可以由其他类派生。如果你要创建一个类,而且已经存在一个类具有你所需要的属性或方法,那么你可以将新创建的类继承该类。

利用继承的方法,可以重用已存在类的方法和属性,而不用重写这些代码。被继承的类称为超类(super class),派生类称为子类(sub class)

没错!super man的那个超类

在 Java 中,接口可理解为对象间相互通信的协议。接口在继承中扮演着很重要的角色。

接口只定义派生要用到的方法,但是方法的具体实现完全取决于派生类。

第二节(对象和类1)

那么在第一节里说了对象是什么,但是究竟是什么?我举个例子

比如有这样一个文章

我买了很多水果,有苹果香蕉......

水果就是类

苹果香蕉就是对象

那么还有这样的文章

苹果,很种在土里可以长出苹果树......

苹果的状态(也就是特征)有

苹果的行为(也就是特性)有种在土里可以长出苹果树

那么在代码方面是什么呢?

A:对象的状态就是属性,行为通过方法体现。

我在第一节讲过方法就是行为那么,那么实例变量又是什么?

A:状态

第二节(对象和类2)

一个类可以包含以下类型变量:

  • 局部变量:在方法、构造方法或者语句块中定义的变量被称为局部变量。变量声明和初始化都是在方法中,方法结束后,变量就会自动销毁。

  • 成员变量:成员变量是定义在类中,方法体之外的变量。这种变量在创建对象的时候实例化。成员变量可以被类中方法、构造方法和特定类的语句块访问。

  • 类变量:类变量也声明在类中,方法体之外,但必须声明为 static 类型。

实战篇

在这里进行实际的操作!内容简单,适合新手学!也可以当作复习用!

第0章(需要准备的东西)

你需要新创建一个文件夹,给文件夹起个名字(gengyoubo),然后用IDEA把文件夹打开,创造一个Main.java

第1章

第1节(java的基本知识)

class Main {
  public static void main(String[] args) {
    System.out.println("");
  }
}

把""里的替换成"Hello World"吧!如果代码没有问题,将会输出

Hello World

第2节

在这里我们需要知道学习java的时候必须要记住的东西!

首先想要输出"耿悠博"的话,代码应该是

System.out.println("耿悠博");

而不是

System.out.println(耿悠博);

因为在java里,没有耿优博的代码,所以需要用""让系统知道你想要输出的字符串。

这个代码(System.out.println())的意思是[输出()里的东西]

最重要的就是,代码的最后需要加;

第3节

在java里有这个代码

System.out.println(114514);
System.out.println("114514");

如果运行上面的代码将会以数值输出114514,而运行下面的代码会以字符串输出114514。

数值可以计算,但是字符串不行,所以需要转换(在第5节中讲)

在java里有这样的运算符

a+b

a-b

a*b

a/b

a%b

从左往右介绍。计算a+b,计算a-b,计算a*(乘)b,计算a/(除)b,计算a/b的余。(在理论篇里会讲)

第4节

在这里,讲解+的另外一种用处,那就是连接字符串。

比如"我是"+"耿悠博",就会输出 我是耿悠博

第5节(变量)

"耿悠博"是属于字符串(String)型,114514是属于整数(int)型(有其他的代替方法,但是这节不讲其他的方法)

这些"型"需要定义:

String name;

int number;

如果是字符串的变量的话需要在前面加String,如果是数值的话就是int(浮点数是例外)

然后在再下一行写变量里的东西

String name;
name ="耿悠博";
int number;
number =114514;

能不能再简单的写呢?

String name="耿悠博";
int number=114514;

定义变量的同时赋值的行为叫做初始化,然后定义完的变量可以直接使用,像

System.out.println(name);

注意:已经定义过的变量不许再次定义!

int number=114514;
int number=114514;//这是错误的例子
number=114514;//这是正确的例子

(大量的同类型的变量可以在一个地方定义)

int a=1,b=1,c=4,d=5,e=1,f=4;

(只要把型换一下就能用这个型的定义方式)

(顺便说一下,这种变量叫做局部变量,只定义不赋值的是实例变量,用static关键字声明的变量叫做类变量,在方法里定义的变量叫做参数变量,等等等等,下面介绍如何写?

写变量最重要的是 型 变量名

局部变量

int number =114514;

成员变量(实例变量)

int number;

静态变量(类变量)

static int number;

参数变量

public void function(int number);

......然后当然可以把计算值并且定义

int a =1;
a=a+1;

当然这也适用于+-*/%

也可以省略

a +=1;

当然这也适用于+-*/%

如果是加或者是减1的话

a ++;
a --;

当然还用其他的,这些东西在理论篇学吧!

数值除了整数还有小数(float(单精数,单浮点)和double(双精数,双浮点))这里讲解double

比如说

double number=114.514

计算时

a =5/2;

输出的只是2,因为默认是整数,所以需要这么写

a =5.0/2.0;
//或者是
a =5d/2d;
//或者是
a =(double)5/(double)2;//这种数值前面的(型)或者是数值后面的(型的省略)只写一个就行(如果你是强迫症的话当我没说)

就行了

第6节(类型转换)

你不小心写了这个

System.out.println("刚满"+18+"岁");

但是不用担心,输出时会把数值自动转换成字符串。

然后你又不小心写了这个

a =5.0/2

当然这个也不用担心,计算时如果是小数的话会自动转换成double。

这个叫做自动类型转换

也有强制类型转换,比如说往系统里输入数字,但是是文本,还想计算,可以用这个(但是没(什么)用)

作业1

输出

我的名字是”“

今年”“岁了

”“里写你认为合适的字符串或者是数值。

第2章

第1节

我:你喜欢我吗?

路人甲:喜欢!

路人丙:不喜欢!

如果在代码的层面里看,我对系统提问你喜欢我吗?如果喜欢!返回true,如果不喜欢!返回false。

这就是布尔值(boolean),只有true和flase。但是可以用这两个值来进行条件语句

从定义变量开始!

boolean DoYouLike=true;

可以赋值,也可以用布尔表达式来求true或者是false。

boolean check = 1+1==2;

布尔表达式就是1+1==2了(当然意思就是1+1等于2吗?true)

如果把==换成!=就是(1+1等于2以外的数值吗?false)

可以比较大小

1<2

(<,>,<=,>=这四种)

还有

和&&

或||

否!

总结下来一共有3种,但是其实应该有6种

  • 算术运算符(+-*/%)

  • 关系运算符(==,!=,<,>,<=,>=)

  • 位运算(&&,||,!)

  • 逻辑运算符

  • 赋值运算符

  • 其他运算符

其他的就在理论篇中讲解

第2节

开始写路人到底喜不喜欢我的代码

if

if (like==1){
System.out.println("我喜欢你");
}

like==1就是布尔表达式了,但是没有写1以外的值会跳过if

else else if

if (like==1){
System.out.println("我喜欢你");
}else{
System.out.println("讨厌");
}

这就是1以外的时候

if (like==1){
System.out.println("我喜欢你");
}else if(like==2){
System.out.println("讨厌");
}

这就是2的时候

switch

switch(like){
    case 1:
    System.out.println("我喜欢你!");
    break;
    case 2:
    System.out.println("讨厌!");
    break;
    default:
    break;
}

如果不写break或者是continue的话会导致程序有问题,具体的作用会在后面写!

第3节

要想要程序循环需要while和for

while

while(true){
System.out.println(6);
}

(如果没有办法跳出循环会无线循环,需要写布尔表达式的条件或者是写什么时候需要break)

for

for(int i =1;i==1;i=1){
System.out.println(6);
}

for的结构是 数值的定义;布尔表达式;数值的变化

break:结束switch,while,for

continue:跳过switch,while,for

第4节

在第一章里虽然可以用

int a=1,b=1,c=4,d=5,e=1,f=4;

的方式定义并赋值,但是我认为还是太麻烦了!所以需要数组

int[] numbers={1,1,4,5,1,4,};(String同理)

它的编号从左数0,1,2,3,4,5的6个,需要哪个时

int[0] numbers=114514;

如果想要知道numbers里有几个元素时写numbers.length

当然,还是太麻烦了。所以居然有加强的for

for(int number:numbers)

型 变量 数组名

这个意思就是循环(元素的个数)次

作业2

int[]={1,4,6,9,13,16};

算出基数的和和偶数的和

第3章(方法和class)

第1节

在第2章为止都是基础知识,从这里开始才是重头戏,首先你要知道什么是方法,那么就先从最开始的开始讲吧

public class Main {
    public static void main(String[] args) {
        System.out.println("Hello, World!");
    }
}

第一行会在第二节会讲,这节主要是讲方法。

方法主要有两种,有返回值的方法和没有返回值的方法

有返回值的

public static int function(){

return result;
}

首先public static int function的意思是返回值为int型(也就是整数型)的方法。那么知道返回值为int型,我们要输出int型的值,

return result的意思是返回result。如果上面的返回值为int型,result必须是int。String型也是同理。

没有返回值的

public static void function(){
}

首先public static void function的意思是没有返回值的方法(void)。既然没有返回值那么也不用写return result。

有返回值的方法一般都是“我给你A,返回了B”。有点像结账系统。

没有返回值的方法是“使用A”。类似使用物品的动作。

写方法是为了容易看代码,所以推荐使用。那么我该如何使用呢?

返回值为void的方法

public class Main {
    public static void main(String[] args) {
        System.out.println("Hello, World!");
    }
}

(话说,public static void main(String[] args)的方法是从编译器来的,它的具体的方式就是它)

首先我想实行一个能输出"Hello,World!"的方法。方法部分可以这么写

public static void Hello(){
    System.out.println("Hello, World!");
}

因为是只是输出方法,所以不要返回值,所以返回值是void(无)。

如何调用方法呢?

public class Main {
    Hello();
}

那么把它结合一下就是

public class Main {
    public static void main(String[] args) {
    Hello();
}
    public static void Hello(){
    System.out.println("Hello, World!");
}
}

方法和其他的不一样,不讲究摆放循序,但是只能在class Main{}内部

如果我不是想调用方法,而是给方法一点值该怎么办?你如果知道我在第二章说了什么接下来你就能理解我说的话了。

首先是调用方法的代码部分

function(1);
//单个值
function(1,1,4,5,1,4,);
//多个值

首先调用方法时,值可以是1个,也可以是1个以上。型也可以是任何

public static void function(int number){...}
public static void function(int number1,int number2,int number3,int number4,int number5,int number6){...}

在方法中需要接受传过来的值,然后定义参数变量。

返回值为非void的方法

首先调用方法的部分就不一样

int number =function();
int number =function(a);

你也可以把它看成一种变量。这个时候就容易理解了。

那么方法部分

public static int function(){...}
public static int function(int a){...}

有返回值的方法不要忘了用ruturn返回!

重写与重载

重写:

跳到第四章

重载:

很简单,方法名一样,但是参数的数量不一样,这样就可以编译代码

第2节

class和方法类似,只不过在文件之间使用方法。但是这还是方法。

从最开始的看

public class Main {
    public static void main(String[] args)  {
            System.out.println("Hello, World!");
    }
}

如果我想在别的文件输出Hello,World该怎么办?

假设我有SubMain.java

//Main.java
public class Main {
    public static void main(String[] args)  {
        SubMain.function();
    }
}
//SubMain.java
public class SubMain {
    public static void function(){
        System.out.println("Hello, World!");
    }
}

你知道了如何调用SubMain.function方法,其实你也应该知道System.out.println("Hello,World!")是什么了,调用系统类 System 中的标准输出对象 out 中的方法 println()。

import

如果我想要导入库该怎么办呢?

import java.lang.Math;

这个链接最好上网上找一找。

作业3

和作业2一样,只不过把方法放在SubMain.java

第3章

第1节(面向对象)

对象是什么?当然不是女朋友男朋友的对象了,它指的是目标物(object)。

那么我们为什么要学对象呢?这个是因为为了方便修改代码了。

那么面向对象又是什么?对象当然就是object了,那么面向在英语里有(在......的中心),把它们结合起来就是在目标物的中心,既然是目标物的中心,当然我们要做的就是写以目标物为中心的代码了(这里的部分不要多想)

那么我们又回到了对象是什么这个问题?简单的来说就是个"东西,物品,个体"比如说人,书,车......等等那些包含信息的东西叫做对象。

当然对象不止有信息,它还有行为。

在学面向对象的知识时务必要知道的东西就是类和实例。

实例你可以理解为对象,类就是设计图。就像是有了设计图才能制造出车。

那么对象拥有信息和行为,那么那些东西该如何定义呢。这就是设计图的工作(也就是类)

class类(C类)的定义在第二章的第二节中讲过,但是我在再写一下

class 类名{

}

但是需要注意的是实例也好类也好都是用C类定义。然后在这里类一般指的是设计图,并不是什么什么类的类)

接下来为了方便,实例部分标注为Main.java,类部分标注为Apple.java

首先我们需要从类里生成实例。

class Main{
    public static void main(String[] args){
    new Apple();
    }
}
class Apple{

}

然后我们需要声明并赋值,但是以往都需要定义是什么型,但是这种情况就不需要。

Apple apple=new Apple();

类型 变量名 类名

但是刚才也说过,但是以往都需要定义是什么型,但是唯独只有实例的导入是不一样的,因为是实例的导入,所以类名会直接变成类型。

当然实例可不是只能生成一个,可以是无数个。

接下来,我们需要让apple输出I'm apple的句子。这个时候在类里,我们可以写

public void passage(){
    System.out.println("I'm apple");
}

然后我想让实例做passage的行为。

apple.passage();

这个apple就是刚才定义的变量。

类字段和实例字段

实例字段就是对象的信息。所以我们现在需要定义apple的颜色

class Apple{
    public String color;
}

现在我们有了字段,这个时候还没有颜色,我们需要导入。

现在我们要导入颜色!

class Main{
    public static void main(String[] args){
    Apple apple=new Apple();
    apple.name ="red";
    }
}

如果要使用的话只需要使用apple.name就行!

但是我们发现,每个apple的颜色都不一样,如何让apple能说出对应的颜色呢?

this。它有【这个】的意思。

class Apple{
    public String color;
    public void passage(){
    System.out.println("My color is"+this.color);
    }
}

然后输出passage方法(执行passage方法)时,就用apple.passage();就行了

函数

有人就说:”我每次设置值那么麻烦,就没有别的方法了吗?“,当然有了!函数啊,用函数啊。

在定义函数时有严格的方法

  1. 函数名要和class名相同

  2. 不能要有任何返回值

这样当生成实例时将自动执行函数。

Apple(){
    
}

然后最重要的就来了!如果要设置每个apple时就需要(类)参数变量,你一定见过!

class Main{
    public static void main(String[] args){
    Apple apple=new Apple("red");
    }
}

然后再类里

class Apple{
    public String color;
    Apple(String color){
    this.color=color;
    }
}

对象方法

public int apple(){
    return 5;
}

当你想要计算或者是组合字符串时用,然后你想使用限定的方法时

int like=this.like();

这个时候就是用当前值进行计算

类字段

有实例字段也就有类字段。

当是类字段时

public static 型 变量名;

没错,当是类字段时一定要加static。

类字段与对象字段的区别就时类字段会保存,但是对象字段用完就会消失。

然后还有类方法。

public static 型 方法名(){}

当函数的内容重复时,为了方便第一个保持原来,第二个把重复的部分用this();执行

我把重点都写在图里了,可以参考(如果需要用这个图来说明时需要引用这个教程)

如何制作并且维护你的mod?(forge版)(1.18.X,1.19.X)-第17张图片

第2节(封闭性)

人都有能展示的东西和不能展示的东西

比如说能展示的东西就是:脸,眼睛,嘴巴,等等等等

不能展示的东西就是:小j(非常抱歉,由于以下原因暂时无法播放......)

所以能展示的东西就用public,不能展示的就用private了。

在public里,在class里也好在class外都能用!

在private里,只能在class里用!

(虽然什么都没写,但是还有default ,这个可写可不写)

如何设置是public和private?

你觉得是隐私的就用private,除此之外就是public(在学访问修饰符之前都可以这么想)

第4章(基础最终章)

第1节(继承)

在上一章中,我们知道了如何创造对象和类,然后我们创造了Apple类,这个教程本来想创造Banana类,但是这里就有一个问题,Apple类和Banana类有重复的代码,如果只是一个两个的话还好,但是如果有很多个的话,到后面就不好维护了,而且说不定还忘记了!那么为了方便维护。我们要使用继承这个功能来写代码!在这个之前我们要知道这两个重要的概念

超类(super class)也叫父类或者是基类。

子类(sub class)也叫派生类或者是继承类。

被继承的叫做超类,继承的叫做子类。(但是在实际写代码的时候只需要知道有超类和子类就行了......)

我们要了解如何继承。为了演示,我需要创建Main.java,Apple.java,Banana.java,Fruit.java这四个文件!(但是主要的代码是围绕这Apple.java,Banana.java和Fruit.java进行!)

首先我们需要知道哪个是超类,哪个是子类。在这里,我们知道:

超类:Fruit.java

子类:Apple.java,Banana.java

实例:Main.java

如果想要继承Frult,就需要继承的关键字extends(顺便说一下,extends在英语里有扩展的意思所以Apple.java和Banana.java就是以fruit.java为基础开始往外扩展。)

class 子类 extends 超类 {}

在这里为了说明继承了会发生什么。在这里我要写代码然后讲解。

如何制作并且维护你的mod?(forge版)(1.18.X,1.19.X)-第18张图片

如何制作并且维护你的mod?(forge版)(1.18.X,1.19.X)-第19张图片

如何制作并且维护你的mod?(forge版)(1.18.X,1.19.X)-第20张图片

首先创造了一个新的实例apple1并且给apple1赋予了Red Apple,然后把刚才的Red Apple输出,就是这么简单的代码。聪明的你一定发现了”为什么Apple.java没有任何代码还能执行代码?“这个就是继承的特性!

”将超类的所有代码都继承到子类“

所以执行时就是超类加上子类的代码了!

注意!

在我写注意之前我需要给你看这样的图

如何制作并且维护你的mod?(forge版)(1.18.X,1.19.X)-第21张图片

如何制作并且维护你的mod?(forge版)(1.18.X,1.19.X)-第22张图片

在执行代码时如果有继承关系时:

呼叫子类执行子类的方法:因为子类包含子类的方法所以是是可行的!

Apple apple1=new Apple();
functionSub();

呼叫子类执行超类的方法:因为子类继承了超类的方法所以是可行的!

Apple apple1=new Apple();
functionSuper();

呼叫超类执行子类的方法:因为超类不包含子类的方法所以是不可行的!

Fruit fruit=new Fruit();
functionSub();

呼叫超类执行超类的方法:因为超类包含超类的方法所以是可行的!

Fruit fruit=new Fruit();
functionSuper();

重写

当有继承关系的类时,如果子类有和超类一样的方法时将会覆盖变成子类的方法

super

当你想执行超类的方法时,只需要写:

super(方法名)就可以了!

然后如果子类函数如果想要执行超类的函数时:

super(参数)就可以了!

protected访问符

当你想要让超类和它的子类使用时,在它的超类设置protected使用就行!但是如果protected是在子类时就用不了变量了!

第2节(抽象与抽象类)

你想象一下你给同学们发了作业,但是有些人写作业的方式都不一样:有的人是老老实实写的,有的人是问老师,有的人是抄的......

在这里我们发现了共同点:

都是做作业,但是每个人的做的方法都是不一样。这个就是抽象。

想要给方法设置一个抽象的方法需要给超类设置一个抽象的方法(abstract)

public abstract void function();

因为不知道具体的执行的方法,方法的内容是空的。但是制定的方法一定要在子类中写具体的执行的方法!不然会报错!!!

public void function(){}

准确的来说既然写了抽象方法,必须要重写!不然会报错!

然后类方法中有一个是抽象方法就必须是抽象类!

 abstract class Fruit{}

第3节(多态)

有这样的文件people.java,他有想买apple又想买banana聪明的你一定会写

public void buy (Apple apple){......}
public void buy (Banana banana){......}

你写的代码时是对的!但是还是像之前的一样,一个两个还好,如果有很多的方法维护起来就很麻烦。为了避免这样的麻烦,直接合并!

public void buy(Fruit fruit){......}

然后在主文件里

people gengyoubo=new people("gengyoubo");
Apple apple=new Apple();
Banana banana=new Banana();
//或者是
//Fruit apple=new Apple();
//Fruit banana=new Banana();
gengyoubo.buy(apple);
gengyoubo.buy(banana);

为什么能这么操作?在子类继承了超类了之后子类既是apple类,也是fruit类。所以可以使用!

模组篇

你有没有想过?我如何能和这个模组联动,如何能和这个模组当附属?这个教程能告诉你!

模组关系的定义

如果你没有看的话建议你看

[#8] 模组关系的定义 - 常见问题 - MC百科社群 - MC百科|最大的Minecraft中文MOD百科 (mcmod.cn)

build.gradle

在构建篇里有提到build.gradle,那么让我对模组部分说明一下

maven仓库

Maven仓库是一个存储了项目构建过程中所需的所有库文件(如jar包)的集中位置。在Maven项目中,仓库分为两种类型:

  1. 本地仓库(Local Repository):

    • 这是每个开发者机器上存放Maven依赖的地方。

    • Maven默认的本地仓库位置是用户的home目录下的.m2/repository文件夹。

    • 开发者可以自定义本地仓库的位置,通过在settings.xml配置文件中设置<localRepository>标签。

  2. 远程仓库(Remote Repository):

    • Maven中央仓库是最著名的远程仓库之一,它包含了大量的公共库和插件。

    • 远程仓库可以是私有的,也可以是公共的,用于存放那些不在中央仓库中的依赖。

    • Maven允许配置多个远程仓库,并且可以指定它们之间的搜索顺序。

    这个看起来不重要,但是在使用时非常重要,尤其是远程仓库(Remote Repository)

    我们使用模组需要有三种方法

    第一种就是使用作者的专门的maven仓库

    第二种就是自己从外部导入模组

    第三种就是使用curseforge之类的模组百科来导入

    我主要来讲第三种

    curseforge的cursemaven仓库

    首先maven仓库是这么写的

    repositories {
       maven {
           url "https://cursemaven.com"
       }
    }

    Gradle版本为5以上

    repositories {
       maven {
           url "https://cursemaven.com"
           content {
               includeGroup "curse.maven"
           }
       }
    }

    Gradle版本为6以上

    repositories {
       exclusiveContent {
           forRepository {
               maven {
                   url "https://cursemaven.com"
               }
           }
           filter {
               includeGroup "curse.maven"
           }
       }
    }

    依赖格式为

    curse.maven:<descriptor>-<projectid>:<fileids>

    <descriptor>:给你的文件起名字,最好是能一眼就知道的

    <projectid>:要添加的文件的项目 ID 

        <fileids>-> 要添加为的文件的文件 ID 

什么是项目 ID ?文件ID?请看VCR

如何制作并且维护你的mod?(forge版)(1.18.X,1.19.X)-第23张图片

项目ID就是蓝色的那个部分,所以是1014304

文件ID就是红色的那个部分,所以是5405930

把它们合起来就是

curse.maven:changedplus-1014304:5405930

然后

dependencies {
    implementation fg.deobf("curse.maven:changedplus-1014304:5405930")
}

注:fg.deobf是用来反编译的

带有mixin的模组

并不是所有的模组都可以这么导入,所以你需要导入maven库。

buildscript {
    repositories {
    //往这里添加这个
        maven { url = 'https://repo.spongepowered.org/repository/maven-public/' }
    }
    dependencies {
    //往这里添加这个
        classpath 'org.spongepowered:mixingradle:0.7-SNAPSHOT'
    }
}

然后添加插件

apply plugin: 'org.spongepowered.mixin'

mixin篇

关于mixin是什么,ChatGPT是这么说的:

如何制作并且维护你的mod?(forge版)(1.18.X,1.19.X)-第24张图片

如何制作并且维护你的mod?(forge版)(1.18.X,1.19.X)-第25张图片

如何制作并且维护你的mod?(forge版)(1.18.X,1.19.X)-第26张图片

如何制作并且维护你的mod?(forge版)(1.18.X,1.19.X)-第27张图片

如何制作并且维护你的mod?(forge版)(1.18.X,1.19.X)-第28张图片那么在minecraft里mixin是用来干什么的?

答:修改minecraft的

有人就会想修改minecraft的不就是模组吗?不是的,完全不是一个概念。我举个例子:

我想要能飞的船:

没有mixin:重新创造一个船

有mixin:在原版的船的基础上在进行添加代码

就比如在某站上看到的能吃的“岩浆桶“,就是修改了岩浆桶的属性将岩浆桶设置为可以吃。

那么当然不限制于船什么的,在这个教程里主要讲解如何运用mixin。

和mixin有点像的反射

反射(Reflection)是一个和mixin比较像,但又不同的东西。主要有2个作用:

能获取私用的类或者是方法

调用私用的类或者是方法

和mixin有什么区别?

mixin:

增加代码

修改代码

论应用能力还是mixin强,那么下一个就是使用mixin时候的问题

加载mixin

打开build.gradle,然后

buildscript {
   repositories {
   }
   dependencies {
   }
}

repositories里添加maven { url = 'https://repo.spongepowered.org/repository/maven-public/' }

dependencies里添加classpath 'org.spongepowered:mixingradle:0.7-SNAPSHOT'

然后在}结束的部分添加apply plugin: 'org.spongepowered.mixin'。

runs {
   client {
        添加这个
       arg '-mixin.config=(模组ID).mixin.json'
       }
   }
}

dependencies {
    添加这个
   annotationProcessor 'org.spongepowered:mixin:0.8.5:processor'
}

然后

jar {
   manifest {
       attributes([
               "MixinConfigs": "(模组ID).mixin.json"
       ])
   }
}

这样build.gradle就加载完成了。

然后在resources的根目录里添加(你的模组ID).mixin.json

然后填写

{
  "required": true,
  "minVersion": "0.8",
  "package": "(写存放mixin的位置)",
  "compatibilityLevel": "JAVA_17",
  "refmap": "",
  "mixins": [
    ""
  ],
  "client": [
  ],
  "server": [
  ],
  "injectors": {
    "defaultRequire": 1
  }
}

关于refmap的部分,如果你是要修改原版的代码的就写"refmap": "(模组ID).refmap.json",否则写false或者是不写。

接下来我将讲解mixin的功能。用B类修改A类的方式

public class A{
something(){}
public int number;
}
public class B{
}

@Mixin(必加)

@Mixin(A.class)
public class B{
}

修改A

@Shadow

引用原类的常量,变量,方法。

@Mixin(A.class)
public class B{
@Shadow public int number;
}
@Mixin(A.class)
public class B{
@Shadow 
something();
}

使用这个的条件:

  1. 调用的是原类的成员,而非原类的父类成员

  2. 自己调用自己类的成员,而非别处

@Unique

往原类里添加代码。

@Mixin(A.class)
public class B{
@Unique
private int number2 =114514;
}

不能是public

@Overwrite

替换原来的方法(不要轻易使用)

@Mixin(A.class)
public class B{
@Overwrite
something(){}
}

@Inject

插入,基本格式为:

@Inject(method = "", at = @At(""))

method = ""是原类的方法

at = @At("")指的是插入的地方。

它的一般是at = @At(value = "",shift = ,by = )

它们的种类是


value
shift
方法顶部HEAD
表示相对于指定点的固定偏移量

At.Shift.BY

返回语句之前RETURN表示在指定点之后At.Shift.AFTER
在方法调用INVOKE
表示在指定点之前At.Shift.BEFORE
方法底部TAIL

如果shift =At.Shift.BY时,需要填写by值,如果不是不写by值。

value=INVOKE

你需要写域描述符(字段描述符)。那是什么?这个有两个部分。

L(类链接);方法V。

比如Lnet/minecraft/entity/projectile/DragonFireballEntity;discard()V

后面会详细说明。

@ModifyArg

修改单个方法参数的值

@ModifyArg(
    method = "targetMethodName", // 目标方法名称
    at = @At(
        value = "INVOKE", // 表示我们要修改的地方是方法调用
        target = "Lpackage/ClassName;targetMethod(Args)ReturnType" // 目标方法的描述符
    ),
    index = 0 // 要修改的参数索引,从0开始
)
private int modifyFirstArgument(int originalValue) {
    return originalValue + 10; // 修改后的参数值
}

@ModifyArgs

修改多个方法参数的值

@ModifyArgs(
    method = "targetMethodName", // 目标方法名称
    at = @At(
        value = "INVOKE", // 表示我们要修改的地方是方法调用
        target = "Lpackage/ClassName;targetMethod(Args)ReturnType" // 目标方法的描述符
    )
)
private void modifyMultipleArguments(Args args) {
    args.set(0, 10); // 修改第一个参数
    args.set(1, "New String"); // 修改第二个参数
}

@ModifyVariable

修改一个局部变量的值

@ModifyVariable(
    method = "targetMethodName", // 目标方法名称
    at = @At(
        value = "STORE", // 变量存储的位置
        ordinal = 0 // 如果同类型的变量出现多次,可以用 ordinal 指定第几个
    )
)
private int modifyLocalVariable(int originalValue) {
    return originalValue * 2; // 修改后的局部变量值
}

@Redirect

重定向

@Mixin(A.class)
public class B{
@Redirect(后面省略) 
something();
}

这里的something是你的方法

域描述符(字段描述符)

Mixin注入 [Fabric Wiki]里(mixin的部分没有很大的区别),列了这个表

如何制作并且维护你的mod?(forge版)(1.18.X,1.19.X)-第29张图片这个主要写了如何定位方法,但是你很难懂,你根本就不知道如何使用,那么我来告诉你如何使用吧。

我们先看这串代码

@Redirect(method = "causeFoodExhaustion", at = @At(value = "INVOKE", target = "Lnet/minecraft/world/food/FoodData;addExhaustion(F)V"))
public void efficientFoodExhaustion(FoodData instance, float amount) {
    instance.addExhaustion(amount * foodEfficiency);
}

这次我们不分析代码,只看target的内容。

target被分为两个部分,一个是方法的地址,另外一个是方法的属性。到这里有很多人就不懂我说的是什么意思了。那么看这张图:

如何制作并且维护你的mod?(forge版)(1.18.X,1.19.X)-第30张图片mixin打算在addExhaustion之后开始重定向至mixin的方法,但是mixin需要知道addExhaustion的方法是从哪来的。target就是定位被mixin的方法里面的方法的属性和位置。还是不懂吗?也就是说target要填的就是被mixin的方法的方法的地址和属性。还是不懂的话继续看。

Lnet/minecraft/world/food/FoodData;addExhaustion(F)V

简单点就是

方法的地址;方法的属性

方法的地址是什么?就是方法是在哪个类的?一般看package XXX就知道了,但是最后需要写类的名称。

方法的属性是什么?还是刚才的例子addExhaustion是方法的名称,(F)就是使用时的参数为Float,V就是没有返回值。

这样是不是就简单多了?

如何制作并且维护你的mod?(forge版)(1.18.X,1.19.X)-第31张图片看这个写不就知道了如何写target了吗?

(字段也是同理!)

欸,有人就问了:“那如果有返回值呢?”

如果返回值是int的话那么就是addExhaustion(F)I

特殊方法

method当中也有比较特殊的方法,就是<init>和<clinit>,那么它们分别都有什么作用呢?

<init>定位构造方法,那么什么是构造函数?

public class Main{
public Main(){}//这个就是
}

<clinit>定位静态初始化块,那么什么是静态初始化块?

public class Main{
static{}//这个就是
}

反射mixin

反射mixin,就像介绍的那样同时使用反射和mixin。那么在介绍反射mixin之前我要介绍一反射的功能

反射

首先需要知道有什么功能?

  • java.lang.Class:表示类的对象。提供了方法来获取类的字段、方法、构造函数等。

  • java.lang.reflect.Field:表示类的字段(属性)。提供了访问和修改字段的能力。

  • java.lang.reflect.Method:表示类的方法。提供了调用方法的能力。

  • java.lang.reflect.Constructor:表示类的构造函数。提供了创建对象的能力。

获取class对象

通过类字面量

Class<?> clazz = String.class;

类字面量是什么?

String.class的.class就是。你还是不懂的话你一定看过@Mixin(String.class)。

通过对象实例

String str = "Hello";
Class<?> clazz = str.getClass();

通过 Class.forName() 方法

Class<?> clazz = Class.forName("java.lang.String");

创造class

Class<?> clazz = Class.forName("java.lang.String");
Object obj = clazz.getDeclaredConstructor().newInstance();

访问字段

Class<?> clazz = Person.class;
Field field = clazz.getDeclaredField("name");
field.setAccessible(true); // 如果字段是私有的,需要设置为可访问
Object value = field.get(personInstance); // 获取字段值field.set(personInstance, "New Name"); // 设置字段值

你不知道字段是什么?

public String color;

public static int wheels = 4; 

public static final int MAX_SPEED = 240; 

类似装东西的盒子。

调用方法

Class<?> clazz = Person.class;
Method method = clazz.getMethod("sayHello");
method.invoke(personInstance);
Method methodWithArgs = clazz.getMethod("greet", String.class);
methodWithArgs.invoke(personInstance, "World");

获取构造函数

Class<?> clazz = Person.class;
Constructor<?> constructor = clazz.getConstructor(String.class, int.class);
Object obj = constructor.newInstance("John", 30);

获取接口和父类

Class<?> clazz = Person.class;

// 获取所有接口
Class<?>[] interfaces = clazz.getInterfaces();
for (Class<?> i : interfaces) {
    System.out.println("Interface: " + i.getName());
}

// 获取父类
Class<?> superClass = clazz.getSuperclass();
System.out.println("Superclass: " + superClass.getName());

为什么不能只使用mixin?

在@Shadow中写到能引用原类的常量,变量,方法。但是它有个缺点!那就是不能引用构造方法。所以这个时候就需要用到反射mixin了!

例子1

@Mixin(DualReactionProcess.class)
public class DRrecipes{
@Inject(method = "<init>", at = @At(value = "TAIL")
public NewDualReactionProcess() {
        try {
            // 获取 addReaction 方法
            Method addReactionMethod = DualReactionProcess.class.getDeclaredMethod("addReaction", DualReactionProcess.Reaction.class);
            addReactionMethod.setAccessible(true);  // 确保可访问

            // 获取 ChemicalReaction 构造函数
            Class<?> chemicalReactionClass = Class.forName("github.com.gengyoubo.Minecraft_Science.procedures.DualReactionProcess$ChemicalReaction");
            Constructor<?> constructor = chemicalReactionClass.getConstructor(String.class, String.class, String.class, String.class, int.class, int.class);

            // 创建 ChemicalReaction 实例,下面的是个例子↓
            Object reactionInstance = constructor.newInstance("mcse:co2", "mcse:h2o", "mcse:c6h12o6", "mcse:o2", 6, 6);

            // 调用 addReaction 方法将新的反应添加到反应列表
            addReactionMethod.invoke(this, reactionInstance);

        } catch (Exception e) {
            e.printStackTrace();
        }
}
}

发表模组篇

发布模组有两种方法可以让你发表。第一种可以用curseforge,MC百科之类的模组百科发布,第二种是自己创造maven库来发表。第二种方法可以让别人用你的模组来制作模组。

如何用模组百科发布?

这里虽然不会介绍如何用模组百科发布,但是我来推荐别人常用来发表模组的模组百科

  1. curseforge:这个网站不仅有名,而且还容易过审。而且还可以下载别人的maven库(详细的看模组篇)

  2. modrinth:比curseforge有名的网站,但是不容易过审。

  3. github:不仅能发布模组(不用过审),而且还可以自己创造自己的maven库

  4. MC百科:有各种各样的攻略可以让你查,但是在发布模组的这个地方需要看审核员的心情。

如何用maven发布?

首先使用IDEA进行Tasks-publishing-publish。然后你就发现会在模组根目录里出现mcmodsrepo文件夹。然后一直打开到版本文件夹的前面你会发现有maven-metadata.xml。然后打开发现是这样的格式

<?xml version="1.0" encoding="UTF-8"?>
<metadata>
  <groupId>github.com.gengyoubo.MCSE</groupId>
  <artifactId>MCSE</artifactId>
  <versioning>
    <latest>1.0.0</latest>
    <release>1.0.0</release>
    <versions>
      <version>1.0.0</version>
    </versions>
    <lastUpdated>20241020143306</lastUpdated>
  </versioning>
</metadata>

我们只需要三个

域名:github.com.gengyoubo.MCSE

名称:MCSE

版本:1.0.0

(可能会不一样,大概的格式就是这样)

使用github发布maven

然后先提交到github上,然后寻找mcmodsrepo。然后复制上面的链接的话一定会变成:

https://github.com/gengyoubo/MCSE/tree/master/mcmodsrepo

然后修改成

https://raw.githubusercontent.com/gengyoubo/MCSE/master/mcmodsrepo/
如何让别人知道如何导入你的模组请看自述文件篇。

游戏崩溃篇

你有没有遇到这些问题?加了一大堆模组,然后发现启动不了,然后又看不懂英语。在这里我来教你如何解决这些问题。

提问的地方

虽然我有时候有回复评论区的问题,但是我不是每次都看评论区的问题,所以为了效率。我创造了可以提问题的地方,这样你也能提交bug,也可以学习如何分析bug。这里

崩溃日志(crash report),日志(log),调试日志(debug log)

崩溃日志(crash report)

崩溃日志(crash report),只有在游戏崩溃的时候才会有。崩溃日志的名称通常是

crash-yyyy-mm-dd_xx.yy.zz-XXX.txt

yyyy通常是年份,比如说2030年

mm通常是几月份,比如11月

dd通常是日期,比如11日

xx.yy.zz通常是事件,比如11点45分14秒

XXX有fml(forge专属)和client,server,integrated-server,openGL,memory,fabric(fabric专属,),java。分别都有不同的意思

fml:forge模组加载器(Forge Mod Loader)相关的问题,这个文件可以帮助你定位模组之间的冲突、不兼容或模组本身的错误。

(大部分都是这个)

client:客户端程序的崩溃,这个文件能够定位游戏本身、资源包、图形设置等问题,而不仅限于 Forge,fabric等。

(第二多的就是这个)

server:服务器程序的错误,当你的服务器崩溃时会返回这个。这个文件能够定位玩家连接、世界生成、服务器负载、模组或插件冲突等问题。

腐竹的话可能第二多的就是这个)

integrated-server:集成服务器的错误。在单人游戏模式中,客户端实际上会同时运行一个内置的“集成服务器”(integrated server)来处理游戏世界。崩溃日志文件中包含 integrated-server 通常表示与这个内置服务器相关的崩溃。这种类型的崩溃可能涉及世界加载、区块数据损坏、命令执行或其他与世界管理相关的问题。

(很少见)

openGL或者是rendering:渲染问题。这类问题常发生在图形设置不当、资源包/着色器不兼容或显卡驱动有问题时。

(很少见)

memory:内存问题。这种类型的崩溃通常需要调整 Java 启动参数,增加分配给游戏的内存(通过 -Xmx 参数)。

(很少见)

fabric:fabric模组加载器的问题。记录因为fabric产生的问题。

(如果是用fabric玩就会有)

java:Java 虚拟机相关的问题。有时,崩溃可能直接由 Java 虚拟机本身引起,文件中可能会包含 java 字样。这类崩溃日志通常记录与 JVM(Java 虚拟机)相关的问题,例如垃圾回收器崩溃、内存不足或其他 Java 运行时错误。这种崩溃常见于 Java 版本不兼容或系统资源不足时。

(很少见)

例子:crash-2024-06-10_17.03.52-fml.txt

用崩溃日志来排查bug的好处就是很快就能找到原因,但是缺点就是不能排查叠加bug(因为某些bug产生了bug)。

崩溃日志只要查看开头就行。

崩溃日志都存在crash-reports里

日志(log)

日志(log),在游戏开始的同时开始记录到游戏结束。

格式有三种

latest.log(最新的日志)

yyyy-mm-dd-n.log(日志的基本形式)

yyyy-mm-dd-n.log.gz(压缩了的日志)

yyyy通常是年份,比如说2030年

mm通常是几月份,比如11月

dd通常是日期,比如11日

n通常是这天的第几个日志。

通常能排查崩溃日志没有排查出来的东西。

日志都存在logs里

调试日志(debug log)

调试日志(debug log),在游戏开始的同时开始记录到游戏结束。和日志不同的是可以记录更详细的东西。

格式有三种

debug.log(最新的调试日志)

debug-n.log(调试日志的基本形式)

debug-n.log.gz(压缩了的调试日志)

n通常是这天的第几个调试日志。

通常能排查日志没有排查出来的东西。

调试日志都存在logs里。

游戏崩溃的例子

接下来我来介绍我收集到崩溃的常见的例子,格式为:

(类的路径):(因为什么导致这个异常),(汉化)

例子:Description: Mod loading error has occurred,模组加载发生错误

然后在下面介绍发生的原因和解决方法。

游戏启动前的崩溃

Description: Mod loading error has occurred,模组加载发生错误

通常原因为

  • 模组和模组之间不兼容

  • 想游玩的模组缺失前置

  • 需要更新forge版本和minecraft版本

第一个的解决方法就是把不兼容的模组给删除就行了,第二个只要添加前置就行了,最后一个就是按照提示更新版本就行了。

java.lang.IllegalArgumentException: No model for layer (modID):(layer)#main,图层 (modID):(layer)#main 没有模型

这个问题在于有没有在正确的路径里有正确的图层模型,最好就是检查一遍。但是如果检查一遍也没有用的话就需要考虑这种可能了。

因为某个错误导致引起了这个错误。(具体可以参考一下这个issue)(是不是不知道我在说什么?不用知道!

那么某个错误是否代表所有的错误呢?其实是不是的。这个某个错误是有大概的范围的。换一句说法就是,可以通过这个错误能判断大概的错误的种类。

当前已确定因为某个错误导致会引发这个错误的都有:

Description: Mod loading error has occurred

解决方法看那个错误就行了。

java.lang.IllegalAccessError: failed to access class (内部类路径)from class(内部类路径)

((内部类路径)is in module (模组或者是游戏版本)of loader '(特定的类加载器类型)' (类加载器实例的内存引用或标识符);...)

(从(内部类路径)到(内部类路径)加载失败((内部类路径)的模组的(模组或者是游戏版本)的(特定的类加载器类型)加载(类加载器实例的内存引用或标识符);...)

这个和Description: Mod loading error has occurred有点相似,发生这种原因主要有:

  • 模组与模组不兼容。

  • 某个类或成员被设为私有或受保护状态,但另一个类尝试访问它。

但是有例外:没错!这个也是因为某个错误导致的错误。(具体可以参考一下这个issue

解决方法和Description: Mod loading error has occurred一样。

org.spongepowered.asm.mixin.transformer.throwables.MixinTransformerError: An unexpected critical error was encountered(遇到意外严重错误)

当同一个模组被多个mixin定位且不兼容时会引发这个bug。

解决方法:删除受影响的模组。

游玩游戏时的崩溃

java.lang.RuntimeException: Unknown property '(属性)' on 'Block{(模组ID):(物品ID)}',(无法识别)

可能是因为属性未定义,模组冲突,版本不兼容等。

解决方法:删除受影响的模组。

java.lang.ClassCastException: class (类路径) cannot be cast to class ((类路径)类无法转换成类)

通常的原因就是某个东西(实体比较多)进行强行的类转换但是失败了。

解决方法:删除受影响的模组

自述文件篇

Q1:自述文件是什么?

A1:

如何制作并且维护你的mod?(forge版)(1.18.X,1.19.X)-第32张图片Q2:都有什么样的格式?

A2:

Markdown(.md)

  • 特点:使用最广泛的一种格式,语法简单,支持文本加粗、标题、列表、链接、图片和代码块等基础格式。

  • 优点:可读性强,渲染效果好,许多平台(如 GitHub、GitLab 等)都支持自动渲染。

  • 文件名示例:README.md

纯文本文件 (.txt)

  • 特点:最基本的文本文件格式,不支持格式化。

  • 优点:任何平台都能轻松打开,最通用的格式。

  • 文件名示例:README.txt

reStructuredText (.rst)

  • 特点:一种轻量级的标记语言,常用于 Python 项目的文档。

  • 优点:支持复杂的文档结构,且 PyPI(Python Package Index)等平台支持渲染。

  • 文件名示例:README.rst

Asciidoc (.adoc)

  • 特点:功能比 Markdown 丰富,支持复杂的文档结构。

  • 优点:可以生成 HTML、PDF 等多种格式,适合需要详细文档的项目。

  • 文件名示例:README.adoc

HTML (.html)

  • 特点:使用 HTML 编写的自述文件,允许丰富的样式和格式化。

  • 优点:可以在浏览器中直接渲染,支持所有 HTML 功能。

  • 文件名示例:README.html

Org Mode (.org)

  • 特点:适用于 Emacs 用户的文档格式,支持层次结构、列表、代码块等。

  • 优点:特别适合使用 Emacs 的开发者,支持导出为其他格式。

  • 文件名示例:README.org

Man Page (.1, .2, 等)

  • 特点:用于 Linux 或 Unix 系统上的命令行手册格式,通常用于系统或终端应用的文档。

  • 优点:适合命令行工具,方便用户直接在终端查看文档。

  • 文件名示例:README.1

在我的世界模组里,主要有readme.txt和readme.md。当然,如果都介绍你们就会看腻(其实是不想水内容),所以我只介绍readme.md(大部分github上的自述文件就是这个,而且这些功能也足够你介绍)

readme.md

就像上头说的那样readme.md就足够用,而且如果想要纯文本也可以(其实你大不了可以用readme.txt)。

那么在教你如何写模组介绍之前,我们必须知道readme.md都有什么功能可以让你用

功能

图片(无链接)

首先当然就是图片了,制作模组时都会把图片放在最开头(当然,如果没有可以跳过)

![](image-url.png)

这个本身不那么重要,主要是image-url.png的部分。如果你有图片链接,把图片链接直接放在那里。如果没有......

首先在模组的根目录里创造picture文件夹,然后在里面放置图片(比如说picture.png)。

然后image-url.png就会变成picture/picture.png。那么结果就是

![](picture/picture.png)

图片(有链接)

什么时候用有链接的图片?当然就是跳转下载链接的时候了!

不知道你是否看到这个?

如何制作并且维护你的mod?(forge版)(1.18.X,1.19.X)-第33张图片

这个就是专门用来跳转专用的图片。那么都有什么的呢?

Kofi:https://raw.githubusercontent.com/intergrav/devins-badges/v2/assets/cozy/donate/kofi-singular_64h.png

如何制作并且维护你的mod?(forge版)(1.18.X,1.19.X)-第34张图片

CurseForge:https://raw.githubusercontent.com/intergrav/devins-badges/v2/assets/cozy/available/curseforge_64h.png

如何制作并且维护你的mod?(forge版)(1.18.X,1.19.X)-第35张图片

modrinth:https://raw.githubusercontent.com/intergrav/devins-badges/v2/assets/cozy/available/modrinth_64h.png

如何制作并且维护你的mod?(forge版)(1.18.X,1.19.X)-第36张图片

discord:[chat with me on]https://raw.githubusercontent.com/intergrav/devins-badges/v2/assets/cozy/social/discord-singular_64h.png

如何制作并且维护你的mod?(forge版)(1.18.X,1.19.X)-第37张图片[join the]https://raw.githubusercontent.com/Y1rd/Y1rd/main/discord-custom_vector.svg

如何制作并且维护你的mod?(forge版)(1.18.X,1.19.X)-第38张图片

github:https://raw.githubusercontent.com/intergrav/devins-badges/v2/assets/cozy/social/github-singular_64h.png

如何制作并且维护你的mod?(forge版)(1.18.X,1.19.X)-第39张图片

mcmod(格式有点特殊,建议直接套用)[英文]:<a target="_blank" href="" rel="mcmod"><img src="https://raw.githubusercontent.com/KessokuTeaTime/Badges-Extra/refs/heads/main/assets/cozy/available/mcmodcn_64h.png" alt="description" width="170" height="55"></a>

如何制作并且维护你的mod?(forge版)(1.18.X,1.19.X)-第40张图片

[中文]:https://i.mcmod.cn/editor/upload/20241011/1728576656_276896_Ezxv.png

如何制作并且维护你的mod?(forge版)(1.18.X,1.19.X)-第41张图片(按照需求调整大小)

等等等等。如果想要这些图片去这位大佬的github库,然后使用是把图片拽到链接里就有了。(大部分的都在这里)

想要展示这样的图片需要

<a target="_blank" href="A" rel="B">![B](C)</a>

A:想要跳转的链接

B:想要跳转的名称

C:图片的链接

文本链接(在根目录内)

想要实现语言的切换?需要准备一下。

首先创造一个readme的文件夹,然后在里面放置readme(zh_cn).md,外面放置readme.md。然后把picture文件夹放进readme文件夹。这样,图片的位置就是readme/picture/picture.png了

然后在需要注意的是,如果是使用图片(无链接)的方法时,readme.md的文件链接是readme/picture/picture.png,但是放在readme文件夹里的readme(zh_cn).md是picture/picture.png。

文本链接(跳转到外部链接)

[显示的文本](https://example.com)

文本链接(特定标题跳转)

[基本流程](#A)
# A

如何写内容

顺序主要是

模组图片

模组链接

模组内容

如何做这个模组的附属

如何做这个模组的附属

首先如果你使用了mixin的话需要让它导入mixin库

buildscript {
    repositories {
        maven { url = 'https://repo.spongepowered.org/repository/maven-public/' }
    }
    dependencies {
        classpath 'org.spongepowered:mixingradle:0.7-SNAPSHOT'
    }
}

apply plugin: 'org.spongepowered.mixin'

然后重点是如何让他下载你的maven库,

repositories {
    maven {
        name = "模组名称"
        url = "maven链接"
    }
}

dependencies {
    implementation fg.deobf("域名:名称:版本")
}

具体可以看发表模组篇