[언리얼엔진] 11. 손전등 기능 구현

2026. 4. 11. 14:31·Unreal Engine 프로젝트/VR 공포게임

결과물

상호작용이 가능한 손전등을 구현했습니다. 켜고 끌 수 있으며, 잡으면 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
'Unreal Engine 프로젝트/VR 공포게임' 카테고리의 다른 글
  • [언리얼엔진] 13. 특수 이벤트 출력 구현
  • [언리얼엔진] 12. 이벤트 출력 및 스캔 기능 구현
  • [언리얼엔진] 10. 레버 중간 클래스 구현 및 오브젝트 모델링
  • [언리얼엔진] 9. Floating Dust 파티클 구현
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
[언리얼엔진] 11. 손전등 기능 구현
상단으로

티스토리툴바