648 lines
16 KiB
C++
648 lines
16 KiB
C++
// Copyright (c) 2012-2021 Wojciech Figat. All rights reserved.
|
|
|
|
#include "Physics.h"
|
|
#include "Engine/Core/Log.h"
|
|
#include "Engine/Threading/Threading.h"
|
|
#include "Engine/Platform/CPUInfo.h"
|
|
#include "PhysicsSettings.h"
|
|
#include "Utilities.h"
|
|
#include "PhysicsStepper.h"
|
|
#include "SimulationEventCallback.h"
|
|
#include "Engine/Level/Level.h"
|
|
#include "Actors/PhysicsActor.h"
|
|
#include "Engine/Profiler/ProfilerCPU.h"
|
|
#include "Engine/Core/Memory/Memory.h"
|
|
#include "Engine/Engine/EngineService.h"
|
|
#include <ThirdParty/PhysX/PxPhysicsAPI.h>
|
|
#include <ThirdParty/PhysX/PxActor.h>
|
|
#if WITH_PVD
|
|
#include <ThirdParty/PhysX/pvd/PxPvd.h>
|
|
#endif
|
|
|
|
// Temporary memory size used by the PhysX during the simulation. Must be multiply of 4kB and 16bit aligned.
|
|
#define SCRATCH_BLOCK_SIZE (1024 * 128)
|
|
|
|
class PhysXAllocator : public PxAllocatorCallback
|
|
{
|
|
public:
|
|
|
|
void* allocate(size_t size, const char* typeName, const char* filename, int line) override
|
|
{
|
|
return Allocator::Allocate(size, 16);
|
|
}
|
|
|
|
void deallocate(void* ptr) override
|
|
{
|
|
Allocator::Free(ptr);
|
|
}
|
|
};
|
|
|
|
class PhysXError : public PxErrorCallback
|
|
{
|
|
public:
|
|
|
|
PhysXError()
|
|
{
|
|
}
|
|
|
|
~PhysXError()
|
|
{
|
|
}
|
|
|
|
public:
|
|
|
|
// [PxErrorCallback]
|
|
void reportError(PxErrorCode::Enum code, const char* message, const char* file, int line) override
|
|
{
|
|
LOG(Error, "PhysX Error! Code: {0}.\n{1}\nSource: {2} : {3}.", static_cast<int32>(code), String(message), String(file), line);
|
|
}
|
|
};
|
|
|
|
PxFilterFlags PhysiXFilterShader(
|
|
PxFilterObjectAttributes attributes0, PxFilterData filterData0,
|
|
PxFilterObjectAttributes attributes1, PxFilterData filterData1,
|
|
PxPairFlags& pairFlags, const void* constantBlock, PxU32 constantBlockSize)
|
|
{
|
|
// Let triggers through
|
|
if (PxFilterObjectIsTrigger(attributes0) || PxFilterObjectIsTrigger(attributes1))
|
|
{
|
|
pairFlags |= PxPairFlag::eNOTIFY_TOUCH_FOUND;
|
|
pairFlags |= PxPairFlag::eNOTIFY_TOUCH_LOST;
|
|
pairFlags |= PxPairFlag::eDETECT_DISCRETE_CONTACT;
|
|
return PxFilterFlag::eDEFAULT;
|
|
}
|
|
|
|
// Send events for the kinematic actors but don't solve the contact
|
|
if (PxFilterObjectIsKinematic(attributes0) && PxFilterObjectIsKinematic(attributes1))
|
|
{
|
|
pairFlags |= PxPairFlag::eNOTIFY_TOUCH_FOUND;
|
|
pairFlags |= PxPairFlag::eNOTIFY_TOUCH_PERSISTS;
|
|
pairFlags |= PxPairFlag::eNOTIFY_TOUCH_LOST;
|
|
pairFlags |= PxPairFlag::eDETECT_DISCRETE_CONTACT;
|
|
return PxFilterFlag::eSUPPRESS;
|
|
}
|
|
|
|
// Trigger the contact callback for pairs (A,B) where the filtermask of A contains the ID of B and vice versa
|
|
if ((filterData0.word0 & filterData1.word1) && (filterData1.word0 & filterData0.word1))
|
|
{
|
|
pairFlags |= PxPairFlag::eSOLVE_CONTACT;
|
|
pairFlags |= PxPairFlag::eDETECT_DISCRETE_CONTACT;
|
|
pairFlags |= PxPairFlag::eNOTIFY_TOUCH_FOUND;
|
|
pairFlags |= PxPairFlag::eNOTIFY_TOUCH_PERSISTS;
|
|
pairFlags |= PxPairFlag::ePOST_SOLVER_VELOCITY;
|
|
pairFlags |= PxPairFlag::eNOTIFY_CONTACT_POINTS;
|
|
return PxFilterFlag::eDEFAULT;
|
|
}
|
|
|
|
// Ignore pair (no collisions nor events)
|
|
return PxFilterFlag::eKILL;
|
|
}
|
|
|
|
enum class ActionType
|
|
{
|
|
Sleep,
|
|
};
|
|
|
|
struct ActionData
|
|
{
|
|
ActionType Type;
|
|
PxActor* Actor;
|
|
};
|
|
|
|
PxPhysics* CPhysX = nullptr;
|
|
PhysXAllocator PhysXAllocatorCallback;
|
|
PhysXError PhysXErrorCallback;
|
|
PxSimulationFilterShader PhysXDefaultFilterShader = PxDefaultSimulationFilterShader;
|
|
#if WITH_PVD
|
|
PxPvd* CPVD = nullptr;
|
|
#endif
|
|
PxTolerancesScale ToleranceScale;
|
|
SimulationEventCallback EventsCallback;
|
|
void* ScratchMemory = nullptr;
|
|
FixedStepper* Stepper = nullptr;
|
|
CriticalSection FlushLocker;
|
|
Array<PxActor*> NewActors;
|
|
Array<ActionData> Actions;
|
|
Array<PxActor*> DeadActors;
|
|
Array<PxMaterial*> DeadMaterials;
|
|
Array<PxBase*> _deadObjects;
|
|
Array<PhysicsColliderActor*> DeadColliders;
|
|
Array<Joint*> DeadJoints;
|
|
bool _isDuringSimulation = false;
|
|
PxFoundation* _foundation = nullptr;
|
|
#if COMPILE_WITH_PHYSICS_COOKING
|
|
PxCooking* Cooking = nullptr;
|
|
#endif
|
|
PxScene* PhysicsScene = nullptr;
|
|
PxMaterial* DefaultMaterial = nullptr;
|
|
PxControllerManager* ControllerManager = nullptr;
|
|
PxCpuDispatcher* CpuDispatcher = nullptr;
|
|
bool Physics::AutoSimulation = true;
|
|
|
|
class PhysicsService : public EngineService
|
|
{
|
|
public:
|
|
|
|
PhysicsService()
|
|
: EngineService(TEXT("Physics"), 0)
|
|
{
|
|
}
|
|
|
|
bool Init() override;
|
|
void LateUpdate() override;
|
|
void Dispose() override;
|
|
};
|
|
|
|
PhysicsService PhysicsServiceInstance;
|
|
|
|
bool PhysicsService::Init()
|
|
{
|
|
#define CHECK_INIT(value, msg) if(!value) { LOG(Error, msg); return true; }
|
|
|
|
auto cpuInfo = Platform::GetCPUInfo();
|
|
auto settings = PhysicsSettings::Instance();
|
|
|
|
// Send info
|
|
LOG(Info, "Setup NVIDIA PhysX {0}.{1}.{2}", PX_PHYSICS_VERSION_MAJOR, PX_PHYSICS_VERSION_MINOR, PX_PHYSICS_VERSION_BUGFIX);
|
|
|
|
// Init PhysX foundation object
|
|
_foundation = PxCreateFoundation(PX_PHYSICS_VERSION, PhysXAllocatorCallback, PhysXErrorCallback);
|
|
CHECK_INIT(_foundation, "PxCreateFoundation failed!");
|
|
|
|
// Recording memory allocations is necessary if you want to
|
|
// use the memory facilities in PVD effectively. Since PVD isn't necessarily connected
|
|
// right away, we add a mechanism that records all outstanding memory allocations and
|
|
// forwards them to PVD when it does connect.
|
|
// This certainly has a performance and memory profile effect and thus should be used
|
|
// only in non-production builds.
|
|
|
|
#if PHYSX_MEMORY_STATS
|
|
_foundation->setReportAllocationNames(true);
|
|
#endif
|
|
|
|
// Config
|
|
ToleranceScale.length = 100;
|
|
ToleranceScale.speed = 981;
|
|
|
|
PxPvd* pvd = nullptr;
|
|
#if WITH_PVD
|
|
{
|
|
// Connection parameters
|
|
const char* pvd_host_ip = "127.0.0.1"; // IP of the PC which is running PVD
|
|
int port = 5425; // TCP port to connect to, where PVD is listening
|
|
unsigned int timeout = 100; // Timeout in milliseconds to wait for PVD to respond, consoles and remote PCs need a higher timeout.
|
|
|
|
// Init PVD
|
|
pvd = PxCreatePvd(*_foundation);
|
|
PxPvdTransport* transport = PxDefaultPvdSocketTransportCreate(pvd_host_ip, port, timeout);
|
|
const bool isConnected = pvd->connect(*transport, PxPvdInstrumentationFlag::eALL);
|
|
if (isConnected)
|
|
{
|
|
LOG(Info, "Connected to PhysX Visual Debugger (PVD)"));
|
|
}
|
|
|
|
CPVD = pvd;
|
|
}
|
|
|
|
#endif
|
|
|
|
// Init top-level PhysX objects
|
|
CPhysX = PxCreatePhysics(PX_PHYSICS_VERSION, *_foundation, ToleranceScale, false, pvd);
|
|
CHECK_INIT(CPhysX, "PxCreatePhysics failed!");
|
|
|
|
// Init Extensions
|
|
const bool extensionsInit = PxInitExtensions(*CPhysX, pvd);
|
|
CHECK_INIT(extensionsInit, "PxInitExtensions failed!");
|
|
#if WITH_VEHICLE
|
|
PxInitVehicleSDK(*Physics);
|
|
#endif
|
|
|
|
#if COMPILE_WITH_PHYSICS_COOKING
|
|
|
|
#if !USE_EDITOR
|
|
if (settings->SupportCookingAtRuntime)
|
|
#endif
|
|
{
|
|
// Init cooking
|
|
PxCookingParams cookingParams(ToleranceScale);
|
|
cookingParams.meshWeldTolerance = 0.1f; // Weld to 1mm precision
|
|
cookingParams.meshPreprocessParams = PxMeshPreprocessingFlags(PxMeshPreprocessingFlag::eWELD_VERTICES);
|
|
Cooking = PxCreateCooking(PX_PHYSICS_VERSION, *_foundation, cookingParams);
|
|
CHECK_INIT(Cooking, "PxCreateCooking failed!");
|
|
}
|
|
|
|
#endif
|
|
|
|
// Create scene description
|
|
PxSceneDesc sceneDesc(CPhysX->getTolerancesScale());
|
|
sceneDesc.gravity = C2P(settings->DefaultGravity);
|
|
sceneDesc.flags |= PxSceneFlag::eENABLE_ACTIVE_ACTORS;
|
|
//sceneDesc.flags |= PxSceneFlag::eEXCLUDE_KINEMATICS_FROM_ACTIVE_ACTORS; // TODO: set it?
|
|
if (!settings->DisableCCD)
|
|
sceneDesc.flags |= PxSceneFlag::eENABLE_CCD;
|
|
if (settings->EnableAdaptiveForce)
|
|
sceneDesc.flags |= PxSceneFlag::eADAPTIVE_FORCE;
|
|
sceneDesc.simulationEventCallback = &EventsCallback;
|
|
sceneDesc.filterShader = PhysiXFilterShader;
|
|
sceneDesc.bounceThresholdVelocity = settings->BounceThresholdVelocity;
|
|
if (sceneDesc.cpuDispatcher == nullptr)
|
|
{
|
|
CpuDispatcher = PxDefaultCpuDispatcherCreate(Math::Clamp<uint32>(cpuInfo.ProcessorCoreCount - 1, 1, 4));
|
|
CHECK_INIT(CpuDispatcher, "PxDefaultCpuDispatcherCreate failed!");
|
|
sceneDesc.cpuDispatcher = CpuDispatcher;
|
|
}
|
|
if (sceneDesc.filterShader == nullptr)
|
|
{
|
|
sceneDesc.filterShader = PhysXDefaultFilterShader;
|
|
}
|
|
|
|
// Create scene
|
|
PhysicsScene = CPhysX->createScene(sceneDesc);
|
|
CHECK_INIT(PhysicsScene, "createScene failed!");
|
|
|
|
// Init characters controller
|
|
ControllerManager = PxCreateControllerManager(*PhysicsScene);
|
|
|
|
// Create default resources
|
|
DefaultMaterial = CPhysX->createMaterial(0.7f, 0.7f, 0.3f);
|
|
|
|
return false;
|
|
|
|
#undef CHECK_INIT
|
|
}
|
|
|
|
void PhysicsService::LateUpdate()
|
|
{
|
|
Physics::FlushRequests();
|
|
}
|
|
|
|
void PhysicsService::Dispose()
|
|
{
|
|
// Ensure to finish (wait for simulation end)
|
|
Physics::CollectResults();
|
|
|
|
// Cleanup
|
|
if (CPhysX)
|
|
Physics::FlushRequests();
|
|
RELEASE_PHYSX(DefaultMaterial);
|
|
SAFE_DELETE(Stepper);
|
|
Allocator::Free(ScratchMemory);
|
|
ScratchMemory = nullptr;
|
|
RELEASE_PHYSX(ControllerManager);
|
|
SAFE_DELETE(CpuDispatcher);
|
|
|
|
// Remove all scenes still registered
|
|
const int32 numScenes = CPhysX ? CPhysX->getNbScenes() : 0;
|
|
if (numScenes)
|
|
{
|
|
Array<PxScene*> PScenes;
|
|
PScenes.Resize(numScenes);
|
|
PScenes.SetAll(nullptr);
|
|
CPhysX->getScenes(PScenes.Get(), sizeof(PxScene*) * numScenes);
|
|
|
|
for (int32 i = 0; i < numScenes; i++)
|
|
{
|
|
if (PScenes[i])
|
|
{
|
|
PScenes[i]->release();
|
|
}
|
|
}
|
|
}
|
|
|
|
#if COMPILE_WITH_PHYSICS_COOKING
|
|
RELEASE_PHYSX(Cooking);
|
|
#endif
|
|
|
|
if (CPhysX)
|
|
{
|
|
#if WITH_VEHICLE
|
|
PxCloseVehicleSDK();
|
|
#endif
|
|
PxCloseExtensions();
|
|
}
|
|
|
|
RELEASE_PHYSX(CPhysX);
|
|
#if WITH_PVD
|
|
RELEASE_PHYSX(CPVD);
|
|
#endif
|
|
RELEASE_PHYSX(_foundation);
|
|
}
|
|
|
|
PxPhysics* Physics::GetPhysics()
|
|
{
|
|
return CPhysX;
|
|
}
|
|
|
|
PxCooking* Physics::GetCooking()
|
|
{
|
|
return Cooking;
|
|
}
|
|
|
|
PxScene* Physics::GetScene()
|
|
{
|
|
return PhysicsScene;
|
|
}
|
|
|
|
PxControllerManager* Physics::GetControllerManager()
|
|
{
|
|
return ControllerManager;
|
|
}
|
|
|
|
PxTolerancesScale* Physics::GetTolerancesScale()
|
|
{
|
|
return &ToleranceScale;
|
|
}
|
|
|
|
Vector3 Physics::GetGravity()
|
|
{
|
|
return PhysicsScene ? P2C(PhysicsScene->getGravity()) : Vector3::Zero;
|
|
}
|
|
|
|
void Physics::SetGravity(const Vector3& value)
|
|
{
|
|
if (PhysicsScene)
|
|
PhysicsScene->setGravity(C2P(value));
|
|
}
|
|
|
|
bool Physics::GetEnableCCD()
|
|
{
|
|
return PhysicsScene ? (PhysicsScene->getFlags() & PxSceneFlag::eENABLE_CCD) == PxSceneFlag::eENABLE_CCD : !PhysicsSettings_DisableCCD;
|
|
}
|
|
|
|
void Physics::SetEnableCCD(const bool value)
|
|
{
|
|
if (PhysicsScene)
|
|
PhysicsScene->setFlag(PxSceneFlag::eENABLE_CCD, value);
|
|
}
|
|
|
|
float Physics::GetBounceThresholdVelocity()
|
|
{
|
|
return PhysicsScene ? PhysicsScene->getBounceThresholdVelocity() : PhysicsSettings_BounceThresholdVelocity;
|
|
}
|
|
|
|
void Physics::SetBounceThresholdVelocity(const float value)
|
|
{
|
|
if (PhysicsScene)
|
|
PhysicsScene->setBounceThresholdVelocity(value);
|
|
}
|
|
|
|
void Physics::Simulate(float dt)
|
|
{
|
|
ASSERT(IsInMainThread() && !_isDuringSimulation);
|
|
ASSERT(CPhysX);
|
|
const auto settings = PhysicsSettings::Instance();
|
|
|
|
// Flush the old/new objects and the other requests before the simulation
|
|
FlushRequests();
|
|
|
|
// Clamp delta
|
|
dt = Math::Clamp(dt, 0.0f, settings->MaxDeltaTime);
|
|
|
|
// Prepare util objects
|
|
if (ScratchMemory == nullptr)
|
|
{
|
|
ScratchMemory = Allocator::Allocate(SCRATCH_BLOCK_SIZE, 16);
|
|
}
|
|
if (Stepper == nullptr)
|
|
{
|
|
Stepper = New<FixedStepper>();
|
|
}
|
|
if (settings->EnableSubstepping)
|
|
{
|
|
// Use substeps
|
|
Stepper->Setup(settings->SubstepDeltaTime, settings->MaxSubsteps);
|
|
}
|
|
else
|
|
{
|
|
// Use single step
|
|
Stepper->Setup(dt);
|
|
}
|
|
|
|
// Start simulation (may not be fired due to too small delta time)
|
|
_isDuringSimulation = true;
|
|
if (Stepper->advance(PhysicsScene, dt, ScratchMemory, SCRATCH_BLOCK_SIZE) == false)
|
|
return;
|
|
EventsCallback.Clear();
|
|
|
|
// TODO: move this call after rendering done
|
|
Stepper->renderDone();
|
|
}
|
|
|
|
void Physics::CollectResults()
|
|
{
|
|
ASSERT(IsInMainThread());
|
|
if (!_isDuringSimulation)
|
|
return;
|
|
ASSERT(CPhysX && Stepper);
|
|
|
|
{
|
|
PROFILE_CPU_NAMED("Physics.Fetch");
|
|
|
|
// Gather results (with waiting for the end)
|
|
Stepper->wait(PhysicsScene);
|
|
}
|
|
|
|
{
|
|
PROFILE_CPU_NAMED("Physics.FlushActiveTransforms");
|
|
|
|
// Gather change info
|
|
PxU32 activeActorsCount;
|
|
PxActor** activeActors = PhysicsScene->getActiveActors(activeActorsCount);
|
|
if (activeActorsCount > 0)
|
|
{
|
|
// Update changed transformations
|
|
// TODO: use jobs system if amount if huge
|
|
for (uint32 i = 0; i < activeActorsCount; i++)
|
|
{
|
|
const auto pxActor = (PxRigidActor*)*activeActors++;
|
|
auto actor = dynamic_cast<IPhysicsActor*>((Actor*)pxActor->userData);
|
|
ASSERT(actor);
|
|
actor->OnActiveTransformChanged(pxActor->getGlobalPose());
|
|
}
|
|
}
|
|
}
|
|
|
|
{
|
|
PROFILE_CPU_NAMED("Physics.SendEvents");
|
|
|
|
EventsCallback.CollectResults();
|
|
EventsCallback.SendTriggerEvents();
|
|
EventsCallback.SendCollisionEvents();
|
|
EventsCallback.SendJointEvents();
|
|
}
|
|
|
|
// End
|
|
_isDuringSimulation = false;
|
|
}
|
|
|
|
bool Physics::IsDuringSimulation()
|
|
{
|
|
return _isDuringSimulation;
|
|
}
|
|
|
|
PxMaterial* Physics::GetDefaultMaterial()
|
|
{
|
|
return DefaultMaterial;
|
|
}
|
|
|
|
void Physics::FlushRequests()
|
|
{
|
|
ASSERT(!IsDuringSimulation());
|
|
ASSERT(CPhysX);
|
|
|
|
PROFILE_CPU();
|
|
|
|
FlushLocker.Lock();
|
|
|
|
// Note: this does not handle case when actor is removed and added to the scene at the same time
|
|
|
|
if (NewActors.HasItems())
|
|
{
|
|
GetScene()->addActors(NewActors.Get(), NewActors.Count());
|
|
NewActors.Clear();
|
|
}
|
|
|
|
for (int32 i = 0; i < Actions.Count(); i++)
|
|
{
|
|
const auto action = Actions[i];
|
|
switch (action.Type)
|
|
{
|
|
case ActionType::Sleep:
|
|
static_cast<PxRigidDynamic*>(action.Actor)->putToSleep();
|
|
break;
|
|
}
|
|
}
|
|
Actions.Clear();
|
|
|
|
if (DeadActors.HasItems())
|
|
{
|
|
GetScene()->removeActors(DeadActors.Get(), DeadActors.Count(), true);
|
|
for (int32 i = 0; i < DeadActors.Count(); i++)
|
|
{
|
|
DeadActors[i]->release();
|
|
}
|
|
DeadActors.Clear();
|
|
}
|
|
|
|
if (DeadColliders.HasItems())
|
|
{
|
|
for (int32 i = 0; i < DeadColliders.Count(); i++)
|
|
{
|
|
EventsCallback.OnColliderRemoved(DeadColliders[i]);
|
|
}
|
|
DeadColliders.Clear();
|
|
}
|
|
|
|
if (DeadJoints.HasItems())
|
|
{
|
|
for (int32 i = 0; i < DeadJoints.Count(); i++)
|
|
{
|
|
EventsCallback.OnJointRemoved(DeadJoints[i]);
|
|
}
|
|
DeadJoints.Clear();
|
|
}
|
|
|
|
for (int32 i = 0; i < DeadMaterials.Count(); i++)
|
|
{
|
|
auto material = DeadMaterials[i];
|
|
|
|
// Unlink ref to flax object
|
|
material->userData = nullptr;
|
|
|
|
material->release();
|
|
}
|
|
DeadMaterials.Clear();
|
|
|
|
for (int32 i = 0; i < _deadObjects.Count(); i++)
|
|
{
|
|
_deadObjects[i]->release();
|
|
}
|
|
_deadObjects.Clear();
|
|
|
|
FlushLocker.Unlock();
|
|
}
|
|
|
|
void Physics::RemoveMaterial(PxMaterial* material)
|
|
{
|
|
ASSERT(material);
|
|
|
|
FlushLocker.Lock();
|
|
DeadMaterials.Add(material);
|
|
FlushLocker.Unlock();
|
|
}
|
|
|
|
void Physics::RemoveObject(PxBase* obj)
|
|
{
|
|
ASSERT(obj);
|
|
|
|
FlushLocker.Lock();
|
|
_deadObjects.Add(obj);
|
|
FlushLocker.Unlock();
|
|
}
|
|
|
|
void Physics::AddActor(PxActor* actor)
|
|
{
|
|
ASSERT(actor);
|
|
|
|
FlushLocker.Lock();
|
|
if (IsInMainThread())
|
|
{
|
|
GetScene()->addActor(*actor);
|
|
}
|
|
else
|
|
{
|
|
NewActors.Add(actor);
|
|
}
|
|
FlushLocker.Unlock();
|
|
}
|
|
|
|
void Physics::AddActor(PxRigidDynamic* actor, bool putToSleep)
|
|
{
|
|
ASSERT(actor);
|
|
|
|
FlushLocker.Lock();
|
|
if (IsInMainThread())
|
|
{
|
|
GetScene()->addActor(*actor);
|
|
if (putToSleep)
|
|
actor->putToSleep();
|
|
}
|
|
else
|
|
{
|
|
NewActors.Add(actor);
|
|
if (putToSleep)
|
|
Actions.Add({ ActionType::Sleep, actor });
|
|
}
|
|
FlushLocker.Unlock();
|
|
}
|
|
|
|
void Physics::RemoveActor(PxActor* actor)
|
|
{
|
|
ASSERT(actor);
|
|
|
|
// Unlink ref to flax object
|
|
actor->userData = nullptr;
|
|
|
|
FlushLocker.Lock();
|
|
DeadActors.Add(actor);
|
|
FlushLocker.Unlock();
|
|
}
|
|
|
|
void Physics::RemoveCollider(PhysicsColliderActor* collider)
|
|
{
|
|
ASSERT(collider);
|
|
|
|
FlushLocker.Lock();
|
|
DeadColliders.Add(collider);
|
|
FlushLocker.Unlock();
|
|
}
|
|
|
|
void Physics::RemoveJoint(Joint* joint)
|
|
{
|
|
ASSERT(joint);
|
|
|
|
FlushLocker.Lock();
|
|
DeadJoints.Add(joint);
|
|
FlushLocker.Unlock();
|
|
}
|