Skip to content

玩家互传(TPA)

玩家之间互相发起、接受传送请求的完整系统,含玩家列表、拒绝与超时处理

社区
官方•Dingding OvO · 2026-08-15

玩家互传(TPA)让任意两名玩家互相发起传送请求:一方在菜单里选中目标玩家,另一方抬头同意即可传送,低头或超时则取消。本文档整理自原作者分享的命令组,并补充了原理解释。

如果你只需要「发起 → 接受 → 传送」的简化版,可参考 /docs/community/3/(雪球菜单)中的发起/接受传送部分。

原理解释

三个核心状态

  • 传送菜单(菜单状态机)—— 记录每位玩家「当前在菜单里做什么」:

    • 40 = 菜单基态(已打开、未选中任何行)
    • 4150 = 正在浏览列表第 110 行
    • 51 = 选中「刷新列表」
    • 100 = 已被选为传送目标(等待回应)
    • 101 = 已接受请求(接受方)
    • 1000+ = 取消/超时标记,2000 = 清理标记
    • 5 = 从上级菜单选中「互传」后的入口状态(指令 1 把它归位成 40)
  • 传送(配对 ID)—— 每位玩家的「槽位号」:

    • 0~39 = 在线玩家的列表编号(第 0 位、第 1 位……)
    • 40+ = 已发起请求的玩家(持有正槽位)
    • -40- = 等待回应的目标(持有负槽位)

    发起方和目标持有同一个槽位号,只是目标取反:谁把正负相加抵消成 0,谁就是发起方,用这一个数记住「谁和谁是一对」。

  • rx/rxm(视角)—— 抬头 rx=-88 = 确认/选择,低头 rxm=88 = 拒绝/取消,与 雪球菜单 一致。

+41 位移技巧

列表第 1 行对应 传送菜单=41,而这一行玩家的槽位号是 0,中间正好差 41。选择时先 add @a 传送 41(指令 7)把所有人整体 +41,于是第 N 行玩家的槽位临时变成 41+N,恰好等于他的 传送菜单。这样就能用 if score @s 传送 = @s 传送菜单 判断「选的是不是自己」(指令 8),用 if score @s 传送 = @p[...] 传送菜单 找出被点中的目标(指令 9~11)。判断完 remove @a 传送 41(指令 13)还原。

配对与接受

发起请求后:目标从 互传记录 领一个新槽位(指令 14),发号器 +1(指令 15),发起方抄下同一个槽位(指令 16),目标再取反为负(指令 18)。接受时目标抬头 → 传送菜单=101(指令 41),把目标的负槽位加到所有人(指令 42):发起方的正槽位被抵消成 0,于是 tp @a[scores={传送=0}] 精确命中发起方并传送到目标(指令 43),最后用相反操作撤销(指令 46)。

为什么用「假玩家」计数器

互传记录(配对发号器,从 40 起发)、负(常量 -1,用于取反)、检测/在线人员/互传(一致性检查)。其中 在线人员 每刻统计当前在线人数,与上一刻的基准 互传 比较:人数一旦变化就清理离线玩家残留的配对,随后把 互传 更新为最新值(指令 28)。用假玩家(fake player)而不是实体,是因为它们永远存在、不占实体,且分数能直接参与 scoreboard players operation 运算。

前置指令

在聊天栏中依次执行(创建计分板与常量):

/scoreboard objectives add 传送菜单 dummy /scoreboard objectives add 传送 dummy /scoreboard players set 负 传送 -1 /scoreboard players set 互传记录 传送 40 /gamerule commandblockoutput false /gamerule sendcommandfeedback false

负 是常量 -1(用于取反标记);互传记录 是配对槽位发号器,从 40 起发,避免与列表编号 0~39 冲突。

本组指令是原作者「传送菜单」系统的互传模块,有两处依赖上级菜单,独立使用前需先补齐:

  1. 入口——玩家以 传送菜单=5 的状态进入本菜单(指令 1 负责把它归位)。独立使用时需要自行设计一个触发器把玩家的 传送菜单 设为 5(例如投掷雪球时 scoreboard players set @p 传送菜单 5)。
  2. 列表槽位——玩家列表按每位玩家的 传送 槽位号 0~39 排序显示,需保证在线玩家持有互不重复的编号。参考实现见文末「常见问题」。

命令方块摆放

按分组顺序摆放:每组以循环命令方块开头(每游戏刻持续监测),其余用连锁命令方块按箭头方向连接、在同一游戏刻内跟随执行。所有条件判断都写在 execute 的 if/unless 里,因此无需「有条件」命令方块。

一、菜单初始化与显示

指令 1 — 从上级菜单进入(把入口状态 传送菜单=5 归位为 40)

/scoreboard players set @a[scores={传送菜单=5,传送=!..-40},rx=-88] 传送菜单 40

指令 2 — 初始化传送分数

/scoreboard players add @a 传送 0

指令 3 — 清理非法状态

/scoreboard players set @a[scores={传送=!0..39,传送菜单=40..52}] 传送菜单 0

指令 4 — 显示菜单(抬头逐行高亮)

/execute as @a[scores={传送菜单=40..51}] run titleraw @s actionbar {"rawtext":[{"text":"§b▎ §f传送菜单 §d۞ §f申请传送 §b▎\n§7 - Select player for TPA -§r§7\n# -----------------------\n"},{"translate":"%%2","with":{"rawtext":[{"selector":"@s[scores={传送菜单=41}]"},{"text":"§e■ "},{"text":"§b□§7 "}]}},{"selector":"@a[scores={传送=0}]"},{"text":"§7\n"},{"translate":"%%2","with":{"rawtext":[{"selector":"@s[scores={传送菜单=42}]"},{"text":"§e■ "},{"text":"§b□§7 "}]}},{"selector":"@a[scores={传送=1}]"},{"text":"§7\n"},{"translate":"%%2","with":{"rawtext":[{"selector":"@s[scores={传送菜单=43}]"},{"text":"§e■ "},{"text":"§b□§7 "}]}},{"selector":"@a[scores={传送=2}]"},{"text":"§7\n"},{"translate":"%%2","with":{"rawtext":[{"selector":"@s[scores={传送菜单=44}]"},{"text":"§e■ "},{"text":"§b□§7 "}]}},{"selector":"@a[scores={传送=3}]"},{"text":"§7\n"},{"translate":"%%2","with":{"rawtext":[{"selector":"@s[scores={传送菜单=45}]"},{"text":"§e■ "},{"text":"§b□§7 "}]}},{"selector":"@a[scores={传送=4}]"},{"text":"§7\n"},{"translate":"%%2","with":{"rawtext":[{"selector":"@s[scores={传送菜单=46}]"},{"text":"§e■ "},{"text":"§b□§7 "}]}},{"selector":"@a[scores={传送=5}]"},{"text":"§7\n"},{"translate":"%%2","with":{"rawtext":[{"selector":"@s[scores={传送菜单=47}]"},{"text":"§e■ "},{"text":"§b□§7 "}]}},{"selector":"@a[scores={传送=6}]"},{"text":"§7\n"},{"translate":"%%2","with":{"rawtext":[{"selector":"@s[scores={传送菜单=48}]"},{"text":"§e■ "},{"text":"§b□§7 "}]}},{"selector":"@a[scores={传送=7}]"},{"text":"§7\n"},{"translate":"%%2","with":{"rawtext":[{"selector":"@s[scores={传送菜单=49}]"},{"text":"§e■ "},{"text":"§b□§7 "}]}},{"selector":"@a[scores={传送=8}]"},{"text":"§7\n"},{"translate":"%%2","with":{"rawtext":[{"selector":"@s[scores={传送菜单=50}]"},{"text":"§e■ "},{"text":"§b□§7 "}]}},{"selector":"@a[scores={传送=9}]"},{"text":"\n"},{"translate":"%%2","with":{"rawtext":[{"selector":"@s[scores={传送菜单=51}]"},{"text":"§e■"},{"text":"§6□§7"}]}},{"text":" Refresh - 刷新列表"},{"text":"§7\n--------------------------\n§l〈§r§a抬头确定§7§l〉§r§b ● §l§7〈§r§c低头关闭§7§l〉\n\n\n\n\n\n\n\n\n\n"}]}

指令 5 — 刷新列表(交换槽位)

/execute as @a[scores={传送菜单=51},rx=-88] unless entity @p[scores={传送菜单=41..50},rx=-88] run scoreboard players operation @a[scores={传送=1}] 传送 >< @a[scores={传送=0..39}] 传送

指令 6 — 刷新后重置光标

/execute as @a[scores={传送菜单=51},rx=-88] unless entity @p[scores={传送菜单=41..50},rx=-88] run scoreboard players set @a[scores={传送菜单=41..52}] 传送菜单 40

二、选择目标并发起请求

指令 7 — 全体槽位 +41 位移(把行号对齐到槽位号)

/execute as @p[scores={传送菜单=41..50},rx=-88] run scoreboard players add @a 传送 41

指令 8 — 检测「自己传自己」

/execute as @p[scores={传送菜单=41..50},rx=-88] if score @s 传送 = @s 传送菜单 run tellraw @s {"rawtext":[{"text":"§c▎§r 玩家互传 §7不可以向自己申请互传"}]}

指令 9 — 告知发起方已发送请求

/execute as @p[scores={传送菜单=41..50},rx=-88] unless score @s 传送 = @s 传送菜单 as @a if score @s 传送 = @p[scores={传送菜单=41..50},rx=-88] 传送菜单 run tellraw @p[scores={传送菜单=41..50},rx=-88] {"rawtext":[{"text":"§b▎ §r玩家互传 §7已向§e"},{"selector":"@s"},{"text":"§7申请互传\n - 低头可取消互传申请§f"}]}

指令 10 — 告知目标收到请求

/execute as @p[scores={传送菜单=41..50},rx=-88] unless score @s 传送 = @s 传送菜单 as @a if score @s 传送 = @p[scores={传送菜单=41..50},rx=-88] 传送菜单 run tellraw @s {"rawtext":[{"text":"§b▎§r 玩家互传 §7玩家§e"},{"selector":"@p[scores={传送菜单=41..50},rx=-88]"},{"text":"§7向你发出互传申请\n §a抬头同意 §7- §c低头拒绝"}]}

指令 11 — 标记目标(传送菜单=100)

/execute as @p[scores={传送菜单=41..50},rx=-88] unless score @s 传送 = @s 传送菜单 as @a if score @s 传送 = @p[scores={传送菜单=41..50},rx=-88] 传送菜单 run scoreboard players set @s 传送菜单 100

指令 12 — 目标不存在时提示

/execute as @p[scores={传送菜单=41..50},rx=-88] unless score @s 传送 = @s 传送菜单 unless entity @a[scores={传送菜单=100}] run tellraw @s {"rawtext":[{"text":"§c▎§r 玩家互传 §7你要申请的玩家不存在"}]}

指令 13 — 撤销 +41 位移

/execute as @p[scores={传送菜单=41..50},rx=-88] run scoreboard players remove @a 传送 41

三、配对记录

指令 14 — 目标领新槽位

/execute as @a[scores={传送菜单=100}] run scoreboard players operation @s 传送 = 互传记录 传送

指令 15 — 发号器 +1

/execute as @a[scores={传送菜单=100}] run scoreboard players add 互传记录 传送 1

指令 16 — 发起方抄下同一槽位

/scoreboard players operation @p[scores={传送菜单=41..50},rx=-88] 传送 = @a[scores={传送菜单=100}] 传送

指令 17 — 无目标时重置光标

/execute as @p[scores={传送菜单=41..50},rx=-88] unless entity @a[scores={传送菜单=100}] run scoreboard players set @s 传送菜单 40

指令 18 — 目标槽位取反(标记等待回应)

/scoreboard players operation @a[scores={传送菜单=100}] 传送 *= 负 传送

四、一致性维护与离线清理

指令 19 — 检测计数器归零

/scoreboard players set 检测 传送 0

指令 20 — 统计槽位 0 的玩家数

/execute as @a[scores={传送=0}] run scoreboard players add 检测 传送 1

指令 21 — 槽位 0 重复 → 标记为 -1

/execute if score 检测 传送 matches 2..40 run scoreboard players set @a[scores={传送=0..39}] 传送 -1

指令 22 — 槽位重复 → 扣除修正

/execute if score 检测 传送 matches 2..40 run scoreboard players operation @a[scores={传送=-1..39}] 传送 -= @a[scores={传送=-1}] 传送

指令 23 — 槽位重复 → 重置光标

/execute if score 检测 传送 matches 2..40 run scoreboard players set @a[scores={传送菜单=41..52,传送=0..39}] 传送菜单 40

指令 24 — 在线人数计数器归零

/scoreboard players set 在线人员 传送 0

指令 25 — 统计在线人数

/execute as @a[scores={传送=0..39}] run scoreboard players add 在线人员 传送 1

指令 26 — 在线人数变化 → 标记为 -1

/execute unless score 互传 传送 = 在线人员 传送 run scoreboard players set @a[scores={传送=0..39}] 传送 -1

指令 27 — 在线人数变化 → 扣除修正

/execute unless score 互传 传送 = 在线人员 传送 run scoreboard players operation @a[scores={传送=-1..39}] 传送 -= @a[scores={传送=-1}] 传送

指令 28 — 更新在线人数基准(互传 ← 在线人员)

/scoreboard players operation 互传 传送 = 在线人员 传送

五、取消 / 拒绝 / 离线

指令 29 — 目标低头 → 已拒绝提示

/tellraw @a[scores={传送=..-40},rxm=88] {"rawtext":[{"text":"§c▎§r 玩家互传 §7已拒绝对方的互传申请"}]}

指令 30 — 发起方低头 → 已取消提示

/tellraw @a[scores={传送=40..},rxm=88] {"rawtext":[{"text":"§c▎§r 玩家互传 §7已取消互传"}]}

指令 31 — 低头重置分数

/scoreboard players set @a[scores={传送=!0..39},rxm=88] 传送 0

指令 32 — 取消者标记 传送菜单=1000

/scoreboard players add @r[scores={传送=!0..39}] 传送菜单 1000

指令 33 — 抵消取消者分数

/execute as @a[scores={传送菜单=1000..}] run scoreboard players operation @a[scores={传送菜单=!1000..}] 传送 += @s 传送

指令 34 — 发起方侧:对方拒绝/离线提示

/execute as @a[scores={传送菜单=1000..,传送=40..}] unless entity @a[scores={传送=0}] run tellraw @s {"rawtext":[{"text":"§c▎§r 玩家互传 §7对方拒绝互传或离线"}]}

指令 35 — 目标侧:对方取消/离线提示

/execute as @a[scores={传送菜单=1000..,传送=..-40}] unless entity @a[scores={传送=0}] run tellraw @s {"rawtext":[{"text":"§c▎§r 玩家互传 §7对方取消互传或离线"}]}

指令 36 — 未配对成功 → 追加标记

/execute as @a[scores={传送菜单=1000..}] unless entity @a[scores={传送=0}] run scoreboard players add @s 传送菜单 1000

指令 37 — 撤销抵消

/execute as @a[scores={传送菜单=1000..}] run scoreboard players operation @a[scores={传送菜单=!1000..}] 传送 -= @s 传送

指令 38 — 清理 传送菜单=2000 残留

/scoreboard players set @a[scores={传送菜单=2000..}] 传送 0

指令 39 — 移除取消标记

/scoreboard players remove @a[scores={传送菜单=1000..}] 传送菜单 1000

六、接受并传送

指令 40 — 清理旧状态

/scoreboard players set @a[scores={传送菜单=100..}] 传送菜单 0

指令 41 — 目标抬头 → 接受(传送菜单=101)

/scoreboard players set @p[scores={传送=..-40},rx=-88] 传送菜单 101

指令 42 — 抵消配对(发起方分数归 0)

/execute as @p[scores={传送菜单=101}] run scoreboard players operation @a[scores={传送菜单=!101}] 传送 += @s 传送

指令 43 — 执行传送

/tp @a[scores={传送=0}] @p[scores={传送菜单=101}]

指令 44 — 告知接受方

/tellraw @p[scores={传送菜单=101}] {"rawtext":[{"text":"§a▎ §f玩家互传 §7已接受§e"},{"selector":"@a[scores={传送=0}]"},{"text":"§7的互传申请"}]}

指令 45 — 告知发起方

/execute as @p[scores={传送菜单=101}] run tellraw @a[scores={传送=0}] {"rawtext":[{"text":"§a▎§f 玩家互传 §7已将您传送至§e"},{"selector":"@s"}]}

指令 46 — 撤销抵消

/execute as @p[scores={传送菜单=101}] run scoreboard players operation @a[scores={传送菜单=!101,传送=!0}] 传送 -= @s 传送

指令 47 — 重置接受方分数

/scoreboard players set @p[scores={传送菜单=101}] 传送 0

常见问题

为什么「自己传自己」会被拒绝?

指令 8 用 if score @s 传送 = @s 传送菜单 检测选中行是否对应自己,若为自己则提示「不可以向自己申请互传」。这是通过 +41 位移后比较「自己的槽位」与「自己的光标」实现的。

为什么点列表没反应 / 提示玩家不存在?

通常是「列表槽位」没维护好:列表按每位玩家的 传送 槽位号(0~39)排序显示,若所有人都停在 传送=0,列表只会显示第一行、无法选中他人。请先按下面的方法给在线玩家分配互不重复的槽位号。

如何给玩家分配列表槽位?

原系统的列表编号由上级「传送菜单」统一发号。独立使用时,可加两个循环命令方块,每刻给最近一个「尚未分配」的玩家发下一个编号:

/execute as @p[scores={传送=0}] run scoreboard players operation @s 传送 = 列表编号 传送 /execute as @p[scores={传送=0}] run scoreboard players add 列表编号 传送 1

并把 列表编号 初始化为 1:

/scoreboard players set 列表编号 传送 1

注意:传送=0 被当作「待分配」的初始值,因此 0 号不会被真正分配出去,列表实际从 1 号开始显示(第一行留空属正常现象)。玩家离线的槽位不会自动回收,长期运行需结合指令 21~23 的一致性检查做清理。

离线玩家会残留导致列表错乱吗?

系统用 检测/在线人员 两个假玩家计数做一致性检查:槽位 0 重复、在线人数变化时,把受影响玩家的 传送 标记为 -1 并扣除修正(指令 19~27),随后指令 28 把在线人数基准 互传 更新为最新值,供下一轮比较。

标题里的「传送菜单」能改吗?

可以,那是原作者的菜单名,纯展示文案。把指令 4 里的 传送菜单 替换成你服务器的名字即可,不影响逻辑。

基岩版注意

  • 全部为基岩版语法:titleraw/actionbar/rawtext、scoreboard players operation 的 ><(交换)、*=(乘)等运算、以及 selector 里的 scores={传送=!0..39}(! 表示「非」,即分数不在该区间)均为基岩版特有写法
  • 涉及 /scoreboard、/execute、/tp、/tellraw、/title、/gamerule 均需 OP 权限
  • 本组指令原为「传送菜单」系统的互传模块:入口状态 传送菜单=5 与列表槽位发号(传送 0~39)由上级菜单维护,独立使用时需自行补齐这两处(见上文说明与常见问题)