在MMORPG游戏中,针对一些范围伤害的计算,会涉及到碰撞/相交检测。在传统的2D或2.5D游戏中,或者要求不那么精确的3D游戏中,这种相交检测可以简化为平面上圆形与各种形状(如圆形、矩形、扇形等)是否相交的检测^1^,但是当考虑上飞行、跳跃等逻辑后,就必须进行3D空间的相交检测了,此时就需要借助物理引擎的功能。
游戏物理引擎中,对于简单的几何体(如球体、胶囊体、立方体)的相交检测,都会将逻辑进行简化。复杂是由简单演化来的,正如几何中的点构成线,线构成面;一维变二维,二维变三维一样。碰撞检测算法也可以从点、线、面出发,计算出体相关的数据^2^。对于更复杂的凸包,我们有万能的解决方案来处理这些问题。那就是大名鼎鼎的GJK(Gilbert-Johnson-Keerthi)算法,它可以用来计算两个凸体间的最小距离。这里的凸体区别于凸包,可以看作是任意数量的点构成的凸形状,所以,从某种意义上来说,点、线段、三角形、四面体、凸包等都可以算作凸体。因此,该算法也可以用来计算简单几何体的碰撞(具体算法见参考资料2)。对于更复杂的三角网格体,一般是通过对三角网格生成BVH(层次包围体树 Bounding Volume Hierarchy)来简化计算。
UE中的物理碰撞一般是在角色蓝图里添加CapsuleComponent
(继承自ShapeComponent
的胶囊体组件,还有球形组件、立方体组件等),或是物理资产中骨骼BodySetup
中配置的物理形状。
这些形状组件或者骨骼中配置的物理资产,保存在BodySetup
中,BodySetup
里面有一个成员变量FKAggregateGeom
,这个结构中保存了在物理资产中配置的物理几何体信息,如上图的红框部分,有胶囊体数组、球体数组、包围盒数组、凸包数组、锥形胶囊体等。
USTRUCT()
struct ENGINE_API FKAggregateGeom
{
GENERATED_USTRUCT_BODY()
UPROPERTY(EditAnywhere, editfixedsize, Category = "Aggregate Geometry", meta = (DisplayName = "Spheres"))
TArray<FKSphereElem> SphereElems;
UPROPERTY(EditAnywhere, editfixedsize, Category = "Aggregate Geometry", meta = (DisplayName = "Boxes"))
TArray<FKBoxElem> BoxElems;
UPROPERTY(EditAnywhere, editfixedsize, Category = "Aggregate Geometry", meta = (DisplayName = "Capsules"))
TArray<FKSphylElem> SphylElems;
UPROPERTY(EditAnywhere, editfixedsize, Category = "Aggregate Geometry", meta = (DisplayName = "Convex Elements"))
TArray<FKConvexElem> ConvexElems;
UPROPERTY(EditAnywhere, editfixedsize, Category = "Aggregate Geometry", meta = (DisplayName = "Tapered Capsules"))
TArray<FKTaperedCapsuleElem> TaperedCapsuleElems;
class FKConvexGeomRenderInfo* RenderInfo;
//... others
}
这些数据在所属组件初始化的时候会创建出对应的物理几何体,并保存在FBodyInstance
运行时对象中,供后续碰撞检测使用。UE提供了一些基础几何体相交检测的上层接口,以球形为例,有以下两个方法:
/**
* Returns an array of actors that overlap the given sphere.
* @param WorldContext World context
* @param SpherePos Center of sphere.
* @param SphereRadius Size of sphere.
* @param Filter Option to restrict results to only static or only dynamic. For efficiency.
* @param ClassFilter If set, will only return results of this class or subclasses of it.
* @param ActorsToIgnore Ignore these actors in the list
* @param OutActors Returned array of actors. Unsorted.
* @return true if there was an overlap that passed the filters, false otherwise.
*/
UFUNCTION(BlueprintCallable, Category="Collision", meta=(WorldContext="WorldContextObject", AutoCreateRefTerm="ActorsToIgnore", DisplayName = "SphereOverlapActors"))
static bool SphereOverlapActors(const UObject* WorldContextObject, const FVector SpherePos, float SphereRadius, const TArray<TEnumAsByte<EObjectTypeQuery>>& ObjectTypes, UClass* ActorClassFilter, const TArray<AActor*>& ActorsToIgnore, TArray<class AActor*>& OutActors);
/**
* Returns an array of components that overlap the given sphere.
* @param WorldContext World context
* @param SpherePos Center of sphere.
* @param SphereRadius Size of sphere.
* @param Filter Option to restrict results to only static or only dynamic. For efficiency.
* @param ClassFilter If set, will only return results of this class or subclasses of it.
* @param ActorsToIgnore Ignore these actors in the list
* @param OutActors Returned array of actors. Unsorted.
* @return true if there was an overlap that passed the filters, false otherwise.
*/
UFUNCTION(BlueprintCallable, Category="Collision", meta=(WorldContext="WorldContextObject", AutoCreateRefTerm="ActorsToIgnore", DisplayName="SphereOverlapComponents"))
static bool SphereOverlapComponents(const UObject* WorldContextObject, const FVector SpherePos, float SphereRadius, const TArray<TEnumAsByte<EObjectTypeQuery>>& ObjectTypes, UClass* ComponentClassFilter, const TArray<AActor*>& ActorsToIgnore, TArray<class UPrimitiveComponent*>& OutComponents);
第一个方法返回的是与球形相交的Actor
,第二个方法返回的是与球形相交的UPrimitiveComponent
(有物理的Component,如USkeletalMeshComponent
即为他的子类),第一个方法也是以第二个方法为基础,返回这些UPrimitiveComponent
的归属Actor。只要我们能参考这些基础形状相交检测接口,根据配置生成对应的物理形状进行相交检测,就可以获取Overlap到的角色对象。
对于默认使用Physx物理引擎的UE4,参考引擎上层提供的几个相交检测接口(如SphereOverlapActors()
),具体方法就是根据传入的参数(如球形接口的球心坐标和半径)生成对应的PxGeometry
对象,然后使用这些几何对象进行相交检测。PxGeometry
的子类有PxSphereGeometry
、PxCapsuleGeometry
、PxBoxGeometry
、PxConvexMeshGeometry
、PxTriangleMeshGeometry
等,基础几何体的接口使用的就是前面三个子类,对于自定义的几何形状,由于三角网格体性能较差,我们使用凸包(PxConvexMeshGeometry
)来进行拟合。
下面以扇形柱(圆柱的一部分)为例,先简单讲一下生成扇形柱的点的算法。扇形柱的主要参数是扇形中心(定义为上下两个扇形面圆心连线的中点)坐标、扇形角度和扇形柱的高度。我们可以把扇形柱表示为多个等分三角柱的拟合体,即把扇形角度等分成N份(N值越大越精细),然后根据等分的角度和半径可以求得扇形弧边的坐标。再把扇形圆心坐标和弧边上的坐标Z分别加减半高即可得到扇形柱上下两个面上的顶点的集合。当然由于凸包的特性,这样无法精确表示大于180度的扇形柱,此时可以用两个小于180度的扇形柱来拟合。
我们得到扇形柱的顶点坐标后,只要能动态生成PxConvexMeshGeometry
对象,就可以仿照球体、胶囊体等相交检测方法来实现一个扇形柱的相交检测。
TSharedPtr<PxConvexMeshGeometry> FConvexMeshGeometryCreator::GetSectorCylinder(float HalfTheta, float Radius, float HalfHeight)
{
TArray<FVector> SectorCylinderVertexes;
// 根据扇形柱参数获得扇形柱顶点集合的方法,这里4表示将扇形等分成8份
GenerateSectorCylinder(SectorCylinderVertexes, HalfTheta, Radius, HalfHeight, 4);
PxConvexMesh* convexMesh;
EPhysXCookingResult result = GetPhysXCookingModule()->GetPhysXCooking()->CreateConvex(FPlatformProperties::GetPhysicsFormat(), EPhysXMeshCookFlags::Default, SectorCylinderVertexes, convexMesh);
if(result == EPhysXCookingResult::Succeeded)
{
TSharedPtr<PxConvexMeshGeometry> newSectorCylinder = MakeShareable(new PxConvexMeshGeometry(convexMesh),
[](PxConvexMeshGeometry* geometry)
{
geometry->convexMesh->release();
});
return newSectorCylinder;
}
return nullptr;
}
上面的代码中展示了如何生成PxConvexMeshGeometry
凸包几何体对象。对于自定义形状只要能根据一些简单参数生成顶点集合,我们就能在运行时动态生成几何体对象。由于凸包比基础形状要更复杂,生成过程会有一定的消耗,我们也可以将这些生成后的对象直接缓存起来供后续调用。
生成自定义物理几何对象后,我们就可以参考UE4实现写出对应的相交检测方法。
bool FHXPhysicsExtLibrary::SectorCylinderOverlapComponents(const UObject* WorldContextObject, const FVector& SectorPos,
const FVector& SectorExtent, const FQuat& SectorRot,
const TArray<TEnumAsByte<EObjectTypeQuery>>& ObjectTypes,
UClass* ComponentClassFilter, const TArray<AActor*>& ActorsToIgnore,
TArray<UPrimitiveComponent*>& OutComponents)
{
OutComponents.Empty();
FCollisionQueryParams Params(SCENE_QUERY_STAT(SectorCylinderOverlapComponents), false);
Params.AddIgnoredActors(ActorsToIgnore);
TArray<FOverlapResult> Overlaps;
FCollisionObjectQueryParams ObjectParams;
for (auto Iter = ObjectTypes.CreateConstIterator(); Iter; ++Iter)
{
const ECollisionChannel& Channel = UCollisionProfile::Get()->ConvertToCollisionChannel(false, *Iter);
ObjectParams.AddObjectTypesToQuery(Channel);
}
UWorld* World = GEngine->GetWorldFromContextObject(WorldContextObject, EGetWorldErrorMode::LogAndReturnNull);
if (World != nullptr)
{
#if PHYSICS_INTERFACE_PHYSX
TSharedPtr<PxConvexMeshGeometry> sectorCylinder = FConvexMeshGeometryCreator::GetSectorCylinder(
SectorExtent.X, SectorExtent.Y, SectorExtent.Z);
auto shapeHandle = FPhysicsInterface::CreateShape(sectorCylinder.Get(), false, true, GEngine->DefaultPhysMaterial);
FPhysicsGeometryCollection geomCollection = FPhysicsInterface::GetGeometryCollection(shapeHandle);
#endif
FPhysicsInterface::GeomOverlapMulti(World, geomCollection, SectorPos, SectorRot, Overlaps,
static_cast<ECollisionChannel>(0), Params,
FCollisionResponseParams::DefaultResponseParam, ObjectParams);
}
for (int32 OverlapIdx = 0; OverlapIdx < Overlaps.Num(); ++OverlapIdx)
{
FOverlapResult const& O = Overlaps[OverlapIdx];
if (O.Component.IsValid())
{
if (!ComponentClassFilter || O.Component.Get()->IsA(ComponentClassFilter))
{
OutComponents.AddUnique(O.Component.Get());
}
}
}
return (OutComponents.Num() > 0);
}
UE5引擎默认使用的自研Chaos物理引擎,如果换引擎的话,上一节方法中直接使用的Physx对象就会失效,因此也要研究一下对应的Chaos实现方法。UE提供的FPhysicsInterface::GeomOverlapMulti()
方法是一致的,因此我们需要找到Chaos中和Physx引擎PxGeometry
对象对应的结构(网上资料较少,可以直接参考UE源码中对于UBodySetup
对象中物理数据的初始化过程)。
Chaos引擎中类似PxGeometry
的结构为FImplicitObject
,对于凸包几何体PxConvexMeshGeometry
,Chaos中对应的类为FImplicitObject
的子类FConvex
,参考Physx实现类似的方法,我们同样可以写出扇形柱的Chaos实现:
TSharedPtr<Chaos::FImplicitObject, ESPMode::ThreadSafe> FConvexMeshGeometryCreator::GetSectorCylinder(float HalfTheta, float Radius, float HalfHeight)
{
TArray<FVector> SectorCylinderVertexes;
// 根据扇形柱参数获得扇形柱顶点集合的方法,这里4表示将扇形等分成8份
GenerateSectorCylinder(SectorCylinderVertexes, HalfTheta, Radius, HalfHeight, 4);
FCookBodySetupInfo InParam;
InParam.bCookNonMirroredConvex = true;
InParam.NonMirroredConvexVertices.Emplace(SectorCylinderVertexes);
TArray<TUniquePtr<Chaos::FImplicitObject>> SimpleImplicits;
// 该方法引擎并未加上ENGINE_API宏,无法跨模块引用,因此需要修改引擎源码加上ENGINE_API宏
Chaos::Cooking::BuildConvexMeshes(SimpleImplicits, InParam);
if(SimpleImplicits.Num() > 0)
{
return MakeShared<Chaos::FConvex, ESPMode::ThreadSafe>(
MoveTemp(SimpleImplicits[0].Release()->GetObjectChecked<Chaos::FConvex>()));
}
return nullptr;
}
以及对应的相交检测方法:
bool FHXPhysicsExtLibrary::SectorCylinderOverlapComponents(const UObject* WorldContextObject, const FVector& SectorPos,
const FVector& SectorExtent, const FQuat& SectorRot,
const TArray<TEnumAsByte<EObjectTypeQuery>>& ObjectTypes,
UClass* ComponentClassFilter, const TArray<AActor*>& ActorsToIgnore,
TArray<UPrimitiveComponent*>& OutComponents)
{
OutComponents.Empty();
FCollisionQueryParams Params(SCENE_QUERY_STAT(SectorCylinderOverlapComponents), false);
Params.AddIgnoredActors(ActorsToIgnore);
TArray<FOverlapResult> Overlaps;
FCollisionObjectQueryParams ObjectParams;
for (auto Iter = ObjectTypes.CreateConstIterator(); Iter; ++Iter)
{
const ECollisionChannel& Channel = UCollisionProfile::Get()->ConvertToCollisionChannel(false, *Iter);
ObjectParams.AddObjectTypesToQuery(Channel);
}
UWorld* World = GEngine->GetWorldFromContextObject(WorldContextObject, EGetWorldErrorMode::LogAndReturnNull);
if (World != nullptr)
{
#if WITH_CHAOS
TSharedPtr<Chaos::FImplicitObject, ESPMode::ThreadSafe> Convex = FConvexMeshGeometryCreator::GetSectorCylinder(
SectorExtent.X, SectorExtent.Y, SectorExtent.Z);
Chaos::TSerializablePtr<Chaos::FImplicitObject> InGeom = Chaos::TSerializablePtr<Chaos::FImplicitObject>(Convex);
TUniquePtr<Chaos::FPerShapeData> ShapeData = Chaos::FPerShapeData::CreatePerShapeData(0, InGeom);
ShapeData->SetCollisionTraceType(Chaos::EChaosCollisionTraceFlag::Chaos_CTF_UseComplexAsSimple);
ShapeData->SetSimEnabled(false);
ShapeData->SetQueryEnabled(true);
ShapeData->UpdateShapeBounds(FTransform(SectorRot, SectorPos));
FPhysicsActorHandle ActorHandle = nullptr;
FPhysicsShapeHandle ShapeHandle = FPhysicsShapeHandle(ShapeData.Get(), ActorHandle);
FPhysicsGeometryCollection geomCollection = FPhysicsInterface::GetGeometryCollection(ShapeHandle);
#endif
FPhysicsInterface::GeomOverlapMulti(World, geomCollection, SectorPos, SectorRot, Overlaps,
static_cast<ECollisionChannel>(0), Params,
FCollisionResponseParams::DefaultResponseParam, ObjectParams);
}
for (int32 OverlapIdx = 0; OverlapIdx < Overlaps.Num(); ++OverlapIdx)
{
FOverlapResult const& O = Overlaps[OverlapIdx];
if (O.Component.IsValid())
{
if (!ComponentClassFilter || O.Component.Get()->IsA(ComponentClassFilter))
{
OutComponents.AddUnique(O.Component.Get());
}
}
}
return (OutComponents.Num() > 0);
}
参考资料:
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。