본문 바로가기

C++로 Blueprint 클래스 확장

핵심 로직은 C++에 두고 표현과 조정은 Blueprint로 확장하는 부모·자식 클래스 협업 방식을 실습합니다.

UObject와 AActor 차이를 이해했다면, 이제 언리얼 하이브리드 개발의 핵심으로 넘어갑니다.

이번 절은 C++ 클래스를 블루프린트로 확장하는 방법을 다룹니다.

언리얼은 C++로 기반 구조를 만들고, 블루프린트로 빠르게 기능을 확장하는 방식을 강력하게 지원하며, 이 조합이 실제 개발 효율을 크게 높여 줍니다.


C++을 블루프린트로 확장해야 하는 이유

이 질문에 대한 답은 명확합니다.

C++과 블루프린트는 각각의 장단점이 있기 때문입니다.

  • C++의 장점
  • 성능: 복잡한 연산, 물리 시뮬레이션, AI 로직 등 성능이 중요한 부분에 유리합니다.
  • 접근성: 엔진의 깊숙한 부분까지 접근하여 커스터마이징할 수 있습니다.
  • 제어: 메모리 관리 등 더 세밀한 제어가 가능합니다.
  • 표준화: 대규모 프로젝트에서 코드의 구조와 규칙을 강력하게 강제할 수 있습니다.
  • 블루프린트의 장점
  • 빠른 프로토타이핑: 시각적인 스크립팅 방식으로 아이디어를 빠르게 구현하고 테스트할 수 있습니다.
  • 비개발자 협업: 프로그래밍 지식이 없는 아티스트나 기획자도 게임 로직에 참여할 수 있습니다.
  • 쉬운 반복 작업: 간단한 로직 변경이나 데이터 수정에 즉각적으로 대응할 수 있습니다.

이상적인 워크플로는 책임을 분리하는 방식입니다.

성능에 민감한 구간, 엔진 핵심 경로, 복잡 로직 기반은 C++로 구현하고, 자주 변경되는 게임플레이 디테일이나 디자이너 조정 영역은 블루프린트로 처리합니다.

C++ 클래스를 블루프린트에서 상속해 사용하면 두 방식의 장점을 동시에 취할 수 있습니다.


C++ 클래스 생성 및 기본 구조

첫 번째 C++ 클래스를 생성하며 실습을 시작합니다.

이번에는 간단한 메시를 표시하고 특정 텍스트를 출력하는 액터를 구현합니다.

언리얼 에디터에서 C++ 클래스 생성: 언리얼 에디터 상단 메뉴에서 Tools (도구) > New C++ Class... (새 C++ 클래스...)를 클릭합니다.

부모 클래스 선택: 부모 클래스 선택(Choose Parent Class) 창이 나타나면, 우리는 3D 월드에 배치될 수 있는 객체를 만들 것이므로 Actor를 선택하고 다음(Next)을 클릭합니다.

이름 지정 및 생성: 마법사에는 접두사 없이 MyAwesomeActor를 입력하고, 저장 경로를 확인한 후 클래스 생성(Create Class)을 클릭합니다.

생성되는 파일은 MyAwesomeActor.h와 MyAwesomeActor.cpp이며, C++ 클래스 이름은 AMyAwesomeActor입니다. IDE가 자동으로 열리지 않으면 프로젝트에서 이 두 파일을 엽니다.

이제 생성된 파일의 기본 구조를 살펴보겠습니다.

아래 예제는 MyFirstCPPProject 모듈의 새 클래스를 기준으로 합니다. 프로젝트 이름이 다르면 헤더의 MYFIRSTCPPPROJECT_API는 마법사가 만든 실제 모듈의 API 매크로를 유지합니다. 앞 절에서 선언만 살펴본 코드를 같은 클래스에 중복 붙이지 말고, 이 실습의 헤더와 cpp 정의를 한 쌍으로 구성합니다.

MyAwesomeActor.h
#pragma once

#include "CoreMinimal.h"
#include "GameFramework/Actor.h"
#include "Components/StaticMeshComponent.h" // 추가할 컴포넌트를 위해 미리 포함
#include "MyAwesomeActor.generated.h" // 항상 마지막

UCLASS()
class MYFIRSTCPPPROJECT_API AMyAwesomeActor : public AActor
{
    GENERATED_BODY()

public:
    // Sets default values for this actor's properties
    AMyAwesomeActor();

protected:
    // Called when the game starts or when spawned
    virtual void BeginPlay() override;

public:
    // Called every frame
    virtual void Tick(float DeltaTime) override;

    // StaticMeshComponent를 위한 포인터 선언
    UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Components")
    UStaticMeshComponent* MyStaticMesh;

    // 블루프린트에서 변경 가능하도록 노출할 속성
    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Custom Properties")
    FString WelcomeMessage;

    // 블루프린트에서 호출할 수 있는 함수
    UFUNCTION(BlueprintCallable, Category = "Custom Functions")
    void PrintWelcomeMessage();
};
MyAwesomeActor.cpp
// MyAwesomeActor.cpp
#include "MyAwesomeActor.h"
#include "Components/StaticMeshComponent.h" // StaticMeshComponent를 위해 다시 포함 (보통 헤더에서 미리 함)
#include "Engine/Engine.h" // GEngine->AddOnScreenDebugMessage를 위해 포함

// Sets default values
AMyAwesomeActor::AMyAwesomeActor()
{
    // 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;

    // UStaticMeshComponent 생성 및 Root Component로 설정
    MyStaticMesh = CreateDefaultSubobject<UStaticMeshComponent>(TEXT("MyMesh"));
    RootComponent = MyStaticMesh;

    // 변수 기본값 설정
    WelcomeMessage = TEXT("Hello from C++!");
}

// Called when the game starts or when spawned
void AMyAwesomeActor::BeginPlay()
{
    Super::BeginPlay();

    // BeginPlay 시 메시 출력
    PrintWelcomeMessage();
}

// Called every frame
void AMyAwesomeActor::Tick(float DeltaTime)
{
    Super::Tick(DeltaTime);

}

void AMyAwesomeActor::PrintWelcomeMessage()
{
    if (GEngine)
    {
        GEngine->AddOnScreenDebugMessage(-1, 5.f, FColor::Green, WelcomeMessage);
    }
    UE_LOG(LogTemp, Warning, TEXT("%s"), *WelcomeMessage);
}

몇 가지 주요 변경사항과 설명을 덧붙이자면:

  • UStaticMeshComponent 추가: 액터에 시각적인 형태를 부여하기 위해 UStaticMeshComponent를 추가했습니다. 생성자에서 CreateDefaultSubobject를 사용하여 컴포넌트를 생성하고, RootComponent로 설정하여 액터의 위치와 회전을 결정하는 기준점으로 만들었습니다.
  • WelcomeMessage (UPROPERTY): 블루프린트에서 편집 가능하도록 UPROPERTY(EditAnywhere, BlueprintReadWrite)로 선언했습니다. 초깃값은 생성자에서 Hello from C++!로 설정했습니다.
  • PrintWelcomeMessage (UFUNCTION): 블루프린트에서 호출 가능하도록 UFUNCTION(BlueprintCallable)로 선언했습니다. 이 함수는 화면에 디버그 메시지를 출력하고, 로그에도 기록합니다.
  • #include "Engine/Engine.h": GEngine 전역 객체에 접근하여 화면에 메시지를 출력하기 위해 이 헤더를 포함해야 합니다.

MyStaticMesh의 VisibleAnywhere와 BlueprintReadOnly는 컴포넌트 포인터의 편집·대입 권한입니다. 참조하는 컴포넌트 내부의 메시·머티리얼 설정까지 모두 읽기 전용이 된다는 뜻은 아닙니다.


C++ 클래스 컴파일

코드를 수정한 후에는 반드시 컴파일해야 합니다.

처음 클래스 구조를 작성하는 이 실습에서는 에디터를 닫고 IDE에서 프로젝트의 Development Editor 타깃을 빌드한 뒤 다시 엽니다. 실행 중 코드 수정에는 Live Coding을 사용할 수 있지만, 기존 인스턴스의 생성자 기본값이 모두 갱신된다고 가정하지 않습니다.


C++ 클래스 기반으로 블루프린트 클래스 생성

이제 C++ 클래스를 컴파일했으니, 이를 기반으로 블루프린트 클래스를 생성합니다.

콘텐츠 브라우저에서 생성: 언리얼 에디터의 콘텐츠 브라우저에서 원하는 폴더(예: Content/Blueprints)로 이동합니다.

빈 공간에서 마우스 오른쪽 버튼을 클릭하여 컨텍스트 메뉴를 엽니다.

블루프린트 클래스 선택: Blueprint Class (블루프린트 클래스)를 선택합니다.

부모 클래스 선택 (중요!): 모든 클래스(All Classes) 섹션에서 MyAwesomeActor를 검색해 방금 만든 C++ 부모 클래스(타입명 AMyAwesomeActor)를 선택하고 선택(Select)을 클릭합니다.

이름 지정: 새로운 블루프린트 클래스의 이름을 BP_MyAwesomeActor로 지정합니다. (블루프린트 클래스는 BP_ 접두사를 붙이는 것이 일반적인 컨벤션입니다.)


블루프린트에서 C++ 속성 및 함수 확인/수정

이제 BP_MyAwesomeActor 블루프린트 클래스를 더블 클릭하여 블루프린트 에디터를 엽니다.

디테일 패널 확인: 블루프린트 에디터의 좌측 상단 컴포넌트(Components) 패널에서 MyStaticMesh 컴포넌트가 추가된 것을 확인할 수 있습니다.

이를 선택하면 디테일 패널에서 메시나 머티리얼을 할당할 수 있습니다.

그다음 Class Defaults(클래스 기본값)를 선택해 액터의 디테일(Details)을 봅니다.

Custom Properties 카테고리 아래에 우리가 C++에서 선언한 WelcomeMessage 변수가 보이는 것을 확인할 수 있습니다!

이 변수의 기본값을 여기서 자유롭게 변경할 수 있습니다.

예를 들어 Hello from Blueprint!로 바꿔 보세요.

그래프 편집기에서 함수 확인: 이벤트 그래프(Event Graph)에서 우클릭 후 컨텍스트 메뉴를 열고, Print Welcome Message를 검색합니다.

함수가 노드로 나타나면 블루프린트에서도 직접 호출할 수 있습니다. 다만 원문 C++의 BeginPlay가 이미 한 번 호출하므로, Event BeginPlay에도 연결하면 별도의 호출이 추가되어 메시지가 중복될 수 있습니다.


레벨에 배치 및 테스트

블루프린트 클래스까지 만들고 내용을 확인했으니, 이제 레벨에 배치하고 테스트해 봅시다.

레벨에 블루프린트 배치: 콘텐츠 브라우저에서 BP_MyAwesomeActor 블루프린트 에셋을 드래그하여 뷰포트에 놓습니다.

Static Mesh 할당 (선택 사항): 뷰포트에서 BP_MyAwesomeActor 인스턴스를 선택한 후, 디테일 패널에서 MyStaticMesh 컴포넌트를 선택합니다.

Static Mesh 항목에 원하는 스태틱 메시(예: Shape_Cube 등 사용 가능한 기본 메시)를 할당합니다. 메시를 할당하지 않아도 로그 실습은 가능하지만, 컴포넌트 생성만으로 화면에 3D 형상이 나타나지는 않습니다.

게임 플레이: 언리얼 에디터 상단의 Play (플레이) 버튼을 클릭하여 게임을 실행합니다.

화면 메시지와 Output Log에서 시험한 인스턴스의 WelcomeMessage를 확인합니다. 값이 예상과 다르면 다음 세 위치를 구분합니다.

UNREAL C++ · CH2-3

WelcomeMessage는 어느 객체의 값인가

C++ 생성자의 초기값, 블루프린트 클래스 기본값, 배치한 인스턴스의 편집값을 구분해 Play에서 확인합니다.

C++ 부모와 Blueprint 기본값 및 두 인스턴스의 값4노드3관계:C++부모를BP자식이상속하는점선은BP→C++이며BP기본값을기준으로A/B두인스턴스의값이정해진다.A만별도HelloA편집,B는HelloBP기본값유지.클래스상속·초기값기준이지함수의실행순서나A→B전달이아니다.C++ 부모Hello from C++!BP 자식 · 기본값Hello from Blueprint!상속인스턴스 AHello from A!별도 편집인스턴스 BHello from Blueprint!별도 편집 없음기본값을 기준으로 배치
C++ 부모와 Blueprint 기본값 및 두 인스턴스의 값4노드3관계:C++부모를BP자식이상속하는점선은BP→C++이며BP기본값을기준으로A/B두인스턴스의값이정해진다.A만별도HelloA편집,B는HelloBP기본값유지.클래스상속·초기값기준이지함수의실행순서나A→B전달이아니다.C++ 부모Hello from C++!BP 자식 · 기본값Hello fromBlueprint!점선: 상속기본값 기준인스턴스 AHello from A!별도 편집인스턴스 BHello fromBlueprint!B: 별도 편집 없음A → B 전달 없음
WelcomeMessage가 정해지는 위치
설정 위치이 예제의 값확인할 대상
C++ 생성자Hello from C++!새 객체의 기본값을 초기화하는 생성자 코드
블루프린트 기본값직접 바꿨다면 Hello from Blueprint!BP_MyAwesomeActor의 Class Defaults
배치한 인스턴스별도로 편집했다면 그 인스턴스의 값월드 아웃라이너에서 선택한 액터의 Details
C++ 생성자
이 예제의 값: Hello from C++!
확인할 대상: 새 객체의 기본값을 초기화하는 생성자 코드
블루프린트 기본값
이 예제의 값: 직접 바꿨다면 Hello from Blueprint!
확인할 대상: BP_MyAwesomeActor의 Class Defaults
배치한 인스턴스
이 예제의 값: 별도로 편집했다면 그 인스턴스의 값
확인할 대상: 월드 아웃라이너에서 선택한 액터의 Details

부모 기본값을 바꿔도 이미 덮어쓴 인스턴스 값이 자동으로 같아지지는 않습니다. BeginPlay에서 출력할 실제 인스턴스를 먼저 선택합니다.

예시: 두 인스턴스의 값을 따로 확인하기

BP_MyAwesomeActor의 Class Defaults를 Hello from Blueprint!로 저장·컴파일하고 새 인스턴스 A와 B를 배치합니다. A에서만 WelcomeMessage를 Hello from A!로 편집하고 B는 별도로 편집하지 않습니다. 추가 Blueprint 호출 없이 원 C++의 BeginPlay만 실행하면 A는 자신의 값 Hello from A!, B는 상속한 기본값 Hello from Blueprint!를 출력합니다. 둘 다 같은 C++ 함수를 호출하지만 읽는 객체가 다릅니다. 메시 없는 경우에도 로그를 확인할 수 있으며, 화면 디버그 표시 자체가 비활성인 상황과 함수 미호출을 구별합니다.

독립 확인

같은 두 인스턴스에서 Event BeginPlay에도 Print Welcome Message 호출 노드를 한 번씩 연결했습니다. 다른 호출과 재스폰은 없고 C++ BeginPlay의 기존 호출도 유지됩니다. 함수의 로그 기록은 전체 몇 번 발생하며, 각 인스턴스에서 어느 문구가 기록될까요? 중복 로그를 하나로 줄이려면 어느 호출 경로를 확인해야 할까요?

값과 호출 수의 풀이

각 인스턴스에서 C++ 호출 1회와 Blueprint 추가 호출 1회가 있으므로 합계는 2 × 2 = 4회입니다. A의 문구는 두 번 모두 Hello from A!, B의 문구는 두 번 모두 Hello from Blueprint!입니다. 같은 함수 노드를 추가했다고 기존 C++ 호출을 대체하지 않습니다. 이 실습에서 한 번만 출력하려면 추가한 Blueprint 호출 경로를 제거해 원 BeginPlay 호출만 남깁니다.

다음 절에서는 BeginPlay와 Tick을 게임 루프 및 클래스의 책임과 연결합니다.