在虚幻引擎4中使用异步碰撞跟踪

大家好。 我是Disruptive Games的工程师,我想分享一些有关Async Collision Traces的知识,以及它们在一点点绑定帮助下如何带来性能上的好处。 在本文中,我们将专门研究游戏线程/物理线程的内容。 我们不会只关注“渲染”或“绘制”线程。

免责声明:在线程方面,我还没有对所有这些系统进行完整的深入研究,因此我试图保持准确,但可能还会丢失一些细节。 像往常一样,描述,假设,改进,重复。 没有度量的优化是愚蠢的。


首先,我们将当前的标题Megalith称为PSVR标题,因此,我们对于保持60fps的性能和技术要求以及对玩家体验的要求都有相当严格的要求。 理想情况下,我们可以达到90fps,以获得最佳体验。 每帧分别有效地为16ms和11ms(偶数!)。 当然,这意味着优化。 以PS4硬件为目标时,您必须始终观看VR的性能。 同样,由于引擎的线程设置和您通常看到的纯单核速度,PC和PSVR之间的执行配置文件也大不相同。 因此,对于PC而言,甚至不会成为偶然的事情很容易成为您不断增加的性能成本。

我不想在这里涉猎过多,只是为在尽可能地提高效率的前提下搭建了舞台,这并没有太大地影响设计/灵活性。

在这种情况下,低调的成果是将同步轨迹转换为物理场景,再转换为异步轨迹。 如果您想了解一些有关碰撞痕迹的背景,请随时在此处访问Epic的文档。 注意:这与跟踪Async场景不同,我们将在本文中对其进行简要讨论。


我们在Megalith的标准游戏框架中使用了大量痕迹,因此,从性能画面中删除这一点并不是很大的收获,但确实有助于从游戏线程中删除违规者。 您必须注意的是它不是免费的。 这些循环走到其他地方,似乎开始于TaskGraph线程。 TaskGraph线程是UE4可以执行的通用任务线程。 因此,您仍然有可能要争夺足够的计算能力才能在特定的时间阈值内完成这项工作。 像任何东西一样,您的行驶里程可能会有所不同。 对于我们来说,它应该在不良帧时退回几毫秒,并有助于稳定帧率。

这里的基本前提是您正在进行一项操作,该操作将立即为您提供有关游戏物理性质状态的结果,并且您要求稍后获得结果。 在这种情况下,以后的时间总是在下一帧的开始处(在任何Tick组发生之前)。

因此,让我们现在看一下熟悉的World跟踪功能与异步跟踪功能之间的比较,其中许多跟踪功能暴露在蓝图中以方便访问。 异步跟踪功能仍然是UWorld对象的一部分,因此它们就可以在此处进行拾取,您只需要创建一些胶水即可使它们更易于使用。

跟踪功能:

  bool UWorld :: LineTraceSingleByChannel
struct FHitResult&OutHit
const FVector&Start,const FVector&End,
ECollisionChannel TraceChannel,
const FCollisionQueryParams&Params,
const FCollisionResponseParams&ResponseParam)const
FTraceHandle UWorld :: AsyncLineTraceByChannel
EAsyncTraceType InTraceType,
const FVector&Start,const FVector&End,
ECollisionChannel TraceChannel,
const FCollisionQueryParams&Params,
const FCollisionResponseParams&ResponseParam,
FTraceDelegate * InDelegate ,uint32 UserData)

顶部函数LineTraceSingleByChannel是同步版本,它立即对PhysX进行RaycastSingle调用。 底部函数AsyncLineTraceByChannel是异步版本,该异步版本返回FTraceHandle ,稍后将使用它来获取结果。

还有:

  • AsyncLineTraceByObjectType
  • AsyncSweepByChannel
  • AsyncSweepByObjectType
  • AsyncOverlapByChannel
  • AsyncOverlapByObjectType

为了简洁起见,我们仅在此处查看AsyncLineTraceByChannel ,但它们的功能都非常相似。 它们采用类似于同步版本的一组参数,然后返回FTraceHandles

请注意,上面我已经加粗了两件事:

  1. FTraceHandle
  2. FTraceDelegate

FTraceHandle

这实际上是您工作的“门票”。 一个简单的比喻是干洗票。 您脱下衣服,买票,并被告知稍后再回来。 返回时,您必须出示票证才能退回物品。 没有门票,您的衣服将在工作日结束时被焚毁! 这只是(对于给定的请求框架)唯一地跟踪您的单个异步工作请求。

FTraceDelegate

如果FTraceHandle是票证,则FTraceDelegate是交付服务。 您脱下衣服,以后,送货服务将您洗过的衣服送回到您家门口。 它只是一个非动态委托,您可以将其绑定到您的跟踪请求上,以在完成工作时调用该委托。 该委托有两个参数,没有返回值。 DECLARE_DELEGATE_TwoParams( FTraceDelegate, const FTraceHandle&, FTraceDatum &);

FTraceDatum

这是工作结果存储在World AsyncTraceState对象上的位置,以及如何通过提供的FTraceDelegate触发或通过使用FTraceHandle查询值将其返回给您的FTraceHandle 。 重要的是要注意这里有一些结构。 FTraceDatum派生FBaseTraceDatum ,它仅持有World,CollisionParams和UserData之类的共性。 FTraceDatum具有:

  1. 请求的世界空间中的起点/终点线位置
  2. TArray OutHits包含结果的TArray OutHits数组
  3. EAsyncTraceType确定单个,多个或测试请求

FOverlapDatum

这种结构是通过查询或委托触发将异步重叠工作结果返回给您的方式。 我只是简短地提到它,以使读者知道根据您对物理场景的要求,路径略有不同。 它具有:

  1. 请求的位置/旋转。
  2. TArray Results任务TArray Results数组。
  3. 重叠的形状信息在基类FBaseTraceDatum

一些有趣的事情要注意:

  • 代表将始终在下一帧的任何刻度功能之前触发。 这是因为在UWorld::Tick很早在帧中,并且在TG_PrePhysics刻度开始之前就调用UWorld::ResetAsyncTrace
  • 另一方面,在最后一个刻度组TG_LastDemotable完成之后,调用UWorld::FinishAsyncTrace 。 在框架的这一点上,所有报价组都已执行完毕。
  • 这些异步任务可以重叠帧边界。 从FinishAsyncTrace (确保所有工作正在进行中)到ResetAsyncTrace (等待所有工作完成)之间的时间是游戏线程结束和下一个开始之间的有效时间。 在那段时间发生了一些内置的事情/几个任务,这些使您有时间通过​​下一次调用ResetAsyncTrace完全完成工作。
  • 由于越过了框架边界,这可能会导致某些对象(我在看您的可破坏物)或任何您玩得太快和太松散的东西的稳定性问题。 YMMV!

由于上述信息,您需要确定是否要使用委托进行跟踪或检查FTraceHandle 。 两种方法都运行良好,代表的开销很小,并且在对象Tick组之外执行。 这可能会影响维护工作负荷并防止由于任意工作负载要求在特定刻度组中完成(或不完成)任务而造成的停顿。 许多人将发现的一个痛苦点是,注册的代表不是动态的,这意味着如果没有一点点额外的工作就无法对它进行蓝图。 稍后再说……

其他有用的位

翻阅WorldCollisionAsync.cpp文件。 它不是很长,而且应该很容易理解如何交接工作以执行和如何将数据编组回给我们。

  1. 如果数据已准备好用于给定的跟踪句柄,则bool UWorld::QueryTraceData(const FTraceHandle&, FTraceDatum&)返回true。并将结果放入第二个参数中。
  2. 如果给定重叠的数据可用,则bool UWorld::QueryOverlapData(const FTraceHandle&, FOverlapDatum&)返回true,如果存在,则在第二个参数中返回结果。
  3. bool UWorld::IsTraceHandleValid(const FTraceHandle& , bool)告诉您是否具有有效的句柄。 如果第二个参数用于重叠任务而不是跟踪任务,则设置为true。

因此,现在我们已经从游戏的角度看了整个图景。 让我们来看一个演员内部的用法示例。 注意:我会尝试为生产环境中的actor创建更多可重用的内容,但是,如果您有一个actor进行每个帧的跟踪,而您想缠住它们,那么这种方法也没有任何问题。

  1. 首先制作一个新的Actor派生Actor。 我正在给我的AsyncTraceActor打电话。 让Unreal进行编译,然后弹出您选择的IDE。
  2. 您将在标题中需要几件事。

因此,让我们深入了解实现细节。 我们先看一下BeginPlay。 我可以写整篇有关BeginPlay的文章,以及在网络游戏设置中使用它进行重要初始化的危险,但这完全是另一篇文章。 现在,让我们假设这是JoeBlow的史诗级单人RPG头衔的一部分。


BeginPlay

 无效AAsyncTraceActor :: BeginPlay() 
{
Super :: BeginPlay(); TraceDelegate.BindUObject(this,&AAsyncTraceActor :: OnTraceCompleted);
}

因此,这FTraceDelegate TraceDelegate我们声明的FTraceDelegate TraceDelegate到实际的实例函数,因此我们可以将其传递给AsyncLineTraceByChannel. 请注意,它正在绑定UObject。 我们可以将其绑定到标准Unreal委托的任何方法中。

委托处理程序方法

 无效AAsyncTraceActor :: OnTraceCompleted(const FTraceHandle&Handle,FTraceDatum&Data) 
{
sure(Handle == LastTraceHandle);
DoWorkWithTraceResults(Data);
LastTraceHandle._Data.FrameNumber = 0; //重置
}

这是不言自明的。 World AsyncState系统通知我们可以使用跟踪结果。 我们确保LastTraceHandle的Handle实际上是LastTraceHandle (如果我们以某种方式重用此委托,则不会这样)。 然后,我们将数据传递给主力函数DoWorkWithTraceResults并重置FrameNumber以使TraceHandle无效。

请求启动异步任务

  FTraceHandle AAsyncTraceActor :: RequestTrace() 
{
UWorld * World = GetWorld();
如果(世界== nullptr)
返回FTraceHandle();

自动频道= UEngineTypes :: ConvertToCollisionChannel(MyTraceType);
FCollisionObjectQueryParams ObjectQueryParams(Channel);

bool bTraceComplex = false;
bool bIgnoreSelf = true;
TArray ActorsToIgnore;

自动参数= UKismetSystemLibrary :: ConfigureCollisionParams(NAME_AsyncRequestTrace,bTraceComplex,ActorsToIgnore,bIgnoreSelf,this); 自动启动= FVector :: ZeroVector;
自动结束= FVector(1000.f); 返回World-> AsyncLineTraceByChannel(EAsyncTraceType :: Single,
开始,结束,
渠道,
参数
FCollisionResponseParams :: DefaultResponseParam,
&TraceDelegate);
}

因此,此功能实际上只是进行跟踪所需的所有标准设置。 主要的样板代码是通过UEngineTypes::ConvertToCollisionChannel,MyTraceType变量转换为CollisionChannel UEngineTypes::ConvertToCollisionChannel,并创建CollisionParams结构。 注意UKismetSystemLibrary:: ConfigureCollisionParams实际上是我们对KismetSystemLibrary所做的修改,以使现有的内联函数可以在文件外部使用。 我们只是将全局名称空间函数填充到静态的蓝图库调用中,就可以开始了!

然后,在示例中,我们设置了一些无效的Vector值以证明其有效(显然,您会输入您感兴趣的值),然后开始跟踪! 就像我们之前讨论的那样,这FTraceHandle 。 我们将使用它来查询有效性,并查看结果是否存在。

启动过程和滴答方法

 无效AAsyncTraceActor :: SetWantsTrace() 
{
//这里不允许重叠的痕迹。
如果(!GetWorld()-> IsTraceHandleValid(LastTraceHandle,false))
{
bWantsTrace = true;
}
}

回想一下SetWantsTrace是BlueprintCallable,因此它可以从蓝图领域触发。 它使整个事件集动起来。 您可以简单地调用RequestTrace()而不是使用bWantsTrace ,这就是我将其构造为将更多详细信息推送到Tick本身的方式。

 无效AAsyncTraceActor :: Tick(float DeltaTime) 
{
Super :: Tick(DeltaTime);
如果(LastTraceHandle._Data.FrameNumber!= 0)
{
FTraceDatum OutData;
如果(GetWorld()-> QueryTraceData(LastTraceHandle,OutData))
{
//清除句柄,以免我们下次输入
LastTraceHandle._Data.FrameNumber = 0;
//跟踪完成,对结果进行处理
DoWorkWithTraceResults(OutData);
}
}

如果(bWantsTrace)
{
LastTraceHandle = RequestTrace();
bWantsTrace = false;
}
}

第一个if语句正在对FrameNumber进行快速有效性检查。 处理跟踪结果后,我们将清除_Data.FrameNumber值。 由于这个原因,在本示例中,使用Tick()逻辑查询跟踪结果与委托方法兼容。

如果该句柄有效,请查看QueryTraceData返回良好结果。 由于Tick()的结构,这基本上应该是命中时间的100%。 我们确保在上一帧中滴答结束时我们发出RequestTrace 。 这意味着,下次我们进入此函数时,我们应该让QueryTraceData向我们返回有效结果。 在这种情况下,我们清除句柄,然后执行DoWorkWithTraceResults

处理结果并将其提供给蓝图

 无效AAsyncTraceActor :: DoWorkWithTraceResults(const FTraceDatum&TraceData) 
{
//在这里做事
ReceiveOnTraceCompleted(TraceData.OutHits);
}

因此,我们在这里没有任何有趣的事情,但是我们可以! 最好的部分是RecieveOnTraceCompleted将命中结果(可能应该提供有关任务的更多信息)作为BlueprintImplementableEvent.传递给Blueprint Land BlueprintImplementableEvent.


结论

我们已经简要讨论了异步跟踪(重叠/测试), UWorld对象内置了对异步跟踪的支持,这有助于减轻游戏线程的压力。 我们得出的结果是C ++ actor内部相当理想的用法。 提出了两种方法:1)我们可以使用非动态c ++委托来执行事件驱动的结果,或者2)我们可以自行检查FTraceHandle是否有效。 它有效,但是肯定会让我们想要更多。 我们如何轻松地在蓝图中使用这些东西? 如何从中获得更多里程?

下一部分,我将讨论如何将其从非常繁重的C ++域转换为蓝图领域。 我们将建立一个简单的基于ActorComponent的系统,该系统使我们可以将动态委托绑定到侦听跟踪结果中。 我们既要赋予设计师权力,又要确保我们能够使他们高效地做事,而又不会太在意统治。 希望我能在下一两周内解决这个问题。 理想情况下,在不久的将来,我们将展示一些性能指标。 不确定我们可以从PS4套件中显示多少配置文件,但我将调查这种可能性,以了解我们正在谈论的是什么样的收益。

谢谢阅读! 请随时在此处发表评论/指出任何不准确之处,虚幻引擎的运行速度如此之快,以至于我希望成为解决方案的一部分,以确保信息是最新的。 互联网上UE4上有太多令人讨厌的旧内容,这让我很难过。 🙁