Skip to content

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对象。所有其他信道都将设置为默认值
PawnPawn对象。可用于任意可操作角色或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的良好示例
WorldDynamicWorldDynamic用于受到动画或代码的影响而移动的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文件可以看到:

cpp
//.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确实是动态生成代码):

cpp
//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代码,通过宏将委托展开为可绑定的委托类,这部分代码会和原本存在的代码一起编译。

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

最后具体的检测逻辑:

cpp
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树结构。

cpp
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

cpp
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
        //...
	}
}

然后递归脏数据树,检查移动过的物体是否与当前物体发生重叠,同样有粗估、精准测试过程。

cpp
		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设置
PawnOverlapPawnBlock
WorldStaticBlockWorldStaticBlock

实际结果是:

场景AB
AB碰撞触发Overlap
A碰撞墙壁触发Hit-
B碰撞墙壁-触发Hit

即只有双方都为Block时,才会触发Hit事件,如果有一方为Overlap,则只触发一次Overlap。

从性能上讲,Overlap只需要检测是否重叠,大部分简单模型或在不开启Complex Collision的情况下一次AABB检测就足够了,而Hit需要检测是否重叠后再进行一次Hit参数的生成,对于位置的计算更精细,因此性能上也更差一些。

如有技术错误或应改进之处感谢前往GitHub指出