[언리얼엔진] 3. VR Hand 물리 효과 구현

2026. 4. 4. 22:50·Unreal Engine 프로젝트/VR 공포게임

결과물

이제 Hand 메시에 물리 효과를 적용해서 오브젝트를 통과하지 않게끔 구현했습니다. 결과물을 보시면 가상의 메시는 뚫고 있지만, 실제 Hand 메시는 오브젝트를 뚫지 않고 막히는 모습을 확인할 수 있습니다.

 


 

구현 내용

1. AVRHand.h / .cpp

새롭게 SceneComponent 를 추가해서 기존 Hand 메시를 MotionController 로부터 분리시켰습니다. 그리고 가상의 Hand 메시를 MotionController의 자식으로 추가해서 PhysicsConstraint 로 기존 Hand 메시가 가상의 Hand 메시를 따라다니도록 구현했습니다.
기존 Hand 메시에는 Physics Asset을 추가해서 물리 효과를 구현했고, **SetAllBodiesBelowSimulatePhysics**와 **SetAllBodiesBelowPhysicsBlendWeight**을 통해서 물리 효과와 애니메이션 사이의 적절한 보간 비율을 설정해주었습니다.

AVRHand.h

더보기
#pragma once

#include "CoreMinimal.h"
#include "GameFramework/Actor.h"
#include "VRHand.generated.h"

class UPhysicsConstraintComponent;
class IGrabbable;
struct FInputActionValue;
class UVRHandAnimInstance;
class UInputAction;
class USphereComponent;
class UWidgetInteractionComponent;
class UMotionControllerComponent;

UCLASS()
class VIRTUALREALITY_API AVRHand : public AActor
{
	GENERATED_BODY()
	
public:
	AVRHand();
	virtual void OnConstruction(const FTransform& Transform) override;
	virtual void PostInitializeComponents() override;
	virtual void BeginPlay() override;
	virtual void Tick(float DeltaSeconds) override;
	
public:
	void SetupPlayerInputComponent(UInputComponent* PlayerInputComponent);
	
	
// Action Binding Function Section	
protected:
	// HandGraspAction Binding Function
	void DoHandGrasp(const FInputActionValue& InValue);
	void StopHandGrasp();
	
	// HandIndexCurl Binding Function
	void DoHandIndexCurl(const FInputActionValue& InValue);
	void StopHandIndexCurl();
	
	// HandPoint Binding Function
	void DoHandPoint();
	void StopHandPoint();
	
	// HandThumbUp Binding Function
	void DoHandThumbUp();
	void StopHandThumbUp();
	
	// GrabAction Binding Function
	void GrabObject();
	void ReleaseObject();
	
private:
	/** 애니메이션과 물리 엔진의 비율을 설정하는 함수입니다. */
	void UpdateHandPhysicsBelow(FName InBoneName, bool bNewSimulate, float InBlendWeight);
	
	
// Component Section	
protected:
	UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "변수|컴포넌트")
	TObjectPtr<USceneComponent> Root;
	
	UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "변수|컴포넌트")
	TObjectPtr<UMotionControllerComponent> MotionController;
	
	UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "변수|컴포넌트")
	TObjectPtr<USkeletalMeshComponent> HandMesh;
	
	UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "변수|컴포넌트")
	TObjectPtr<USkeletalMeshComponent> VirtualHandMesh;
	
	UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "변수|컴포넌트")
	TObjectPtr<UPhysicsConstraintComponent> PhysicsConstraint;
	
	UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "변수|컴포넌트")
	TObjectPtr<UWidgetInteractionComponent> WidgetInteractionComponent;
	
	UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "변수|컴포넌트")
	TObjectPtr<USphereComponent> GrabCollision;
	
	
// Input Section
protected:
	UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "변수|입력")
	TObjectPtr<UInputAction> HandGraspAction;
	
	UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "변수|입력")
	TObjectPtr<UInputAction> HandIndexCurlAction;
	
	UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "변수|입력")
	TObjectPtr<UInputAction> HandPointAction;
	
	UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "변수|입력")
	TObjectPtr<UInputAction> HandThumbUpAction;
	
	UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "변수|입력")
	TObjectPtr<UInputAction> GrabAction;


// Variable Section	
protected:	
	UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "변수|손")
	EControllerHand HandType;
	
	UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "변수|손")
	uint8 bMirrorAnimation : 1 = false;
	
	UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "변수|손")
	FName BoneName;
	
	
	
// Cached Section	
private:
	UPROPERTY()
	TObjectPtr<UVRHandAnimInstance> AnimInstance;
	
	TScriptInterface<IGrabbable> CurrentlyGrabbedActor;
	
	FVector LastLocation;
	FVector CurrentCalculatedVelocity;
	
public:
	FORCEINLINE FVector GetHandVelocity() const { return CurrentCalculatedVelocity; }
	
};

 

AVRHand.cpp

더보기
#include "VRHand.h"
#include "EnhancedInputComponent.h"
#include "InputActionValue.h"
#include "MotionControllerComponent.h"
#include "VirtualReality.h"
#include "Animation/VRHandAnimInstance.h"
#include "Components/SphereComponent.h"
#include "Components/WidgetInteractionComponent.h"
#include "Define/Define.h"
#include "Interface/Grabbable.h"
#include "PhysicsEngine/PhysicsConstraintComponent.h"


AVRHand::AVRHand()
{
	PrimaryActorTick.bCanEverTick = true;
	
	// SceneComponent 초기화
	Root = CreateDefaultSubobject<USceneComponent>(TEXT("Root"));
	SetRootComponent(Root);
	
	// MotionController 초기화
	MotionController = CreateDefaultSubobject<UMotionControllerComponent>(TEXT("MotionController"));
	MotionController->SetupAttachment(Root);
	
	// 가상 HandMesh 초기화
	VirtualHandMesh = CreateDefaultSubobject<USkeletalMeshComponent>(TEXT("VirtualHandMesh"));
	VirtualHandMesh->SetupAttachment(MotionController);
	VirtualHandMesh->SetGenerateOverlapEvents(false);
	VirtualHandMesh->SetCollisionEnabled(ECollisionEnabled::QueryOnly);
	VirtualHandMesh->SetClothMaxDistanceScale(ECR_Overlap);
	
	// PhysicsConstraint 초기화
	PhysicsConstraint = CreateDefaultSubobject<UPhysicsConstraintComponent>(TEXT("PhysicsConstraint"));
	PhysicsConstraint->SetupAttachment(MotionController);
	
	// HandMesh 초기화
	HandMesh = CreateDefaultSubobject<USkeletalMeshComponent>(TEXT("HandMesh"));
	HandMesh->SetupAttachment(Root);
	HandMesh->SetSimulatePhysics(true);
	HandMesh->SetCollisionProfileName(FName("PhysicsActor"));
	
	// WidgetInteractionComponent 초기화
	WidgetInteractionComponent = CreateDefaultSubobject<UWidgetInteractionComponent>(TEXT("WidgetInteractionComponent"));
	WidgetInteractionComponent->SetupAttachment(HandMesh);
	
	// GrabCollision 초기화
	GrabCollision = CreateDefaultSubobject<USphereComponent>(TEXT("GrabCollision"));
	GrabCollision->SetupAttachment(HandMesh);
	GrabCollision->SetSphereRadius(15.0f);
	GrabCollision->SetCollisionEnabled(ECollisionEnabled::QueryOnly);
	GrabCollision->SetCollisionResponseToAllChannels(ECR_Ignore);
	GrabCollision->SetCollisionResponseToChannel(ECC_GRABBABLE, ECR_Overlap);
}

void AVRHand::SetupPlayerInputComponent(UInputComponent* PlayerInputComponent)
{
	if (UEnhancedInputComponent* EIC = Cast<UEnhancedInputComponent>(PlayerInputComponent))
	{
		if (HandGraspAction)
		{
			EIC->BindAction(HandGraspAction, ETriggerEvent::Triggered, this, &AVRHand::DoHandGrasp);
			EIC->BindAction(HandGraspAction, ETriggerEvent::Canceled, this, &AVRHand::StopHandGrasp);
			EIC->BindAction(HandGraspAction, ETriggerEvent::Completed, this, &AVRHand::StopHandGrasp);
		}
		
		if (HandIndexCurlAction)
		{
			EIC->BindAction(HandIndexCurlAction, ETriggerEvent::Triggered, this, &AVRHand::DoHandIndexCurl);
			EIC->BindAction(HandIndexCurlAction, ETriggerEvent::Canceled, this, &AVRHand::StopHandIndexCurl);
			EIC->BindAction(HandIndexCurlAction, ETriggerEvent::Completed, this, &AVRHand::StopHandIndexCurl);
		}
		
		if (HandPointAction)
		{
			EIC->BindAction(HandPointAction, ETriggerEvent::Started, this, &AVRHand::DoHandPoint);
			EIC->BindAction(HandPointAction, ETriggerEvent::Canceled, this, &AVRHand::DoHandPoint);
			EIC->BindAction(HandPointAction, ETriggerEvent::Completed, this, &AVRHand::StopHandPoint);
		}
		
		if (HandThumbUpAction)
		{
			EIC->BindAction(HandThumbUpAction, ETriggerEvent::Started, this, &AVRHand::DoHandThumbUp);
			EIC->BindAction(HandThumbUpAction, ETriggerEvent::Canceled, this, &AVRHand::DoHandThumbUp);
			EIC->BindAction(HandThumbUpAction, ETriggerEvent::Completed, this, &AVRHand::StopHandThumbUp);
		}
		
		if (GrabAction)
		{
			EIC->BindAction(GrabAction, ETriggerEvent::Started, this, &AVRHand::GrabObject);
			EIC->BindAction(GrabAction, ETriggerEvent::Canceled, this, &AVRHand::ReleaseObject);
			EIC->BindAction(GrabAction, ETriggerEvent::Completed, this, &AVRHand::ReleaseObject);
		}
	}
}

void AVRHand::OnConstruction(const FTransform& Transform)
{
	Super::OnConstruction(Transform);

	switch (HandType)
	{
		case EControllerHand::Left:
			MotionController->MotionSource = FName("Left");
			BoneName = FName("hand_l");
			break;
		case EControllerHand::Right:	
			MotionController->MotionSource = FName("Right");
			BoneName = FName("hand_r");
			break;
		default:
			break;
	}
}

void AVRHand::PostInitializeComponents()
{
	Super::PostInitializeComponents();

	AnimInstance = Cast<UVRHandAnimInstance>(HandMesh->GetAnimInstance());
}

void AVRHand::BeginPlay()
{
	Super::BeginPlay();
	
	AnimInstance->bIsMirror = bMirrorAnimation;
	
	UpdateHandPhysicsBelow(BoneName, true, 0.15f);
}

void AVRHand::Tick(float DeltaSeconds)
{
	Super::Tick(DeltaSeconds);
	
	FVector CurrentLocation = MotionController->GetComponentLocation();
	CurrentCalculatedVelocity = (CurrentLocation - LastLocation) / DeltaSeconds;
	LastLocation = CurrentLocation;
	
	GEngine->AddOnScreenDebugMessage(-1, 0.0f, FColor::Green, FString::Printf(TEXT("Hand Speed: %.2f"), CurrentCalculatedVelocity.Size()));
}

void AVRHand::GrabObject()
{
	TArray<AActor*> OverlappedActors;
	GrabCollision->GetOverlappingActors(OverlappedActors);
	if (OverlappedActors.IsEmpty()) return;
	
	AActor* FirstActorUnderCollision = OverlappedActors[0];
	if (!FirstActorUnderCollision) return;
	
	CurrentlyGrabbedActor = TScriptInterface<IGrabbable>(FirstActorUnderCollision);
	if (CurrentlyGrabbedActor)
	{
		CurrentlyGrabbedActor->OnGrab(HandMesh, GrabCollision->GetComponentLocation());
	}
}

void AVRHand::ReleaseObject()
{
	if (CurrentlyGrabbedActor)
	{
		CurrentlyGrabbedActor->OnRelease(HandMesh);
		CurrentlyGrabbedActor = nullptr;
	}
}

void AVRHand::UpdateHandPhysicsBelow(FName InBoneName, bool bNewSimulate, float InBlendWeight)
{
	if (HandMesh)
	{
		HandMesh->SetAllBodiesBelowSimulatePhysics(InBoneName, bNewSimulate, true);
		HandMesh->SetAllBodiesBelowPhysicsBlendWeight(InBoneName, InBlendWeight, false, true);
	}
}


void AVRHand::DoHandGrasp(const FInputActionValue& InValue)
{
	const float ActionValue = InValue.Get<float>();
	
	AnimInstance->PoseAlphaGrasp = ActionValue;
}

void AVRHand::DoHandIndexCurl(const FInputActionValue& InValue)
{
	const float ActionValue = InValue.Get<float>();

	AnimInstance->PoseAlphaIndexCurl = ActionValue;
}

void AVRHand::DoHandPoint()
{	
	AnimInstance->PoseAlphaPoint = 0.0f;
}

void AVRHand::DoHandThumbUp()
{	
	AnimInstance->PoseAlphaThumbUp = 0.0f;
}

void AVRHand::StopHandGrasp()
{
	AnimInstance->PoseAlphaGrasp = 0.0f;
}

void AVRHand::StopHandIndexCurl()
{
	AnimInstance->PoseAlphaIndexCurl = 0.0f;
}

void AVRHand::StopHandPoint()
{
	AnimInstance->PoseAlphaPoint = 1.0f;
}

void AVRHand::StopHandThumbUp()
{
	AnimInstance->PoseAlphaThumbUp = 1.0f;
}

 


 

2. Physical Asset

손가락 관절마다의 Constraint 를 추가해서 정밀한 물리 효과를 구현하고자 했습니다.

 


 

3. PhysicsConstraintComponent

PhysicsConstraintComponent의 속성 값은 다음과 같이 설정해주었습니다.

VRHand 컴포넌트 계층 구조
Constraint 설정
Limits 및 Motor 설정

 


 

마무리

손의 물리 엔진을 구현하고나니 오브젝트와의 상호작용이 훨씬 자연스러워졌습니다. 이제 남은 건 Procedural Grip 기능인데, 며칠동안 찾아봐도 C++을 활용해서 구현한 사례는 전혀 없고, 오직 블루프린트 강의만 있는 상태라 쉽지가 않네요 .. 그래도 완성될 결과물을 생각해서라도 열심히 해봐야겠습니다 ! ㅎㅎ 화이팅입니다 ~

 

'Unreal Engine 프로젝트 > VR 공포게임' 카테고리의 다른 글

[언리얼엔진] 6. VR Hand 애니메이션 제작  (0) 2026.04.09
[언리얼엔진] 5. VR 최적화  (0) 2026.04.09
[언리얼엔진] 4. VR 레버 구현  (2) 2026.04.08
[언리얼엔진] 2. VR 버튼 구현  (0) 2026.04.04
[언리얼엔진] 1. VR Grab/Release 기능 구현 (C++)  (0) 2026.04.04
'Unreal Engine 프로젝트/VR 공포게임' 카테고리의 다른 글
  • [언리얼엔진] 5. VR 최적화
  • [언리얼엔진] 4. VR 레버 구현
  • [언리얼엔진] 2. VR 버튼 구현
  • [언리얼엔진] 1. VR Grab/Release 기능 구현 (C++)
Meoyoung's Development Logs
Meoyoung's Development Logs
내가 보려고 만든 블로그
  • Meoyoung's Development Logs
    이게뭐영
    Meoyoung's Development Logs
  • 전체
    오늘
    어제
    • 분류 전체보기 (289)
      • Unreal Engine 프로젝트 (36)
        • 더 퍼스트 버서커 : 카잔 (16)
        • VR 공포게임 (13)
        • Paper-ZD (7)
      • 언리얼 엔진 (72)
        • GAS (10)
        • 트러블슈팅 (27)
        • 캐릭터 (2)
        • VR (1)
        • Lighting (2)
        • 멀티스레드 (2)
        • Lyra (1)
      • C++ (31)
        • 문법 정리 (8)
        • [서적] Fundamental C++ 프로그래밍 .. (5)
        • [서적] 이것이 C++이다 (11)
        • [서적] Effective C++ (7)
      • 게임잼 (3)
      • 강의 (36)
        • [강의] 이득우의 언리얼 프로그래밍 Part1 (13)
        • [강의] 이득우의 언리얼 프로그래밍 Part2 (2)
        • [강의] 이득우의 언리얼 프로그래밍 Part3 (12)
        • [강의] 소울라이크 개발 A-Z (4)
        • [강의] Udemy-2D (5)
      • C# (1)
        • [서적] 이것이 C#이다 (1)
      • 코딩테스트 (26)
        • 프로그래머스 (6)
        • 알고리듬 (13)
        • 자료구조 (7)
      • 컴퓨터 과학 (27)
        • 운영체제 (11)
        • 데이터베이스 (0)
        • 디자인패턴 (0)
        • 자료구조 (5)
        • 네트워크 (0)
        • 컴퓨터구조 (11)
      • 면접준비 (0)
        • C++ (0)
        • 운영체제 (0)
        • 자료구조 (0)
      • 기타 (48)
        • [팀프로젝트] The Fourth Descenda.. (5)
        • GetOutOf (15)
        • [개인프로젝트] FPS 구현 맛보기 (5)
        • [서적] 인생 언리얼5 (4)
        • 스파르타코딩클럽 (15)
        • 객체지향프로그래밍 (2)
        • 컴퓨터회로 (2)
  • 블로그 메뉴

    • 홈
    • 태그
    • 방명록
  • 링크

  • 공지사항

  • 인기 글

  • 태그

    게임개발
    셸정렬
    선택정렬
    쉘정렬
    알고리즘
    삽입정렬
    참가후기
    버블정렬
    게임잼
    경북게임잼
    자료구조
  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.3
Meoyoung's Development Logs
[언리얼엔진] 3. VR Hand 물리 효과 구현
상단으로

티스토리툴바