Skip to content

自定义传送门系统 ​

自定义传送门系统(Portal Gate)是一套完整的维度跳转方案,包含门框形状定义、激活物品检测、传送门方块填充、维度切换和传送门定位器(Portal Forcer)。与 自定义传送门方块 只负责"站上去就传送"不同,传送门系统让你可以实现类似原版下界传送门的完整交互流程:搭建框架 → 使用物品激活 → 门框填充传送门方块 → 进入传送 → 目标维度自动创建对应出口。

前置条件

请先阅读 自定义传送门方块 了解传送门方块的注册和配置,并阅读 自定义维度配置 了解如何声明自定义维度。本功能需要使用 Python Mod SDK 编写脚本逻辑。

概述 ​

一个完整的传送门系统包含以下组成部分:

组成部分说明
门框形状(Pattern)用二维数组定义门框的方块排列
方块定义(Block Defines)将形状中的字符映射到具体方块
激活物品玩家使用特定物品点击特定位置来激活传送门
门框检测使用 DetectStructure API 检查门框是否搭建正确
传送门方块激活后填充到门框内部的方块
Portal Forcer在目标维度搜索或创建对应的传送门出口

典型流程:

  1. 玩家用萤石搭建 4x5 的门框
  2. 玩家手持骨粉右键门框内侧指定位置
  3. 脚本检测门框结构是否正确
  4. 检测通过后,在门框内部填充传送门方块
  5. 玩家进入传送门,触发维度切换
  6. Portal Forcer 在目标维度寻找已有传送门或创建新的

门框形状定义 ​

使用 Python 二维列表定义门框的形状,每个字符代表一种方块:

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 — 门框内部空气(激活后替换为传送门方块)

激活物品配置 ​

定义用于激活传送门的物品和点击位置:

python
activation_item = {
    'name': 'minecraft:bone_meal',
    'aux': 0
}

# 激活检测位置:门框内部的第一个空气方块坐标
# 相对于门框左下角的偏移
activation_positions = [
    (1, 1, 0),  # 从门框内侧点击
    (2, 1, 0)
]

门框检测 ​

使用 DetectStructure API 检查玩家附近是否存在正确搭建的门框:

python
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

传送门方块填充 ​

检测通过后,将门框内部的空气替换为传送门方块:

python
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 事件来实现此逻辑。

实现思路 ​

  1. 玩家完成维度切换后,事件触发
  2. 以玩家当前坐标为中心,在一定范围内搜索已有的传送门方块
  3. 如果找到,将玩家传送到该传送门旁边
  4. 如果没找到,在玩家当前位置创建一个新的传送门(含门框),并记录坐标

传送门坐标持久化 ​

使用关卡的 extraData 存储已创建的传送门坐标,确保重新进入世界后仍能找到已有传送门:

python
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 nearest

DimensionChangeFinishServerEvent 处理 ​

python
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)

完整脚本示例 ​

以下示例展示了一个简化版的传送门系统完整流程:

python
# -*- 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:portalPython 脚本
功能玩家站上去直接传送完整的框架检测 + 激活 + 定位器
门框检测不支持通过 DetectStructure 检测
目标位置坐标不变Portal Forcer 搜索/创建出口
适用场景简单的维度传送类似原版下界传送门的完整体验

参考 Demo

官方提供了 portalGateDemo 示例工程,包含完整的传送门系统实现。建议在阅读本文后下载 Demo 对照学习。

常见问题 ​

激活传送门时没有反应 ​

排查步骤:

  1. 确认使用了正确的激活物品(检查物品名称和 aux 值)
  2. 确认门框结构搭建正确(每个方块的位置和类型都必须与 pattern 完全匹配)
  3. 确认 DetectStructure 的搜索范围覆盖了门框的起始位置
  4. 检查脚本中的事件监听是否正确注册

传送后没有出现在传送门旁边 ​

检查 DimensionChangeFinishServerEvent 是否正确监听。确认 Portal Forcer 的搜索半径足够大,以及 extraData 中的传送门坐标数据格式正确。

传送门在重新进入世界后失效 ​

确保使用 extraData 进行传送门坐标持久化。extraData 会随存档保存,重新加载后仍然可用。

双向传送门如何实现 ​

需要在两个维度中分别监听激活事件和维度切换事件。每个维度的传送门都应有对应的 Portal Forcer 逻辑,并在 extraData 中记录双向的坐标映射。

下一步 ​