Where does Unreal calculate
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!).
There are two fundamental concepts that a game developer expects from any physics engine:
- Simulation - Tracking and updating positions of actors in the scene, both in isolation and in relation to other actors (i.e. collisions)
- 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:
- Actor Registration - Initializing the physics engine’s model of the scene, as well as adding/removing new actors to/from the model
- Ticking/Input - Prompting the physics engine to advance the simulation, taking into account any changes from user input, and handling the results
- 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.
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.
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
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
ULevel does. That is in fact
ULevel’s primary purpose. But
ULevels don’t exist without a
ULevel is owned by
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:
- Actor Registration:
- Scene Ticking:
UWorld::Tick()is responsible for kicking off every physics simulation step.
UWorldcontains 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:
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 (inheriting from
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
EndFrame(). It also creates and owns
An honorable mention is due for
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 (inheriting from
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 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
TPBDGeometryCollectionParticles. These Structures hold the Arrays that encode the data (position, mass, geometry, etc) for all of the particles in our scene.
FChaosMarshallingManager manages queues of
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 (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.
This section covers the classes that represent the objects in motion - the things that are being manipulated by the classes in the previous section.
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 is the parent class for components that contain geometry. This includes the familiar
USkeletalMeshComponent, which we might use solely for their rendered geometry, but also
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
FBodyInstance member variable.
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 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
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),
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.
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
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.
InitGamePhys(). At one point this kicked off a lot of boilerplate for PhysX setup, but nowadays it simply loads the
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::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
So the engine initializes our modules and starts up 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
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
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
ULevel to the
AActors to their
For the latter case of registration on spawn,
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() is where
FBodyInstance comes to life, with the help of its dark underlings,
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
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
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
FSingleParticlePhysicsProxy objects are a pretty critical aspect of the physics framework, but they are primarily an interface to the two pointers they hold:
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.
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
CreateActor_AssumesLocked() creates our
FGeometryParticle, and generates the
FSingleParticlePhysicsProxy “Actor Handle” to hold the reference to it.
CreateShapes_AssumesLocked adds the
FImplicitObject entries associated with our geometry to the
The Rest of InitBodies
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
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
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
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
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_PostPhysics, along with their less famous (but lowkey more important) brethren
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 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
FTickTaskManager in the
TG_EndPhysics groups respectively. Immediately after those are registered
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
StartFrame() starts off its day lazily, letting its friend
FPhysicsReplication and anyone subscribed to its
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:
FPhysicsSolverAdvanceTask, which call
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
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.
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
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
ControlInputVector field, which is applied to
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
OnUpdateTransform() interacts with our
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
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
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
AdvanceSolver(), the latter of which we discussed in the previous section. It’s now time that we circle back on
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.
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
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
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
FEndPhysicsTickFunction simply calls
FChaosScene::EndFrame(). And much like how
StartFrame() iterated over each Solver calling
EndFrame() iterates over each Solver calling
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
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
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!
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
TSQTraits::SceneTrace(), the core function called by
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
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.
Whether through blueprints or the C++ API, collision events are propagated primarily via the
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.
OnComponentEndOverlap get broadcast by
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
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
All of these pending overlaps are iterated over in
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
That’s all there is to it! Whether via physics simulation or manual movement, the primitive components manage overlap event handling via the
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:
DispatchBlockingHit(). In both cases, it calls
NotifyHit() (which calls the BlueprintImplementableEvent
ReceiveHit), then broadcasts the
AActor::DispatchPhysicsCollisionHit() is the most frequent path that broadcasts the component hit event. It is executed by
FPhysScene_Chaos::HandleCollisionEvents(), which is an event that is dispatched in
FChaosScene::EndFrame() immediately after
SyncBodies() is called.
FCollisionEventData events, which are generated by
FEventDefaults::RegisterCollisionEvent(). We did not and will continue to not look closely at
FEventDefaults, except to see that
RegisterCollisionEvent() is executed when
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
AActor::DispatchBlockingHit() is a more “general purpose” way to dispatch a hit event on an
DispatchPhysicsCollisionHit. Its signature is notably different from that of
DispatchPhysicsCollisionHit(): while that function takes physics-specific structs
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!
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:
Click the carat ^ to return to the text.
- ^ You may notice directly under
PhysicsScenethat there are two more pointers to
FPhysScene_Chaosobjects. These are never used in practice, and are likely a holdover from the PhysX to Chaos migration.
- ^ 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.
- ^ Pedantically, the engine loads a dummy world during start up before the real map is loaded.
- ^ 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!
- ^ It also calls
FPhysScene_Chaos::UpdateKinematicsOnDeferredSkelMeshes()here, which seems pretty important for skeletal physics but is out of my scope.
- ^ Another example of an interaction might be calling
AddImpulse()on a primitive component. This takes a different path (winding through
FBodyInstance) but reaches a similar place to our direct movement example, culminating in
AddDirtyProxy()being called the same way.
- ^ 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.
- ^ First
BufferPhysicsResults()), which assembles the particles into
FPBDRigidDirtyParticlesBufferOut->DirtyGameThreadParticles, an array of
FSingleParticlePhysicsProxyobjects. However, I can’t find where this is ever used.
FChaosResultsManageris a helper class owned by
FPBDPhysicsSolverwhich manipulates pull data from
FChaosMarshallingManagerfor this step.
- ^ 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
PTTraitsis used for the
Traitstemplate parameter, although what that difference means is not something I investigated.