UE5 Collision
目录:
- Collision Presets 碰撞预设值 (基本内容同UE5官方文档,可跳过)
- 分析Overlap&Hit
产生碰撞的方式主要有两个:
- OnCompoment{Begin/End}Overlap
- OnComponentHit
Collision Presets 碰撞预设值
| 属性 | 说明 |
|---|---|
| Default | 使用已在静态网格体编辑器中应用给静态网格体的设置 |
| Custom | 可自定义所有碰撞设置 |
| NoCollision | 无碰撞 |
| BlockAll | 在默认情况下阻挡所有Actor的WorldStatic对象,所有新自定义信道都将使用其本身的默认响应 |
| OverlapAll | 在默认情况下与所有Actor重叠的WorldStatic对象,所有新自定义信道都将使用其本身的默认响应 |
| BlockAllDynamic | 在默认情况下阻挡所有Actor的WorldDynamic对象,所有新自定义信道都将使用其本身的默认响应 |
| OverlapAllDynamic | 在默认情况下与所有Actor重叠的WorldDynamic对象,所有新自定义信道都将使用其本身的默认响应 |
| IngoreOnlyPawn | 忽略Pawn和载具的WorldDynamic对象。所有其他信道都将设置为默认值 |
| OverlapOnlyPawn | 与Pawn、摄像机和载具重叠的WorldDynamic对象。所有其他信道都将设置为默认值 |
| Pawn | Pawn对象。可用于任意可操作角色或AI的胶囊体 |
| Spectator | 忽略除WorldStatic以外的所有其他Actor的Pawn对象 |
| CharacterMesh | 用于Pawn对象视觉网格。所有其他信道都将设置为默认值(允许射线检测\摄像机追踪,不允许产生物理碰撞) |
| PhysicsActor | 模拟Actor |
| Destructible | 可破坏物Actor |
| InvisibleWall | 不可见的WorldStatic对象 |
| InvisibleWallDynamic | 不可见的WorldDynamic对象 |
| Trigger | 用于触发器的WorldDynamic对象。所有其他信道都将设置为默认值 |
| Ragdoll | 模拟骨架网格体组件。所有其他信道都将设置为默认值 |
| Vehicle | 阻挡载具(Vehicle)、WorldStatic和WorldDynamic的载具对象。所有其他信道都将设置为默认值 |
| UI | 在默认情况下与所有Actor重叠的WorldStatic对象。所有新自定义信道都将使用其本身的默认响应 |
CollisionEnabled 启用碰撞
| 属性 | 说明 |
|---|---|
| 无碰撞(No Collision) | 在物理引擎中此形体将不具有任何表示。不可用于空间查询(光线投射、Sweep、重叠)或模拟(刚体、约束)。此设置可提供最佳性能,尤其是对于移动对象 |
| 仅查询(Query Only) | 此形体仅可用于空间查询(光线投射、Sweep和重叠)。不可用于模拟(刚体、约束)。对于角色运动和不需要物理模拟的对象,此设置非常有用。通过缩小物理模拟树中的数据来实现一些性能提升 |
| 仅物理(Physics Only) | 此形体仅可用于物理模拟(刚体、约束)。不可用于空间查询(光线投射、Sweep、重叠)。对于角色上不需要按骨骼进行检测的模拟次级运动,此设置非常有用。通过缩小查询树中的数据来实现一些性能提升 |
| 启用碰撞(Collision Enabled) | 此形体可用于空间查询(光线投射、Sweep、重叠)和模拟(刚体、约束) |
ObjectType 对象类型
| 属性 | 说明 |
|---|---|
| WorldStatic | 应用于不移动的任意Actor。静态网格体Actor是类型为"WorldStatic"的Actor的良好示例 |
| WorldDynamic | WorldDynamic用于受到动画或代码的影响而移动的Actor类型;运动学。电梯和门是WorldDynamic Actor的典型例子 |
| Pawn | 任何由玩家控制的实体的类型都应为Pawn。玩家角色应为"Pawn" |
| Vehicle | 载具默认类型 |
| Destructible | 可破坏物网格体的默认类型 |
Collision Responses 碰撞响应
| 响应 | 说明 |
|---|---|
| Ignore | 无论另一个物理形体的"碰撞响应(Collision Responses)"为何,此物理形体都将忽略交互 |
| Overlap | 如果已将另一个物理形体设置为"重叠(Overlap)"或"阻挡(Block)"此物理形体的"对象类型(Object Type)",将发生重叠事件 |
| Block | 如果已将另一个物理形体设置为"阻挡(Block)"此物理形体的"对象类型(Object Type)",将发生撞击事件 |
Object Responses 对象响应
当与对应类型物理形体对象交互时,此物理形体应该如何做出反应。
分析Overlap&Hit
Overlap
Overlap参数
| 参数 | 用法 |
|---|---|
| UPrimitiveComponent* OverlappedComp | 触发Overlap的组件 |
| AActor* OtherActor | 触发Overlap的另一个物体 |
| UPrimitiveComponent* OtherComp | 另一个物体具体触发Overlap的组件 |
| int32 OtherBodyIndex | 另一个物体具体哪个刚体触发Overlap |
| bool bFromSweep | 本次碰撞是否由MoveComponent/SetActorLocaiton等触发碰撞扫描的方法触发 |
| const FHitResult& SweepResult | 如果bFromSweep为真,表示Sweep的具体信息 |
Overlap工作原理
绑定
Overlap事件的生成在UBT执行期间发生,而不是初始保留在引擎内,引擎完全启动后进行,在Generate阶段就已经完成了静态解析,查看.generate.h文件可以看到:
//.generate.h
#define FID_forlearn_Source_forlearn_Public_ActorBase_h_14_RPC_WRAPPERS_NO_PURE_DECLS \
DECLARE_FUNCTION(execOnOverlapBegin);
//ObjectMacro.h
// This macro is used to declare a thunk function in autogenerated boilerplate code
#define DECLARE_FUNCTION(func) static void func( UObject* Context, FFrame& Stack, RESULT_DECL )根据注释可知,Overlap函数是自动生成的样板代码,通过传名调用(thunk function),具体一点,是函数指针(UFunction*,引擎内部可识别的指针类型)和函数名组成。
为什么不直接传入函数指针?
常见原因:不支持序列化,程序重启后函数指针地址变化,序列化的地址无意义;不支持蓝图,蓝图对象是字节码,不存在可记录的指针。
通过委托调用牺牲了一部分性能(差距约10x-100x),但是由于Overlap本身是低频事件,对性能影响并不是很大,但是确保了事件系统的安全性。如果需要极为高频的Overlap检查,也可以手动实现函数指针版本的委托,但是会失去蓝图和序列化等引擎特化功能的支持。
接下来是具体的Overlap委托生成过程,查看AddDymamic宏(顾名思义,也证明了Overlap确实是动态生成代码):
//Delegate.h
/**
* Helper macro to bind a UObject instance and a member UFUNCTION to a dynamic multi-cast delegate.
*
* @param UserObject UObject instance
* @param FuncName Function pointer to member UFUNCTION, usually in form &UClassName::FunctionName
*/
#define AddDynamic( UserObject, FuncName ) __Internal_AddDynamic( UserObject, FuncName, STATIC_FUNCTION_FNAME( TEXT( #FuncName ) ) )
//SparseDelegate.h
/**
* Binds a UObject instance and a UObject method address to this multi-cast delegate.
*
* @param InUserObject UObject instance
* @param InMethodPtr Member function address pointer
* @param InFunctionName Name of member function, without class name
*
* NOTE: Do not call this function directly. Instead, call AddDynamic() which is a macro proxy function that
* automatically sets the function name string for the caller.
*/
template< class UserClass >
void __Internal_AddDynamic( UserClass* InUserObject, typename FDelegate::template TMethodPtrResolver< UserClass >::FMethodPtr InMethodPtr, FName InFunctionName )
{
check( InUserObject != nullptr && InMethodPtr != nullptr );
// NOTE: We're not actually storing the incoming method pointer or calling it. We simply require it for type-safety reasons.
FDelegate NewDelegate;
NewDelegate.__Internal_BindDynamic( InUserObject, InMethodPtr, InFunctionName );
this->Add( NewDelegate );
}AddDynamic()宏旨在将一个成员函数和它的UObject实例绑定为一个多播委托,目的是为了通知引擎监听该事件。其中执行的__Internal_AddDynamic将该委托绑定到全局的invocation list,确保在物体tick时可以被正常监听。
动态生成的具体规则在SparseDelegate.h中,调用链比较长,不在这里一一列举,仅展示部分生成规则代码。注意:这里的“动态生成”不是指运行期动态生成,而是指编译前生成cpp代码,通过宏将委托展开为可绑定的委托类,这部分代码会和原本存在的代码一起编译。
//SparseDelegate.h
#define FUNC_DECLARE_DYNAMIC_MULTICAST_SPARSE_DELEGATE(SparseDelegateClassName, OwningClass, DelegateName, FuncParamList, FuncParamPassThru, ...) \
FUNC_DECLARE_DYNAMIC_MULTICAST_DELEGATE(SparseDelegateClassName##_MCSignature, SparseDelegateClassName##_DelegateWrapper, FUNC_CONCAT(FuncParamList), FUNC_CONCAT(FuncParamPassThru), __VA_ARGS__) \
struct SparseDelegateClassName##InfoGetter \
{ \
static const char* GetDelegateName() { return #DelegateName; } \
template<typename T> \
static size_t GetDelegateOffset() { return offsetof(T, DelegateName); } \
}; \
struct SparseDelegateClassName : public TSparseDynamicDelegate<SparseDelegateClassName##_MCSignature, OwningClass, SparseDelegateClassName##InfoGetter> \
{ \
};调用
Overlap委托的调用不由物体对象本身控制,而是由引擎统一控制,且不是每次tick都会触发。触发条件常见如组件移动、启用/禁用Collision等可能产生碰撞的事件。 这里举例SetActorLocationAndRotation的调用链,由于调用链过长,不再粘贴代码,仅展示调用过程。
SetActorLocationAndRotation->MoveComponent->MoveComponentImpl->UpdateOverlaps->UpdateOverlapsImpl->ComponentOverlapMultiImpl->OverlapMulti
最后具体的检测逻辑:
FPhysicsCommand::ExecuteRead(TargetInstance->ActorHandle, [&](const FPhysicsActorHandle& Actor)
{
if(FPhysicsInterface::IsValid(Actor))
{
// Get all the shapes from the actor
FInlineShapeArray PShapes;
const int32 NumShapes = FillInlineShapeArray_AssumesLocked(PShapes, Actor);
// Iterate over each shape
TArray<struct FOverlapResult> TempOverlaps;
for (int32 ShapeIdx = 0; ShapeIdx < NumShapes; ShapeIdx++)
{
// Skip this shape if it's CollisionEnabled setting was masked out
if (Params.ShapeCollisionMask && !(Params.ShapeCollisionMask & GetShapeCollisionEnabled(ShapeIdx)))
{
continue;
}
FPhysicsShapeHandle& ShapeRef = PShapes[ShapeIdx];
FPhysicsGeometryCollection GeomCollection = FPhysicsInterface::GetGeometryCollection(ShapeRef);
if(IsShapeBoundToBody(ShapeRef) == false)
{
continue;
}
if (!ShapeRef.GetGeometry().IsConvex())
{
continue;
}
TempOverlaps.Reset();
if(FPhysicsInterface::GeomOverlapMulti(World, GeomCollection, BodyInstanceSpaceToTestSpace.GetTranslation(), BodyInstanceSpaceToTestSpace.GetRotation(), TempOverlaps, TestChannel, Params, ResponseParams, ObjectQueryParams))
{
bHaveBlockingHit = true;
}
InOutOverlaps.Append(TempOverlaps);
}
}
});引擎遍历模型为Actor注册的原始模型形状,如Cube等,不会按照模型的每一个三角面遍历,除非开启Complex Collision,即使模型本身建模复杂,如果没有开启Complex Collision,引擎也会自动为模型生成简单的凸体集合作为模型形状。
实现算法
首先在SQVisitor.h中有一次宽相/窄相检测,筛选出物体是否需要精准检测,节省部分性能。
具体实现在AABBTree.h中,函数名QueryImp。它是Chaos BVH的核心遍历逻辑,快速找出碰撞体,按顺序处理全局静态物体、未重建BVH的动态物体、BVH树结构。
for(const auto& Elem : GlobalPayloads)
{
//...
if(TAABBTreeIntersectionHelper<TQueryFastData,Query>::Intersects(Start, CurData, TOI, InstanceBounds, QueryBounds, QueryHalfExtents, Dir, InvDir, bParallel))
{
//...
if(Query == EAABBQueryType::Overlap)
{
bContinue = Visitor.VisitOverlap(VisitData);
} else
{
bContinue = Query == EAABBQueryType::Sweep ? Visitor.VisitSweep(VisitData,CurData) : Visitor.VisitRaycast(VisitData,CurData);
}
}
}首先对遍历全局静态物体,粗估检测AABB是否重叠,即上述代码最外层if条件,快速剔除大部分无关物体,然后在内部对疑似重叠物体进行精准测试,即VisitOverlap。
if (DirtyElementTree)
{
if (!DirtyElementTree->template QueryImp<Query, TQueryFastData, SQVisitor>(Start, CurData, QueryHalfExtents, QueryBounds, Visitor, Dir, InvDir, bParallel))
{
return false;
}
}
else if (bMutable)
{
{
//粗估->精准测试
}
if (DirtyElements.Num() > 0)
{
bool bUseGrid = false;
if (DirtyElementGridEnabled() && CellHashToFlatArray.Num() > 0)
{
if constexpr (Query == EAABBQueryType::Overlap)
{
bUseGrid = !TooManyOverlapQueryCells(QueryBounds, DirtyElementGridCellSizeInv, DirtyElementMaxGridCellQueryCount);
}
//...其它检测
}
if (bUseGrid)
{
PHYSICS_CSV_SCOPED_VERY_EXPENSIVE(PhysicsVerbose, QueryImp_DirtyElementsGrid);
TArray<DirtyGridHashEntry> HashEntryForOverlappedCells;
auto AddHashEntry = [&](int32 QueryCellHash)
{
const DirtyGridHashEntry* HashEntry = CellHashToFlatArray.Find(QueryCellHash);
if (HashEntry)
{
HashEntryForOverlappedCells.Add(*HashEntry);
}
return true;
};
if constexpr (Query == EAABBQueryType::Overlap)
{
DoForOverlappedCells(QueryBounds, DirtyElementGridCellSize, DirtyElementGridCellSizeInv, AddHashEntry);
}
//...其它检测
if (!DoForHitGridCellsAndOverflow(HashEntryForOverlappedCells, IntersectAndVisit))
{
return false;
}
} // end overlap
//...
}
}然后递归脏数据树,检查移动过的物体是否与当前物体发生重叠,同样有粗估、精准测试过程。
constexpr int32 MaxNodeStackNumOnSystemStack = 255;
TArray<FNodeQueueEntry, TSizedInlineAllocator<MaxNodeStackNumOnSystemStack,32> > NodeStack;
while (NodeStack.Num())
{
PHYSICS_CSV_SCOPED_VERY_EXPENSIVE(PhysicsVerbose, QueryImp_NodeTraverse);
const FNodeQueueEntry NodeEntry = NodeStack.Pop(EAllowShrinking::No);
if constexpr (Query != EAABBQueryType::Overlap)
{
if (NodeEntry.TOI > CurData.CurrentLength)
{
continue;
}
}
const FNode& Node = Nodes[NodeEntry.NodeIdx];
if (Node.bLeaf)
{
PHYSICS_CSV_SCOPED_VERY_EXPENSIVE(PhysicsVerbose, NodeTraverse_Leaf);
const auto& Leaf = Leaves[Node.ChildrenNodes[0]];
if constexpr (Query == EAABBQueryType::Overlap)
{
if (Leaf.OverlapFast(QueryBounds, Visitor) == false)
{
return false;
}
}
}
else
{
PHYSICS_CSV_SCOPED_VERY_EXPENSIVE(PhysicsVerbose, NodeTraverse_Branch);
int32 Idx = 0;
{
for (const TAABB<T, 3>&AABB : Node.ChildrenBounds)
{
if (TAABBTreeIntersectionHelper<TQueryFastData, Query>::Intersects(Start, CurData, TOI, FAABB3(AABB.Min(), AABB.Max()), QueryBounds, QueryHalfExtents, Dir, InvDir, bParallel))
{
NodeStack.Emplace(FNodeQueueEntry{ Node.ChildrenNodes[Idx], TOI });
}
++Idx;
}
}
}
}最后遍历BVH树,使用栈模拟递归遍历,检查未移动的物体是否产生重叠(一般大部分物体都未移动),同样进行粗估和精准测试。
粗估算法为检查AABB重叠,有重叠则加入CollectResults作为待检物体,不重叠则直接剔除,时间复杂读O(1)。
精细检测算法具体代码没找到,后续找到了再补充。
Hit
Hit参数
| 参数 | 说明 |
|---|---|
| UPrimitiveComponent* HitComp | 触发Hit的组件 |
| AActor* OtherActor | 触发Hit的另一个物体 |
| UPrimitiveComponent* OtherComp | 触发Hit的另一个物体的对应组件 |
| FVector NormalImpulse | 碰撞产生的冲量在碰撞法线方向上的分量 |
| FHitResult& Hit | 包含本次碰撞详细信息的结构体 |
Hit工作原理
绑定 | 调用
和Overlap类似,不再重复说明。
实现算法
由Sweep扫掠检测和Raycast射线检测共同实现,具体代码逻辑和Overlap类似,不再重复说明。
Overlap和Hit的触发区别
Overlap的触发要求触发委托的对象Collision设置为Overlap,且双方都开启Collision且对方设置为Overlap/Block。
Hit要求触发委托的对象Collision设置为Block,同时对方也要设置为Block。
总的来讲,在双方启用Collision的情况下,Overlap的优先级要高于Block。
| 物体A | 设置 | 物体B | 设置 |
|---|---|---|---|
| Pawn | Overlap | Pawn | Block |
| WorldStatic | Block | WorldStatic | Block |
实际结果是:
| 场景 | A | B |
|---|---|---|
| AB碰撞 | 触发Overlap | 无 |
| A碰撞墙壁 | 触发Hit | - |
| B碰撞墙壁 | - | 触发Hit |
即只有双方都为Block时,才会触发Hit事件,如果有一方为Overlap,则只触发一次Overlap。
从性能上讲,Overlap只需要检测是否重叠,大部分简单模型或在不开启Complex Collision的情况下一次AABB检测就足够了,而Hit需要检测是否重叠后再进行一次Hit参数的生成,对于位置的计算更精细,因此性能上也更差一些。