리플렉션은 C++에서 구성한 클래스를 블루프린트에서 수정가능하게 만드는 시스템이다.
블루프린트는 내부에 이미 C++로 구현이 되어 있는 것을 다시 한 번 감싼 것이기 때문에 그냥 C++로 프로그램을 구성하는 것보다 속도가 느리다.
또한 언리얼에선 방대한 로직들이 존재하는데 그 로직들을 블루프린트로만 관리하는 것이 어렵고 블루프린트 프로그램은 디버깅도 C++클래스에 비해서 쉽지 않기 때문에 블루프린트는 프로그래머들은 블루프린트로 클래스를 구성하진 않는다.
그런데도 C++코드를 블루프린트 클래스로 변환해주는 리플렉션을 사용하는 이유는 메쉬 적용 같은 간단한 작업을 할 때 C++코드로 작업을 하면 일일이 코드를 작성해야하기 때문이다. 게다가 현업에선 에셋 적용은 프로그래머가 아닌 사람들이 작업을 하기 때문에 그분들을 위해서라도 C++코드들을 리플렉션 시스템을 통해 블루프린트로도 작업 가능하도록 해주어야 한다.
#include "CoreMinimal.h"
#include "GameFramework/Actor.h"
#include "Item.generated.h" // <<
UCLASS()
class SPARTAPROJECT_API AItem : public AActor
{
GENERATED_BODY()
}
언리얼 엔진으로 C++클래스를 만들면 .generated.h헤더 파일이 만들어지는데 이 헤더는 리플렉션 시스템을 지원하는데 필수적인 파일이다.
include코드 중에선 항상 마지막에 있어야 한다. 그렇지 않으면 오류가 발생하여 빌드가 되지 않는다.
#include "CoreMinimal.h"
#include "GameFramework/Actor.h"
#include "Item.generated.h"
UCLASS() // <<
class SPARTAPROJECT_API AItem : public AActor
{
GENERATED_BODY()
}
UCLASS()는 클래스를 리플렉션 시스템에 등록해주는 일종의 매크로다.UCLASS() 아래에 존재하는 클래스가 리플렉션 시스템에 등록되게 한다.
UCLASS()에는 몇 개의 인자가 들어갈 수 있다.
UCLASS(Blueprintable, BlueprintType)
이 코드는 UCLASS()와 똑같은 기능을 한다. UCLASS의 항목을 비워놓으면 위와 같은 인자들이 자동으로 기입되어 실행된다.
Blueprintable : 이 클래스가 블루프린트에서 상속이 가능하다는 뜻이다. 만약 NotBlueprintable라고 적는다면 블루프린트에서 상속이 불가능하다.
BlueprintType : 블루프린트에서 이 클래스를 변수로 선언하거나 참조할 수 있게된다.
만약 UCLASS(BlueprintType)라고 적게되면 블루프린트에서 변수로 선언하거나 참조할 수 있게되지만 블루프린트에서 상속은 불가능해진다.

언리얼 에디터의 C++클래스를 우클릭하면 C++클래스를 기반으로 블루프린트 클래스를 만들 수 있다.

블루프린트 클래스를 만들면 블루프린트 클래스 창이 뜨게 된다.

여기서 변수를 생성하여 타입을 설정할 때 Item클래스가 변수로 생성할 수 있게된 것을 확인할 수 있다.
#include "CoreMinimal.h"
#include "GameFramework/Actor.h"
#include "Item.generated.h"
UCLASS()
class SPARTAPROJECT_API AItem : public AActor
{
GENERATED_BODY()
public:
AItem();
protected:
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category="Item|Components") // <<
USceneComponent* SceneRoot; // 씬 루트 컴포넌트
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Item|Components") // <<
UStaticMeshComponent* StaticMeshComp; // 스태틱 메쉬 컴포넌트
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Item|Properties") // <<
float RotationSpeed;
}
UPROPERTY()를 변수 위에 적으면 아래에 있는 변수가 리플렉션 시스템에 등록된다. 변수 등록같은 경우엔 인자를 비워 놓으면 등록은 되지만 그 외엔 아무런 효과를 가지지 않는다. 그래서 인자 값을 따로 설정해주어야 한다.
VisibleAnywhere : 수정은 불가능하다.
EditAnywhere : 클래스 디폴트에서도 수정이 가능하고 인스턴스도 수정이 가능하다.
EditDefaultsOnly : 클래스 디폴트만 수정이 가능하고 인스턴스에선 수정이 불가능 하다.
EditInstanceOnly : 클래스 디폴트에선 수정이 불가능하고 인스턴스에서만 수정이 가능하다.
Category는 해당 변수의 카테고리를 설정하는 것으로 이 카테고리를 설정하면 디테일 바에서 표시되는 카테고리를 상위 카테고리와 하위 카테고리로 나누어서 설정이 가능하다.
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category="Item|Components")이 코드를 보면 상위 카테고리를 Item으로 설정하고 하위 카테고리를 Components로 설정한 것이다.
클래스를 어떤 식으로 변수로 불러올 것인지에 대해서도 설정이 가능하다.
BlueprintReadOnly : 클래스를 변수로 만들 때 Get만 가능하다. Set은 불가능하다.
BlueprintReadWrite : Get, Set 둘 다 가능하다.

코드를 빌드하고 언리얼 에디터를 껐다 켠후에 Item클래스를 기반으로 블루프린트 클래스를 다시 만들어보자 그러면 클래스를 선택했을 때 위와 같은 설정창이 뜨게된다.
확인해보면 수정이 불가능하도록 설정한 씬 루트 컴포넌트는 보이지 않고 클래스 디폴트를 수정할 수 있도록 설정한 스태틱 메쉬 컴포넌트와 RotationSpeed만 보이는 것을 확인할 수 있다.
스태틱 메쉬 컴포넌트는 EditAnywhere로 설정했기 때문에 에디터상에서도 수정이 가능하다. 그래서 생성자에서 설정을 해놨던 에셋 설정 코드들을 지워주어도 상관이 없다.

코드에서 에셋 설정 코드를 지우고 블루프린트에서 에셋을 따로 설정해줄 수도 있다.

C++클래스를 기반으로 만든 블루프린트 클래스로 들어가 스태틱 메쉬와 메테리얼을 설정해줄 수 있다.
#include "CoreMinimal.h"
#include "GameFramework/Actor.h"
#include "Item.generated.h"
UCLASS()
class SPARTAPROJECT_API AItem : public AActor
{
GENERATED_BODY()
public:
AItem();
protected:
...
UFUNCTION(BlueprintCallable, Category = "Item|Actions")
void ResetActorPosition();
UFUNCTION(BlueprintPure, Category = "Item|Properties")
float GetRotationSpeed() const;
UFUNCTION(BlueprintImplementableEvent, Category= "Item|Event")
void OnItemPickedUP();
}
UFUNCTION() 매크로를 이용해 함수를 리플렉션 시스템에 등록할 수 있다.
UFUNCTION()에 인자가 아예 없을 때는 UPROPERTY()의 항목이 비었을 때처럼 언리얼 엔진에서 함수의 존재만 인식 가능하다.
BlueprintCallable : 블루프린트에서 이 함수를 호출할 수 있게 된다.
BlueprintPure : 블루프린트에서 return값만 반환받는 함수로만 호출 할 수 있다.
BlueprintImplementableEvent : C++이 아닌 블루프린트 클래스에서 구현을 한 것으로 C++에서 호출이 가능하도록 한다.
// Item.cpp
void AItem::ResetActorPosition()
{
SetActorLocation(FVector::ZeroVector); // 원점(0,0,0)을 의미한다.
}
float AItem::GetRotationSpeed() const
{
return RotationSpeed;
}
이렇게 cpp파일에서 함수를 설정해준 다음 빌드 해주고 다시 Item클래스를 기반으로 블루프린트 클래스를 만든 다음 이벤트 그래프로 가서 함수를 생성해보자.

보시다시피 우리가 C++코드 상에서 생성한 함수들을 블루프린트에서 사용가능하게 바뀌었다. OnItemPickedUp은 C++코드에서 선언만 해준것인데 이렇게 블루프린트에서 함수를 구체적으로 정의할 수 있다.
함수를 C++코드 내에서 구체적으로 정의하지 않더라도 이 함수를 C++에 BeginPlay나 Tick에 넣어주면 정상 작동한다.
GameMode클래스 - 게임의 총괄 관리자
- 플레이어블 캐릭터 : Pawn클래스 or Character클래스
- PlayerController클래스 : 캐릭터에 빙의
- 게임 규칙 관리 : 로직(함수)관리 -> 점수의 규칙 설정
- GameState 클래스 : 게임 전역 데이터 -> 점수를 저장
PlayerState클래스 : 개별 캐릭터마다의 데이터를 저장(멀티 게임 만들 때 많이 사용)

새로운 C++클래스를 만들고 이 클래스의 부모 클래스를 지정할 때 GameMode클래스와 GameModeBase클래스가 있다.
GameModeBase클래스는 초급자들이 사용하기 쉬운 단순화된 형태를 가지고 있다. 멀티플레이 로직이 전혀 없다고 보면된다. 간단한 게임을 만들 때 사용하는 것이 권장된다.
GameMode클래스는 GameModeBase의 자손으로 멀티플레이 로직도 지원하고 GameState클래스와 PlayerState클래스와도 연동이 되어 있다. 거의 모든 기능 들을 지원한다.

GameMode클래스로 생성하고 만든 클래스를 상속하여 블루프린트 클래스로 만들어준다.
실제 게임 프로젝트에서도 C++로 게임모드 클래스를 사용하는 것보다 블루프린트로 한 번 감싸서 사용하는게 더 좋을 때가 있다.
블루프린트 클래스 내에서 여러 파라미터를 수정하는게 C++에서 수정하는 것보다 더 편리하고 유용한 점이 더 많다.

눌러보면 우측 상단에 Sparta Game Mode클래스를 상속받고 있다는 걸 확인할 수 있다.

이제 우리가 만든 게임모드 클래스를 전체 프로젝트의 게임모드로 바꿔주어야 한다.
Edit에서 Project Settings를 누른다.

Map & Modes에서 Default GameMode에서 우리가 만든 게임모드 클래스를 설정해준다.

아래에는 게임 모드의 세부사항을 설정해주는 곳이다.
여기서 맨 아래 Spectator클래스는 관전하는 클래스를 뜻하는데 피파 게임에서 관중 시점으로 축구장을 바라보는 것이나 FPS게임에서 플레이어블 캐릭터가 죽었을 때 캐릭터가 쓰러지는 것을 보는 시점을 표현할 수 있다.

맵의 레벨 별로 게임 모드를 개별로 설정할 수도 있다.
Window에서 World Settings를 눌르면 우측 하단에 Details옆에 World Settings가 생긴 걸 볼 수 있다.


여기에서 GameMode Override에서 우리가 만든 게임 모드 클래스를 선택해주면 이 레벨만 따로 게임 모드 클래스를 지정해줄 수 있다.

월드 세팅에서도 게임 모드의 세부 사항을 설정해줄 수 있다.
만약 현재 레벨과 전체 프로젝트의 게임 모드가 다를 경우 현재 레벨에서 설정한 게임 모드가 가장 우선시 된다.

이제 게임을 실행하고 아웃라이너를 보면 설정한 게임모드가 객체로써 활성화 되어 있는 것을 볼 수 있다.
폰 클래스 : 캐릭터와 AI도 가능하다. 플레이어블 캐릭터 클래스 중 가장 상위 클래스다. 가장 상위 클래스이기 때문에 움직이게 만들려면 모든 움직임을 전부 구현해주어야 한다.
캐릭터 클래스 : 캐릭터 무브먼트 등 다양한 컴포넌트가 자동으로 붙어서 생성된다.
단점은 2족 보행을 하는 인간형 캐릭터만 만들 수 있다는 단점이 있다.
폰, 캐릭터 클래스 모두 액터 클래스를 상속받는 다는 것을 꼭 기억해두어야 한다.

이제 C++ 캐릭터 클래스를 생성해보자

생성한 캐릭터 클래스를 상속해 블루프린트 클래스를 만든다.

들어가보면 캐릭터 블루프린트 클래스 창이 뜬다.

컴포넌트를 보면 캐릭터 클래스를 만들 때 자동으로 생성되는 컴포넌트들이 보인다.
캡슐 컴포넌트 : 캐릭터가 부딪히는 반응이 생길 때 캐릭터가 충돌을 처리할 범주라고 할 수 있다.
애로우 컴포넌트 : 캐릭터가 어느 방향을 바라보고 있는지 표시하기 위해서 존재한다.
메쉬 컴포넌트 : 움직이는 캐릭터이기 때문에 스켈레탈 메쉬 컴포넌트가 생성되었다. 이 컴포넌트는 뼈대가 있고 이 뼈대를 중심으로 움직이는게 가능한 메쉬라고 할 수 있다.
캐릭터 무브먼트 컴포넌트 : 캐릭터의 이동, 점프, 중력 등 캐릭터의 물질적인 이동 로직들을 전부 구현해놓은 것이다.

스켈레탈 메쉬 항목에서 에셋을 골라 입혀준다. 메테리얼은 메쉬를 선택하면 자동으로 선택된다.

그 다음 루트 컴포넌트에서 캐릭터의 정면을 애로우 컴포넌트 방향과 맞춰준다.

그리고 캐릭터를 캡슐 컴포넌트에 맞게 맞춰주고 캡슐 컴포넌트의 크기도 캐릭터의 크기에 맞게 설정해준다.

블루프린트 클래스를 컴파일/ 저장하고 뷰포트에 드래그 드랍해보면 뷰포트에서 방금 설정한대로 맵에 캐릭터가 생성되는 것을 볼 수 있다.

먼저 비주얼 스튜디오를 열어서 캐릭터 클래스의 헤더파일에 스프링암 컴포넌트, 카메라 컴포넌트 변수를 추가해준다.
하지만 막상 적어보면 변수 타입에 빨간 줄이 표시 되는 것을 볼 수 있다.
이것은 헤더파일이 부재되어 변수 타입을 인식하지 못 한 것이다.
이럴 때 헤더파일에 헤더파일을 전부 참조해버리면 컴파일 과정에서 이 헤더파일들을 전부 참조하는 과정에서 자원이 낭비된다고 할 수 있다.

그래서 헤더파일에서 스프링암, 카메라 클래스를 미리 선언해주어서 컴파일 과정에서 부하를 줄여주고 실제 헤더파일 참조는 구현부에서 하면 된다.
#include "SpartaCharacter.h"
#include "Camera/CameraComponent.h"
#include "GameFramework/SpringArmComponent.h"
ASpartaCharacter::ASpartaCharacter()
{
PrimaryActorTick.bCanEverTick = false;
SpringArmComp = CreateDefaultSubobject<USpringArmComponent>(TEXT("SpringArm"));
}
먼저 Tick 함수는 사용하지 않을 것이기 때문에 Tick함수를 지워주고 Tick함수를 사용하지 않겠다는 코드를 추가해준다.
그 다음 SpringArmComp 포인터 변수에 스프링암 컴포넌트를 생성하여 붙인다.
#include "SpartaCharacter.h"
#include "Camera/CameraComponent.h"
#include "GameFramework/SpringArmComponent.h"
ASpartaCharacter::ASpartaCharacter()
{
PrimaryActorTick.bCanEverTick = false;
SpringArmComp = CreateDefaultSubobject<USpringArmComponent>(TEXT("SpringArm"));
SpringArmComp->SetupAttachment(RootComponent);
}
그 다음 스프링암 컴포넌트를 루트 컴포넌트의 자손으로 붙인다.
#include "SpartaCharacter.h"
#include "Camera/CameraComponent.h"
#include "GameFramework/SpringArmComponent.h"
ASpartaCharacter::ASpartaCharacter()
{
PrimaryActorTick.bCanEverTick = false;
SpringArmComp = CreateDefaultSubobject<USpringArmComponent>(TEXT("SpringArm"));
SpringArmComp->SetupAttachment(RootComponent);
CameraComp = CreateDefaultSubobject<UCameraComponent>(TEXT("Camera"));
CameraComp->SetupAttachment(SpringArmComp, USpringArmComponent::SocketName);
}
이제 CameraComp 포인터 변수에도 카메라 컴포넌트를 생성하여 연결해준다.
그리고 카메라 컴포넌트는 스프링암 컴포넌트의 끝에 붙일 것이다.
SetupAttachment의 첫 번째 인자엔 SpringArmComp를 넣어주고 두 번째 인자엔 USpringArmComponent::SocketName을 넣어준다. 이는 스프링 암 컴포넌트의 끄트머리에 카메라 컴포넌트를 붙이겠다는 의미를 가진다.
#include "SpartaCharacter.h"
#include "Camera/CameraComponent.h"
#include "GameFramework/SpringArmComponent.h"
ASpartaCharacter::ASpartaCharacter()
{
PrimaryActorTick.bCanEverTick = false;
SpringArmComp = CreateDefaultSubobject<USpringArmComponent>(TEXT("SpringArm"));
SpringArmComp->SetupAttachment(RootComponent);
SpringArmComp->TargetArmLength = 300.0f;
SpringArmComp->bUsePawnControlRotation = true;
CameraComp = CreateDefaultSubobject<UCameraComponent>(TEXT("Camera"));
CameraComp->SetupAttachment(SpringArmComp, USpringArmComponent::SocketName);
}
스프링암의 길이를 300으로 설정해주고 bUsePawnControlRotation을 통해 마우스를 이용해 캐릭터를 회전할 때 스프링 암이 캐릭터의 시점을 따라가도록 하는 기능을 bUsePawnControlRotation을 통해 활성화해준다.
#include "SpartaCharacter.h"
#include "Camera/CameraComponent.h"
#include "GameFramework/SpringArmComponent.h"
ASpartaCharacter::ASpartaCharacter()
{
PrimaryActorTick.bCanEverTick = false;
SpringArmComp = CreateDefaultSubobject<USpringArmComponent>(TEXT("SpringArm"));
SpringArmComp->SetupAttachment(RootComponent);
SpringArmComp->TargetArmLength = 300.0f;
SpringArmComp->bUsePawnControlRotation = true;
CameraComp = CreateDefaultSubobject<UCameraComponent>(TEXT("Camera"));
CameraComp->SetupAttachment(SpringArmComp, USpringArmComponent::SocketName);
CameraComp->bUsePawnControlRotation = false;
}
그 다음 캐릭터의 시점이 돌아갈 때 스프링암에 붙어있던 카메라는 캐릭터의 시점에 따라 카메라가 회전하지 않고 계속 정면만을 바라보도록 캐릭터의 회전에 따라 카메라가 회전하는 기능을 false로 바꿔준다.
class USpringArmComponent;
class UCameraComponent;
UCLASS()
class SPARTAPROJECT_API ASpartaCharacter : public ACharacter
{
GENERATED_BODY()
public:
ASpartaCharacter();
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Camera")
USpringArmComponent* SpringArmComp;
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Camera")
UCameraComponent* CameraComp;
protected:
virtual void SetupPlayerInputComponent(class UInputComponent* PlayerInputComponent) override;
};
캐릭터 헤더 파일로 돌아가서 설정한 변수들이 블루프린트 클래스에서 보이도록 UPROPERTY를 설정해주자.

C++코드를 빌드해주고 언리얼 에디터를 켜서 C++클래스를 상속한 블루프린트 클래스를 열어보면 우리가 추가한 컴포넌트들이 에디터 상에 추가된 것을 볼 수 있다.

플레이어 시점은 카메라로 고정될 것이기 때문에 시점이 잘 보이도록 카메라의 위치를 조정해준다.

이제 게임모드 블루프린트 클래스로 가보면 디폴트 폰 클래스를 설정할 수 있는데 이건 에디터 상에서도 가능하지만 C++코드로도 설정할 수 있다.
// SpartaGameMode.h
#include "CoreMinimal.h"
#include "GameFramework/GameMode.h"
#include "SpartaGameMode.generated.h"
UCLASS()
class SPARTAPROJECT_API ASpartaGameMode : public AGameMode
{
GENERATED_BODY()
public:
ASpartaGameMode();
};
게임 모드 클래스의 헤더파일로 가서 생성자를 생성해준다.
// SpartaGameMode.cpp
#include "SpartaGameMode.h"
#include "SpartaCharacter.h"
ASpartaGameMode::ASpartaGameMode()
{
DefaultPawnClass = ASpartaCharacter::StaticClass();
}
캐릭터 클래스 헤더 파일을 참조시켜주고 생성자의 구현부에 디폴트 폰 클래스를 캐릭터 클래스로 지정해주는데 스태틱 클래스를 사용하여 지정해주자.
이러면 객체를 생성하지 않고도 캐릭터 클래스를 지정해줄 수 있다.

에디터의 뷰포트를 보면 사진과 같이 게임 스타트 지점이 보이는데 이 지점에서 캐릭터가 스폰된다. 이건 게임 모드가 해주는 역할이다.
이제 게임을 시작하면 스타트 지점에 자동으로 캐릭터가 생성되면 된다.

게임을 실행해보면 캐릭터가 잘 생성되는 것을 볼 수 있다.
블루프린트로 캐릭터를 생성하고 스프링암 컴포넌트를 사용해 3인칭을 구현하는 작업은 해본 적이 있긴 한데 C++코드로 구현을 해보니까 블루프린트로 만들었을 때보다 더 신경쓸 부분이 많은 것 같다.
C++클래스를 만들고 블루프린트로 상속시켜서 코드로 설정하기 귀찮은 세부사항을 블루프린트로 설정해주는 것은 꽤나 우아한 아이디어라는 생각이 들었다.