C++: 내일배움캠프 5번 과제 - 언리얼 엔진 기초 구현
4번 과제의 경우 간단히 주어진 코드를 분석하고 기능을 일부 추가하는 과제였다. 주어진 코드를 바로 붙여넣어 공유하기엔 저작권 문제도 있을 것이고 어렵기보단 간단한 과제였으니, 따로 글로 남기는 것은 넘어가도록 하겠다. 내용에 대해 요약하자면 포션을 만드는 연금술 공방 예시 코드에서 주어진 코드가 객체지향 설계에 왜 맞는지 설명하고 포션 레시피 검색, 재고관리 등 코드를 일부분 수정, 추가하는 내용이였다.
그 다음 2챕터 C++ 문법 마지막 과제는 5번 과제는 언리얼 에디터에서의 기초적인 사용과 C++ 스크립트를 이용한 기초적인 상호작용을 연습하는 과제다. 오히려 과거 과제들보다 어렵진 않다고 할 수 있겠다.
조건
- 액터 생성
- Actor를 상속하는 c++ 클래스와 해당 c++ 클래스를 상속하는 블루프린트 생성
- StaticMech 컴포넌트를 추가하고 Cube로 외형 설정
- 시작 위치를 (0,50,0)으로 설정
- 이동/회전
- 액터를 설정한 거리만큼 이동시키는 함수 구현
- 액터를 설정한 각도만큼 회전시키는 함수 구현
- 위 두 함수를 10번 무작위로 반복 실행
- 로그
- 액터가 이동하면 실시간으로 AddOnScreenDebugMessage를 통해 좌표 표시.
간단한 기능들이고 도전으로 원하면 더하는 기능들도 있다.
- 이동할 때마다 몇번째 이동인지 출력
- 이벤트 함수를 구현하고, 이동마다 50%확률로 실행되도록 설정
- 10회 이동 후에 총 이동거리 계산(FVector::Dist(A,B) 이용) 후에 이벤트 발생 횟수와 함께 출력 기초적인 언리얼 사용법을 연습하는 과제라 할 수 있겠다. 그럼 시작해보자.
액터 구현
C++파일을 이용해 Actor클래스를 상속받는 새 클래스를 만들어야 한다. 하나의 클래스만 구현하면 되기에 기본 네이밍인 MyActor를 쓰도록 하겠다. 간혹 프로젝트를 블루프린트로 잘못 생성했거나, C++ 클래스 폴더가 콘텐츠 브라우저에서 보이지 않아 추가하지 못할 때가 있다. 그럴때는 최상단 메뉴에서 Tools 안에 C++클래스를 추가하는 버튼이 있는데, 이걸로 새 C++ 클래스를 추가하면 폴더도 보일 것이다. 파일은 생성됬어도 해당 C++클래스의 존재를 언리얼 프로젝트에서 인식 못하는지 껐다가 키면 다시 폴더가 사라지는 때도 있는데, 당황하지 말고 해당 프로젝트의 비주얼스튜디오 프로젝트 파일(.sln)을 열어 새로 빌드해주면 추가했던 C++클래스가 잘 보일것이다. 처음 생성한 액터는 다음처럼 기본적인 틀만 가지고있다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
//MyActor.h
// Fill out your copyright notice in the Description page of Project Settings.
#pragma once
#include "CoreMinimal.h"
#include "GameFramework/Actor.h"
#include "MyActor.generated.h"
UCLASS()
class NBC5_API AMyActor : public AActor
{
GENERATED_BODY()
public:
// Sets default values for this actor's properties
AMyActor();
protected:
// Called when the game starts or when spawned
virtual void BeginPlay() override;
public:
// Called every frame
virtual void Tick(float DeltaTime) override;
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
//MyActor.cpp
// Fill out your copyright notice in the Description page of Project Settings.
#include "MyActor.h"
// Sets default values
AMyActor::AMyActor()
{
// Set this actor to call Tick() every frame. You can turn this off to improve performance if you don't need it.
PrimaryActorTick.bCanEverTick = true;
}
// Called when the game starts or when spawned
void AMyActor::BeginPlay()
{
Super::BeginPlay();
//시작 위치 고정
SetActorLocation(FVector(0, 50, 0));
}
// Called every frame
void AMyActor::Tick(float DeltaTime)
{
Super::Tick(DeltaTime);
}
BeginPlay()와 Tick(), 생성자 정도만 있는 기본적인 클래스 구조이다. 지금 해놓은 것은 시작 위치를 (0, 50, 0)로 설정하는 것 만 해놓았다. 블루프린트 클래스를 만들어 액터를 배치하고 작동하는지 확인해보자. 
설정해놓은 시작위치에 액터가 없더라도 시작하면 해당 위치로 움직이는 것을 볼 수 있다.
이동 횟수 출력
이제 여기서 10번 랜덤하게 회전 혹은 이동하도록 함수를 구현해보자.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
//MyActor.h
#pragma once
#include "CoreMinimal.h"
#include "GameFramework/Actor.h"
#include "MyActor.generated.h"
UCLASS()
class NBC5_API AMyActor : public AActor
{
GENERATED_BODY()
public:
// Sets default values for this actor's properties
AMyActor();
protected:
// Called when the game starts or when spawned
virtual void BeginPlay() override;
FVector Origin;
UPROPERTY(EditAnywhere, Category = "MyActor")
double MoveDistance;
int Steps;
double MoveCooldown;
public:
// Called every frame
virtual void Tick(float DeltaTime) override;
void Move(); //이동 함
void Rotate(); //회전 함수
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
//MyActor.cpp
#include "MyActor.h"
// Sets default values
AMyActor::AMyActor()
{
// Set this actor to call Tick() every frame. You can turn this off to improve performance if you don't need it.
PrimaryActorTick.bCanEverTick = true;
//맴버 변수 초기화
Steps = 0;
MoveCooldown = 0.0;
Origin = FVector(0, 0, 0);
}
// Called when the game starts or when spawned
void AMyActor::BeginPlay()
{
Super::BeginPlay();
//시작 위치 고정
SetActorLocation(FVector(0, 50, 0));
Origin = GetActorLocation();
GEngine->AddOnScreenDebugMessage(-1, 60.0, FColor::Cyan, FString::Printf(TEXT("시작합니다!")), true);
}
// Called every frame
void AMyActor::Tick(float DeltaTime)
{
Super::Tick(DeltaTime);
//이동 쿨타임 끝나면
if (Steps < 10 && MoveCooldown <= 0) {
Steps++;
MoveCooldown += 2; //2초마다 반복
if (FMath::FRand() < 0.5) { //50%확률로 이동
Move();
}
else { //50%확률로 회전
Rotate();
}
}
else if (Steps == 10) {
Steps++;
GEngine->AddOnScreenDebugMessage(-1, 60.0, FColor::Orange, FString(TEXT("이동이 끝났습니다!")), true);
GEngine->AddOnScreenDebugMessage(-1, 60.0, FColor::Red, FString::Printf(TEXT("총 이동 거리: %f"), FVector::Dist(Origin, GetActorLocation())), true);
}
else { //쿨타임중이면
MoveCooldown -= DeltaTime;
}
}
void AMyActor::Move() { //전방으로 이동
SetActorLocation(GetActorLocation() + GetActorForwardVector() * MoveDistance);
GEngine->AddOnScreenDebugMessage(-1, 60.0, FColor::Red, FString::Printf(TEXT("큐브가 움직였습니다: (%f , %f)"), GetActorLocation().X, GetActorLocation().Y), true);
}
void AMyActor::Rotate(){ //yaw 90도 회전
SetActorRotation(GetActorRotation() + FRotator(0,90,0));
GEngine->AddOnScreenDebugMessage(-1, 60.0, FColor::Green, FString::Printf(TEXT("큐브가 회전했습니다: %f"), GetActorRotation().Yaw), true);
}
먼저 이동과 회전을 담당하는 Move()와 Rotate()가 추가다. Move()는 액터가 바라보는 전방 방향으로 주어진 이동거리인 Movedistance만큼 움직일 것이다. 나같은 경우 블루프린트에서 200.0으로 설정해놓았고, 움직임을 볼 충분한 거리면 상관 없다. 각 이동에서는 AddOnScreenDebugMessage()를 이용해 화면 안에 붉은색 메세지로 어떤 좌표로 이동했는지 표시한다. Rotate()같은 경우 Yaw로 +90만큼, 그러니까 액터를 위에서 바라볼 때 시계 방향으로 90도 회전시킨다. Tick()함수에선 MoveCooldown을 이용해 각 단계 실행 간에 2초의 시간을 보장한다. 스탭을 실행할 때가 되면, FRand()를 통해 얻은 값을 바탕으로 50%확률로 Move()를, 다른 50%확률로 Rotate()를 호출한다. 경우에 따라서는 원점으로부터 멀리 갈 수도 있고, 아니면 거의 제자리에서 맴돌 수도 있다. 10회 반복이 끝나면 dist()를 이용해 원래 있던 위치인 Origin과 현재 위치의 거리를 계산해 표시한다. 10번 반복후에는 반복을 끊기위해 예시가 크거나 복잡하지 않기에, 간단하게 Steps 카운터를 증가시켜 조건을 이탈시키는 방법을 이용했다. 추가 동작이 게임 실행 내내 필요 없음이 확실한 때에는 bCanEverTick을 false로 바꾸는 방법이 자원 절약에 더 좋을 수 있겠다.
여기까지 필수 기능인 액터 클래스, 이동, 회전, 랜덤 반복, 좌표 출력까지 구현해보았다. 이제 도전 과제를 구현해보자.
이벤트 생성 및 결과 출력
간단하게 이벤트가 발생했음을 나타내는 함수를 추가해보고, 이를 Tick()에서 발동시키도록 수정해보자.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
//MyActor.h
#pragma once
#include "CoreMinimal.h"
#include "GameFramework/Actor.h"
#include "MyActor.generated.h"
UCLASS()
class NBC5_API AMyActor : public AActor
{
protected:
//...
public:
//...
void SimpleEvent(); //이벤트 함수
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
//MyActor.cpp
#include "MyActor.h"
// Sets default values
AMyActor::AMyActor()
{
// Set this actor to call Tick() every frame. You can turn this off to improve performance if you don't need it.
PrimaryActorTick.bCanEverTick = true;
//맴버 변수 초기화
Steps = 0;
MoveCooldown = 0.0;
EventCount = 0; //이벤트 카운터 초기화
Origin = GetActorLocation();
}
//...
// Called every frame
void AMyActor::Tick(float DeltaTime)
{
//...
if (Steps < 10 && MoveCooldown <= 0) {
Steps++;
MoveCooldown += 2; //2초마다 반복
if (FMath::FRand() < 0.5) { //50%확률로 이동
Move();
}
else { //50%확률로 회전
Rotate();
}
if (FMath::FRand() < 0.5) { //50%확률로 이벤트 발동
SimpleEvent();
}
}
else if (Steps == 10) {
Steps++;
GEngine->AddOnScreenDebugMessage(-1, 60.0, FColor::Orange, FString(TEXT("이동이 끝났습니다!")), true);
GEngine->AddOnScreenDebugMessage(-1, 60.0, FColor::Red, FString::Printf(TEXT("총 이동 거리: %f"), FVector::Dist(Origin, GetActorLocation())), true);
GEngine->AddOnScreenDebugMessage(-1, 60.0, FColor::Red, FString::Printf(TEXT("이벤트 발생 횟수: %d"), EventCount), true); //이벤트 발생횟수 출력
}
//...
}
//...
void AMyActor::SimpleEvent(){ //이벤트 메세지 출력
EventCount++;
GEngine->AddOnScreenDebugMessage(-1, 60.0, FColor::Magenta, FString::Printf(TEXT("이벤트가 호출됬습니다!")), true);
}
이벤트를 알리기 위한 SimpleEvent()함수가 추가되었다. Tick()에서 50%의 확률로 호출되며, 호출되면 횟수를 기억하는 변수를 1증가 시키고 메세지를 출력한다. 10번의 반복이 끝나면 총 발생 횟수가 기존에 구현해놨던 총 이동거리와 함께 출력된다.
이렇게 도전 과제인 기초적인 언리얼 기능들에 대한 연습이라 크게 무리는 없었으리라. 기초를 연습삼아 다시 해보는 복습의 시간이였다.
오늘의 경험: 기본으로 돌아가기
특별히 무언가를 구현한다기 보단 어떤 기능이 있는지, 그 기능들을 이해하는지 살펴보는 과제였기에 경험이 있던 사람에겐 크게 어렵지 않았으리라. AddOnScreenDebugMessage()는 평소엔 UE_LOG()를 주로 사용하는 입장에선 쓸일이 없었기에 복습하는 기회가 되긴 하였다. UE_LOG()는 플레이가 종료된 후에도 기록이 남아있고 별도의 콘솔창에 뜨기에 많은 양의 메세지를, 또 무슨 일이 있었는지를 나중에도 파악할 수 있지만, AddOnScreenDebugMessage()는 정해진 시간 동안만 게임 플레이 화면에 떠있는 메세지이기에 특정 상황에 대한 간단한 알림정도의 목적이라면 더 적합할수도 있겠다.