컴퓨터 및 게임 관련 전공인이 아닌 문외한 일반인이 언리얼 엔진을 배우면서 정리한 내용입니다.
그날그날 배웠던 내용을 제가 나중에 보기 위해 정리한 것으로, 지금 당장 보기에 부족한 점이 아주아주 많고 추후에 수정이 될 수도 있습니다.
오탈자 및 잘못 기재된 내용 지적, 부족한 내용 설명은 언제든지 환영입니다.
본글은 언리얼 엔진 5.5.4 버전 영문판을 기준으로 합니다.
----------------------------------------------------------------------------------------------------------------
11일차 학습 내용
- C++ 문법
Virtual & Override
순수 가상 함수
Property
Map
Initialization
Delegate
- 캐릭터 설정
스켈레톤 메시 입히기
Property를 사용하여 카메라 및 Spring Arm 배치
Enhanced Input Plugin을 이용한 캐릭터 조작 설정
C++ 문법
Virtual & Override
"Virtual" 은 파생 클래스에서 함수를 재정의하여 사용할 수 있게 해주는 것이고, "Override" 는 기반 클래스에서 가져온 함수를 파생 클래스가 재정의하여 사용할 수 있게 해주는 것이다.
아래의 코드 예시를 보도록 하겠다.
TestActor.h
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
class AAnimal
{
public:
virtual void Speak() // virtual : 자식 클래스가 해당 함수를 재정의 가능하게 해줌
{
GEngine->AddOnScreenDebugMessage(-1, 51.0f, FColor::Yellow, TEXT("Animal is making a sound!"));
}
};
class ACat : public AAnimal // ACat이 AAnimal 상속받음을 의미; AAnimal의 기능 사용이 가능함을 의미
{
public:
void Speak() override // override : 재정의
{
GEngine->AddOnScreenDebugMessage(-1, 51.0f, FColor::Yellow, TEXT("Cat! Cat!"));
}
};
|
cs |
TestActor.cpp
|
1
2
3
4
|
Begin Play();
AAnimal* MyPet = new ACat(); // 부모 포인터로 자식 객체를 새 메모리에 할당함을 의미
MyPet->Speak();
|
cs |
'Virtual' 인 " void Speak()" 함수가 "ACat" 에서 'Override' 의 결과로 똑같이 정의되어, 'MyPet' 에 'ACat' 이 할당되어 결과적으로 "Cat! Cat!" 이 출력된다.
이번엔 'Virtual' 과 'Override' 를 지워보도록 하겠다.
TestActor.h
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
class AAnimal
{
public:
void Speak() // virtual 지움
{
GEngine->AddOnScreenDebugMessage(-1, 51.0f, FColor::Yellow, TEXT("Animal is making a sound!"));
}
};
class ACat : public AAnimal
{
public:
void Speak() // override 지움
{
GEngine->AddOnScreenDebugMessage(-1, 51.0f, FColor::Yellow, TEXT("Cat! Cat!"));
}
};
|
cs |
이렇게 될 경우, "ACat" 의 함수가 재정의되지 않고 기본 클래스인 "AAnimal" 의 함수가 그대로 호출되어, "Animal is making a sound!" 가 출력된다.
순수 가상 함수
"virtual void Fun() = 0" 이런 식으로 끝에 "=0" 이 붙으면 순수 가상 함수로 분류된다.
순수 가상 함수는 구현되지 않았기에, 반드시 파생 클래스에서 구현이 되어야만 한다.
아래의 코드 예시를 보도록 하겠다.
TestActor.h
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
class AAnimal
{
public:
virtual void MakeSound() = 0; // 순수 가상 함수는 선언만 가능, 본문{} 을 가질 수 없음
};
class ACat : public AAnimal
{
public:
void MakeSound() override
{
GEngine->AddOnScreenDebugMessage(-1, 51.0f, FColor::Yellow, TEXT("Cat! Cat!"));
}
};
class ADog : public AAnimal
{
public:
void MakeSound() override
{
GEngine->AddOnScreenDebugMessage(-1, 51.0f, FColor::Yellow, TEXT("Dog! Dog!"));
}
};
|
cs |
TestActor.cpp
|
1
2
3
4
5
6
7
|
Begin Play();
AAnimal* MyPet = new ACat();
MyPet->MakeSound();
AAnimal* MyPet2 = new ADog();
MyPet2->MakeSound();
|
cs |
이렇게 될 경우 순수 가상 함수가 "ACat" 과 "ADog" 에서 재정의되어 "Cat! Cat!" 과 "Dog! Dog!" 가 출력된다.
'Override' 를 통해 자식에서 함수가 재정의되어 사용되기에, 위 예시처럼 함수 이름이 "MakeSound" 로 같아도 오류 없이 출력이 가능하다.
그리고 재정의를 한다는 것은 'Override' 가 들어간다는 것이고, 'Override' 는 'Virtiual' 과 함께 쓰이기에 순수 가상 함수에 'Virtiual' 이 반드시 포함되어 있어야한다.
Property
"Property" 는 외부에서 여러 카테고리를 불러올 수 있게 해준다.
TestActor.h
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
|
private:
UPROPERTY(EditAnywhere, BlueprintReadOnly, meta = (AllowPrivateAccess = "true"))
int Health; // "AllowPrivateAccess" 는 변수가 BP에 접근 가능하도록 하는 명령어
UPROPERTY(EditAnywhere, meta = (ClampMin = "0", ClampMax = "100"));
int PlayerScore; // "Clamp" 는 변수값을 제한하는 명령어
UPROPERTY(EditAnywhere, meta = (DisplayName = "PlayerSpeed"))
float speed; // "Displayname" 은 에디터 상에서 변수명이 변경가능하게 하는 명령어
UPROPERTY(EditAnywhere, meta = (Tooltip = "플레이어의 체력을 나타냅니다."))
int PlayerHP; // "Tooltip" 은 변수에 설명 추가하는 명령어
UPROPERTY(EditAnywhere, Category = "GamePlay Settings")
float MovementSpeed; // "Category" 는 BP에서 카테고리를 지정할 수 있게 해주는 명령어
public:
UPROPERTY(EditAnywhere, BlueprintReadWrite);
int PlayerAttack; // "BlueprintReadWrite" 는 BP 상에서 편집 가능하게 하는 명령어
|
cs |

* "BlueprintReadWrite" 는 'private' 가 아닌 'public' 으로 해줘야 정상적으로 작동

위의 코드대로 작성한 뒤, C++ 클래스의 폴더로 가서 위에서 작성한 C++ 클래스를 찾고 우클릭을 하면 위 사진과 같은 창이 뜬다.
"Create Blueprint class based on TestActor" 를 클릭하면 위에서 작성한 C++ 코드를 바탕으로 BP를 제작해준다.
그리고 제작된 BP를 레벨에 배치하면 적용 사항들을 확인 가능하다.

우선은 레벨 상에서 배치된 BP를 누르고 "details" 를 보면 "Class Option" 의 "Parents class" 가 해당 C++의 액터 이름인지 확인한다.

그리고 생성된 BP를 더블클릭하여 BP 편집창에 들어가 "details" 를 보고 위에서 작성한 카테고리들이 제대로 들어갔는지 확인해주면 된다.
주의해야하는 점은 항상 C++ 을 컴파일한 뒤에 BP를 컴파일 해야한다는 점이다.
C++로 만들어진 BP이다보니 우선순위가 C++ 우선이라, BP 상에서 수정하고 컴파일할 시 C++에 적용되지 않아 오류가 발생할 수도 있다.
Map
"Map" 은 배열처럼 많은 자료를 저장할 수 있다.
배열과 다른 점은 배열은 인덱스를 통해 각 요소에 접근 가능하지만, "Map" 은 키(Key)와 값(Value), 쌍으로 데이터를 저장하고, 각 키를 사용하여 해당 키에 연결된 값으로 접근이 가능하다.
"Map" 은 보통 "<str, int>" 의 형식으로 나타내며, 'str' 자리에 'int' 나 'float' 의 자료형도 넣을 수 있다.
언리얼에서는 보통 'TMap' 으로 해당 기능을 사용할 수 있고, 아래와 같은 형식 및 용도로 사용한다.
TestActor.h
|
1
2
3
4
5
6
|
public:
UPROPERTY(EditAnywhere, Category = "Weapons")
TMap<FString, bool> bIsGetWeapons; // Fstring은 무기 이름, bool은 소유 여부 표시
private:
void InitializatioinWeaponMap();
|
cs |
*언리얼에서 bool 형 표시는 'b', 'Is', 'bIs' 로 함
위 코드는 무기 소유 여부를 관리하는 변수를 선언하고, 무기 목록을 초기화하는 함수이다.
Initialization
"Initialization (초기화)" 는 쓰레기 값을 없애거나, 버그를 방지하는 등 여러 용도로 쓰이는데, 이외에도 게임 실행 중에 무기 목록이 비어 있는지 체크하고, 비어 있다면 초기 데이터를 설정하는 용도로 쓰인다.
아래 코드는 무기 목록 초기 데이터 설정용 코드이다.
TestActor.cpp
|
1
2
3
4
5
6
7
8
9
10
11
12
13
|
Super::BeginPlay();
InitializationWeaponMap();
void ATestActor::InitializatioinWeaponMap()
{
if (bIsGetWeapons.Num()== 0)
{
bIsGetWeapons.Add("AK47", false);
bIsGetWeapons.Add("K2", false);
bIsGetWeapons.Add("M16", false);
}
}
|
cs |
위 코드를 해석하면 "게임이 시작되면 무기 정보를 초기화하고, 갖고 있는 무기가 없으면 아래 무기들을 맵에 출력한다." 는 의미이다.

위 BP를 맵에 배치하고 'alt + p' 로 실행 후, "outliner" 에서 해당 BP를 클릭하고 "details" 쪽을 보면 위 사진과 같이 카테고리가 생긴 걸 확인할 수 있다.
Delegate
위 코드를 보면 "bIsGetWeapons.Add()" 함수가 나열돼있는 걸 볼 수 있다.
이럴 경우 무기의 종류가 여러개일 경우 위 함수를 일일이 넣어줘야하는 번거로움이 있다.
이런 번거로움을 줄이기 위해 "Delegate" 를 사용하여 동시에 함수 호출할 수 있다.
일단은 "Delegate" 의 개념과 아래와 같은 코드로 사용할 수 있다는 점만 인지하고 있으면 되겠다.
TestActor.h
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
DECLARE_DELEGATE(FTestDelegateFunction); // 매개변수가 없을 경우 바인딩할 함수를 넣어줌 + "Delegate" 선언
DECLARE_DELEGATE_TwoParams(FTestDelegateFunction_TwoParams, int, int); // 매개변수가 2개 있는 경우
public:
FTestDelegateFunction Test_Del_Fun; // 깡통변수
FTestDelegateFunction_TwoParams Test_Del_Fun_Two;
UFUNCTION() // 깡통 변수에 넣을 함수
void TestPrint();
UFUNCTION()
void TestPrint_Two(int num1, int num2);
virtual void EndPlay(const EEndPlayReason::Type EndPlayReason) override;
|
cs |
TestActor.cpp
|
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
|
Super::BeginPlay();
Test_Del_Fun.BindUObject(this, &ATestActor::TestPrint); // this: 자기자신을 지목하는 포인터
Test_Del_Fun_Two.BindUObject(this, &ATestActor::TestPrint_Two); // 클래스의 주소에 접근해 멤버의 함수를 바인딩
if (Test_Del_Fun.IsBound()) // 바인딩 확인
{
Test_Del_Fun.Execute(); // 바인딩한 모든 함수 호출
}
if (Test_Del_Fun_Two.IsBoound())
{
Test_Del_Fun_Two.Execute(10, 20);
}
void ATestActor::TestPrint()
{
GEngine->AddOnScreenDebugMessage(-1, 5.0f, FColor:Red, TEXT("TestPrint 호출"));
}
void ATestActor::TestPrint_Two(int num1, int num2)
{
FString FormatString = FString::Printf(TEXT("%d+ %d = %d"), num1, num2, num1 + num2);
GEngine->AddOnScreenDebugMessage(-1, 5.0f, FColor:Blue, FormatString);
}
void ATestActor::EndPlay(const EEndPlayReason::Type EndPlayReason)
{
Test_Del_Fun.Unbind(); // 바인딩했던 걸 풀어줌으로써 디폴트로 돌아감
Test_Del_Fun_Two.Unbind();
}
|
cs |
위 코드를 입력하면 아래와 같이 'alp + p' 로 게임 실행 시에 카테고리 선택창이 나온다.

캐릭터 설정
6일차에서 배웠던 캐릭터 설정을 BP가 아닌 C++로 하는 것이다.
원리는 똑같은 코드 및 코드 해석만 올리도록 하겠다.
스켈레톤 메시 입히기
우선 캐릭터 설정 관련 C++ 클래스를 만들고, 캐릭터에 입힐 스켈레톤 메시를 준비한다.
생성한 C++ 클래스를 더블클릭하여 Visual Studio 를 열고, 아래의 코드를 입력한다.
PlayerControl.cpp
|
1
2
3
4
5
6
7
8
9
10
11
12
13
|
#include "PlayerCharacterControl.h" // 캐릭터 설정하기 위해 만든 C++ 클래스 파일명
#include "Components/SkeletalMeshComponent.h"
PrimaryActorTick.bCanEverTick = true;
ConstructorHelpers::FObjectFinder<USkeletalMesh>PlayerMesh(TEXT("/Script/Engine.SkeletalMesh'/Game/Asset/Player/Survival_Character/Meshes/SK_Survival_Character.SK_Survival_Character'")); // 스켈레톤 메시의 주소를 붙여넣음
if (PlayerMesh.Succeeded())
{
GetMesh()->SetSkeletalMesh(PlayerMesh.Object); // 스켈레톤 메시 설정
GetMesh()->SetWorldLocationAndRotation(FVector(0, 0, -90), FRotator(0, -90, 0));// 스켈레톤 메시를 화면 시점과 일치키는 위치값으로 조정
}
}
|
cs |
저장 후 언리얼 엔진에서 컴파일 한 뒤, 해당 C++를 우클릭하여 "Create Blueprint class based on TestActor" 를 눌러 새로운 BP를 생성해준다.

그리고 위에서 생성한 BP를 클릭하여 위 사진과 같이 스켈레톤 메시가 잘 적용됐는지 확인해본다.
GameMode BP도 새로 만들어주고, "Default Pawn Class" 를 위에서 만든 캐릭터 설정 관련 BP로 설정해준다.
위까지 설정했다면 'alt + P' 를 눌러 제대로 동작하는지 확인해본다.
Property를 사용하여 카메라 및 Spring Arm 배치
PlayerControl.h
|
1
2
3
4
5
6
7
|
protected:
UPROPERTY(EditAnywhere)
class USpringArmComponent* SpringArm;
UPROPERTY(EditAnywhere)
class UCameraComponent* Camera;
|
cs |
PlayerControl.cpp
|
1
2
3
4
5
6
7
8
9
10
11
12
13
|
#include "GameFramework/SpringArmComponent.h"
#include "Camera/CameraComponent.h"
SpringArm = CreateDefaultSubobject<USpringArmComponent>(TEXT("SpringArm")); //컴포넌트 생성
SpringArm->SetupAttachment(RootComponent);
SpringArm->SetWorldLocation(FVector(0, 0, 55.0f)); // SpringArm 위치값 수정
SpringArm->TargetArmLength = 200; // TargetArmLength 값 수정
SpringArm->SocketOffset = FVector(0, 40, 0); // SocketOffset 값 수정
SpringArm->bUsePawnControlRotation = true; // "SpringArm" 이 컨트롤러(마우스)의 회전을 따라가도록 설정
Camera = CreateDefaultSubobject<UCameraComponent>(TEXT("Camera")); //컴포넌트 생성
Camera->SetupAttachment(SpringArm); //SpringArm에 연결
|
cs |

Enhanced Input Plugin을 이용한 캐릭터 조작 설정
Enhanced Input 을 사용한 조작 설정을 하려면, 6일차에서 배웠던 캐릭터 설정과 비슷한 사전 작업을 해줘야한다.
"Input Action" 파일을 2개 만들고, 각각 이름을 "IA_Move" , "IA_Look" 으로 해준 다음, 둘의 "Action Value Type" 을 "Axis2D" 로 설정해준다.
그 다음 "Input Action" 파일 옆에 "Input Mapping Context" 파일을 만들고, "IMC_PlayerController" 로 이름 지어준 다음, 아래 사진과 같이 설정해준다.
6일차와 같다.

그리고 "Enhanced Input" 플러그인이 설치돼있는지 확인해줘야한다.
우선 "edit" 탭의 "Plugins" 에서 "Enhanced Input" 을 검색하여 플러그인이 설치돼 있는지 확인해준다.
그리고 "Project Settings" 의 "Input"의 "Default Classes" 에서 "Enhanced Input" 으로 돼있는지 확인해준다.

플러그인이 전부 있는 걸 확인한 뒤 프로젝트 파일 우클릭 "Generate Visual Studio project files" 를 클릭해준다.

솔루션 탐색기에서 "자신의 프로젝트 이름.Build.cs" 파일을 클릭한다.

그리고 파일을 열어 C++ 코드가 열리면 해당 코드 중에 "EnhancedInput" 이 있는 걸 확인하고, 있으면 사전준비는 완료된 것이다.
아래 코드를 입력하면 조작 관련 설정이 가능하다.
PlayerControl.h
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
|
#include "CoreMinimal.h" // 아까 입력한 것들
#include "GameFramework/Character.h"
#include "InputActionValue.h" // 반드시 "generated" 위에 있어야함
#include "PlayerCharacterControl.generated.h"
private:
UPROPERTY(VisibleAnywhere, Category = "Input")
class UInputMappingContext* DefaultContext;
UPROPERTY(VisibleAnywhere, Category = "Input")
class UInputAction* MoveAction;
UPROPERTY(VisibleAnywhere, Category = "Input")
class UInputAction* LookAction;
UPROPERTY(VisibleAnywhere, Category = "Input")
float mousespeed = 30.0f;
void Move(const FInputActionValue& value); // Move와 Look 함수 alt + enter 로 정의 만들기
void Look(const FInputActionValue& value);
|
cs |
PlayerControl.cpp // 위에서 작성한 코드 바로 아래에 추가하면 됨
|
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
|
#include "EnhancedInputComponent.h"
#include "EnhancedInputSubsystems.h"
#include "InputMappingContext.h"
static ConstructorHelpers::FObjectFinder<UInputMappingContext>InputContext(TEXT("/Script/EnhancedInput.InputMappingContext'/Game/Input/IMC_PlayerInput.IMC_PlayerInput'")); // TEXT에 "IMC_" 파일 주소 삽입
if (InputContext.Object != nullptr)
{
DefaultContext = InputContext.Object;
}
static ConstructorHelpers::FObjectFinder<UInputAction>InputMove(TEXT("/Script/EnhancedInput.InputAction'/Game/Input/IA_Move.IA_Move'")); // TEXT에 "IA_Move" 파일 주소 삽입
if (InputMove.Object != nullptr)
{
MoveAction = InputMove.Object;
}
static ConstructorHelpers::FObjectFinder<UInputAction>InputLook(TEXT("/Script/EnhancedInput.InputAction'/Game/Input/IA_Look.IA_Look'")); // TEXT에 "IA_Look" 파일 주소 삽입
if (InputLook.Object != nullptr)
{
LookAction = InputLook.Object;
}
Super::BeginPlay();
if (APlayerController* PlayerController = Cast<APlayerController>(GetController()))
{
if(UEnhancedInputLocalPlayerSubsystem* Subsystem = ULocalPlayer::GetSubsystem<UEnhancedInputLocalPlayerSubsystem>(PlayerController->GetLocalPlayer()))
{
Subsystem->AddMappingContext(DefaultContext, 0);
}
}
void APlayerCharacterControl::SetupPlayerInputComponent(UInputComponent* PlayerInputComponent)
{
Super::SetupPlayerInputComponent(PlayerInputComponent);
UEnhancedInputComponent* EnhancedInputComponent = Cast<UEnhancedInputComponent>(PlayerInputComponent);
EnhancedInputComponent->BindAction(MoveAction, ETriggerEvent::Triggered, this, &APlayerCharacterControl::Move); // 생성한 C++ 클래스 파일명
EnhancedInputComponent->BindAction(LookAction, ETriggerEvent::Triggered, this, &APlayerCharacterControl::Look); // 생성한 C++ 클래스 파일명
}
void APlayerCharacterControl::Move(const FInputActionValue& value)
{
const FVector2D MovementVector = value.Get<FVector2D>();
const FRotator Rotation = Controller->GetControlRotation();
const FRotator YawRotation(0, Rotation.Yaw, 0);
const FVector ForwardDirection = FRotationMatrix(YawRotation).GetUnitAxis(EAxis::X);
const FVector RightDirection = FRotationMatrix(YawRotation).GetUnitAxis(EAxis::Y);
AddMovementInput(ForwardDirection, MovementVector.Y);
AddMovementInput(RightDirection, MovementVector.X);
}
void APlayerCharacterControl::Look(const FInputActionValue& value)
{
FVector2D LookAxisVector = value.Get<FVector2D>();
AddControllerYawInput(LookAxisVector.X * GetWorld()->DeltaTimeSeconds * mousespeed);
AddControllerPitchInput(LookAxisVector.Y * GetWorld()->DeltaTimeSeconds * -mousespeed); // Pitch 마우스 조작 시 -1
}
|
cs |
전체적인 코드를 보면 아래와 같다.
PlayerControl.h

PlayerControl.cpp


'Unreal Engine > GCC Class (5.5.4)' 카테고리의 다른 글
| Unreal Engine 5 배워보기 - 13일차 (0) | 2025.04.03 |
|---|---|
| Unreal Engine 5 배워보기 - 12일차 (0) | 2025.04.02 |
| Unreal Engine 5 배워보기 - 10일차 (0) | 2025.03.31 |
| Unreal Engine 5 배워보기 - 9일차 (0) | 2025.03.28 |
| Unreal Engine 5 배워보기 - 8일차 (0) | 2025.03.27 |