KoreanFoodie's Study

이득우의 언리얼 C++ 6 : 캐릭터의 제작과 컨트롤 본문

Game Dev/Unreal C++ : Tutorial

이득우의 언리얼 C++ 6 : 캐릭터의 제작과 컨트롤

GoldGiver 2022. 2. 28. 19:11

이득우님의 "이득우의 언리얼 C++ 게임 개발의 정석" 책을 따라가며 실습한 내용을 정리한 포스팅입니다. 실습에 필요한 자료들은 이 링크에서, 제가 작업한 예제 소스 완성본은 여기에서 찾아보실 수 있습니다. (저는 언리얼 4.27.2 버전 기준으로 작업하였습니다)

캐릭터 모델

Pawn 대신 Character 액터를 만들어 조작해보자. Character 액터는 Pawn 액터를 상속받는데, Pawn 과 다른 점은 CharacterMovement 컴포넌트를 사용해 움직임을 관리한다는 것이다(Actor>>Pawn>>Character). 이 컴포넌트가 FloatingMovement 에 비해 가지는 장점은 다음과 같다.

  1. 점프와 같은 중력을 반영한 움직임을 제공
  2. 다양한 움직임 설정 가능(기어가기, 날아가기, 수영하기 등등) + 현재 움직임에 좀 더 많은 정보 전달
  3. 멀티 플레이 네트워크 환경에서 캐릭터들의 움직임을 자동으로 동기화한다.

 

 

GTA vs DIABLO 시점 및 조작 구현

이번 포스팅에서는 GTA와 DIABLO 스타일의 카메라 이동 및 조작 설정을 해 보도록 하자.

 

GTA (삼인칭 컨트롤 구현) :

GTA 방식은 기본적인 흰색 마네킹과 동일한 방식으로 작동한다. 세부 특징은 다음과 같다.

  • 캐릭터의 이동 : 현재 보는 시점을 기준으로 상하, 좌우 방향으로 마네킹이 이동하고 카메라는 회전하지 않음
  • 캐릭터의 회전 : 캐릭터가 이동하는 방향으로 마네킹이 회전함
  • 카메라 지지대 길이 : 450cm
  • 카메라 회전 : 마우스 상하좌우 이동에 따라 카메라 지지대가 상하좌우로 회전
  • 카메라 줌 : 카메라 시선과 캐릭터 사이에 장애물이 감지되면 캐릭터가 보이도록 카메라를 장애물 앞으로 줌인

 

DIABLO (삼인칭 컨트롤 구현) :

DIABLO 방식은 고정된 삼인칭 시점에서 캐릭터를 따라다니는 컨트롤이다. 세부 특징은 다음과 같다.

  • 캐릭터의 이동 : 상하좌우 키를 조합해 캐릭터가 이동할 방향을 결정
  • 캐릭터의 회전 : 캐릭터는 입력한 방향으로 회전
  • 카메라 지지대 길이 : 800cm
  • 카메라 회전 : 카메라의 회전 없이 항상 고정 시선으로 45도로 내려다봄
  • 카메라 줌 : 없음. 카메라와 캐릭터 사이에 장애물이 있는 경우 외곽선으로 처리

 

먼저 캐릭터를 만든 후, GameMode 에서 캐릭터를 플레이어 컨트롤러로 사용하도록 설정한다.

 

ABGameMode.cpp

#include "ABCharacter.h"

AABGameMode::AABGameMode()
{
	DefaultPawnClass = AABCharacter::StaticClass();
	PlayerControllerClass = AABPlayerController::StaticClass();

	//static ConstructorHelpers::FClassFinder<APawn> BP_PAWN_C(TEXT("/Game/ThirdPersonCPP/Blueprints/ThirdPersonCharacter.ThirdPersonCharacter_C"));
	//if (BP_PAWN_C.Succeeded())
	//{
	//	DefaultPawnClass = BP_PAWN_C.Class;
	//}
}

void AABGameMode::PostLogin(APlayerController* NewPlayer)
{
	ABLOG(Warning, TEXT("PostLogin Begin"));
	Super::PostLogin(NewPlayer);
	ABLOG(Warning, TEXT("PostLogin End"));
}

AABCharacter::StaticClass( ) 를 DefaultPawnClass 로 설정한다. StaticClass 는 그냥 해당 UObject 의 클래스를 리턴한다. 자세한 GetClass 는 오브젝트의 클래스를, StaticClass 는 해당 클래스의 클래스를 리턴한다. StaticClass 는 해당 클래스 정의의 클래스를, GetClass 는 해당 객체의 실제 타입의 클래스를 리턴한다. 즉, Actor 를 가리키는 포인터(ObjectPtr) 가 원래 UObject 타입이었다면, UObject::StaticClass() 는 UObject 를, UObjectPtr->GetClass() 는 Actor 클래스를 리턴할 것이다.

 

ABCharacter.h

// Fill out your copyright notice in the Description page of Project Settings.

#pragma once

#include "ArenaBattle.h"
#include "GameFramework/Character.h"
#include "ABCharacter.generated.h"

UCLASS()
class ARENABATTLE_API AABCharacter : public ACharacter
{
	GENERATED_BODY()

public:
	// Sets default values for this character's properties
	AABCharacter();

protected:
	// Called when the game starts or when spawned
	virtual void BeginPlay() override;

	enum class EControlMode
	{
		GTA,
		DIABLO
	};

	void SetControlMode(EControlMode NewControlMode);
	EControlMode CurrentControlMode = EControlMode::GTA;
	FVector DirectionToMove = FVector::ZeroVector;

	float ArmLengthTo = 0.0f;
	FRotator ArmRotationTo = FRotator::ZeroRotator;
	float ArmLengthSpeed = 0.0f;
	float ArmRotationSpeed = 0.0f;

public:	
	// Called every frame
	virtual void Tick(float DeltaTime) override;

	// Called to bind functionality to input
	virtual void SetupPlayerInputComponent(class UInputComponent* PlayerInputComponent) override;

	UPROPERTY(VisibleAnywhere, Category = Camera)
	USpringArmComponent* SpringArm;

	UPROPERTY(VisibleAnywhere, Category = Camera)
	UCameraComponent* Camera;

private:
	void UpDown(float NewAxisValue);
	void LeftRight(float NewAxisValue);
	void LookUp(float NewAxisValue);
	void Turn(float NewAxisValue);

	void ViewChange();
};

먼저 EControlMode enum 을 선언해 GTA, DIABLO 를 넣어준다. 그리고 SetControlMode 함수로 컨트롤 모드를 설정해 주도록 한다. 초기화 값은 GTA 로 설정했다.

DirectionToMove 는 DIABLO 설정에서 상하좌우 축 입력이 들어왔을 경우 그 값을 받게 된다.

ArmLengthTo, ArmRotationTo, ArmLengthSpeed, ArmRotationSpeed 는 시점 변환 시에 효과를 부드럽게 만들어 주기 위해 사용할 것이다.

 

SetupPlayerInputComponent 는 Binding 에 설정된 축/액션 매들을 연결시켜 주는 역할을 수행할 것이다.

AABCharacter 에 SpringArm, Camera 를 정의해 주고, Binding 입력에 추가된 상하좌우 / 마우스 시점 변환 값을 받기 위한 UpDown, LeftRight, LookUp, Turn 함수를 선언한다. 그리고 시점 변환을 위한 함수인 ViewChange 함수를 선언한다.

 

ABCharacter.cpp

// Fill out your copyright notice in the Description page of Project Settings.


#include "ABCharacter.h"

// Sets default values
AABCharacter::AABCharacter()
{
 	// Set this character to call Tick() every frame.  You can turn this off to improve performance if you don't need it.
	PrimaryActorTick.bCanEverTick = true;

	// SpringArm, Camera 컴포넌트 생성
	SpringArm = CreateDefaultSubobject<USpringArmComponent>(TEXT("SPRINGARM"));
	Camera = CreateDefaultSubobject<UCameraComponent>(TEXT("CAMERA"));

	// SpringArm 은 Capsule 에 붙이고, Camera 는 SpringArm 에 붙인다.
	SpringArm->SetupAttachment(GetCapsuleComponent());
	Camera->SetupAttachment(SpringArm);

	// 이전에 제작한 폰의 설정값을 사용해 준다.
	GetMesh()->SetRelativeLocationAndRotation(FVector(0.0f, 0.0f, -88.0f), FRotator(0.0f, -90.f, 0.0f));
	SpringArm->TargetArmLength = 400.0f;
	SpringArm->SetRelativeRotation(FRotator(-15.0f, 0.0f, 0.0f));

	// 에셋을 불러올 때는 FObjectFinder 를 이용한다.
	static ConstructorHelpers::FObjectFinder<USkeletalMesh> SK_CARDBOARD(TEXT("/Game/InfinityBladeWarriors/Character/CompleteCharacters/SK_CharM_Cardboard.SK_CharM_Cardboard"));
	if (SK_CARDBOARD.Succeeded())
	{
		GetMesh()->SetSkeletalMesh(SK_CARDBOARD.Object);
	}

	// 이전에 사용했던 애니메이션 블루프린트를 사용한다.
	GetMesh()->SetAnimationMode(EAnimationMode::AnimationBlueprint);

	// 블루프린트 클래스를 사용할 때는 FClassFinder 를 쓴다.
	// 애셋의 레퍼런스 끝에 _C 를 붙여주면 된다.
	static ConstructorHelpers::FClassFinder<UAnimInstance> WARRIOR_ANIM(TEXT("/Game/Book/Animations/WarriorAnimBlueprint.WarriorAnimBlueprint_C"));
	if (WARRIOR_ANIM.Succeeded())
	{
		GetMesh()->SetAnimInstanceClass(WARRIOR_ANIM.Class);
	}

	// 기본 EControlMode 를 DIABLO 로 설정할 것이다.
	SetControlMode(EControlMode::DIABLO);
	//SetControlMode(EControlMode::GTA);

	// 화면 전환시, ArmLength 와 ArmRotation 의 속도를 지정한다.
	ArmLengthSpeed = 3.0f;
	ArmRotationSpeed = 10.0f;
}

// Called when the game starts or when spawned
void AABCharacter::BeginPlay()
{
	Super::BeginPlay();
	
}

// Called every frame
void AABCharacter::Tick(float DeltaTime)
{
	Super::Tick(DeltaTime);

	// SpringArm 의 길이와 회전 값이 목표 값까지 서서히 변경되도록 제어한다.
	SpringArm->TargetArmLength = FMath::FInterpTo(SpringArm->TargetArmLength, ArmLengthTo, DeltaTime, ArmLengthSpeed);

	switch (CurrentControlMode)
	{
	case EControlMode::DIABLO:
		SpringArm->SetRelativeRotation(FMath::RInterpTo(SpringArm->GetRelativeRotation(), ArmRotationTo, DeltaTime, ArmRotationSpeed));
		//SpringArm->RelativeRotation = FMath::RInterpTo(SpringArm->RelativeRotation, ArmRotationTo, DeltaTime, ArmRotationSpeed);
		break;
	}

	// DIABLO 모드일때는 FVector 로 선언된 DirectionToMove 를 이용한다(AddMovementInput).
	switch (CurrentControlMode)
	{
	case EControlMode::DIABLO:
		if (DirectionToMove.SizeSquared() > 0.0f)
		{
			GetController()->SetControlRotation(FRotationMatrix::MakeFromX(DirectionToMove).Rotator());
			AddMovementInput(DirectionToMove);
		}
		break;
	}
}

// Called to bind functionality to input
void AABCharacter::SetupPlayerInputComponent(UInputComponent* PlayerInputComponent)
{
	Super::SetupPlayerInputComponent(PlayerInputComponent);

	// 시점 변환, 이동, 마우스 시선 처리를 바인딩해 준다.
	PlayerInputComponent->BindAction(TEXT("ViewChange"), EInputEvent::IE_Pressed, this, &AABCharacter::ViewChange);

	PlayerInputComponent->BindAxis(TEXT("UpDown"), this, &AABCharacter::UpDown);
	PlayerInputComponent->BindAxis(TEXT("LeftRight"), this, &AABCharacter::LeftRight);
	PlayerInputComponent->BindAxis(TEXT("LookUp"), this, &AABCharacter::LookUp);
	PlayerInputComponent->BindAxis(TEXT("Turn"), this, &AABCharacter::Turn);
}

void AABCharacter::UpDown(float NewAxisValue)
{
	//AddMovementInput(GetActorForwardVector(), NewAxisValue);

	// GTA 버전에서는 카메라의 방향을 중심으로 움직이도록 이동 방향을 변경해 주고 있다.
	// 액터의 회전 값(GetControlRotation, {0,0,0} ) 은 그 액터가 바라보는 방향이
	// 월드의 X 축 방향, 즉 {1, 0, 0} 임을 의미한다.
	// 스프링 암의 회전 값은 옵션에 의해 컨트롤 회전 값과 동일하므로
	// 컨트롤 회전 값이 카메라가 바라보는 방향이라고 할 수 있다.
	// 컨트롤 회전 값으로부터 회전행렬을 생성한 후,
	// 원하는 방향 축을 대입해 캐릭터가 움직일 방향을 가져온다.
	// 시선방향은 X 축, 우측 방향은 Y 축을 의미한다.
	switch (CurrentControlMode)
	{
	case EControlMode::GTA:
		AddMovementInput(FRotationMatrix(FRotator(0.0f, GetControlRotation().Yaw, 0.0f)).GetUnitAxis(EAxis::X), NewAxisValue);
		break;
	case EControlMode::DIABLO:
		DirectionToMove.X = NewAxisValue;
		break;
	}
	//ABLOG(Warning, TEXT("%f"), NewAxisValue);
}

void AABCharacter::LeftRight(float NewAxisValue)
{
	//AddMovementInput(GetActorRightVector(), NewAxisValue);
	switch (CurrentControlMode)
	{
	case EControlMode::GTA:
		AddMovementInput(FRotationMatrix(FRotator(0.0f, GetControlRotation().Yaw, 0.0f)).GetUnitAxis(EAxis::Y), NewAxisValue);
		break;
	case EControlMode::DIABLO:
		DirectionToMove.Y = NewAxisValue;
		break;
	}
	//ABLOG(Warning, TEXT("%f"), NewAxisValue);
}

void AABCharacter::LookUp(float NewAxisValue)
{
	// GTA 에서의 시선처리
	switch (CurrentControlMode)
	{
	case EControlMode::GTA:
		AddControllerPitchInput(NewAxisValue);
		break;
	}

}

void AABCharacter::Turn(float NewAxisValue)
{
	// GTA 에서의 시선처리
	switch (CurrentControlMode)
	{
	case EControlMode::GTA:
		AddControllerYawInput(NewAxisValue);
		break;
	}
}

void AABCharacter::SetControlMode(EControlMode NewControlMode)
{
	// 컨트롤 모드에 따라 설정값을 변경한다.
	CurrentControlMode = NewControlMode;
	switch (CurrentControlMode)
	{
	case EControlMode::GTA:
		//SpringArm->TargetArmLength = 450.0f;
		//SpringArm->SetRelativeRotation(FRotator::ZeroRotator);
		ArmLengthTo = 450.0f;

		// 스프링암의 회전 값을 컨트롤 회전 값과 동일하게 맞춰준다.
		SpringArm->bUsePawnControlRotation = true;
		SpringArm->bInheritPitch = true;
		SpringArm->bInheritRoll = true;
		SpringArm->bInheritYaw = true;

		// bDoCollisionTest 를 끄면 카메라와 캐릭터 사이에
		// 장애물이 있어도 줌 기능이 작동하지 않는다.
		SpringArm->bDoCollisionTest = true;

		// 컨트롤러의 회전 방향으로 캐릭터를 회전한다.
		// 4.26 부터 bOrientRotationToMovement 에 의해 대체됨.
		bUseControllerRotationYaw = false; 

		// 캐릭터가 움직이는 방향으로 캐릭터를 자동으로 회전시켜 준다.
		GetCharacterMovement()->bOrientRotationToMovement = true;

		// 컨트롤러가 "원하는" 방향으로 캐릭터를 회전한다.
		// 즉, 오른쪽+위를 누르면 "정확히" 45도 방향으로 캐릭터가 회전해서 이동하는 식이다.
        // 실제로 캐릭터의 회전 방향이 "딱딱 떨어지는" 느낌을 준다.
		GetCharacterMovement()->bUseControllerDesiredRotation = false;

		// 회전을 부드럽게 만들어 주기 위해 RotationRate 를 조정한다.
		GetCharacterMovement()->RotationRate = FRotator(0.0f, 720.0f, 0.0f);
		break;
	case EControlMode::DIABLO:
		//SpringArm->TargetArmLength = 800.0f;
		//SpringArm->SetRelativeRotation(FRotator(-45.0f, 0.0f, 0.0f));
		ArmLengthTo = 800.0f;
		ArmRotationTo = FRotator(-45.0f, 0.0f, 0.0f);
		SpringArm->bUsePawnControlRotation = false;
		SpringArm->bInheritPitch = false;
		SpringArm->bInheritRoll = false;
		SpringArm->bInheritYaw = false;
		SpringArm->bDoCollisionTest = false;
		// rotate pawn to the direction it is heading
		bUseControllerRotationYaw = false; 
		GetCharacterMovement()->bOrientRotationToMovement = false;
		GetCharacterMovement()->bUseControllerDesiredRotation = true;
		GetCharacterMovement()->RotationRate = FRotator(0.0f, 720.0f, 0.0f);
		break;
	}
}

void AABCharacter::ViewChange()
{
	switch (CurrentControlMode)
	{
	case EControlMode::GTA:
		GetController()->SetControlRotation(GetActorRotation());
		SetControlMode(EControlMode::DIABLO);
		break;
	case EControlMode::DIABLO:
		GetController()->SetControlRotation(SpringArm->GetRelativeRotation());
		SetControlMode(EControlMode::GTA);
		break;
	}
}

완성된 ABCharacter.cpp 소스 코드의 길이는 좀 긴데, 위부터 차례대로 분석을 해 보자. 각각의 항목은 주석을 통해 설명을 달아놓았다.

 

DIABLO 시의 시점

DIABLO 에서는, 축 입력에 따라 캐릭터의 방향이 바뀐다. 예를 들어, 아래와 오른쪽을 동시에 누르게 되면 캐릭터의 방향은 오른쪽 대각선 아래 방향을 바라보며 이동을 하게 된다.

 

DIABLO 시의 시점

그리고 위와 같이 장애물이 있을 경우, 따로 줌을 해 주지도 않는다.

 

DIABLO -&amp;amp;amp;amp;amp;gt; GTA 로 시점 변환이 이루어지고 있다

이전 코드에서 카메라 전환 속도를 조절하기 위해 Interp 를 사용했었는데, 자세히 보면 카메라 시점이 변환되는 도중이라는 것을 눈치챌 수 있다(카메라 노출을 길게 잡은 것처럼, 분수가 뿌옇게 보임).

 

GTA 시의 시점

GTA 시의 시점에서 캐릭터는 자신이 가려는 방향으로 몸을 회전하게 된다. 예를 들어 캐릭터가 왼쪽을 바라보고 있을 때, 오른쪽 축 입력을 주면 캐릭터는 오른쪽으로 몸을 돌린 후 이동을 한다.

 

GTA 시의 시점

또한 이전 DIABLO 와는 다르게, 카메라와 캐릭터 사이에 장애물이 있을 경우, 자동으로 줌을 해 준다. 위에서는 분수대가 카메라와 캐릭터를 가로막고 있다.

 

Comments