Hello!
We are investigating viable ways to construct a very large vehicle, in an already very large level with high physics complexity. The default Mover with the minimal number of meshes for the vehicle is already hitting > 2ms on the game thread, mostly updating the child transforms and overlaps. We want to allow the player to build inside, drop physics objects, and dock smaller vehicles to it. So we need a stable physics sim inside and we can’t just turn off collisions and overlaps on everything. It also needs to work in multiplayer, and we have replication graph to do custom relevancy behavior if needed.
Its possible we can get that 2ms baseline down, but it seems like we will always be fighting for performance with the ‘naive’ approach.
The most promising solution so far that I’d love some feedback on, is to have two actors: an exterior actor and the interior:
- The exterior is a minimal set of meshes and colliders attached to the movement root.
- The interior is as complicated as it needs to be, but does not actually move.
This interior actor sits far away from the game world, and we manually call FScene::UpdatePrimitiveTransform_RenderThread in TG_LastDemotable on all its primitive components, to offset the primitive proxies on the render thread to look like they are attached to the exterior actor in the world.
- Any player that enters the vehicle teleports to the interior, then has their primitives and view transform offset back to the real world, but not their interaction traces.
- Any gameplay actors added to the interior actor will draw as if they were attached to the exterior. By using the primitive proxy transforms, we get correct culling, shadows, and mesh distance fields. It seems to ‘just work ™’…
- It also gives us a nice physics sim because none of the interior actors are actually moving.
- Replication relevancy can be handled by a special replication graph node that knows about the offset.
However, there will also be many knock on effects for gameplay systems relying on player and actor world transforms, as well as audio listener locations. But these are solvable, and we consider fixing these sorts of issues to be potentially easier than trying to optimize the naive approach.
The scarier side of this is the rendering; as I said it looks like it works, but ISM need more work and Niagara may not work so easily. Basically, we don’t know what other rendering issues might come up?
So I’m also curious about approaches that may be less drastic.
// Offset a list of primtive components to appear relative to an arbitraty transform
for (UPrimitiveComponent* Primitive : OffsetComponents)
{
FPrimitiveData PrimitiveData;
PrimitiveData.PrimitiveComponentId = Primitive->GetPrimitiveSceneId();
// Calculate world transforms and update PrimtiveData
PrimitiveDataList.Add(PrimitiveData);
// Just flags the transform as not dirty, so that the normal end of frame logic doesn't override our manual transform
Primitive->FlushRenderTransformDirty();
}
if (GetWorld()->Scene)
{
FScene* Scene = static_cast<FScene*>(GetWorld()->Scene);
ENQUEUE_RENDER_COMMAND(UpdateOffsetRenderingStatesRendering)(
[Scene, bMovedThisFrame, PrimitiveDataListRT = MoveTemp(PrimitiveDataList)](FRHICommandListImmediate& RHICmdList)
{
for (const auto& PrimitiveData : PrimitiveDataListRT)
{
FPrimitiveSceneInfo* PrimitiveSceneInfo = Scene->GetPrimitiveSceneInfo(PrimitiveData.PrimitiveComponentId);
if (!PrimitiveSceneInfo || !PrimitiveSceneInfo->Proxy)
{
continue;
}
Scene->UpdatePrimitiveTransform_RenderThread(
PrimitiveSceneInfo->Proxy,
PrimitiveData.WorldBounds,
PrimitiveData.LocalBounds,
PrimitiveData.RenderMatrix,
PrimitiveData.ActorLocation,
PrimitiveData.OldRenderTransform);
Scene->UpdatePrimitiveVelocityState_RenderThread(PrimitiveSceneInfo, bMovedThisFrame);
}
});
}
[Attachment Removed]