前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >UE运行时动态生成自定义物理形状碰撞检测

UE运行时动态生成自定义物理形状碰撞检测

原创
作者头像
Kill Console
修改2022-08-22 15:24:29
3K0
修改2022-08-22 15:24:29
举报
文章被收录于专栏:游戏开发经验总结

1 背景

  在MMORPG游戏中,针对一些范围伤害的计算,会涉及到碰撞/相交检测。在传统的2D或2.5D游戏中,或者要求不那么精确的3D游戏中,这种相交检测可以简化为平面上圆形与各种形状(如圆形、矩形、扇形等)是否相交的检测^1^,但是当考虑上飞行、跳跃等逻辑后,就必须进行3D空间的相交检测了,此时就需要借助物理引擎的功能。

  游戏物理引擎中,对于简单的几何体(如球体、胶囊体、立方体)的相交检测,都会将逻辑进行简化。复杂是由简单演化来的,正如几何中的点构成线,线构成面;一维变二维,二维变三维一样。碰撞检测算法也可以从点、线、面出发,计算出体相关的数据^2^。对于更复杂的凸包,我们有万能的解决方案来处理这些问题。那就是大名鼎鼎的GJK(Gilbert-Johnson-Keerthi)算法,它可以用来计算两个凸体间的最小距离。这里的凸体区别于凸包,可以看作是任意数量的点构成的凸形状,所以,从某种意义上来说,点、线段、三角形、四面体、凸包等都可以算作凸体。因此,该算法也可以用来计算简单几何体的碰撞(具体算法见参考资料2)。对于更复杂的三角网格体,一般是通过对三角网格生成BVH(层次包围体树 Bounding Volume Hierarchy)来简化计算。

2 UE中物理引擎动态生成物理网格体

  UE中的物理碰撞一般是在角色蓝图里添加CapsuleComponent(继承自ShapeComponent的胶囊体组件,还有球形组件、立方体组件等),或是物理资产中骨骼BodySetup中配置的物理形状。

UBodySetup
UBodySetup

  这些形状组件或者骨骼中配置的物理资产,保存在BodySetup中,BodySetup里面有一个成员变量FKAggregateGeom,这个结构中保存了在物理资产中配置的物理几何体信息,如上图的红框部分,有胶囊体数组、球体数组、包围盒数组、凸包数组、锥形胶囊体等。

代码语言:txt
复制
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提供了一些基础几何体相交检测的上层接口,以球形为例,有以下两个方法:

代码语言:txt
复制
/**
 * 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到的角色对象。

2.1 Physx引擎实现

  对于默认使用Physx物理引擎的UE4,参考引擎上层提供的几个相交检测接口(如SphereOverlapActors()),具体方法就是根据传入的参数(如球形接口的球心坐标和半径)生成对应的PxGeometry对象,然后使用这些几何对象进行相交检测。PxGeometry的子类有PxSphereGeometryPxCapsuleGeometryPxBoxGeometryPxConvexMeshGeometryPxTriangleMeshGeometry等,基础几何体的接口使用的就是前面三个子类,对于自定义的几何形状,由于三角网格体性能较差,我们使用凸包(PxConvexMeshGeometry)来进行拟合。

  下面以扇形柱(圆柱的一部分)为例,先简单讲一下生成扇形柱的点的算法。扇形柱的主要参数是扇形中心(定义为上下两个扇形面圆心连线的中点)坐标、扇形角度和扇形柱的高度。我们可以把扇形柱表示为多个等分三角柱的拟合体,即把扇形角度等分成N份(N值越大越精细),然后根据等分的角度和半径可以求得扇形弧边的坐标。再把扇形圆心坐标和弧边上的坐标Z分别加减半高即可得到扇形柱上下两个面上的顶点的集合。当然由于凸包的特性,这样无法精确表示大于180度的扇形柱,此时可以用两个小于180度的扇形柱来拟合。

扇形柱示意图
扇形柱示意图

  我们得到扇形柱的顶点坐标后,只要能动态生成PxConvexMeshGeometry对象,就可以仿照球体、胶囊体等相交检测方法来实现一个扇形柱的相交检测。

代码语言:txt
复制
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实现写出对应的相交检测方法。

代码语言:txt
复制
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);
}

2.2 Chaos引擎实现

  UE5引擎默认使用的自研Chaos物理引擎,如果换引擎的话,上一节方法中直接使用的Physx对象就会失效,因此也要研究一下对应的Chaos实现方法。UE提供的FPhysicsInterface::GeomOverlapMulti()方法是一致的,因此我们需要找到Chaos中和Physx引擎PxGeometry对象对应的结构(网上资料较少,可以直接参考UE源码中对于UBodySetup对象中物理数据的初始化过程)。

  Chaos引擎中类似PxGeometry的结构为FImplicitObject,对于凸包几何体PxConvexMeshGeometry,Chaos中对应的类为FImplicitObject的子类FConvex,参考Physx实现类似的方法,我们同样可以写出扇形柱的Chaos实现:

代码语言:txt
复制
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;
}

  以及对应的相交检测方法:

代码语言:txt
复制
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);
}

参考资料:

  1. 检测点是否在扇形之内
  2. 现代游戏物理引擎入门

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 1 背景
  • 2 UE中物理引擎动态生成物理网格体
    • 2.1 Physx引擎实现
      • 2.2 Chaos引擎实现
      领券
      问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档