@kylawl your solution worked like a dream for me! It took some minor modifications because IsRigidDynamic() and IsRigidStatic() are deprecated now but you can just cast the PxRigidActor reference to PxRigidDynamic or PxRigidStatic and check if the result is null.
Can anyone confirm that there is still no built-in distance constraint in UE4? It seems pretty essential for something that still requires a custom implementation.
Also if anyone else uses kylawl’s example you should know there is a bug in InitConstraint:
DistJoint->setMaxDistance(MinDistance == -1.0f ? BodyDist : MaxDistance);
should be
DistJoint->setMaxDistance(MaxDistance == -1.0f ? BodyDist : MaxDistance);
otherwise setting MinDistance to something other than -1 makes the joint go haywire.
EDIT: There were some other issues with deprecation and bugs like the physics scene needing to be locked. I’ve added my updated version that I’ve turned into a blueprint spawnable actor component. Just call SetupConstraint() in your BP beginplay and plug in the two components you want to constrain (both have to be mobile/dynamic). I’m using in 4.16. I can’t guarantee it’s perfect but it’s working for me:
DistanceConstraint.h
#pragma once
#include "CoreMinimal.h"
#include "DistanceConstraint.generated.h"
struct FBodyInstance;
namespace physx
{
class PxDistanceJoint;
}
UENUM()
enum class EJointFrame : uint8
{
Frame1,
Frame2
};
/**
*
*/
UCLASS(BlueprintType, ClassGroup = "Physics", Meta = (BlueprintSpawnableComponent))
class [YOURPROJECT]_API UDistanceConstraint : public UActorComponent
{
GENERATED_BODY()
public:
class physx::PxDistanceJoint* ConstraintData;
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = Joint)
uint32 bMinDistanceEnabled : 1;
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = Joint)
uint32 bMaxDistanceEnabled : 1;
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = Joint)
float MinDistance;
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = Joint)
float MaxDistance;
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = Joint)
uint32 bCollisionEnabled : 1;
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = Joint)
FMatrix RefFrame1;
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = Joint)
FMatrix RefFrame2;
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = Spring)
uint32 bSpringEnabled : 1;
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = Spring)
float SpringStiffness;
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = Spring)
float SpringDampening;
UDistanceConstraint();
~UDistanceConstraint();
UFUNCTION(BlueprintCallable, Category = "Constraint")
void SetupConstraint(UPrimitiveComponent* ComponentA, UPrimitiveComponent* ComponentB)
{
if (ComponentA != nullptr && ComponentB != nullptr) {
InitConstraint(ComponentA->GetBodyInstance(), ComponentB->GetBodyInstance());
}
};
void InitConstraint(FBodyInstance* Body1, FBodyInstance* Body2);
void TerminateConstraint();
UFUNCTION(BlueprintCallable, Category = "Constraint")
bool IsValidConstraintInstance() const { return ConstraintData != nullptr; }
UFUNCTION(BlueprintCallable, Category = "Constraint")
void SetMaxDistance(float NewMaxDistance);
UFUNCTION(BlueprintCallable, Category = "Constraint")
void SetMinDistance(float NewMinDistance);
UFUNCTION(BlueprintCallable, Category = "Constraint")
void SetDistance(float NewDistance);
// Pass in reference frame in. If the constraint is currently active, this will set it's active local pose. Otherwise the change will take affect in InitConstraint.
void SetRefFrame(EJointFrame Frame, const FMatrix& RefFrame);
// Pass in reference position in (maintains reference orientation). If the constraint is currently active, this will set its active local pose. Otherwise the change will take affect in InitConstraint.
void SetRefPosition(EJointFrame Frame, const FVector& RefPosition);
private:
// The index of the PhysX scene this constraint exists in
int32 SceneIndex1;
int32 SceneIndex2;
};
DistanceConstraint.cpp
#include "[YOURPROJECT].h"
#include "DistanceConstraint.h"
#include "PhysicsPublic.h"
#include "PhysXIncludes.h"
#include "Runtime/Engine/Public/PhysXPublic.h"
// PhysX
#include "PxJoint.h"
#include "PxDistanceJoint.h"
FORCEINLINE physx::PxJointActorIndex::Enum U2PJointFrame(EJointFrame Frame)
{
// Swap frame order, since Unreal reverses physx order
return (Frame == EJointFrame::Frame1) ? physx::PxJointActorIndex::eACTOR1 : physx::PxJointActorIndex::eACTOR0;
}
UDistanceConstraint::UDistanceConstraint()
: ConstraintData(nullptr)
, bMinDistanceEnabled(false)
, bMaxDistanceEnabled(true)
, MinDistance(-1.0f)
, MaxDistance(-1.0f)
, bCollisionEnabled(true)
, RefFrame1(FMatrix::Identity)
, RefFrame2(FMatrix::Identity)
, bSpringEnabled(false)
, SpringStiffness(0.0f)
, SpringDampening(0.0f)
{
PrimaryComponentTick.bCanEverTick = false;
}
UDistanceConstraint::~UDistanceConstraint()
{
TerminateConstraint();
}
void UDistanceConstraint::TerminateConstraint()
{
#if WITH_PHYSX
if (ConstraintData != nullptr)
{
PxScene* PScene = ConstraintData->getScene();
SCOPED_SCENE_WRITE_LOCK(PScene);
if (PScene != nullptr) {
ConstraintData->release();
ConstraintData = nullptr;
}
}
#endif // WITH_PHYSX
}
void UDistanceConstraint::SetMaxDistance(float NewMaxDistance)
{
MaxDistance = NewMaxDistance;
#if WITH_PHYSX
if (ConstraintData != nullptr)
{
ConstraintData->setMaxDistance(NewMaxDistance);
}
#endif // WITH_PHYSX
}
void UDistanceConstraint::SetMinDistance(float NewMinDistance)
{
MinDistance = NewMinDistance;
#if WITH_PHYSX
if (ConstraintData != nullptr)
{
ConstraintData->setMinDistance(NewMinDistance);
}
#endif // WITH_PHYSX
}
void UDistanceConstraint::SetDistance(float NewDistance)
{
MinDistance = NewDistance;
MaxDistance = NewDistance;
#if WITH_PHYSX
if (ConstraintData != nullptr)
{
ConstraintData->setMinDistance(NewDistance);
ConstraintData->setMaxDistance(NewDistance);
}
#endif // WITH_PHYSX
}
void UDistanceConstraint::SetRefFrame(EJointFrame Frame, const FMatrix& RefFrame)
{
if (Frame == EJointFrame::Frame1)
{
RefFrame1 = RefFrame;
}
else
{
RefFrame2 = RefFrame;
}
#if WITH_PHYSX
if (ConstraintData)
{
physx::PxJointActorIndex::Enum PxFrame = U2PJointFrame(Frame);
PxTransform PxRefFrame = UMatrix2PTransform(RefFrame);
ConstraintData->setLocalPose(PxFrame, PxRefFrame);
}
#endif // WITH_PHYSX
}
// Pass in reference position in (maintains reference orientation). If the constraint is currently active, this will set its active local pose. Otherwise the change will take affect in InitConstraint.
void UDistanceConstraint::SetRefPosition(EJointFrame Frame, const FVector& RefPosition)
{
if (Frame == EJointFrame::Frame1)
{
RefFrame1.SetOrigin(RefPosition);
}
else
{
RefFrame2.SetOrigin(RefPosition);
}
#if WITH_PHYSX
if (ConstraintData)
{
physx::PxJointActorIndex::Enum PxFrame = U2PJointFrame(Frame);
PxTransform PxRefFrame = ConstraintData->getLocalPose(PxFrame);
PxRefFrame.p = U2PVector(RefPosition);
ConstraintData->setLocalPose(PxFrame, PxRefFrame);
}
#endif // WITH_PHYSX
}
void UDistanceConstraint::InitConstraint(FBodyInstance* Body1, FBodyInstance* Body2)
{
// if there's already a constraint, get rid of it first
if (ConstraintData != nullptr)
{
TerminateConstraint();
}
FPhysScene* PhysScene = GetWorld()->GetPhysicsScene();
if (!PhysScene) return;
SceneIndex1 = Body1->GetSceneIndex();
SceneIndex2 = Body2->GetSceneIndex();
PxScene* Body1Scene = GetPhysXSceneFromIndex(SceneIndex1);
PxScene* Body2Scene = GetPhysXSceneFromIndex(SceneIndex2);
SCOPED_SCENE_WRITE_LOCK(Body1Scene);
SCOPED_SCENE_WRITE_LOCK(Body2Scene);
if (Body1Scene != nullptr && Body2Scene != nullptr) {
PxRigidActor* PActor1 = Body1 ? Body1->GetPxRigidActor_AssumesLocked() : nullptr;
PxRigidActor* PActor2 = Body2 ? Body2->GetPxRigidActor_AssumesLocked() : nullptr;
PxRigidDynamic* PRigidDynamic1 = (physx::PxRigidDynamic*) PActor1;
PxRigidDynamic* PRigidDynamic2 = (physx::PxRigidDynamic*) PActor2;
// Do not create joint unless you have two actors and they're both dynamic
if (PActor1 == nullptr || PActor2 == nullptr || PRigidDynamic1 == nullptr || PRigidDynamic2 == nullptr)
{
return;
}
// Need to worry about the case where one is static and one is dynamic, and make sure the static scene is used which matches the dynamic scene
/*if (PActor1 != nullptr && PActor2 != nullptr)
{
if (PActor1->isRigidStatic() && PActor2->isRigidDynamic())
{
const uint32 SceneType = Body2->RigidActorSync != nullptr ? PST_Sync : PST_Async;
PActor1 = Body1->GetPxRigidActor(SceneType);
}
else if (PActor2->isRigidStatic() && PActor1->isRigidDynamic())
{
const uint32 SceneType = Body1->RigidActorSync != nullptr ? PST_Sync : PST_Async;
PActor2 = Body2->GetPxRigidActor(SceneType);
}
}*/
// record if actors are asleep before creating joint, so we can sleep them afterwards if so (creating joint wakes them)
const bool bActor1Asleep = PRigidDynamic1->isSleeping();
const bool bActor2Asleep = PRigidDynamic2->isSleeping();
FTransform Transform1 = Body1->GetUnrealWorldTransform();
FTransform Transform2 = Body2->GetUnrealWorldTransform();
float BodyDist = FVector::Dist(Transform1.GetLocation(), Transform2.GetLocation());
// Distances will be the initial body distance if they haven't been set
if (MinDistance == -1.0f) {
MinDistance = BodyDist;
}
if (MaxDistance == -1.0f) {
MaxDistance = BodyDist;
}
PxTransform PxBody1Frame = UMatrix2PTransform(RefFrame1);
PxTransform PxBody2Frame = UMatrix2PTransform(RefFrame2);
PxDistanceJoint* DistJoint = PxDistanceJointCreate(*GPhysXSDK, PActor2, PxBody2Frame, PActor1, PxBody1Frame);
if (DistJoint == nullptr)
{
UE_LOG(LogPhysics, Log, TEXT("UDistanceConstraint::InitConstraint - Invalid Distance Joint"));
return;
}
DistJoint->setConstraintFlag(PxConstraintFlag::eCOLLISION_ENABLED, bCollisionEnabled);
DistJoint->setDistanceJointFlag(PxDistanceJointFlag::eMIN_DISTANCE_ENABLED, bMinDistanceEnabled);
DistJoint->setDistanceJointFlag(PxDistanceJointFlag::eMAX_DISTANCE_ENABLED, bMaxDistanceEnabled);
DistJoint->setDistanceJointFlag(PxDistanceJointFlag::eSPRING_ENABLED, bSpringEnabled);
DistJoint->setMinDistance(MinDistance);
DistJoint->setMaxDistance(MaxDistance);
if (bSpringEnabled)
{
DistJoint->setStiffness(SpringStiffness);
DistJoint->setDamping(SpringDampening);
}
ConstraintData = DistJoint;
}
}