Object Grabbing like in Voices of the Void (or half-life 2, or Amnesia TDD)

Hey there, hope this is not the millionth time somebody has asked for help with object grabbing (however i find it weird there is little to no information online about how to achieve it)
So here i am, struggling with Unreal Engine once again, and probably not for the last time either.

First time as i was approaching this problem i’ve made a decision to handle it with a magic wand called PhysicsHandle, however result was underwhelming to say at least. If not configured it would just freeze the object (can be seen if you try to grab a falling object) so i decided to update the grabbed object’s position manually every tick (which didn’t help much since it was constantly snapping each time player was making so the ray is not hitting the object, which isn’t smooth by any means), also held object can be pushed through the ground and walls very easy, something needed to change.

So, i decided to use Physics constraints, and it worked.
Not quite the end of the story yet, actually. As it turns out, physics constraint isn’t actually meant for such usage, which leads to unforeseen consequences.
First of all: the snapping of the grabbed object to the constraint’s parent mesh is much better, but only on ONE axis… if player decides to move camera on Y axis they’ll experience extreme discomfort from what they will see. vid attached

Also, objects can be pushed through the floor and walls too! But it’s a tiny bit more rare than with the PhysicsHandle, which is quite weird considering it was MADE for the reasons i’m trying to achieve.

So… Here is my class (sorry for jank, not intended for it to be public)

Character0.h

#pragma once

#include "CoreMinimal.h"
#include "GameFramework/Character.h"
#include "Character0.generated.h"

class UInputComponent;
class UCameraComponent;
class USkeletalMeshComponent;
class UInputMappingContext;
class UInputAction;
class UPhysicsConstraintComponent;
struct FInputActionValue;

UCLASS(config = Game)
class ACharacter0 : public ACharacter {
	GENERATED_BODY()

	UPROPERTY(VisibleDefaultsOnly, Category = Mesh)
	USkeletalMeshComponent* Mesh1P = nullptr;

	UPROPERTY(VisibleDefaultsOnly, Category = Mesh)
	UStaticMeshComponent* HeldObjectSlot;

	UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = Camera, meta = (AllowPrivateAccess = "true"))
	UPhysicsConstraintComponent* PhysicsConstraint;

	UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = Camera, meta = (AllowPrivateAccess = "true"))
	UCameraComponent* Camera = nullptr;

	UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = Input, meta = (AllowPrivateAccess = "true"))
	UInputMappingContext* PlayerMappingContext = nullptr;

	UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = Input, meta = (AllowPrivateAccess = "true"))
	UInputAction* MoveAction = nullptr;

	UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = Input, meta = (AllowPrivateAccess = "true"))
	UInputAction* JumpAction = nullptr;

	UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = Input, meta = (AllowPrivateAccess = "true"))
	UInputAction* CrouchAction = nullptr;
	
	UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = Input, meta = (AllowPrivateAccess = "true"))
	float Reach = 384.0;

	UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = Input, meta = (AllowPrivateAccess = "true"))
	float ThrowPower = 2000.0f;

	UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = Input, meta = (AllowPrivateAccess = "true"))
	float GrabMassBarrierKG = 50.0f;

	UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = Input, meta = (AllowPrivateAccess = "true"))
	UInputAction* GrabAction = nullptr;

	UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = Input, meta = (AllowPrivateAccess = "true"))
	UInputAction* ThrowAction = nullptr;

public:
	ACharacter0();

protected:
	virtual void BeginPlay() override;

public:
	UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = Input)
	UInputAction* LookAction;

protected:
	void Move(const FInputActionValue& Value);
	void Look(const FInputActionValue& Value);

	void Grab();
	void Release();
	void Throw();

protected:
	virtual void SetupPlayerInputComponent(UInputComponent* InputComponent) override;

	UPrimitiveComponent* HeldObj = nullptr;
	FVector HRIP;

public:
	/** Returns Mesh1P subobject **/
	USkeletalMeshComponent* GetMesh1P() const { return Mesh1P; }
	/** Returns CameraComponent subobject **/
	UCameraComponent* GetCameraComponent() const { return Camera; }

	inline FVector GetReachLineStart();
	inline FVector GetReachLineEnd();

private:
	void StartCrouch();
	void StopCrouch();

	virtual void Jump() override {
		UnCrouch();
		bPressedJump = true;
		JumpKeyHoldTime = 0.0f;
	}
};

Character0.cpp:

#include "Character0.h"
#include "Components/InputComponent.h"
#include "Camera/CameraComponent.h"
#include "Components/CapsuleComponent.h"
#include "PhysicsEngine/PhysicsConstraintComponent.h"
#include "GameFramework/PlayerController.h"

#include "InputActionValue.h"
#include "EnhancedInputComponent.h"
#include "EnhancedInputSubsystems.h"

ACharacter0::ACharacter0() {

	GetCapsuleComponent()->InitCapsuleSize(55.f, 96.0f);

	PrimaryActorTick.bCanEverTick = true;

	PhysicsConstraint = CreateDefaultSubobject<UPhysicsConstraintComponent>(TEXT("PhysicsConstraint"));

	Camera = CreateDefaultSubobject<UCameraComponent>(TEXT("Camera"));

	Camera->bUsePawnControlRotation = true;
	Camera->SetupAttachment(GetRootComponent());

	Mesh1P = CreateDefaultSubobject<USkeletalMeshComponent>(TEXT("CharacterMesh1P"));

	Mesh1P->SetOnlyOwnerSee(true);
	Mesh1P->SetupAttachment(Camera);
	Mesh1P->bCastDynamicShadow = false;
	Mesh1P->CastShadow = false;

	HeldObjectSlot = CreateDefaultSubobject<UStaticMeshComponent>(TEXT("HeldObjectSlot"));
	HeldObjectSlot->SetRelativeLocation(Camera->GetRelativeLocation());

	HeldObjectSlot->AttachToComponent(Camera, FAttachmentTransformRules::KeepRelativeTransform);
	PhysicsConstraint->AttachToComponent(HeldObjectSlot, FAttachmentTransformRules::KeepRelativeTransform);

	Mesh1P->SetRelativeLocation(FVector(-30.f, 0.f, -150.f));
}

void ACharacter0::BeginPlay() {

	Super::BeginPlay();

	if (APlayerController* PlayerController = Cast<APlayerController>(GetController())) {
		if (UEnhancedInputLocalPlayerSubsystem* Subsystem = ULocalPlayer::GetSubsystem<UEnhancedInputLocalPlayerSubsystem>(PlayerController->GetLocalPlayer())) {

			Subsystem->AddMappingContext(PlayerMappingContext, 0);
		}
	}
}

#pragma region Input

void ACharacter0::SetupPlayerInputComponent(UInputComponent* PlayerInputComponent) {

	if (UEnhancedInputComponent* EnhancedInputComponent = CastChecked<UEnhancedInputComponent>(PlayerInputComponent)) {

		EnhancedInputComponent->BindAction(JumpAction, ETriggerEvent::Started,   this, &ACharacter::Jump);
		EnhancedInputComponent->BindAction(JumpAction, ETriggerEvent::Completed, this, &ACharacter::StopJumping);

		EnhancedInputComponent->BindAction(CrouchAction, ETriggerEvent::Triggered, this, &ACharacter0::StartCrouch);
		EnhancedInputComponent->BindAction(CrouchAction, ETriggerEvent::Completed, this, &ACharacter0::StopCrouch);
		
		EnhancedInputComponent->BindAction(MoveAction, ETriggerEvent::Triggered, this, &ACharacter0::Move);
		EnhancedInputComponent->BindAction(LookAction, ETriggerEvent::Triggered, this, &ACharacter0::Look);

		EnhancedInputComponent->BindAction(GrabAction, ETriggerEvent::Triggered, this, &ACharacter0::Grab);
		EnhancedInputComponent->BindAction(GrabAction, ETriggerEvent::Completed, this, &ACharacter0::Release);
		EnhancedInputComponent->BindAction(ThrowAction, ETriggerEvent::Triggered, this, &ACharacter0::Throw);
	}
}

void ACharacter0::StartCrouch() { Crouch(); }

void ACharacter0::StopCrouch() { UnCrouch(); }

void ACharacter0::Move(const FInputActionValue& Value) {

	FVector2D MovementVector = Value.Get<FVector2D>();

	if (Controller != nullptr) {
		
		AddMovementInput(GetActorForwardVector(), MovementVector.Y);
		AddMovementInput(GetActorRightVector(), MovementVector.X);
	}
}

void ACharacter0::Look(const FInputActionValue& Value) {
	
	FVector2D LookAxisVector = Value.Get<FVector2D>();

	if (Controller != nullptr) {
		
		AddControllerYawInput(LookAxisVector.X);
		AddControllerPitchInput(LookAxisVector.Y);
	}
}

inline FVector ACharacter0::GetReachLineStart() {
	return Camera->GetComponentLocation();
}

inline FVector ACharacter0::GetReachLineEnd() {
	return Camera->GetComponentLocation() + Camera->GetForwardVector() * Reach;
}

void ACharacter0::Grab() {

	FHitResult HitResult;

	GetWorld()->LineTraceSingleByObjectType(
		OUT HitResult, GetReachLineStart(), GetReachLineEnd(),
		FCollisionObjectQueryParams(ECollisionChannel::ECC_PhysicsBody),
		FCollisionQueryParams(FName(TEXT("PropHold")), false, GetOwner()));

	auto ComponentToGrab = HitResult.GetComponent();
	
	if (HitResult.GetActor()) {

		if (!ComponentToGrab->ComponentHasTag(FName("Prop"))) { return; }

		HRIP = HitResult.ImpactPoint;

		HeldObjectSlot->SetWorldLocation(ComponentToGrab->GetMass() > GrabMassBarrierKG ? HRIP : ComponentToGrab->GetComponentLocation());

		PhysicsConstraint->SetConstrainedComponents(HeldObjectSlot, FName(""), ComponentToGrab, FName(""));
		
		ComponentToGrab->SetCollisionResponseToChannel(ECollisionChannel::ECC_Pawn, ECollisionResponse::ECR_Ignore);
		
		HeldObj = ComponentToGrab;
	}
}

void ACharacter0::Release() {

	if (HeldObj == NULL) { return; }

	PhysicsConstraint->BreakConstraint();
	HeldObj->SetCollisionResponseToChannel(ECollisionChannel::ECC_Pawn, ECollisionResponse::ECR_Block);
	HeldObj = nullptr;
}

void ACharacter0::Throw() {

	if (HeldObj == NULL) { return; }

	PhysicsConstraint->BreakConstraint();
	HeldObj->SetCollisionResponseToChannel(ECollisionChannel::ECC_Pawn, ECollisionResponse::ECR_Block);
	
	HeldObj->AddImpulseAtLocation(Camera->GetForwardVector() * ThrowPower, HRIP);
	
	HeldObj = nullptr;
	HeldObjectSlot->SetRelativeLocation(Camera->GetRelativeLocation());
}

#pragma endregion

The basic idea was as follows: make objects player can grab/drag have a tag “Prop” to indecate that; make so if the mass of object player wants to grab is more than a certain amount - make them drag it instead; if the mass is less than a certain amount - player can grab and hold the object like normal.

that’s where this comes from:

HeldObjectSlot->SetWorldLocation(ComponentToGrab->GetMass() > GrabMassBarrierKG ? HRIP : ComponentToGrab->GetComponentLocation());

But… physics constraints aren’t meant for my purposes, once again.
I don’t know if it’s even possible to make so object can be both dragged and held depending on the circumstances, what am i even supposed to do now? Voices of the Void was also made on Unreal Engine (4th, to be exact), how did the dev manage to accomplish such divine result with being extremely close to the physics interaction to Amnesia: The Dark Descent?

I really need your help guys, documentation barely ever helps me, unlike people who actually care enough to go and make in-depth tutorials just to help people, you, guys… hopefully