[33-34. Character Assets]
https://www.mixamo.com/#/?page=1&query=castle&type=Character
위의 링크에서 Castle Guard 01을 다운로드 받고 프로젝트에 모두 임포트 한다.
필요한 애니메이션도 모두 받아서 Import 하는데 스켈레탈 메시는 캐슬 가드를 골라주도록 한다.
[35. The Character Class #1]
Character를 상속받은 C++ 클래스를 Main 이라는 이름으로 생성한다.
ACharacter를 상속받게 되는데, 이 부모 클래스에 USkeletalMeshComponent* 타입의 Mesh라는 변수가 이미 선언되어있다.
그 외에 CharacterMovement, CapsuleComponent도 같이 선언되어 있고 CapsuleComponent가 루트 컴포넌트가 된다.
루트 컴포넌트가 될 수 있는것은 CapsuleComponent 뿐이다.
private:
/** The main skeletal mesh associated with this Character (optional sub-object). */
UPROPERTY(Category=Character, VisibleAnywhere, BlueprintReadOnly, meta=(AllowPrivateAccess = "true"))
USkeletalMeshComponent* Mesh;
UPROPERTY 속성의 meta=(Allow..) 키워드는 private임에도 에디터에서 보이게 한다.
블루프린트 내부에서 접근할 수 있지만 외부에서는 접근할 수 없다.
// Main.h
public:
/* Camera boom positioning the camera behind the player */
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = Camera, meta = (AllowPrivateAccess=true))
class USpringArmComponent* CameraBoom;
/* Follow Camera */
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = Camera, meta = (AllowPrivateAccess = true))
class UCameraComponent* FollowCamera;
// Main.cpp
#include "GameFramework/SpringArmComponent.h"
#include "Camera/CameraComponent.h"
AMain::AMain()
{
// 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;
// Create Camera Boom (pulls towards the player if there's a collision)
CameraBoom = CreateDefaultSubobject<USpringArmComponent>(TEXT("CameraBoom"));
CameraBoom->SetupAttachment(GetRootComponent());
CameraBoom->TargetArmLength = 600.f; // Camera follows at this distance
CameraBoom->bUsePawnControlRotation = true; // Rotate arm based on controller
// Create Follow Camera
FollowCamera = CreateDefaultSubobject<UCameraComponent>(TEXT("FollowCamera"));
FollowCamera->SetupAttachment(CameraBoom, USpringArmComponent::SocketName);
// Attach the camera to the end of the boom and let the boom adjust to match
// the controller orientation
FollowCamera->bUsePawnControlRotation = false;
}
bUsePawnControlRotation는 컨트롤러의 회전을 따라갈 것인가를 정하는 것이다.
스프링 암은 컨트롤러를 따라가는 것이 좋고, 카메라는 스프링암을 따라가야 하기 때문에 각각 true/false로 둔다.
빌드를 하고 블루프린트 클래스로 만들어둔다.
[36. The Character Class #2]
스켈레탈 메시를 우리가 받은것으로 설정하고 캡슐 컴포넌트의 크기도 캐릭터와 맞게 대략적으로 맞춰준다.
메시의 y축과 화살표의 방향도 맞춰준다.
조작을 구현할 차례이다.
이전의 폰에서 마우스 움직임으로 회전을 구현했을 때는 마우스 이동시 캐릭터가 회전하기 때문에 하위 계층의 스프링 암과 카메라가 움직이는 방식이었다면 이번에는 카메라만 회전시키는 방식으로 구현해 본다.
부모 클래스인 ACharacter에 포함된 CharacterMovementComponent에 접근하여 사용한다.
사용하기 전에 nullptr 여부를 검사하고 사용하는 것이 좋다.
// Main.h
public:
/* Base turn rates to scale turning functions for the camera */
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = Camera)
float BaseTurnRate;
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = Camera)
float BaseLookUpRate;
// Main.cpp
AMain::AMain()
{
// Set our turn rates for input
BaseTurnRate = 65.f;
BaseLookUpRate = 65.f;
}
/* Called for forwards/backwards input */
void AMain::MoveForward(float Value)
{
if ((Controller != nullptr) && (Value != 0.0f))
{
// find out which way is forward
const FRotator Rotation = Controller->GetControlRotation();
const FRotator YawRotation(0.f, Rotation.Yaw, 0.f);
// FRotationMatrix: FRotator를 회전행렬로 변환한다.
// GetUnitAxis: 특정 축을 정규화 한것을 반환한다.
const FVector Direction = FRotationMatrix(OUT YawRotation).GetUnitAxis(EAxis::X);
AddMovementInput(Direction, Value);
}
}
/* Called for side to side input */
void AMain::MoveRight(float Value)
{
if ((Controller != nullptr) && (Value != 0.0f))
{
// find out which way is forward
const FRotator Rotation = Controller->GetControlRotation();
const FRotator YawRotation(0.f, Rotation.Yaw, 0.f);
const FVector Direction = FRotationMatrix(OUT YawRotation).GetUnitAxis(EAxis::Y);
AddMovementInput(Direction, Value);
}
}
FRotationMatrix로 Rotator를 회전 행렬로 변환하고 거기서 X축에 대한 정규 벡터를 반환받고 이것이 방향 벡터가 된다.
유의해야 하는 점은 Direction은 메시의 방향벡터가 아닌 컨트롤러(마우스)의 방향벡터이다.
차후 마우스 움직임과 메시의 회전을 분리시키고 이동 자체는 마우스의 방향을 따라가도록 하기 위함이다.
[37. The Character Class #3]
키보드 조작과 마우스 조작 둘 다 구현해본다.
// Main.h
/* Called via input to turn at a given rate
* @param Rate This is a normalized rate, i.e. 1.0 means 100% of desired turn rate
*/
void TurnAtRate(float Rate);
/* Called via input to look up/down at a given rate
* @param Rate This is a normalized rate, i.e. 1.0 means 100% of desired look up/down rate
*/
void LookUpAtRate(float Rate);
// Main.cpp
void AMain::SetupPlayerInputComponent(UInputComponent* PlayerInputComponent)
{
Super::SetupPlayerInputComponent(PlayerInputComponent);
check(PlayerInputComponent);
PlayerInputComponent->BindAction("Jump", IE_Pressed, this, &ACharacter::Jump);
PlayerInputComponent->BindAction("Jump", IE_Released, this, &ACharacter::StopJumping);
PlayerInputComponent->BindAxis("MoveForward", this, &AMain::MoveForward);
PlayerInputComponent->BindAxis("MoveRight", this, &AMain::MoveRight);
PlayerInputComponent->BindAxis("Turn", this, &APawn::AddControllerYawInput);
PlayerInputComponent->BindAxis("LookUp", this, &APawn::AddControllerPitchInput);
PlayerInputComponent->BindAxis("TurnRate", this, &AMain::TurnAtRate);
PlayerInputComponent->BindAxis("LookUpRate", this, &AMain::LookUpAtRate);
}
void AMain::TurnAtRate(float Rate)
{
AddControllerYawInput(Rate * BaseTurnRate * GetWorld()->GetDeltaSeconds());
}
void AMain::LookUpAtRate(float Rate)
{
AddControllerPitchInput(Rate * BaseLookUpRate * GetWorld()->GetDeltaSeconds());
}
전체 코드. 여기서 눈여겨 볼 부분이 있다.
PlayerInputComponent->BindAxis("Turn", this, &APawn::AddControllerYawInput);
PlayerInputComponent->BindAxis("LookUp", this, &APawn::AddControllerPitchInput);
PlayerInputComponent->BindAxis("TurnRate", this, &AMain::TurnAtRate);
PlayerInputComponent->BindAxis("LookUpRate", this, &AMain::LookUpAtRate);
위의 두개는 Pawn의 AddControllerYawInput을 바인딩 하고 아래 두개는 따로 만든것을 바인딩한다.
하지만 따로 만든 함수의 내용도 보면 똑같이 AddControllerYawInput을 호출한다.
차이점이 있다면 위의 두개는 마우스 이동을 위해서 상위 클래스의 함수를 바로 바인딩 한거고 아래 두개는 키보드로 회전을 하려면 별도의 계수가 필요하므로 계산식을 추가한 것이다.
BaseTurnRate, BaseLookUpRate가 클수록 회전 속도는 빠르게 될 것이다. 부드러운 회전을 위해 델타타임을 곱해주면 부드럽게 회전이 된다.
프로젝트 세팅의 바인딩도 수정해 주어야 한다.
추가로 BindAxis는 매 틱마다 함수를 호출하는데 비해 BindAction은 이벤트가 발생할 때만 호출되기 때문에 조금 더 최적화가 되어있다.
Pawn의 컨트롤을 구현할 때와는 달리 별다른 코딩을 하지 않고 상위 클래스의 컨트롤러를 상속받아 사용했다.
만약 추가적인 구현이 필요하다면 오버라이딩이 가능한지 확인 후 오버라이딩 해서 사용하면 될 것이다.
아직은 카메라만 따로 회전하는게 아니라 캐릭터가 회전하고 있지만 곧 수정하게 될 것이다.
[38. The Character Class #4]
가끔 C++ 코드를 변경했음에도 기반으로 하는 블루프린트 클래스가 변화하지 않는다면 에디터를 껐다가 켜보고 그럼에도 안되면 블루프린트 클래스를 삭제했다가 재생성하는 방법을 이용해야 한다.
다만 블루프린트 클래스의 값이 변하지 않았다고 무작정 삭제하기 전에 값 되돌리기 버튼을 눌러서 하드코딩한 값이 적용되는지 확인하고 진행해야 한다.
이제 카메라의 회전과 캐릭터의 회전을 분리시켜보도록 한다. 매우 간단하다.
// Main.cpp
AMain::AMain()
{
// Don't rotate when the controller rotates.
// Let that just affect the camera.
bUseControllerRotationYaw = false;
bUseControllerRotationPitch = false;
bUseControllerRotationRoll = false;
}
특정 축만 false로 만들수도 있지만 지금은 모든 축에 대해 컨트롤러 조작시 캐릭터가 회전하지 않도록 한다.
실행 시 카메라(정확하게는 스프링 암)의 회전과 캐릭터의 회전이 분리가 되었고 W키를 입력시 카메라가 바라보는 방향으로 이동한다.
상속받은 Character 클래스에 포함된 CharacterMovementComponent는 굉장히 많은 변수와 기능이 포함되어있다.
AMain::AMain()
{
// Configure character movement
GetCharacterMovement()->bOrientRotationToMovement = true; // Character moves in the direction of input...
GetCharacterMovement()->RotationRate = FRotator(0.f, 540.f, 0.f); // ...at this rotation rate
GetCharacterMovement()->JumpZVelocity = 650.f;
GetCharacterMovement()->AirControl = 0.2f;
}
bOrientRotationToMovement는 true시 RotationRate를 회전 변경 속도로 사용하여 캐릭터를 가속 방향으로 회전시킨다.
W키를 누른 상태에서 카메라를 돌리거나 ASD키로 움직일 시 캐릭터가 회전하게 된다.
즉, 회전 보간이 적용된다.
RotationRate는 회전의 속도라고 보면 된다.
JumpZVelocity는 점프의 가속도, AirControl은 캐릭터의 공중 조작의 자유도라고 보면 된다.
AirControl의 수치가 클수록 방향전환이 잘되고 작을수록 안된다.
'언리얼 엔진 > UE4 C++' 카테고리의 다른 글
[UE C++] Gameplay Mechanics #1 (0) | 2022.09.06 |
---|---|
[UE C++] The Character Class #2 (0) | 2022.09.06 |
[UE C++] The Pawn Class #2 (0) | 2022.09.05 |
[UE C++] The Pawn Class #1 (0) | 2022.09.04 |
[UE C++] The Actor Class #2 (0) | 2022.09.04 |