언리얼 엔진에서 Player Controller는 Pawn과 그것을 제어하려는 사람 사이의 인터페이스로서, 플레이어의 의지를 대변하는 클래스이다. Player Controller는 Controller 클래스를 상속받는데, Possess() 함수로 Pawn의 제어권을 획득하고, UnPossess() 함수로 제어권을 포기한다.
이러한 설명이 언리얼 문서 상에서의 Player Controller에 대한 내용의 대부분이고, 실제적인 C++ 코드에서의 사용법은 레퍼런스에서 찾아서 멤버 변수와 함수 등을 직접 찾아서 읽어야 하는 불편함이 있기 때문에 입문자는 Player Controller의 사용법을 제대로 익히기가 쉽지 않다.
그럼 이제부터 Player Controller의 기본적인 기능과 사용법에 대해서 알아보도록 하자.
마우스 인터페이스
플레이어 컨트롤러가 기본적으로 제공하는 기능 중에는 마우스 인터페이스에 관련된 기능이 있다.
플레이어 컨트롤러를 상속받는 블루프린트 클래스를 만들고 에디터를 열어보면 디테일 패널의 Mouse Interface 카테고리에서 플레이어 컨트롤러가 기본적으로 제공하는 마우스 인터페이스와 관련된 기능들을 한눈에 볼 수 있다. 디테일 패널에서 보이는 것들은 아주 기본적인 내용으로 이보다 심도깊은 기능에 대해서 배우고자 한다면 APlayerController 클래스의 언리얼 레퍼런스를 확인하는게 좋다.
C++ 코드 상에서는 UPlayerController 클래스의 위와 같은 프로퍼티들을 통해서 옵션을 바꿀 수 있다.
Show Mouse Cursor
Show Mouse Cursor 옵션은 말 그대로 게임 도중에 마우스 커서를 보이게 할 것인가에 대한 옵션이다. 예를 들어보자면, 전통적인 PC 플랫폼에서의 탑다운뷰 RPG 게임은 캐릭터를 이동시킬 때 주로 마우스를 이용한다. 때문에 커서가 항상 보이도록 이 옵션을 true로 해줘야 한다. 하지만 FPS 게임은 화면 중앙의 크로스헤어에 적을 일치시키고 공격하는 방식이기 때문에 커서가 보일 필요가 없다. 혹은 위 두 가지 사례가 융합된 장르들은 전투나 이동 같은 플레이 중일 때는 커서를 비활성화시킨 상태로 크로스헤어를 사용하고, 인벤토리나 지도를 열면 커서를 활성화시키는 방식으로 사용하기도 한다.
bShowMouseCursor = true;
bShowMouseCursor = false;
C++ 코드 상에서는 UPlayerController 클래스의 bShowMouseCursor 프로퍼티를 통해서 옵션을 바꿀 수 있다.
Event
그 다음 네 가지 옵션은 월드에 배치된 액터/컴포넌트가 마우스나 터치에 대해서 이벤트를 발생시킬지에 대한 프로퍼티이다.
Click Event / Touch Event는 액터/컴포넌트에 대한 클릭/터치 이벤트이고, Mouse Over Event / Touch Over Event는 액터/컴포넌트에 커서나 터치가 올라가 있는 상태에 대한 이벤트이다.
간단하게 예시를 보자면 언리얼 프로젝트에 테스트용 C++ 클래스를 하나 만들고 NotifyActorOnClicked() 함수를 덮어씌워서 다음과 같이 구현하고 프로젝트를 다시 빌드한다.
그리고 새 Player Controller를 하나 만들어서 Show Mouse Cursor와 Enable Click Event를 true로 설정한 뒤, 프로젝트 설정의 맵 & 모드에서 기본 플레이어 컨트롤러로 설정한다.
그 다음 아까 만든 액터를 레벨에 배치하고 클릭할 수 있게 스태틱 메시 컴포넌트를 추가해주자.
레벨 에디터에서 플레이 버튼을 누르고 추가한 액터를 클릭해보면 출력 로그 패널에서 "NotifyActorOnClicked"라고 출력되는 것을 확인할 수 있다.
액터나 컴포넌트에 Click Event를 추가했더라도 Player Controller에서 Enable Click Event를 false로 설정했다면, NotifyActorOnClicked() 이벤트는 호출되지 않는다.
Default Mouse Cursor
Default Mouse Cursor 프로퍼티는 기본적인 마우스 커서 모양을 정하는 프로퍼티이다. 커서 모양의 종류는 언리얼 문서에서 확인할 수 있다.
DefaultMouseCursor = EMouseCursor::Default;
C++ 코드 상에서는 UPlayerController 클래스의 DefaultMouseCursor 프로퍼티를 통해서 옵션을 바꿀 수 있다.
Default Click Trace Channel
Default Click Trace Channel은 클릭 이벤트가 발생했을 때, 플레이어가 클릭한 대상을 3D 공간에서 찾기 위해서 트레이스를 통해 광선 같은 개념의 선을 쏘는데 어떤 채널에 속하는 객체가 맞았을 때 찾았다고 판정할 지를 정하는 프로퍼티이다. "Visibility"가 기본값으로 되어 있는데, 이는 화면에 보이는 객체가 맞으면 무조건 맞았다고 판정한다는 의미이다.
이 값의 종류에는 Visibility 이 외에도, Pawn만 찾아내는 Pawn, 월드에 스태틱으로 배치된 액터만 찾아내는 WorldStatic 등이 있다.
C++ 코드 상에서는 UPlayerController 클래스의 DefaultClickTraceChannel 프로퍼티를 통해서 옵션을 바꿀 수 있다.
Trace Distance
Trace Distance 프로퍼티는 클릭 이벤트 등이 발생했을 때, 화면으로부터 얼마나 멀리 떨어진 지점까지 대상을 탐색할 것인가에 대한 것이다.
HitResultTraceDistance = 10000.0f;
그 외의 유용한 마우스 관련 함수
1. GetHitResultUnderCursor()
GetHitResultUnderCursor() 함수는 함수 이름 그대로 화면에서 마우스 커서 위치에 트레이스를 쏴서 그 결과를 가져오는 함수이다. 일반적인 함수의 사용법은 아래와 같으며, 이 함수는 레벨에 배치된 오브젝트를 선택하거나, RPG 식으로 클릭한 위치로 캐릭터를 이동시킬 때, 유용하게 사용할 수 있다.
플레이어 컨트롤러는 액터 클래스에서 상속받는 Tick() 함수 외에 PlayerTick() 이라는 별도의 틱 함수를 하나 더 가진다. 일반 Tick() 함수는 어디서나 작동하는 반면에, PlayerTick() 함수는 Player Controller에 PlayerInput 객체가 있는 경우에만 호출된다. 따라서 로컬로 제어되는 Player Controller에서만 플레이어 틱이 호출된다. 이 말인 즉슨, 만약 멀티플레이 게임이라면 자기 자신의 플레이어 컨트롤러에서만 플레이어 틱이 호출된다는 것이다.
플레이어 컨트롤러는 기본적으로 자신이 빙의(Possess)하여 컨트롤하는 폰을 레퍼런스로 가지고 있게 된다. 일반적으로 프로젝트 세팅의 맵 & 모드나 월드 세팅에서 디폴트 폰을 None이 아닌 특정 폰으로 설정해둔 상태라면, 게임이 시작되면 폰이 생성되고 플레이어 컨트롤러는 생성된 폰에 자동으로 빙의된다.
디폴트 폰이 None이어서 자동으로 빙의되는 상황이 아니라면, Possess() 함수를 이용하여 빙의하고자 하는 폰에 컨트롤러를 빙의시키면된다.
Possess(TargetPawn);
이 반대의 경우로 현재 빙의하고 있는 폰에서 제어권을 포기하고 탈출하려고 한다면 UnPossess() 함수를 사용하면 된다.
UnPossess();
컨트롤러가 빙의 중인 폰을 가져와서 사용하기 위해서는 다음과 같이 GetPawn() 함수를 사용하면 된다.
APawn* MyPawn = GetPawn();
만약에 컨트롤러가 현재 컨트롤 중인 폰이 없는 상태라면 GetPawn() 함수는 nullptr를 반환한다.
프로젝트를 새로 생성하고 Pawn 클래스를 상속받는 "CollidingPawn"을 생성한다. 이 폰은 컴포넌트를 가지고 레벨 안에서 이동하고 입체 오브젝트와 충돌하게 된다.
CollidingPawn.h의 클래스 정의 하단부에 UParticleSystemComponent를 추가한다.
UParticleSystemComponent* OurParticleSystem;
UParticleSystemComponent가 정의되어 있지 않다고 에러가 발생한다면, CollidingPawn.generated.h 포함 전처리기 위쪽에서 "Engine/Classes/Particles/ParticleSystemComponent.h"을 포함시켜 주면 된다.
// Fill out your copyright notice in the Description page of Project Settings.
여기에 대한 또 다른 해결책으로는 UParticleSystemComponent 타입의 변수를 선언할 때, 아래처럼 앞에 class를 붙여주면 헤더를 .h에 포함하지 않아도 에러가 발생하지 않는다.
classUParticleSystemComponent* OurParticleSystem;
대신 이 경우에는 .cpp에서 해당 타입의 변수를 사용할 때, 불완전한 형식을 사용할 수 없다는 에러가 발생할 것이기 때문에 .cpp의 헤더 포함 전처리기에 "Engine/Classes/Particles/ParticleSystemComponent.h"를 포함하는 코드를 추가시켜주어야 한다.
멤버 변수로 만들지 않아도 컴포넌트를 만들 수 있지만, 코드에서 컴포넌트를 사용하려면 클래스 멤버 변수로 만들어야 한다.
이 다음에는 CollidingPawn.cpp의 ACollidingPawn::ACollidingPawn() 생성자 함수를 편집해서 필요한 컴포넌트들을 스폰할 코드를 추가하고 계층구조로 배치해야 한다. 물리 월드와 상호작용을 위한 Sphere Component, 콜리전 모양을 시각적으로 보여줄 Static Mesh Component, 시각적인 효과를 더하며 켜고 끌 수 있는 Particle System Component, 게임 내의 시점 제어를 위해 Camera Component에 덧붙일 Spring Arm Component를 만든다.
먼저 계층구조에서 루트가 될 컴포넌트를 결정해야 한다. 이 튜토리얼에서는 Sphere Component가 루트 컴포넌트가 된다. 물리적으로 실존이 있고, 게임 월드와의 상호작용이 가능하기 때문이다. 참고로 액터에는 계층구조 안에서 다수의 물리 기반 컴포넌트가 있을 수 있지만, 이 튜토리얼에서는 하나만 사용한다.
ConstructorHelpers가 정의되어 있지 않은 문제는 CollidingPawh.cpp에 "ConstructorHelpers.h"를 포함시켜주면 된다.
#include "ConstructorHelpers.h"
여기까지 해결하고 나면 ConstructorHelpers::FObjectFinder에서 [클래스 템플릿 "ConstructorHelpers::FObjectFinder"에 대한 인수 목록이 없습니다.] 라는 에러가 발생할 것이다. 이 문제를 해결하기 위해서 ConstructorHelpers::FObjectFinder의 원형을 살펴보면 ConstructorHelpers::FObjectFinder는 템플릿을 사용하는 것을 알 수 있다. 그렇다면 여기서 중요한 점은 템플릿 인자에 어떤 타입이 들어가야 하는가가 문제인데, 이 것은 SphereVisualAsset의 선언 2줄 아래를 보면 이 변수가 SetStaticMesh() 함수에 대입되는 것을 알 수 있다. 이 함수가 받는 매개변수의 타입은 UStaticMesh로서 SphereVisualAsset.Object는 UStaticMesh 타입임을 유추할 수 있다.
Spring Arm Component는 폰보다 느린 가속/감속을 따라다니는 카메라에 적용시킬 수 있기 때문에, 카메라의 부드러운 부착점이 된다. 또한 카메라가 입체 오브젝트를 뚫고 지나가지 못하게 하는 기능을 내장하고 있어서, 삼인칭 게임에서 구석에서 벽을 등지는 상황에 유용하게 사용된다.
언리얼 에디터로 돌아왔다면, 프로젝트의 입력 세팅을 할 차례다. 이 세팅은 편집 드롭다운 메뉴의 프로젝트 세팅에서 찾을 수 있다.
프로젝트 세팅 창을 열었다면, 좌측의 엔진 섹션에서 입력을 찾아서 클릭한 뒤 아래와 같이 입력 매핑을 세팅하자.
이번에는 Pawn에서 모든 이동 처리를 하는 대신에, Movement Component를 만들어서 관리를 시키도록 해보자. 이 튜토리얼에서 Pawn Movement Component 클래스를 확장해서 사용한다.[각주:1] 파일 드롭다운 메뉴의 [새로운 C++ 클래스] 명령을 선택한다.
Pawn 클래스와 달리 Pawn Movement Component 클래스는 기본적으로 보이지 않기 때문에 모든 클래스 보기 옵션을 선택해야 한다.
검색창에 movement를 검색하면 찾고자 하는 클래스의 범위를 빠르게 좁힐 수 있다.
우리가 만든 Pawn 클래스의 이름이 "CollidingPawn"이기 때문에 이 Movement Component의 이름은 "CollidingPawnMovementComponent"로 정하자.
입력 환경설정에 대한 정의와 CollidingPawnMovementComponent의 생성으로 모두 끝마쳤으므로, 비주얼 스튜디오로 돌아가서 다시 코드 작업을 해야한다.
비주얼 스튜디오로 돌아왔으면 이제 커스텀 폰 무브먼트 컴포넌트의 작동방식을 코딩하면 된다. Actor의 Tick() 함수 역할을 하는 TickComponent() 함수가 각 프레임 별로 어떻게 동작할지를 정의해야 한다. 우선은 부모 클래스의 TickComponent() 함수를 덮어쓰는 것으로 시작한다.
이 컴포넌트는 다른 컴포넌트들과 달리 컴포넌트 계층구조에 붙일 필요가 없다. 다른 컴포넌트들의 경우에는 모두 씬 컴포넌트로 물리적인 위치가 필요한 것들이었지만, 이 컴포넌트는 물리적 오브젝트를 나타내는 것이 아니기 때문에, 물리적인 위치에 존재한다든가 다른 컴포넌트에 덧붙인다던가 하는 개념을 가지지 않는다.
Pawn 클래스에는 GetMovementComponent() 라는 함수가 있는데 이것은 엔진의 다른 클래스들이 현재 Pawn이 사용중인 Pawn Movement Component에 접근할 수 있도록 하는데 사용된다. 이 함수가 커스터마이징한 CollidingPawnMovementComponent를 반환하도록 하려면 이 함수를 덮어씌워야 한다. CollidingPawn.h에 다음 코드를 추가한다.
컴포넌트가 이동하다가 충돌이 발생했을 때, 제자리에 멈추는 대신 충돌체의 표면을 타고 미끄러지듯이 이동하도록 도와주는 함수
AddInputVector(Vector);
매개변수로 받은 벡터를 누적 입력에 더하는 함수
7. FVector
FVector Vector;
언리얼 엔진에서 3D 상의 위치나, 속도를 나타내는데 쓰이는 구조체
Vector.GetClampedToMaxSize(Value);
길이가 Value인 이 벡터의 복사본을 만들어서 반환하는 함수
Vector.IsNearlyZero();
지정된 허용오차 내에서 벡터의 길이가 0에 근접하는지 확인하는 함수
8. FHitResult
FHitResult Hit;
충돌에 대한 정보를 담고 있는 구조체
Hit.Time;
Hit가 발생했을 때, TraceStart와 TraceEnd 사이의 충돌이 발생한 시간을 의미한다. (0.0~1.0)
Hit.Normal
충돌이 발생한 오브젝트의 월드 공간 상의 법선 방향
Hit.IsValidBlockingHit();
막히는 충돌이 발생했을 때 true를 반환하는 함수
9. AActor
GetActorRotation();
액터의 현재 회전을 반환하는 함수
SetActorRotation(FRotator());
액터의 회전을 설정하는 함수
Pawn Movement Component 에는 흔한 물리 함수성에 도움이 되는 강력한 내장 기능이 몇 가지 들어있어, 여러가지 폰 유형에 무브먼트 코드를 공유하기가 좋다. 컴포넌트 를 사용하여 함수성을 분리시켜 놓는 것은 매우 좋은 습관인데, 프로젝트의 덩치가 커지면서 폰 도 복잡해 지기 때문이다. [본문으로]
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 의 클래스 정의 하단부에 추가하자.
그리고 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;
게임에서 특정한 키를 눌렀을 때, 특정 동작을 하도록 만드는 것을 언리얼에서는 입력 매핑이라고 한다. 이러한 입력 매핑에는 두 가지 종류가 있다.
액션 매핑(Action Mapping) - 마우스나 조이스틱, 패드, 키보드 버튼처럼 누르거나, 떼거나, 더블 클릭하거나, 특정 시간동안 누르고 있을 때 보고한다. 점프, 공격, 상호작용 등이 액션 매핑의 예시이며, X를 눌러서 조이를 표하는 것도 액션 매핑에 속한다.
축 매핑(Axis Mapping) - 연속적인 것으로 마우스의 위치나 조이스틱 막대의 기울기 같은 것으로 "일정량"의 입력으로 생각하면 된다. 움직이지 않더라도 매 프레임 값을 보고한다. 걷기, 달리기, 둘러보기, 탈 것의 방향조절 같은 것들이 주로 축 매핑으로 처리된다.
코드에서도 직접 입력 매핑을 할 수 있지만, 일반적으로는 에디터에서 정의하는 경우가 많으니, 이 튜토리얼에서는 그 방식을 따른다.
1. 언리얼 엔진 에디터에서 편집 드롭다운 메뉴에서 프로젝트 세팅 옵션을 선택한다.
2. 왼쪽의 엔진 섹션의 입력 항목을 선택하고 바인딩(Binding) 카테고리에 다음과 같이 하나의 액션 매핑과 두 개의 축 매핑을 추가한다.
3. 입력 환경 설정이 모두 끝났다면, 레벨에 MyPawn을 배치한다. 콘텐츠 브라우저에 있는 MyPawn 클래스를 레벨 에디터에 끌어다 놓으면 된다.
4. 레벨에 MyPawn을 배치한 뒤에는, 우리가 배치한 Pawn이 움직이는 것을 볼 수 있게 하기 위해서 OurVisibleComponent의 스태틱 메시(Static Mesh) 카테고리에 "Shape_Cylinder"를 넣어야 한다고 언리얼 튜토리얼 문서에 나와있다.
하지만 우리가 배치한 MyPawn의 OurVisibleComponent에서는 스태틱 메시 카테고리가 보이지 않는 것을 알 수 있다.
이 문제의 원인을 추측해보자면 언리얼 튜토리얼의 예시 코드에는 CreateDefaultSubobject() 함수로 컴포넌트를 생성할 때, 명시적인 컴포넌트 타입이 없었기 때문에 헤더에 추가한 OurVisibleComponent의 타입에 맞춰서 USceneComponent로 생성했기 때문에 발생한 문제로 보인다.
그렇다면 스태틱 메시 카테고리가 나오도록 하려면 어떻게 해야할까? 바로 CreateDefaultSubobject() 함수로 UStaticMeshComponent를 생성해서 OurVisibleComponent에 대입시켜 주면 될 것 같다. 언리얼 엔진 문서에 따르면 UStaticMeshComponent는 USceneComponent를 상속받고 있기 때문에 충분히 가능한 코드이다. 여기까지 유추했다면 코드를 다음과 같이 수정해보자.
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.
축 입력 매핑에 대한 동작을 구현할 때, 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);