维度 & 传送门 26.1 Fabric
本篇教程的视频
本篇教程的源代码
GitHub地址:
本篇教程目标
- 搞清维度要写哪两份数据
- 用数据生成写维度类型,手写维度本体
- 自己实现一个传送门:方块、框架检测、点燃、传送
- 知道水平传送门、流体激活该怎么改
介绍
这是这个系列最后一期
维度就是一个独立的世界:自己的群系、自己的地形、自己的规则
做维度要干两件事
一是告诉游戏「有这么个维度」,这是纯数据
二是告诉玩家「怎么进去」,这就是传送门
传送门为什么要自己写
以前做传送门有现成的库,比如 CustomPortalAPI,几行代码就能建一个门,框架方块、激活物品、目的地都能指定
现在这个库跟不上了,我们没有能直接用的东西
所以这期自己写一个,照着原版下界传送门的思路抄一遍
其实原版的逻辑不复杂,核心就三块:一个门方块、一段框架检测、一个传送目的地
第一部分:维度
两份数据
dimension_type:这个维度是什么样。有没有天空光、有没有天花板、多高、床能不能睡、时间流不流
dimension:这个维度的地形从哪来。用哪个区块生成器、哪个群系
名字起得有点像,别混:前者是「属性」,后者是「内容」
维度类型
新建 ModDimensions,先写注册键
1 | public static final ResourceKey<Level> ICE_ETHER_LEVEL = |
第一个是运行时用来找世界的键,第二个是维度类型的键
然后 bootstrap
1 | public static void bootstrapType(BootstrapContext<DimensionType> context) { |
26.1 的 DimensionType 比老版本多了好几个参数,都是这套新系统带进来的
Skybox 决定抬头看到什么(主世界、下界、末地、没有)
CardinalLighting 是「按方向来的光照」,主世界默认,下界和末地是另一套
EnvironmentAttributeMap 就是上一期群系用的那个环境属性表
这里不能给空,得把主世界那套抄过来
这里踩过的一个坑:地平线以下发黑
第一版我图省事,直接塞了 EnvironmentAttributeMap.EMPTY
进维度一看,头顶的天空还行,但地平线以下是一片死黑,飞高了往下看很诡异
原因是天空渲染要靠环境属性里的 FOG_COLOR 来画远处
给它空,远处就没有雾色可混,直接按黑色画
主世界那份写得很清楚
1 | .set(EnvironmentAttributes.FOG_COLOR, -4138753) // #FFC0D8FF |
所以整段照抄过来就对了
顺便把 BED_RULE(能不能睡)、BACKGROUND_MUSIC(放什么音乐)、AMBIENT_SOUNDS(洞穴音效)也一起抄了,不然维度里睡觉会提示无法入睡、音乐也怪
EnvironmentAttributes 这些键在 26.1 是 minecraft:visual/fog_color 这种带斜杠的名字,看生成的 json 一眼就懂
timelines 和 defaultClock 是 26.1 新的「时间线 / 世界时钟」系统,我们直接借主世界的
三个高度为什么是 -64 / 384,而不是随手写的 0 / 256
因为维度本体里 settings 用的是 minecraft:overworld,那套噪声是照着 -64 到 320 设计的
维度类型的高度比它小,地形就会被从底下削掉一截,进去一看脚下全是深板岩
后面「维度本体」那节会再讲一遍
维度本体
维度本体是另一份 json,但它不能靠数据生成,只能手写
这份 json 的骨架很简单,难的是 biome_source:这个维度里放哪些群系、它们怎么分布
用可视化工具生成
手写那堆气候参数很容易出错,好在有个现成的生成器
打开 https://misode.github.io/dimension/
页面分三块
- 左边是树形的参数编辑区,展开一层就是 json 里的一层
- 右上是一大块可视化预览,能直接看到群系在世界里怎么分布
- 右下是
Files,里面是它帮你生成好的文件,dimension/xxx.json和dimension_type/xxx.json各一份
我们只要 dimension 那份,维度类型那一份我们自己用数据生成写
怎么填
先在 Generator 里选 minecraft:noise,就是用原版那套噪声地形
然后把 Biome source 选成 minecraft:multi_noise,也就是多重噪声,了解过地形生成的应该都知道这个噪声是什么东西
这时候右上角会变成一张群系分布图,可以拖动着看
接着在 biomes 里一条条加
biome 填 tutorial:ice_ether_forest,命名空间加注册名,写错的话世界生成会直接崩
下面那七个参数就是上一期讲过的气候维度
temperature温度humidity湿度continentalness大陆性erosion侵蚀weirdness奇异度depth深度offset偏移
想再混几个原版群系进去,就继续加条目,比如丛林、平原、海洋
参数随便玩,改完看右上预览,觉得分布合理了就点 Files 里那份下载
我们生成出来的
结构大概是这样,完整的在我们仓库里
1 | { |
type 指向我们数据生成出来的维度类型
generator.type 用 minecraft:noise,就是原版那种噪声地形
settings 用主世界那套噪声设置,地形长什么样就看它
biome_source 里每一块都是一个参数点
游戏生成区块时,先算出这个位置的气候参数,然后在这堆点里找最近的那个,用它的群系
所以点给得越稀,边界就越硬,看起来一块一块像拼图
原版那张表有几万个点,密密麻麻铺满整个空间,主世界的群系才是连续过渡的
我们自己写几个点,能跑,但过渡会很生硬,这是正常的
别忘了改 type
下载下来的那份,type 是工具自己编的名字
我们这次就是直接下载没改,写成了 tutorial:test_type,进世界直接报维度类型找不到
要改成我们注册的那个:tutorial:ice_ether_type
这大概是这期最容易漏的一步,工具不知道我们代码里叫什么名字
只要一个群系的写法
如果你不想搞这么多参数,就想「整个维度都是冰以太森林」,用 fixed 更省事
1 | "biome_source": { |
fixed 就是整个世界一个群系,不参与气候计算,也不用填参数
multi_noise 和 fixed 二选一,看你要「混着来的世界」还是「纯一个群系的世界」
两种都行,我们仓库里现在挂的是 multi_noise 那版
高度得对得上
settings 用了 minecraft:overworld
那套噪声是照着 -64 到 320 设计的,海平面 63
进去一看:脚下一片深板岩,水面几乎贴着基岩,山上还少一截
所以维度类型里的三个高度,要和 settings 那套噪声对齐
我们把 ModDimensions 改成了 -64 / 384 / 384,跟主世界一模一样
数据生成
buildRegistry 里加一行
1 | registryBuilder.add(Registries.DIMENSION_TYPE, ModDimensions::bootstrapType); |
ModDynamicRegistryProvider 里加一行
1 | entries.addAll(registries.lookupOrThrow(Registries.DIMENSION_TYPE)); |
跑完会生成
1 | data/tutorial/dimension_type/ice_ether_type.json |
维度的本体还是手写那份
第二部分:传送门
门方块
先看原版怎么做的:NetherPortalBlock
它 implements Portal,那个接口只有两个方法
getPortalDestination:算出去哪、从哪出来
getPortalTransitionTime:站多久才会被传走
剩下的事交给 entityInside:实体碰到门方块,就把自己标记成「在传送门里」,游戏每 tick 检查
我们的门方块照着写
1 | public class IceEtherPortalBlock extends Block implements Portal { |
AXIS 是固定的那一套,叫 HORIZONTAL_AXIS,只有 X 和 Z 两个值
它表示的是「门宽度沿着哪个轴」,不是「门朝哪边」
这个区别后面会单独讲,写反了门板就会跟框架垂直
getShape 给一个 4 像素厚的板,这样玩家是「站在门里」,不是「站在方块上」
entityInside 就是原版那两行,setAsInsidePortal 之后游戏会自己计时
然后是目的地
1 |
|
逻辑是:从主世界进就送去冰以太,从冰以太进就送回主世界
目标坐标直接取玩家当前位置,先在附近 16 格找找有没有现成的门,没有就在地表现搭一个
注意这里不能写成 new BlockPos(x, 0, z)
我一开始图省事把 y 写死成 0,结果 findExisting 的搜索范围变成 y ∈ [-16, 16],而门一般在 y=60 以上
于是永远找不到,每次传送回来都在原地重新搭一个门,跟原来那个叠在一起
这个 bug 很隐蔽:门确实「能用」,就是会越来越多
TeleportTransition 是 26.1 的叫法(老版本叫 DimensionTransition),带上目的地、坐标、朝向和落地后的回调
PLAY_PORTAL_SOUND 就是传送完放个音效,PLACE_PORTAL_TICKET 是让目标区块保持加载
框架检测
原版有个 PortalShape 类专门干这个,但它把框架方块写死成了黑曜石
1 | private static final BlockBehaviour.StatePredicate FRAME = (state, level, pos) -> state.is(Blocks.OBSIDIAN); |
这个字段是 private 的,我们改不了,所以只能自己写一份
1 | public final class IceEtherPortalShape { |
思路很朴素
被点的那格框架的上面一格,当作门内左下角
然后试两个方向:沿 Z 铺(SOUTH)、沿 X 铺(EAST)
只试两个就够了,因为 isValidFrame 两头都查了(-1 和 WIDTH 那两圈)
isValidFrame 检查两件事:内部 2×3 全是空气,四周一圈都是 ICE_ETHER_BLOCK
fillPortal 把内部六格替换成门方块,AXIS 直接取 widthDir.getAxis()
沿 Z 铺就填 Z,沿 X 铺就填 X,不用自己配对
原版支持任意尺寸的门,我们固定 2×3,这样代码短、也好讲
(顺带说一句:原版的门宽高范围是 2~21,它是一格一格探边界探出来的)
这里踩过的一个坑:门板歪了
第一版我把两个轴配错了,写成了这样
1 | for (Direction.Axis axis : new Direction.Axis[]{Direction.Axis.X, Direction.Axis.Z}) { |
axis = X 却沿 SOUTH(Z 轴)铺宽度
进游戏一看,框架立得好好的,门方块却是歪的,跟框架垂直
原因就是那个 AXIS 我一直把它当成「门朝哪边」
它其实是「门宽度沿着哪个轴」
对照原版的方块状态就清楚了
1 | "axis=x": { "model": "minecraft:block/nether_portal_ns" }, |
axis=x 用的是 _ns,也就是 north-south
因为宽度沿 X 的时候,门板是立在 X-Y 平面上的,脸朝着南北
所以填 AXIS 时,看的是「你说的那个方向铺出去的那条轴」,不是它的垂线
激活
用冰以太右键框架
1 | public class IceEtherItem extends Item { |
只在服务端处理,客户端给 SUCCESS 只是为了不让手上出现「用不了」的动作
把 ModItems.ICE_ETHER 换成 new IceEtherItem(...) 就行了
出口的搭建
buildPortal 就是反向作业:先摆框架,再填门方块
我们从地表开始,往上门搭一个 4 宽 × 5 高 的框(内部 2×3),然后点亮
显示
门方块没有自己的模型,这次直接借了原版下界门的
assets/tutorial/blockstates/ice_ether_portal.json
1 | { |
所以游戏里看起来是紫色的门
注意这份映射跟前面那个坑是同一件事:axis=x 配的是 _ns
写了 axis=z 却配 _ns,门板就转到别的平面上去了
想换成自己的颜色,就照着原版那两个模型做两份 json,换成自己的贴图(贴图要有透明通道)
还能怎么做
水平传送门
末地那种传送门是躺在地上的,其实它根本不能「点燃」
它的做法是:十二个末地框架摆成一个环,游戏自己检测到环完整,就在中间生成传送门方块
想仿的话,改动点有两处
一是「激活」不是右键,而是结构检测:在框架方块的 onPlace / neighborChanged 里判断周围一圈是不是凑齐了
二是目的地可以写死:末地传送门永远送你去固定的那个坐标,不需要像我们这样找出口、建出口
门方块本身还是 Portal 那一套,entityInside 和 getPortalDestination 一模一样
用流体激活
把 useOn 换成流体检测就行
思路是:在门内那个位置的下方放一个自己写的方块,覆写 onPlace 或者 neighborChanged,检查这个位置是不是变成了水或者岩浆(level.getFluidState(pos).is(FluidTags.WATER))
成立就调 IceEtherPortalShape.tryLight(...)
这样倒一桶水就能开门,适合做「献祭」「封印」这类玩法
测试
先在平地上搭一个框
外框 4 宽 × 5 高,用的是 ICE_ETHER_BLOCK,中间 2×3 留空
然后拿冰以太右键最下面那根框架
- 中间六格应该变成传送门方块,是紫色的
- 站上去等一秒,会被传走
- 落在冰以太维度里,脚下是一个新建的门
- 那边的天是浅蓝色,雾发白
- 脚下这一片应该是冰以太森林,往外走走会碰到丛林、平原、海洋这些原版群系(
multi_noise是混着来的) - 走回去应该回到主世界你刚才站的地方附近
F3看维度那一行,应该显示tutorial:ice_ether- 往下挖,
-64到320这个高度段应该和主世界一样,有深板岩、有水、有山
想只看纯冰以太森林
把 dimension json 里的 biome_source 换成 fixed 那版,重新进世界
整个维度就只剩一个群系,方便截图和录视频
如果门没有亮,先检查内部是不是真的空的(有草、有雪都不行)
常见问题(大肥鱼总结版)
- 右键没反应:内部不是
2×3空气,或者框架缺一块;注意外框是4×5 - 亮了但站进去不传送:
getPortalDestination返回了null,说明目标世界没拿到(维度 json 没生效) - 传送之后卡在方块里:出口位置没算好,我们的做法是取地表,如果你那边地形很怪就会卡
- 门方块是竖着的、和框架垂直:
AXIS填错了,它要填「宽度方向的轴」,不是门的法线 - 每次传送回来原地又多一个门:
findExisting的搜索中心 y 写死成 0,范围只剩[-16, 16],改成entity.blockPosition() - 维度里地平线以下一片黑:维度类型的
EnvironmentAttributeMap给了空,缺FOG_COLOR,抄主世界那套 - 传送门是紫黑方块:
blockstates/ice_ether_portal.json里模型路径写错 - 启动报
Registry minecraft:dimension is not loaded from datapacks:LEVEL_STEM不能进FabricDynamicRegistryProvider,维度本体手写 json - 维度能进但地形是空的:
dimensionjson 里generator写错,或者settings指向了不存在的键 - 进去就掉进虚空:维度类型的
min_y/height和settings那套噪声没对齐,地形生成在范围外 - 进世界直接报「找不到维度类型」:
dimensionjson 里的type还是工具编的名字(test_type),要改成你自己注册的 - 脚下全是深板岩、水面贴着基岩:维度类型的高度比
settings小,地形被从底下削掉了一截 - 群系边界像拼图、一块一块的:
multi_noise里参数点给太少,最近邻匹配就是这个效果,想连续过渡得铺满整张表 - 想改门的颜色:换模型和贴图,贴图要有透明通道,不然是一块实心板
- 想支持更大的门:
IceEtherPortalShape的WIDTH/HEIGHT是写死的常量,改成探测边界 - 拆了框架门还在:我们没写「框架破了就销毁」的逻辑,原版是靠
updateShape自己消失的,可以自己补
最后
到这期,这个系列就写完了
从一个空工程,到一个有方块、物品、配方、流体、世界生成、结构、群系、维度的模组
中间踩过的坑基本都写进文章了,剩下的就靠你自己往下挖
模组开发这东西,看懂原版怎么写的,基本就什么都会写了







