결과물

상호작용이 가능한 손전등을 구현했습니다. 켜고 끌 수 있으며, 잡으면 Socket에 부착되도록 구현했습니다. 손 모양에 따라 물리적으로 잡는 기능을 생각해봤지만, 긴급한 상황에 손전등을 잡기 위해 컨트롤러와 손 메시의 모양을 신경쓰는 상황이 게임의 흐름을 해칠 것 같다고 판단하여 Socket 기반으로 구현했습니다.
구현 내용
1. AFlash.h / .cpp
레버는 레버 메시의 Socket에 손 메시를 부착하는 방식이었습니다. 하지만, 손전등의 경우 손전등 메시의 Socket이 아닌 손 메시의 Socket에 손전등을 부착하는 방식으로, 기존 방식과는 반대입니다. 때문에, 왼손으로 잡을 때와 오른손으로 잡을 때를 구분해주어야 합니다. 저는 왼손, 오른손 각각 손전등에 대한 Socket의 위치를 잡아주는 방식으로 구현을 했습니다. 매번 Socket의 위치를 각각 잡아줘야한다는 점이 단점인데요. 이후에 더 좋은 방법이 떠오르면 해당 방법으로 변경하도록 하겠습니다.
AFlash.h
더보기
#pragma once
#include "CoreMinimal.h"
#include "Actor/Grabbable/VRGrabbableActor.h"
#include "Interface/Interactable.h"
#include "Flash.generated.h"
class USpotLightComponent;
UCLASS()
class VIRTUALREALITY_API AFlash : public AVRGrabbableActor, public IInteractable
{
GENERATED_BODY()
// Lifecycle Section
public:
AFlash();
virtual void BeginPlay() override;
// IGrabbable Interface
public:
virtual void DoGrab(USkeletalMeshComponent* InComponent) override;
virtual void DoRelease(USkeletalMeshComponent* InComponent) override;
// IInteractable Interface
public:
virtual void Interact() override;
// Component Section
protected:
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "변수|컴포넌트")
TObjectPtr<USpotLightComponent> FlashLight;
// Grab Section
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "변수|소켓")
FName RightGrabSocketName = FName(TEXT("FlashSocket_R"));
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "변수|소켓")
FName LeftGrabSocketName = FName(TEXT("FlashSocket_L"));
};
AFlash.cpp
더보기
#include "Flash.h"
#include "Components/SpotLightComponent.h"
#include "Player/VRHand.h"
AFlash::AFlash()
{
static ConstructorHelpers::FObjectFinder<UStaticMesh> SM_Flash(TEXT("/Game/_VirtualReality/Mesh/Flash/SM_Flash"));
if (SM_Flash.Succeeded())
{
Mesh->SetStaticMesh(SM_Flash.Object);
}
FlashLight = CreateDefaultSubobject<USpotLightComponent>(TEXT("FlashLight"));
FlashLight->SetupAttachment(Mesh);
GrabbableType = EGrabbableType::Flash;
}
void AFlash::BeginPlay()
{
Super::BeginPlay();
FlashLight->SetVisibility(false);
}
void AFlash::DoGrab(USkeletalMeshComponent* InComponent)
{
Super::DoGrab(InComponent);
// HandType에 따라 SocketName을 다르게 적용합니다.
FName GrabSocketName;
CachedHand->GetHandType() == EControllerHand::Right ? GrabSocketName = RightGrabSocketName : GrabSocketName = LeftGrabSocketName;
// 메시의 물리엔진을 비활성화 하고 Socket에 부착합니다.
Mesh->SetSimulatePhysics(false);
bIsHeld = Mesh->AttachToComponent(InComponent, FAttachmentTransformRules::SnapToTargetNotIncludingScale, GrabSocketName);
if (bIsHeld)
{
GrabbedBySkeletalMesh = InComponent;
}
}
void AFlash::DoRelease(USkeletalMeshComponent* InComponent)
{
Super::DoRelease(InComponent);
if (bIsHeld)
{
if (GrabbedBySkeletalMesh == InComponent)
{
Mesh->SetSimulatePhysics(true);
}
}
FlashLight->SetVisibility(false);
}
void AFlash::Interact()
{
FlashLight->SetVisibility(!FlashLight->IsVisible());
}
2. IInteractable
이제 컨트롤러의 Index 버튼을 누르면 손전등이 켜지도록 구현을 해야 하는데요. Index 버튼 입력 시 Interface 기반으로 상호작용 함수를 호출하도록 구현했습니다.
IInteractable.h
더보기
#pragma once
#include "CoreMinimal.h"
#include "UObject/Interface.h"
#include "Interactable.generated.h"
UINTERFACE()
class UInteractable : public UInterface
{
GENERATED_BODY()
};
class VIRTUALREALITY_API IInteractable
{
GENERATED_BODY()
public:
virtual void Interact() = 0;
};
3. AVRHand.h / .cpp
Grab을 할 때 Overlap된 Actor를 캐싱해놓고, Index 입력이 들어오면, IInteractable 인터페이스로 캐스팅 검사를 수행하여 상호작용 함수를 호출하는 방식으로 구현했습니다.
AVRHand.h
더보기
#pragma once
#include "CoreMinimal.h"
#include "GameFramework/Actor.h"
#include "MotionControllerComponent.h"
#include "VRHand.generated.h"
class IInteractable;
enum class EGrabbableType : uint8;
class UPhysicsConstraintComponent;
class IGrabbable;
struct FInputActionValue;
class UVRHandAnimInstance;
class UInputAction;
class USphereComponent;
class UWidgetInteractionComponent;
class UVRHapticComponent;
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
public:
// GrabAction Binding Function
void GrabObject();
void ReleaseObject();
protected:
// HandGraspAction Binding Function
void DoHandGrasp(const FInputActionValue& InValue);
void StopHandGrasp();
// HandIndexCurl Binding Function
void DoInteract();
void DoHandIndexCurl(const FInputActionValue& InValue);
void StopHandIndexCurl();
// HandPoint Binding Function
void DoHandPoint();
void StopHandPoint();
// HandThumbUp Binding Function
void DoHandThumbUp();
void StopHandThumbUp();
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;
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "변수|컴포넌트")
TObjectPtr<UVRHapticComponent> HapticComponent;
// 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;
private:
uint8 bIsGrabbing : 1 = false;
EGrabbableType CurrentGrabbableType;
// Cached Section
private:
UPROPERTY()
TObjectPtr<UVRHandAnimInstance> AnimInstance;
UPROPERTY()
TObjectPtr<AActor> CurrentGrabbedActor;
TScriptInterface<IGrabbable> CachedGrabbable;
TScriptInterface<IInteractable> CachedInteractable;
FVector LastLocation;
FVector CurrentCalculatedVelocity;
public:
FORCEINLINE FVector GetHandVelocity() const { return CurrentCalculatedVelocity; }
FORCEINLINE uint8 GetIsGrabbing() const { return bIsGrabbing; }
FORCEINLINE EGrabbableType GetCurrentGrabbableType() const { return CurrentGrabbableType; }
FORCEINLINE FVector GetMotionControllerLocation() const { return MotionController->GetComponentLocation(); }
FORCEINLINE UVRHapticComponent* GetHapticComponent() const { return HapticComponent; }
FORCEINLINE EControllerHand GetHandType() const { return HandType; }
};
AVRHand.cpp
더보기
#include "VRHand.h"
#include "EnhancedInputComponent.h"
#include "InputActionValue.h"
#include "MotionControllerComponent.h"
#include "VirtualReality.h"
#include "Animation/VRHandAnimInstance.h"
#include "Component/VRHapticComponent.h"
#include "Components/SphereComponent.h"
#include "Components/WidgetInteractionComponent.h"
#include "Define/Define.h"
#include "Interface/Grabbable.h"
#include "Interface/Interactable.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(10.0f);
GrabCollision->SetCollisionEnabled(ECollisionEnabled::QueryOnly);
GrabCollision->SetCollisionResponseToAllChannels(ECR_Ignore);
GrabCollision->SetCollisionResponseToChannel(ECC_GRABBABLE, ECR_Overlap);
// HapticComponent 초기화
HapticComponent = CreateDefaultSubobject<UVRHapticComponent>(TEXT("HapticComponent"));
}
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::Started, this, &AVRHand::DoInteract);
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);
// HapticComponent에 손 타입을 전달합니다.
HapticComponent->Initialize(HandType);
}
void AVRHand::Tick(float DeltaSeconds)
{
Super::Tick(DeltaSeconds);
FVector CurrentLocation = MotionController->GetComponentLocation();
CurrentCalculatedVelocity = (CurrentLocation - LastLocation) / DeltaSeconds;
LastLocation = CurrentLocation;
}
void AVRHand::GrabObject()
{
// GrabCollision과 겹쳐있는 액터들 중 첫 번째 액터를 가져옵니다.
TArray<AActor*> OverlappedActors;
GrabCollision->GetOverlappingActors(OverlappedActors);
if (OverlappedActors.IsEmpty()) return;
CurrentGrabbedActor = OverlappedActors[0];
if (!CurrentGrabbedActor) return;
// 해당 액터가 잡을 수 있는 액터라면 OnGrab을 호출합니다.
// 만약, 이미 잡혀있는 상태라면 반환합니다.
CachedGrabbable = TScriptInterface<IGrabbable>(CurrentGrabbedActor);
if (CachedGrabbable)
{
if (CachedGrabbable->IsHeld())
{
CachedGrabbable = nullptr;
return;
}
bIsGrabbing = true;
CurrentGrabbableType = CachedGrabbable->GetGrabbableType();
CachedGrabbable->OnGrab(HandMesh);
}
}
void AVRHand::ReleaseObject()
{
bIsGrabbing = false;
if (CachedGrabbable)
{
CachedGrabbable->OnRelease(HandMesh);
CachedGrabbable = nullptr;
UpdateHandPhysicsBelow(BoneName, true, 0.15f);
}
CurrentGrabbedActor = 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::DoInteract()
{
if (CachedGrabbable)
{
CachedInteractable = TScriptInterface<IInteractable>(CurrentGrabbedActor);
CachedInteractable->Interact();
}
}
void AVRHand::StopHandIndexCurl()
{
AnimInstance->PoseAlphaIndexCurl = 0.0f;
}
void AVRHand::StopHandPoint()
{
AnimInstance->PoseAlphaPoint = 1.0f;
}
void AVRHand::StopHandThumbUp()
{
AnimInstance->PoseAlphaThumbUp = 1.0f;
}
마무리
공포게임에 손전등을 빼놓을 수 없죠. 손전등 기능을 구현하고 직접 작동시켜보니, 꽤 그럴듯한 비주얼로 표현이 되는 것을 확인할 수 있었습니다. 이제 저 문에서 무언가가 나올 것 같은 느낌을 연출해보겠습니다 ㅎㅎ
'Unreal Engine 프로젝트 > VR 공포게임' 카테고리의 다른 글
| [언리얼엔진] 13. 특수 이벤트 출력 구현 (1) | 2026.04.15 |
|---|---|
| [언리얼엔진] 12. 이벤트 출력 및 스캔 기능 구현 (0) | 2026.04.13 |
| [언리얼엔진] 10. 레버 중간 클래스 구현 및 오브젝트 모델링 (0) | 2026.04.10 |
| [언리얼엔진] 9. Floating Dust 파티클 구현 (0) | 2026.04.09 |
| [언리얼 엔진] 8. 채널 전환 버튼 구현 (0) | 2026.04.09 |