예를 들어, 데미지 타입을 지정할 수 있는 발사체 클래스를 만든다고 가정했을 때, 다음 코드처럼 UClass 타입의 UPROPERTY를 만들어서 에디터에 노출시킨 뒤, 에디터 작업자에게 이 프로퍼티에 UDamageType의 파생 클래스만 할당해 달라고 한다면 어떻게 될까?
블루프린트 에디터의 디테일 패널에서 Damage Type 프로퍼티에 클래스를 할당하기 위해서 드롭다운 메뉴를 확장해보면 클래스 타입에 상관없이 모든 클래스가 표시되고 있음을 볼 수 있다. 이런 상황에서 UDamageType의 파생 클래스만 할당해달라고 한다면, 낮은 확률으로라도 언젠가는 잘못된 클래스를 할당하는 일이 분명 생길 수 밖에 없다.
이러한 문제를 예방하기 위해서 존재하는 것이 바로 TSubclssOf 클래스이다. 다음 코드와 같이 TSubclassOf<UDamageType>으로 UPROPERTY를 만든다.
그렇게 하면, 블루프린트 창의 디테일 패널에서 Damage Type의 드롭다운 메뉴에서는 UDamageType의 파생 클래스만 표시된다. 이렇게 되면 개발자가 가끔 잘못된 데미지 타입을 골라서 넣는 실수는 할 수 있겠지만, 애초에 잘못된 클래스를 선택하는 문제는 발생하지 않을 것이다.
또한 TSubclassOf 클래스는 이런 UPROPERTY에 대한 안정성 이외에 C++ 수준의 타입 안정성 역시 제공하기 때문에 서로 호환되지 않는 TSubclassOf 타입을 서로 할당하려고 하면 컴파일 오류가 발생하고, UClass 타입을 할당하려고 하면 할당을 수행할 수 있는지 런타임중에 검사한다. 런타임 검사에 실패하면 결과값은 nullptr이 된다.
제대로 따라가기 (8) C++ 프로그래밍 튜토리얼 :: 일인칭 슈팅 C++ 튜토리얼 (3)
작성버전 :: 4.21.0
언리얼 엔진 튜토리얼인 일인칭 슈팅 C++ 튜토리얼에서는 C++ 코드 작업을 통해서 기본적인 일인칭 슈팅(FPS) 게임을 만드는 법을 배울 수 있다.
이번 튜토리얼은 각 하위 섹션들의 길이가 길어서 분할되어 작성된다.
튜토리얼대로 하면 문제가 발생해서 제대로 따라갈 수 없는 부분으로 동작이 가능하게 수정해야하는 부분은 빨간 블럭으로 표시되어 있다.
이번 튜토리얼에서 새로 배우게 되는 내용은 글 제일 끝에 "이번 섹션에서 배운 것"에 정리된다.
수정
지난 섹션에서 VisibleDefaultOnly는 버전이 바뀌어서 사라진 지정자라고 했던 부분은 잘못된 부분입니다.
VisibleDefaultsOnly는 정상적으로 존재하는 UPROPERTY 지정자입니다. 제가 실수로 VisibleDefaultOnly로 오타를 내서 컴파일러가 지정자가 없다고 에러를 띄웠었습니다. 잘못된 정보로 혼동을 드린 점에 대해서 사과드립니다. 다음부터는 제대로된 확인을 거친 후, 글을 올리도록 하겠습니다.
이전 섹션에서 캐릭터 구성을 마쳤으니, 이제 발사체 무기를 구현하여 발사하면 단순한 수류탄 같은 발사체가 화면 중앙에서 발사되어 월드에 충돌할 때까지 날아가도록 만들어보자. 이번 단계에서는 발사체(Projectile)에 쓸 입력을 추가하고 새 코드 클래스를 만들 것이다.
발사 액션 매핑 추가
편집 메뉴에서 프로젝트 세팅 창을 연다. 그리고 엔진 섹션에서 입력을 선택한 뒤, 액션 매핑에 아래와 같이 "Fire" 라는 입력 세팅을 추가 한다.
발사체(Projectile) 클래스 추가
파일 메뉴에서 새로운 C++ 클래스... 를 선택하고 Actor 클래스를 부모 클래스로 선택하고 다음을 클릭한다.
새 클래스 이름을 "FPSProjectile"로 하고 클래스 생성을 클릭한다.
USphereComponent 추가
FPSProjectile.h로 가서 USphereComponent의 선언을 다음처럼 추가해준다.
ProjectileMovementComponent에서 함수를 호출하려고 할 때, 불완전한 형식은 사용할 수 없다는 에러가 발생하면 "Engine/Classes/GameFramework/ProjectileMovementComponent.h"를 cpp의 전처리기에 추가해주자.
일반적으로 축 매핑(Axis Mappings)을 통해서 키보드, 마우스, 컨트롤러 입력을 "친근한 이름"으로 매핑시킨뒤 나중에 이동 등의 게임 동작에 바인딩할 수 있다. 축 매핑은 지속적으로 폴링되어, 부드러운 전환 및 게임 동작이 가능하다. 컨트롤러의 조이스틱 같은 하드웨어 축은 "눌렸다", "안눌렸다" 같은 식의 구분되는 입력이 아닌 연속적인 입력 수치를 제공한다. 컨트롤러 조이스틱 입력 방법이 스케일식 동작 입력을 제공해 주기는 하지만, 축 매핑으로 WASD 처럼 지속적 폴링되는 게임 동작을 위한 일반 이동 키 매핑도 가능하다.
프로젝트 세팅 창을 열고 엔진 섹션의 입력을 선택한다. 그리고 입력 매핑 세팅을 다음처럼 구성한다.
함수 위에 붙여준 UFUNCTION() 매크로는 엔진에게 이 함수들을 인식시켜 직렬화(Serialization), 최적화, 기타 엔진 함수성에 포함될 수 있도록 해준다.
동작 함수 구현
전형적인 FPS 조작법에서, 캐릭터의 동작 축은 카메라에 상대적이다. "전방"이란 "카메라가 향하는 방향"을, "우측"이란 "카메라가 향하는 방향의 우측"을 뜻한다. 캐릭터의 제어 방향을 구하는 데는 PlayerController를 사용할 것이다. 또한 MoveForward() 함수는 제어 회전의 피치 컴포넌트를 무시하고 입력을 XY 면으로 제한시켜 위아래를 쳐다보더라도 캐릭터는 땅과 평행으로 움직일 수 있도록 한다.
FPSCharacter.cpp에서 AFPSCharacter::SetupPlayerInputComponent() 함수의 하단에 다음 코드를
회전과 쳐다보기에 대한 마우스 입력 처리를 하는 코드를 추가할 차례이다. Character 베이스 클래스는 카메라 회전 컨트롤에 대해서 다음과 같은 필수 함수 둘을 제공한다. 그렇기 때문에 FPSCharacter 클래스에 별도의 함수를 정의하고 구현할 필요없이 바로 AFPSCharacter::SetupPlayerInputComponent() 함수에 바인딩하는 코드를 추가하면 된다.
액션 매핑은 별도의 이벤트에 대한 입력을 다루며, "친근한 이름"에 매핑시켜 나중에 이벤트 주도형 동작에 바인딩시킬 수 있도록 해준다. 최종 효과는 키나 마우스 버튼, 혹은 키패드 버튼에 대해서 누르기/떼기를 통해서 게임 동작을 실행시키도록 하는 거이다.
이번 단계에서는, 스페이스 바에 대한 액션 매핑을 구성하여 캐릭터에 점프 능력을 추가하는 것이다.
점프 액션 매핑
프로젝트 세팅 창을 열고 엔진 섹션에서 입력을 선택한다. 그리고 액션 매핑을 다음과 같이 추가한다.
입력 처리 구현
Character 베이스 클래스의 인터페이스(*.h) 파일 안을 보면, 캐릭터 점프에 대한 지원이 내장되어 있는 것을 볼 수 있다. 캐릭터 점프는 bPressedJump 변수에 묶여 있어서, 점프 키를 누르면, 이 변수를 true로, 떼면 false로 설정해주기만 하면 된다.
이 코드는 카메라의 위치를 캐릭터의 눈 살짝 위쪽으로 잡으면서 폰이 카메라 로테이션을 제어할 수 있도록 해준다.
이대로 빌드하면 Camera Component에서 에러가 발생해서 컴파일에 실패할 수 있다. 코드를 작성할 때는 에러가 뜨지 않아서 방심했지만 이 에러는 충분이 아는 에러일 것이다. 지금 비주얼 스튜디오가 한글 버전이라 로그가 깨져있지만 튜토리얼을 진행하면서 생긴 경험으로 미루어 짐작하건데, 헤더의 30라인에서 발생하는 에러는 UCameraComponent가 정의되지 않았다는 에러일 것이다. UCameraComponent 선언 앞에 class를 붙여주자.
UPROPERTY(VisibleAnywhere)
class UCameraComponent* FPSCameraComponent;
FPS에서 흔히 사용되는 방법은, 전신 바디 메시 하나, 무기와 손 메시하나, 이렇게 별개의 캐릭터 메시 두 개를 사용하는 것이다. 전신 메시는 삼인칭 시첨에서 캐릭터를 보거나 다른 캐릭터를 볼대 사용되고, 플레이어가 일인칭 시점에서 게임을 볼 때는 이 전신 메시를 숨긴다. 그리고 "무기와 손" 메시는 전형적으로 카메라에 붙여 플레이어가 일인칭 시점에서 맵을 볼 때 플레이어에게만 보이는 것이다. 이 파트에서는 캐릭터에 일인칭 메시를 추가해보자.
Game Mode는 게임의 규칙과 승리 조건등을 정의하는 클래스로, 프로젝트를 구성할 때, 언리얼 엔진이 기본 Game Mode 클래스를 생성해주었다. 우리는 이 Game Mode에 기본 게임플레이 프레임워크 유형에 사용될 기본 클래스를, Pawn, PlayerController, HUD 등을 포함해서 설정할 계획이다. 이 파트에서는 에디터를 통해서 비주얼 스튜디오를 열고 거기서 프로젝트의 Game Mode 클래스를 확인해 볼 것이다.
언리얼 에디터의 파일 메뉴에서 Visual Studio 열기를 선택하여 비주얼 스튜디오를 연다.
비주얼 스튜디오가 열리면, 솔루션 탐색기를 통해서 프로젝트에 포함된 소스파일(.cpp)과 헤더파일(.h)가 보인다.
언리얼 엔진이 버전업 되면서 프로젝트에 자동 생성되는 Game Mode 클래스명 끝에 Base가 붙도록 변경되었다. 그래서 열어봐야할 소스파일의 이름은 "FPSProjectGameModeBase.cpp가 된다. 이것은 헤더파일에도 포함되는 것이다.
그리고 처음에 불필요한 빌드 및 컴파일 시간은 줄이기 위해서 기본적으로 필요한 헤더를 제외한 헤더의 포함을 최소화 하도록 변경되었기 때문에 FPSProjectGameModeBase.cpp 파일의 전처리기에 "FPSProject.h"가 포함되어 있지 않을 것이다. 그렇기 때문에 소스파일의 내용은 다음과 같을 것이다.
#include "FPSProjectGameModeBase.h"
다음 부분 부터는 FPSProjectGameModeBase를 기준으로 설명할 것이다.
FPSProjectGameModeBase.h 안에는 다음과 같은 코드가 있다.
#pragma once
#include "CoreMinimal.h"
#include "GameFramework/GameModeBase.h"
#include "FPSProjectGameModeBase.generated.h"
/**
*
*/
UCLASS()
class FPSPROJECT_API AFPSProjectGameModeBase : public AGameModeBase
{
GENERATED_BODY()
};
비어있는 PrivateDependencyModuleNams를 주석 처리하고 "Slate"와 "SlateCore"가 있는 부분을 주석 해제 한다.
//PrivateDependencyModuleNames.AddRange(new string[] { });
// Uncomment if you are using Slate UI
PrivateDependencyModuleNames.AddRange(new string[] { "Slate", "SlateCore" });
UMG 구성이 완료되었다면, 프로젝트의 커스텀 게임 모드에 코드를 추가하여 게임 메뉴를 만들고 표시할 수 있다.
게임이 시작되면 유저 위젯(User Widget)을 새로 만들어 표시하거나, 나중에 제거할 수 있도록 하기 위해서 Game Mode(게임 모드) 클래스에 함수와 프로퍼티를 추가하자. 각 프로젝트에는 커스텀 게임 모드 클래스가 딸려오므로, HowTo_UMGGameMode.h에 다음 코드를 추가하면 된다.
버전이 바뀌면서 GameMode 클래스 파일의 이름이 "ProjectNameGameMode.h", "ProjectNameGameMode.cpp"에서 "ProjectNameGameModeBase.h", "ProjectNameGameModeBase.cpp"로 바뀌었다. 비주얼 스튜디오의 솔루션 탐색기에서 HowTo_UMGGameModeBase.h를 열어서 작업하자.
TSubclassOf 클래스를 사용할 때, [클래스 템플릿 "TSubclassOf"에 대한 인수 목록이 없다]는 에러가 발생한다. 전체 코드를 보고 유추해보건데, BeginPlay() 함수에서 StartingWidgetClass의 내용물을 CurrentWidget 변수에 넣어주거나 ChangeMenuWidget() 함수가 동작할 때, 매개변수로 받은 NewWidgetClass를 CurrentWidget에 대입해주는 방식으로 동작할 것임을 알 수 있다. 그렇기 때문에 여기에서는 TSubclassOf의 템플릿 인수로 UUserWidget 타입을 넣어주는 것이 올바른 해결책일 것이다.
부모 클래스의 함수를 덮어쓸 때는, 여기 BeginPlay에서 하듯이 해당 함수의 부모 클래스 버전을 호줄하는 것이 중요한 경우가 많다. 우리가 구현하는 함수의 버전은 기존의 절차의 끝 부분에 한 단계를 추가하기 위한 것이므로, 함수 첫 줄에 Super::BeginPlay()를 호출한다.
그리고 여기서는 UUserWidget을 사용하기 위해서, HowTo_UMGGameModeBase.cpp 상단에 "Blueprint/UserWidget.h"를 포함시켜 주어야 한다.
#include "Blueprint/UserWidget.h"
계속 해서, 메뉴 간의 전환 방식을 구현해야 한다. 뷰포트에 활성화된 유저 위젯이 있다면 제거하고 난 다음에 유저 위젯을 새로 만들어 뷰포트에 추가해주도록 구현한다.
이 코드는 제공된 위젯 인스턴스를 만들어 화면에 넣는다. 언리얼 엔진은 한 번에 다수의 위젯을 표시하고 상호작용처리가 가능하며, 한 번에 하나만 활성화 되도록 제거를 할 수도 있다. 하지만 위젯을 직접 소멸시킬 필요는 없는데, 뷰포트에서의 제거 밑 레퍼런싱하는 모든 변수 소거(또는 변경) 작업은 언리얼 엔진의 가비지 컬렉션 시스템에서 해주기 때문이다.
마지막으로 Player Controller 클래스에 입력 모드를 설정해야 한다. 그러기 위해서 Player Controller를 기반으로 새로운 C++ 클래스를 추가하자. 이 클래스 안해서 게임이 시작될 때 함수 하나를 추가로 호출해주기만 하면 UI 요소와 상호작용이 가능하도록 할 수 있다.
HowTo_UMGPlayerController.h에서 클래스에 다음 오버라이드를 추가하고 .cpp에 구현한다.
언리얼 에디터에서 컴파일 버튼을 누르면 수정된 코드가 빌드된다. 이를 통해서 유저 위젯을 메뉴로 사용할수 있게 된다.
이제 게임 모드가 메뉴로 사용할 유저 위젯을 생성해보자. 콘텐츠 브라우저의 "신규 추가" 버튼을 누르고 유저 인터페이스 카테고리에서 위젯 블루프린트(Widget Blueprint) 클래스를 선택해서, "MainMenu"와 "NewGameMenu"라는 이름으로 두 개의 유저 위젯을 만든다.
방금 만든 "MainMenu" 위젯을 더블클릭하면 블루프린트 디자이너 창이 열리며, 여기서 메뉴 레이아웃을 만들 수 있다.
팔레트 패널의 일반 섹션에서 버튼(Button)과 텍스트(Text)를 끌어 그래프에 배치한다. 이 버튼은 새 게임 메뉴를 여는데 사용될 것이다.
버튼의 위치와 크기를 다음과 같이 수정하고, 함수성 연결을 해줄 때 알아보기 쉽게 하기 위해서 이름을 "NewGameButton"으로 변경한다.
그리고 이 버튼이 무슨 버튼인지 보여주기 위해서 텍스트 블록(Text Block)을 버튼 위로 끌어다 놓고 디테일을 다음과 같이 수정한다.
- Text를 "New Game"으로 변경
- Visibility를 Hit Test Visibility로 변경한다. 그러면 버튼을 누르려는 클릭을 텍스트 블록이 막지 않는다.
- 이름을 "NewGameText"로 변경한다. 필수는 아니지만 나중에 계층구조에서 어떤 UI인지 찾기 쉬워지기 때문에 들여두면 좋은 습관이 된다.
두 번째 버튼과 텍스트 블록을 만들어서 "Quit"(종료) 기능을 만든다. 버튼 이름은 "QuitButton", 버튼 위치는 (600, 100), 텍스트 블록 이름은 "QuitText"로 설정한다.
그 다음은, 버튼을 클릭했을 때, 코드가 실행되도록 버튼에 이벤트를 추가하는 작업을 해야한다. 디테일 패널에서 적합한 이벤트의 이름 옆에 "+"버튼을 찾아서 누르면 되는데 이 경우에는 "OnClicked" 이벤트를 추가하면 된다.
NewGameButton의 OnClicked 이벤트를 다음과 같이 구성한다.
QuitButton의 OnClicked 이벤트를 다음과 같이 구성한다.
메인 메뉴를 만들었으니, 레벨이 시작되면 메인 메뉴를 로드하는 게임 모드 애셋을 구성하면 된다.
콘텐츠 브라우저에서 프로젝트의 게임 모드에 맞는 블루프린트 클래스를 두 개 추가할 것이다. 그러면 그 두 클래스에 노출된 변수를 원하는 값으로 설정하는 것이 가능하다.
콘텐츠 브라우저에서 신규 추가버튼에서 블루프린트 클래스를 클릭한다.
부모 클래스로 HowTo_UMGGameModeBase를 선택해서 "MenuGameMode" 블루프린트 클래스를 만든다.
그리고 게임 내에서 마우스 커서를 보이게 하기 위해서, 플레이어 컨트롤러의 블루프린트 클래스도 만들어 주어야 한다. 콘텐츠 브라우저에서 블루프린트 클래스를 클릭하고 Player Controller 클래스 상속받아서 "MenuPlayerController"라는 이름으로 클래스를 생성하자.
"MenuPlayerController" 클래스가 생성되었으면, 콘텐츠 브라우저에서 블루프린트 파일을 더블클릭해서 블루프린트 에디터를 연다. 그리고 디테일 창에서 "Show Mouse Cursor" 박스를 체크한다.
다음은 "MenuGameMode"를 편집한다.
Starting Widget Class를 "MainMenu" 애셋으로 설정해서 게임 시작시 메뉴가 뜨도록 만든다.
Default Pawn Class를 Default Pawn이 아닌 Pawn으로 설정해서 플레이어가 메뉴에 있을 때, 이리저리 날아다니지 않도록 만든다.
Player Controller Class를 방금 만든 "MenuPlayerController" 애셋으로 설정해서 메인 메뉴에서 마우스 커서가 표시되도록 만든다.
우리가 만든 게임 모드 블루프린트를 사용하려면, 레벨 에디터 창으로 돌아와 세팅 버튼을 통해 현재 레벨에 대한 월드 세팅을 변경해야 한다.
프로젝트 세팅 메뉴의 맵 & 모드에서도 기본 게임 모드 설정이 가능하다. 이 방법을 사용하면 따로 덮어쓰지 않는 한 모든 레벨에서 기본 게임 모드로 설정된다. 어느 방법을 사용할지는 프로젝트 구성에 따라 달라질 수 있다.
월드 세팅 패널에서 Game Mode Override 항목을 "MenuGameMode" 애셋으로 설정한다.
이제 레벨에 메인 메뉴를 로드하고, 마우스 커서를 표시하는 플레이어 컨트롤러를 사용하도록 환경설정된 커스텀 게임 모드 애셋이 적용되었다. 이제 게임을 실행하면 Quit 버튼은 정상적으로 작동하지만, 아직 New Game 버튼은 빈 메뉴 화면으로 이동한다. 다음 단계에서는 New Game Menu를 구성해주자.
콘텐츠 브라우저에서 아까 만든 "NewGameMenu" 애셋을 연다. 이 메뉴는 이름을 입력할 수 있는 텍스트 박스와, 이름을 입력하기 전에는 누를 수 없는 '게임 플레이' 버튼, 메인 메뉴로 돌아가는 버튼으로 구성된다.
이름 입력 박스를 만들기 위해, 레이아웃에 Text Box(텍스트 박스)를 배치한다.
텍스트 박스의 설정은 다음과 같다.
이전 메뉴에서 버튼을 만들었던 것과 같은 방식으로 텍스트 블록 라벨이 있는 게임 플레이 버튼을 만든다.
버튼 : 이름은 PlayGameButton, 위치는 200, 300, 크기는 200, 100으로 변경한다.
텍스트 블록 : 이름은 PlayGameText, Visibility는 Hit Test Visible로, 내용은 Play Game으로 변경한 다음 PlayGameButton위에 배치한다.
게임 플레이 버튼의 경우, 만약 플레이어 이름 입력란이 비어있다면 작동하지 않도록 특수한 기능을 추가한다. UMG의 바인드 기능을 사용하여 (Behavior섹션 아래) "Is Enabled" 칸에 새로운 함수를 만들면 된다.
텍스트 박스가 공백이 아니어서 버튼이 활성화 될 수 있는 상태인지 확인하려면, 텍스트 박스에서의 텍스트를 스트링으로 변환한 다음 길이가 0보다 큰지 검사하면 된다.
이제 메인 메뉴로 돌아갈 수 있도록 버튼을 하나 추가해보자. 메인 메뉴에서 게임 플레이 버튼과 비슷하지만, 위치 기준이 좌상단이 아닌 우하단 구석이 될 것이다. 그러기 위해서는 디테일 패널에서 "앵커" 드롭다운을 클릭한 다음, 팝업 메뉴에서 우하단 부분을 나타내는 모양을 선택한다.
버튼 이름을 MainMenuButton으로 설정한다.
위치를 -400, -200으로 설정한다.
크기를 200x100으로 설정한다.
이제 NewGameMenu 위젯의 버튼들에도 OnClicked 이벤트들을 추가하자
메인 메뉴 버튼의 경우, 다시 메인 메뉴 위젯을 열어주지만, 게임 플레이 버튼은 누르면 메뉴를 비활성시킨 후, 게임에서 더 이상 아무것도 할 수 없게 만든다. 보통 이 시점에서 첫 레벨을 로드하고, 오프닝 동영상을 재상하거나 폰을 스폰시켜 빙의하는 등의 처리를 하게 된다.
모든 작업을 마치고 플레이 해보면 다음 스크린샷과 같은 장면을 얻을 수 있다.
이번 섹션에서 배운 것
1. 언리얼 모듈 종속성
언리얼 엔진의 기능은 다수의 모듈로 나누어져 있고, 그 중에 필요한 모듈을 묶어서 사용하는 방식이다. 이번 섹션 처음 부분에 build.cs파일에서 모듈 종속성을 구성할 때도 보았겠지만, 기본적으로 언리얼은 Core, CoreUObject, Engine, InputCore 모듈을 사용하고 있었고, UI와 관련된 기능을 사용하기 위해서 UMG 모듈과 Slate, SlateCore 모듈을 구성에 추가해주었다.
추후의 일이지만, 언리얼 엔진을 커스터마이징하고자 할 때, 새롭게 추가하는 기능을 이러한 모듈로 만들어 덧붙이게 될 것이다.
2. TSubClassOf<T>
UClass 타입 안정성을 보장하는 템플릿 클래스. TSubClassOf에 전달된 인수가 템플릿 인자로 받은 타입과 일치하거나 템플릿 인자로 받은 타입을 상속받은 타입인지를 런타임 중에 확인하도록 도와주는 클래스이다.
3. UUserWidget
UUserWidget* UserWidget;
Widget Blueprint를 통해서 확장할 수 있는 사용자 위젯.
UserWidget->AddToViewport();
유저 위젯을 뷰 포트에 추가하는 함수.
UserWidget->RemoveToViewport();
유저 위젯을 뷰 포트에서 제거하는 함수.
4. AActor::GetWorld()
GetWorld();
UWorld 객체를 가져오는 함수. UWorld는 액터나 컴포넌트들을 포함하는 맵이나 샌드박스의 최상위 객체이다.
5. CreateWidget()
CreateWidget(GetWorld(), newWidget);
위젯을 생성하는 함수
6. APlayerController::SetInputMode()
SetInputMode(FInputModeGameAndUI());
플레이어 컨트롤러의 입력 모드를 설정하는 함수. Game 입력만 받을지, UI 입력만 받을지, 아니면 둘 다 받을지를 정할 수 있다.
위의 코드에 따르면, 비어있는 Scene Component가 루트로 생성되며, 거기에 Spring Arm Component를 만들어 붙인다. 그리고 Spring Arm의 기본 피치(Pitch)를 -60도로(60도 아래쪽으로) 설정하고, 위치는 루트의 50유닛 위로 정한다. Spring Arm Component의 길이와 유연성을 위한 변수도 설정해주었다.
Spring Arm의 설정이 끝났다면 Spring Arm Component 끝의 소켓에 Camera Componenet를 만들어서 연결해주면 된다.
이제 게임에서 사용할 수 있는 입력 매핑이 생겼으니, 입력 매핑으로부터 받은 데이터를 저장할 멤버 변수들을 구성할 차례이다.
업데이트 중에 이동과 마우스 방향 축을 알아야 하는데 이 값으로는 FVector2D 타입이 적합하다. 그리고 줌인 중인지 줌아웃 중인지도 알아야하며, 얼마나 줌된 상태인지를 알아야한다. 그것을 위해서 PawnWithCamera.h의 클래스 정의에 다음과 같이 멤버 변수 선언을 추가해주자.
이 코드에서는 줌인/줌아웃할 때, 걸이는 시간, FOV 값, 스프링 암의 거리 등을 하드코딩해서 사용하고 있지만, 이 값들을 멤버 변수로 만들어서 UPROPERTY(EditAnywhere)로 설정해서 에디터에 노출시키면 프로그래머가 아닌 개발자들도 에디터에서 값을 변경할 수 있고, 프로그래머도 값을 바꿀때마다 컴파일을 새로 할 필요가 없게 만들 수 있다.
이 코드 블록은 Pawn의 요(Yaw)를 마우스 X축으로 직접 회전시키되, 카메라 시스템은 마우스 Y축의 피치(Pitch) 변화에만 반응한다. 액터나 그 서브클래스를 회전시키면, 실제로 루트 레벨의 컴포넌트가 회전되어 거기에 붙어있는 모든 오브젝트에 간접적으로 영향을 미친다.
프로젝트를 새로 생성하고 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);