본문 바로가기

Level Blueprint vs C++ 스크립트

레벨 전용 연출은 Level Blueprint에, 재사용할 게임 로직은 C++에 배치하는 선택 기준을 세웁니다.

레버를 당기면 문이 열리는 장면을 하나 만든다고 해 봅시다. 가장 빠른 방법은 레벨 블루프린트(Level Blueprint)를 열어 레버와 문을 끌어다 놓고 노드 몇 개로 잇는 것입니다. 그런데 비슷한 문이 다섯 레벨에 하나씩 생기면 같은 그래프를 다섯 번 복사하게 되고, 잠금 규칙이 바뀌면 다섯 곳을 모두 고쳐야 합니다. 반대로 한 레벨에서 한 번만 쓰는 컷신 순서까지 모두 C++ 클래스로 만들면, 사소한 연출 하나를 바꿀 때마다 빌드해야 합니다.

씬 구성 요소 - Light, Camera, Mesh에서 레벨을 채우는 요소를 보았으니, 이 글에서는 그 레벨 안의 로직을 레벨 블루프린트와 C++ 클래스 가운데 어디에 둘지를 비교합니다. 두 방식의 장점과 한계를 살펴보고, 레벨에 묶인 연결과 여러 곳에서 다시 쓸 규칙을 나누는 기준을 세웁니다.


레벨 블루프린트: 한 레벨에만 있는 그래프

레벨 블루프린트는 특정 레벨(맵) 안에서만 동작하는 전용 블루프린트로, 그 레벨의 LevelScriptActor와 함께 동작합니다. 레벨 스트리밍을 쓰면 여러 레벨이 함께 불러와질 수 있으며, 같은 레벨이 불러와져 있는 동안에는 플레이어가 다른 구역으로 갔다고 그 레벨 블루프린트가 곧바로 사라지지는 않습니다.

레벨 블루프린트는 그 레벨에서만 필요한 이벤트나 상호작용에 잘 맞습니다. 특정 지점에 도착하면 문이 열리거나 레벨이 시작될 때 컷신이 재생되는 것처럼 한 번 일어나는 이벤트가 대표적입니다. 레벨에 배치된 액터를 직접 참조하기도 쉽습니다. 아웃라이너에서 배치한 액터를 그래프로 끌어오거나, 액터를 선택한 채 그래프의 참조 생성 메뉴를 쓰면 됩니다. 노드 기반이라 프로그래밍 경험이 적은 아티스트나 기획자도 참여하기 쉽고, C++처럼 빌드를 기다리지 않고 바로 바꿔 시험해 볼 수 있어 빠른 프로토타이핑에 유리합니다.

하지만 한계도 분명합니다. 레벨 블루프린트의 로직은 그 레벨에 묶여 있어서, 다른 레벨에서 쓰려면 복사해야 하고 복사본이 늘수록 유지 보수가 어려워집니다. 공통 부분은 블루프린트 클래스, 컴포넌트, 함수 라이브러리, 또는 C++로 분리할 수 있습니다. 로직이 복잡해지고 노드가 많아지면 그래프를 보고 흐름을 파악하기 어려워지는데, 실행 비용은 노드의 개수가 아니라 호출 빈도와 실제로 하는 일을 측정해 판단합니다.

또 블루프린트는 텍스트가 아닌 에셋 파일로 저장되므로, Git이나 Perforce 같은 버전 관리 시스템에서 두 사람이 같은 그래프를 고쳤을 때 충돌을 해결하기 어렵습니다. 런타임 성능도 C++ 코드보다 떨어질 수 있지만, 대부분의 게임 로직에서는 큰 문제가 되지 않습니다.

그래서 레벨 블루프린트는 레벨이 시작하거나 끝날 때의 이벤트, 특정 트리거 볼륨에 들어갔을 때의 이벤트, 그 레벨에 고정된 문과 엘리베이터, 퍼즐 요소의 직접 제어, 날씨 변화나 배경 음악 전환 같은 분위기 연출, 그리고 C++로 만들기 전에 기능을 빨리 시험해 볼 때 씁니다.


C++ 클래스: 어느 레벨에서나 쓰는 단위

C++ 방식은 지금까지 해 온 것처럼 AActor나 UActorComponent 같은 클래스를 C++로 직접 정의해 게임 로직을 만드는 방식입니다. C++ 클래스의 코드는 Source의 헤더와 소스 파일에 있고, 그 클래스를 상속한 블루프린트 에셋은 Content의 .uasset 파일로 저장됩니다. 인스턴스를 만드는 방법은 액터인지 컴포넌트인지 같은 부모 타입에 따라 다릅니다.

C++ 클래스는 특정 레벨에 묶이지 않는 독립된 단위라서, 한 번 만들면 여러 레벨의 여러 액터에서 인스턴스를 만들어 쓸 수 있습니다. 컴파일된 코드로 실행되므로 복잡한 연산이나 자주 불리는 로직에서는 블루프린트 가상 머신이 노드를 하나씩 실행하는 비용을 줄일 수 있습니다. 다만 실제 병목이 렌더링이나 입출력(I/O) 같은 다른 곳에 있다면 언어를 바꾸는 것만으로는 해결되지 않습니다.

C++로는 엔진 깊숙한 부분까지 접근해 고치고 제어할 수 있고, 엄격한 타입 검사와 분명한 문법 덕분에 큰 프로젝트에서 코드의 구조와 규칙을 일관되게 유지하기 좋습니다. 텍스트 파일이므로 버전 관리 시스템에서 충돌을 해결하기도 비교적 쉽습니다. 또 UCLASS, UPROPERTY, UFUNCTION 매크로로 기능을 블루프린트에 공개할 수 있어, C++의 장점과 블루프린트의 유연함을 함께 쓸 수 있습니다.

대신 코드를 고칠 때마다 빌드해야 하므로 작은 변경에도 시간이 걸리고, C++ 언어 자체의 복잡함과 언리얼의 방대한 API를 이해해야 합니다. 프로그래밍을 하지 않는 팀원이 직접 고치기도 어렵습니다.

그래서 C++는 캐릭터 이동, 공격, 인벤토리, AI, 충돌 처리 같은 핵심 게임플레이 시스템, 체력이나 상호작용처럼 여러 액터에 붙여 쓸 컴포넌트, 물리나 복잡한 수학 계산 같은 성능이 중요한 로직, 엔진의 기존 기능을 재정의하거나 새 기능을 더하는 일, 그리고 구조와 협업 규칙을 분명히 해야 하는 큰 프로젝트에 씁니다.


레벨의 연결과 다시 쓸 규칙 나누기

UNREAL C++ · CH4-2

레벨의 연결과 재사용할 규칙을 분리합니다

예를 들어 여러 문이 같은 상호작용 규칙을 쓰더라도, 이 레벨에서 어느 문을 어떤 순서로 여는지는 별도 책임입니다. 재사용 가능성은 구현 언어만으로 결정되지 않습니다.

레벨별 배치 참조와 재사용할 문 동작숲 레벨의 연결은 문 F 인스턴스를 호출하고, 도시 레벨의 연결은 문 C 인스턴스를 호출한다. 두 문은 같은 클래스의 열기 규칙을 각자의 상태에 적용한다. 공통 규칙 노드는 같은 구현 정의를 뜻하며 하나의 공유 문 상태가 아니다. Blueprint Class 또는 C++로 구현할 수 있다.숲 레벨 연결레버 이벤트문 F 인스턴스F의 잠금 · 열림 상태도시 레벨 연결퍼즐 완료 이벤트문 C 인스턴스C의 잠금 · 열림 상태공통 열기 규칙같은 클래스의 동작각 상태에 적용
레벨별 배치 참조와 재사용할 문 동작숲 레벨의 연결은 문 F 인스턴스를 호출하고, 도시 레벨의 연결은 문 C 인스턴스를 호출한다. 두 문은 같은 클래스의 열기 규칙을 각자의 상태에 적용한다. 공통 규칙 노드는 같은 구현 정의를 뜻하며 하나의 공유 문 상태가 아니다. Blueprint Class 또는 C++로 구현할 수 있다.숲 레벨 연결레버 이벤트문 F 인스턴스F의 잠금 · 열림 상태도시 레벨 연결퍼즐 완료 이벤트문 C 인스턴스C의 잠금 · 열림 상태공통 열기 규칙같은 클래스의 동작각 상태에 적용

선은 특정 문에 동작을 요청하고 그 인스턴스가 같은 구현 규칙을 사용하는 관계입니다. F와 C 사이의 호출이나 자동 상태 공유는 없습니다.

레벨에 종속된 연결

이 맵의 트리거·문 인스턴스 참조와 퍼즐 순서·컷씬을 연결합니다.

Level Blueprint는 그 레벨의 LevelScriptActor와 함께 동작합니다. 스트리밍으로 여러 레벨이 함께 로드될 수도 있습니다.

여러 인스턴스가 공유할 규칙

상호작용 검사·열기 동작·체력 계산처럼 반복되는 규칙을 클래스·컴포넌트·함수 라이브러리로 분리합니다.

Blueprint Class 또는 C++로 구현할 수 있습니다. 측정된 병목·코드 구조·편집 책임을 보고 방식을 선택합니다.

연결 예시 · 배치 참조 → 공개된 동작

레벨의 퍼즐 순서가 배치된 문 인스턴스를 선택하고, 문이 제공하는 상호작용 함수를 호출합니다. 문 내부 규칙은 다른 레벨에서도 재사용합니다.

호출할 함수와 조정할 값의 공개 범위를 명시합니다. C++ 기반 클래스를 Blueprint 에셋으로 확장해 데이터를 조정하는 방법도 함께 사용합니다. 언어를 바꾸는 것만으로 렌더링·I/O 병목이 해결되지는 않습니다.

위 그림은 여러 문이 같은 상호작용 규칙을 쓰더라도, 각 레벨에서 어느 문을 어떤 순서로 여는지는 따로 정해야 하는 일임을 보여 줍니다. 숲 레벨의 연결은 레버 이벤트가 일어나면 문 F 인스턴스를 부르고, 도시 레벨의 연결은 퍼즐 완료 이벤트가 일어나면 문 C 인스턴스를 부릅니다. 두 문은 같은 클래스의 공통 열기 규칙을 쓰지만, 그 규칙을 각자의 잠금, 열림 상태에 적용합니다. 그림의 공통 규칙 노드는 같은 구현을 뜻할 뿐 하나의 공유된 문 상태가 아니며, F와 C 사이에는 호출도 자동 상태 공유도 없습니다.

그림의 아래쪽은 이 구분을 두 칸으로 정리합니다. 레벨에 묶인 연결은 그 맵의 트리거와 문 인스턴스 참조, 퍼즐 순서와 컷신을 잇는 일입니다. 여러 인스턴스가 함께 쓸 규칙은 상호작용 검사, 열기 동작, 체력 계산처럼 되풀이되는 규칙으로, 클래스나 컴포넌트, 함수 라이브러리로 분리합니다. 이 규칙은 블루프린트 클래스로도 C++로도 만들 수 있으므로, 측정한 병목과 코드 구조, 누가 편집할지를 보고 방식을 고릅니다. 마지막 칸의 예처럼 레벨의 퍼즐 순서는 배치된 문 인스턴스를 골라 문이 공개한 상호작용 함수를 부르고, 문 내부의 규칙은 다른 레벨에서도 그대로 다시 씁니다.

구체적인 예로 따라가 봅시다. 숲 레벨에는 문 F를, 도시 레벨에는 문 C를 배치하고 둘 다 같은 문 클래스를 씁니다. 문 클래스에는 잠금 여부를 확인하고 문을 여는 동작을 두고, 숲에서는 레버를 당겼을 때 F를, 도시에서는 퍼즐을 풀었을 때 C를 부릅니다. 문 클래스를 다시 쓴다고 해서 F와 C의 현재 잠금, 열림 상태가 하나로 합쳐지지는 않습니다. 레벨의 연결은 실제 인스턴스를 골라 공개된 동작을 부르고, 문 안의 공통 규칙은 그 인스턴스의 상태에 적용됩니다.

여기서 중요한 점은 다시 쓸 수 있는지가 구현 언어로 정해지지 않는다는 것입니다. 블루프린트 클래스도 여러 레벨에서 다시 쓸 수 있고, C++로 만들어도 레벨의 특정 인스턴스를 직접 참조하면 그 레벨에 묶입니다. 반복되는 기능이 생기면 먼저 레벨 전용 참조와 다시 쓸 규칙을 나누고, 그다음에 규칙을 어떤 언어로 만들지 고릅니다.

월드 파티션(World Partition)을 쓰는 월드라면 한 가지를 더 확인합니다. 레벨 블루프린트가 직접 참조한 액터는 항상 불러오는 액터(Always Loaded)로 표시되어, 플레이어의 위치와 관계없이 불러와진 상태로 남습니다. 재사용할 코드를 분리하는 것과 스트리밍에 영향을 주는 참조를 줄이는 것은 서로 다른 확인 사항이며, 월드 파티션은 레벨 스트리밍과 World Partition에서 자세히 다룹니다.


C++와 블루프린트를 함께 쓰기

언리얼 개발에서 가장 흔한 작업 방식은 C++와 블루프린트의 장점을 섞어 쓰는 것입니다. C++로는 게임의 핵심 로직과 기반 시스템, 성능이 중요한 부분, 여러 곳에서 다시 쓸 유틸리티 함수와 클래스를 만들고, UPROPERTY와 UFUNCTION으로 블루프린트에 공개합니다. 블루프린트로는 그 기반 위에 게임 콘텐츠와 특정 레벨의 이벤트, 기획자나 아티스트가 직접 조정할 데이터와 로직을 만들고, 빠르게 프로토타이핑합니다. C++ 클래스를 상속해 기능을 넓히거나, 레벨 블루프린트에서 C++ 액터를 제어하는 식입니다.

예를 들어 캐릭터 이동은 ACharacter와 UCharacterMovementComponent를 바탕으로 C++에서 걷기, 뛰기, 점프 같은 기본 로직을 만듭니다. 공격은 C++로 UWeaponComponent를 만들어 발사 로직과 피해 계산 같은 핵심 기능을 넣습니다. UI는 APlayerController의 C++ 코드에서 위젯을 만들고 초기화하되, 위젯 안의 버튼 클릭 같은 이벤트는 블루프린트에서 처리합니다. 특정 레벨의 퍼즐은 C++로 상호작용 기능을 제공하는 UInteractableComponent를 만들고, 레벨 블루프린트에서 그 컴포넌트를 가진 액터들을 직접 참조해 퍼즐의 순서와 결과를 만듭니다.

이렇게 나누면 안정적이고 빠른 기반 위에서 콘텐츠를 빠르게 더하고 바꿀 수 있습니다. 구현 위치를 정할 때는 다음 네 가지를 물어봅니다.

  • 이 로직이 특정 레벨의 배치 참조와 연출 순서에 묶여 있는가?
  • 여러 인스턴스에서 같은 규칙이 되풀이되어 공통 클래스나 컴포넌트로 뺄 수 있는가?
  • 측정한 병목이 있어서 옮기려는 것인가, 아니면 편집하기 편하려고 옮기려는 것인가?
  • 기획자가 조정할 값과 부를 함수를 어디까지 공개할 것인가?

정리

  • 레벨 블루프린트는 한 레벨에서만 동작하며, 그 레벨의 배치 액터를 쉽게 참조하고 빠르게 고칠 수 있어 레벨 전용 이벤트와 연출에 알맞습니다.
  • 레벨 블루프린트의 로직은 다른 레벨에서 다시 쓰기 어렵고, 에셋 파일이라 버전 관리 충돌을 해결하기 어렵습니다.
  • C++ 클래스는 어느 레벨에서나 다시 쓸 수 있고, 자주 불리는 로직의 실행 비용을 줄이며, 구조와 버전 관리에 유리하지만 빌드 시간과 학습 비용이 듭니다.
  • 다시 쓸 수 있는지는 구현 언어가 아니라 레벨 전용 참조와 공통 규칙을 나눴는지로 정해집니다. 공통 규칙은 블루프린트 클래스로도 C++로도 만들 수 있습니다.
  • 같은 클래스의 인스턴스는 규칙을 공유할 뿐, 잠금이나 열림 같은 상태는 각자 가집니다.
  • 월드 파티션에서는 레벨 블루프린트가 직접 참조한 액터가 항상 불러오는 액터로 표시된다는 점도 확인합니다.

연습 문제

1. 다섯 레벨의 문

다섯 레벨의 문이 모두 같은 잠금 검사를 합니다. 그 가운데 두 레벨에는 서로 다른 컷신이 끝난 뒤 특정 문을 여는 순서가 있습니다. 잠금 검사와 컷신에 쓰는 배치 참조를 모두 레벨 블루프린트에 다섯 번 복사해야 할까요? 이 조건만으로 공통 문 규칙을 반드시 C++로 바꿔야 할까요?

해설 보기

복사할 필요가 없습니다. 되풀이되는 잠금 검사는 문 클래스나 컴포넌트의 동작으로 모으고, 각 맵의 컷신 순서와 문 인스턴스 연결만 그 레벨의 레벨 블루프린트에 둡니다.

블루프린트 클래스도 규칙을 다시 쓸 수 있으므로, 이 조건만으로 C++로 바꾸는 것이 필수는 아닙니다. 측정한 병목, 코드 구조, 누가 편집할지를 보고 구현 방식을 고릅니다. 어느 쪽이든 레벨에서 부를 함수와 조정할 값에는 용도에 맞는 블루프린트 접근 지정자를 함께 정합니다.

2. 한 문을 열었더니

숲 레벨에서 레버를 당겨 문 F를 열었습니다. 같은 문 클래스를 쓰는 도시 레벨의 문 C도 열린 상태가 될까요?

해설 보기

되지 않습니다. F와 C는 같은 클래스의 서로 다른 인스턴스이므로, 열기 규칙은 함께 쓰지만 잠금과 열림 상태는 각자 가집니다. F에서 일어난 일이 C의 상태를 바꾸지 않으며, 두 문 사이에는 호출도 자동 상태 공유도 없습니다.

여러 문이 같은 상태를 공유해야 한다면 그 상태를 문 바깥의 다른 객체에 두고, 문들이 그 객체를 참조하도록 따로 설계해야 합니다.

레벨 블루프린트로 할 수 있는 일과 여는 방법은 Epic의 Level Blueprint 문서에서 확인할 수 있습니다.

다음 글 액터 생성과 관리에서는 C++에서 액터를 실행 중에 스폰하고, 찾고, 파괴하는 방법과 그때 지켜야 할 수명 규칙을 살펴봅니다.