제대로 따라가기 (2) C++ 프로그래밍 튜토리얼 :: 플레이어 입력 및 폰

 

작성버전 :: 4.20.3

 

언리얼 엔진 튜토리얼인 플레이어 입력 및 폰 문서에서는 폰(Pawn)[각주:1] 클래스를 확장해서 플레이어의 입력에 반응하도록 하는 법을 배울 수 있다.

 

튜토리얼대로 하면 문제가 발생해서 제대로 따라갈 수 없는 부분으로 동작이 가능하게 수정해야하는 부분은 빨간 블럭으로 표시되어 있다.
 
이번 튜토리얼에서 새로 배우게 되는 내용은 글 제일 끝에 "이번 섹션에서 배운 것"에 정리된다.

 

 

1. 폰 커스터마이즈(Pawn Customize)(튜토리얼)

 

프로젝트를 생성하고 Pawn 클래스를 상속받는 MyPawn 클래스를 생성해보자.

 

 

 

 

MyPawn 클래스의 생성이 성공적으로 끝났다면, 게임이 시작되었을 때 MyPawn이 자동으로 플레이어의 입력에 반응하도록 설정해보자. Pawn 클래스에는 초기화 중에 자동으로 플레이어의 입력에 반응하도록 설정해주는 변수를 제공한다. MyPawn.cpp의 AMyPawn::AMyPawn() 생성자를 다음과 같이 수정하자.

 

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

    AutoPossessPlayer = EAutoReceiveInput::Player0;
}

 

컴포넌트의 기록 유지를 위해서[각주:2] 다음의 코드를 MyPawn.h 의 클래스 정의 하단부에 추가하자.

 

UPROPERTY(EditAnywhere)
USceneComponent* OurVisibleComponent;

 

그리고 MyPawn.cpp로 돌아와서 폰에 카메라를 붙이고 위치와 회전을 설정하기 위해 다음과 같이 코드를 수정한다.

 

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

    AutoPossessPlayer = EAutoReceiveInput::Player0;

    RootComponent = CreateDefaultSubobject(TEXT("RootComponent"));
    UCameraComponent* OurCamera = CreateDefaultSubobject(TEXT("OurCamera"));
    OurVisibleComponent = CreateDefaultSubobject(TEXT("OurVisibleComponent"));
    OurCamera->SetupAttachment(RootComponent);
    OurCamera->SetRelativeLocation(FVector(-250.0f, 0.0f, 250.0f));
    OurCamera->SetRelativeRotation(FRotator(-45.0f, 0.0f, 0.0f));
    OurVisibleComponent->SetupAttachment(RootComponent);
}

 

하지만 이 구간에서 튜토리얼을 제대로 따라갈 수 없는 문제가 다시 발생한다.

 

 

 

1) 제대로 따라가기 (1) 섹션에서도 보았듯이 CreateDefaultSubobject() 함수에 템플릿 인자가 들어가 있지 않아서 어떤 오브젝트를 생성해야되는지 몰라서 신텍스 에러가 발생한다.

 

해결 :: CreateDefaultSubobject() 함수를 다음과 같이 수정하자.

 

RootComponent = CreateDefaultSubobject<USceneComponent>(TEXT("RootComponent"));
UCameraComponent* OurCamera = CreateDefaultSubobject<UCameraComponent>(TEXT("OurCamera"));
OurVisibleComponent = CreateDefaultSubobject<USceneComponent>(TEXT("OurVisibleComponent"));

 

2) UCameraComponent가 정의되어 있지 않다고 신텍스 에러가 발생한다.

 

해결 :: MyPawn.cpp의 헤더 포함 전처리기 아래에 "Engine/Classes/Camera/CameraComponent.h"를 포함시키자.

 

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

#include "MyPawn.h"
#include "Engine/Classes/Camera/CameraComponent.h"

 

이 두 가지를 모두 적용하고 나면 신텍스 에러가 더 이상 발생하지 않음을 볼 수 있다.

 

 

 

코드 수정이 모두 끝났다면 변경사항을 모두 저장하고 에디터로 돌아가서 컴파일을 해보자.

 

 

 

 

 

2. 게임 입력 환경설정(튜토리얼)

 

게임에서 특정한 키를 눌렀을 때, 특정 동작을 하도록 만드는 것을 언리얼에서는 입력 매핑이라고 한다. 이러한 입력 매핑에는 두 가지 종류가 있다.

 

액션 매핑(Action Mapping) - 마우스나 조이스틱, 패드, 키보드 버튼처럼 누르거나, 떼거나, 더블 클릭하거나, 특정 시간동안 누르고 있을 때 보고한다. 점프, 공격, 상호작용 등이 액션 매핑의 예시이며, X를 눌러서 조이를 표하는 것도 액션 매핑에 속한다.

 

축 매핑(Axis Mapping) - 연속적인 것으로 마우스의 위치나 조이스틱 막대의 기울기 같은 것으로 "일정량"의 입력으로 생각하면 된다. 움직이지 않더라도 매 프레임 값을 보고한다. 걷기, 달리기, 둘러보기, 탈 것의 방향조절 같은 것들이 주로 축 매핑으로 처리된다.

 

코드에서도 직접 입력 매핑을 할 수 있지만, 일반적으로는 에디터에서 정의하는 경우가 많으니, 이 튜토리얼에서는 그 방식을 따른다.

 

1. 언리얼 엔진 에디터에서 편집 드롭다운 메뉴에서 프로젝트 세팅 옵션을 선택한다.

 

 

2. 왼쪽의 엔진 섹션의 입력 항목을 선택하고 바인딩(Binding) 카테고리에 다음과 같이 하나의 액션 매핑과 두 개의 축 매핑을 추가한다.

 

 

3. 입력 환경 설정이 모두 끝났다면, 레벨에 MyPawn을 배치한다. 콘텐츠 브라우저에 있는 MyPawn 클래스를 레벨 에디터에 끌어다 놓으면 된다.

 

 

 

4. 레벨에 MyPawn을 배치한 뒤에는, 우리가 배치한 Pawn이 움직이는 것을 볼 수 있게 하기 위해서 OurVisibleComponent의 스태틱 메시(Static Mesh) 카테고리에 "Shape_Cylinder"를 넣어야 한다고 언리얼 튜토리얼 문서에 나와있다.

 

 

 

하지만 우리가 배치한 MyPawn의 OurVisibleComponent에서는 스태틱 메시 카테고리가 보이지 않는 것을 알 수 있다.

 

 

 

이 문제의 원인을 추측해보자면 언리얼 튜토리얼의 예시 코드에는 CreateDefaultSubobject() 함수로 컴포넌트를 생성할 때, 명시적인 컴포넌트 타입이 없었기 때문에 헤더에 추가한 OurVisibleComponent의 타입에 맞춰서 USceneComponent로 생성했기 때문에 발생한 문제로 보인다.

 

언리얼 튜토리얼의 예시 코드

OurVisibleComponent = CreateDefaultSubobject(TEXT("OurVisibleComponent"));

 

수정한 예시코드

OurVisibleComponent = CreateDefaultSubobject<USceneComponent>(TEXT("OurVisibleComponent"));

 

그렇다면 스태틱 메시 카테고리가 나오도록 하려면 어떻게 해야할까? 바로 CreateDefaultSubobject() 함수로 UStaticMeshComponent를 생성해서 OurVisibleComponent에 대입시켜 주면 될 것 같다. 언리얼 엔진 문서에 따르면 UStaticMeshComponent는 USceneComponent를 상속받고 있기 때문에 충분히 가능한 코드이다. 여기까지 유추했다면 코드를 다음과 같이 수정해보자.

 

OurVisibleComponent = CreateDefaultSubobject<UStaticMeshComponent>(TEXT("OurVisibleComponent"));

 

UStaticMeshComponent가 USceneComponent를 상속받고 있기 때문에 충분히 대입이 가능할거라고 생각했는데 할당할 수 없다는 에러가 발생한다.

 

 

 

이 경우는 타이머를 배울 때, GetWorldTimerManager() 함수를 호출해서 기능을 사용하려고 했을 때를 생각해보자. 그 때 불완전한 형식은 사용할 수 없다는 에러가 떴었던 것과 그 문제를 해결하기 위해서 "TimerManager.h"를 포함시켜주었던 것을 기억할 수 있다.

 

그와 같이 MyPawn.cpp의 헤더 포함 전처리기 부분에 "Engine/Classes/Components/StaticMeshComponent.h"를 포함시키면 CreateDefaultSubobject()로 생성한 UStaticMeshComponent가 성공적으로 OurVisibleComponent에 대입되는 것을 확인할 수 있다.

 

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

#include "MyPawn.h"
#include "Engine/Classes/Camera/CameraComponent.h"
#include "Engine/Classes/Components/StaticMeshComponent.h"

 

코드를 모두 수정하고 에디터로 돌아가서 컴파일을 진행하면 아까 전까지는 보이지 않았던 OurVisibleComponent의 스태틱 메시 카테고리가 보이는 것을 확인할 수 있다.

 

그럼 이제 Static Mesh에 Shape_Cylinder를 넣어주자.

 

 

 

 

 

 

3. 게임 액션 프로그래밍 및 바인딩(튜토리얼)

 

게임 입력 환경설정 파트에서 매핑한 입력 매핑과 코드의 함수 동작을 묶어서 입력이 들어오면 입력 매핑에 묶어준 함수가 실행되도록 하는 것을 바인딩(Binding)이라고 한다.

 

입력 매핑에 바인딩할 함수들과 동작에 관련된 변수들을 MyPawn.h에 추가해보도록 하자.

 

void Move_XAxis(float AxisValue);
void Move_YAxis(float AxisValue);
void StartGrowing();
void StopGrowing();

FVector CurrentVelocity;
bool bGrowing;

 

헤더에 함수들을 모두 정의했다면 MyPawn.cpp에서 함수들을 구현해야 한다.

 

void AMyPawn::Move_XAxis(float AxisValue)
{
    CurrentVelocity.X = FMath::Clamp(AxisValue, -1.0f, 1.0f) * 100.0f;
}

void AMyPawn::Move_YAxis(float AxisValue)
{
    CurrentVelocity.Y = FMath::Clamp(AxisValue, -1.0f, 1.0f) * 100.0f;
}

void AMyPawn::StartGrowing()
{
    bGrowing = true;
}

void AMyPawn::StopGrowing()
{
    bGrowing = false;
}

 

축 입력 매핑에 대한 동작을 구현할 때, FMath::Clamp()함수를 사용했는데 이것은 입력된 값이 -1.0과 1.0 사이를 벗어나지 않도록 만들어 준다. 전 파트에서 우리가 축 매핑을 추가할 때, MoveX의 입력을 W와 S만을 추가했는데 만약 다른 입력 방식도 사용하기 위해서 위쪽 화살표와 아래쪽 화살표로도 MoveX 입력을 받도록 만들었을 때, 만약 Clamp로 입력의 범위를 제한하지 않았다면 W와 위쪽 화살표를 동시에 누른다면 캐릭터가 두 배의 속도로 빠르게 움직이는 버그가 발생할 것이다.

 

입력 함수의 정의와 구현을 모두 끝냈으니, 적합한 입력에 반응하도록 바인딩을 진행할 차례다. AMyPawn::SetupPlayerInputComponent() 함수 안에 다음 코드를 작성하자.

 

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

    InputComponent->BindAction("Grow", IE_Pressed, this, &AMyPawn::StartGrowing);
    InputComponent->BindAction("Grow", IE_Released, this, &AMyPawn::StopGrowing);

    InputComponent->BindAxis("MoveX", this, &AMyPawn::Move_XAxis);
    InputComponent->BindAxis("MoveY", this, &AMyPawn::Move_YAxis);
}

 

InputComponent의 함수를 호출해서 사용하려고 할 때 여기서도 불완전한 형식을 사용할 수 없다는 에러가 발생할 것이다.

 

MyPawn.cpp의 전처리기 파트 아래쪽에 "Engine/Classes/Components/InputComponent.h"를 포함시켜주자.

 

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

#include "MyPawn.h"
#include "Engine/Classes/Camera/CameraComponent.h"
#include "Engine/Classes/Components/StaticMeshComponent.h"
#include "Engine/Classes/Components/InputComponent.h"

 

입력 매핑과 바인딩을 모두 끝냈으니, 입력으로 변하는 변수를 통해서 동작하는 코드를 작성해보자. AMyPawn::Tick() 함수를 다음과 같이 수정하자.

 

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

    {
        float CurrentScale = OurVisibleComponent->GetComponentScale().X;
        if (bGrowing)
        {
            CurrentScale += DeltaTime;
        }
        else
        {
            CurrentScale -= (DeltaTime * 0.5f);
        }

        CurrentScale = FMath::Clamp(CurrentScale, 1.0f, 2.0f);
        OurVisibleComponent->SetWorldScale3D(FVector(CurrentScale));
    }

    {
        if (!CurrentVelocity.IsZero())
        {
            FVector NewLocation = GetActorLocation() + (CurrentVelocity * DeltaTime);
            SetActorLocation(NewLocation);
        }
    }
}

 

마지막으로 수정한 코드를 저장하고, 에디터로 돌아와서 컴파일을 한 뒤에 플레이해보면 WASD를 입력하면 배치한 MyPawn이 움직이고 스페이스바를 누르면 커지고 손을 떼면 다시 작아지는 것을 볼 수 있다.

 

 

 

 

 

 


 

 

이번 섹션에서 배운 것

 

1. Pawn(언리얼 엔진 문서)

 

Pawn 클래스는 플레이어나 AI가 컨트롤할 수 있는 모든 액터의 베이스 클래스다.

 

2. APawn::AutoPossessPlayer

 

레벨이 시작되거나 폰이 생성되었을 때, 플레이어 컨트롤러가 있다면 어떤 플레이어 컨트롤러가 자동으로 이 폰을 소유해야 되는지에 대한 변수다.

 

3. USceneComponent

 

USceneComponent* RootComponent;

USceneComponent* SubComponent;

 

USceneComponent는 트랜스폼을 가지고 있고 다른 컴포넌트를 이 컴포넌트에 덧붙이는(Attachment) 것을 지원하지만 충돌 같은 물리적 효과를 지원하지 않고 렌더링 기능이 없다. 계층 구조에서 더미로 활용하기 좋다.

 

SubComponent->SetupAttachment(RootComponent);

 

SetupAttachment() 함수는 컴포넌트를 다른 컴포넌트의 아래 계층으로 붙이는데 사용된다. 위의 예시 코드에 따르면 SubComponent는 계층적으로 자식 컴포넌트가 되고 RootComponent는 부모 컴포넌트가 되는 것이다.

 

SubComponent->SetRelativeLocation(FVector(-250.0f, 0.0f, 250.0f));

 

SetRelativeLocation() 함수는 현재 컴포넌트가 상위 계층의 컴포넌트나 오브젝트로부터 얼마나 떨어진 위치에 있을지 정한다.

 

SubComponent->SetRelativeRotation(FRotator(-45.0f, 0.0f, 0.0f));

 

SetRelativeRotation() 함수는 현재 컴포넌트가 부모를 기준으로 얼마나 회전된 상태인지 정한다.

 

SubComponent->GetComponentScale();

 

GetComponentScale() 함수는 월드 스페이스에서의 컴포넌트 크기를 가져온다.

 

SubComponent->SetWorldScale3D(FVector(0.0f, 0.0f, 0.0f));

 

SetWorldScale3D() 함수는 월드 스페이스에서의 컴포넌트 크기를 수정한다.

 

4. UCameraComponent

 

액터에 덧붙일 수 있는 카메라 컴포넌트이다.

 

5. UStaticMeshComponent

 

엑터에 덧붙일 수 있는 스태틱 메시 컴포넌트이다. 월드에 렌더링된다.

 

6. AActor::InputComponent

 

입력이 활성화된 액터에 대한 입력을 처리하는 컴포넌트이다.

 

InputComponent->BindAction("Action", IE_Pressed, this, &AMyActor::ActionProcess);

 

액션 매핑에 처리 함수를 바인딩하는 함수다.

 

첫 번째 매개변수는 바인딩할 액션 매핑의 이름이다.

 

두 번째 매개변수는 처리할 키 이벤트다. 기본적으로 사용되는 이벤트는 키가 눌렸을 때를 뜻하는 IE_Pressed와 눌린 키가 떼졌을 때를 뜻하는 IE_Released가 있다.

 

세 번째 매개변수는 입력을 바인딩하는 오브젝트이다.

 

네 번째 매개변수는 입력이 들어왔을 때 입력을 처리하는 함수이다.

 

InputComponent->BindAxis("Axis", this, &AMyPawn::AxisProcess);

 

축 매핑에 처리 함수를 바인딩하는 함수다.

 

첫 번째 매개변수는 바인딩할 축 매핑의 이름이다.

 

두 번째 매개변수는 입력을 바인딩하는 오브젝트이다.

 

세 번째 매개변수는 입력이 들어왔을 때 입력을 처리하는 함수이다.

 

7. AActor::GetActorLocation()

 

GetActorLocation();

 

액터의 월드 스페이스 상의 위치를 가져오는 함수이다.

 

8. AActor::SetActorLocation()

 

SetActorLocation(FVector(0.0f, 0.0f, 0.0f));

 

액터의 월드 스페이스 상의 위치를 정하는 함수이다.

 

9. FMath::Clamp()

 

FMath 클래스는 수학적인 기능들을 제공한다.

 

FMath::Clamp(Value, Min, Max);

 

Clamp() 함수는 Value의 값이 Min보다 값이 작으면 Min 값을, Max보다 크면 Max 값을 돌려주고, 그 사잇값이라면 Value를 돌려주는 함수이다. 값이 특정한 범위를 벗어나면 안되는 경우에 사용하면 좋다.

  1. 폰(Pawn)이란 플레이어나 AI의 컨트롤러가 빙의(연결)되어 제어받을 수 있도록 설계된 클래스이다. [본문으로]
  2. UPROPERTY() 매크로가 적용된 변수는 언리얼 에디터에서 볼 수 있고, 게임이 실행되거나, 프로젝트나 레벨을 닫고 다시 불러와도 변수가 리셋되지 않는다. [본문으로]

 

[투네이션]

 

-

 

toon.at

[Patreon]

 

WER's GAME DEVELOP CHANNEL님이 Game making class videos 창작 중 | Patreon

WER's GAME DEVELOP CHANNEL의 후원자가 되어보세요. 아티스트와 크리에이터를 위한 세계 최대의 멤버십 플랫폼에서 멤버십 전용 콘텐츠와 체험을 즐길 수 있습니다.

www.patreon.com

[디스코드 채널]

 

Join the 베르의 게임 개발 채널 Discord Server!

Check out the 베르의 게임 개발 채널 community on Discord - hang out with 399 other members and enjoy free voice and text chat.

discord.com

 

반응형
  1. Artsdayo 2019.02.04 03:09

    정말 이 블로그 밖에 없네요.... 정말 고맙습니다 ㅠㅠㅠ
    튜토리얼에서 3시간동안 삽질 했네요... 정말... 고맙습니다 ㅠㅠㅠ

    • wergia 2019.02.04 11:25 신고

      저도 언리얼 처음 공부할 때 이 부분에서 시간을 많이 썼습니다 ㅎㅎ

  2. Teoun 2020.04.29 22:05

    적어 놓으신 강의를 쭉 보고 있는데 너무 도움됩니다...ㅠㅠ
    감사합니다..ㅠㅠㅠ

  3. NogameNoHope 2020.09.20 02:02

    많은 블로그를 둘러보았지만 이 곳 만큼 쉬운 해설과 오류 발생시의 대처법 등을 상세히 적어놓은 곳을 보지 못했습니다.
    언리얼 공식 튜토리얼보다도 훨씬 이해가 쉽고 따라하기 좋습니다. 감사합니다.

    • wergia 2020.10.20 00:08 신고

      저도 공식 튜토리얼 보다가 어려워서 적어봤습니다 ㅎㅎ

+ Recent posts