The Graveyard - Modding
type: channel
link: https://blog.constance.moe/index.php/tag/modding/
items: 末影龙核心行为的底层实现的简要分析
items: 召唤扩展Mod指令使用教程
items: （MC1.16.5）float的精度引起的一个渲染问题

末影龙核心行为的底层实现的简要分析
type: item
date: 2024-07-21T01:46:00+08:00
description: 补充内容2024-08-09附上视频：2024-08-07本文中对“寻路”的理解可能存在一定的问题。我查了维基百科上对“寻路”一词的解释，上面说寻路是“由计算机应用程序规划两点之间的最短路线”。因此实际上此处应该是不包括具体飞向路径的过程的。为了与视频中内容尽量呼应而不自相矛盾，我没有修改正文中的内容2024-08-03下文中所有“已编号节点”的编号都是“MC规定好”的，而不是我自己规定的下文中所有阶段的中文名是我综合了Wiki中的末影龙页面对末影龙行为的描述、末影龙各个阶段的类的MCP名以及各个阶段的实际作用而命名的前言近来笔者想投稿与末影龙原理相关的视频，但笔者虽然对末影龙有一定的了解，却尚未对末影龙进行较细致的分析，因此写了这样一篇文章。笔者写这篇文章既是为了给自己即将制作的视频理清思路，也是为了提供一些有关末影龙的代码层面的解读。本文代码部分较多，因此较适合对Java与Minecraft模组制作有一定的了解的读者进行阅读。当然本文对于不了解Java语言或Minecraft modding的读者也有一定的参考意义。注：使用的Minecraft版本为1.20.2，Forge版本为48.0.30，MCP版本为20230921.100330，笔者曾查看了1.16.5与1.21版本中末影龙的底层实现，总体来看均没有明显差异（当然不排除笔者有疏忽的可能）本文只是简要的分析，因而只会选择核心内容，并不面面俱到。1.20.1和1.20.2中末影龙的代码是没有区别的，但以后更新Polonium上有关末影龙的内容时笔者会写得更详细一些~在原版世界中，末影龙是驻守在末路之地的一个强大的龙形生物，也是原版Minecraft的最终boss，不少玩家将击杀末影龙作为通关MC的标准。末影龙的底层实现也相当复杂，它不仅使用了一套独立的AI系统，寻路的算法也与一般的生物有一定的区别。一年多前笔者曾经仿照末影龙在一个未发布的mod中制作了一种与末影龙相似，但行为更复杂的龙型生物作为自定义维度的一个小boss，因此对末影龙有一定的了解。本文将从末影龙的寻路系统，末影龙的AI，及末影龙的其他重要行为这三个方面进行分析。末影龙的寻路系统在分析寻路系统之前，先介绍一下末影龙的24个已编号的路径节点（下文中会把这24个路径节点称为“已编号节点”）的分布。已编号节点的分布末影龙的已编号节点共分为3圈，其中外圈12个节点，中圈8个节点，内圈4个节点。可以用以下的一张图来表示（下图中青色点表示坐标原点，淡青色细线表示坐标轴，红色、蓝色箭头所指方向分别为x、z轴正半轴方向，蓝绿色、绿色、黄绿色点分别表示外、中、内圈的已编号节点）：将外圈已编号节点用0~11编号，中圈已编号节点用12~19编号，内圈已编号节点用20~23编号，可得到下图：而查阅源代码可知，外、中、内圈的已编号节点距离原点的水平距离分别近似为60、40、20。将它们分别放在3个同心圆上可得下图（注意不一定所有已编号节点都恰好位于同心圆上，因为任何路径节点的坐标都是取整存储的）：这是一张末地的地形图（从30多张末地地形图中选出来的比较好看的一张233）：两图叠加可得：从这张叠加的图中我们可以直观地看出24个已编号节点的大致分布情况，同时可以发现中圈与黑曜石柱所在圆的位置相近（黑曜石柱中心到原点的水平距离约为42，这个数值与中圈的已编号节点到原点的水平距离40接近）。末影龙的寻路流程下面正式开始分析末影龙的寻路系统，先看EnderDragon类里声明的这样的3个与寻路相关的成员变量。private final Node[] nodes = new Node[24]; private final int[] nodeAdjacency = new int[24]; private final BinaryHeap openSet = new BinaryHeap();其中nodes是末影龙的路径节点数组，其中所有节点的x、z坐标分别是定值，但y坐标根据末地的实际地形与节点在哪一圈来确定。nodeAdjacency是邻接矩阵，用于记录与每个节点相邻的节点的编号。而openSet是二叉堆，在末影龙的寻路过程中起到作用。总的来说，末影龙的寻路过程的步骤可分为这样几步：确定起点和终点的位置寻找起点到终点的一条最短路径。这条路径中除了终点之外，其余的路径节点各自一定是已编号节点中的一个根据寻路得到的路径，一步一步按路径点飞向终点[note type="info flat"]2024-08-07更新：这里的“寻路过程”改为“寻路与沿路径飞行的过程”更合适，因为第三步实际上不是“寻路”过程中的一部分[/note]末影龙每次寻路时，都需要先确定到自己最近的节点的编号，下面两个方法则寻找了已编号节点中离末影龙自己最近的节点。publ…

召唤扩展Mod指令使用教程
type: item
date: 2023-07-14T20:36:00+08:00
description: 文章说明下文中(op)表示暂时使用的参数名[op]表示可选操作或参数op1/op2表示并列的必需操作或参数[op1]/[op2]表示并列的可选操作或参数指令格式指令名：/summoningex (op)（其中op的类型为area或summon）area提供了对召唤区域（Summoning Area）的操作格式：/summoningex area (op)（其中op的类型为define，list，query，remove，show或hide）define用于定义和命名召唤区域格式：/summoningex area define (x1), (y1), (z1), (x2), (y2), (x2) as #(name) [colored (colorop) (colorval) [in (dim)]]/[in (dim)]x1, y1, z1和x2, y2, z2定义了召唤区域的位置（以（x1, y1, z1）与（x2, y2, z2）两个方块坐标（BlockPos）为顶点）注意x1, y1, z1, x2, y2, z2为整数且x1, x2之间，y1, y2之间与z1, z2之间没有严格的大小限制（也就是说x1既可以大于x2，又可以等于或小于x2）x1与x2，y1与y2，z1与z2都相同时表示了一个但方块的召唤区域name定义了召唤区域的名称（指令中的"#"号不可省略且不能为中文）colorop（可以传入custom或preset）及colorval当color传入custom时，colorval应传入一个[0, 16777215]间的十进制整数，表示了召唤区域的RGB颜色值当color传入preset时，colorval应传入原版的一种染料颜色（DyeColor），该颜色表示了召唤区域的颜色（但这个颜色的饱和度最大值被限制在了88%）若不指定颜色则会随机为召唤区域分配一个颜色相关代码染料颜色的获取（getDyeColor(CommandContext, String)静态方法返回输入的染料颜色）：public static int getIntDyeColor(CommandContext<CommandSourceStack> context, String name) { int textColor = getDyeColor(context, name).getTextColor(); float[] hsbvals = new float[3]; Color.RGBtoHSB(textColor >> 16 & 0xFF, textColor >> 8 & 0xFF, textColor & 0xFF, hsbvals); // Restricts the saturation hsbvals[1] = Math.min(0.88f, hsbvals[1]); int color = Color.HSBtoRGB(hsbvals[0], hsbvals[1], hsbvals[2]); // Make sure the color value is positive color &= 0xffffff; return color; }颜色的随机分配private static int randomColor(RandomSource random) { // Randomly selects hue from 0 to 0.95 (step=0.05) float hue = 0.05f * random.nextInt(20); // Two types of saturation - low(0.5) and high(0.8) float saturation = random.nextBoolean() ? 0.5f : 0.8f; // Randomly selects hue from 0.5 to 1 (step=0.125) float brightness = 0.5f + 0.125f * random.nextInt(5); return Color.HSBtoRGB(hue, saturation, brightness); }dim定义了召唤区域所在的维度若不指定维度则会使用当前所在维度举例/summoningex area define 0 100 0 10 110 10 as #Example colored custom 16777215 in minecraft:overworld在主世界定义了一个名为Example，两个顶点分别为（0，100，0），（10，110，10）两个方块坐标，颜色为白色的召唤区域/summoningex area define 20 100 20 25 9…

（MC1.16.5）float的精度引起的一个渲染问题
type: item
date: 2023-04-29T08:41:00+08:00
description: 引入在世界边境附近调试Mod实体时，偶然发现了这样的问题（图中激光渲染的位置出现异常偏移）这是怎么回事呢？常用技巧在Minecraft的Mod开发中，开发者们可能常使用以下的代码辅助世界的渲染：Vector3d projectedView = Minecraft.getInstance().gameRenderer.getMainCamera().getPosition(); matrixStack.translate(-projectedView.x, -projectedView.y, -projectedView.z);这样做的好处显而易见——可以在渲染中用绝对坐标替换相对坐标。不过使用这样的方式，有时也会导致一些问题，先从float和double的有效数字分析Java中的浮点数下表为float和double的一些属性比较属性floatdouble符号位/bit11指数位/bit811尾数位/bit2352占用内存空间4 Byte (32bit)8 Byte (64bit)由此可知float的有效数字位数为6~7位，而double则为15~16位float的局限性MatrixStack的translate方法以下是MatrixStack类中translate方法的源代码public void translate(double x, double y, double z) { MatrixStack.Entry entry = this.poseStack.getLast(); entry.pose.multiply(Matrix4f.createTranslateMatrix((float) x, (float) y, (float) z)); }可以发现，此方法内做了一个强制类型转换，将双精度的double转换为了单精度的float！世界的大小在World（SRG名，1.16.5）类中，有如下的私有静态方法private static boolean isInWorldBoundsHorizontal(BlockPos pos) { return pos.getX() >= -30000000 && pos.getZ() >= -30000000 && pos.getX() < 30000000 && pos.getZ() < 30000000; }由此可知，MC的世界是60000001x60000001格的，这会使得当坐标足够大时，用float表示坐标会使float的小数部分丢失，从而发现了问题所在问题发生的原因在未解决bug时，由于直接在translate中传入世界真实坐标，导致强制类型转换，进而导致double后面的小数部分丢失问题的解决translate时，传入激光起始位置的坐标与玩家坐标的差，使数字变小，以确保float的小数部分保留Vector3d cameraPos = Minecraft.getInstance().gameRenderer.getMainCamera().getPosition(); double x = src.x - cameraPos.x; double y = src.y - cameraPos.y; double z = src.z - cameraPos.z; matrixStack.translate(x, y, z);最后效果如下图所示激光渲染的位置的异常偏移消失了！总结float不精确，开发中无性能需求应尽量使用double（虽然也不是100%精确）Minecraft的世界坐标（x和z）的绝对值上限为30000000而不能无限增大，一定程度上与double的精度限制有关（毕竟每个世界坐标都是double的，但主要还是因为int能表示的范围有限）原版下实体的生命上限为1024，这也在一定程度上与float的精度限制有关