We implement our own physics based on Bullet in UE4. Part 1


I recently ran into a non-trivial task that I needed to solve while working on our next (as yet unannounced) game. I needed deterministic physics. That is, I needed to be sure that if I take a well-defined initial state and apply the same forces to it within the simulation, the result will be always the same.

In general, it is extremely difficult to do this not only for different platforms, but even within the same platform for different binaries. So I had to simplify my end goal a bit: given the same build on the same platform, the results should be deterministic. This less ambitious goal is already, of course, technically achievable.

The UE uses PhysX (as does Unity), but the way it does it doesn’t support determinism. For a physics engine to be deterministic, absolutely everything must be predictable:

  • The simulation should only progress in fixed time intervals.

  • All interactions must be resolved in a well-defined, reproducible order.

PhysX can be deterministic if you use fixed time intervals and enable “enhanced determinism” which means contacts are resolved in a non-random order. You can enable this mode in the UE settings, but you will not be able to implement simulation at fixed time intervals without making changes to the source code of the engine. Epic decided that they would run the simulation at framerate, with the ability to do sub-steps as needed, but the simulation would always be frame-synced; this means that even if you implement an intermediate step with fixed time intervals, the final time “slice” (slice) will be variable, since it depends on the frame rate. it rules out any determinism.

I would prefer to use this framerate based time span as forecast, but as an actual simulation step, but UE doesn’t give us that option. We have various hooks for physics ticks in the engine, but we will not be able to solve this problem without changing the UE source code, which I really did not want to do.

I quickly came to the conclusion that the only way to do this cleanly was to plug in my own physics simulation. Although I could create another parallel running PhysX scene, I decided to try doing it with Bullet based on the following considerations:

  • I could be 100% sure that the UE would not interfere with her work.

  • Bullet is a good built-in system for interpolating fixed ticks of a variable frame rate simulation.

  • Bullet supports sliding and rolling friction, which initially interested me a lot (but ended up falling short of my expectations)

Luckily, since UE gives you access to C++ internals, integrating a third party engine isn’t too hard, and this story is about that.

Bullet Assembly

There are a few nuances to how you should build Bullet for UE compatibility:

DLL version of Microsoft C++ Runtime Library (CRT)

Bullet premake builds use a static CRT by default, which will cause conflicts if you try to link them to a UE module. I sent a PR (already frozen) to add this feature, however later it turned out that I needed another option that the Premake version did not support. So I switched to a CMake build that already supports this option.

Release version of Microsoft C++ Runtime Library (CRT) in debug

UE uses the release version of the CRT even in debug builds, so if you link a debug version of the Bullet library that doesn’t match this, it will be unhappy. But you you can instruct the UE module to use the debug CRT instead (option bDebugBuildsActuallyUseDebugCRT = true in your build.cs file), but it’s actually better to use the release version of the CRT, because you probably won’t need to debug it.

So I added this as an option to the CMake build for Bullet to keep things consistent.

Assembly Instructions

  1. Run UI CMake

  2. It doesn’t matter what you choose for “Where to build the binaries”

  3. Check parameter USE_MSVC_RUNTIME_LIBRARY_DLL

  4. Check parameter USE_MSVC_RELEASE_RUNTIME_ALWAYS

  5. Choose a location for LIBRARY_OUTPUT_PATH inside your UE project

  6. Click the configure button

  7. Click Generate

  8. Click Open Project

  9. In VS select Build > Batch Build

After that, you should have static libraries in LIBRARY_OUTPUT_PATH/Debug|Release|RelWithDebInfo.

Adding to the assembly of your UE project

As with any C++ project, you need access to header files and libraries. Unfortunately, Bullet doesn’t separate its header and cpp files, so it’s easiest to make sure you have access to the source code. Personally, I have Bullet as a submodule inside my project, and my CMake “where to build” setting is set outside of the project’s monitored directory in order to avoid problems with the cleanup.

Then you need to edit Project.Build.cs by adding the header file and library folders. It should look something like this:

public class MyProject : ModuleRules
{
	public MyProject(ReadOnlyTargetRules Target) : base(Target)
	{
        // Эта часть вполне стандартная, но ваша может быть больше
		PCHUsage = PCHUsageMode.UseExplicitOrSharedPCHs;
		PublicDependencyModuleNames.AddRange(new string[] { "Core", "CoreUObject", "Engine", "InputCore" });
		PrivateDependencyModuleNames.AddRange(new string[] {  });
		AddBullet();
	}
    /// Помощник для предоставления нам ProjectRoot/ThirdParty
	private string ThirdPartyPath
    {
        get { return Path.GetFullPath( Path.Combine( ModuleDirectory, "../../ThirdParty/" ) ); }
    }
	protected AddBullet() 
	{
        // Тут все достаточно базово, только для одной платформы и конфигурации (Win64)
        // Если вы планируете большее количество вариантов сборки, то здесь вам придется проделать дополнительную работу 
        bool bDebug = Target.Configuration == UnrealTargetConfiguration.Debug || Target.Configuration == UnrealTargetConfiguration.DebugGame;
        bool bDevelopment = Target.Configuration == UnrealTargetConfiguration.Development;
        string BuildFolder = bDebug ? "Debug":
            bDevelopment ? "RelWithDebInfo" : "Release";
        string BuildSuffix = bDebug ? "_Debug":
            bDevelopment ? "_RelWithDebugInfo" : "";
        // Путь к библиотеке
        string LibrariesPath = Path.Combine(ThirdPartyPath, "lib", "bullet", BuildFolder);
        PublicAdditionalLibraries.Add(Path.Combine(LibrariesPath, "BulletCollision" + BuildSuffix + ".lib")); 
        PublicAdditionalLibraries.Add(Path.Combine(LibrariesPath, "BulletDynamics" + BuildSuffix + ".lib")); 
        PublicAdditionalLibraries.Add(Path.Combine(LibrariesPath, "LinearMath" + BuildSuffix + ".lib")); 
        // Путь для инклюда (здесь я просто использую исходники, так как у Bullet src и заголовки смешаны)
        PublicIncludePaths.Add( Path.Combine( ThirdPartyPath, "bullet3", "src" ) );
        PublicDefinitions.Add("WITH_BULLET_BINDING=1");
    }
}

Inclusion of Bullet in UE sources

One more nuance – there are several differences from the UE build environment that will cause problems if you make a banal #include Bullet in your UE code.

Whenever you include a Bullet header, you need to surround it with custom UE macros:

// Этот необходим для подавления некоторых варнингов, которые передает UE4, а Bullet нет
THIRD_PARTY_INCLUDES_START
// Этот необходим для исправления проблем с выравниванием данных в памяти
PRAGMA_PUSH_PLATFORM_DEFAULT_PACKING
#include <btBulletDynamicsCommon.h>
PRAGMA_POP_PLATFORM_DEFAULT_PACKING
THIRD_PARTY_INCLUDES_END

Memory alignment is especially tricky because it’s supposed to work without a macro, but you’ll keep getting random memory errors because Bullet and UE order data differently.

Personally, I got around this by creating wrapper headers that do all this for me so I don’t have to remember it anymore:

BulletMinimal.h (for inclusion in headers)

This header includes the basic value types (vectors, etc.) and the headers for the classes we inherit from (see below), along with forward declarations for everything we point to. So it’s perfect for including in our UE header files that need to pass Bullet data.

#pragma once
#include "CoreMinimal.h"

// Самый минимальный инклюд для Bullet, который нам необходим в заголовках для типов значений/подклассов
// Этот макрос необходим для подавления некоторых варнингов, которые передает UE4, а Bullet нет
THIRD_PARTY_INCLUDES_START

// Этот макрос необходим для исправления проблем с выравниванием данных в памяти
PRAGMA_PUSH_PLATFORM_DEFAULT_PACKING

// Типы значений
#include <LinearMath/btQuaternion.h>
#include <LinearMath/btTransform.h>
#include <LinearMath/btVector3.h>

// Основные вещи, которые мы переопределяем
#include <LinearMath/btDefaultMotionState.h>
#include <LinearMath/btIDebugDraw.h>

// Предварительные объявления для всего остального, что мы используем
class btCollisionConfiguration;
class btCollisionDispatcher;
class btBroadphaseInterface;
class btConstraintSolver;
class btDynamicsWorld;
class btCollisionShape;
class btBoxShape;
class btCapsuleShape;
class btConvexHullShape;
class btCompoundShape;
class btSphereShape;
class btRigidBody;
class btCollisionObject;
PRAGMA_POP_PLATFORM_DEFAULT_PACKING
THIRD_PARTY_INCLUDES_END

BulletMain.h (for inclusion in sources)

This header is what I include in source files that need to call Bullet in full. This is a shorter title, but it (indirectly) includes the most.

#pragma once
#include "CoreMinimal.h"

// Более полное включение Bullet

// Этот макрос необходим для подавления некоторых варнингов, которые передает UE4, а Bullet нет
THIRD_PARTY_INCLUDES_START
// Этот макрос необходим для исправления проблем с выравниванием данных в памяти
PRAGMA_PUSH_PLATFORM_DEFAULT_PACKING
#include <btBulletDynamicsCommon.h>
#include <BulletCollision/CollisionShapes/btBoxShape.h>
PRAGMA_POP_PLATFORM_DEFAULT_PACKING
THIRD_PARTY_INCLUDES_END

Okay, we’re done with the preparations. Let’s finally start using Bullet.

Principles of use

I wasn’t trying to completely replace UE collisions and physics, much less pretend to be a simplified replacement that should work for everything. I always advocate simplicity, so I only implemented what I needed.

In my case, I know exactly what objects are in my simulation ahead of time, and I have an Actor that “owns” them all. Therefore, I implemented almost all of my physics integration in that single actor that controlled the life cycle of everything else.

This actor also acted as a local simulation center, so no matter where it was, the Bullet simulation ran locally around it, which helped maintain accuracy (which meant I only needed single precision).

It’s important to note that while all UE objects had physics disabled, they still had PhysX colliders and I continued to use UE collisions for non-physics interactions like overlap triggers. This allowed me to limit the scope to only the physical simulation.

Of course, you can develop this idea into a more complete solution if you wish, but I decided to keep it as simple as possible.

Actor “Physics World”

As mentioned above, almost all the implementation is in a single actor.

Data storage

That’s pretty much all of the Bullet world data we have in one place. Again, due to the simplicity of my requirements, there are no (direct) references to Bullet in any other actors. Everything is exclusively here because this actor owns all the objects in this part of the world. You will see later how it all fits together.

...
#include "BulletMinimal.h"
...

class MYPROJECT_API APhysicsWorldActor : public AActor
{
    ...

	// Раздел Bullet
    // Глобальные объекты
	btCollisionConfiguration* BtCollisionConfig;
	btCollisionDispatcher* BtCollisionDispatcher;
	btBroadphaseInterface* BtBroadphase;
	btConstraintSolver* BtConstraintSolver;
	btDynamicsWorld* BtWorld;
    // Пользовательский интерфейс дебага
	btIDebugDraw* BtDebugDraw;
    // Динамические тела
	TArray<btRigidBody*> BtRigidBodies;
    // Static colliders// Статические коллайдеры
	TArray<btCollisionObject*> BtStaticObjects;
    // Повторно используемые формы коллизий
	TArray<btBoxShape*> BtBoxCollisionShapes;
	TArray<btSphereShape*> BtSphereCollisionShapes;
	TArray<btCapsuleShape*> BtCapsuleCollisionShapes;

	// Структура для хранения реюзабельных ConvexHull-форм на основе исходных BodySetup / subindex / scale
	struct ConvexHullShapeHolder
	{
		UBodySetup* BodySetup;
		int HullIndex;
		FVector Scale;
		btConvexHullShape* Shape;
	};

	TArray<ConvexHullShapeHolder> BtConvexHullCollisionShapes;
	// Эти формы предназначены для *потенциально* составных твердых фигур
	struct CachedDynamicShapeData
  
	{
		FName ClassName; // имя класса для кэша
		btCollisionShape* Shape;
		bool bIsCompound; // если true, это составная фигура, поэтому ее нужно удалять
		btScalar Mass;
		btVector3 Inertia; // потому что мы бы хотели предварительно вычислять это
	};
	TArray<CachedDynamicShapeData> CachedDynamicShapes;
    ...

};    

Everything that you see here, I will talk about a little later, so do not worry if something is not clear to you.

I decided to store everything in simple arrays. When I need to find something, I just iterate over them. I know that my world size is small, and that most of the work is done immediately after loading, so more complex data access patterns are not required here. But you are entitled to your opinion on this matter.

World initialization

First of all, let’s load the Bullet world.

// Это довольно стандартная загрузка Bullet
	BtCollisionConfig = new btDefaultCollisionConfiguration();
	BtCollisionDispatcher = new btCollisionDispatcher (BtCollisionConfig);
	BtBroadphase = new btDbvtBroadphase ();
	BtConstraintSolver = new btSequentialImpulseConstraintSolver();
	BtWorld = new btDiscreteDynamicsWorld (BtCollisionDispatcher, BtBroadphase, BtConstraintSolver, BtCollisionConfig);
	// Я повозился с несколькими настройками в BtWorld->getSolverInfo(), но они специфичны для моих нужд
	// Вектор гравитации в наших единицах (1=1 см)
	BtWorld->setGravity(BulletHelpers::ToBtDir(FVector(0, 0, -980)));

All of this is pretty harmless. AT there is nothing in our world yet, but everything is already set up and knows in which direction gravity acts.

But wait, what is this BulletHelpers?

It’s just a converter of UE units to Bullet units (and vice versa). Bullet uses 1 = 1 m while UE uses 1 = 1 cm. To achieve realistic physics, we need to use the correct units. As I mentioned earlier, I’m running Bullet as a local simulation relative to the Physics World actor so I can maintain maximum accuracy, and that’s true for BulletHelpers as well. I don’t think there’s much point in pointing this out because it’s simple math, but I’ll demonstrate it at the end of the article.

That’s all for now. In the next part, we will talk about colliders and rigid bodies.


We invite you to an open lesson “Peculiarities of MMO development on Unreal Engine”, where we will consider the features and tools provided by Unreal Engine when developing MMOs. Registration via link.

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *