UE4里的组件和碰撞,这两个词在项目里几乎天天能碰到,但说实话,很多人对它们的理解一直停留在“知道怎么用”却说不清“为什么要这样用”的阶段。我最近在做一个动作玩法Demo,为了让一个可拾取物品表现出“靠近提示—按键拾取—拾取后碰撞消失—下次刷新再出现”这套完整流程,前前后后改了四版方案。每次出问题,定位到最后几乎都绕回组件组织和碰撞通道的设置上。
组件没有规划,碰撞口径不统一,玩法逻辑就会变成一团乱麻:明明功能都写了,事件偶尔触发偶尔不触发,性能也在悄悄下降。这篇东西与其说是教程,不如说是我自己把这些年的实战心得——从组件该拆到什么粒度,到碰撞通道表的建立,再到事件机制的选型——完整梳理一遍,希望能给正在用UE4做交互玩法或者被碰撞问题折磨的朋友一些可落地的参考。
1. 为什么说“组件思维”是UE4里最容易被低估的架构功底
很多教程讲到UE4的组件,只会说“组件就是一个可以挂到Actor上的对象,用来给Actor添加功能”。这话没错,但它完全没讲清楚组件对整个项目长期演进的影响。真正把UE4用顺的人,看一个Actor文件,一眼就能判断出作者有没有组件思维:是写了一个几千行的上帝类,还是把功能拆成可以独立维护的积木块。
1.1 Actor是壳,组件是五脏六腑
我用一个最直观的例子来说明Actor和组件的关系:把Actor想象成一个外卖盒子,盒子本身不能吃,它存在的意义是打包和运输。组件才是里面的菜:SceneComponent负责摆盘位置,StaticMeshComponent让盒子有外观,BoxComponent负责“你碰到我了”的信号,LogicComponent负责复杂的业务规则。
在UE4的底层实现上,Actor本质上是一个容器,真正参与场景变换、渲染、物理计算的往往是它身上的各类Component。USceneComponent提供了Transform,也就是位置、旋转、缩放;UPrimitiveComponent在此基础上增加可视化和物理能力;UActorComponent则是脱离场景坐标的纯逻辑组件,比如技能冷却计时、背包数据管理这类不需要空间位置的功能。
新手最常见的坑,就是把所有逻辑堆在Actor蓝图里,然后到处拖引用去找其他Actor。这种写法在演示Demo里跑得通,但项目一旦超过五个交互物品,蓝图连线就开始像蜘蛛网,每一步改动都要担心破坏其他功能。改成组件化之后,每个组件只负责一件具体的事,Actor的角色就变成了“组装和调度”。
1.2 我总结的组件搭建顺序
拿到一个新功能的Actor,我习惯按“先有根、再外观、后碰撞、加逻辑”的顺序挂组件:
- 根组件:决定这个Actor在场景里怎么被移动。绝大多数带空间属性的Actor选SceneComponent作为Root就够了,需要物理表现的角色或投掷物才选用CapsuleComponent或SphereComponent当根。
- 外观组件:StaticMeshComponent或SkeletalMeshComponent,纯粹负责渲染。这里注意,渲染网格体不应该承担复杂的碰撞逻辑,后者的精度和性能要求完全不同。
- 碰撞组件:这是独立于外观的一层,BoxComponent、SphereComponent、CapsuleComponent或者自定义的碰撞体,按功能需求配置响应通道。
- 逻辑组件:蓝图里用自定义蓝图组件,C++里用UActorComponent的子类,把“行为”放进来。比如“可被拾取”、“可被推开”、“进入区域后播放动画”分别做成独立组件。
举个例子,我在某独立原型中做了一个炸药桶,它的组件结构是:
| 组件 | 作用 | 关键设置 |
|---|---|---|
| Root(SceneComponent) | 整体根节点,方便整体位移 | 默认 |
| StaticMeshComponent | 渲染桶外观 | 关闭碰撞 |
| SphereComponent | 爆炸范围判定 | Overlap响应,通道设为WorldDynamic |
| BoxComponent | 可被近战攻击命中的判定区 | Block响应,通道设为Pawn |
| 自定义BP组件“可摧毁” | 血量和爆炸逻辑 | 勾选Replicates |
这个结构的好处是,当我想换成木箱、车辆、路障的时候,只需要替换渲染网格和调整碰撞体形状,逻辑组件完全不用动。
1.3 哪些东西该拆成组件,哪些不该
组件不是拆得越细越好,拆过头同样会变成灾难。我的判断标准是三条:
- 这份逻辑是否被多个Actor复用?如果只有关卡里一个特殊机关要用,做组件反而增加跳转成本,直接写在那个Actor里更直观。
- 这份逻辑是否希望被独立开关?组件可以通过SetComponentTickEnabled、SetVisibility等方法单独控制,适合需要按状态启停的系统。
- 这份逻辑是否依赖与其他Actor的引用?如果组件内部全是ExternalReference,那它并不比把代码写在Actor里更解耦。
我见过有人为了讨好“组件化”三个字,把一个简单的开门机关拆成四个组件,结果组件之间互相调用,依赖关系比原来还乱。组件化的本质是降低耦合,而不是追求数量。合理的状态是:根组件负责挂载,外观组件负责展示,碰撞组件负责规则,业务组件负责大脑,各司其职。
2. 碰撞面板里那堆选项,到底在表达什么
如果说组件决定了Actor的“身体结构”,那么碰撞设置就决定了Actor与外部世界“打交道的方式”。UE4的碰撞系统之所以让人头晕,是因为它把“身份”和“规则”混在了一张面板里。搞清楚这个概念,之后所有的事件问题都会迎刃而解。
2.1 先理解碰撞的三层结构
UE4的碰撞设置包含三层信息,把它们拆开看:
- Collision Preset(碰撞预设):一组提前配好的碰撞规则集合,官方叫Preset,你可以理解为“整套外交政策”。选择预设等于一键套用了多组规则,比如Pawn预设默认对WorldStatic是Block,对WorldDynamic是Overlap。
- Object Type(对象类型):Actor的数字身份标识,相当于“你是哪国人”。它是单选的,一个碰撞体只能有一个Object Type。
- Collision Responses(碰撞响应):这个身份对其他对象类型采取的态度——Ignore(忽略)、Overlap(重叠)、Block(阻挡),相当于“你如何对待另一国”。
我看到很多新手直接改预设里的Response表,却忽略Object Type,导致运行结果完全不是预期。举个真实案例:有个朋友调试弹幕碰撞,弹幕的Object Type设成了WorldDynamic,而墙壁也是WorldDynamic,两者碰撞Response是Overlap,于是弹幕直接穿过墙壁,而不是被挡下来。如果他当时把弹幕设为Projectile对象类型、墙壁设为WorldStatic,然后指定Projectile对WorldStatic是Block,一下就解决了。
2.2 物理、查询、遮挡:三类碰撞不是一回事
UE4中的碰撞并不只有“撞”这一种含义。它在底层把PrimitiveComponent的碰撞能力分成三类,面板里已经默认勾选:
- Physics Collision(物理碰撞):用于推动刚体、模拟物理力、阻挡物体移动。它直接参与物理引擎的动力学计算。比如球撞墙反弹,靠的就是它。
- Query Collision(查询碰撞):用于射线检测、扫描和重叠判断。它不影响主动物理运动,但能被LineTrace、OverlapEvents检测到。绝大多数玩法交互实际上用的都是这一类。
- Navigation Collision(导航碰撞):用于阻挡寻路,使AI不会穿过该物体。
我在给静态网格体配置碰撞时,如果只做阻挡和交互,一般打开Physics和Query,Navigation只在真正需要阻挡寻路时才开。因为每次多开一个碰撞类型,引擎在场景更新时就要多维护一份数据结构,全场景几十上百个Actor累积起来,帧率会悄悄下滑。
2.3 建立一张项目级碰撞通道表
新项目创建后,默认的Object Channel和Preset往往不够用。我强烈建议项目一启动就开一次会或自己规划一组通道,而不是随手用。下面是一份我常用的通道表:
| 对象类型 | 可被谁Block | 可被谁Overlap | 备注 |
|---|---|---|---|
| WorldStatic | Pawn、Projectile、Vehicle | Trigger、Camera | 场景静态摆设 |
| WorldDynamic | Pawn、Projectile | Trigger、Camera、Projectile | 门、箱子、可移动物 |
| Pawn | WorldStatic、WorldDynamic、Vehicle | Trigger、Camera、Prop | 角色胶囊 |
| Vehicle | WorldStatic、WorldDynamic | Pawn、Projectile | 车辆 |
| Projectile | WorldStatic、WorldDynamic、Pawn、Vehicle | Trigger | 弹丸和射线武器 |
| Trigger | 无 | 全部 | 纯触发器,不做物理遮挡 |
| Camera | WorldStatic、WorldDynamic | Pawn | 第三人称相机防穿墙 |
这张表的价值不在让你照抄,而在于固定一套团队语言。以后写某个碰撞体的时候,只要明确“我是Trigger”或“我是Projectile”,所有交互行为都能预判,不需要每次去查面板。
2.4 别小看“Ignore”的双向性
碰撞响应在UE4中是按方向计算的,A对B设成Block,不代表B对A也会Block。我在项目里就被这类“单向Block”坑过:机关门去推角色时,门收到的是WorldStatic对Pawn Block,但角色胶囊体对WorldStatic的Response被误设成了Ignore。结果门推角色角色完全无视,直接走进穿模态。
还有一类问题更隐蔽:A和B都设了Block,但A在移动时物理引擎到底先处理谁的碰撞,取决于Actor的移动组件和物理优先级。如果两边都开启了Simulate Physics,就可能出现互相推搡的抖动。所以,移动的角色和静态机关交互,尽量让一边是查询碰撞,一边是物理碰撞,避免两个物理刚体硬顶。
3. 碰撞组件怎么选、怎么装,才算没白挂
UE4提供的碰撞组件看上去就是几个形状,但每个形状背后对应着完全不同的算法开销和适用场景。选错碰撞体,不只是表现怪,还会带来性能和事件稳定性问题。
3.1 常用碰撞组件选型对照
我把经常用的几个碰撞组件放在一起比较:
| 组件 | 适用场景 | 精度 | 性能开销 | 常见坑 |
|---|---|---|---|---|
| BoxComponent | 开关、箱子、区域触发、按钮 | 低 | 很低 | 与斜向墙体贴合时出现缝隙 |
| SphereComponent | 拾取半径、爆炸范围、探测器 | 低 | 很低 | 放射性物体外形差感明显 |
| CapsuleComponent | 角色、柱子、长条形人体包围体 | 中 | 低 | 两头半球易与窄缝卡住 |
| Convex Collider(凸包) | 复杂网格的简易碰撞 | 中 | 中 | 只支持凸包,凹形会被拆成多块 |
| Complex Collision(网格体复杂碰撞) | 精确切割检测,如刀砍石头 | 高 | 高 | 大量三角面计算极易卡顿,不推荐常开 |
对于可拾取物,我通常用一个比模型略大的SphereComponent做拾取判定,再挂一个小的BoxComponent做交互“Press E”时的范围判定。两者各管一件事,互不干扰。
而角色碰撞体,我几乎从来不会直接用Mesh本身的碰撞。主因是渲染网格体顶点太多,用Complex Collision跑每帧的物理检测会吃掉大量CPU。角色根节点的CapsuleComponent才是真正参与碰撞的实体,Mesh只是摆在它上面的“衣服”。
3.2 角色胶囊体怎么配才算靠谱
角色胶囊体的Radius和HalfHeight要按玩法需求微调,而不是随手填成默认值。我一般会拆成两步:
- 第一步:根据游戏尺度确定胶囊直径。如果角色肩膀宽约40厘米,胶囊Radius取35到45厘米,留出视觉余量,避免角色摸着墙壁却被卡镜头。
- 第二步:让胶囊底部略微低于角色脚底模型一段距离,避免模型因精度问题和地面产生微小的穿插闪烁。
更重要是把角色的碰撞响应设置成:对WorldStatic是Block,对WorldDynamic是Overlap,对Trigger是Overlap。这样靠近机关门时能推动门,而在拾取物范围内却不会被拾取物本身挡住路。如果在项目里碰到“角色走路像被空气墙卡住”的现象,先检查角色胶囊体对WorldDynamic的响应是不是设成了Block——八成是它。
3.3 武器命中检测:究竟是撞击体还是扫描线
新手做近战武器时,习惯给武器模型挂一个BoxComponent,开启动态碰撞,然后监听碰撞事件。这个做法很快就会发现两个问题:
- 角色挥刀时如果身体和敌人已经贴在一起,Box在挥到一半时才开始检测,会漏掉起手那几帧的命中。
- Box本身是个静态体,即使开了Simulate Physics,驱动它移动的频率和速度也与挥砍动画不一致,命中表现非常飘。
更稳的做法是:挥砍过程中连续做几次SphereTrace或SweepSingleByChannel,每次30毫秒到50毫秒一次,从角色手部起始点朝武器末端方向扫。这里要注意,扫描检测需要的是正确起始点和结束点,而不是单纯扫“命中结果”。我一般会记录上一次扫描位置,把当前扫描位置到上一次扫描位置之间的空间做成胶囊样条,这样可以避免高速运动导致子弹跳过墙的“隧穿效应”。
3.4 碰撞体和渲染网格体可以不是一回事
这是很多视觉向项目绕不开的课题。我自己在做一个地台机关时,美术给的石头模型表面到处是凸起,如果用简化的Convex碰撞,触发区域就会覆盖到模型前面一大截;如果精确Convex太多块,又拖慢性能。最后我直接用了一个比模型稍微小一圈的BoxComponent作为碰撞体,机关按按钮的范围全部走自定义碰撞体,表现完全在预期内。
想清楚一个逻辑:碰撞体的目的不是复刻视觉形状,而是定义“逻辑边界”。一颗看起来圆滚滚的果实用一个带倒角的Box碰撞,玩家不会察觉;但如果你的目标是砍瓜果表皮、切出不同颜色,那才需要细分碰撞。不分场景地追求碰撞精度,只会浪费性能。
4. 重叠事件、命中事件与主动检测:三套机制别糊涂
UE4的碰撞交互看起来事件很多,但拆开就三类:Overlap(重叠)、Hit(命中)、主动检测(Trace/Overlap)。很多事件的“不触发”问题,根源在于选错了机制。
4.1 重叠事件:只适合“位置关系判断”
Overlap事件适合的场景是:“两个物体有没有碰在一起”这件事本身,而不需要关心撞击方向和力度。
典型例子:
- 拾取物的半径探测,角色走进Sphere范围就弹出提示UI。
- 区域触发器,进入一片区域播放BGM、改变环境雾效。
- 陷阱区域检测,玩家走进去触发提示对话。
用Overlap时最需要留意的,是“Overlap事件变化需要至少一方移动”。如果两个Actor都静止地开始重叠,引擎不会触发BeginOverlap,因为事件是在运动过程中边计算边广播的。我第一次做自动贩卖机时就犯了这错:角色出生点就在贩卖机的探测范围内,玩家一转身看不见提示,因为出生时引擎没有重新计算重叠,只报告了“重叠已发生”。
解决这个问题的通用做法是,在角色出生后主动用UNMoverComponent的Velocity或手动调用Component的Overlap测试一次,强制生成一次重叠事件。
4.2 碰撞/命中事件:要有物理味但不一定要真物理
Hit事件的触发条件是“移动中的碰撞体与另一个碰撞体相遇,并且会真实阻挡”。Hit事件带出来的是撞击结果,比如撞击点、法线、冲量等,这种数据是做打击反馈和碎片的黄金信息。
但是Hit事件并不适合所有“接触型交互”。我见过一个项目做“棒球打击”,给球加了一个按速度移动的物理组件,然后监听Bat的Box的Hit事件。结果发现球经常被“打”走,但是Hit事件却有时触发有时不触发。原因在于球的移动使用SetActorLocation逐帧移动,而不是物理Simulate,物理引擎认为它没有“物理移动”,于是碰撞事件经常被跳过。
正确的做法是:需要使用物理事件的对象要么用Simulate Physics,要么用AddForce/AddImpulse驱动,要么就主动做Sweep移动(MovementComponent的Sweep参数)。如果只是视觉上要“球被撞飞”,用ActiveDetect+Sweep,比依赖被动事件稳定得多。
4.3 主动检测:复杂交互的王牌
主动检测包括LineTraceByChannel、SweepSingleByChannel、OverlapMultiByObjectType等,它们完全可以替代部分事件,而且在控制精度上更胜一筹。我的习惯是:
- 攻击命中:用手动扫描而不是碰撞体事件,因为扫描可以设定忽略自己和队友。
- 远程武器:每帧发射一次LineTrace,把上次到这次的射线连起来,形成“连续命中”。
- 拾取探测:用OverlapMulti而不是依赖物理重叠包,可以在同一帧收集所有可交互对象,按最近距离排序。
主动检测有一个额外好处:响应结果独立于物理时钟,事件逻辑更容易在多线程环境中保持一致性。代价是每帧主动查询会引入额外开销,不过对现代引擎来说,几十次轻量射线完全不是问题。
4.4 事件没触发?五层排查法
我在定位“为什么不触发”时,会严格按照下面的清单逐层排查:
- 组件有没有开碰撞:CapsuleComponent若CollisionEnabled设为NoCollision,事件肯定全灭。
- 对象类型和通道响应对不对:看对方对己方是不是Ignore。比如Trigger对Pawn是Ignore的话,怎么都重叠不了。
- 移动方式对不对:用SetActorLocation移动且Sweep参数为false,碰撞检测不会主动更新。
- 是否勾选了Generate Overlap Events:PrimitiveComponent上专门有个开关,默认是关闭的。忘开它,BeginOverlap永远不来。
- 事件绑定是不是绑在了错误的对象:蓝图里把事件绑定到静态网格的StaticMeshComponent却让角色用Capsule去碰,行为自然不沾边。
这五层一旦走完,九成以上的事件问题都能定位到根因。
4.5 重复触发与事件抖动的处理
即使事件正常触发,你还会遇到另一个问题:一个逻辑被连续进入、退出、再进入,导致状态被反复重置。比如宝箱开启事件要求“角色进入半径→按E→播放动画→关闭Trigger”,如果关闭Trigger后角色仍然在半径里,就有可能在退出时再触发一次退出事件,把宝箱重新打开。
我的处理方式是引入“锁存状态”:在第一次完成交互后,把当前Trigger的CollisionEnabled设为NoCollision或把GenerateOverlapEvents关闭,再配合一个自定义的冷却时间。等要重新启用时,先用SetCollisionEnabled恢复,再调用一次更新重叠状态的接口,让事件队列里不再残留旧的Overlap记录。
5. 实战里那些容易翻车的碰撞细节
碰撞系统在基础功能跑通以后,真正的挑战大多出在表现层和玩法层。下面这几个细节我在不同项目里都踩过,写出来希望帮你提前绕开。
5.1 命中部位判定:别用网格逻辑去渲染网格
ARPG里的“爆头”、“断腿”需求,很多人第一反应是给骨骼挂上碰撞体。思路没错,但实现时容易犯一个错:用整个Mesh的ComplexCollision来检测命中,然后拿Hit点的物理材质或骨骼名去判断部位。
一次误判的代价是:玩家明明打在胸口,系统判定成头部,伤害翻倍。这个问题在网格碰撞与骨骼绑定不稳定的情况下特别常见。
我更推荐的做法是,在角色蓝图里把头部、躯干、四肢分别挂上无菌的SphereComponent或BoxComponent,并把这些碰撞体的ObjectType设为BodyPart,在武器射线回调里读取命中对象的名字或对象通道,然后映射到伤害倍率。这样既不用分析三角形网格,也不用依赖骨骼物理体,性能好还稳定。
| 部位 | 碰撞体形状 | 伤害倍率 |
|---|---|---|
| Head | SphereComponent 半径22 | 2.0 |
| Torso | BoxComponent 54x38x42 | 1.0 |
| Arms | CapsuleComponent 半径10,HalfHeight 26 | 0.8 |
| Legs | CapsuleComponent 半径12,HalfHeight 36 | 0.9 |
5.2 地面材质判定:物理材质不只是给物理用的
很多项目想让角色踩在草地、泥地、金属板上有不同的脚步声和粒子效果。UE4为每个PrimitiveComponent可以指定PhysMaterial,一个常见的坑是很多人只在PhysicsMaterial的物理属性里填摩擦和弹性,忘了它可以作为游戏性数据来读取。
具体做法是:角色胶囊体底下一定高度处做Sweep或者LineTrace,得到Hit事件的PhysMaterial,然后根据PhysicsMaterial的名称或自定义的Tag映射成对应音效和脚印材质。这里有个细节:LineTrace必须查询到包含物理材质的表面,也就是这条Trace要打到PrimitiveComponent,而不是只射了一个空Trigger。我为了这个还专门给地面加了一层很薄的Physics材质覆盖层,避免Trace打到下面的无关碰撞体。
5.3 物理约束和碰撞通道打架的场面
物理约束常见于门、橱柜、抽屉这类铰接关节。很多人在关节调试中会把门的碰撞响应设成对Pawn是Block,结果玩家推门时发现门不顺滑、有阻力,甚至把角色用手顶开。原因在于门的PhysicsConstraint和碰撞响应同时在推同一个物体,互相较劲。
解决思路是:铰接物体对角色只做查询碰撞(QueryOnly),不做物理阻挡(PhysicsBlock)。需要阻止角色穿门时,用门口的窄Trigger或者门口立柱的碰撞体挡住,而不是让门自身跟物理约束抢控制权。
5.4 动态物体的碰撞体缩放雷区
在运行时对碰撞组件执行SetWorldScale3D或者对包含碰撞的Actor执行SetActorScale3D要格外小心。碰撞体与渲染Mesh的缩放比例在非均匀缩放时会导致物理形状失真,尤其是Capsule和Convex。而且当Actor自身带有Hierarchical Instanced Static Mesh组件时,缩放碰撞体还会导致Instancing数据重建,造成明显的卡顿。
如果必须让一个物体从“大”变“小”,更稳妥的方法是直接替换碰撞体形状,或者调整碰撞组件参数而不是缩放父节点。我的电子秤项目里,把一个BoxComponent的BoxExtent从(80,80,10)动态改到(80,40,10)时反而零开销,因为只改了碰撞体几何参数,没有触发组件的整体缩放重算。
6. 性能、同步和调试:碰撞问题到最后都是这三件事
功能写完,剩下的就是让它跑得稳、跑得快、远程对战时不出错。碰撞系统如果只盯着功能不碰性能,项目后期必然要付出代价。
6.1 碰撞查询的开销到底怎么评估
很多人误以为LineTrace都是“一次射线,很便宜”。实际上不设限制的Trace可能遍历整个空间的碰撞体并执行复杂碰撞测试,如果查询频率高,性能会立刻拉裤。我常用的降本方式有三个:
- 优先使用多通道查询的专用函数,比如SweepSingleByChannel,并指定只测试特定ObjectType,避免扫描所有物体。
- 在Trace结果中,尽量使用Simple Collision而不是Complex Collision,尤其当扫描目标只是Trigger和Box时,没必要跑三角形级别检测。
- 善用CollisionQueryParams的TraceComplex参数,保持false;同时把需要被查询但不需要被命中检测的东西统一放在Ignore列表中。
上个项目中,我一次攻击要做三次扫描,最开始时每次扫描都用默认的Complex碰撞,结果帧率从120降到70。改成Simple Collision并把队友、自己加入IgnoreList后,帧率回到110以上。性能问题的缘起来自无差别的扫描逻辑,不是引擎本身慢。
6.2 静态网格体碰撞复杂度:能用凸包就不用三角形
StaticMesh的Collision Complexity面板里有“Use Complex Collision as Simple”这个选项,很多人在Mesh里直接打开它,结果每个物体几千个三角面全参与了物理和查询。一个场景摆几十个这样的物体,CPU直接就被压垮。
正确套路是:手动物品用凸包简化碰撞;墙体等大体积物体用多个Box组合(甚至可以把一组Box合成一个Convex)模拟简单碰撞;真正需要高精度的“切割面”类型的物体,再把复杂碰撞单独开给那一小块区域,而不是开给整个Mesh。模型越复杂,越要遵守这个原则。
项目中可以用顶层的“Collision > Remove Collision”把默认自动生成的复杂碰撞全部删掉,再手动加简化碰撞体。删的时候要小心,删掉后记得在Collision面板的Setup里重新生成凸包,否则物体一路穿墙到天荒地老。
6.3 多人游戏网络同步下的碰撞坑
多人联机环境下,碰撞的坑比单机翻倍。最大一套坑来自“客户端碰撞”和“服务器碰撞”不一致。
举例:某个可拾取物只在客户端设了SphereComponent的重叠采集,但服务器端没有同步生成该碰撞体或者碰撞事件没绑定。于是玩家在客户端看到自己捡到了道具,但服务器不承认,回包后道具又出现在原地。
常规做法是:
- 所有影响玩法状态的碰撞事件,都应该在服务器端触发并验证。
- 客户端只负责表现和手感,比如摇杆偏移、命中特效、击退动画。
- 拾取、开门、造成伤害这类事件,必须由服务器采样碰撞结果后广播给客户端。
在做近战武器时,我把伤害判定全部放在服务器端做Sweep,客户端只负责播放挥砍动画。碰到延迟高的网络环境,玩家感觉“明明挥中了却没伤害”几乎是无法避免的,因为这就是服务器判定模型的特点。如果想要手感,可以客户端预显示命中特效,但伤害数字以服务器的回包为准,这样才能避免作弊。
6.4 调试碰撞的实用方法总结
调试碰撞问题,我建议按一套固定流程来做,不然无异于大海捞针:
- 关掉所有无关Actor的碰撞:把场景干掉只剩两个Actor,再逐步开启,能快速定位“谁和谁在互相碰撞”。
- 开启DrawDebug:在蓝图或C++里调用DrawDebugBox、DrawDebugSphere、DrawDebugLine关于碰撞体的实际范围,很多“看不见的碰撞”和“看起来不存在的碰撞”一眼可见。
- 使用VisualLogger:对于Movment和碰撞历史记录,VisualLogger会把每帧的碰撞响应打印成可视化日志,比凭空猜高到不知哪里去了。
- 组件控制台命令:UE4的Console中可以通过 cvar 开关显示碰撞,比如相关的Debug命令能直接列出场景中碰撞组件的对象类型和预设,结合日志过滤更快定位。
另外一个小技巧,我几乎在每个项目里都会用:在碰撞事件回调里加LogString,打印触发的组件名、对方组件名、触发类型和帧序号。你会发现一个表面上的“碰撞事件没触发”,往往其实是“触发了一次但被其他逻辑消耗掉了”,有了日志,才能顺着链路看看是谁拦截了信号。
我在实际项目的最后阶段,还会把所有碰撞预设统一导出一份项目文档,写上每个通道的用途和注意事项。这样后续任何人接手,都不需要重新对着面板猜规则。组件和碰撞这两套系统,说到底是项目从“能跑”走向“好维护”的基础。把它们的规则定清楚,调起交互来就只剩创作玩法本身的乐趣了。