mc mod开发复盘-Ruler 一、需求分析 在对Java技术有一定掌握后,萌生了为儿时圆梦,想进行MinecraftMod开发。但是第一次开发mc的mod对各个接口都不熟悉,肯定要选择先开发轻量级的mod练手,于是就想起之前一个个数格子建东西的不便经历,选择开发一个能测量方块的直尺mod。
二、环境搭建
参考了b站up主北山的教程:模组开发指北- 文集 哔哩哔哩专栏 ,在idea导入fabric的mod模板,进行1.20的mod开发。
修改对应的配置文件:
fabric.mod.json中的模组信息和个人信息
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 { "schemaVersion" : 1 , "id" : "ruler-mod" , "version" : "${version} " , "name" : "Ruler Mod" , "description" : "提供了几个用于测量的尺子" , "authors" : [ "Liuliy" ], "custom" : { "modmenu" : { "links" : { "modmenu.issues" : "https://github.com/Liuliy1/MinecraftRulerMod/issues" } } }, "contact" : { "sources" : "https://github.com/Liuliy1/MinecraftRulerMod" }, "license" : "MIT" , "icon" : "assets/ruler-mod/icon.png" , "environment" : "*" , "entrypoints" : { "main" : [ "com.liuliy.ruler.RulerMod" ], "client" : [ "com.liuliy.ruler.RulerModClient" ], "fabric-datagen" : [ "com.liuliy.ruler.RulerModDataGenerator" ] }, "mixins" : [ "ruler-mod.mixins.json" ], "depends" : { "fabricloader" : ">=0.16.10" , "minecraft" : ">=1.20" , "java" : ">=17" , "fabric-api" : "*" }, "suggests" : { "another-mod" : "*" } }
三、资料获取 主要是 [Fabric Wiki] 里面有写各种功能的接口和案例,再参考其他教程中的案例,就能基本知道开发的整体逻辑。
四、实际开发 其实开发这些mod和其他的Java项目并没有什么区别,和之前开发的杀戮尖塔mod也有相似的地方。也就是先建立各种类,实现他们的方法,然后实例化,再把他们的逻辑连接起来,添加到游戏中。
1. Mod入口 首先我们需要一个mod入口实现了ModInitializer接口,用于进行mod的初始化:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 package com.liuliy.ruler;import com.liuliy.ruler.client.ParticleManager;import com.liuliy.ruler.client.Visualization;import com.liuliy.ruler.registry.ModItems;import net.fabricmc.api.ModInitializer;import net.fabricmc.fabric.api.event.lifecycle.v1.ServerTickEvents;import net.minecraft.client.MinecraftClient;import net.minecraft.world.World;public class RulerMod implements ModInitializer { public static final String MOD_ID = "ruler-mod" ; @Override public void onInitialize () { ModItems.register(); ParticleManager.init(); ModItemGroup.registerModItemGroup(); } }
2.注册所有道具 然后就是用类定义想要开发的道具,然后在mod入口进行注册:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 package com.liuliy.ruler.registry;import com.liuliy.ruler.items.*;import net.fabricmc.fabric.api.itemgroup.v1.FabricItemGroupEntries;import net.fabricmc.fabric.api.itemgroup.v1.ItemGroupEvents;import net.minecraft.item.Item;import net.minecraft.item.ItemGroups;import net.minecraft.registry.Registries;import net.minecraft.registry.Registry;import net.minecraft.util.Identifier;import static com.liuliy.ruler.ModItemGroup.RULER_ITEM_GROUP_KEY;import static com.liuliy.ruler.ModItemGroup.Ruler_ITEM_GROUP;public class ModItems { public static final Item STRAIGHT_RULER = new StraightRulerItem (); public static final Item LASER_RANGEFINDER = new LaserRangefinderItem (); public static final Item LASER_RULER = new LaserRulerItem (); public static void register () { Registry.register(Registries.ITEM, new Identifier ("ruler-mod" , "straight_ruler" ), STRAIGHT_RULER); Registry.register(Registries.ITEM, new Identifier ("ruler-mod" , "laser_rangefinder" ), LASER_RANGEFINDER); Registry.register(Registries.ITEM, new Identifier ("ruler-mod" , "laser_ruler" ), LASER_RULER); Registry.register(Registries.ITEM_GROUP, RULER_ITEM_GROUP_KEY, Ruler_ITEM_GROUP); ItemGroupEvents.modifyEntriesEvent(RULER_ITEM_GROUP_KEY).register(ModItems::addItemsToIG); } private static void addItemsToIG (FabricItemGroupEntries fabricItemGroupEntries) { fabricItemGroupEntries.add(STRAIGHT_RULER); fabricItemGroupEntries.add(LASER_RANGEFINDER); fabricItemGroupEntries.add(LASER_RULER); } }
显然,我们在这里定义了直尺等多个物品。接下来就要具体实现他们的类
3.实现道具 3.1实现基类 我先写了一个类rulerTool,继承Item,作为所有基础测量工具的子类:
这里限制了所有工具的堆叠数量,以及右键到方块的逻辑useOnBlock,还要存储测量结果的内部类,以及计算测量距离的算法。因为计算距离的算法是用坐标计算的,所以跨纬度使用时完全没用,因此记录使用的维度用来禁止两个点跨纬度计算
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 package com.liuliy.ruler.items;import net.fabricmc.api.EnvType;import net.fabricmc.api.Environment;import net.minecraft.entity.player.PlayerEntity;import net.minecraft.item.Item;import net.minecraft.item.ItemUsageContext;import net.minecraft.text.Text;import net.minecraft.util.ActionResult;import net.minecraft.util.math.BlockPos;import net.minecraft.util.math.Direction;import net.minecraft.world.World;import java.util.HashMap;import java.util.Map;public abstract class RulerTool extends Item { protected static final Map<PlayerEntity, MeasurementData> MEASUREMENTS = new HashMap <>(); public RulerTool (Settings settings) { super (settings.maxCount(1 )); } @Override public ActionResult useOnBlock (ItemUsageContext context) { PlayerEntity player = context.getPlayer(); BlockPos pos = context.getBlockPos(); Direction direction = context.getSide(); World world = context.getWorld(); if (!context.getWorld().isClient) { return ActionResult.PASS; } if (player.isSneaking()) { MEASUREMENTS.remove(player); player.sendMessage(Text.translatable("message.ruler-mod.measure_clear" ), false ); clearParticle(); return ActionResult.SUCCESS; } handleMeasurement(player, world, pos,direction); return ActionResult.SUCCESS; } protected abstract ActionResult handleMeasurement ( PlayerEntity player, World world, BlockPos pos ,Direction dir ) ; protected abstract void clearParticle () ; protected static class MeasurementData { public BlockPos[] points = new BlockPos [2 ]; public World[] worlds = new World [2 ]; public int step = 0 ; public MeasurementData () { points[0 ] = null ; points[1 ] = null ; } } protected static double calculateDistance (BlockPos a, BlockPos b) { int dx = b.getX() - a.getX(); int dy = b.getY() - a.getY(); int dz = b.getZ() - a.getZ(); return Math.max(Math.abs(dx),Math.max(Math.abs(dy),Math.abs(dz))); } }
还增加了一个RulerToolPlus,区别主要是一个需要点击方块useOnBlock,一个可以凭空远距离右键use
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 package com.liuliy.ruler.items;import net.fabricmc.api.EnvType;import net.fabricmc.api.Environment;import net.minecraft.entity.player.PlayerEntity;import net.minecraft.item.Item;import net.minecraft.item.ItemStack;import net.minecraft.text.Text;import net.minecraft.util.Hand;import net.minecraft.util.TypedActionResult;import net.minecraft.util.hit.BlockHitResult;import net.minecraft.util.hit.HitResult;import net.minecraft.util.math.BlockPos;import net.minecraft.util.math.Direction;import net.minecraft.world.World;import java.util.HashMap;import java.util.Map;public abstract class RulerToolPlus extends Item { protected static final Map<PlayerEntity, MeasurementData> MEASUREMENTS = new HashMap <>(); public RulerToolPlus (Settings settings) { super (settings.maxCount(1 )); } @Override public TypedActionResult<ItemStack> use (World world, PlayerEntity player, Hand hand) { if (!world.isClient) { return TypedActionResult.pass(player.getStackInHand(hand)); } BlockHitResult hitResult = (BlockHitResult) player.raycast(128 , 0 , false ); if (hitResult.getType() != HitResult.Type.BLOCK) { player.sendMessage(Text.translatable("message.ruler-mod.aim_at_block" ), false ); return TypedActionResult.fail(player.getStackInHand(hand)); } BlockPos pos = hitResult.getBlockPos(); Direction direction = hitResult.getSide(); if (player.isSneaking()) { MEASUREMENTS.remove(player); clearParticle(); return TypedActionResult.success(player.getStackInHand(hand)); } return handleMeasurement(player, world, hand, pos,direction); } protected abstract TypedActionResult<ItemStack> handleMeasurement ( PlayerEntity player, World world, Hand hand, BlockPos pos ,Direction dir ) ; protected abstract void clearParticle () ; protected static class MeasurementData { public BlockPos[] points = new BlockPos [2 ]; public World[] worlds = new World [2 ]; public int step = 0 ; public MeasurementData () { points[0 ] = null ; points[1 ] = null ; } } protected static double calculateDistance (BlockPos a, BlockPos b) { int dx = b.getX() - a.getX(); int dy = b.getY() - a.getY(); int dz = b.getZ() - a.getZ(); return Math.max(Math.abs(dx),Math.max(Math.abs(dy),Math.abs(dz))); } }
继承了基类后,子类只需要实现测量方法和清除粒子效果方法的具体实现:
3.2实现直尺 对于直尺,实现的逻辑是:
点击第一下,记录a点坐标,显示粒子效果
点击第二下,记录b点坐标,显示粒子效果并且计算出距离,用粒子效果将ab连成线
之后的点击就重复显示测量的距离,直到取消测量
取消测量后,粒子效果随之消失
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 package com.liuliy.ruler.items;import com.liuliy.ruler.client.ParticleManager;import com.liuliy.ruler.client.Visualization;import net.fabricmc.api.EnvType;import net.fabricmc.api.Environment;import net.minecraft.entity.player.PlayerEntity;import net.minecraft.item.Item;import net.minecraft.text.Text;import net.minecraft.util.ActionResult;import net.minecraft.util.math.BlockPos;import net.minecraft.util.math.Direction;import net.minecraft.util.math.Vec3d;import net.minecraft.world.World;import java.util.ArrayList;import java.util.List;import static com.liuliy.ruler.client.Visualization.spawnParticlesBetween;public class StraightRulerItem extends RulerTool { public StraightRulerItem () { super (new Item .Settings()); } PlayerEntity player; public static List<Vec3d> activePos = new ArrayList <>(); @Environment(EnvType.CLIENT) @Override protected ActionResult handleMeasurement (PlayerEntity player, World world, BlockPos pos, Direction dir) { MeasurementData data = MEASUREMENTS.computeIfAbsent(player, p -> new MeasurementData ()); this .player=player; if (data.step == 0 ) { data.points[0 ] = pos; data.step = 1 ; data.worlds[0 ]=world; player.sendMessage(Text.translatable("message.ruler-mod.measure_start" ), false ); ParticleManager.addParticle(data.points[0 ],dir); activePos.add(Visualization.getPosition(pos, dir)); } else if (data.step == 1 ) { data.points[1 ] = pos; data.worlds[1 ]=world; if (data.worlds[0 ]!=world){ player.sendMessage(Text.translatable("message.ruler-mod.cross_dimension_error" ), false ); return ActionResult.FAIL; } double distance = calculateDistance(data.points[0 ], data.points[1 ]); player.sendMessage(Text.translatable("message.ruler-mod.measure_distance" , (int )Math.floor(distance+1 ) ), false ); data.step = 2 ; ParticleManager.addParticle(data.points[1 ],dir); activePos.add(Visualization.getPosition(pos, dir)); spawnParticlesBetween(world, data.points[0 ], data.points[1 ],dir,"ruler" ); }else if (data.step == 2 ) { double distance = calculateDistance(data.points[0 ], data.points[1 ]); player.sendMessage(Text.translatable("message.ruler-mod.measure_distance" , (int )Math.floor(distance+1 ) ), false ); } return ActionResult.SUCCESS; } @Environment(EnvType.CLIENT) @Override protected void clearParticle () { for (Vec3d pos : StraightRulerItem.activePos){ ParticleManager.removeParticle(pos); } StraightRulerItem.activePos.clear(); } }
3.3实现激光测距仪 相当于一次性的直尺,测量目标到玩家的距离,粒子效果不保留
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 package com.liuliy.ruler.items;import com.liuliy.ruler.client.ParticleManager;import net.fabricmc.api.EnvType;import net.fabricmc.api.Environment;import net.minecraft.entity.player.PlayerEntity;import net.minecraft.item.Item;import net.minecraft.item.ItemStack;import net.minecraft.text.Text;import net.minecraft.util.ActionResult;import net.minecraft.util.Hand;import net.minecraft.util.TypedActionResult;import net.minecraft.util.math.BlockPos;import net.minecraft.util.math.Direction;import net.minecraft.world.World;import static com.liuliy.ruler.client.Visualization.spawnParticlesBetweenNow;public class LaserRangefinderItem extends RulerToolPlus { public LaserRangefinderItem () { super (new Item .Settings()); } @Environment(EnvType.CLIENT) @Override protected TypedActionResult<ItemStack> handleMeasurement (PlayerEntity player, World world, Hand hand, BlockPos pos, Direction dir) { double distance = calculateDistance(player.getBlockPos(), pos); player.sendMessage(Text.translatable("message.ruler-mod.measure_distance" ,(int )Math.floor(distance+1 )), false ); spawnParticlesBetweenNow(world,player.getBlockPos(), pos,dir); return TypedActionResult.success(player.getStackInHand(hand)); } @Override protected void clearParticle () { } }
3.4实现激光直尺 在普通直尺的基础上和激光测距仪合成,不需要点击方块,可以在远处测量
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 package com.liuliy.ruler.items;import com.liuliy.ruler.client.ParticleManager;import com.liuliy.ruler.client.Visualization;import net.fabricmc.api.EnvType;import net.fabricmc.api.Environment;import net.minecraft.entity.player.PlayerEntity;import net.minecraft.item.Item;import net.minecraft.item.ItemStack;import net.minecraft.text.Text;import net.minecraft.util.ActionResult;import net.minecraft.util.Hand;import net.minecraft.util.TypedActionResult;import net.minecraft.util.math.BlockPos;import net.minecraft.util.math.Direction;import net.minecraft.util.math.Vec3d;import net.minecraft.world.World;import java.util.ArrayList;import java.util.List;import static com.liuliy.ruler.client.Visualization.spawnParticlesBetween;public class LaserRulerItem extends RulerToolPlus { public static List<Vec3d> activePos = new ArrayList <>(); public LaserRulerItem ( ) { super (new Item .Settings()); } PlayerEntity player; @Environment(EnvType.CLIENT) @Override protected TypedActionResult<ItemStack> handleMeasurement ( PlayerEntity player, World world, Hand hand, BlockPos pos, Direction dir) { MeasurementData data = MEASUREMENTS.computeIfAbsent(player, p -> new MeasurementData ()); this .player=player; if (data.step == 0 ) { data.points[0 ] = pos; data.step = 1 ; data.worlds[0 ]=world; player.sendMessage(Text.translatable("message.ruler-mod.measure_start" ), false ); ParticleManager.addParticle(data.points[0 ],dir); activePos.add(Visualization.getPosition(pos, dir)); } else if (data.step == 1 ) { data.points[1 ] = pos; data.worlds[1 ]=world; if (data.worlds[0 ]!=world){ player.sendMessage(Text.translatable("message.ruler-mod.cross_dimension_error" ), false ); return TypedActionResult.fail(player.getStackInHand(hand)); } double distance = calculateDistance(data.points[0 ], data.points[1 ]); player.sendMessage(Text.translatable("message.ruler-mod.measure_distance" ,(int )Math.floor(distance+1 )), false ); data.step = 2 ; ParticleManager.addParticle(data.points[1 ],dir); activePos.add(Visualization.getPosition(pos, dir)); spawnParticlesBetween(world, data.points[0 ], data.points[1 ],dir,"laserRuler" ); }else if (data.step == 2 ) { double distance = calculateDistance(data.points[0 ], data.points[1 ]); player.sendMessage(Text.translatable("message.ruler-mod.measure_distance" ,(int )Math.floor(distance+1 )), false ); } return TypedActionResult.success(player.getStackInHand(hand)); } @Environment(EnvType.CLIENT) @Override protected void clearParticle () { if (this .player == null ) { return ; } player.sendMessage(Text.translatable("message.ruler-mod.measure_clear" ), false ); for (Vec3d pos : LaserRulerItem.activePos){ ParticleManager.removeParticle(pos); } LaserRulerItem.activePos.clear(); } }
4.粒子效果 前面实现了测量后,我们需要呈现出粒子效果
我在这里使用了两个类来管理
4.1 Visualization 用于实现呈现粒子效果的方法,计算粒子效果的坐标.
需要注意的一点是,我通过在尺子自身的类中记录创建粒子效果的位置,用于取消测量时消除对应的粒子效果。但是生成一条直线时,我的粒子效果的位置是通过一个公共的方法spawnParticlesBetween中计算得知的,因此需要传入一个字符串,来表明生成的粒子效果属于谁,再记录。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 package com.liuliy.ruler.client;import com.liuliy.ruler.items.LaserRulerItem;import com.liuliy.ruler.items.StraightRulerItem;import net.minecraft.particle.ParticleTypes;import net.minecraft.util.math.BlockPos;import net.minecraft.util.math.Direction;import net.minecraft.util.math.Vec3d;import net.minecraft.world.World;import net.fabricmc.api.EnvType;import net.fabricmc.api.Environment;@Environment(EnvType.CLIENT) public class Visualization { @Environment(EnvType.CLIENT) public static void spawnParticleAt (World world, Vec3d position) { if (world.isClient) { world.addParticle(ParticleTypes.END_ROD, position.x, position.y, position.z, 0 , 0 , 0 ); } } @Environment(EnvType.CLIENT) public static void spawnParticlesBetween (World world, BlockPos point1, BlockPos point2,Direction dir,String item) { if (world.isClient) { double dx = point2.getX() - point1.getX(); double dy = point2.getY() - point1.getY(); double dz = point2.getZ() - point1.getZ(); int particleCount = (int ) Math.max(Math.abs(dx), Math.max(Math.abs(dy), Math.abs(dz))); Vec3d offset = getOffset(dir); for (int i = 0 ; i <= particleCount; i++) { double fraction = i / (double ) particleCount; double x = point1.getX() + dx * fraction; double y = point1.getY() + dy * fraction; double z = point1.getZ() + dz * fraction; Vec3d position = new Vec3d (x,y,z).add(offset); ParticleManager.addParticle(position,dir); switch (item){ case "ruler" : StraightRulerItem.activePos.add(position); break ; case "laserRuler" : LaserRulerItem.activePos.add(position); break ; default : break ; } } } } @Environment(EnvType.CLIENT) public static void spawnParticlesBetweenNow (World world, BlockPos point1, BlockPos point2,Direction dir) { if (world.isClient) { double dx = point2.getX() - point1.getX(); double dy = point2.getY() - point1.getY(); double dz = point2.getZ() - point1.getZ(); int particleCount = (int ) Math.max(Math.abs(dx), Math.max(Math.abs(dy), Math.abs(dz))); Vec3d offset = getOffset(dir); for (int i = 0 ; i <= particleCount; i++) { double fraction = i / (double ) particleCount; double x = point1.getX() + dx * fraction; double y = point1.getY() + dy * fraction; double z = point1.getZ() + dz * fraction; spawnParticleAt(world,new Vec3d (x,y,z).add(offset)); } } } public static Vec3d getPosition (BlockPos pos,Direction dir) { Vec3d position = new Vec3d (pos.getX(), pos.getY() , pos.getZ()).add(getOffset(dir)); return position ; } public static Vec3d getOffset (Direction dir) { Vec3d baseOffset = new Vec3d (0.5 , 0.5 , 0.5 ); double Value=0.7 ; Vec3d faceOffset = switch (dir) { case NORTH -> new Vec3d (0 , 0 , -Value); case SOUTH -> new Vec3d (0 , 0 , Value); case WEST -> new Vec3d (-Value, 0 , 0 ); case EAST -> new Vec3d (Value, 0 , 0 ); case UP -> new Vec3d (0 , Value, 0 ); case DOWN -> new Vec3d (0 , -Value, 0 ); default -> Vec3d.ZERO; }; return baseOffset.add(faceOffset); } }
4.2 ParticleManager 生成粒子效果后,我们将获得的坐标传入 ParticleManager中进行管理,在这里用activeParticles存储了所有正在显示的粒子效果,并且注册了一个事件来进行每tick更新,保持粒子效果的持续显示
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 package com.liuliy.ruler.client;import net.fabricmc.api.EnvType;import net.fabricmc.api.Environment;import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientTickEvents;import net.minecraft.util.math.BlockPos;import net.minecraft.util.math.Direction;import net.minecraft.util.math.Vec3d;import java.util.ArrayList;import java.util.List;import static com.liuliy.ruler.client.Visualization.*;public class ParticleManager { private static final List<ActiveParticle> activeParticles = new ArrayList <>(); public static class ActiveParticle { public BlockPos blockPos; public Vec3d position; public Direction direction; public long startTime; public boolean isParticleActive; public ActiveParticle (Vec3d position, Direction dir) { this .position=position; this .direction = dir; this .startTime = System.currentTimeMillis(); isParticleActive=true ; } } public static void init () { ClientTickEvents.END_CLIENT_TICK.register(client -> { if (client.world != null ) { activeParticles.forEach(ActiveParticle -> spawnParticleAt(client.world, ActiveParticle.position) ); } }); } public static void addParticle (BlockPos pos, Direction dir) { activeParticles.add(new ActiveParticle (getPosition(pos,dir), dir)); } public static void addParticle (Vec3d pos, Direction dir) { activeParticles.add(new ActiveParticle (pos, dir)); } public static void removeParticle (Vec3d pos) { activeParticles.removeIf(m -> m.position.equals(pos)); } public static void removeParticle () { activeParticles.clear(); } }
5.图形材质 在resources/assets/ruler-mod/models/item/straight_ruler.json中存放模型json文件表示渲染方式,这里只举个直尺的例子:
注意种类的ruler-mod就是前文配置文件中的modid
1 2 3 4 5 6 { "parent" : "minecraft:item/generated" , "textures" : { "layer0" : "ruler-mod:item/straight_ruler" } }
同理在对应的resources/assets/ruler-mod/textures/item中存放贴图文件
前文的注册中有引用贴图资源: Registry.register(Registries.ITEM, new Identifier(“ruler-mod”, “straight_ruler”), STRAIGHT_RULER);
这里的model文件和贴图文件,以及代码中声明的名词要相同即都是“straight_ruler”
6.自定义物品栏 我们以及建立了物品,但是目前只能通过指令获得,接下来就可以做一个自定义物品栏,呈现所有的内容。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 package com.liuliy.ruler;import com.liuliy.ruler.registry.ModItems;import net.fabricmc.fabric.api.itemgroup.v1.FabricItemGroup;import net.minecraft.item.ItemGroup;import net.minecraft.item.ItemStack;import net.minecraft.registry.Registries;import net.minecraft.registry.Registry;import net.minecraft.registry.RegistryKey;import net.minecraft.text.Text;import net.minecraft.util.Identifier;public class ModItemGroup { public static void registerModItemGroup () { } public static final RegistryKey<ItemGroup> RULER_ITEM_GROUP_KEY = RegistryKey.of(Registries.ITEM_GROUP.getKey(), Identifier.of(RulerMod.MOD_ID, "ruler_group" )); public static final ItemGroup Ruler_ITEM_GROUP = FabricItemGroup.builder() .icon(() -> new ItemStack (ModItems.STRAIGHT_RULER)) .displayName(Text.translatable( "itemGroup.ruler-mod.group_name" )) .build(); }
在这个代码中,我们声明了一个物品栏类ModItemGroup,以及他的构造函数,还有一个用于获取物品栏的key “RULER_ITEM_GROUP_KEY”,以及物品栏本身 “Ruler_ITEM_GROUP”。
接下来还是老样子,进行注册,我在前面的代码已经包括了这部分的内容,分别在RulerMod和ModItems中进行注册
1 2 3 4 5 6 7 8 9 @Override public void onInitialize () { ModItems.register(); ParticleManager.init(); ModItemGroup.registerModItemGroup(); }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 public static void register () { Registry.register(Registries.ITEM, new Identifier ("ruler-mod" , "straight_ruler" ), STRAIGHT_RULER); Registry.register(Registries.ITEM, new Identifier ("ruler-mod" , "laser_rangefinder" ), LASER_RANGEFINDER); Registry.register(Registries.ITEM, new Identifier ("ruler-mod" , "laser_ruler" ), LASER_RULER); Registry.register(Registries.ITEM_GROUP, RULER_ITEM_GROUP_KEY, Ruler_ITEM_GROUP); ItemGroupEvents.modifyEntriesEvent(RULER_ITEM_GROUP_KEY).register(ModItems::addItemsToIG); }private static void addItemsToIG (FabricItemGroupEntries fabricItemGroupEntries) { fabricItemGroupEntries.add(STRAIGHT_RULER); fabricItemGroupEntries.add(LASER_RANGEFINDER); fabricItemGroupEntries.add(LASER_RULER); }
7.自定义配方 增加自定义物品栏后,我们能在创造模式下获得我们的物品了,但是生存模式并不行,所有接下来我们要添加对应的配方。
fabric有一个自动生成配方的方式,用代码的方式配置之后, 就会自动生成对应的json文件。但是那样比较麻烦,我还没弄明白,加上这个mod的内容较少,所以选择直接自己写json文件:
目录为resources/data/ruler-mod/recipes/straight_ruler.json,名字也跟之前一样
不过要注意的是这里的写法只适用于1.20~1.20.4,低版本我并没有进行测试,但是高版本中高版本好像说recipes是recipe;结果 item是id。我也还没有进行测试
下面的”B” “G”是可以自己随便改的,只是一个指代符号。对应的物品id可以上wiki查找
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 //straight_ruler { "type": "minecraft:crafting_shaped" , "pattern" : [ " GG" , "GBG" , "GG " ], "key" : { "B ": { "item": "minecraft:black_dye" }, "G ": { "item": "minecraft:glass_pane" } }, "result": { "item": "ruler-mod:straight_ruler" , "count" : 1 } }
8.本地化 翻看前面的代码可以看见,当我向玩家发送信息时,用的是 player.sendMessage(Text.translatable(“message.ruler-mod.measure_distance” ,这里的“message.ruler-mod.measure_distance”就是用来本地化的内容。
通过在resources/assets/ruler-mod/lang/zh_cn.json中定义相关的翻译,即可根据游戏的语言显示对应的语言. .zh_cn对应的是中文,en_us就是英文
1 2 3 4 5 6 7 8 9 10 11 12 13 14 //zh_cn.json { "item.ruler-mod.straight_ruler" : "直尺" , "item.ruler-mod.laser_rangefinder" : "激光测距仪" , "item.ruler-mod.laser_ruler" : "激光直尺" , "message.ruler-mod.measure_start" : "§a起点已记录!" , "message.ruler-mod.cross_dimension_error" : "§c尺子不能跨维度使用!" , "message.ruler-mod.measure_clear" : "§a已清除测量标记!" , "message.ruler-mod.measure_distance" : "§a两点间的距离: %1$s §a格" , "message.ruler-mod.aim_at_block" : "§c请对准方块使用" , "itemGroup.ruler-mod.group_name" : "尺子" }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 //en_us { : , : , : , : , : , : , : , : , : , : }
五、注意事项
开发过程中踩了很多坑,其中有一个非常难忘:默认每一条代码都会在服务端和客户端都计算一次 这导致了有的+1操作变成了+2,在刚开始开发时对我造成了很大的困扰,一直找不到bug所在。因此程序中很多地方进行了判断: (world.isClient),判断是客户端才进行执行而服务端不执行。不过我整体的代码对服务器和客户端代码的处理并不严谨,因为涉及的逻辑较少,就有混用的情况.
单机游戏也相当于同时存在客户端和服务端。一般来说
服务端主要负责游戏逻辑处理
客户端主要负责画面和音效
六、发布 我分别把mod发布在了,具体发布方式就不赘述了
至此,复盘结束