本篇教程由作者设定未经允许禁止转载。
如果你已经阅读了前一章并完成了开发环境的搭建的话,那么现在就正式进入匠魂2的模组开发过程吧!
匠魂词条编写
观前提示,想要会这部分内容,建议去学学java,不要求太多,你把数据类型,条件语句、循环、运算符这些看完就行,对象与类可以慢慢做慢慢学
配置你的模组
在编写词条之前,你需要先配置你的模组,比如为它取名
MC模组开发的项目结构中src/main/java目录存放Mod的Java源代码,src/main/resources目录存放Mod的资源文件
现在先让我们来解决java代码
打开你的src——main——java文件夹,你可能会看到这样一串文件com.example.examplemod,这个是你模组的包名,显然,实际情况我们并不愿意使用它例子的包名。
所以现在,你需要先给自己的模组选择一个合适的包名,但在取包名前,你需要知道这些
包名通常有以下规定:
● 包名不应该使用数字和小写字母之外的字符,包名由小数点分隔的每一节的第一个字符不应该是数字。
● 如果包名的每一节中有多个单词,那么应该简单地拼接在一起,而不应使用下画线等字符分隔。
实际情况下一般以作者名称/id+项目名称命名,比如我的包名
而它的修改方法也很简单,右键目标,选择重构重命名即可
这里还留有一个com只是因为我懒疏忽了()
命名完包名之后,打开ExampleMod文件(我这里已经改好了,但问题不大,结构都一样)
@Mod(modid = ParasiticTechnologyMixinAffiliation.MODID, name = ParasiticTechnologyMixinAffiliation.NAME, version = ParasiticTechnologyMixinAffiliation.VERSION)
//ParasiticTechnologyMixinAffiliation是你的这个类的类名,这个类也是你mod的主类,具体填写就根据你自己类命名就行
public class ParasiticTechnologyMixinAffiliation
//class后ParasiticTechnologyMixinAffiliation就是你的类名,由于这个类被public修饰,所以这个类名要与文件名一样
{
public static final String MODID = "parasitic_technology_mixin_affiliation";//你的MODID,影响注册名
public static final String NAME = "Parasitic Technology Mixin Affiliation";//你的MOD名字,影响你的MOD显示的名字
public static final String VERSION = "0.1";//你的MOD版本,随便取,什么0.1版啦什么0.2版啦
//下面的内容暂且省略不讲
private static Logger logger;
@EventHandler
public void preInit(FMLPreInitializationEvent event)
{
logger = event.getModLog();
}
@EventHandler
public void init(FMLInitializationEvent event)
{
// some example code
logger.info("DIRT BLOCK >> {}", Blocks.DIRT.getRegistryName());
}
}
然后是编写mod的信息文件
打开mcmod.info文件
[
{
//注意,没打//的部分不要修改
"modid": "parasitic_technology_mixin_affiliation",//填你之前写的modid
"name": "Parasitic Technology Mixin Affiliation",//填你之前写的mod名字
"description": "寄生科技整合包的mixin模组",//对模组内容的描述
"version": "${version}",
"mcversion": "${mcversion}",
"url": "",
"updateUrl": "",
"authorList": ["cszxymzx"],//mod作者名单
"credits": "感谢那些在我学习路上帮助过我的人",//可以写一些感谢XXX的话
"logoFile": "",
"screenshots": [],
"dependencies": []
}
]
最后是配置构建MOD时的配置信息别倒别倒,我知道这些东西很麻烦,但你还是得做
打开build.gradle文件,你需要修改这段代码
version = '0.1'//第一行代码设置了Mod的版本号,请将其与@Mod注解中的数据保持一致
group = 'com.cszxymzx.parasitictechnologymixinaffiliation' // 第二行代码设置了Mod的包名,请与你设置的包名保持一致
archivesBaseName = 'parasitic_technology_mixin_affiliation'//第三行代码设置了Mod在构建完成后的文件名前缀,在大部分情况下,它的名称应该和Mod的名称保持一致。
好!总算是把mod相关的描述性信息修改好了,辛苦辛苦,那接下来就进入正式开发吧!
编写词条
你首先要了解的是,如果你要编写一个词条,你需要经过三个步骤,其分别为实现词条,注册词条,键值汉化
这里我以我改写的尸山为例带你们走一下流程
实现词条
@Mod.EventBusSubscriber()
//Mod.EventBusSubscriber是一个注解,用于在Minecraft Forge mod中标记一个类,以便它可以注册到Forge的事件总线上
//当一个类使用@Mod.EventBusSubscriber注解时,它表示这个类是一个事件订阅者,可以监听和处理特定类型的事件。
//被标记的类中可以添加用于处理事件的方法,并使用事件注解@SubscribeEvent来标记这些方法。
//也就是说如果你想要使用@SubscribeEvent注册一个事件,那么你必须在类头部加上这个注解,当然如果没有订阅事件就别加它
public class TraitCorpseMountain extends AbstractTrait
//首先新建一个类,给它取个合适的名字,由于我们写的是匠魂词条,这里我建议Trait+词条名的方式进行命名(为了方便阅读,建议单词的首字母大写)
//然后使用extends 继承AbstractTrait类,这是匠魂词条的抽象类,当然你暂时不需要抽象类是什么,你只需要知道写词条必须继承它就行
{
public TraitCorpseMountain()
//每个类都必须有它的构造方法,方法名就是类名,且用public进行修饰
{
super("trait_corpse_mountain",0xff0000);
//第一个参数是特性的注册名,即"trait_corpse_mountain"(注意,注册名不能有大写字母),通过魔法匠魂的配置文件调用注册名就可以将词条附加到你想要的材料上
//当然具体请看相关教程
//第二个参数是特性的颜色,即红色(0xff0000)
}
//实现击杀成长的核心
@SubscribeEvent//注册一个事件以供触发
public static void onTargetKilled(LivingDeathEvent event)
//对于事件监听器,你构建时必须使用static关键字进行修饰
//上面注解的作用是用于将一个方法注册为事件监听器,也就是onTargetKilled,而监听器需要一个参数(事件)来制定要监听的事件类型
//而LivingDeathEvent事件为当有生物实体死亡时触发,该事件的信息将被传递给该方法的event参数
//因此,在onTargetKilled(LivingDeathEvent event)方法中可以通过event参数来获取死亡实体的信息,从而进行相应的处理
{
if (event.getSource().getTrueSource() instanceof EntityPlayer && event.getEntity() instanceof EntityLiving)
//instanceof 是一个 Java 关键字,用于检查一个对象是否是一个特定类的实例,语法为object instanceof Class
//object 是要检查的对象,Class 是要检查的类。如果 object 是 Class 的一个实例,表达式的结果为 true,否则为 false
//&& 是逻辑与运算符,用于判断两个布尔值是否都为 true
//如果两个操作数都是 true,则表达式返回 true,否则返回 false
//所以if的判断式为
//真实的伤害来源是不是玩家(左)
//这个死亡的实体是不是生物(右)
// 意思是如果伤害来源是玩家且死亡的实体是生物就执行下面的代码
{
World w = event.getSource().getTrueSource().world;
//获取攻击者所在的世界
//event 是 LivingDeathEvent 类型的事件对象,表示一个生物体(实体)死亡的事件。
//event.getSource() 返回这个事件的来源,即伤害的原因。在这个事件中,来源是 DamageSource 类型的对象。
//event.getSource().getTrueSource() 返回实际造成伤害的实体,即攻击者。在这个事件中,攻击者是一个实体(Entity)对象。
//event.getSource().getTrueSource().world 返回攻击者所在的世界,即这个实体所在的 World 对象。
// 因此,这行代码的作用是获取攻击者所在的世界。
ItemStack tool = ((EntityPlayer) event.getSource().getTrueSource()).getHeldItemMainhand();
// 意思是在 LivingDeathEvent 事件中获取造成伤害的玩家手中的主手物品,并将其保存到 tool 变量中
//event.getSource()
// 返回事件源对象,类型为 DamageSource,表示造成伤害的来源,比如实体、环境、魔法等等。
//event.getSource().getTrueSource()
// 返回实际的伤害源对象,类型为 Entity,表示造成伤害的实体,比如玩家、生物、陷阱等等。如果伤害源不是实体,则返回 null。
//(EntityPlayer) event.getSource().getTrueSource()
// 将实际的伤害源对象转换为玩家对象,类型为 EntityPlayer。如果实际伤害源不是玩家,则会抛出 ClassCastException 异常。
//((EntityPlayer) event.getSource().getTrueSource()).getHeldItemMainhand()
// 获取玩家手中的主手物品,类型为 ItemStack,即物品栏中的一个物品栏槽对应的物品。如果手中没有物品,则返回一个空的 ItemStack 对象。
// 获取事件中实体玩家手中的主手物品,并将其保存到 ItemStack 类型的变量 tool 中
NBTTagCompound tag = TagUtil.getExtraTag(tool);
//获取 tool 物品栏中附加标签的 NBTTagCompound 对象并记录为tag
CxmxNBTData data = CxmxNBTData.read(tag);
//读取工具中CxmxNBTData类的数据并存储在新的CxmxNBTData类data对象中
// 注意!注意!注意!这里的CxmxNBTData是我自定义的一个工具类,具体的data是什么请看后面我给的CxmxNBTData类代码
// 如果要抄的话就自己新建个类把CxmxNBTData类的源代码复制上去就行
if (!w.isRemote && TinkerUtil.hasTrait(TagUtil.getTagSafe(tool), "trait_corpse_mountain")&&data.bonus<50)
//判断当前代码运行的环境是否为客户端:!w.isRemote
//检查手持物品(即主手物品)是否包含了名为identifier的特质(Trait)TinkerUtil.hasTrait(TagUtil.getTagSafe(tool), identifier)
//&& 是逻辑与运算符,用于判断两个布尔值是否都为 true
//如果两个操作数都是 true,则表达式返回 true,否则返回 false
//TinkerUtil.hasTrait()方法用于检查指定的物品是否包含指定的特质
// 而TagUtil.getTagSafe()方法则用于获取物品的NBT标签数据,以便判断物品是否包含指定特质。
// 意思就是如果当前代码运行的环境不是为客户端,且击杀生物时手持的物品包含了名为trait_corpse_mountain的匠魂词条,则会执行条件块中的代码
{
float health = ((EntityLiving) event.getEntity()).getMaxHealth();
//event.getEntity()获取到了事件触发时受到伤害的实体
//强制类型转换为EntityLiving
//由于EntityLiving继承自EntityLivingBase类
//可以使用getMaxHealth()方法来获取最大生命值
//最终将该值存储在float类型的变量health中
// 获取事件触发时实体的最大生命值并存储在float类型的health中
data.killcount += 1;
//击杀计数加一
data.health = health;
//记录击杀目标的最大生命值
float divisor = 25000f;
//定义了一个float类型的变量divisor,并将其赋值为25000f,用作后续的数值计算。
float bonus = Math.round(random.nextFloat() * health * 100) / divisor;
//随机生成一个浮点数,范围为0到1,乘以实体的最大生命值并乘以100,得到一个值。
//将上一步得到的值除以divisor,得到bonus的值,并四舍五入到最近的整数。
// 定义了一个浮点型变量bonus,并通过一系列计算得出击杀目标后提升的伤害
data.bonus += bonus;
//成长奖励增加
data.bonus = (float) Math.round(data.bonus * 100f) / 100f;
//Math.round(data.bonus * 100f) / 100f的意思是将data.bonus乘以100并四舍五入到最接近的整数
//然后再除以100来保留两位小数。最后将计算得到的结果赋值给data.bonus变量
// 作用是将data.bonus变量的值经过计算后保留两位小数
data.write(tag);
//将 data 对象中的数据写入 tag 对象中
TagUtil.setExtraTag(tool, tag);
// TagUtil 是一个工具类,它提供了一组方便的静态方法来操作 NBT 标签数据
//setExtraTag 方法用于在物品栏中设置一个附加的 NBT 标签数据
//这个方法的第一个参数是一个 ItemStack 对象,表示需要设置标签数据的物品,
// 第二个参数是一个 NBTTagCompound 对象,表示需要写入的 NBT 标签数据。
// 也就是将计算后增加的伤害 tag 对象作为附加数据写入 tool 物品栏中进行保存
//MC里想要保存数据你需要使用nbt来进行保存
}
}
}
//将上面的结果用来改变伤害
@Override//覆盖方法
public float damage
(ItemStack tool, EntityLivingBase player, EntityLivingBase target,
float damage, float newDamage, boolean isCritical)
//该方法的名称是 damage,返回一个 float 类型的值。该方法有 6 个参数:
//tool:一个 ItemStack 类型的参数,表示正在使用的工具。
//player:一个 EntityLivingBase 类型的参数,表示使用工具的玩家。
//target:一个 EntityLivingBase 类型的参数,表示被攻击的实体。
//damage:一个 float 类型的参数,表示原始伤害值。
//newDamage:一个 float 类型的参数,表示计算后的伤害值。
//isCritical:一个 boolean 类型的参数,表示攻击是否为暴击。
// 该方法的作用是根据玩家使用的工具和攻击目标的不同,计算并返回修改后的伤害值。
// 补充,你这个类所继承AbstractTrait抽象类还有很多方法用来修改匠魂工具不同行为的效果,想要查看这些方法可以使用Ctrl点击damage这个方法名跳转查看
{
NBTTagCompound tag = TagUtil.getExtraTag(tool);
//获取 tool 物品栏中附加标签的 NBTTagCompound 对象并记录为tag
CxmxNBTData data = CxmxNBTData.read(tag);
//读取工具中CxmxNBTData类的数据并存储在新的CxmxNBTData类data对象中
float bonus = data.bonus;
//获取成长奖励
return newDamage + bonus;
//将源伤害加上这个新伤害并返回,也就是修改伤害
}
//显示信息用
@SideOnly(Side.CLIENT)
//仅在客户端运行
//注意!注意!这个很重要!有些只会出现在客户端的东西必须使用这个@SideOnly(Side.CLIENT)注解,因为服务端无法调用自己没有的方法,如果你不想让游戏崩溃的话()
@SubscribeEvent
public static void onItemTooltip(ItemTooltipEvent e)
//订阅了ItemTooltipEvent事件,该事件会在玩家查看物品的提示信息时触发
//e为被触发的事件对象,也就是被查看的物品
{
ItemStack tool = e.getItemStack();
//获取当前正在被展示tooltip的物品堆栈,赋值给ItemStack类的变量tool
if (TinkerUtil.hasTrait(TagUtil.getTagSafe(tool), "trait_corpse_mountain"))
//检查tool是否具有指定的匠魂词条
{
NBTTagCompound tag = TagUtil.getExtraTag(tool);
//获取 tool 物品栏中附加标签的 NBTTagCompound 对象并记录为tag
CxmxNBTData data = CxmxNBTData.read(tag);
//读取工具中CxmxNBTData类的数据并存储在新的CxmxNBTData类data对象中
if (data.killcount != 0)
//判断killcount的数据是否为0,若为零就跳过
{
e.getToolTip().add(TextFormatting.WHITE + "Killed: " + TextFormatting.WHITE + data.killcount);
//e.getToolTip() 返回一个包含物品当前提示信息的列表
//add() 方法可以将一个字符串添加到列表末尾
//TextFormatting.WHITE 是一个枚举值,表示添加的字符串颜色为白色
//"Killed: "是要添加的字符串。
//data.killcount 是一个整数值,表示被击杀的实体数量,会被转换成字符串并添加到提示信息列表中
// 将一个文本字符串添加到物品的提示信息列表中
e.getToolTip().add(TextFormatting.WHITE + "Bonus: " + TextFormatting.WHITE + data.bonus);
//同上
}
}
}
}
以上便是匠魂词条的实现
附CxmxNBTData类代码
public class CxmxNBTData extends NBTTagCompound {
public int killcount;
public float health;
public int brokenblocks;
public float bonus;
public int curse;
public String name;
public float radius;
public int hunger;
public float dfloat;
public int dint;
public int times;
public boolean active;
public int soul;
public float damage;
public boolean cxmxswitch;
public static CxmxNBTData read(NBTTagCompound tag) {
CxmxNBTData data = new CxmxNBTData();
data.killcount = tag.getInteger("killcount");
data.brokenblocks = tag.getInteger("brokenblocks");
data.health = tag.getFloat("health");
data.bonus = tag.getFloat("bonus");
data.curse = tag.getInteger("curse");
data.name = tag.getString("name");
data.radius = tag.getFloat("radius");
data.dfloat = tag.getFloat("dfloat");
data.hunger = tag.getInteger("hunger");
data.dint = tag.getInteger("dint");
data.times = tag.getInteger("times");
data.active = tag.getBoolean("active");
data.soul = tag.getInteger("soul");
data.damage = tag.getFloat("damage");
data.cxmxswitch = tag.getBoolean("cxmxswitch");
return data;
}
public void write(NBTTagCompound tag) {
tag.setInteger("killcount", killcount);
tag.setInteger("brokenblocks", brokenblocks);
tag.setFloat("health", health);
tag.setFloat("bonus", bonus);
tag.setInteger("curse", curse);
tag.setString("name", name);
tag.setFloat("radius", radius);
tag.setInteger("dint", dint);
tag.setInteger("hunger", hunger);
tag.setInteger("times", times);
tag.setFloat("dfloat", dfloat);
tag.setBoolean("active", active);
tag.setInteger("soul", soul);
tag.setFloat("damage", damage);
tag.setBoolean("cxmxswitch", cxmxswitch);
}
}
注册词条
接下来你需要为游戏注册词条,不注册的话它就不知道你给它添加了词条,你也就无法使用
现在回到你之前写的mod主类
@Mod(modid = ParasiticTechnologyMixinAffiliation.MODID, name = ParasiticTechnologyMixinAffiliation.NAME, version = ParasiticTechnologyMixinAffiliation.VERSION)
public class ParasiticTechnologyMixinAffiliation
{
public static final String MODID = "parasitic_technology_mixin_affiliation";
public static final String NAME = "Parasitic Technology Mixin Affiliation";
public static final String VERSION = "0.1";
private static Logger logger;
@EventHandler
public void preInit(FMLPreInitializationEvent event)
{
TinkerRegistry.addTrait(new TraitCorpseMountain());
//在这里按照这个格式为游戏注册词条
//TraitCorpseMountain()可以换成其他你任意想要注册词条的构建方法,如果构建方法存在参数,那你还需要填上参数
logger = event.getModLog();
}
@EventHandler
public void init(FMLInitializationEvent event)
{
// some example code
logger.info("DIRT BLOCK >> {}", Blocks.DIRT.getRegistryName());
}
}
键值汉化
如果你不汉化,那么词条显示在匠魂书上就是一串奇怪的符号,这自然不是我们想要的,所以接下来就进入汉化
在resources文件夹下新建一个assets. +MODID的包,比如这个
然后在这个包下再新建一个名为lang的包,这个包存储的就是MC的语言文件,我们可以新建一个lang文件用来存储语言,命名为zh_cn表示是中文的汉化文件
然后打开这个文件写上文字
匠魂词条的汉化格式为
modifier. + 词条注册名+ .name=词条名称
modifier. + 词条注册名+ .desc=词条描述
其中§o代表它后面跟着的文字显示为斜体(italic)样式
§r用于重置文本样式,将之前设置的样式(比如颜色、粗体、斜体等)重置为默认样式,你可以在文本中使用"§r"来确保之后的文本不受之前的样式影响
\n是换行符,代表换行输出
如果你想了解更多格式符的知识,可以看看这个格式化代码 - Minecraft Wiki,最详细的我的世界百科 (fandom.com)
modifier.trait_corpse_mountain.name=尸山
modifier.trait_corpse_mountain.desc=§o——尸体越多,它的体型就越是庞大。而且永远不会满足;§r\n武器将会随着你剑刃上淋漓的鲜血而逐渐提升伤害,最高提升50
其具体效果如下
如果你不会给材料添加词条,可以看大轩的这篇教程1.12.2修改匠魂2各个属性的办法 - 匠魂调整 (Tweakers Construct) - MC百科|最大的Minecraft中文MOD百科 (mcmod.cn)
其配置文件在idea开发环境的位置在这里
Mixin运用
(注:非必要,可不看,仅了解)
首先是Mixin目的匠魂武器在攻击后会根据伤害大小产生特殊的伤害粒子,这对于普通情况来说没有影响,但在怪物平均血量非常高的整合包比如说我的包里,一瞬间渲染数万的伤害粒子的效果属实非常炸裂,能把电脑卡好一会,所以我们必须关闭。但我tnnd没想到普通红心是原版的伤害粒子,有哪位好心的大哥大佬能帮我指一下生成伤害红心的代码在哪个类哪个方法,真的求求了 M_M
寻找代码根源并不是一个简单的工作,尤其是找到切入点,它没有定式,所以我只能从你初步找到切入点的时候讲起
为了消除武器工具攻击的伤害粒子,我们可以尝试翻翻武器的源代码,经过仔细阅读,我们很幸运的直接就找到了它但这个纯属运气,有的作者喜欢把一些东西分开后堆一起处理,那是真的见鬼
我们可以看到存在一个spawnAttackParticle()方法,翻译过来就是产生攻击粒子,结合前文可以看出这里有个条件语句,检查攻击是否命中并且攻击者是否准备进行特殊攻击。如果条件满足就使它TinkerTools.proxy调用spawnAttackParticle方法,来生成一个特殊的攻击粒子效果。
那我们就Mixin这里吗?
显然不行,因为工具都有自己产生粒子效果的方法,如果这样mixin那就意味着每个工具都要mixin一遍,这是非常愚蠢且危险的麻烦行为,我们应当研究这个方法,通过mixin使这个方法无法被执行。
所以我们按住Ctrl点击这个spawnAttackParticle()方法,idea就会自动跳转到这个方法的来源
有很多同名方法看起来很可怕?
不急,经过仔细观察,你发现最关键的就是这一段
划横线的这段使用网络通道向客户端分发通信包,通知客户端进行渲染粒子,我们只需阻止它进行通信就可以了,不过先别急,我们观察一下附近的文件,一般来说,类似内容的文件代码会存放在一起。
果然,我们在CommonProxy所在的文件夹下找到了继承了它ClientProxy客户端代码文件
里面有这个方法
我们把它也mixin了,所以具体代码如下
@Mixin(value = ClientProxy.class,remap = false
//指示Mixin将被应用到ClientProxy类上,并且remap参数设置为false,表示在混淆后的代码中不重新映射类和方法名
public abstract class ClientProxyMixin extends CommonProxy
//这是一个抽象类,它扩展了(也就是加了extends关键字)CommonProxy类,并用于实现Mixin
{
@Inject(method = "spawnParticle",at = @At("HEAD"),cancellable = true)
//这是一个注入(Injection)注解。它指示Mixin将注入到ClientProxy类的spawnParticle方法的头部(HEAD)
public void __test(CallbackInfo ci)
//这是注入的方法,命名为__test。该方法接受一个CallbackInfo对象作为参数,用于通知注入成功后的回调。
{
ci.cancel();//ci.cancel()被调用,它会取消方法的正常执行
}
}
@Mixin(value = CommonProxy.class,remap = false)
//这是Mixin注解的开始。它指示Mixin将被应用到CommonProxy类上,并且remap参数设置为false,表示在混淆后的代码中不重新映射类和方法名。
public abstract class CommonProxyMixin// 这是一个抽象类(也就是添加了abstract关键字),用于实现Mixin
{
@Inject(
method = "spawnParticle(Lslimeknights/tconstruct/library/client/particle/Particles;Lnet/minecraft/world/World;DDDDDD[I)V",
at = @At("HEAD"),
cancellable = true
)
//这是一个注入(Injection)注解。它指示Mixin将注入到CommonProxy类的spawnParticle方法的头部(HEAD)
public void __test(CallbackInfo ci)//这是注入的方法,命名为__test。该方法接受一个CallbackInfo对象作为参数,用于通知注入成功后的回调
{
ci.cancel();//ci.cancel()被调用,它会取消方法的正常执行。
}
}
然后把这两个Mixin类的类名填到上一个教程内mixin部分所创建的json里就行
完成模组后
你需要干两件事
第一,使用idea现场运行模组,检查是否可用
按下Ctrl+Shift+A组合键,打开执行相关任务的选项框,并在搜索框中输入Execute,选择执行Gradle任务
输入runClient运行游戏
然后自行检查模组内容即可
第二,导出模组文件
打开idea下方的终端,输入./gradlew.bat build即可
如果要清理旧模组文件,输入./gradlew.bat clean
然后你就可以在build文件夹下的libs里找到自己的模组文件了
最后,如果你想学习更多的模组开发知识,可以看看这些参考