KoreanFoodie's Study
이득우의 언리얼 C++ 6 : 캐릭터의 제작과 컨트롤 본문
이득우님의 "이득우의 언리얼 C++ 게임 개발의 정석" 책을 따라가며 실습한 내용을 정리한 포스팅입니다. 실습에 필요한 자료들은 이 링크에서, 제가 작업한 예제 소스 완성본은 여기에서 찾아보실 수 있습니다. (저는 언리얼 4.27.2 버전 기준으로 작업하였습니다)
캐릭터 모델
Pawn 대신 Character 액터를 만들어 조작해보자. Character 액터는 Pawn 액터를 상속받는데, Pawn 과 다른 점은 CharacterMovement 컴포넌트를 사용해 움직임을 관리한다는 것이다(Actor>>Pawn>>Character). 이 컴포넌트가 FloatingMovement 에 비해 가지는 장점은 다음과 같다.
- 점프와 같은 중력을 반영한 움직임을 제공
- 다양한 움직임 설정 가능(기어가기, 날아가기, 수영하기 등등) + 현재 움직임에 좀 더 많은 정보 전달
- 멀티 플레이 네트워크 환경에서 캐릭터들의 움직임을 자동으로 동기화한다.
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 에서는, 축 입력에 따라 캐릭터의 방향이 바뀐다. 예를 들어, 아래와 오른쪽을 동시에 누르게 되면 캐릭터의 방향은 오른쪽 대각선 아래 방향을 바라보며 이동을 하게 된다.
그리고 위와 같이 장애물이 있을 경우, 따로 줌을 해 주지도 않는다.
이전 코드에서 카메라 전환 속도를 조절하기 위해 Interp 를 사용했었는데, 자세히 보면 카메라 시점이 변환되는 도중이라는 것을 눈치챌 수 있다(카메라 노출을 길게 잡은 것처럼, 분수가 뿌옇게 보임).
GTA 시의 시점에서 캐릭터는 자신이 가려는 방향으로 몸을 회전하게 된다. 예를 들어 캐릭터가 왼쪽을 바라보고 있을 때, 오른쪽 축 입력을 주면 캐릭터는 오른쪽으로 몸을 돌린 후 이동을 한다.
또한 이전 DIABLO 와는 다르게, 카메라와 캐릭터 사이에 장애물이 있을 경우, 자동으로 줌을 해 준다. 위에서는 분수대가 카메라와 캐릭터를 가로막고 있다.
'Game Dev > Unreal C++ : Tutorial' 카테고리의 다른 글
이득우의 언리얼 C++ 8 : 애니메이션 시스템 활용 (Montage, Notify, ComboAttack) (0) | 2022.03.04 |
---|---|
이득우의 언리얼 C++ 7 : 애니메이션 시스템의 설계 (0) | 2022.03.01 |
이득우의 언리얼 C++ 5 : 폰의 제작과 조작 (0) | 2022.02.27 |
이득우의 언리얼 C++ 4 : 게임 모드(GameMode), 플레이어 컨트롤러(PlayerController) 제작 (1) | 2022.02.21 |
이득우의 언리얼 C++ 3 : 로그, 액터 움직이기, 액터 삭제 (0) | 2022.02.16 |