自定义传送门系统
自定义传送门系统(Portal Gate)是一套完整的维度跳转方案,包含门框形状定义、激活物品检测、传送门方块填充、维度切换和传送门定位器(Portal Forcer)。与 自定义传送门方块 只负责"站上去就传送"不同,传送门系统让你可以实现类似原版下界传送门的完整交互流程:搭建框架 → 使用物品激活 → 门框填充传送门方块 → 进入传送 → 目标维度自动创建对应出口。
概述
一个完整的传送门系统包含以下组成部分:
| 组成部分 | 说明 |
|---|---|
| 门框形状(Pattern) | 用二维数组定义门框的方块排列 |
| 方块定义(Block Defines) | 将形状中的字符映射到具体方块 |
| 激活物品 | 玩家使用特定物品点击特定位置来激活传送门 |
| 门框检测 | 使用 DetectStructure API 检查门框是否搭建正确 |
| 传送门方块 | 激活后填充到门框内部的方块 |
| Portal Forcer | 在目标维度搜索或创建对应的传送门出口 |
典型流程:
- 玩家用萤石搭建 4x5 的门框
- 玩家手持骨粉右键门框内侧指定位置
- 脚本检测门框结构是否正确
- 检测通过后,在门框内部填充传送门方块
- 玩家进入传送门,触发维度切换
- Portal Forcer 在目标维度寻找已有传送门或创建新的
门框形状定义
使用 Python 二维列表定义门框的形状,每个字符代表一种方块:
# 4x5 萤石门框,类似原版下界传送门
portal_pattern = [
['A', 'A', 'A', 'A'],
['A', 'O', 'O', 'A'],
['A', 'O', 'O', 'A'],
['A', 'O', 'O', 'A'],
['A', 'A', 'A', 'A']
]
block_defines = {
'A': {
'name': 'minecraft:glowstone',
'aux': 0
},
'O': {
'name': 'minecraft:air',
'aux': 0
}
}A— 门框方块(萤石)O— 门框内部空气(激活后替换为传送门方块)
激活物品配置
定义用于激活传送门的物品和点击位置:
activation_item = {
'name': 'minecraft:bone_meal',
'aux': 0
}
# 激活检测位置:门框内部的第一个空气方块坐标
# 相对于门框左下角的偏移
activation_positions = [
(1, 1, 0), # 从门框内侧点击
(2, 1, 0)
]门框检测
使用 DetectStructure API 检查玩家附近是否存在正确搭建的门框:
import server.extraServerApi as serverApi
comp = serverApi.GetEngineCompFactory().CreateGame(levelId)
def check_portal_frame(pos, dimensionId):
"""检测指定位置附近是否有正确的传送门框架"""
# 遍历可能的门框起始位置
for offset_x in range(-3, 1):
for offset_y in range(-4, 1):
origin = (pos[0] + offset_x, pos[1] + offset_y, pos[2])
result = comp.DetectStructure(
origin,
portal_pattern,
block_defines,
dimensionId
)
if result:
return origin
return None传送门方块填充
检测通过后,将门框内部的空气替换为传送门方块:
def fill_portal_blocks(origin, dimensionId, axis='x'):
"""在门框内部填充传送门方块"""
block_comp = serverApi.GetEngineCompFactory().CreateBlockInfo(levelId)
# aux=1 表示 X 轴方向,aux=2 表示 Z 轴方向
aux = 1 if axis == 'x' else 2
portal_block = {'name': 'mymod:blue_portal', 'aux': aux}
for row in range(1, 4): # 门框内部行(Y 方向)
for col in range(1, 3): # 门框内部列(X 或 Z 方向)
if axis == 'x':
block_pos = (origin[0] + col, origin[1] + row, origin[2])
else:
block_pos = (origin[0], origin[1] + row, origin[2] + col)
block_comp.SetBlockNew(block_pos, portal_block, dimensionId=dimensionId)避免 aux = 0
传送门方块的 aux 值必须为 1(X 轴方向)或 2(Z 轴方向)。使用 aux = 0 会导致方块朝向和粒子效果异常。始终通过 /setblock 指令或 SetBlockNew 接口放置传送门方块,不要手动放置。
Portal Forcer(传送门定位器)
Portal Forcer 负责在目标维度中搜索已有的传送门或创建新的。你需要监听 DimensionChangeFinishServerEvent 事件来实现此逻辑。
实现思路
- 玩家完成维度切换后,事件触发
- 以玩家当前坐标为中心,在一定范围内搜索已有的传送门方块
- 如果找到,将玩家传送到该传送门旁边
- 如果没找到,在玩家当前位置创建一个新的传送门(含门框),并记录坐标
传送门坐标持久化
使用关卡的 extraData 存储已创建的传送门坐标,确保重新进入世界后仍能找到已有传送门:
import server.extraServerApi as serverApi
PORTAL_DATA_KEY = "mymod_portal_locations"
def save_portal_location(dimensionId, pos):
"""保存传送门位置到 extraData"""
comp = serverApi.GetEngineCompFactory().CreateExtraData(levelId)
data = comp.GetExtraData(PORTAL_DATA_KEY) or {}
dim_key = str(dimensionId)
if dim_key not in data:
data[dim_key] = []
data[dim_key].append(list(pos))
comp.SetExtraData(PORTAL_DATA_KEY, data)
def find_nearest_portal(dimensionId, pos, search_radius=128):
"""在指定维度中搜索最近的已记录传送门"""
comp = serverApi.GetEngineCompFactory().CreateExtraData(levelId)
data = comp.GetExtraData(PORTAL_DATA_KEY) or {}
dim_key = str(dimensionId)
portals = data.get(dim_key, [])
nearest = None
min_dist = float('inf')
for p in portals:
dist = (p[0] - pos[0]) ** 2 + (p[2] - pos[2]) ** 2
if dist < min_dist and dist <= search_radius ** 2:
min_dist = dist
nearest = tuple(p)
return nearestDimensionChangeFinishServerEvent 处理
def on_dimension_change_finish(self, args):
"""维度切换完成后,搜索或创建传送门"""
playerId = args['playerId']
toDimensionId = args['toDimensionId']
# 获取玩家当前坐标
pos_comp = serverApi.GetEngineCompFactory().CreatePos(playerId)
pos = pos_comp.GetFootPos()
# 搜索附近已有的传送门
nearest = find_nearest_portal(toDimensionId, pos)
if nearest:
# 找到已有传送门,传送玩家过去
pos_comp.SetFootPos(nearest)
return
# 没有找到,创建新传送门
build_new_portal(pos, toDimensionId)
save_portal_location(toDimensionId, pos)完整脚本示例
以下示例展示了一个简化版的传送门系统完整流程:
# -*- coding: utf-8 -*-
import server.extraServerApi as serverApi
ServerSystem = serverApi.GetServerSystemCls()
class PortalGateSystem(ServerSystem):
def __init__(self, namespace, systemName):
ServerSystem.__init__(self, namespace, systemName)
# 门框形状:4x5 萤石框架
self.pattern = [
['A', 'A', 'A', 'A'],
['A', 'O', 'O', 'A'],
['A', 'O', 'O', 'A'],
['A', 'O', 'O', 'A'],
['A', 'A', 'A', 'A']
]
self.block_defines = {
'A': {'name': 'minecraft:glowstone', 'aux': 0},
'O': {'name': 'minecraft:air', 'aux': 0}
}
self.activation_item = 'minecraft:bone_meal'
self.portal_block = 'mymod:blue_portal'
self.target_dimension = 23333
self.ListenForEvent(
serverApi.ENGINE_NAMESPACE,
serverApi.ENGINE_SYSTEM_NAME,
'ServerItemUseOnEvent',
self,
self.on_item_use_on
)
self.ListenForEvent(
serverApi.ENGINE_NAMESPACE,
serverApi.ENGINE_SYSTEM_NAME,
'DimensionChangeFinishServerEvent',
self,
self.on_dimension_change_finish
)
def on_item_use_on(self, args):
"""玩家使用物品点击方块时检测传送门激活"""
playerId = args['entityId']
item_name = args.get('itemName', '')
if item_name != self.activation_item:
return
pos = (args['x'], args['y'], args['z'])
dim_comp = serverApi.GetEngineCompFactory().CreateDimension(playerId)
dimensionId = dim_comp.GetEntityDimensionId()
# 检测门框结构
game_comp = serverApi.GetEngineCompFactory().CreateGame(levelId)
origin = self._detect_frame(game_comp, pos, dimensionId)
if origin:
# 填充传送门方块
self._fill_portal(origin, dimensionId)
def _detect_frame(self, game_comp, pos, dimensionId):
"""在点击位置附近检测门框"""
for ox in range(-3, 1):
for oy in range(-4, 1):
test_origin = (pos[0] + ox, pos[1] + oy, pos[2])
result = game_comp.DetectStructure(
test_origin, self.pattern,
self.block_defines, dimensionId
)
if result:
return test_origin
return None
def _fill_portal(self, origin, dimensionId):
"""在门框内填充传送门方块"""
block_comp = serverApi.GetEngineCompFactory().CreateBlockInfo(levelId)
portal = {'name': self.portal_block, 'aux': 1}
for row in range(1, 4):
for col in range(1, 3):
block_pos = (origin[0] + col, origin[1] + row, origin[2])
block_comp.SetBlockNew(block_pos, portal, dimensionId=dimensionId)
def on_dimension_change_finish(self, args):
"""维度切换完成后搜索或创建传送门"""
playerId = args['playerId']
toDimId = args['toDimensionId']
pos_comp = serverApi.GetEngineCompFactory().CreatePos(playerId)
pos = pos_comp.GetFootPos()
extra_comp = serverApi.GetEngineCompFactory().CreateExtraData(levelId)
portal_data = extra_comp.GetExtraData('portal_locations') or {}
dim_key = str(toDimId)
# 搜索最近的已有传送门
portals = portal_data.get(dim_key, [])
nearest = None
min_dist = float('inf')
for p in portals:
dist = (p[0] - pos[0]) ** 2 + (p[2] - pos[2]) ** 2
if dist < min_dist and dist <= 128 ** 2:
min_dist = dist
nearest = p
if nearest:
pos_comp.SetFootPos(tuple(nearest))
else:
# 在当前位置创建新传送门
self._build_portal_at(pos, toDimId)
if dim_key not in portal_data:
portal_data[dim_key] = []
portal_data[dim_key].append(list(pos))
extra_comp.SetExtraData('portal_locations', portal_data)
def _build_portal_at(self, pos, dimensionId):
"""在指定位置构建完整的传送门(框架 + 传送门方块)"""
block_comp = serverApi.GetEngineCompFactory().CreateBlockInfo(levelId)
origin = (int(pos[0]) - 1, int(pos[1]), int(pos[2]))
# 放置门框
frame_block = {'name': 'minecraft:glowstone', 'aux': 0}
for row in range(5):
for col in range(4):
if self.pattern[row][col] == 'A':
block_pos = (origin[0] + col, origin[1] + row, origin[2])
block_comp.SetBlockNew(block_pos, frame_block, dimensionId=dimensionId)
# 填充传送门方块
self._fill_portal(origin, dimensionId)传送门方块 vs 传送门系统
| 对比 | 传送门方块 | 传送门系统 |
|---|---|---|
| 页面 | 自定义传送门方块 | 本页 |
| 实现方式 | JSON 组件 netease:portal | Python 脚本 |
| 功能 | 玩家站上去直接传送 | 完整的框架检测 + 激活 + 定位器 |
| 门框检测 | 不支持 | 通过 DetectStructure 检测 |
| 目标位置 | 坐标不变 | Portal Forcer 搜索/创建出口 |
| 适用场景 | 简单的维度传送 | 类似原版下界传送门的完整体验 |
参考 Demo
官方提供了 portalGateDemo 示例工程,包含完整的传送门系统实现。建议在阅读本文后下载 Demo 对照学习。
常见问题
激活传送门时没有反应
排查步骤:
- 确认使用了正确的激活物品(检查物品名称和 aux 值)
- 确认门框结构搭建正确(每个方块的位置和类型都必须与 pattern 完全匹配)
- 确认
DetectStructure的搜索范围覆盖了门框的起始位置 - 检查脚本中的事件监听是否正确注册
传送后没有出现在传送门旁边
检查 DimensionChangeFinishServerEvent 是否正确监听。确认 Portal Forcer 的搜索半径足够大,以及 extraData 中的传送门坐标数据格式正确。
传送门在重新进入世界后失效
确保使用 extraData 进行传送门坐标持久化。extraData 会随存档保存,重新加载后仍然可用。
双向传送门如何实现
需要在两个维度中分别监听激活事件和维度切换事件。每个维度的传送门都应有对应的 Portal Forcer 逻辑,并在 extraData 中记录双向的坐标映射。
