본문 바로가기

개인 프로젝트(과제)

7번 과제(Pawn 클래스로 3D 캐릭터 만들기) 도전 과제

도전 과제 (선택 요구 사항)

도전 과제 1번 - 6자유도 (6 DOF) 드론/비행체 구현 (난이도 상)

  • 6축 이동 및 회전 액션 구현
    • 이동
      • 전/후 (W/S) - 로컬 X축 이동
      • 좌/우 (A/D) - 로컬 Y축 이동
      • 상/하 (Space/Shift) - 로컬 Z축 이동
    • 회전
      • Yaw - 좌우 회전, 마우스 X축 이동
      • Pitch - 상하 회전, 마우스 Y축 이동
      • Roll - 기울기 회전, 마우스 휠 또는 별도 키
  • Orientation 기반 이동 구현
    • 현재 Pawn의 회전 상태에 따라 이동 방향이 결정되는 비행체 움직임을 구현합니다.
    • 단순 월드 좌표계 이동이 아닌, Pawn의 로컬 좌표계 기준 이동을 구현합니다.

도전 과제 2번 - 중력 및 낙하 구현 (난이도 최상)

  • 인공 중력 구현
    • Tick 함수를 통해 매 프레임 중력 가속도를 직접 계산합니다.
    • 적절한 중력 상수 (예: -980 cm/s²)를 사용하여 낙하 속도를 구현합니다.
    • LineTrace 또는 SweepTrace를 사용하여 지면 충돌을 감지합니다.
    • 착지 시 Z축 속도를 0으로 초기화합니다.
  • 에어컨트롤 구현 (공중 WASD 제어)
    • 공중에서는 지상 이동속도의 30~50% 정도로 제한합니다.
    • 지상/공중 상태에 따라 이동 로직을 구분하여 자연스러운 움직임을 구현합니다.

6자유도 (6 DOF) 드론/비행체 구현

6축 이동

void AMovablePawn::Move(const FInputActionValue& value)
{
	if (!Controller) return;

	AddActorLocalOffset({ value.Get<FVector>().X * 8,value.Get<FVector>().Y * 8, 0 });
}

Z축 이동도 추가하기 위해 제일 먼저 IA_Move의 Value Type을 Vector2D에서 Vector로 수정하고 Move 함수도 이에 맞게 수정했다.(FVector2D -> FVector)

그리고 IMC에서 Space와 Shift를 매핑하고 Shift를 누를 때 내려가도록 모디파이어를 Negate로 설정하고 키보드 입력 값을 Z축 값으로 보내기 위해 Order를 ZYX로 설정했다.(ZXY도 상관없다고 한다.)

void AMovablePawn::Move(const FInputActionValue& value)
{
	if (!Controller) return;

	AddActorLocalOffset({ value.Get<FVector>().X * 8, value.Get<FVector>().Y * 8, value.Get<FVector>().Z * 8});
}

IA에서 입력받은 값을 Move 함수의 Z축에 넣는다. 테스트를 해보니 위 아래로 잘 움직인다.

 

회전 액션 구현

기울기를 담당할 새로운 IA_Roll을 만들고 ValueType은 좌우 기울기만 담당하기에 Axis1D로 설정했다.

IMC에서 Q를 눌렀을때 왼쪽으로 기울도록 Modifiers를 Negate로 설정했다.

 

이제 C++로 가서 입력받은 값을 활용해서 행동 로직을 구현해야한다.

우선 만든 IA_Roll을 플레이어 컨트롤러에 추가한다.

// 플레이어 컨트롤러.h
	UPROPERTY(EditDefaultsOnly, Category = "Input")
	UInputAction* Roll;
// 플레이어 컨트롤러.cpp 생성자
Roll = nullptr;

에디터에서 IA_Roll을 넣는다.

C++에서 Roll 함수를 만들어서 행동 로직을 구현한다.

// MovablePawn.h
UFUNCTION()
void Roll(const FInputActionValue& value);

MovablePawn을 스태틱 메시로 바꾸고 드론 스태틱 메시 에셋을 적용시켰다.

그리고 캡슐 컴포넌트에서 박스 컴포넌트로 바꾸고 크기를 맞춰줬다.

// MovablePawn.cpp

	BoxCollision = CreateDefaultSubobject<UBoxComponent>(TEXT("RootCollision"));
	SetRootComponent(BoxCollision);
	BoxCollision->SetSimulatePhysics(false);
    
	StaticMeshComp = CreateDefaultSubobject<UStaticMeshComponent>(TEXT("StaticMesh"));
	StaticMeshComp->SetupAttachment(BoxCollision);
	StaticMeshComp->SetSimulatePhysics(false);

여기까지 구현하고 다시 과제의 요구사항을 보니 도전 과제 1번의 회전 부분

  • 회전
    • Yaw - 좌우 회전, 마우스 X축 이동
    • Pitch - 상하 회전, 마우스 Y축 이동
    • Roll - 기울기 회전, 마우스 휠 또는 별도 키

위 요구사항이 어떤 회전을 말하는건지 잘 이해가 안됐는데

  • Orientation 기반 이동 구현
    • 현재 Pawn의 회전 상태에 따라 이동 방향이 결정되는 비행체 움직임을 구현합니다.
    • 단순 월드 좌표계 이동이 아닌, Pawn의 로컬 좌표계 기준 이동을 구현합니다.

이 요구사항을 보고 월드 좌표계 이동이 아닌 단순히 이 폰의 회전 방향에 따라서 움직임을 구현하기로 정했다.

그래서 필수 과제에서 구현했던 폰에는 Yaw 회전만 주고 스프링암에 Pitch 회전만 주는 로직을 스프링암 관련 로직은 모두 삭제하고 Pawn에 마우스의 입력값을 이용해 모든(Pitch, Yaw) 회전 값을 줬다.(Roll은 그대로 Q, E로 기울인다.)

// MovablePawn.cpp

void AMovablePawn::Look(const FInputActionValue& value)
{
	AddActorLocalRotation({ value.Get<FVector2D>().Y,value.Get<FVector2D>().X,0 });
}

void AMovablePawn::Roll(const FInputActionValue& value)
{
	AddActorLocalRotation({ 0,0,value.Get<float>()});
}

도전과제 1번 결과


중력 및 낙하 구현

도전과제 2번은 강의에서는 배운 내용이 하나도 없어서 아예 모르는 상태로 챗GPT를 이용해서 모르는 함수(라인트레이스)의 사용방법을 물어보고 어떤 방식으로 구현하면 좋을지는 스스로 생각했다.

인공 중력 구현

우선 Tick 함수를 사용하기 위해 PrimaryActorTick.bCanEverTick 를 false에서 true로 수정한다.

PrimaryActorTick.bCanEverTick = true;

그리고 중력과 낙하속도 변수를 선언하고 중력은 과제에서 나온 예시 -980.0f 값을 넣고 낙하속도는 중력상수를 초마다 누적하도록 Tick 함수에 구현했다.

// MovablePawn.h
	float Gravity;
	float VelocityZ;
// MovablePawn.cpp
// 생성자
	Gravity = -980.0f;
	VelocityZ = 0;
    
// Tick 함수
	VelocityZ += Gravity * DeltaTime;

이제 라인트레이스를 사용해서 MovablePawn이 현재 공중에 있는지를 판단해서 공중에 있을 때 VelocityZ가 누적이 되어 중력 효과를 받도록 할것이다.

MovablePawn이 공중에 있는걸 판단하려면 라인트레이스를 아래(바닥)방향으로 짧게 쏴서 바닥이 감지되면 바닥에 있는걸로 판단하여 VelocityZ를 0으로 초기화 할 것이다.

// MovablePawn.h
	FHitResult Hit; // 라인트레이스로 충돌 감지한 정보를 담는 변수
	FVector LineStart; // 라인트레이스 시작점
	FVector LineEnd; // 라인트레이스 끝점
	FCollisionQueryParams Params; // 트레이스할 때 규칙(조건?)추가
	bool bHit; // 현재 라인트레이스가 충돌한 상태인지(바닥인지)
// MovablePawn.cpp
// 생성자
	Params.AddIgnoredActor(this); // 자기 자신 무시하는 규칙
    
// Tick
void AMovablePawn::Tick(float DeltaTime)
{
	Super::Tick(DeltaTime);

	LineStart = GetActorLocation(); // 트레이스 시작점은 액터의 위치
	LineEnd = LineStart - FVector(0, 0, 50); // 액터에서 아래로 50cm 길이의 선을 쏨

	// 충돌했는지 판단
	bHit = GetWorld()->LineTraceSingleByChannel(
		Hit,
		LineStart,
		LineEnd,
		ECC_Visibility
	);

	if (!bHit) // 충돌 감지 안됐으면(공중에 있을 때) 중력속도 활성화
	{
		VelocityZ += Gravity * DeltaTime;
	}
	else // 감지 되면(바닥에 있을 때)
	{
		VelocityZ = 0;
	}

	AddActorLocalOffset(FVector(0, 0, VelocityZ * DeltaTime)); // 중력속도만큼 Z축으로 이동
}
ECC_Visibility와 Params

위처럼 구현하니 공중에 있을 때 중력 효과를 받아서 아래로 떨어지긴 하지만 중력속도가 무한대로 빨라져서 스페이스바를 꾹 눌러도 어느정도 높이로 오른 뒤 그 이후는 올라갈 수 없는 현상이 발생했다. 그래서 키를 입력받을 때는 중력효과가 발생하지 않도록 수정을 해야된다.

// MovablePawn.h
	bool bHasMoveInput; // 키가 입력중인지 판단하는 변수
// MovablePawn.cpp
void AMovablePawn::Move(const FInputActionValue& value)
{
	if (!Controller) return;

	bHasMoveInput = true; // Move함수에서 true
	AddActorLocalOffset({ value.Get<FVector>().X * Speed, value.Get<FVector>().Y * Speed, value.Get<FVector>().Z * 8});
}

// Tick함수
	if(!bHasMoveInput) // Move함수가 호출이 안될 때(키 입력이 없을 때)
	{
		if (!bHit) // 충돌 감지 안됐으면(공중에 있을 때) 중력속도 활성화
		{
			VelocityZ += Gravity * DeltaTime;
		}
		else // 감지 되면(바닥에 있을 때) 중력 0
		{
			VelocityZ = 0;
		}
	}
    
	AddActorLocalOffset(FVector(0, 0, VelocityZ * DeltaTime)); // 중력속도만큼 Z축으로 이동
	bHasMoveInput = false; // Tick 함수의 마지막 줄에 false

이제 키를 누르면 bHasMoveInput이 true가 되어 중력 관련 로직이 실행이 되지 않고 키를 떼면 false가 되어 중력 관련 로직이 활성화가 된다.

 

현재 중력 적용이 될때 AddActorLocalOffset 함수를 이용해서 드론이 회전한 상태로 중력효과를 받게되면 바닥방향이 아닌 드론의 아래 방향으로 이동이 되는 문제가 발견되어 AddActorWorldOffset 함수로 수정했다.

// Tick 함수
	AddActorWorldOffset(FVector(0, 0, VelocityZ * DeltaTime)); // 중력속도만큼 Z축으로 이동

중력 효과를 받는 도중에 키를 입력하면 VelocityZ가 초기화가 되지 않아서 떨어지면서 이동하는(제 속도를 못내는) 문제가 발생하여 키 입력을 받을 때(bHasMoveInput이 true일 때) VelocityZ를 0으로 초기화 했다.

// Tick 함수
	if(!bHasMoveInput) // Move함수가 호출이 안될 때(키 입력이 없을 때)
	{
		if (!bHit) // 충돌 감지 안됐으면(공중에 있을 때) 중력속도 활성화
		{
			VelocityZ += Gravity * DeltaTime;
		}
		else // 감지 되면(바닥에 있을 때) 중력 0
		{
			VelocityZ = 0;
		}
	}
	else // Move함수 호출될 때 (추가)
	{
		VelocityZ = 0;
	}

이제 인공 중력 구현은 구현이 됐고 에어컨트롤 구현이 남았다.

에어컨트롤 구현

공중에 있을 때 지상 이동속도의 50%로 제한하고 속도가 서서히 감소하도록 구현을 했다.

 

이동 속도를 담을 Speed 변수를 선언하고 생성자에서 8.0으로 초기화 했다.

// MovablePawn.h
	float Speed;
// MovablePawn.cpp
//생성자
	Speed = 8.0f;

Move함수에서 중력 구현을 할 때 선언한 bool bHit 변수를 활용하여 지상과 공중을 판단했다

// Movable.cpp
// Move함수
	if (bHit) // 지상에 있을 때
	{
		Speed = 8.0f;
	}
	else // 공중에 있을 때
	{
		if (Speed > 4.0f) // 4 초과일 때 서서히 감소
		{
			Speed *= 0.995f;
		}
		else // 4 이하가 되면 4 고정
		{
			Speed = 4.0f;
		}
	}

도전과제 2번 결과