Where does Unreal calculate x=x+v*dt, really?

Preamble

Unreal’s physics framework and its new(ish) physics engine Chaos are some pretty sophisticated software. Certainly there are things to complain about and improve on, but it’s easy to take for granted having such a robust and performant 3D physics/collision system Just Work by choosing a component in a dropdown, without having to read and reproduce a computer science PhD thesis.

Of course, when working on a project with a non-trivial use case for the physics engine, there are times when it doesn’t Just Work, and things become a familiar game of helplessly searching for the magic checkbox or contriving a nasty workaround, and if worse comes to worst it’s entirely possible that the situation might call for engine modification. In these cases it becomes necessary to pop the hood and at least read some engine code - a daunting task in most cases, but particularly so for such a technical and pervasive aspect of the game engine.

This blog post aims to provide a basic overview of the internal C++ engine classes associated with Unreal’s physics framework, and outline the major code paths between the API and these classes. It does not explore advanced physics features, and it doesn’t cover the implementation of the physics simulation itself. Rather, it connects the dots between the familiar Unreal API classes and the “black box” that updates the positions of actors in the scene. This is not a tutorial or infallible documentation: I claim no expertise in physics simulation nor any insider information regarding the engine’s design. It is merely the result of reading and stepping through engine code.

This post was written by referencing the 5.1.0 release of Unreal’s source. It assumes familiarity with C++ and Unreal’s C++ API, and access to the Unreal Engine GitHub repository (all of the GitHub links will 404 otherwise!).

Introduction

There are two fundamental concepts that a game developer expects from any physics engine:

  1. Simulation - Tracking and updating positions of actors in the scene, both in isolation and in relation to other actors (i.e. collisions)
  2. Interaction - The ability to query and modify the state of actors in the scene, both by configuring initial parameters and reacting to input at runtime

Simulation without interaction is an animation; interaction without simulation is data entry. From an engine architecture perspective, these concepts interrelate quite a bit. I have opted to organize the exploration of the framework into the following broad API interactions:

  1. Actor Registration - Initializing the physics engine’s model of the scene, as well as adding/removing new actors to/from the model
  2. Ticking/Input - Prompting the physics engine to advance the simulation, taking into account any changes from user input, and handling the results
  3. Querying - Directly checking the state of the scene, e.g. line tracing, as well as being notified of physics events in the scene, e.g. collisions/overlaps

I will cover each of these in turn in the second section, but before that it’s necessary to enumerate the cast of characters that make this entire production come together.

Major Classes

In this section I will briefly cover the major classes that we’ll see as we explore the API interactions mentioned in the introduction. This is our high level overview of the structural relationships of the physics framework, before we start actually walking through it.

The World

UWorld is “the top level object representing a map in which Actors and Components will exist and be rendered.” This should be the most familiar of the classes noted here if you’ve spent any time at all working in Unreal with C++; GetWorld() might be the most frequently used API call in the engine. As is mentioned in the documentation, in general there is only one world at a time, but there can be more for various reasons, particularly in the editor. Blueprint nodes omit it as a parameter because their scope never makes it ambiguous which “World” they’re concerned with. For the purposes of this blog post we aren’t particularly concerned with the ramifications of a multiverse, but it’s good to keep in mind as we move deeper into the engine and see what a single UWorld holds.

The documentation’s description of a UWorld is technically true, but belies a lot of what UWorld does, and does not do. For example, UWorld does not maintain an array of AActors: ULevel does. That is in fact ULevel’s primary purpose. But ULevels don’t exist without a UWorld (ULevel is owned by UWorld), and UWorld provides the main interface for modifying the AActor list (e.g. UWorld::SpawnActor()). So, technically true. But we are concerned with how UWorld relates to the physics engine. Naturally, the most significant aspect is that AActor list, or more specifically their associated FBodyInstances, which are the AActors' physics representation. Less trivially UWorld owns the FPhysScene1, the stage for our proverbial physics performance.

In addition to its position at the top of the object hierarchy, UWorld’s interface provides our entrypoint into all three of our API interactions:

  1. Actor Registration: SpawnActor() and DestroyActor().
  2. Scene Ticking: UWorld::Tick() is responsible for kicking off every physics simulation step.
  3. Querying: UWorld contains a suite of methods for performing trace, sweep, and overlap queries of the physics scene.

Each of these will be explored in further detail in their respective sections. As a side note, when spelunking in code it’s useful to remember that UWorld’s source is split across multiple .cpp files: World.cpp, LevelActor.cpp, WorldCollisionAsync.cpp, PhysLevel.cpp, and LevelTick.cpp.

The Grand Stage

The classes introduced in this section form a one-to-one, top-to-bottom object hierarchy, and are the major controllers between our recognizable Unreal API and the mathematics of the physics simulation. Naturally, between and within each level there are layers of abstraction, but these classes will be the key landmarks as we explore the major code paths in the following section.

FPhysScene_Chaos

FPhysScene_Chaos (inheriting from FChaosScene), AKA FPhysScene, is the primary interface to the physics simulation. It owns the networked physics replication handler (a subject for another time), handles registration and dispatch of collision events (Querying), and provides the API used for actually applying forces to physics particles.

More importantly than those duties, however, it provides the API for actually advancing the simulation (Ticking) that is called by UWorld with StartFrame() and EndFrame(). It also creates and owns FPBDRigidsSolver.

FPhysInterface_Chaos

An honorable mention is due for FPhysInterface_Chaos, AKA FPhysicsInterface. If you follow along with the code you will see it mentioned in various places, but I aggressively tried to gloss over it for ease of understanding because it is what it says on the tin: an interface to the more interesting physics classes, particularly FPhysScene_Chaos and Querying helpers.

FPBDRigidsSolver

FPBDRigidsSolver (inheriting from FPhysicsSolverBase), AKA FPhysicsSolver, is the last bastion standing between the “game engine” and the “physics engine”. It owns FPBDRigidsEvolutionGBF, the heart of the physics simulation; FPBDRigidsSOAs, the canonical collection of particles; and FChaosMarshallingManager, the harried middle-man juggling communications between various game threads to make the entire production come together.

“PBD” stands for “Position Based Dynamics”, which is the real computer science PhD thesis territory. Rest assured, exploring how PBD works and how Unreal implements it is far outside of the scope of this blog post. Suffice to say that this is the conceptual basis for the physics simulation.

FPBDRigidsSOAs

FPBDRigidsSOAs is our master list of physics particles. “SOA” stands for “structure of arrays”, a common memory layout optimization, and the plural “SOAs” refers to FPBDRigidsSOAs' pointers to different collections inheriting from TGeometryParticlesImp, e.g. FGeometryParticles, FKinematicGeometryParticles, or TPBDGeometryCollectionParticles. These Structures hold the Arrays that encode the data (position, mass, geometry, etc) for all of the particles in our scene.

FChaosMarshallingManager

FChaosMarshallingManager manages queues of FPushPhysicsData and FPullPhysicsData structs, which are the vessels for transmitting updates from the game thread to the physics thread and vice versa. We will follow these more closely in the section on Ticking.

FPBDRigidsEvolutionGBF

FPBDRigidsEvolutionGBF (inheriting from FPBDRigidsEvolutionBase) is where the magic happens. It is where finally, a dozen abstraction layers down, we find recognizable equations for simulating physics. Naturally, it holds a reference to the Solver’s FPBDRigidsSOAs structure, since that is the data that it is manipulating. Furthermore, it uses that particle data to maintain structures for accelerating collision detection, i.e. “Broad Phase” optimization, along with other implementation-specific structures that we won’t be investigating.

“GBF” stands for “Guendelman, Bridson, Fedkiw”, and refers to a paper by those authors on collisions between rigid bodies. Like PBD, rest assured that the details fall outside the scope of this post.

Dramatis Personae

This section covers the classes that represent the objects in motion - the things that are being manipulated by the classes in the previous section.

AActor

If you haven’t at least heard of AActor, you probably haven’t made it this far into the blog post. These are the top level things in the level, the action figures we’re mashing together. As discussed in the UWorld section, the ULevel maintains the list of AActors that exist in the world. The thing about AActors that we are most interested in here, however, is their Components. Most importantly, we care if they have a UPrimitiveComponent. Without one of these, they have no effect on the physics simulation, and indeed do not exist from the point of the view of the physics engine.2

UPrimitiveComponent

UPrimitiveComponent is the parent class for components that contain geometry. This includes the familiar UStaticMeshComponent and USkeletalMeshComponent, which we might use solely for their rendered geometry, but also USphereComponent, UBoxComponent, and UCapsuleComponent, which are explicitly used for collisions, replacing or working in tandem with more complex geometry from Meshes. Regardless of which subclass we’re looking at, they all interact with the physics framework through UPrimitiveComponent’s FBodyInstance member variable.

FBodyInstance

FBodyInstance is a “container for a physics representation of an object”. They are directly owned by UPrimitiveComponents, and they hold most of the properties that are set in the “Physics” and “Collisions” sections of the Details pane in the editor: collision channels/responses, transform constraints, mass/damping overrides, etc.

Architecturally, there are three properties that are of particular interest. The first is TWeakObjectPtr<UBodySetupCore> BodySetup, which is a “cookie-cutter” for the FBodyInstance. We’ll cover UBodySetup (which inherits from UBodySetupCore) in more detail when we talk about actor registration. The second is FBodyInstance* WeldParent, which enables tree-like parent-child relationships between FBodyInstances. We’ll see this used in our discussion later but we won’t follow it too closely. The third property is FPhysicsActorHandle ActorHandle, which is actually FSingleParticlePhysicsProxy* ActorHandle.

FSingleParticlePhysicsProxy

FSingleParticlePhysicsProxy is our interface to the “particles” that are the objects that are actually being simulated by the physics engine. The physics engine doesn’t care what color something is or what team it’s on, it cares about pure geometric shapes, their positions, and which other things they interact with (i.e. their collision channels). This is all held in the TGeometryParticle which our Proxy holds a pointer to, which is the game thread’s representation of the physics particle. The proxy structure also holds a pointer to a TGeometryParticleHandle. This is a window into the physics thread’s representation of the particle. Remembering that the particle information for the physics thread is maintained in FPBDRigidsSOAs, a TGeometryParticleHandle holds a reference to a SOA (TGeometryParticlesImp) and an index into that structure’s arrays. These two pieces of information uniquely identify a single particle in the scene, and the ParticleHandle provides an interface for setting/getting properties (e.g. position) of that particle.

There exist other types of physics proxies, including FGeometryCollectionPhysicsProxy (which are related to Chaos Destruction), FJointConstraintPhysicsProxy, and FSuspensionConstraintPhysicsProxy. All of these inherit from the same base class IPhysicsProxyBase, so we will frequently see code for handling these types next to FSingleParticlePhysicsProxy inside switch statements at various points in the code. In general though, FSingleParticlePhysicsProxy is the most common and most representative case for basic physics simulation, so we won’t be paying much attention to the others in our investigation.

API Interactions

Before we dive into the code paths, a brief note on threads. Unreal Engine makes extensive use of multithreading in various ways. You may hear someone distinguish between the “Game” thread and the “Render” thread, which work in parallel for the duration of gameplay. In the context of physics we might make reference to the “physics thread”, but it is worth noting that there is no singular, capital P “Physics Thread” like there is for Game or Render - physics calculation tasks are dispatched to worker threads by the Game thread and managed as Unreal’s scheduler sees fit.

0. Engine Initialization

Surprise, this wasn’t one of the things in the list in the introduction! Well, the implicit extra thing that a developer would expect from a physics engine’s API is for it to, you know, start up.

First, there was nothing. But then came the Big Bang, and all of the stars in the sky. Skip forward a few years and we’ve just started up the Unreal Engine executable. Each platform has its own way of reaching it, but they all meet up in GuardedMain(). This function calls FEngineLoop::PreInit(), then FEngineLoop::Init() (or EditorInit()), then hops into a tight while-loop over FEngineLoop::Tick() until the end of time (or we quit, whichever comes first). Obviously there is a lot (read: the entire rest of the engine) going on in each of those functions that we’re ignoring, but it’s good to center ourselves in the knowledge that at the end of the day this is a program with a while loop in a main function like any other. For a deeper exploration of engine initialization, I recommend “The Unreal Engine Game Framework: From int main() to BeginPlay” by Alex Forsythe.

FEngineLoop::PreInit() calls InitGamePhys(). At one point this kicked off a lot of boilerplate for PhysX setup, but nowadays it simply loads the Chaos and ChaosSolverEngine modules, the latter of which starts up ChaosSolversModule. I’m not particularly concerned with what each module packages for this investigation, so we will not be looking closely at these.

FEngineLoop::Init() is an extremely meaty function, but for our purposes it hits only a couple of key items. First it creates GEngine, the trusty global UObject that sits on a throne above even UWorld. It then uses GEngine to create and start the UGameInstance. UGameInstance::StartGameInstance() is primarily concerned with loading the first3 map, calling UEngine::Browse() (and thus UEngine::LoadMap()) on the default configured map.

After that, we are out of the realm of engine bootstrapping, and now in the realm of runtime behavior. UEngine::LoadMap() is the central logic for loading a map during play, whether that’s through a manual GEngine->Browse() call or a seamless travel. UEngine::LoadMap() does a whole slew of interesting things, but the thing we care about most is that it creates our UWorld, and then calls UWorld::InitWorld(). This is where our FPhysScene is constructed, which leverages the ChaosSolversModule to construct our FPBDRigidsSolver instance, which in turn constructs our FPBDRigidsEvolutionGBF instance.

So the engine initializes our modules and starts up the UGameInstance, the UGameInstance starts up our UWorld, and our UWorld starts up our physics framework. There we have it: the stage is set. Except, aren’t we’re forgetting our esteemed AActors and their moneymaking FBodyInstances? What is a performance without our actors, waiting in the wings?

1. Actor Registration

AActor Initialization

Now that the physics engine has started up, it’s time to get the cast on stage and populate our scene with FBodyInstances. There are two situations where we would expect to register actors: on level load, and when spawning them during runtime. Both situations meet up in the same place, but reach it from different directions.

For the former case of registration on level load, a little bit after UWorld::InitWorld() is called in UEngine::LoadMap(), UWorld::InitializeActorsForPlay() is called. This in turn calls ULevel::IncrementalRegisterComponents(), which iterates through all of the AActors in the level, calling AActor::IncrementalRegisterComponents() on each of them. This function then calls UActorComponent::RegisterComponentWithWorld() on all of their components. A pretty straightforward initialization trickle-down from UWorld to ULevel to the AActors to their UActorComponents.

For the latter case of registration on spawn, UWorld::SpawnActor() calls AActor::PostSpawnInitialize() on the spawned actor, which calls AActor::IncrementalRegisterComponents(), just like in the level load case. If the actor is a blueprint, however, it takes a slightly different route. At the end of AActor::PostSpawnInitialize(), it kicks off the blueprint actor construction process with ExecuteConstruction(). A few steps down the call stack we hit USimpleConstructionScript::RegisterInstancedComponent(), which calls UActorComponent::RegisterComponentWithWorld() just like AActor::IncrementalRegisterComponents() does above.

So in every case we find ourselves in UActorComponent::RegisterComponentWithWorld(). This is the path that any component on an actor would take to start up, whether or not it has anything to do with the physics engine. But, in case it does have something to do with the physics engine, that method calls UActorComponent::CreatePhysicsState(). Now we’re finally getting somewhere - it says “physics” right there! This function calls the virtual function UActorComponent::OnCreatePhysicsState(), which doesn’t do much, but if it’s a UPrimitiveComponent rather than simply a UActorComponent, things get a bit more interesting in the overridden function. It is here that we finally find our masked phantom skulking in the rafters: FBodyInstance::InitBody().

FBodyInstance Initialization

FBodyInstance::InitBody() is where FBodyInstance comes to life, with the help of its dark underlings, UBodySetup and FInitBodiesHelper.

FBodyInstance’s Lackeys

UBodySetup “contains all collision information that is associated with a single asset”. It holds the actual geometric data associated with a mesh. You can think of it as the template from which our FBodyInstance is stamped. When you select an option in the “Collision” menu in the Static Mesh Editor UI, you are directly modifying the UBodySetup associated with that mesh.

FInitBodiesHelper is a somewhat opaque class which ultimately does what it says on the tin, and does the actual initialization work when FInitBodiesHelperBase::InitBodies() is called by FBodyInstance::InitBody(). First and most importantly, it calls FInitBodiesHelperBase::CreateShapesAndActors().

Regarding “Actors” and “Shapes”

To understand what FInitBodiesHelperBase::CreateShapesAndActors() does, we should first ask what these “Actors” and “Shapes” are that we’re creating. Briefly, a physics engine as often as possible would prefer to consider objects to be simple as possible. This simplifies the math a lot, but there’s only so far you can go with spheres and cubes.4 For anything more complicated, we need a “convex mesh”, which, much like in rendering, is a 3D “object” made out of triangles. Either way, pure sphere or mesh, these are the “Shapes” that are being created here - representations of our 3D geometry. “Shapes” in this sense are represented by two data types: FImplicitObject (and its inheritors) and FPerShapeData. FImplicitObject contains the actual geometric data about a shape - a sphere’s position and radius, for instance. FPerShapeData carries other important data about these shapes that isn’t their pure geometry, such as their Physical Material and their collision response channels.

“Actors” in this context are objects that hold collections of Shapes, and maintain data about their own position. These are FSingleParticlePhysicsProxy objects. It is my assumption that in this level of the engine, “actor” (as in FBodyInstance::CreateActor()) is roughly synonymous with “particle”. In general the term “actor” is not used at a lower level than in FBodyInstance. FSingleParticlePhysicsProxy objects are a pretty critical aspect of the physics framework, but they are primarily an interface to the two pointers they hold: TGeometryParticle and TGeometryParticleHandle. As we touched on before, TGeometryParticle is the game thread representation of a physics particle, while TGeometryParticleHandle is the physics thread representation. Both hold data about the particle’s position, and maintain an array of FPerShapeData objects. At this point, we are operating in the game thread, and only the TGeometryParticle is pertinent, or even set. The TGeometryParticleHandle will come into play later.

CreateShapesAndActors

The first thing that FInitBodiesHelperBase::CreateShapesAndActors() does is call UBodySetup::CreatePhysicsMeshes(), which loads the convex meshes for our not-so-simple physics shapes. Most of the time this will have been called for each UBodySetup as the level is loading, in which case it skips the work here. With confidence that the shape data is available, it then checks if the FBodyInstance has a parent, in which case they should be “welded” together. Welding adds a lot of complexity to our structures, but at the end of the day it is doing a lot of bookkeeping to add the “Shapes” of the child body to the parent body’s “Actor”. If it does indeed need to do some welding, it does that now and the function exits out here, because the weld logic handles setting up the actors and shapes. If it doesn’t need to weld, then the time for that is now: it calls FInitBodiesHelperBase::CreateActor_AssumesLocked() and then FInitBodiesHelperBase::CreateShapes_AssumesLocked(). CreateActor_AssumesLocked() creates our FGeometryParticle, and generates the FSingleParticlePhysicsProxy “Actor Handle” to hold the reference to it. CreateShapes_AssumesLocked adds the FPerShapeData and FImplicitObject entries associated with our geometry to the FGeometryParticle.

The Rest of InitBodies

Now that CreateShapesAndActors() is out of the way, we can move into the rest of FInitBodiesHelperBase::InitBodies(). Assuming that we did not do any welding (since that implies the parent body’s actor has already been added), it’s time to add our actors to the scene. We enter into a FPhysicsCommand::ExecuteWrite() lambda expression, which is a simple mutex pattern to lock our FPhysScene object, and do a few simple things. First it does a bit of bookkeeping for the bodies about collision traces and collision enabled enums. Then it calls FChaosScene::AddActorsToScene_AssumesLocked(), which calls FPBDRigidsSolver::RegisterObject() on our FSingleParticlePhysicsProxy - more on that later, but with this done, the actor is now Known by the physics engine and ready to be simulated. Finally, InitBodies() registers our actor for collision events with FPhysScene.

So to sum up, when AActors are created their components are initialized; if a component is a UPrimitiveComponent it initializes an FBodyInstance, which generates a physics particle FSingleParticlePhysicsProxy and its associated geometry, and then adds the particle to our physics representation via FPBDRigidsSolver.

Actor Destruction

With all that setup, I’d be remiss if I didn’t briefly discuss what happens when an actor is destroyed. Generally this will happen when UWorld::DestroyActor() is called. That function does quite a bit of bookkeeping, most of which the physics engine does not even remotely care about. What it does care about, though, is when it calls UActorComponent::UnregisterComponent(). This isn’t only called by actor destruction, but no matter how we reach it it spells the end of the line for our UPrimitiveComponent and its attached FBodyInstance. UnregisterComponent() calls ExecuteUnregisterEvents(), which calls DestroyPhysicsState(), which calls the virtual OnDestroyPhysicsState(). This is a no-op for a UActorComponent, but is overridden by UPrimitiveComponent to unweld the FBodyInstance from any parents or children and call FBodyInstance::TermBody(). This prompts the FPhysScene to do some of its bookkeeping, and finally culminates in FPBDRigidsSolver::UnregisterObject(), which manages all of the final bookkeeping to remove our FSingleParticlePhysicsProxy from any low level structures it could be cluttering up.

2. Scene Tick

In this section we’re going as deep as we need to into these caves until we find something that reflects our intuition of how any physics simulation would work: iterating over a list of objects and updating their positions.

If you’ve made it this far in such a dry post about the Unreal Engine you are hopefully familiar with the concept of Ticking. Back in GuardedMain() at the tippy-top of the call stack, the Unreal program is running FEngineLoop::Tick() as frequently as possible until the end of time. A whole lot of stuff happens in that function as it juggles the concerns of the renderer, statistics, and who knows what else, but smack in the middle of that function it calls UGameEngine::Tick(). Just like its parent, UGameEngine::Tick() has a lot of concerns, but most importantly to us is when it iterates over its list of UWorlds, calling UWorld::Tick() on each in turn.

UWorld::Tick() starts off slow with a lot of bookkeeping, but things start to heat up when it begins iterating over its ULevel collection. Here we find what may be some familiar enums if you’ve done any kind of advanced work with actor ticking: the “tick group” enums TG_PrePhysics, TG_DuringPhysics, and TG_PostPhysics, along with their less famous (but lowkey more important) brethren TG_StartPhysics and TG_EndPhysics. This is where UWorld executes tick handlers for each value in turn: all of the tick groups are purely for positioning the tick callbacks relative to the physics tick, as immediately as possible. Each of these steps are run with FTickTaskManager::RunTickGroup().

Briefly, FTickTaskManager manages asynchronous execution of its registered tasks, and allows tick functions to be registered in the various enumerated stages. This is precisely what UWorld does when it calls UWorld::SetupPhysicsTickFunctions() just a few lines up from the first RunTickGroup() call. This registers FStartPhysicsTickFunction and FEndPhysicsTickFunction with FTickTaskManager in the TG_StartPhysics and TG_EndPhysics groups respectively. Immediately after those are registered UWorld calls FTickTaskManager::StartFrame(), which queues up all of the registered tick functions, which are then dispatched in turn by each execution of RunTickGroup(). By default AActors tick first, in the TG_PrePhysics group. After that, in the TG_StartPhysics group, is where FStartPhysicsTickFunction gets executed. This is where the magic happens.

The Physics Frame

FStartPhysicsTickFunction::ExecuteTick() does only one thing, but it does it well: it calls FChaosScene::StartFrame(). StartFrame() starts off its day lazily, letting its friend FPhysicsReplication and anyone subscribed to its OnPhysScenePreTick or OnPhysSceneStep events that it’s totally, definitely about to get some work done today, and maybe they should get started as well.5 With all of that hard work done, it lets FPhysicsSolverBase know that it’s time to AdvanceAndDispatch_External(), and then tucks itself back into bed.

FPhysicsSolverBase::AdvanceAndDispatch_External() starts off with some deltatime bookkeeping, including some setup for physics substepping. After this it calls FPBDRigidsSolver::PushPhysicsState(). This is a critical step which starts the process of synchronizing the physics thread’s models of the particles with the game thread’s ahead of the calculations. Finally, it queues (or directly calls, if single-threaded) three consecutive tasks: FPhysicsSolverProcessPushDataTask, FPhysicsSolverFrozenGTPreSimCallbacks, and FPhysicsSolverAdvanceTask, which call ProcessPushData(), GTPreSimCallbacks(), and AdvanceSolver() respectively. We’ll revisit ProcessPushData() in the following section, and we’re going to gloss over GTPreSimCallbacks() - it is a fairly self explanatory callback entrypoint. We’ve got a golden idol to find, and AdvanceSolver() knows which way to go!

AdvanceSolver() kicks us over to AdvanceOneTimeStepTask::DoWork(), which first does a lot of work setting up various structures for the physics calculation, but critically calls FPBDRigidsEvolutionGBF::AdvanceOneTimeStep(), which is where the real, actual calculation is done for PBD physics simulation. I promised (both you and myself) that the details of that are out of scope, so if you’re looking to dive into the real nitty gritty of the physics simulation that is the place to do it. Suffice to say that this is where FPBDRigidsEvolutionGBF::Integrate() is called, which is where, for each particle in our FPBDRigidsSOAs, acceleration begets velocity and velocity begets position, just as the prophets foretold.

Phew! It took us a while and most of our ropes and bombs to spelunk down these caves, but finally we’ve hit paydirt and found that golden idol, and everything has moved one time step. Now all that’s left is to find our way back out again. That is to say, the physics engine has shuffled everything around as requested, but who’s going to let the game engine know? After all, we care about AActors, not TGeometryParticleHandles!

Push and Pull: Moving Data

Before we can talk about how we get information out of the physics engine, we need to talk about how information gets in. In the previous section we were chasing the critical path of logic, sniffing out which function calls actually led to x += v * dt. In this section we will take a closer look at the critical path of data: how inputs become outputs.

FChaosMarshallingManager is the unsung workhorse that keeps this entire operation running smoothly. Wielding FPushPhysicsData in one hand and FPullPhysicsData in the other, it deftly controls the flow of data between the game and physics threads. “Pushing” and “Pulling” in this context are from the point of view of the game thread: the physics engine is a black box that we “push” our inputs into, and “pull” the results out of.

FPushPhysicsData

Once things are underway, the physics engine has a pretty good idea of what’s going on in the scene. In many ways it is the canonical version of the scene - the other parts of Unreal have to take the physics engine on its word that the actor is in a different place than it was in the last tick. After all, it’s the one that’s keeping track of these crazy “forces” and “colliders”. Left to its own devices the physics engine is perfectly happy to keep plugging away without taking in any more input, but that is purely Simulation. We were promised Interaction! There are two situations where we need to Push to the physics simulation: when we add or remove something from the simulation, and when we directly modify something that is already in the simulation.

We’ve already talked about adding and removing actors to and from the scene. In that section we talked mostly about the structures that are created for the physics representation of an actor, and glossed over what FPBDRigidsSolver::RegisterObject() does after we call FChaosScene::AddActorsToScene(). Well I’m here now to tell you that it calls FPhysicsSolverBase::AddDirtyProxy(), which sticks the FSingleParticlePhysicsProxy we created into FPushPhysicsData’s DirtyProxiesDataBuffer.

Before we follow that thought further, let’s look at the other situation: directly modifying something already in the simulation. There are a lot of ways that this can be done in practice, so we’ll talk about one that I think is fairly elucidating: moving your character.

We’ll start specifically from ADefaultPawn::MoveForward(), which is what is called when you hold the W key. This function simply updates APawn’s ControlInputVector field, which is applied to APawn’s Velocity field when the Pawn Ticks (in TG_PrePhysics!), which triggers an update of the associated USceneComponents, resulting in OnUpdateTransform() being called for that component. And similarly to what we saw when registering our actors, if it is a UPrimitiveComponent rather than simply a USceneComponent, OnUpdateTransform() interacts with our FBodyInstance, calling FBodyInstance::SetBodyTransform(). This calls SetX() on the FSingleParticlePhysicsProxy, which changes the position value of the particle and then marks it as dirty, calling the same FPhysicsSolverBase::AddDirtyProxy() that we saw when we registered our actor above!6

PushPhysicsState

So no matter how gently we modify our game state, the end result as far as the physics engine is concerned is that we’ve “dirtied” one of its precious Physics Proxies. We come to the physics engine hat in hand with an itemized list of the toys we played with when we call FPBDRigidsSolver::PushPhysicsState(). For each item in the DirtyProxiesDataBuffer, it calls TGeometryParticle::SyncRemoteData(), which goes over the properties on the particle (such as its position) and calls FDirtyChaosProperties::SyncRemote() on them. If that particular property is dirty,7 it updates a value in the FDirtyPropertiesManager, a member of FPushPhysicsData.

ProcessPushData

Let’s recall our context here. UWorld::Tick() running the TG_StartPhysics group kicked off FPhysicsSolverBase::AdvanceAndDispatch_External(), which calls PushPhysicsState(), and then dispatches tasks to consecutively run ProcessPushData(), GTPreSimCallbacks(), and AdvanceSolver(), the latter of which we discussed in the previous section. It’s now time that we circle back on ProcessPushData().

ProcessPushData() does a bit of bookkeeping (notably applying physics-related console variables), but we are most interested in it calling FPBDRigidsSolver::ProcessPushedData_Internal(). This is where all of the data that FPushPhysicsData holds is consumed. The work that we are concerned with is performed in ProcessSinglePushedData_Internal(). This function iterates over PushData.DirtyProxiesDataBuffer (recalling that this is the collection of our dirty FSingleParticlePhysicsProxys), and first checks if the Proxy is uninitialized, which means we’ve just created it. If it is, then this is where it generates our TGeometryParticleHandle - the physics thread’s representation of the particle - and attaches it to the Proxy. In this case, the particle still does not exist in any of the SOA structures, but this is soon resolved by calling FPBDRigidsSOAs::CreateStaticParticles(). After this, whether it’s new or not, it calls FSingleParticlePhysicsProxy::PushToPhysicsState(). This is where the Properties that we dirtied are applied to the physics model: by calling functions like FPDBRigidsEvolutionGBF::SetParticleTransform(), which ultimately calls TGeometryParticleHandle::X(), we move the “dirty” property information from the FDirtyPropertiesManager - set from the game thread’s model of the particle - into the physics thread’s model (FPBDRigidsSOAs) of the particle.

And that’s as far as we’re following the data down the rabbit hole! Content in the knowledge that our updates made it into FPBDRigidsSOAs, we can ignore the screams as they are processed by FPDBRigidsEvolutionGBF; what the physics engine does behind closed doors is its own business.

FPullPhysicsData

Once the precocious physics gremlin in the black box is done with whatever he does in there, he slides out the results using FPullPhysicsData. This structure holds a collection of all of the changes made in a particular simulation step. Every single thing that moved in this physics frame needs to be notified so we can change those shiny glowing pixels on the screen!

Let’s recenter ourselves again. We were just discussing the ProcessPushData() task that is kicked off by FPhysicsSolverBase::AdvanceAndDispatch_External(), preceding the AdvanceSolver() task. Let’s zoom back in on AdvanceSolver() now, all the way down to AdvanceOneTimeStepTask::DoWork(), where we’ve just executed FPBDRigidsEvolutionGBF::AdvanceOneTimeStep(), which was where the actual Integration for the physics simulation happens. The physics engine has done it’s work, now all that’s left to do is pull the results out. DoWork() calls some functions to clean up after itself, and finally calls FPBDRigidsSolver::CompleteSceneSimulation(). This function is responsible for assembling our results to be consumed via FPBDRigidsSolver::BufferPhysicsResults().8 BufferPhysicsResults() fetches the “dirty particles” (this time “dirty” is from the game thread’s point of view) using FPBDRigidsSOAs::GetDirtyParticlesView() and populates FPullPhysicsData::DirtyRigids() with the position and velocity information for each dirty particle by calling FSingleParticlePhysicsProxy::BufferPhysicsResults(). Finally, back up in FPBDRigidsSolver::AdvanceSolverBy(), FChaosMarshallingManager::PreparePullData() is called to enqueue our simulation step’s PullData to be consumed.

Now that our FPullPhysicsData has been populated and enqueued, we can crawl up (down?) the call stack all the way back to UWorld::Tick(). We’re finally done with the TG_StartPhysics tick group! Take a deep breath, the air is much cleaner up here. Don’t dawdle too long though, because with that done we’re immediately diving right back down into the TG_EndPhysics tick group!

Mirroring its brother Start, FEndPhysicsTickFunction simply calls FChaosScene::EndFrame(). And much like how StartFrame() iterated over each Solver calling AdvanceAndDispatch_External(), EndFrame() iterates over each Solver calling FChaosScene::SyncBodies().

The work of SyncBodies() is handled by FPhysScene_Chaos::OnSyncBodies(), which indulges in some indirection by passing a lambda to FPhysicsSolverBase::PullPhysicsStateForEachDirtyProxy_External(). This function, with some help from FChaosResultsManager,9 contains logic for interpolating the queue of physics results in an asynchronous setting. This is reasonably well-commented, which is good because I’m going to gloss over it. In the case where we are not interpolating, it simply takes the latest update’s FPullPhysicsData with FChaosResultsManager::PullSyncPhysicsResults() and assembles a collection of transforms for the UPrimitiveComponents that own the associated dirty Proxy. Finally, OnSyncBodies() iterates over this collection, calling UPrimitiveComponent::MoveComponent() on each component based on those collected transforms.

And with that we’re back in familiar territory! It is reassuring that the culmination of all of the physics calculation is to manipulate our AActors by calling a function that we as the API consumer might call ourselves. After those MoveComponent() calls, UWorld::Tick() is done with the TG_EndPhysics group, then executes the TG_PostPhysics group, and gets on with its day. Then it starts the whole process over a few milliseconds later!

3. Querying

After trawling through the entire physics frame, this section should be a fairly straightforward tying of loose ends. The UPrimitiveComponent knowing where it is after an update suffices for a lot of what we need from the physics engine - transformation information reaches the rendering engine without any input necessary from us as the developer, and we can directly manipulate and react to the positions of our AActors in our game logic without having to worry about the machinery we’ve spent the last 5000 or so words exploring. However, while one could approximate the results of a collision event handler or tracing with polling or something, that would be less performant for a worse result than simply tying into the calculations that are already being done by the physics engine!

Tracing, Sweeping, Overlapping

Line tracing, also known as ray casting (but slightly less overloaded a term), refers to the concept of defining a start and end point in space, constructing a line between them, and finding which geometric objects in the scene intersect with that line. “Sweeping” refers to the same concept, but rather than checking for intersections with a mathematically pure line segment, we are checking for intersections with a volume that corresponds to a 3-dimensional shape being “swept” through space along that line segment. Like dragging a spoon across the top of a fresh carton of ice cream, rather than a needle. “Overlapping” is perhaps the most self explanatory of the three, checking if several geometric objects occupy the same space.

UWorld, finishing off its hat-trick of being our entrypoint to the physics framework, provides our interface for traces, sweep, and overlap queries. UWorld contains an absolute mound of functions for sweeps, traces, and overlaps, with plenty of options for filtering the query by collision channel or object type, for getting lists of intersections or just the first, or for doing it asynchronously vs synchronously. The good news is that all of these calls, after going through several levels of templating or task manager shenanigans, end up in one of two places: TSceneCastCommonImp()10 for traces and sweeps, and GeomOverlapMultiImp() for overlaps.

Slightly disappointingly, the bottleneck of TSceneCastCommonImp() branches back out through more template shenanigans to LowLevelRaycast or LowLevelSweep via TSQTraits::SceneTrace(), the core function called by TSceneCastCommonImp(). Meanwhile GeomOverlapMultiImp() calls LowLevelOverlap. These three LowLevel functions are fairly similar, fetching the “spatial acceleration structure” that is maintained by FPBDRigidsEvolutionGBF, the bottom level of our physics stack as we’ve explored, and calling Raycast(), Sweep(), or Overlap() to query it.

The implementation details of the spatial acceleration structure, like the details of PBD simulation, fall outside of the scope of this blog post. I hope that you will be content, as I am, in seeing the connection between our UWorld trace API and FPBDRigidsEvolutionGBF, the class wherein we found the physics integration calculations.

Collision Events

Whether through blueprints or the C++ API, collision events are propagated primarily via the OnComponentHit and OnComponentBeginOverlap delegates defined by UPrimitiveComponent. There are other physics-related delegates defined alongside them, but we’ll be focusing on these two, since they’re the most generally useful, and the ones that are visible via the Details editor.

OnComponentBeginOverlap

OnComponentBeginOverlap and OnComponentEndOverlap get broadcast by UPrimitiveComponent::UpdateOverlaps() calling UPrimitiveComponent::BeginComponentOverlap() or UPrimitiveComponent::EndComponentOverlap() respectively. UpdateOverlaps() can be called from AActor::UpdateOverlaps(), primarily by edge cases such as on level load or if the AActor’s collision-relevant state is changed at runtime - situations that would call for immediate refreshing of overlap events. The more general and frequent case where UPrimitiveComponent::UpdateOverlap() is called is toward the end of UPrimitiveComponent::MoveComponent(), the same function that is called at the end of our physics frame to reflect the simulation step.

The most significant question in the scope of this blog post is how we assemble the list of components that we’re calling BeginComponentOverlap() on. UpdateOverlaps() takes a list of pending overlaps as a parameter. If it’s executed from AActor::UpdateOverlaps(), this will be null. It will only be non-null if it’s executed from MoveComponent() with the bSweep parameter set to true, for instance such as when calling AActor::SetActorLocation(). In this case it does a sweep query and uses the results as the pending overlaps. Most frequently though bSweep will be false, particularly when MoveComponent() is called by FPhysScene_Chaos::OnSyncBodies().

All of these pending overlaps are iterated over in UPrimitiveComponent::UpdateOverlaps(), broadcasting OnComponentBeginOverlap events. After that, whether there were pending overlaps or not, it calls UPrimitiveComponent::ComponentOverlapMulti() on itself, which ends up at the same LowLevelOverlap that we saw in the trace/sweep section. Some work is done comparing the results of that overlap query, the overlaps that were passed in, and a list of overlapping components frame the previous frame to determine which components still need to trigger OnComponentBeginOverlap, and which components now need to trigger OnComponentEndOverlap.

That’s all there is to it! Whether via physics simulation or manual movement, the primitive components manage overlap event handling via the UpdateOverlaps() function.

OnComponentHit

While OnComponentHit is defined in and pertains specifically to UPrimitiveComponent, it is in fact only ever Broadcast by AActor member functions. Obnoxiously, there are two nearly identical functions in AActor that dispatch hit events, used in different contexts: DispatchPhysicsCollisionHit() and DispatchBlockingHit(). In both cases, it calls NotifyHit() (which calls the BlueprintImplementableEvent ReceiveHit), then broadcasts the AActor::OnActorHit() and UPrimitiveComponent::OnComponentHit() events.

DispatchPhysicsCollisionHit

AActor::DispatchPhysicsCollisionHit() is the most frequent path that broadcasts the component hit event. It is executed byFPhysScene_Chaos::HandleCollisionEvents(), which is an event that is dispatched in FChaosScene::EndFrame() immediately after SyncBodies() is called.

FPhysScene_Chaos::HandleCollisionEvents() consumes FCollisionEventData events, which are generated by FEventDefaults::RegisterCollisionEvent(). We did not and will continue to not look closely at FEventManager or FEventDefaults, except to see that RegisterCollisionEvent() is executed when AdvanceOneTimeStepTask::DoWork() calls FEventManager::FillProducerData() shortly after calling FPBDRigidsEvolutionGBF::AdvanceOneTimeStep(). The particulars of how the collisions are detected dives headfirst into FPBDRigidsEvolutionGBF’s inner workings, so we will not be covering them. Suffice to say that the collisions are accumulated almost immediately after the simulation step is completed, and then are propagated while ending the physics frame.

Something that is worth covering, however, is the work that FPhysScene_Chaos does to manage these collision events. It only compiles collision events if they are being listened for, which it manages with FPhysScene_Chaos::RegisterForCollisionEvents(). This is called by FInitBodiesHelperBase::InitBodies(), right after FChaosScene::AddActorsToScene_AssumesLocked()! It is also called, naturally, if we start or stop listening to collision events at runtime, such as with UPrimitiveComponent::SetNotifyRigidBodyCollision().

DispatchBlockingHit

AActor::DispatchBlockingHit() is a more “general purpose” way to dispatch a hit event on an AActor than DispatchPhysicsCollisionHit. Its signature is notably different from that of DispatchPhysicsCollisionHit(): while that function takes physics-specific structs FRigidBodyCollisionInfo and FCollisionImpactData and uses those to determine the UPrimitiveComponents and the FHitResult arguments for the event, DispatchBlockingHit() directly takes pointers to UPrimitiveComponents and passes a provided FHitResult without modification. This makes it the natural choice if you wanted to manually dispatch a hit event on an AActor. Other than that, it is used for “scoped movement” or if UPrimitiveComponent::MoveComponent() is called with the bSweep parameter set to true and a blocking collision occurs, such as when calling AActor::SetActorLocation(), just like we saw for overlaps.

And that about does it for querying. While it’s not as all-encompassing as actor representations or simulation ticks, it did find its way into some familiar locales!

Conclusion

And that is all about all I have to say about the physics framework! We got an idea for how the physics engine conceptualizes of our actors, we dug down in the tick functions until we found actual physics-related math, and we figured out how that physics-related math bubbles back up into the engine. I sincerely hope that this blog post is helpful in some way, or at the very least helps you know where to stick a breakpoint for your own investigation. This was a hell of an undertaking for me. It is incredible how much time I spent on this, and I still feel like I only scratched the surface. Unreal is truly an enormous codebase, and you can really only get an idea of its size by nearly choking on too big a bite of it. Nevertheless I think this was a worthwhile exercise, I learned quite a bit about C++ without the kid gloves of Unreal’s API, and I feel much more empowered to spelunk in the engine’s depths. And if you got anything out of it as well then it was doubly worthwhile.

Special thanks to Micah Johnston for proofreading and emotional support.

Thanks so much for reading! Follow me on social media:

Footnotes

Click the carat ^ to return to the text.

  1. ^ You may notice directly under PhysicsScene that there are two more pointers to FPhysScene_Chaos objects. These are never used in practice, and are likely a holdover from the PhysX to Chaos migration.
  2. ^ This isn’t quite true, for instance Physics Constraint Components do not inherit from UPrimitiveComponent (although they do interact with them!), but it is true enough for our exploration.
  3. ^ Pedantically, the engine loads a dummy world during start up before the real map is loaded.
  4. ^ Chaos has four “primitive” types of shapes: Sphere, Box, “Sphylinder” (capsule), and “Tapered Capsule”. Unreal 5.1.0 added a new type, a Level Set - we will not be looking at these. Everything else is a “Convex Shape”. This is a very interesting blog post by a PhysX developer on why thet did not add a “Cylinder” shape to PhysX, which is likely why Chaos does not have it either!
  5. ^ It also calls FPhysScene_Chaos::UpdateKinematicsOnDeferredSkelMeshes() here, which seems pretty important for skeletal physics but is out of my scope.
  6. ^ Another example of an interaction might be calling AddImpulse() on a primitive component. This takes a different path (winding through FPhysScene_Chaos and FBodyInstance) but reaches a similar place to our direct movement example, culminating in AddDirtyProxy() being called the same way.
  7. ^ The “Property” that is dirty is a separate object than the “Proxy” that is added to FPushPhysicsData’s dirty buffer, but it is marked dirty in the same place as the proxy when we’re updating an actor like in the player movement case. Naturally, we’re not dirtying a property when we register the actor for the first time in the spawning case, so we can have a dirty proxy without having a dirty property, but not the other way around.
  8. ^ First CompleteSceneSimulation() calls FPBDRigidDirtyParticlesBuffer::BufferPhysicsResults() (distinct from FPBDRigidsSolver’s BufferPhysicsResults()), which assembles the particles into FPBDRigidDirtyParticlesBufferOut->DirtyGameThreadParticles, an array of FSingleParticlePhysicsProxy objects. However, I can’t find where this is ever used.
  9. ^ FChaosResultsManager is a helper class owned by FPBDPhysicsSolver which manipulates pull data from FChaosMarshallingManager for this step.
  10. ^ One item of note in TSceneCastCommon() is a conditional selecting between templated functions based on whether the thread context is the Game thread or the Physics thread. The two paths are identical except for whether Traits or PTTraits is used for the Traits template parameter, although what that difference means is not something I investigated.