A → B 传送:脚下 WP、目标 WP、重力锁的时序

同图无缝传送(blackScreenType = NONE)· 客户端 TypeScript 0.7.0 · 关注点:SetGravityScale(0) 什么时候锁、什么时候解

一句话:服务器把位置瞬间改成 B 并下发协议,客户端角色也会立刻被挪到 B —— 但此刻 B 的地形一格都还没加载。 中间这段"人已经在 B、地还没到"的空窗,全靠 SetGravityScale(0) + MOVE_None 把人钉在空中托着。 地形流送完 + 3×3 贴地预检通过,才解锁放人落地。

四个角色

服务器

位置是瞬间改的,不等客户端。先发 15512 TeleportSelf,紧接着发 15511 ServerEntityMove(带 B 坐标)。客户端回 15513 LoadDone 才认为你"到位了"。

玩家灯 StreamingSourceInfo

每帧跟随角色位置(Tick())。角色被挪到 B 的下一帧,它就跟到 B —— A 脚下的 cell 随即失去覆盖,开始卸载。

落点灯 TargetStreamingSourceInfo

被 SetStreamingLockStateAndPosition(true, B) 锁死在 B,不跟人。负责在交接完成前把 B 的地形先拉出来。

重力锁

TeleportController.LockLocalPlayerMovementForTeleport():存下原 MovementMode → SetGravityScale(0) → MOVE_None。解锁时原样恢复。

完整时序(8 步)

0
服务器改位置并下发15512 TeleportSelfSCNotify

服务端此刻已经认为你在 B 了。客户端还什么都没做。

A 脚下 WP已加载 · 玩家灯在 A
B 目标 WP未加载 · 落点灯未点亮
重力正常
1
🔒 加锁 —— 关重力 + MOVE_None TeleportController.ts:753 → :833 → :1168-1184

收到 15512 立刻做四件事:① 瞬时盖遮罩 OpenSeamlessTransitionMask;② 启 15s 兜底 timer;③ 注册 LocalPlayerServerMoveApplied 监听;④ 加重力锁。

注意加锁在收到位置包之前——因为 15511 可能同帧就到,SafeMoveTeleport 是同步的,慢一步人就已经在 B 的空气里开始掉了。

A 脚下 WP已加载 · 人还站在 A
B 目标 WP未加载
重力锁定开始 GravityScale=0
2
位置包到 —— 角色被瞬间挪到 B 15511 ServerEntityMove BaseMoveSyncComponent.SafeMoveTeleport

最危险的一帧:角色 Actor 已在 B 的坐标上,B 的 cell 一格未加载,脚下是纯空气。此刻唯一托着人的就是第 1 步的重力锁。

同时玩家灯下一帧 Tick 就跟到 B,A 脚下的 cell 从这一刻起失去覆盖,开始卸载(这是正常回收,不是 bug)。

A 脚下 WP开始卸载 玩家灯已离开
B 目标 WP玩家灯刚到 · 开始请求 cell
重力锁定中(唯一防坠手段)
3
分流 → 无缝路径 → 点亮落点灯 :860 HandleLocalPlayerTeleportApplied → :928 OnSeamlessLevel → :1203 StartLevelStreamingFromSnapshot

非重度走 OnSeamlessLevel,用快照里的 B 坐标构建 SceneLoadingTeleportCmd 命令链。链里 SceneLoadingNewSceneCmd 把落点灯锁在 B 并置 LoadAndVisible。同时开启协议缓存(AOI/实体增删挂起)。

A 脚下 WP继续卸载
B 目标 WP落点灯点亮 双灯同时拉 cell
重力锁定中
4
等流送完成(轮询 100ms)SceneWaitWpStreamingCmd → GameModeController.CheckTargetStreamingCompleted

IsStreamingCompleted(Activated, radius=7000) 每 100ms 查一次,直到 B 周围 cell 全部进入 Activated。正常 约 100–300ms。

⚠ 这个 Cmd 内部没有超时,executor 超时是 TimerSystemConstants.MaxTime(等于不限)。某些 DataLayer 会永久返回 false,一旦命中就只能等第 1 步那个 15s 兜底。

A 脚下 WP基本卸完
B 目标 WP加载中 → Activated
重力锁定中 · 人悬在 B 上空
5
灯的交接(铁律)SceneLoadingMoveCmd.ts:15-22

先 CopyStreamingSourceInfo(玩家灯 ← 落点灯)(抄位置 + LevelLayerName + 状态),然后才把落点灯 Unload。顺序颠倒 = B 的 cell 瞬间无源 = 地形消失。

A 脚下 WP已卸载
B 目标 WP改由玩家灯持有 落点灯熄灭
重力仍锁定
6
解锁前贴地预检(3×3 射线)TeleportController.ts:1254 → :1280-1352

cell 到 Activated 不等于物理碰撞已 build 完。以 B 为中心做 3×3 = 9 条向下 LineTrace(水平 132cm、向下 200cm),9 点全命中才算地能托住人。

首次同步检查命中就直接放行;不命中每 50ms 重试,最多 20 次(1s 窗口),超时也强制放行并打 warning。

A 脚下 WP—
B 目标 WPActivated,等碰撞就绪
重力锁定中(最后 0–1s)
7
🔓 解锁 —— 恢复重力与 MovementMode TeleportController.ts:1220-1235 finalize → :1187-1200

按顺序:UnlockLocalPlayerMovementForTeleport()(ResetGravityScale() + 恢复原 MovementMode)→ 关遮罩 → 结束协议缓存 → Emit TeleportSteamingComplete。15513 LoadDone 在这之前已发(:1249)。

A 脚下 WP已卸载
B 目标 WP加载完成 · 玩家灯持有
重力解锁 · 恢复正常
8
收尾:保活守护(仅带流送的瞬移路径)BaseMoveSyncComponent.ts:643-730

落点灯会被重新点亮做兜底,直到玩家水平移动 > 500cm 或 30s 超时才关。这时人已经解锁能走了,地形由玩家灯正常维持。

A 脚下 WP—
B 目标 WP玩家灯维持 · 落点灯延迟熄灭
重力正常

重力锁窗口有多长

✅ 正常路径(约 200–400ms)

锁定:等流送 + 贴地预检
解锁 · 正常游戏
015512 → 15511 → 流送完成 → 3×3 通过≈1s

⚠ 地形慢 / 碰撞未就绪(+ 最多 1s 预检重试)

锁定:流送慢 + 20 次 ×50ms 重试
超时强制解锁(打 warning)
0日志:传送-解锁前贴地预检超时,强制解锁兜底≈2s

❌ 流送卡死(IsStreamingCompleted 永久 false)

锁定 15s —— 玩家全程悬空、动不了
15s 兜底强制解锁 + Done
0TeleportSelfFallbackTimeoutMs = 1500015s

解锁的 6 个出口(任何一个都会恢复重力)

出口位置触发条件
✅ 正常 finalizeTeleportController.ts:1225流送 success + 3×3 贴地预检通过(或预检 1s 超时兜底)
⏱ 15s 兜底:945 OnTeleportSelfTimeout15s 内没走完整套流程,强制解锁并回报 LoadDone
↪ 跨图交还:872ServerMove 应用时发现已进入跨图加载,同步瞬移不接管
↪ REVIVE 交还:898reason = REVIVE,交回复活链路自己处理
⚠ 快照为空:1208StartLevelStreamingFromSnapshot 拿不到快照,直接放人
⚠ LoadDone 全部重试失败:108115513 重试 5 次仍失败,不能把人永远锁着

两种典型翻车

解锁太早 → 穿地下坠。cell 已 Activated 但 Chaos 碰撞体还没 build 完,或者只命中了跨范围的占位 Actor(如 map 9999 的 Cube563)。 地形其实在,只是托不住。第 6 步的 3×3 多点检测就是防它——单点命中不算数。
日志特征:传送-解锁前贴地预检完成 [result: ready-on-first-check] 之后紧跟 [坠落重生] 玩家Z坐标低于关卡允许的最小Z值
灯没交接 → 地形真被卸。第 4 步流送卡死 → 第 5 步的 CopyStreamingSourceInfo 从未执行 → 玩家灯没拿到 LevelLayerName; 等第 8 步保活 30s 到点把落点灯一关,B 的 cell 无源 → 地形肉眼可见消失,人直接掉下去(典型现象:传送成功约 1 分钟后突然坠落)。
日志特征:只有 等待WP场景流送完成(开始) 没有 (结束),之后出现 传送-保活守护结束,关闭 TargetStreamingSource
区分方法:看 等待WP场景流送完成(结束) 有没有打印。
没打 = 灯没交接,是真·地形卸载;打了但人还是掉 = 碰撞未就绪,是假·地形消失。

关键常量

常量值含义
TeleportSelfFallbackTimeoutMs15000 ms整套同步瞬移的兜底上限,也是重力锁最长时间
GroundReadyCheckIntervalMs × MaxAttempts50 × 20贴地预检重试,最多多锁 1s
GroundReadyTraceHorizontalDist / Vertical132 / 2003×3 射线的水平间距与向下长度(cm)
CheckStreamingInterval100 ms流送完成轮询间隔
StreamingSourceRadius7000 cm流送完成检查半径
TeleportStreamingMaxAliveMs30000 ms落点灯保活上限(解锁之后的事)
TeleportKeepAliveMoveThreshold500 cm玩家走出这么远就关落点灯
代码位置:Src/Game/Module/Teleport/TeleportController.ts · Src/Game/World/Entity/Common/BaseMoveSyncComponent.ts · Src/Game/World/SceneLoading/StreamingSourceInfo.ts · Src/Game/World/SceneLoading/Commands/SceneLoadingCmds/
同系列:08_流送源怎么丢的.html(灯为什么会被回收)· 传送地形消失修复方案.md