UE5 蓝图实现「无缝传送」:无限循环走廊的技术解剖

一个第一人称恐怖游戏 Demo 的核心机制:玩家穿过出口门后方的传送盒,被瞬间送回走廊起点——画面零跳变、速度不中断、方向不改变,同时整个世界在玩家眼皮底下完成一次”版本切换”。

本文拆解这套纯蓝图实现的完整方案:坐标重映射算法、蓝图节点链、与版本切换的同帧联动,以及让”无缝”真正成立的三个关卡设计条件。


一、问题:传送为什么容易穿帮

这个游戏的核心体验是无限循环走廊:玩家沿着 C 形走廊走到尽头,开一扇门,走进去——发现自己回到了起点,但走廊里的内容已经变了。

普通的传送实现是 SetActorLocation——直接把人挪到目标点。这在一款要求”直行无感”的恐怖游戏里是致命的,因为传送瞬间有五件事会暴露它:

# 执行要求 穿帮方式
1 视角连续 镜头跳变/闪帧,玩家立刻意识到”被传送了”
2 方向一致 传送后朝向不对:玩家向前走,落地却面朝墙
3 运动保持 速度清零或方向突变:正在走的人落地瞬间”顿一下”
4 光照一致 传送两侧明暗/色温不同,眼前一黑一亮
5 音频连续 环境音断裂、混响突变

前三条是技术问题,后两条是关卡设计问题。技术方案要一次解决前三条,同时给后两条留出配合空间。

二、总体架构:C 形走廊 + 传送闭环

1
2
3
4
5
入口门(短A端头,装饰)

短A 3m(中性:出生点 + 传送落点)──拐角1── 主走廊 30m(Ver_* 版本区)──拐角2── 副走廊(Ver_* 版本区)── 出口门(每圈E键开)
↑ │
└──────────────────────── 无缝传送回短A(门后 Entrance 盒 → 短A Exit 盒)─────────────────────────────────┘

几个关键决策:

  • 走廊不物理闭合。没有回程段、没有双半区、没有中间墙——C 形布局,闭环靠传送。
  • 传送发生在门后。玩家开门 → 穿过门后的 Entrance 触发盒 → 被瞬间送回短A的 Exit 落点。门的开合动画天然遮蔽了循环边界,玩家看不到传送发生的那一刻周围环境的变化(因为落点环境与门后环境一致,见§六)。
  • 传送即切换。传送完成的同时,整个关卡完成一次版本切换(v1→v2→…→v8→v1),8 个版本 = 8 圈。玩家落地的短A永远中性,变化全部发生在玩家视野之外(拐角后面),等他走过去才慢慢发现”这一圈不一样了”。

方案对比:曾经考虑过”零位移版本切换”(传送不动人、只切内容)和”口形双半区”(物理闭合 + 中间墙),最终选择传送闭环,因为它保留了”出口门 → 真出口”的 M3 结局翻转空间(最后一圈停用传送盒,门后就是真正的结局)。

三、核心算法:本地坐标 → 世界坐标的重映射

传送的本质是把玩家在入口盒局部坐标系中的姿态,原样搬运到出口盒的世界坐标系里

设:

  • E = Entrance 盒的世界变换(门后)
  • X = Exit 盒的世界变换(短A落点)
  • P = 玩家当前世界变换

三步:

1
2
P_rel = Inv(E) · P        (玩家姿态 → 入口局部系)
P' = X · P_rel (入口局部系 → 出口世界系)

用蓝图里的说法:MakeRelativeTransform(P, E)ComposeTransforms(X, P_rel)

这一做法的妙处在于速度和朝向的连续性自动成立

  • 玩家在入口盒里偏左 0.3m、面朝门走 → 落地时在出口盒里偏左 0.3m、面朝同样的走廊前进方向。相对关系被完整保留,直行感不打折。
  • 速度同理:把玩家当前世界速度从入口局部系旋转到出口世界系——动量方向跟着走廊朝向一起转,玩家”正在往前走”的状态不中断,甚至倒着走、侧着走都成立(实测:倒走穿过传送盒,倒着出现在短A,动量连续)。

速度重映射的数学形式:

1
2
V_rel = Inv(Rot_E) · V_world
V' = Rot_X · V_rel

蓝图中用 MakeRotFromX(出口前向向量 → Rotator)+ Vector << Rotator / Rotator >> Vector 旋转节点实现。

还有一个极易被忽略的细节:控制器朝向必须单独处理K2_SetActorTransform 只挪动 Pawn 本体,摄像机的朝向由 Controller 的 ControlRotation 单独维护。因此流程里要有:

1
2
3
OldRot = GetControlRotation()          (传送前取)
NewRot = 重映射(OldRot, E → X) (同样的旋转差)
SetControlRotation(NewRot) (传送后写回)

漏掉这一步的典型症状:传送后 Pawn 面向正确,但玩家视角还停留在传送前方向,必须手动甩一下鼠标才”回正”——穿帮。

四、蓝图落地:BP_SeamlessTeleportBox

从资产逆向出的完整结构如下。

组件

组件 类型 作用
根组件 SceneComponent 挂载
Entrance BoxComponent 触发盒,摆门洞后 0.3–0.5m,覆盖整个走廊截面(2.2×2.8×0.5m),红箭头朝行进方向
ArrowComponent Arrow 编辑器里可视化出口朝向

变量

变量 类型 说明
Exit SceneComponent 引用(Instance Editable) 关卡里指向短A落点处的定位组件
Exitfwd Vector(Instance Editable) 出口前向(落点后继续直行的方向,指向拐角1)
LoopManagerRef BP_LoopManager 引用(Instance Editable) 版本切换管理器
DestinationTransform / DestinationVelocity Transform / Vector 传送帧内的临时计算结果

事件链(OnComponentBeginOverlap,绑在 Entrance 上)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
玩家进入 Entrance 盒
→ GetPlayerCharacter / GetPlayerController / GetMovementComponent
→ GetControlRotation (记下当前视角朝向)
→ GetDestinationTransform(): (自定义函数)
K2_GetComponentToWorld(Entrance) (入口盒世界变换 E)
MakeRelativeTransform(P, E) (玩家姿态 → 入口局部系)
GetTransform(Exit) (出口世界变换 X)
ComposeTransforms(X, P_rel) (→ 目标世界变换)
→ K2_SetActorTransform(Destination, bTeleport = true) (瞬移,不触发物理滑动)
→ GetDestinationVelocity(): (自定义函数)
MakeRotFromX(Exitfwd) (出口朝向 → Rotator)
Vector << / >> Rotator (世界速度按入口→出口旋转差重映射)
→ 写回 MovementComponent 的 Velocity (动量方向同步到出口朝向)
→ SetControlRotation(NewRotation) (视角重映射,见§三)
→ Branch(LoopManagerRef Is Valid)
→ LoopManagerRef.AdvanceVersion() (同帧切换版本,见§五)

两个值得解释的节点:

  • K2_SetActorTransform(..., bTeleport = true):Teleport 语义——不扫掠、不触发碰撞响应、不产生物理速度突变,是最干净的”挪位置”方式。用它而不是 SetActorLocation,是避免落地时和地面碰撞体互相推挤(抖动/卡住)的关键。
  • 事件只绑 Entrance:Exit 盒不绑任何事件。这自然消灭了”折返多切”问题——玩家落地后往回走、或反复进出短A,都不会触发任何东西,每圈只推进一次。

五、同帧联动:BP_LoopManager 版本切换

传送负责”回环”,版本切换负责”世界在变”。两者的时序要求:同帧完成,玩家不可能看到中间态

LoopManager 是关卡里的单实例 Actor,逻辑极其简单:

BeginPlay:隐藏 v2..v8

1
2
3
For Loop(Index = 2 .. MaxVersion)
→ Get All Actors With Tag(Tag = "Ver_" + Index)
→ ForEach → Set Actor Hidden In Game = true

AdvanceVersion()(由传送盒调用):

1
2
3
4
藏旧:Get All Actors With Tag("Ver_" + CurrentVersion) → ForEach → Set Hidden = true
CurrentVersion = CurrentVersion % MaxVersion + 1 (v8 之后回卷到 v1)
显新:Get All Actors With Tag("Ver_" + CurrentVersion) → ForEach → Set Hidden = false
OnLoopAdvanced.Broadcast(CurrentVersion) (事件分发器,留给异常系统订阅)

设计要点:

  • 单关卡 + Tag 显隐,不用 Level Streaming。8 个版本的内容全部预生成在同一个持久关卡里,切换 = 一组 Actor 的显隐翻转,没有加载、没有卡顿——“切换会不会卡”这个问题根本不存在
  • 动态 Tag 查询"Ver_" Append ToString(Index) → String to Name。不缓存数组、不用 Map,切换时现场查询。版本组里增删任何物体都不需要改管理器代码。
  • OnLoopAdvanced(Version) 分发器是 M3 异常系统的挂载点:每个异常事件按版本号订阅,切换瞬间自动生效。

六、让无缝真正成立的三个关卡设计条件

技术做对了”位置/速度/视角”,但传送瞬间玩家眼睛会看到什么,是关卡设计决定的。三条铁律,违反任何一条当场穿帮:

  1. 短A 绝对中性:切换瞬间玩家正站在短A里——短A 放任何版本差异内容,切换的那一刻它就会在玩家眼皮底下变。所以短A(出生点 + 传送落点)永远干净,是全关卡唯一”不变的锚点”。
  2. 拐角干净区 5m:玩家站在短A,透过拐角1 能看到主走廊头段的一条”余光楔形”。主走廊前 5m 必须所有版本共用、无差异内容,版本变化从干净区之后才开始。
  3. 传送两侧环境一致:Entrance 盒周边(门后)和 Exit 盒周边(短A)的墙、地板、灯光必须一模一样。穿门瞬间画面构图不跳,玩家才会觉得”我就是走到了短A”,而不是”被传送了”。

配套细节:灯光全部 Movable、不烘焙(显隐切换的 Actor 与烘焙光照不兼容);步速锁 180cm/s(慢步,恐怖感的底子,也降低传送瞬间的感知敏感度);环境底噪全关卡共享,传送不切音频。

七、踩坑记录(实测掉进去过的)

# 症状 解法
1 盒子红箭头朝向反了 180° 玩家落地头朝后、速度反向 Entrance/Exit 红箭头都朝玩家行进方向
2 BoxSize = 0 重叠事件永远不触发 在实例上设置盒子尺寸
3 事件绑到 Exit 盒上 出生即触发,开局就传送 事件只绑 Entrance,Exit 不绑
4 Set CurrentVersion 接在循环体 then 上 组里几个物体就跳几个版本 必须接在 ForEach 的 Completed 引脚上
5 Tag 加在 Component Tags Get All Actors With Tag 查不到 只有 Actor 分类下的 Tags 才被匹配
6 Tag 拼写/大小写不一致 藏旧显新失效,两个版本叠在一起 Ver_1 全关卡拼写完全一致

八、验收测试

传送盒上线时的 7 项测试,其中第 2、4 项是整个方案的地基:

# 操作 预期
1 开局走向主走廊 看到 v1 内容,v2 不可见
2 走完整圈、开门、穿过传送盒 回到短A,画面零跳变,朝向/速度连续
3 第二圈走主走廊 看到 v2 内容,v1 已消失
4 传送瞬间盯着屏幕 短A 内无任何可见变化
5 倒走穿过传送盒 倒着出现在短A,动量连续(方向无关性)
6 传送后立刻回头 看到短A/入口门场景,不是副走廊
7 连走多圈 每圈版本 +1,v8→v1 回卷

九、通用化与展望

这套”入口局部系 → 出口世界系”的重映射方案,抽掉游戏背景就是一个通用的 A→B 无缝传送工具,可以直接套用:

  • 不可能空间:任何两个视觉上”不该连通”的房间之间开一扇门,门后放 Entrance 盒,门外放 Exit 盒;
  • 无尽楼梯 / 无尽走廊:比物理闭环方案省一整段回程几何;
  • 传送门游戏(Portal-like):把 Exit 盒换成另一个动态可移动的引用即可,算法不变。

本项目的下一步(M3):最后一圈(v8)停用传送盒,出口门后放真出口——同一个门,前 7 圈通向循环,最后一圈通向结局。到此完成闭环。


技术栈:UE 5.8 · 纯蓝图(零 C++)· 单持久关卡。传送盒与版本切换均已实测通过(含倒走穿盒、连续多圈回卷)。