본문 바로가기

언리얼C++게임개발/06.캐릭터의 제작과 컨트롤

삼인칭 컨트롤 구현(GTA방식)

3. 삼인칭 컨트롤 - GTA

언리얼에서 제공하는 흰색 마네킹과 동일한 기능을 구현할 수 있다.

흰색 마네킹 설정

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

 

ABCharacter 클래스에 SetControlMode 멤버 함수를 만들어 SpringArm 컴포넌트를 활용한 카메라 설정을 구현해보자.

ABCharacter.h

...

protected:
	void SetControlMode(int32 ControlMode);
	
...

 

ABCharacter.cpp

AABCharacter::AABCharacter()
{
    
	...
    
	SetControlMode(0);
}

...

void AABCharacter::SetControlMode(int32 ControlMode)
{
	if (ControlMode == 0)
	{
		SpringArm->TargetArmLength = 450.0f;
		SpringArm->SetRelativeRotation(FRotator::ZeroRotator);
		SpringArm->bUsePawnControlRotation = true;
		SpringArm->bInheritPitch = true;
		SpringArm->bInheritYaw = true;
		SpringArm->bInheritRoll = true;
		SpringArm->bDoCollisionTest = true;
		bUseControllerRotationYaw = false;
	}
}

 

결과 화면

캐릭터는 회전하지 않지만 카메라 지지대가 마우스 움직임에 따라 회전한다.

 

캐릭터가 카메라 방향을 중심으로 움직이도록 이동 방향을 변경해주어야 한다. 회전 값인 FRotator 데이터에서 방향 값 FVector데이터를 얻을 수 있다.

액터의 회전 값 (0, 0, 0)은 그 액터가 바라보는 월드의 X축 방향 (1, 0, 0)임을 의미한다. 액터가 회전하면 액터의 시선 방향도 자연스럽게 다른 값으로 변하게 된다.

카메라가 바라보는 방향인 컨트롤 회전 값으로부터 회전 행렬을 생성하고 원하는 방향 축을 대입해 캐릭터가 움직일 방향을 가져올 수 있다.

(X축: 시선 방향, Y축: 우측 방향)

 

캐릭터가 움직일 방향만 가져오게 되면, X축만 바라보며 시선 방향으로 이동하므로 부자연스러운 결과물이 나온다..

bOrientRotationToMovement 기능을 사용해 캐릭터가 움직이는 방향을 따라 자동으로 회전시켜주도록 한다.

 

ABCharacter.cpp

void AABCharacter::SetControlMode(int32 ControlMode)
{
	if (ControlMode == 0)
	{
		...
            
		// 캐릭터가 움직이는 방향으로 캐릭터 자동 회전
		GetCharacterMovement()->bOrientRotationToMovement = true;
		// 회전 속도를 지정해 캐릭터가 부드럽게 회전하도록 함
		GetCharacterMovement()->RotationRate = FRotator(0.0f, 720.0f, 0.0f);
	}
}

...
    
void AABCharacter::UpDown(float NewAxisValue) 
{
	// 회전 값으로부터 시선 방향(X)과 우측 방향(Y)의 벡터값 가져옴
	// AddMovementInput(GetActorForwardVector(), NewAxisValue);
	AddMovementInput(FRotationMatrix(GetControlRotation()).GetUnitAxis(EAxis::X), NewAxisValue);
}

void AABCharacter::LeftRight(float NewAxisValue) 
{
	// AddMovementInput(GetActorRightVector(), NewAxisValue);
	AddMovementInput(FRotationMatrix(GetControlRotation()).GetUnitAxis(EAxis::Y), NewAxisValue);
}​

 

결과 화면

 


4. 삼인칭 컨트롤 - 디아블로

고정된 삼인칭 시점에서 캐릭터를 따라다니는 컨트롤을 구현해보자.

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

 

컨트롤 회전 값 비교

  • GTA: SpringArm의 회전에 사용 (카메라와 캐릭터가 동시에 회전)
  • 디아블로: 캐릭터의 방향에 사용 (카메라 고정)

 

4.1. 컨트롤 설정 변경

ViewChange라는 액션 매핑을 추가하고 Shift+V 키를 누를 때마다 SetControlMode함수에 다른 인자값이 들어가도록 코드를 작성하자. SetupPlayerInputComponent 멤버함수에 BindAction 내부 함수를 선언해 입력 키가 눌렸을 때 ViewChange 함수를 호출하도록 한다.

  • BindAction : 버튼이 눌렸는지, 떼어졌는지에 대한 부가 인자 지정 가능
    • EInputEvent::IE_Pressed : 버튼을 누른 직후
    • EInputEvent::IE_Released : 버튼을 떼었을 때
  • InterpTo : 지정 속력으로 목표 지점까지 이동(FMath에서 제공)
    • float형: FInterpTo
    • Vector형: VInterpTo
    • Rotator형: RInterpTo
    • (eg. 회전 보간 : FMath::RInterpTo(이전 회전값, 목표 회전값, DeltaTime, 회전 속도))

 

ABCharacter.h

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

#pragma once

#include "ABLog.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;
	//void SetControlMode(int32 ControlMode); 
	enum class EControlMode
	{
		GTA,
		DIABLO
	};

	void SetControlMode(EControlMode NewControlMode);
	EControlMode CurrentControlMode = EControlMode::GTA;

	// UPROPERTY를 사용하지 않는 값 타입 변수들은 초기값 미리 지정
	FVector DirectionToMove = FVector::ZeroVector;

	// ViewChange 키 입력 시 시점 부드럽게 전환
	// SetControlMode 함수에서 정의 후 Tick 함수의 인자로 쓰임
	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();
};

 

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 = CreateDefaultSubobject<USpringArmComponent>(TEXT("SPRINGARM"));
	Camera = CreateDefaultSubobject<UCameraComponent>(TEXT("CAMERA"));

	SpringArm->SetupAttachment(GetCapsuleComponent());
	Camera->SetupAttachment(SpringArm);

	GetMesh()->SetRelativeLocationAndRotation(FVector(0.0f, 0.0f, -88.0f), FRotator(0.0f, -90.0f, 0.0f));
	SpringArm->TargetArmLength = 400.0f;
	SpringArm->SetRelativeRotation(FRotator(-15.0f, 0.0f, 0.0f));

	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);

	static ConstructorHelpers::FClassFinder<UAnimInstance> WARRIOR_ANIM(TEXT("/Game/Book/Animations/WarriorAnimBlueprint.WarriorAnimBlueprint_C"));

	if (WARRIOR_ANIM.Succeeded())
	{
		GetMesh()->SetAnimInstanceClass(WARRIOR_ANIM.Class);
	}

	SetControlMode(EControlMode::DIABLO);

	ArmLengthSpeed = 3.0f;
	ArmRotationSpeed = 10.0f;
}

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

}


//void AABCharacter::SetControlMode(int32 ControlMode)
//{
//	if (ControlMode == 0)
//	{
//		SpringArm->TargetArmLength = 450.0f;
//		SpringArm->SetRelativeRotation(FRotator::ZeroRotator);
//		SpringArm->bUsePawnControlRotation = true;
//		SpringArm->bInheritPitch = true;
//		SpringArm->bInheritYaw = true;
//		SpringArm->bInheritRoll = true;
//		SpringArm->bDoCollisionTest = true;
//		bUseControllerRotationYaw = false;
//		// 캐릭터가 움직이는 방향으로 캐릭터 자동 회전
//		GetCharacterMovement()->bOrientRotationToMovement = true;
//		// 회전 속도를 지정해 캐릭터가 부드럽게 회전하도록 함
//		GetCharacterMovement()->RotationRate = FRotator(0.0f, 720.0f, 0.0f);
//	}
//}


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;
		SpringArm->bDoCollisionTest = true;
		bUseControllerRotationYaw = false;
		GetCharacterMovement()->bOrientRotationToMovement = true;
		GetCharacterMovement()->bUseControllerDesiredRotation = false;
		GetCharacterMovement()->RotationRate = FRotator(0.0f, 720.0f, 0.0f);
		break;
	case EControlMode::DIABLO:
		// 카메라 길이 800, 45도에서 시점 고정
		// 마우스 입력 전부 해제(키보드 입력만 받음)
		//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;
		// 회전 끊김 방지 => 속성 해제 후 회전 보완 코드 작성
		// bOrientRotationToMovement 로 대체됨(UE4.26이상)
		bUseControllerRotationYaw = false;
		// 캐릭터 자동 회전 (키보드로 캐릭터 회전시키므로 해제)
		GetCharacterMovement()->bOrientRotationToMovement = false;
		// 컨트롤 회전이 가리키는 방향으로 캐릭터 회전
		GetCharacterMovement()->bUseControllerDesiredRotation = true;
		// 캐릭터가 부드럽게 회전하도록 보완 (회전 속도 지정)
		GetCharacterMovement()->RotationRate = FRotator(0.0f, 720.0f, 0.0f);
		break;
	}
}

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

	SpringArm->TargetArmLength = FMath::FInterpTo(SpringArm->TargetArmLength, ArmLengthTo, DeltaTime, ArmLengthSpeed);

	switch (CurrentControlMode)
	{
	case EControlMode::DIABLO:
		// SpringArm의 길이와 회전값이 목표 지점까지 변경
		// 변경 속도: ArmRotationSpeed
		SpringArm->GetRelativeRotation() = FMath::RInterpTo(SpringArm->GetRelativeRotation(), ArmRotationTo, DeltaTime, ArmRotationSpeed);
		break;
	}

	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->BindAxis(TEXT("UpDown"), this, &AABCharacter::UpDown);
	PlayerInputComponent->BindAxis(TEXT("LeftRight"), this, &AABCharacter::LeftRight);
	PlayerInputComponent->BindAxis(TEXT("Turn"), this, &AABCharacter::Turn);
	PlayerInputComponent->BindAxis(TEXT("LookUp"), this, &AABCharacter::LookUp);
	// 버튼을 누른 직후 ViewChange함수 호출
	PlayerInputComponent->BindAction(TEXT("ViewChange"), EInputEvent::IE_Pressed, this, &AABCharacter::ViewChange);

}

void AABCharacter::UpDown(float NewAxisValue)
{
	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;
	}
}

void AABCharacter::LeftRight(float 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;
	}
}

void AABCharacter::LookUp(float NewAxisValue)
{
	switch (CurrentControlMode)
	{
	case EControlMode::GTA:
		AddControllerPitchInput(NewAxisValue);
		break;
	}
}

void AABCharacter::Turn(float NewAxisValue)
{
	switch (CurrentControlMode)
	{
	case EControlMode::GTA:
		AddControllerYawInput(NewAxisValue);
		break;
	}
}

void AABCharacter::ViewChange()
{
	switch (CurrentControlMode)
	{
	case EControlMode::GTA:
		// 시점 변경이 자연스럽도록 회전 값 미리 부여
		// GTA->DIABLO이므로 캐릭터의 회전값 지정
		GetController()->SetControlRotation(GetActorRotation());
		SetControlMode(EControlMode::DIABLO);
		break;
	case EControlMode::DIABLO:
		// DIABLO->GTA이므로 SpringArm 회전값 지정
		// UE4.24이상부터 RelativeRotation->GetRelativeRotation()으로 변경
		GetController()->SetControlRotation(SpringArm->GetRelativeRotation());
		SetControlMode(EControlMode::GTA);
		break;
	}
}

컨트롤 회전 값은 GTA방식에서는 SpringArm의 회전을 사용하고 디아블로방식에서는 캐릭터의 방향에 사용한다. 컨트롤을 전환할때 어색해 보이지 않도록 이 값을 미리 지정해주는 코드를 ViewChange함수에 추가하였다.

결과 화면

Shift+V키를 누르면 부드럽게 시점이 변경되는 모습을 확인할 수 있다!