引子:走廊里的那只眼睛
小周负责的第三人称动作项目最近进入打磨期。主角是一名能够在废墟间跑酷的侦查兵,战斗以近战连招和闪避为主。周二下午,策划老赵把一段录屏发到群里:玩家贴墙移动时,镜头整个钻进了混凝土墙面,屏幕里只剩一片灰褐色贴图,连敌人在哪都看不到。
另一段录屏更严重。 Boss 战里玩家向后闪避,SpringArm 被身后的石柱挡住,镜头瞬间贴到主角后脑勺。Boss 高举斧头准备下劈,屏幕却全是主角的肩膀。小周当时坐在工位上,盯着视频里那个几乎贴在脸上的镜头,意识到相机系统还有很多参数没有调到位。
这类问题在第三人称游戏里几乎绕不开。SpringArm 是 UE 提供的一个伸缩杆组件,Camera 挂在它的末端。玩家跑动、转身、靠墙时,这根杆的长度、偏移、延迟和碰撞检测共同决定了最终画面。参数调得好,镜头像幽灵一样顺滑地绕过障碍;调不好,镜头就成了玩家最大的敌人。
这篇文章把 UE5.8 里 SpringArm 与 Camera 的关键配置拆开,结合我们踩过的穿墙、贴脸、遮挡几类坑,把每个参数的实际意义和调试方法说清楚。
一、SpringArm 的结构:一根带弹性的虚拟伸缩杆
SpringArmComponent 继承自 SceneComponent,核心作用是把 Camera 固定在角色背后一个可控的位置。它的内部结构可以简化成三个点:
- Root / Target:通常绑定在角色 Capsule 或 Mesh 的某个 Socket 上,代表镜头要围绕谁转。
- Arm End / Socket:SpringArm 末端的位置,Camera 就挂在这里。
- Camera:实际产生视图矩阵和投影矩阵的 CameraComponent。
SpringArm 每帧会先根据角色的旋转和 Arm Length 算出 Arm End 的世界位置,再把 Camera teleports 过去。Camera 本身不再负责「我该站在哪」,只负责「我看到什么」。这种分层让第三人称、俯视角、越肩视角可以用同一套 Camera 逻辑,只改 SpringArm 参数就能切换风格。
architecture-beta
group character[角色 Actor]
service mesh[Skeletal Mesh]
service capsule[CapsuleComponent]
service spring[SpringArmComponent]
service camera[CameraComponent]
end
mesh:R --> L:spring
capsule:R --> L:spring
spring:R --> L:cameraCameraComponent 不一定要直接 Attach 到 SpringArm。SpringArm 有一个 ChildActorClass 属性,也可以把 Camera 放到一个 Blueprint 里再挂上去。但最常见的做法是在 Character Blueprint 的组件树里把 Camera 拖成 SpringArm 的子节点。这样 SpringArm 旋转时,Camera 会跟着绕角色做球面运动。
SpringArm 的旋转来源默认是父组件的世界旋转。如果你的角色朝向由 Controller 控制,需要把 SpringArm 的 bUsePawnControlRotation 设为 true。否则快速转动视角时,SpringArm 不会跟随鼠标或右摇杆,镜头会 lag 在角色背后。
有一个细节容易忽略:SpringArm 的位置更新发生在 USpringArmComponent::UpdateDesiredArmLocation。这个函数在每一帧的 Tick 里被调用,先计算 desired 位置,再处理 lag 和 collision,最后把 Camera 放到最终位置。如果你写自定义相机逻辑,重载这个函数是最直接的切入口。
二、Target Arm Length、Socket Offset、Target Offset
这三个参数决定了 Camera 相对于角色的几何关系。理解它们的最佳方式不是背定义,而是在编辑器里打开「Show Collision」和「Camera Frustum」,边拖动滑块边看变化。
Target Arm Length
Target Arm Length 是 SpringArm 未受碰撞压缩时的理想长度,记作 。它衡量的是 SpringArm 根部与 Camera Socket 之间的直线距离。默认值通常是 300 个单位左右,对应第三人称动作游戏常见的过肩距离。
这个值没有 universal 最优解。近战游戏往往把 放到 250 以下,让玩家看清招式;射击游戏可能放到 350 以上,换取更开阔的视野;俯视角项目会把 拉到 1200 甚至更高,同时把 SpringArm 的俯仰角调到 -60 度左右。
当 DoCollisionTest 开启且 Arm End 与角色之间有遮挡时,实际 Arm Length 会小于 。我们把被压缩后的长度记作 。碰撞处理会在第四节详细讲。
Socket Offset
Socket Offset 是在 Arm End 局部坐标系里的一个平移,记作 。它不改变 SpringArm 本身的长度,只改变 Camera 挂在末端的位置。
最常见的用法是把 Camera 向右上方挪动一点,模拟越肩视角。例如 会让 Camera 位于角色右肩后方。右肩还是左肩通常由项目的美术构图决定,但要考虑掩体射击时哪只手持枪。
Socket Offset 对俯视角也很有用。把 Camera 在局部 Z 轴上抬高,可以获得更居中的俯瞰画面。如果只调 Arm Length 而不调 Socket Offset,Camera 会沿着 SpringArm 方向越走越远,俯视角容易出现「斜着看地面」的感觉。
Target Offset
Target Offset 是在 SpringArm 根部坐标系里的平移,记作 。它改变的是 SpringArm 围绕谁转,而不是 Camera 挂在哪。
例如把 Target Offset 的 Z 设为 60,SpringArm 的原点会上移到角色胸口位置,Camera 也会整体抬高。这在第一人称和越肩视角里特别重要。如果 SpringArm 原点还在 Capsule 底部,镜头会始终从角色腰部出发,经过 Socket Offset 再抬高,容易产生奇怪的视角漂移。
一个实用的调试技巧:把 bDrawDebugLagMarkers 设为 true,运行时能看到代表 Target 和 Desired 位置的两个小球。Target Offset 会让 Target 小球偏离 Capsule 中心,Socket Offset 会让 Desired 小球偏离 Arm End。两个小球都看清楚,参数就不会调乱。
下面这张图把三个偏移的关系画了出来。
flowchart LR
A[角色根节点] --> B[Target Offset 平移]
B --> C[SpringArm 旋转中心]
C --> D[Target Arm Length 沿旋转方向延伸]
D --> E[Arm End]
E --> F[Socket Offset 平移]
F --> G[Camera 最终位置]三、Camera Lag 与 Rotation Lag:让镜头变重
没有延迟的镜头像粘在角色背后,移动时画面僵硬。Lag 给镜头加上惯性,让 Camera 的运动像有质量一样滞后于角色。UE5.8 提供两套参数:CameraLagSpeed / CameraLagMaxDistance 控制位置滞后,CameraRotationLagSpeed 控制旋转滞后。
位置 Lag
位置 Lag 的物理意义是:Camera 不会立刻到达 Desired 位置,而是以一定速度向它靠拢。用公式描述就是
其中 是这一帧 Camera 位置, 是 Desired 位置, 是 CameraLagSpeed, 是帧间隔时间。 越大,指数衰减越快,镜头越跟手; 越小,镜头越飘。
CameraLagMaxDistance 限制 Camera 与 Target 的最大距离。没有这个上限,角色高速冲刺时 Camera 可能被甩出去几十米。一般把最大距离设为 Arm Length 的 0.3 到 0.5 倍。
另一个相关参数是 CameraLagSubstepping。开启后,Lag 计算会在一帧内分多步执行,减少高帧率下的数值误差。动作游戏建议打开,尤其是角色移动速度经常超过 1000 cm/s 的项目。
旋转 Lag
CameraRotationLagSpeed 控制 SpringArm 旋转的惯性。它的行为与位置 Lag 类似,只是作用在旋转分量上。值越大,镜头转向越灵敏;值越小,转身时镜头会慢半拍。
旋转 Lag 在近战游戏里需要谨慎。玩家连续闪避绕背时,如果旋转 Lag 太大,镜头会跟不上角色朝向,玩家看不清身后敌人。我们项目把这个值从默认的 10 调到 25,同时保留了位置 Lag,让跑动有重量感但不影响战斗跟手度。
下面这个时序图展示了输入、角色旋转、SpringArm Lag、Camera 更新之间的关系。
sequenceDiagram
participant Input as 玩家输入
participant PC as PlayerController
participant Pawn as Character
participant Spring as SpringArm
participant Cam as Camera
Input->>PC: 鼠标/摇杆输入
PC->>Pawn: AddControllerYawInput
Pawn->>Spring: 更新目标旋转
Spring->>Spring: 应用 Rotation Lag
Spring->>Spring: 计算 Desired Arm Location
Spring->>Spring: 应用 Camera Lag
Spring->>Spring: 执行 DoCollisionTest
Spring->>Cam: 设置 Camera 世界变换
Cam->>PC: 返回 ViewTarget四、DoCollisionTest 与碰撞处理
SpringArm 最重要的功能之一是自动处理相机穿墙。bDoCollisionTest 控制是否开启这个特性。开启后,SpringArm 会从角色位置向 Arm End 发射一条射线,如果射线碰到墙壁,就把 Camera 推到碰撞点内侧,避免镜头进入障碍物。
Probe Channel 与 Probe Size
碰撞检测默认使用 ProbeChannel = ECC_Camera,也可以用 ECC_Visibility 或自定义通道。通道的选择直接影响哪些物体会把镜头推回来。场景里的玻璃、树叶、半透明帘子如果设置了 Camera 碰撞,镜头会被它们挡住;如果不想让这些小物体推镜头,需要单独调整它们的 Collision Response。
ProbeSize 决定射线检测的粗细。默认值 12 相当于一根细线,适合狭窄室内;调到 30 以上会让 SpringArm 更早地与门框、转角发生反应,减少镜头突然贴脸的概率,但在复杂场景里可能让镜头过于保守。
碰撞压缩的数学
碰撞处理会沿着 SpringArm 方向把 Camera 从 Arm End 向根部推,直到射线不再命中。压缩后的有效长度 满足
其中 是射线命中点到 SpringArm 根部的距离,Margin 是安全余量,由 ProbeSize 和引擎内部处理共同决定。如果命中点就在根部附近, 可能变得非常小,这就是老赵录屏里镜头贴到主角后脑勺的原因。
平滑过渡
直接把 Camera 从 切到 会产生明显抖动。UE5.8 在 SpringArm 里做了插值,碰撞压缩和恢复都不是瞬时的。DoCollisionTest 开启时,每帧计算的新 Arm Length 会经过一个内部阻尼函数,避免镜头在墙角来回弹跳。
还有一个容易被忽略的参数:bInheritPitch、bInheritYaw、bInheritRoll。它们控制 SpringArm 是否继承父组件的对应旋转分量。第一人称视角通常把 Pitch 和 Yaw 继承关闭,让 Camera 完全由 Controller 控制;第三人称则通常全部继承,让镜头随角色动画自然摆动。
下面这张图是 DoCollisionTest 的完整流程。
flowchart TD
A[每帧 Tick] --> B[计算 Desired Arm Location]
B --> C{CameraLagSpeed > 0?}
C -->|是| D[对 Desired 位置应用 Lag 插值]
C -->|否| E[使用 Desired 位置]
D --> F{bDoCollisionTest?}
E --> F
F -->|否| G[Camera 放到 Arm End]
F -->|是| H[沿 SpringArm 方向发射 Probe]
H --> I{是否命中?}
I -->|否| G
I -->|是| J[计算命中距离 d_hit]
J --> K[Clamp 有效长度 L']
K --> L[对 L' 做平滑插值]
L --> M[Camera 放到压缩后的位置]
M --> N[下一帧]
G --> N五、Camera 的 Field of View 与 Aspect Ratio
CameraComponent 的 Field of View 决定水平视角宽度,Aspect Ratio 决定宽高比。两者共同影响玩家看到的画面范围。这两个参数看起来简单,但和 SpringArm 配合时会产生一些容易忽视的视觉问题。
FOV 的选取
UE5.8 默认 FOV 是 90 度。动作游戏通常维持在 90 到 100 度之间,太窄会限制侧方视野,太宽则会产生鱼眼畸变。我们把项目默认值设成 96,因为近战需要看到两侧敌人,而 100 以上会让场景边缘拉伸得难以忍受。
FOV 和 Arm Length 之间存在耦合。提高 FOV 后,同样 Arm Length 下 Camera 能看到的范围变大,角色在屏幕上的占比变小。如果只调 FOV 不调 Arm Length,玩家会感觉角色突然变远。反过来,缩短 Arm Length 也会放大角色占比,需要配合降低 FOV 才能保持构图稳定。
用公式表达 FOV 与成像范围的关系:
其中 是感光面上成像高度, 是焦距。在游戏引擎里可以近似理解为:距离 Camera 处的垂直可视范围是 。FOV 越大, 越大,画面装下的场景越多。
Aspect Ratio
Aspect Ratio 通常由当前显示设备决定,PC 上常见 16:9、21:9、16:10。CameraComponent 默认会跟随视口比例自动计算垂直 FOV,保持水平 FOV 不变。这意味着 21:9 超宽屏玩家会比 16:9 玩家看到更多左右内容。
如果你的项目对竞技公平性要求很高,可以关闭 bConstrainAspectRatio 的某些行为,或者把 Camera 的 Projection Mode 设为 Orthographic。但第三人称动作游戏一般不需要这么严格,保持默认透视投影即可。
graph LR
A[CameraComponent] --> B[Projection Mode]
B --> C[Perspective]
B --> D[Orthographic]
C --> E[FOV 控制水平视野]
C --> F[Aspect Ratio 控制宽高比]
E --> G[视角宽度随距离扩大]
F --> H[不同屏幕显示范围不同]Camera 的 Post Process 与 FOV 联动
CameraComponent 上还可以挂 Post Process Settings。高 FOV 容易让画面边缘产生暗角和运动模糊,可以通过 Post Process Volume 或 Camera 上的设置做边缘补偿。不过这个已经超出 SpringArm 的范畴,本文只提一点:调整 FOV 后一定要重新跑一遍镜头碰撞测试,因为可视锥变宽后,原本不会穿帮的墙角可能进入画面。
六、俯视角、越肩视角、第一人称的 SpringArm 配置差异
同一套 SpringArm 可以通过参数组合做出完全不同的视角风格。下面是我们项目里三种常用配置的对比。
俯视角
俯视角的 SpringArm 通常拉得很长,俯仰角设为 -60 到 -75 度,偏航角跟随角色或固定。Target Offset 的 Z 要抬高,让 Camera 从角色正上方往下看。Arm Length 一般在 1000 到 2000 之间。
这种配置下 DoCollisionTest 非常关键。镜头离角色很远,中间很容易穿过屋顶、树木、桥洞。如果不做碰撞处理,玩家会看到大量建筑内部贴图。我们的做法是把 Probe Channel 设为 Visibility,并且给大型建筑屋顶设置 Camera 阻挡,让小物件和敌人不推镜头。
CameraLagSpeed 可以设得较低,比如 3 到 5,让镜头移动更沉稳。RotationLagSpeed 也可以放低,因为俯视角不需要快速转身跟枪。
越肩视角
越肩视角的核心是 Socket Offset。把 Camera 放在角色右肩后方, 控制在 200 到 300,Pitch 保持平视或轻微俯视。Target Offset 的 Z 可以稍微抬高到胸部高度,避免镜头从腰部出发造成的仰视感。
这种视角对碰撞最敏感。玩家贴墙时,Camera 会迅速被压到角色身后,画面只剩主角肩膀。常见的处理办法有两个:一是把 ProbeSize 调大,让 SpringArm 在距离墙面还有一段距离时就开始收缩;二是给角色身边的墙体添加一个薄的透明触发盒,碰到时主动把 Camera 切换到另一侧肩膀。
我们项目最终采用了第二种方案。策划定义了一个「相机禁区」体积,当 Camera 进入禁区时,把 Socket Offset 的 Y 从正 60 翻转到负 60,镜头自动换到左肩。这个切换需要配合短时间的 blend,否则会跳帧。
第一人称
第一人称通常不需要 SpringArm,直接把 Camera 绑到角色头部 Socket 上。但如果项目想要在第一人称和第三人称之间平滑切换,保留一个长度为 0 或很短的 SpringArm 会更方便。
第一人称的 SpringArm 配置要点是:关闭 Camera Lag 和 Rotation Lag,关闭 bDoCollisionTest,关闭 bInheritRoll,把 bUsePawnControlRotation 打开。Camera 的位置完全由头部 Socket 和 Controller 输入决定,任何延迟都会让玩家产生晕动症。
第一人称的 FOV 通常比第三人称低,常见 80 到 90 度。太低会让玩家像隔着望远镜看东西,太高又会加快边缘运动速度,加重眩晕。
下面这张表总结了三种视角的典型参数范围。
| 参数 | 俯视角 | 越肩视角 | 第一人称 |
|---|---|---|---|
| Target Arm Length | 1200 - 2000 | 200 - 350 | 0 - 50 |
| Pitch | -60° ~ -75° | -5° ~ -15° | 0° |
| Socket Offset Y | 0 | 40 ~ 80 | 0 |
| Target Offset Z | 80 ~ 150 | 40 ~ 80 | 角色眼高 |
| CameraLagSpeed | 3 - 6 | 8 - 15 | 0 或很高 |
| RotationLagSpeed | 3 - 6 | 10 - 25 | 0 或很高 |
| DoCollisionTest | 开启 | 开启 | 关闭 |
七、相机遮挡与透明处理
DoCollisionTest 解决的是 Camera 自身穿墙,但还有一种情况:Camera 没穿墙,角色和 Camera 之间却出现了遮挡物。例如角色背靠着灌木丛,灌木在 Camera 和角色之间,把主角遮得只剩两条腿。
深度检测与遮挡查询
处理这类问题的第一步是确定遮挡物。常用做法是从 Camera 向角色发射多条射线,命中结果按距离分类。距离 Camera 较近的物体如果挡在角色前面,就可以标记为需要透明化。
UE5.8 提供了 LineTraceSingleByChannel 和 MultiSphereTraceByChannel 两种常用接口。多条射线比单条射线稳定,因为单条射线很容易在角色两腿之间漏过去。我们项目用了五根射线,分别指向角色头顶、胸口、腰、左右肩。
材质透明化
确定遮挡物后,常见处理方案有三种:
方案一:动态修改材质参数。 在遮挡物的 Material Instance 上调整 Opacity 或 Cutout Threshold,让物体半透明。优点是视觉自然,缺点是需要材质支持,且大量物体同时透明时开销较大。
方案二:隐藏 Mesh Section。 只对遮挡角色的那一部分 Mesh 做隐藏。例如一堵墙只隐藏中间几块砖。实现复杂,需要按 Section 或按材质索引处理,适合有模块化建筑的项目。
方案三:Camera Collision Volume。 给 Camera 周围加一个球形碰撞体,凡是被球体碰到的物体都淡出。这种方案最简单,但会把镜头附近所有物体都透明化,包括敌人武器,可能导致战术信息丢失。
我们的项目综合使用了方案一和方案三。小物件用 Camera Collision Volume 批量淡出,大型建筑用射线检测后单独调整材质透明度。
淡出曲线
透明化不能瞬间完成,否则画面会闪。通常会维护一个 Opacity 目标值和当前值,每帧用插值靠近目标。插值速度按遮挡物类型区分:植被可以淡出得快一些,建筑墙面则要慢一些,避免玩家察觉到墙体突然消失。
淡出方向也值得考虑。只把角色前面的物体透明,角色后面的物体保持不透明,这样既能看清主角,又不会把整个世界都变成玻璃。判断前后关系可以用点积:取遮挡物表面法线和 Camera-to-Character 方向,点积大于 0 说明物体在角色前方。
八、C++ 与 Blueprint 示例
下面这段 C++ 代码展示了在 Character 构造函数里创建 SpringArm 和 Camera,并配置常用参数。
AMyCharacter::AMyCharacter()
{
PrimaryActorTick.bCanEverTick = true;
SpringArm = CreateDefaultSubobject<USpringArmComponent>(TEXT("SpringArm"));
SpringArm->SetupAttachment(RootComponent);
SpringArm->TargetArmLength = 300.0f;
SpringArm->SocketOffset = FVector(0.0f, 60.0f, 60.0f);
SpringArm->TargetOffset = FVector(0.0f, 0.0f, 60.0f);
SpringArm->bUsePawnControlRotation = true;
SpringArm->bDoCollisionTest = true;
SpringArm->ProbeChannel = ECC_Camera;
SpringArm->ProbeSize = 12.0f;
SpringArm->CameraLagSpeed = 12.0f;
SpringArm->CameraLagMaxDistance = 120.0f;
SpringArm->CameraRotationLagSpeed = 20.0f;
Camera = CreateDefaultSubobject<UCameraComponent>(TEXT("Camera"));
Camera->SetupAttachment(SpringArm, USpringArmComponent::SocketName);
Camera->FieldOfView = 96.0f;
Camera->bUsePawnControlRotation = false;
}Blueprint 里的配置和 C++ 完全对应。在 Character Blueprint 的组件树里添加 SpringArm,把 Camera 拖成它的子节点,然后在 Details 面板里找到 Camera 栏位。关键节点包括:
SpringArm:
Target Arm Length: 300
Socket Offset: {X: 0, Y: 60, Z: 60}
Target Offset: {X: 0, Y: 0, Z: 60}
Use Pawn Control Rotation: true
Do Collision Test: true
Probe Channel: Camera
Probe Size: 12
Camera Lag Speed: 12
Camera Lag Max Distance: 120
Camera Rotation Lag Speed: 20
Camera:
Field Of View: 96
Use Pawn Control Rotation: false运行时切换视角的 Blueprint 逻辑可以这样写:
事件: 按下切换视角键
分支: 当前为第三人称?
真:
设置 SpringArm.TargetArmLength = 0
设置 SpringArm.SocketOffset = {0, 0, 0}
设置 SpringArm.bDoCollisionTest = false
设置 Camera.FieldOfView = 85
假:
设置 SpringArm.TargetArmLength = 300
设置 SpringArm.SocketOffset = {0, 60, 60}
设置 SpringArm.bDoCollisionTest = true
设置 Camera.FieldOfView = 96注意第一人称切换时不能简单把 Arm Length 设成 0。如果 Target Offset 或 Socket Offset 不为 0,Camera 仍然不会精确位于眼睛位置。最好把这两个偏移也清零,或者在第一人称模式下直接把 Camera Attach 到头部 Socket。
结语
小周修完相机穿墙问题后,在 Boss 战里又多调了两周。最后确定的参数并不复杂:Arm Length 280,Socket Offset 右移 55、抬高 50,CameraLagSpeed 12,RotationLagSpeed 22,ProbeSize 18。但每一个数值背后都是一次具体的玩家体验问题。
SpringArm 和 Camera 的配置没有标准答案。项目类型、角色体型、场景尺度、输入设备都会影响最佳参数。调相机时最重要的是把调试工具打开:显示 Camera Frustum、显示 Collision Probe、打开 Debug Lag Markers。眼睛看到镜头是怎么动的,比背参数定义有效得多。
这篇文章覆盖了 SpringArm 的核心结构、长度与偏移、Lag、碰撞检测、FOV 与宽高比、三种视角差异、遮挡透明化,以及 C++ 和 Blueprint 的基础写法。希望这些记录能帮你在 UE5.8 里少踩几个镜头相关的坑。