本篇教程由作者设定使用 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应用到游戏开发上)版本通用←它在施工
游戏开发后(发表模组)
发表模组篇(做完的模组如何发表)版本通用
游戏崩溃篇(教你如何处理游戏崩溃)版本通用
自述文件篇(想介绍你的模组的人的可以学这个)版本通用
需要的东西
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;
}
}
(虽然在实际演示里使用了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:"
}
}
左边是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(中文的本地化键)
都有哪个国家的本地化键
南非荷兰语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。
你可以参考一下这个表
但是有例外。
如果是聪明的你一定能发现这个Void和void有点相似。
但是并不常见......。
然后定义字段时。
public final ForgeConfigSpec.ConfigValue<Integer> Example;
那么这里必须是包装类,但是是所有封装类吗?并不是。必须是基本类型与包装类型是对应的才行。
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);
引用配置文件的语法
引用时,使用
模组主文件夹.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关键字)
引用: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();就行了
函数
有人就说:”我每次设置值那么麻烦,就没有别的方法了吗?“,当然有了!函数啊,用函数啊。
在定义函数时有严格的方法
函数名要和class名相同
不能要有任何返回值
这样当生成实例时将自动执行函数。
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();执行
我把重点都写在图里了,可以参考(如果需要用这个图来说明时需要引用这个教程)
第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 超类 {}
在这里为了说明继承了会发生什么。在这里我要写代码然后讲解。
首先创造了一个新的实例apple1并且给apple1赋予了Red Apple,然后把刚才的Red Apple输出,就是这么简单的代码。聪明的你一定发现了”为什么Apple.java没有任何代码还能执行代码?“这个就是继承的特性!
”将超类的所有代码都继承到子类“
所以执行时就是超类加上子类的代码了!
注意!
在我写注意之前我需要给你看这样的图
在执行代码时如果有继承关系时:
呼叫子类执行子类的方法:因为子类包含子类的方法所以是是可行的!
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仓库
本地仓库(Local Repository):
这是每个开发者机器上存放Maven依赖的地方。
Maven默认的本地仓库位置是用户的home目录下的.m2/repository文件夹。
开发者可以自定义本地仓库的位置,通过在settings.xml配置文件中设置<localRepository>标签。
远程仓库(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
项目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是这么说的:
那么在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();
}
使用这个的条件:
调用的是原类的成员,而非原类的父类成员
自己调用自己类的成员,而非别处
@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的部分没有很大的区别),列了这个表
这个主要写了如何定位方法,但是你很难懂,你根本就不知道如何使用,那么我来告诉你如何使用吧。
我们先看这串代码
@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被分为两个部分,一个是方法的地址,另外一个是方法的属性。到这里有很多人就不懂我说的是什么意思了。那么看这张图:
mixin打算在addExhaustion之后开始重定向至mixin的方法,但是mixin需要知道addExhaustion的方法是从哪来的。target就是定位被mixin的方法里面的方法的属性和位置。还是不懂吗?也就是说target要填的就是被mixin的方法的方法的地址和属性。还是不懂的话继续看。
Lnet/minecraft/world/food/FoodData;addExhaustion(F)V
简单点就是
方法的地址;方法的属性
方法的地址是什么?就是方法是在哪个类的?一般看package XXX就知道了,但是最后需要写类的名称。
方法的属性是什么?还是刚才的例子addExhaustion是方法的名称,(F)就是使用时的参数为Float,V就是没有返回值。
这样是不是就简单多了?
看这个写不就知道了如何写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库来发表。第二种方法可以让别人用你的模组来制作模组。
如何用模组百科发布?
这里虽然不会介绍如何用模组百科发布,但是我来推荐别人常用来发表模组的模组百科
curseforge:这个网站不仅有名,而且还容易过审。而且还可以下载别人的maven库(详细的看模组篇)
modrinth:比curseforge有名的网站,但是不容易过审。
github:不仅能发布模组(不用过审),而且还可以自己创造自己的maven库
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:
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)
图片(有链接)
什么时候用有链接的图片?当然就是跳转下载链接的时候了!
不知道你是否看到这个?
这个就是专门用来跳转专用的图片。那么都有什么的呢?
modrinth:https://raw.githubusercontent.com/intergrav/devins-badges/v2/assets/cozy/available/modrinth_64h.png
discord:[chat with me on]https://raw.githubusercontent.com/intergrav/devins-badges/v2/assets/cozy/social/discord-singular_64h.png
[join the]https://raw.githubusercontent.com/Y1rd/Y1rd/main/discord-custom_vector.svg
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>
[中文]:https://i.mcmod.cn/editor/upload/20241011/1728576656_276896_Ezxv.png
(按照需求调整大小)
等等等等。如果想要这些图片去这位大佬的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("域名:名称:版本")
}
具体可以看发表模组篇