제대로 따라가기 (1) C++ 프로그래밍 튜토리얼 :: 변수, 타이머, 이벤트 (타이머를 사용하는 액터 만들기)
작성버전 :: 4.20.3
언리얼 엔진은 다양한 기능을 제공하며, 그 기능에 대한 튜토리얼들이 문서에 존재한다. 언리얼 엔진을 공부하기 위해선 필수적으로 이러한 튜토리얼들을 첫걸음으로 따라가게 되는데, 언리얼 튜토리얼 문서는 가끔 따라가다보면 제대로 진행이 안되고 막히는 부분이 존재한다. 튜토리얼은 배우는 단계인데 아직 엔진에 전혀 숙련되지 못한 사람이 이런 문제에 부딪히면 생각보다 많은 시간은 잡아먹게 된다. 제대로 따라가기는 이런 튜토리얼 도중에 막히는 부분을 빠르게 해소하고 따라가기 위해 제작되었다.
튜토리얼대로 하면 문제가 발생해서 제대로 따라갈 수 없는 부분으로 동작이 가능하게 수정해야하는 부분은 빨간 블럭으로 표시되어 있다.
이번 튜토리얼에서 새로 배우게 되는 내용은 글 제일 끝에 "이번 섹션에서 배운 것"에 정리된다.
변수, 타이머, 이벤트 (1. 타이머를 사용하는 액터 만들기)
변수, 타이머, 이벤트 튜토리얼은 변수와 함수를 에디터에 노출시키는 법, 타이머를 사용하여 코드 실행을 지연 또는 반복시키는 법, 이벤트를 사용하여 액터 사이의 통신을 하는 법을 알려주는 튜토리얼이다.
Countdown 클래스 추가
우선 C++ 프로젝트에서 Actor 클래스를 상속받는 Countdown 클래스를 생성하도록 한다.
카운트다운 진행 상황을 보여주기 위한 기능 추가
클래스가 생성되었다면 비주얼 스튜디오를 열어서 생성된 클래스에 카운트다운할 시간 변수와 카운트다운 진행 상황을 보여줄 텍스트 렌더 컴포넌트와 함수를 추가해야 한다. 그 예시 코드는 다음과 같다.
#pragma once
#include "CoreMinimal.h"
#include "GameFramework/Actor.h"
#include "Countdown.generated.h"
UCLASS()
class CODEPRACTICE_API ACountdown : public AActor
{
GENERATED_BODY()
public:
// Sets default values for this actor's properties
ACountdown();
protected:
// Called when the game starts or when spawned
virtual void BeginPlay() override;
public:
// Called every frame
virtual void Tick(float DeltaTime) override;
int32 CountdownTime;
UTextRenderComponent* CountdownText;
void UpdateTimerDisplay();
};
추가된 것은 int32 CountdownTime, UTextRenderComponent* CountdownText, void UpdateTimerDisplay()이다.
바로 이 부분에서 막히는 사람들이 꽤 많을 거라고 생각한다.
바로 UTextRenderComponent가 정의되어 있지 않다고 신텍스 에러가 뜨기 때문이다. 이 문제를 해결하기 위해서는 UTextRenderComponent가 정의된 헤더를 포함시켜줘야 한다. UTextRenderComponent 클래스는 Engine/Classes/Components/TextRenderComponent.h 에 정의되어 있다.하지만 이 TextRenderComponent.h를 추가해야 된다는 걸 깨달았다고 모든 문제가 해결되지는 않았다. 바로 헤더 포함 순서 문제가 남아있기 때문이다. 습관적으로 새로 추가하는 헤더를 가장 뒤에 추가하는 프로그래머들이 많을텐데 언리얼 C++프로그래밍에서는 헤더를 포함할 때 순서를 지켜야 한다. 새로 추가되는 헤더는 무조건 generated.h보다 위쪽에서 추가되어야 한다.#pragma once #include "CoreMinimal.h" #include "GameFramework/Actor.h" #include "Engine/Classes/Components/TextRenderComponent.h" #include "Countdown.generated.h" UCLASS() class CODEPRACTICE_API ACountdown : public AActor { GENERATED_BODY() public: // Sets default values for this actor's properties ACountdown(); protected: // Called when the game starts or when spawned virtual void BeginPlay() override; public: // Called every frame virtual void Tick(float DeltaTime) override; int32 CountdownTime; UTextRenderComponent* CountdownText; void UpdateTimerDisplay(); };
위의 예시 코드처럼 generated.h 위의 적당한 위치에 TextRenderComponent.h를 포함시켜주면 신텍스 에러가 발생하지 않는다.
그 다음 작업은 ACountdown 클래스의 생성자에서 액터의 프로퍼티 값들을 초기화해주는 것이다. 언리얼 엔진 문서에서 제공하는 예시코드는 다음과 같다.
// Sets default values
ACountdown::ACountdown()
{
// Set this actor to call Tick() every frame. You can turn this off to improve performance if you don't need it.
PrimaryActorTick.bCanEverTick = false;
CountdownText = CreateDefaultSubobject(TEXT("CountdownNumber"));
CountdownText->SetHorizontalAlignment(EHTA_Center);
CountdownText->SetWorldSize(150.0f);
RootComponent = CountdownText;
CountdownTime = 3;
}
이 클래스에서 Tick 기능은 사용하지 않기 때문에 bCanEverTick은 false로 하고 CountdownText에 TextRenderComponent를 생성해서 루트 컴포넌트에 붙여주고 CountdownTime을 3초로 설정한다.
하지만 코드가 과거버전 기준으로 만들어지고 문서가 업데이트되지 않은 문제인지, CreateDefaultSubobject()함수를 호출하는 부분에서 신텍스 에러가 발생한다. 그래서 CreateDefaultSubobject() 함수를 살펴보면 템플릿 함수임을 알 수 있다.
CountdownText = CreateDefaultSubobject<UTextRenderComponent>(TEXT("CountdownNumber"));
CountdownText 변수가 받아야하는 UTextRenderComponent를 템플릿 파라미터에 넣어주면 문제없이 신텍스 에러가 사라진다.
그 다음은 아까 정의해둔 UpdateTimerDisplay() 함수를 구현하는 것이다. 이 함수는 남은 시간을 TextRenderComponent에 업데이트하고 시간이 다되면 0을 표시하도록 한다.
void ACountdown::UpdateTimerDisplay()
{
CountdownText->SetText(FString::FromInt(FMath::Max(CountdownTime, 0)));
}
타이머(Timer)
화면에 대한 준비를 끝냈다면 이번에는 시간을 체크할 타이머를 추가할 차례다. 타이머란 사용자가 정의한 시간마다 사용자가 지정한 동작이 실행되도록 하는 것이다. 이러한 동작은 물론 Tick() 함수에서 DeltaTime 값을 받아서 같은 동작을 수행하도록 할 수는 있지만, 사용자가 지정한 동작이 지속적으로 실행될 필요가 없이 특정한 순간에만 몇 번 실행되면 되거나 실행될 텀이 1초를 넘는 경우라면 Tick() 함수에서 시간을 재서 실행하는 것보다는 타이머를 이용하는 편이 좋다.
타이머에 대해 이해가 되었다면 이제 타이머에 필요한 멤버 변수와 함수들을 Countdown.h의 Countdown 클래스의 하단에 추가해보자.
void AdvanceTimer();
void CountdownHasFinished();
FTimerHandle CountdownTimerHandle;
AdvanceTimer() 함수는 Timer가 돌아가면서 호출될 함수이다.
CountdownHasFinished() 타이머가 사용자가 의도한 만큼 돌아간 뒤의 처리를 위한 함수이다.
차량에 달린 핸들이 차량의 이동 방향을 컨트롤하기 위한 것이듯, FTimerHandle 역시 타이머를 컨트롤하기 위한 구조체로서 CountdownTimerHandle 변수는 카운트다운이 끝났을 때, 타이머가 계속해서 돌아가지 않도록 종료하기 위해서 필요하다.
AdvanceTimer() 함수와 CountdownHasFinished() 함수를 모두 정의했다면 이번에는 각 함수를 구현해보자.
void ACountdown::AdvanceTimer()
{
--CountdownTime;
UpdateTimerDisplay();
if (CountdownTime < 1)
{
// 카운트다운이 완료되면 타이머를 중지
GetWorldTimerManager().ClearTimer(CountdownTimerHandle);
CountdownHasFinished();
}
}
AdvanceTimer() 함수의 예시 코드는 위와 같은데 이 함수를 구현하면서 문제가 다시 발생한다. 이번에는 GetWorldTimerManager() 함수에서 ClearTimer() 함수를 호출할 때 "불완전한 형식은 사용할 수 없습니다." (E0070 :: Incomplete type is not allowed.) 라는 에러가 발생한다.
이 문제는 아래의 예시 코드와 같이 Countdown.cpp의 상단에 TimerManager.h를 포함시켜주면 해결된다.
// Fill out your copyright notice in the Description page of Project Settings. #include "Countdown.h" #include "TimerManager.h"
CountdownHasFinished() 함수의 코드는 다음과 같다.
void ACountdown::CountdownHasFinished()
{
CountdownText->SetText(TEXT("Go!"));
}
다음 작업은 BeginPlay() 함수에서 텍스트 표시를 초기화하고 타이머를 동작시키는 것이다.
void ACountdown::BeginPlay()
{
Super::BeginPlay();
UpdateTimerDisplay();
GetWorldTimerManager().SetTimer(CountdownTimerHandle, this, &ACountdown::AdvanceTimer, 1.0f, true);
}
에디터 컴파일과 레벨 배치 그리고 테스트 실행
모든 코드 작업이 끝났다면 이제 언리얼 에디터로 돌아가서 컴파일 버튼을 눌러보자.
만약 컴파일 에러 없이 컴파일에 성공했다면 위의 이미지와 같이 컴파일 완료라고 에디터의 오른쪽 하단에 출력될 것이다.
컴파일이 완료된 다음에 우리가 작성한 Countdown 클래스를 레벨 에디터에 드래그 앤 드롭해서 배치할 수 있다.
배치를 완료했다면 플레이 버튼을 눌러서 실행해보자. 그러면 화면의 Text 글자가 3, 2, 1, Go!로 바뀌는 것을 확인할 수 있다.
이번 섹션에서 배운 것
1. CreateDefaultSubobject<T>() (언리얼 엔진 문서)
UObject 클래스를 상속받는 모든 클래스에서 사용가능한 함수이다. 하위 오브젝트나 컴포넌트를 생성할 때 사용되는 함수로 2번의 UTextRenderComponent를 생성하는 예시와 같이 사용된다. 이 함수는 T의 포인터(T*) 타입을 반환한다.
2. UTextRenderComponent(언리얼 엔진 문서)
UTextRenderComponent* TextRenderComponent;
설정된 텍스트를 3D 공간 상에 렌더링하는 컴포넌트이다. 글자 색, 크기, 폰트, 정렬 등을 설정할 수 있으며 액터 등에 컴포넌트로 덧붙여서 사용할 수 있다. 이 컴포넌트를 사용하기 위해서는 "Engine/Classes/Components/TextRenderComponent.h"를 포함해야 한다.
TextRenderComponent = CreateDefaultSubobject<UTextRenderComponent>(TEXT("TextRenderComponent"));
코드 상에서 UTextRenderComponent를 생성하는 방법은 위와 같다.
TextRenderComponent->SetHorizontalAlignment(EHTA_Center);
렌더링되는 텍스트의 수평 정렬을 설정하는 함수이다. 정렬 방식은 EHTA_Center, EHTA_Left, EHTA_Right가 있다.
TextRenderComponent->SetWorldSize(100.0f);
렌더링되는 텍스트의 월드에서의 크기를 설정하는 함수이다.
TextRenderComponent->SetText(TEXT("TEXT"));
렌더링되는 텍스트의 문자열 내용을 설정하는 함수이다.
3. Timer
타이머는 사용자가 정의한 시간마다 사용자가 지정한 동작이 실행되도록 만든다.
1) FTimerHandle (언리얼 엔진 문서)
FTimerHandle TimerHandle;
FTimerHandle은 타이머를 구별할 수 있는 유일한 핸들이다. 타이머를 생성하는 함수는 타이머를 생성할 때, 타이머의 핸들을 돌려주는데, 이 핸들을 가지고 있어야 생성한 타이머를 중지시킬 수 있다.
2) GetWorldTimerManager() (언리얼 엔진 문서)
AActor 클래스를 상속받는 모든 클래스에서 호출가능한 함수이다. 월드 타이머 매니저를 반환한다. GetWorldTimerManager()의 호출이 정상적으로 되지 않을 경우 "TimerManager.h"를 포함시키면 된다.
GetWorldTimerManager().SetTimer(TimerHandle, this, &ACountdown::AdvenceTimer, 1.0f, true);
SetTimer() 함수는 타이머를 생성하고 시작시키는 함수로 여러가지 오버로드가 존재하지만 우선은 위의 오버로드 형식만 살펴보자.
첫 번째 매개변수는 지금 생성되는 타이머의 핸들이다. 위에서 설명했듯이 이 핸들을 가지고 있어야 나중에 타이머를 종료할 수 있다.
두 번째 매개변수는 타이머 함수를 호출하는 오브젝트이다.
세 번째 매개변수는 타이머가 발동할 때마다 호출될 함수이다.
네 번째 매개변수는 타이머가 호출될 시간이다. 만약 값을 1로 두면 1초에 한 번씩 함수가 호출된다.
다섯 번째 매개변수는 타이머의 반복 여부이다. 만약 값이 false라면 타이머는 반복되지 않고 정해진 시간에 한 번만 호출된다.
GetWorldTimerManager().ClearTimer(TimerHandle);
ClearTimer() 함수는 돌아가고 있는 타이머를 중지시키고 해당 핸들을 무효화시키는 함수이다.
[투네이션]
[Patreon]
[디스코드 채널]
'Unreal Engine4 > Tutorial' 카테고리의 다른 글
[UE4] 제대로 따라가기 (6) C++ 프로그래밍 튜토리얼 :: 일인칭 슈팅 C++ 튜토리얼 (1) (4) | 2018.11.29 |
---|---|
[UE4] 제대로 따라가기 (5) C++ 프로그래밍 튜토리얼 :: UMG와 유저 인터페이스 (4) | 2018.11.28 |
[UE4] 제대로 따라가기 (4) C++ 프로그래밍 튜토리얼 :: 플레이어 제어 카메라 (2) | 2018.11.27 |
[UE4] 제대로 따라가기 (3) C++ 프로그래밍 튜토리얼 :: 컴포넌트와 콜리전 (0) | 2018.11.27 |
[UE4] 제대로 따라가기 (2) C++ 프로그래밍 튜토리얼 :: 플레이어 입력 및 폰 (6) | 2018.11.25 |