본문 바로가기
언리얼엔진 개발/VR

[UnrealEngine5] VR 나사 돌리기 만들기(1)

by yeni_0224 2023. 5. 11.
728x90
반응형
VR 게임 나사 돌리기 유사 시뮬레이션?
VR Screw Simulation

https://yeni-0224.tistory.com/entry/UnrealEngine%EB%B2%84%ED%8A%BC%EC%9D%84-%EB%88%84%EB%A5%B4%EB%A9%B4-%EB%AC%B8-%EC%97%B4%EA%B3%A0-%EB%8B%AB%EA%B8%B0-C-1

 

[UnrealEngine5]버튼을 누르면 문 열고 닫기 C++ (1)

현재 VR 게임을 개발 중에 있다. 지난 짧은 기간동안 언리얼엔진을 열심히 공부하고, 정말 이제는 공부한 내용들을 실전 적용해야할 때가 온 것이다. 작은 부분이라도 로직을 짜고, 코딩해나가는

yeni-0224.tistory.com

VR 게임을 만들고 있는 중이다. 아트도 하고싶고 기술도 하고싶은, 두마리 토끼를 다 잡고 싶은 나는 아트도 하면서 코딩도 하고있다. 말로 구성하면 너무너무 쉬운데 막상 구현하기는 어렵다. 물론 나에게는.

그래도 악바리로 해내고싶은 마음이 크기 때문에 끝까지 최선을 다하는 편이다. 서론이 이렇게 긴 이유는 나사와의 씨름 끝에 해냈기 때문이다! 물론 완벽하다고 할 수 없지만 그래도 해냈다는게 어디인가.

아직 완벽히 마무리했다고는 할 수 없다. 테스트하고, 게임 제작하면서 발생하는 문제들을 계속 수정해나가야하기 때문이다. 수정사항이 발생하면 계속 글을 업데이트 할 것이다.

 

플러그인을 사용하지 않고 구현해냈다. 완전히 과학을 기반으로 한 물리적인 현상을 반영하여 제작했다고 할 수는 없다. 물리적인 부분까지 들어가는건 매우 어렵기 때문이다. 비전공자인 내가 구현할 수 있는 최선의 방법으로 구현했다.

5번 넘게 갈아엎은 것 같다. 그래서 해냈기에 더더욱 기쁘다.

 

구현 클래스

1. Screw Driver

2. Screw 

 

전제

손으로 물체를 잡으면 손에 물체가 attach되고, 잡힌 물체는 SetSimulatedPhysics == false 인 상태가 된다.


1. Work Flow

1) 내가 구현해야할 내용을 정리했다.

구현 내용은 최대한 상세하게, 순서대로 작성해주었다. 이 구현해야할 내용은 계속 바뀌었다. 

같은 내용이어도 어떻게 구현해나갈지의 방향성도 바뀌었다.

나사를 구현하는 과정에서 가장 중요한 것은 회전이다. 덕분에 회전에 대한 이해력이 상승했다.

 

/*나사*/

벽에 붙어있는 나사

드라이버에 닿으면 드라이버의 회전에 따라 같이 회전할 것이다.

회전하면서 동시에 플레이어의 방향으로 나올것이다.

일정 거리 이상 나오면 못은 다 빠져나온 것이다.

다 빠져나오면 무게, 중력이 적용되고

바닥으로 떨어진다.

 

/*드라이버*/

손이 드라이버에 닿으면

무게, 중력이 사라진다.

>>>>>>>>>>>>>>>>>>

이후에 드라이버가 회전하는 만큼 나사도 회전한다

이런 형식으로 작업했다. 

2. 드라이버 (Screw Driver)

UPROPERTY(EditDefaultsOnly, Category = "ScrewDriver")
	class UBoxComponent* boxComp;
	UPROPERTY(EditDefaultsOnly, Category = "ScrewDriver")
	class UStaticMeshComponent* meshComp;
	UPROPERTY(EditDefaultsOnly, Category = "ScrewDriver")
	class UGrabComponent* grabComp;
	UPROPERTY(EditDefaultsOnly, Category = "ScrewDriver")
	class UArrowComponent* arrowComp;

	//드라이버에 나사가 닿으면
	UFUNCTION()
	void AttachtoScrew(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex, bool bFromSweep, const FHitResult& SweepResult);
	//드라이버에서 나사가 떨어지면
	UFUNCTION()
	void DettachFromScrew(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex);

	//닿았는가
	bool isAttaching;	

	//드라이버의 실시간 회전값
	float driverRot;
	//드라이버의 이전 회전값
	float prevDriverRot;
	//변화되는 값?
	float deltaRot;

 arrow Component를 사용해서 오브젝트의 정면방향을 정해주고, 이를 기준으로 회전값을 적용했다.

드라이버가 닿았는지 여부가 중요하다.

grabComponent : 손으로 물체를 잡을 수 있도록 하는 component이고 어떻게 잡을 것인지도 정할 수 있다.

드라이버 자체의 Collision은 삭제해주고, 오직 드라이버의 끝부분으로 나사와의 접촉을 감지할 수 있도록 했다.

테스트를 위해 임시로 boxCollision의 사이즈는 크게 설정했다.

boxComp = CreateDefaultSubobject<UBoxComponent>(TEXT("Screw Collision"));
	SetRootComponent(boxComp);
	boxComp->SetBoxExtent(FVector(5, 5, 50));
	boxComp->SetCollisionProfileName(TEXT("PuzzleObjectPreset"));
	meshComp = CreateDefaultSubobject<UStaticMeshComponent>(TEXT("ScrewMesh"));
	meshComp->SetupAttachment(RootComponent);
	meshComp->SetRelativeLocation(FVector(0, -8, 0));
	meshComp->SetRelativeRotation(FRotator(90, 0, 90));
	meshComp->SetRelativeScale3D(FVector(5));
	ConstructorHelpers::FObjectFinder<UStaticMesh> dMesh(TEXT("/Script/Engine.StaticMesh'....'"));
	if(dMesh.Succeeded())
	{
		meshComp->SetStaticMesh(dMesh.Object);
	}
	grabComp = CreateDefaultSubobject<UGrabComponent>(TEXT("GrabComp"));
	grabComp->SetupAttachment(RootComponent);
	arrowComp = CreateDefaultSubobject<UArrowComponent>(TEXT("ArrowComp"));
	arrowComp->SetupAttachment(RootComponent);

생성자 부분이다. 이부분의 hierarchy 부분도 많이 수정하면서 작업했는데, 이유는 알 수 없으나 grabComponent가 어디 붙어있는지에 따라 드라이버의 grab이 먹히지 않기도 하고 나사와의 접촉도 안되고, 심지어는 box Collision이 mesh와 분리되는 현상까지 발생했다. grab Component는 일단 항상 RootComponent에 붙여주는 것이 좋겠다.

 

아직 BeginPlay에서는 현재 충돌 Binding만 해주었다.

void AScrewDriver::BeginPlay()
{
	Super::BeginPlay();
	boxComp->OnComponentBeginOverlap.AddDynamic(this, &AScrewDriver::AttachtoScrew);	
	boxComp->OnComponentEndOverlap.AddDynamic(this, &AScrewDriver::DettachFromScrew);
}

Tick에서는 드라이버의 회전 값을 확인하도록 해주었는데, .Roll 이 것 자체가 첫 회전 위치와 다음 회전 위치 사이의 값을 의미한다고 한다. 회전을 감지하는건 회전하고있다! 는 전제 하에서 회전값을 알아내야하기 때문에 조건을 달아주었다.

현재 회전하고있는 값에서 최초 회전값을 빼주었다.

void AScrewDriver::Tick(float DeltaTime)
{
	Super::Tick(DeltaTime);	
	if (isAttaching == true)
	{
		deltaRot = GetActorRotation().Roll - prevDriverRot;
	}	
}

 드라이버와 나사가 닿았을 때 bool값을 true로 해주고, 나사와 닿았을 때 최초의 위치값을 저장해준다. Tick에서 회전값을 계산해줘야하기 때문이다.

AScrew* attachedScrew = Cast<AScrew>(OtherActor);
	if (attachedScrew != nullptr)
	{
		//UE_LOG(LogTemp, Warning, TEXT("OtherActor = %s"), *OtherActor->GetName())
		isAttaching = true;
		//닿았을 때 최초 회전값
		prevDriverRot = GetActorRotation().Roll;
	}

여기서 발생한 회전값을 바탕으로 이제 나사를 돌려줄 것이다.

3. 나사 (Screw)

일단은 나사에 착! 하고 자석같이 달라붙게는 하지 않았다. 나중에 수정할지도 모르지만

	/*컴포넌트 생성*/
	UPROPERTY(EditDefaultsOnly, Category = "Screw")
	class UBoxComponent* boxComp;
	UPROPERTY(EditDefaultsOnly, Category = "Screw")
	class UStaticMeshComponent* meshComp;
	UPROPERTY(EditDefaultsOnly, Category = "Screw")
	class UArrowComponent* arrowComp;
	UPROPERTY(EditDefaultsOnly, Category = "ScrewDriver")
	class UGrabComponent* grabComp;

	UPROPERTY()
	class AScrewDriver* driver;

나사 역시도 회전이 중요하기 때문에 arrowComponent를 달아서 회전의 중심을 잡아주었다.

나사와의 상호작용이 중요하기 때문에 driver class 변수도 하나 만들어주었다.

boxComp = CreateDefaultSubobject<UBoxComponent>(TEXT("Screw Collision"));
	SetRootComponent(boxComp);
	boxComp->SetBoxExtent(FVector(2, 1, 3));
	boxComp->SetRelativeRotation(FRotator(0, 0, 0));
	meshComp = CreateDefaultSubobject<UStaticMeshComponent>(TEXT("ScrewMesh"));
	meshComp->SetupAttachment(RootComponent);
	arrowComp = CreateDefaultSubobject<UArrowComponent>(TEXT("ArrowComp"));
	arrowComp->SetupAttachment(RootComponent);
	grabComp = CreateDefaultSubobject<UGrabComponent>(TEXT("GrabComp"));
	grabComp->SetupAttachment(RootComponent);

hierarchy 부분 확인하고!

 

다음 게시물에 계속...

 

728x90
반응형