Unity TPS相机系统全解析:从跟随、旋转到遮挡避让与手感调校

发布时间:2026/10/14 15:36:57
Unity TPS相机系统全解析:从跟随、旋转到遮挡避让与手感调校
写TPS相机这个话题其实挺有意思的。很多刚接触Unity的朋友第一件事就是搭一个第三人称视角结果发现随便摆个相机跟着角色走手感就是不对。镜头太硬、穿墙、旋转跟手不跟手、角色一转身画面就乱晃这些问题十个新手九个踩。今天就把一个能用于实际项目的TPS相机系统整个拆开讲清楚包含原理、代码、调参和避坑。1. 先搞清楚TPS相机到底在解决什么问题1.1 相机并不是“跟着角色这么简单”先理解一个核心逻辑TPS第三人称射击相机并不是单纯把相机放在角色后面就完事它要同时满足三个需求——位置跟随、视角旋转、遮挡规避。这三个需求乍一听都很简单但叠加在一起之后问题就变复杂了。位置跟随很好懂角色走相机跟着动。但怎么跟直接每帧把相机位置设成角色位置加一个偏移量这叫硬跟随镜头完全不缓冲角色一停相机立刻停视觉上会感觉镜头像钉在角色身上一样玩游戏时会有一种“脖子被拽着走”的僵硬感。常规的做法是给跟随加上阻尼比如用Vector3.SmoothDamp或者插值让相机的运动比角色稍微“慢半拍”这样镜头才有自然的惯性感。视角旋转是微妙的那部分。TPS相机不仅要跟随还要让玩家能通过鼠标改变观察方向。这里有个关键分岔路一种做法是相机绕角色转角色模型也跟着相机朝向走另一种做法是只有相机转角色保持自己的朝向开火时角色再单独转向。这两种模式对应了不同的游戏类型——前者偏动作游戏后者偏射击游戏。本篇讲的是射击游戏里最常用的“鼠标控制相机绕角色旋转、角色朝向与相机分离”的方案。遮挡规避是进阶需求但也是绝对必备项。当角色走到墙边或者墙角挡住镜头时如果相机不做任何处理画面里就是一堵墙的背面角色完全看不见这游戏基本就没法玩了。所以要实现光线检测发现相机和角色之间有障碍物时自动把相机沿着视线方向拉近直到障碍物消失。这三个需求叠加起来才是一个能用的TPS相机。只做其中任何一个都会让人觉得“哪里怪怪的”。1.2 两种主流视角风格先选再写开始实现之前必须先确定你要的是哪一种风格。这直接决定了代码结构。第一种是越肩视角。相机偏向左肩或右肩画面中角色占左下或右下区域能看到自己角色的侧面和部分场景。大多数掩体射击游戏都是这种比如常见的战术射击游戏。实现时需要一个水平方向的角度偏置比如向左偏15度再配合一个竖直方向的高度。第二种是正后方视角。相机精确指向角色背后角色位于屏幕正中。这种更偏动作游戏或部分ARPG实现上简单一点因为不需要肩位偏置准星和角色朝向天然一致。两者在代码上的差异不大核心都是“偏移量旋转矩阵”区别只在于偏移量的坐标值。我会把这两种的需求统一用一个方案去覆盖——通过调整offset参数就能同时适配两种风格。2. 核心设计我用的这套TPS相机方案是怎么构思的2.1 偏移量、旋转矩阵与空间变换的关系TPS相机的经典设计思路可以理解成一句话相机的位置 角色位置 偏移量经过旋转后的。“经过旋转后的”才是关键。因为当我们转动视角时相机不再是固定在角色背后某一个点而是要在角色周围的一个球面上移动。这个球面的半径就是偏移量的长度而鼠标的横向移动对应球面上的经度变化纵向移动对应纬度变化。那么数学上怎么做先定义一个初始偏移量比如new Vector3(0, 1.6f, -3)意思是相机比角色高1.6米、往后3米。当玩家转动鼠标后这个偏移量要被旋转矩阵处理一下才能得到新的位置。在Unity里不需要手写矩阵直接用四元数乘以向量即可Quaternion rotation Quaternion.Euler(pitch, yaw, 0); Vector3 desiredPosition target.position rotation * offset;这段代码是指把初始偏移量按当前的pitch和yaw角度旋转得到新的偏移方向然后加到角色位置上。这句代码就是整个TPS相机的数学基石。后续所有的平滑、遮挡处理都是围绕这个desiredPosition来做文章。有些方案喜欢直接用相机transform的forward方向反推位置效果差不多但用旋转矩阵的好处是你始终知道相机理论上应该处于球面的哪个点上对后面做碰撞修正很方便。2.2 为什么选择“角色朝向与相机朝向分离”的架构我做这个相机时比较看重的一个架构决策是角色模型朝向和相机朝向互不干扰。很多教程会写“让角色模型面朝相机前进方向”这在只做第三人称演示时没问题但放到射击游戏里就会出大问题。试想一下玩家面朝左边瞄准掩体后面的敌人但操作角色向左移动时如果角色模型直接转向左边那你马上会看到角色在侧身跑——这其实还算正常再极端一点玩家用鼠标环顾四周角色会一直扭来扭去打枪的时候该脸朝哪边就彻底乱了。标准的射击游戏方案是相机负责视觉角色模型负责战斗朝向移动方向交给输入系统。具体来说角色移动时模型朝向由“移动方向”决定站立瞄准时模型朝向由“最后一次开火方向”或“准星指向”决定。这和相机没有直接关系。所以在相机脚本里我不会去旋转角色transform而是把旋转信息通过一个字段暴露出来public float CurrentYaw yaw; public float CurrentPitch pitch;其它系统比如角色动画控制器、武器瞄准系统需要的时候自己来取这个角度。这种解耦方式非常干净调试起来也舒服——改相机不会影响到角色状态角色状态也不会导致镜头乱晃。2.3 平滑算法选型SmoothDamp还是LerpTPS相机的平滑处理是这个项目里最影响手感的部分。我测试过好几套方案最终固定使用Vector3.SmoothDamp处理位置用Quaternion.Slerp或角度SmoothDamp处理旋转。很多人刚学的时候习惯用Vector3.Lerp(target, current, 0.1f)简单但Lerp的问题是它的速度不是恒定的。它会把当前值和目标值做一个比例混合这意味着如果每帧调用位移量越大单帧移动越大越接近目标移动越小属于指数趋近。看起来没问题但实际手感非常“橡皮筋”——镜头启动快接近目标时又慢悠悠地蹭过去射击游戏里一旦有这种粘滞感准星跟手度就差很多。SmoothDamp不同它模拟的是一个阻尼弹簧系统。参数smoothTime表示“大约需要多少秒到达目标”它的加速度、减速度分布更均匀接近目标后不会过度衰减。实测下来同样的手感曲线SmoothDamp就是比Lerp更有韧性、更可控。代码float smoothTime 0.08f; Vector3 velocity Vector3.zero; transform.position Vector3.SmoothDamp(transform.position, desiredPosition, ref velocity, smoothTime);smoothTime设置得越小镜头越“紧”跟手度高但会显得生硬设置得越大镜头越飘逸有电影感但射击时难受。推荐在0.08到0.15之间做微调。旋转的平滑我也用SmoothDamp处理角度值而不是直接对Quaternion做Slerp。因为直接Slerp四元数在跨越正负180度边界时容易产生绕远路旋转的问题。而把yaw和pitch两个float值分别做SmoothDamp再把结果组装成Quaternion.Euler就完全避开了万向锁和边界翻转的坑。3. 完整实现从零搭好一个第三人称相机3.1 第一步基础跟随和鼠标旋转先搭建最核心的跟随旋转部分。创建一个C#脚本命名为TpsCameraController然后挂到相机GameObject上。角色对象的引用从Inspector拖入。using UnityEngine; public class TpsCameraController : MonoBehaviour { [Header(目标引用)] public Transform target; [Header(视角参数)] public Vector3 offset new Vector3(0f, 1.6f, -3f); public float mouseSensitivity 2f; public float minPitch -30f; public float maxPitch 60f; [Header(平滑参数)] public float followSmoothTime 0.1f; public float rotateSmoothTime 0.05f; private float yaw; private float pitch; private Vector3 followVelocity Vector3.zero; private float yawVelocity; private float pitchVelocity; private void Start() { Cursor.lockState CursorLockMode.Locked; Cursor.visible false; } private void LateUpdate() { if (target null) return; float mouseX Input.GetAxis(Mouse X) * mouseSensitivity; float mouseY Input.GetAxis(Mouse Y) * mouseSensitivity; yaw mouseX; pitch - mouseY; pitch Mathf.Clamp(pitch, minPitch, maxPitch); // 目标位置初始偏移量经过旋转后叠加到角色身上 Quaternion rotation Quaternion.Euler(pitch, yaw, 0); Vector3 desiredPosition target.position rotation * offset; // 位置平滑 transform.position Vector3.SmoothDamp(transform.position, desiredPosition, ref followVelocity, followSmoothTime); // 旋转平滑这里先直接赋值后续优化 transform.rotation Quaternion.Euler(pitch, yaw, 0); // 暴露角度给其他系统 FinalYaw yaw; FinalPitch pitch; } public float FinalYaw { get; private set; } public float FinalPitch { get; private set; } }需要解释几个关键点。首先是为什么用LateUpdate而不是Update。Unity的脚本执行顺序中Update里角色移动代码先跑LateUpdate里相机跟随时角色的最新位置已经确定了。如果在Update里更新相机很可能在角色移动前的旧位置触发相机跟随就会出现肉眼可察觉的镜头滞后或抖动。这是很多初学者第一个性能/手感坑。其次是为什么pitch用减法pitch - mouseY。默认Mouse Y向上是正值但相机朝下的方向是负的角度。如果直接加鼠标向上推时镜头会向下完全反直觉。减法和Clamp配在一起就得到“鼠标向上看天、向下看地”的正常操作。当前这个版本还没做旋转平滑直接赋值的旋转会显得非常生硬。下面把旋转也平滑了。3.2 第二步优化平滑旋转上面代码里transform.rotation被直接赋值为Quaternion.Euler(pitch, yaw, 0)这会导致鼠标一动镜头立刻甩过去没有滞后感。为了让旋转跟手又不生硬拖一个小小的平滑private void LateUpdate() { // ... 获取鼠标输入并累加yaw/pitch不做旋转赋值 float targetYaw yaw; float targetPitch pitch; float smoothYaw Mathf.SmoothDamp(currentRigYaw, targetYaw, ref yawVelocity, rotateSmoothTime); float smoothPitch Mathf.SmoothDamp(currentRigPitch, targetPitch, ref pitchVelocity, rotateSmoothTime); Quaternion rotation Quaternion.Euler(smoothPitch, smoothYaw, 0); Vector3 desiredPosition target.position rotation * offset; transform.position Vector3.SmoothDamp(transform.position, desiredPosition, ref followVelocity, followSmoothTime); transform.rotation rotation; currentRigYaw smoothYaw; currentRigPitch smoothPitch; }这里引入了一组currentRigYaw和currentRigPitch作为实际使用的角度而输入累加用的yaw和pitch相当于目标值。这样鼠标在快速甩动的时候镜头的旋转有一个微小的滞后不会像“拨动开关”一样跳变。注意rotateSmoothTime要设置得比followSmoothTime小通常保持一个比例关系比如follow0.12rotate0.04。因为旋转如果平滑太重玩家转视角时画面会“漂”晕3D玩家会骂人。3.3 第三步碰撞检测与遮挡处理这是让相机不穿墙的关键环节。思路其实很简单从角色位置向相机“理论位置”发一条射线或者从相机位置向角色发如果射线撞到了障碍物就把相机位置沿着射线方向拉近直到射线不被阻挡。实现上有几个细节。其一射线要从角色头顶附近出发而不是从角色脚底因为角色脚底经常在地面附近很容易被地面误触发从胸口或头顶高度出发才合理。其二要过滤Layer角色自身的碰撞体要忽略否则相机把角色自己也当成遮挡物了。其三要预留一个“安全距离”不要让相机极限贴近墙表面否则画面边缘会被墙面裁切看着非常难受。private Vector3 ApplyCameraCollision(Vector3 desiredPosition) { Vector3 targetPos target.position; Vector3 targetHeadPos targetPos Vector3.up * 1.2f; Vector3 direction desiredPosition - targetHeadPos; float distance direction.magnitude; direction.Normalize(); float collisionRadius 0.15f; float minDistance 0.4f; if (Physics.SphereCast(targetHeadPos, collisionRadius, direction, out RaycastHit hit, distance, collisionMask)) { // 如果撞到了障碍物把相机拉到障碍物前并留一点缓冲距离 desiredPosition targetHeadPos direction * (hit.distance - collisionRadius - 0.1f); if (Vector3.Distance(targetHeadPos, desiredPosition) minDistance) { desiredPosition targetHeadPos direction * minDistance; } } return desiredPosition; }为什么用SphereCast而不是普通的Raycast因为相机本身有一个视野范围不是无限细的一个点。Raycast判定的是“从角色看向相机的那条线上有没有物体”但相机有一定的体积贴墙时虽然中心点没有穿模相机边缘已经嵌进墙体了。SphereCast用一个半径大约0.15米的球体去探测相当于给相机一个体积概念碰撞检测的覆盖范围更接近真实情况。另外有个小细节容易被忽略SphereCast返回的hit.distance是球形表面到起点的距离而不是球心到起点的距离。所以做位置计算时要把radius减掉再额外留一个0.1的缓冲距离这一步很多人会漏。漏掉之后镜头虽然不进墙但会贴墙转动视角时还会有“蹭墙”的视觉摩擦感。碰撞检测做完之后把结果和SmoothDamp联动。顺序要注意先根据鼠标输入算出理想位置desiredPosition再做碰撞修正得到修正后的位置最后才对修正后的位置做SmoothDamp。如果先平滑再修正镜头在高速旋转时可能出现来回跳动——平滑后的位置离墙太近修正又拉回来修正结果又被平滑来回往复就变成了高频抖动。3.4 第四步整合完整代码把以上三个环节全部合到同一个LateUpdate流程里形成最终的完整实现。为了方便实际项目使用我还加了一个可选的“肩部视角偏置”开关以及脚本内注释补充说明参数含义。using UnityEngine; public class TpsCameraController : MonoBehaviour { [Header(目标)] public Transform target; [Header(视角参数)] public Vector3 offset new Vector3(0f, 1.6f, -3f); public float mouseSensitivity 2f; public float minPitch -30f; public float maxPitch 60f; public bool enableShoulderOffset false; [Header(平滑参数)] public float followSmoothTime 0.12f; public float rotateSmoothTime 0.04f; [Header(遮挡处理)] public LayerMask collisionMask ~0; public float collisionRadius 0.15f; public float collisionMinDistance 0.4f; private float yaw; private float pitch; private float currentYaw; private float currentPitch; private Vector3 followVelocity Vector3.zero; private float yawVelocity; private float pitchVelocity; public float FinalYaw currentYaw; public float FinalPitch currentPitch; private void Start() { Cursor.lockState CursorLockMode.Locked; Cursor.visible false; currentYaw target.eulerAngles.y; currentPitch 0f; yaw currentYaw; pitch currentPitch; } private void LateUpdate() { if (target null) return; // 1. 鼠标输入 float mouseX Input.GetAxis(Mouse X) * mouseSensitivity; float mouseY Input.GetAxis(Mouse Y) * mouseSensitivity; yaw mouseX; pitch - mouseY; pitch Mathf.Clamp(pitch, minPitch, maxPitch); // 2. 角度平滑 currentYaw Mathf.SmoothDamp(currentYaw, yaw, ref yawVelocity, rotateSmoothTime); currentPitch Mathf.SmoothDamp(currentPitch, pitch, ref pitchVelocity, rotateSmoothTime); Quaternion rotation Quaternion.Euler(currentPitch, currentYaw, 0); // 3. 目标位置支持肩部偏置 Vector3 baseOffset offset; if (enableShoulderOffset) { baseOffset new Vector3(Mathf.Abs(offset.x), offset.y, offset.z); } Vector3 desiredPosition target.position rotation * baseOffset; // 4. 碰撞修正 desiredPosition ApplyCameraCollision(desiredPosition); // 5. 位置平滑 transform.position Vector3.SmoothDamp(transform.position, desiredPosition, ref followVelocity, followSmoothTime); transform.rotation rotation; } private Vector3 ApplyCameraCollision(Vector3 desiredPosition) { Vector3 headPos target.position Vector3.up * 1.2f; Vector3 direction desiredPosition - headPos; float distance direction.magnitude; direction.Normalize(); if (Physics.SphereCast(headPos, collisionRadius, direction, out RaycastHit hit, distance, collisionMask)) { desiredPosition headPos direction * Mathf.Max(hit.distance - collisionRadius - 0.1f, collisionMinDistance); } return desiredPosition; } }这就是一套完整的TPS相机核心逻辑。放在一个空场景里把角色对象拖进target运行之后应该就能获得基本的第三人称操控体验。不过这只是第一步真正的难点在下一步调参。参数调不好代码写得再完整手感也是一坨屎。4. 调参经验手感和避坑才是真正的分水岭4.1 关键参数一览与推荐取值范围不同游戏类型对相机的参数要求差异巨大但有一个比较通用的起步范围。我用表格整理了一下方便大家参照。参数推荐范围适用场景说明followSmoothTime0.08 ~ 0.2射击游戏取小值动作游戏取大值越小镜头越紧越大越“拖尾”rotateSmoothTime0.02 ~ 0.08竞技类取最小值剧情类取大值转视角生硬多半是这里太大mouseSensitivity1.5 ~ 3.5取决于个人习惯及DPI建议提供游戏内设置不要硬编码minPitch / maxPitch-30 / 60 或 -40 / 70让玩家能看到脚下和天空范围不要设到±90会翻到角色头顶collisionRadius0.1 ~ 0.2相机体积模拟太小穿模太大镜头离墙远offset高度1.4 ~ 1.8角色身高的一半左右角色太矮或太高时需重新调offset距离2.5 ~ 4越远视野越大角色越小瞄准类建议近一些3左右合适我见过很多项目在float调参上花的时间不少于写核心代码的时间这是正常的。一个TPS游戏调试下来光鼠标灵敏度、镜头回中力度、死角修正这几个参数就能磨两三天。4.2 实战中调整手感的几个经验顺序如果你觉得手感不对不要漫无目的地乱调参数。按照我下面这个顺序调整能少走很多弯路。先调旋转平滑。把rotateSmoothTime设到0看看镜头的“直接跟手”是什么感觉然后一点点加大直到镜头不那么“弹”但又不会“飘”这个值就是你的旋转基准。一般从这个基准开始再调跟随平滑。旋转和跟随这两个平滑值要配合起来如果旋转已经很大了比如0.06跟随却很小比如0.05镜头就会出现“位置已经到了、方向还没转过来”的割裂感玩家会觉得镜头在“点头”。然后调视角范围。把pitch的上下限单独开着测试自由跑动、趴下起立、爬坡下坡看看哪些动作会导致镜头窜到地下或飞过头顶。注意不要让最低角度低于-40否则贴地找物品时镜头会穿到地面以下非常出戏。最后调碰撞参数。多找几个墙角、门框、贴图复杂的掩体来测试。一边移动一边转视角确认SphereCast的radius和缓冲距离是否足够不能让相机在贴近墙面的瞬间产生一跳一跳的“呼吸感”。4.3 关于鼠标灵敏度与玩家自定义灵敏度这个参数强烈建议做成Runtime可调的。不要把它写死在脚本字段里然后让程序改代码至少在游戏设置界面里应该暴露一个slider。因为玩家显示器分辨率、鼠标DPI、鼠标加速度各不相同同一套灵敏度在不同设备上的感受完全不同。我常用的做法是把mouseSensitivity拆成“水平灵敏度”和“垂直灵敏度”两个独立参数。因为大多数人竖直方向的旋转幅度小于水平方向两者分开后玩家可以只把水平调快、垂直调慢手感更容易贴合个人习惯。另外还要考虑帧率影响鼠标输入用的是Input.GetAxis它已经做了帧率归一化但在极端帧率下比如30帧以下依然会有掉帧带来的转向不流畅这个不是代码逻辑问题是帧率问题不要为了迁就低帧率去把rotateSmoothTime调大否则高帧率玩家又会觉得太拖。5. 常见问题与排查技巧实录5.1 相机抖动最常见也最烦人抖动问题我在开发过程中遇到过至少三种来源每种现象还不一样。第一种是**“高频微抖”**角色静止时相机也在以极小的幅度颤抖。这种大多数是因为在LateUpdate里直接用了角色的位置而角色本身的位置在Update里做了某种插值或者动画驱动导致角色的位置每一帧有微小波动。解决办法是在角色身上增加一个“视觉锚点”一个空的子物体放在脖子位置相机跟随这个锚点而不是角色根节点可以有效过滤掉根节点的数值噪声。第二种是**“旋转时跳变”**鼠标匀速移动时镜头不是匀速转而是顿一下动一下。原因通常是yaw角度累加到了超过360或低于-360的范围导致Quaternion.Euler在角度值巨大时精度下降。我一般在累加之后做一步Wrap处理yaw (yaw 360) % 360但注意这样会导致跨边界时角度突变所以更稳的方案是不要Wrap直接用float累加让float本身不溢出就好。第三种是**“SmoothDamp的ref参数初始化问题”**。SmoothDamp要求ref velocity在第一次调用前必须初始化为Vector3.zero如果脚本里忘了初始化或者不小心在OnEnable里清了零行为会变得非常怪异表现为启动瞬间镜头猛冲一段再回位。给followVelocity和yawVelocity/pitchVelocity都加上显式初始化这个坑就解决了。5.2 穿模和遮挡失效穿模一般出现在两大场景近距离贴墙、急转弯。近距离贴墙时SphereCast的结果可能是“打到了某个很靠近角色的物体”比如角色自身的手臂或武器导致相机被压得特别近。解决方式是把武器、手臂等部件放到一个单独的Layer上然后在collisionMask里排除掉这个Layer相机就不会和角色自身配件碰撞了。急转弯穿模则是因为方向变化太快SphereCast从新的desiredPosition往角色打射线时一瞬间方向从正后方变成了侧后方这个瞬时的转向让射线扫过墙体侧面。我的处理办法是在碰撞修正后再对修正后的位置做一次“微调”——如果修正后的方向和上一帧的方向夹角大于某个阈值比如30度就限制方向变化的速率。这个逻辑写出来会多不少代码但效果非常明显尤其是高速跑动贴拐角的时候相机不会再“穿墙后弹出来”。5.3 旋转方向与操作反了如果发现鼠标左移镜头右转、鼠标上推镜头下看多半是符号问题。yaw累加的正负、pitch累加的正负都需要检查和你的输入习惯对照。我自己的习惯是Mouse X为正 yaw增加 镜头向右转这个方向用的是正的mouseXMouse Y为正 pitch减少 镜头向下看因为相机朝下是负pitch所以用减法。如果你想要“mac式”的反方向控制鼠标右移镜头左转把yaw的累加改成减法就行。还有一个很隐蔽的问题是角色初始朝向导致的。如果角色模型在场景里是向X轴正方向摆放的初始yaw应该设为0如果是向Y轴或Z轴的方向摆放初始yaw就要对应设置。比如角色在场景里面朝Y轴正方向那初始yaw应该是90否则一进游戏相机先歪一个角度。我通常用target.eulerAngles.y做初始化但前提是目标物体自身没有嵌套旋转如果有嵌套父物体要先用Quaternion.Euler(0, target.eulerAngles.y, 0)提取出世界Y轴旋转。5.4 游戏分辨率与宽屏适配TPS相机在高宽比不同的屏幕上视野边缘的可见范围会变化。同样一个offset在16:9和21:9下面角色在画面里的位置比例会不一样。Unity的Camera会自动适配这个但有个小坑如果做瞄准状态时把FOV调小比如从60降到40画面里的角色大小会变化相机的偏移量不会自动调整导致瞄准状态下角色略微偏离屏幕中心的视觉效果。我做了个简单的补偿方案在进入瞄准状态时同时把offset的距离缩短一点让角色稍微靠近屏幕中心。这个参数可以在运行时由瞄准系统调用相机的一个Public方法去修改比如SetAiming()方法里做插值。注意瞄准时缩短的是offset.z的绝对值不是offset的y否则镜头会下压表现变成“低头看角色”。6. 进一步扩展这套相机还能怎么复用TPS相机写完不要只停留在射击游戏里。我后来把同一套相机系统扩展到了多个项目包括一个动作冒险Demo和一个飞行竞速Demo改动量都不大说明这套架构的扩展性还可以。动作冒险场景改成角色不分离朝向也就是让角色模型始终朝向相机forward方向投影到地面的方向。这时“角色朝向与相机分离”的架构变成了“角色朝向与相机绑定”只需要在其他脚本里取相机的yaw角用来旋转角色的模型朝向即可。移动时朝向由输入方向决定站立时朝向保持相机方向这类游戏的手感就兼顾了。飞行竞速场景相机的pitch不被限制可以全角度旋转。只需要把minPitch和maxPitch放开到-180到180然后把SmoothDamp的rotateSmoothTime调大到0.2左右镜头就会有带有延迟感的“追拍”效果非常适合展示速度感。ARPG锁定视角很多ARPG在锁定Boss时相机不是随时跟着玩家鼠标转而是自动围绕目标旋转。扩展思路是让yaw跟随“玩家位置-目标位置”的方向角每帧把yaw修正为这个角度玩家鼠标再叠加一个偏移。这个在相机核心代码上不需要改动外部驱动yaw值即可。核心代码就是那一套外层逻辑其实都是在往yaw和pitch这两个float里塞东西。想明白这一点就不怕各种游戏类型对相机系统的花式需求了。7. 最后分享两个我自己很受用的心得第一点是多写可视化调试工具。相机的问题很难光看代码发现必须在实际场景里运行观察。我习惯在OnDrawGizmos里把目标位置、修正前位置、修正后位置、射线碰撞点画出来。Gizmos颜色用不同的修正前位置画绿点修正后位置画红点射线路程画黄线。这样一眼就能看出遮挡修正是否合理、平滑是否在正确区间。尤其是穿模问题没有可视化调试靠肉眼在Game视图里找问题效率极低。第二点是参数尽量用Inspector暴露不要写在常量里。我之前有个项目把平滑参数直接硬编码在代码里每次调整都要改代码、保存、等编译、回场景看效果来回折腾十几分钟。后来统一改成Inspector序列化字段调参数变成了“改一下数字点运行看效果”效率至少提升一倍。如果你的项目有多个策划同事一起参与把合适的参数预设存成Prefab还能让策划自己微调手感不用每次跑来找程序改。TPS相机看起来是“几十行代码的小功能”但它其实是整个游戏操控感的基石。镜头稳、跟手、不穿模玩家才会下意识觉得“这游戏很跟手”镜头飘、穿模、反转再好的射击手感也会被毁掉。希望这篇拆解能帮你在做相机时少踩几个坑把更多时间花在真正让游戏好玩的事情上。