캐릭터의 제작과 컨트롤
‘이득우의 언리얼 c++ 게임 개발의 정석’ 책을 참고하여 작성한 포스트입니다.
캐릭터 모델을 기반으로 움직이는 캐릭터를 제작한다.
캐릭터 모델
- 캐릭터 모델은 인간형 폰을 좀 더 효과적으로 제작하게 해주는 모델이다.
- C++ 클래스 Character 를 생성해보자.
- 이 클래스는
ACharacter
클래스를 상속받고 있다. - 타고 올라가보면
ACharacter
클래스는APawn
클래스를 상속받고 있다. - 앞서 만들었던
ABPawn
클래스와 비슷하게 Capsule, SkeletalMesh 컴포넌트를 사용하고 있으며, - CharacterMovement 라는 컴포먼트로 움직임을 관리함을 볼 수 있다.
- 그리고 private으로 선언된 컴포넌트의 포인터를 상속받은 클래스들이 접근할 수 있도록,
GetCapsuleComponent
,GetMesh
,GetCharacterMovement
함수를 제공하고 있다.- 앞서의 폰과 비슷하게 코드를 작성해본다.
// 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;
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);
};
// 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("/Script/Engine.SkeletalMesh'/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("/Script/Engine.AnimBlueprint'/Game/Animations/WarriorAnimBlueprint.WarriorAnimBlueprint_C'"));
if(WARRIOR_ANIM.Succeeded())
{
GetMesh()->SetAnimInstanceClass(WARRIOR_ANIM.Class);
}
}
// Called when the game starts or when spawned
void AABCharacter::BeginPlay()
{
Super::BeginPlay();
}
// Called every frame
void AABCharacter::Tick(float DeltaTime)
{
Super::Tick(DeltaTime);
}
// 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);
}
void AABCharacter::UpDown(float NewAxisValue)
{
AddMovementInput(GetActorForwardVector(), NewAxisValue);
}
void AABCharacter::LeftRight(float NewAxisValue)
{
AddMovementInput(GetActorRightVector(), NewAxisValue);
}
- 그리고 이 캐릭터 클래스를 게임 모드에서 기본 폰으로 설정해준다
// ABGameMode.cpp
// Fill out your copyright notice in the Description page of Project Settings.
#include "ABGameMode.h"
//#include "ABPawn.h"
#include "ABCharacter.h"
#include "ABPlayerController.h"
AABGameMode::AABGameMode()
{
//DefaultPawnClass = AABPawn::StaticClass();
DefaultPawnClass = AABCharacter::StaticClass();
PlayerControllerClass = AABPlayerController::StaticClass();
}
void AABGameMode::PostLogin(APlayerController* NewPlayer)
{
ABLOG(Warning, TEXT("PostLogin Begin"));
Super::PostLogin(NewPlayer);
ABLOG(Warning, TEXT("PostLogin End"));
}
- 폰 모델과 대부분 비슷하나, 캐릭터 무브먼트에서 차이점이 발생한다.
- 점프와 같은 중력을 반영한 움직임을 제공한다.
- 다양한 움직임 설정이 가능하다. 기어가기, 날아가기 등
- 멀티 플레이 네트워크 환경에서 캐릭터들의 움직임을 자동으로 동기화한다
컨트롤 회전의 사용
- 폰이 관리하는 속성 중 대표적인 것은 속도(velocity)다.
- 폰의 현재 이동상태를 알려주는 중요한 데이터다.
- 플레이어 컨트롤러는 컨트롤 회전(control Rotation)이라는 속성을 제공ㅎ나다.
- 마우스 움직임에 따라 폰이 회전해야 할 목표 회전 값을 설정하고, 일정한 속도로 회전하는 기능을 제작해보자.
- 마우스의 움직임은 삼인칭 템플릿에서 제공하는 Turn과 LookUp의 축(axis) 입력 설정을 사용해 받아올 수 있다.
- 프로젝트 설정 -> 입력 -> Axis Manager 에서 확인 혹은 설정하면 된다.
- 입력 값에 따라 회전하도록
AddControllerInputYaw
,AddControllerInputRoll
,AddControllerInputPitch
가 있다. - Turn 은 Z축 회전, LookUp은 Y축 회전에 대응되므로, 각각 연동해보자
// ABCharacter.h
...
private:
void Turn(float NewAxisValue);
void LookUp(float NewAxisValue);
...
// ABCharacter.cpp
...
// 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);
}
...
void AABCharacter::Turn(float NewAxisValue)
{
AddControllerYawInput(NewAxisValue);
}
void AABCharacter::LookUp(float NewAxisValue)
{
AddControllerPitchInput(NewAxisValue);
}
- 플레이 해보면 마우스 상하 이동은 안먹히고, 좌우만 먹힌다.
- 틸드(~) 키를 눌러 콘솔 명령어 입력 창을 띄우고
displayall PlayerController ControlRotation
을 입력하면, - 뷰포트 왼쪽 상단에 현재 플레이어 컨트롤러의 컨트롤 회전(contorl rotation) 값이 보인다.
- ABPlayerController 액터를 보면 트랜스폼 회전 값과 화면에 나오는 컨트롤 회전 값이 일치함을 알 수 있다.
- 캐릭터 모델은 기본으로 컨트롤 회전의 Yaw 회전(Z축 회전) 값과 폰의 Yaw 회전이 서로 연동돼 있다.
- 액터의 폰 섹션을 보면 ‘Use Controller Rotation Yaw’ 가 체크되어 있음을 볼 수 있다.
- 그래서 좌우 회전(Z축 회전)은 먹히지만 상하는 영향을 주지 않는 것이다.
삼인칭 컨트롤 구현(GTA 방식)
- 기존의 흰색 마네킹이 움직이던 것을 구현해 본다
- 이동: 현재 보는 시점을 기준으로 상하, 좌우 방향 이동. 카메라는 회전 x
- 캐릭터 회전: 캐릭터 이동 방향으로 마네킹이 회전함
- 지지대 길이 : 450cm
- 카메라 회전: 마우스 상하좌우 이도엥 따라 카메라 지지대가 상하좌우로 회전
- 카메라 줌: 카메라 시선과 캐릭터 사이에 장애물이 감지되면 캐릭터가 보이도록 카메라를 장애물 앞으로 줌인
- SpringArm 컴포넌트를 이용하면 삼인칭 시점의 카메라 설정 구현에 편한다.
ABCharacter
클래스에SetControlMode
멤버 함수를 넣어 삼인칭 조작 관련된 기능을 설정해본다.
// ABCharcter.h
...
protected:
// Called when the game starts or when spawned
virtual void BeginPlay() override;
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->bInheritRoll = true;
SpringArm->bInheritYaw = true;
SpringArm->bDoCollisionTest = true;
bUseControllerRotationYaw = false;
}
}
...
- 실행해보면 마우스 움직임에 따라 캐릭터는 회전하지 않고, 카메라 지지대만 회전한다.
- 이제 카메라 방향을 중심으로 움직이도록 이동 방향을 변경해 주자.
- 컨트롤 회전(FRotator)에 따라 캐릭터 이동 방향(FVector)를 구해주면 된다.
- 설정한 스프링암의 회전 값이 컨트롤 회전 값과 동일하므로 컨트롤 회전 값이 카메라가 바라보는 방향이라 볼 수 있다.
- 회전 행렬을 생성하고 캐릭터가 움직일 방향 값을 구해 보자.
- 시선 방향은 X축, 우측 방향은 Y축이다.
// ABChracter.cpp
...
void AABCharacter::UpDown(float NewAxisValue)
{
//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);
}
...
- 카메라의 시선 방향으로 움직이지만, 아직 회전 기능이 없어 캐릭터가 회전하지 않는다.
- 캐릭터가 움직이는 방향으로 캐릭터를 자동으로 회전시켜주는 캐릭터 무브먼트 컴포넌트의
OrientRotationToMove
기능이 있다.
//ABCharacter.cpp
...
void AABCharacter::SetControlMode(int32 ControlMode)
{
if (ControlMode == 0)
{
...
GetCharacterMovement()->bOrientRotationToMovement = true;
GetCharacterMovement()->RotationRate = FRotator(0.0f, 720.0f, 0.0f);
}
}
...
삼인칭 컨트롤 구현(디아블로 방식)
- 디아블로는 고정된 삼인칭 시점에서 캐릭터를 따라다니는 방식이다.
- 캐릭터의 이동: 상하좌우 키를 조합해 캐릭터가 이동할 방향 결정
- 캐릭터의 회전: 캐릭터는 입력한 방향으로 회전
- 카메라 길이: 800cm
- 카메라 회전: 고정 시선으로 45도
- 카메라 줌: 없음. 장애물이 있으면 외곽선 처리
- SetControlMode 의 인자 값을 분리해서 구현해 본다.
- 입력 모드를 구분하도록 열거형을 선언하고, 상하 좌우 키를 조합할 벡터 변수도 추가한다.
- UPROPERTY를 사용하지 않는 FVector와 같은 값 타입 변수들은 항상 초기 값을 미리 지정하는 것이 안전하다!!
- 입력 이벤트가 발생하면 DirectionToMove 멤버 변수를 업데이트 하고,
- 이후 발생하는 Tick 로직에서 최종 멤버 변수를 참고해 이동시킨다.
- 입력의 Axis 이벤트와 Tick 이벤트는 모두 매 프레임마다 호출되므로,
- 입력 함수를 먼저 호출하고, Tick 함수를 호출하도록 해주어야 한다.
- 그리고 현재 캐릭터가 45도 단위로 끊어져 회전하는데,
- 캐릭터 무브먼트의 bUseControllerDesiredRotation 속성을 체크하면 컨트롤 회전이 가리키는 방향으로 캐릭터가 부드럽게 회전한다.
- bUseControllerRotationYaw 속성을 해제하고 위 속성을 지정한다.
//ABCharacter.h
...
protected:
...
EContorlMode CurrentControlMode = EControlMode::GTA;
FVector DirectionToMove = FVector::ZeroVector;
...
//ABCharcter.cpp
...
AABCharacter::AABCharacter()
{
...
SetControlMode(EControlMode::DIABLO);
}
...
void AABCharacter::SetControlMode(EControlMode NewControlMode)
{
CurrentControlMode = NewControlMode;
switch (CurrentControlMode)
{
case EControlMode::GTA:
SpringArm->TargetArmLength = 450.0f;
SpringArm->SetRelativeRotation(FRotator::ZeroRotator);
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:
SpringArm->TargetArmLength = 800.0f;
SpringArm->SetRelativeRotation(FRotator(-45.0f, 0.0f, 0.0f));
SpringArm->bUsePawnControlRotation = false;
SpringArm->bInheritPitch = false;
SpringArm->bInheritRoll = false;
SpringArm->bInheritYaw = false;
SpringArm->bDoCollisionTest = false;
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);
switch(CurrentControlMode)
{
case EControlMode::DIABLO:
if (DirectionToMove.SizeSquared() > 0.0f)
{
GetController()->SetControlRotation(FRotationMatrix::MakeFromX(DirectionToMove).Rotator());
AddMovementInput(DirectionToMove);
}
break;
}
}
...
void AABCharacter::UpDown(float NewAxisValue)
{
//AddMovementInput(GetActorForwardVector(), NewAxisValue);
//AddMovementInput(FRotationMatrix(GetControlRotation()).GetUnitAxis(EAxis::X), 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)
{
//AddMovementInput(GetActorRightVector(), NewAxisValue);
//AddMovementInput(FRotationMatrix(GetControlRotation()).GetUnitAxis(EAxis::Y), 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::Turn(float NewAxisValue)
{
//AddControllerYawInput(NewAxisValue);
switch (CurrentControlMode)
{
case EControlMode::GTA:
AddControllerYawInput(NewAxisValue);
break;
}
}
void AABCharacter::LookUp(float NewAxisValue)
{
//AddControllerPitchInput(NewAxisValue);
switch (CurrentControlMode)
{
case EControlMode::GTA:
AddControllerPitchInput(NewAxisValue);
break;
}
}
- FRotationMatrix는 회전된 좌표계 정보를 저장하는 행렬이다.
- GTA 에서는 FRotator 값으로 회전 행렬을 생성하고, 이를 토대로 변환된 좌표계의 x y 축 방향을 가져왔다.
- 디아블로 방식에서는 거꾸로 하나의 벡터 값과 이에 직교하는 나머지 두 축을 구해 회전 행렬을 생성하고,
- 이와 일치하는 FRotator 값을 얻어 왔다.
- 벡터로부터 회전 행렬을 구축하는 명령은 MakeFromX, Y, Z가 있는데,
- 두 축의 입력을 합산한 최종 벡터 방향과 캐릭터의 시선 방향(x축)이 일치해야 하므로 MakeFromX를 사용했다.
컨트롤 설정의 변경
- 특정 키 입력에 따 두 가지 조작 방식이 전환되도록 해본다.
- 프로젝트 설정 -> 입력 -> 에서 액션 매핑을 새로 하나 추가하고 원하는 키를 할당한다.
- 이 키가 입력될 때마다 SetControlMode 함수에 다른 인자 값이 들어가도록 해준다.
- 액션 매핑 입력 설정과 연동하는 함수는
BindAction
이다. - 이 함수의 부가 인자로 눌렸는 지(EInputEvent::IE_Pressed), 떼어졌는지(EInputEvent::IE_Released) 지정 가능하다.
// ABCharacter.h
...
private:
...
void ViewChange();
...
// ABCharacter.cpp
...
void AABCharacter::SetupPlayerInputComponent(UInputComponent* PlayerInputComponent)
{
...
PlayerInputComponent->BindAction(TEXT("ViewChange"), EInputEvent::IE_Pressed, this, &AABCharacter::ViewChange);
}
...
void AABCharacter::ViewChange()
{
switch (CurrentControlMode)
{
case EControlMode::GTA:
SetControlMode(EControlMode::DIABLO);
break;
case EControlMode::DIABLO:
SetControlMode(EControlMode::GTA);
break;
}
}
- 이제 컨트롤 방식 바뀔때 마다 카메라가 부드럽게 전환되도록 해주자
- SpringArm의 길이와 회전 값이 목표 값까지 서서히 변경되도록 FMath 클래스의 InterpTo 명령어를 사용한다.
- InterpTo 명령어는 지정한 속력으로 목표 지점까지 진행하되, 목표 지점까지 도달하면 그 값에서 멈춘다.
- FInterpTo : float형
- VInterpTo : Vector형
- RInterpTo : Rotator형
// AABCharacter.h
...
private:
...
float ArmLengthTo = 0.0f;
FRotator ArmRotaionTo = FRotator::ZeroRotator;
float ArmLengthSpeed = 0.0f;
float ArmRotationSpeed = 0.0f;
...
//AABCharacter.cpp
AABCharacter::AABCharacter()
{
...
ArmLengthSpeed = 3.0f;
ArmRotationSpeed = 10.0f;
}
...
void AABCharacter::SetControlMode(EControlMode NewControlMode)
{
CurrentControlMode = NewControlMode;
switch (CurrentControlMode)
{
case EControlMode::GTA:
//SpringArm->TargetArmLength = 450.0f;
//SpringArm->SetRelativeRotation(FRotator::ZeroRotator);
ArmLengthTo = 450.0f;
...
case EControlMode::DIABLO:
//SpringArm->TargetArmLength = 800.0f;
//SpringArm->SetRelativeRotation(FRotator(-45.0f, 0.0f, 0.0f));
ArmLengthTo = 800.0f;
ArmRotaionTo = FRotator(-45.0f, 0.0f, 0.0f);
...
}
}
void AABCharacter::Tick(float DeltaTime)
{
Super::Tick(DeltaTime);
SpringArm->TargetArmLength = FMath::FInterpTo(SpringArm->TargetArmLength, ArmLengthTo, DeltaTime, ArmLengthSpeed);
switch(CurrentControlMode)
{
case EControlMode::DIABLO:
SpringArm->SetRelativeRotation(FMath::RInterpTo(SpringArm->GetRelativeRotation(), ArmRotationTo, DeltaTime, ArmRotationSpeed));
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;
}
}
...
- 컨트롤 회전 값은 GTA 방식에서는 SpringArm의 회전에 사용하고, 디아블로 방식에서는 캐릭터의 방향에 사용했다.
Leave a comment