【项目复盘】Minecraft mod开发复盘 - Ruler

mc mod开发复盘-Ruler

一、需求分析

在对Java技术有一定掌握后,萌生了为儿时圆梦,想进行MinecraftMod开发。但是第一次开发mc的mod对各个接口都不熟悉,肯定要选择先开发轻量级的mod练手,于是就想起之前一个个数格子建东西的不便经历,选择开发一个能测量方块的直尺mod。

二、环境搭建

  1. 参考了b站up主北山的教程:模组开发指北- 文集 哔哩哔哩专栏,在idea导入fabric的mod模板,进行1.20的mod开发。

  2. 修改对应的配置文件:

    1. 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", //mod的唯一标识符
      "version": "${version}",
      "name": "Ruler Mod",
      "description": "提供了几个用于测量的尺子", //mod迷哦奥数
      "authors": [
      "Liuliy" //贡献者
      ],
      "custom": {
      "modmenu": {
      "links": {
      "modmenu.issues": "https://github.com/Liuliy1/MinecraftRulerMod/issues"
      }
      }
      },
      "contact": {
      "sources": "https://github.com/Liuliy1/MinecraftRulerMod"
      },
      "license": "MIT", //遵循的开源协议,在LICENSE文件中修改完整内容
      "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();
//注册mod物品栏
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)); // 限制物品堆叠数量为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;
}

// Shift+右键清除测量
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)); // 限制物品堆叠数量为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();

// Shift+右键清除测量
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实现直尺

对于直尺,实现的逻辑是:

  1. 点击第一下,记录a点坐标,显示粒子效果
  2. 点击第二下,记录b点坐标,显示粒子效果并且计算出距离,用粒子效果将ab连成线
  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
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);
}
// ParticleManager.removeParticle();
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);
}
// ParticleManager.removeParticle();
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){
// 计算粒子效果的位置:离方块面向玩家的面0.5格的距离
// 基础偏移量:方块中心点 (0.5, 0.5, 0.5)
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); // 北面:Z轴-0.5方向,略微突出
case SOUTH -> new Vec3d(0, 0, Value); // 南面:Z轴+0.5方向
case WEST -> new Vec3d(-Value, 0, 0); // 西面:X轴-0.5方向
case EAST -> new Vec3d(Value, 0, 0); // 东面:X轴+0.5方向

// ----------- 顶部/底部 -----------
case UP -> new Vec3d(0, Value, 0); // 顶面:Y轴+0.5方向
case DOWN -> new Vec3d(0, -Value, 0); // 底面:Y轴-0.5方向

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;
}
}

// 注册客户端Tick事件
public static void init() {
ClientTickEvents.END_CLIENT_TICK.register(client -> {
if (client.world != null ) {
// 每Tick更新所有活动测量
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
//RulerMod.cs
@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
//ModItems.cs
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
{
"item.ruler-mod.straight_ruler": "Ruler",
"item.ruler-mod.laser_rangefinder": "Laser Rangefinder",
"item.ruler-mod.laser_ruler": "Laser Ruler",
"message.ruler-mod.measure_start": "§aThe starting point is recorded!",
"message.ruler-mod.cross_dimension_error": "§cRulers cannot be used across dimensions!",
"message.ruler-mod.measure_clear": "§aThe measure marker has been cleared!",
"message.ruler-mod.measure_distance": "§aThe distance between the two points : %1$s §ablocks",
"message.ruler-mod.measure_block": "§aBlock:",
"message.ruler-mod.aim_at_block": "§cPlease aim at the block",
"itemGroup.ruler-mod.group_name": "Ruler"

}

五、注意事项

  1. 开发过程中踩了很多坑,其中有一个非常难忘:默认每一条代码都会在服务端和客户端都计算一次
    这导致了有的+1操作变成了+2,在刚开始开发时对我造成了很大的困扰,一直找不到bug所在。因此程序中很多地方进行了判断: (world.isClient),判断是客户端才进行执行而服务端不执行。不过我整体的代码对服务器和客户端代码的处理并不严谨,因为涉及的逻辑较少,就有混用的情况.
  2. 单机游戏也相当于同时存在客户端和服务端。一般来说
    1. 服务端主要负责游戏逻辑处理
    2. 客户端主要负责画面和音效

六、发布

我分别把mod发布在了,具体发布方式就不赘述了

至此,复盘结束


【项目复盘】Minecraft mod开发复盘 - Ruler
http://example.com/2025/02/25/Minecraft直尺mod开发复盘-1/
作者
Liuliy
发布于
2025年2月25日
许可协议