▣ C++ 클래스 장점
- 빠른 런타임 퍼포먼스: 일반적으로 C++ 로직은 블루프린트 로직보다 훨씬 빠릅니다. 이유는 다음과 같습니다.
- 명확한 디자인: C++ 에서 변수나 함수를 노출하면 세밀한 제어를 통해 원하는 것을 정확히 노출할 수 있으므로, 특정 함수/변수를 보호하고 클래스의 공식 "API"를 만들 수 있습니다. 따라서 지나치게 크고 따라잡기 어려운 블루프린트를 만들지 않아도 됩니다.
- 광범위한 액세스: C++ 에서 정의(하고 제대로 노출)한 함수와 변수는 다른 모든 시스템에서 액세스할 수 있어, 여러 시스템 사이 정보 전달에 완벽합니다. 또한, C++ 에는 블루프린트보다 많은 엔진 함수 기능이 노출되어 있습니다.
- 더 많은 데이터 제어: C++ 에서는 데이터 저장과 로드 관련해서 구체적인 함수 기능을 더 많이 사용할 수 있습니다. 버전 변경 및 시리얼라이즈 처리를 다양한 사용자 지정 방식으로 처리할 수 있습니다.
- 네트워크 리플리케이션: 블루프린트의 리플리케이션 지원은 간단하며 작은 게임이나 고유한 일회성 액터에 사용하도록 설계되었습니다. 리플리케이션 대역폭이나 타이밍같은 것을 엄격하게 제어해야 하는 경우 C++ 를 사용해야 합니다.
- 강력한 연산력: 블루프린트로 복잡한 수학 연산을 하는 것은 어렵고 약간 느릴 수도 있습니다. 복잡한 수학 연산은 C++ 를 고려하세요.
- 쉬운 Diff/Merge: C++ 코드와 데이터는 (구성 및 커스텀 솔루션을 포함해서) 텍스트로 저장되므로, 여러 브랜치에서의 동시 작업이 쉽습니다.
▣ 블루프린트 클래스 장점
- 빠른 생성: 대부분의 경우 블루프린트 클래스를 새로 만들어 변수와 함수를 추가하는 것이 비슷한 작업을 C++ 로 하는 것보다 빠릅니다. 그래서 완전 새로운 시스템의 프로토타입을 만드는 작업은 보통 블루프린트로 하는 것이 빠릅니다.
- 빠른 반복처리: 블루프린트 로직을 수정하고 에디터 안에서 미리보는 것이 핫 리로드 기능이 있더라도 게임을 다시 컴파일하는 것보다 훨씬 빠릅니다. 성숙한 시스템은 물론 새로운 시스템에서도 마찬가지이므로 "미세조정" 가능한 모든 값은 가급적 애셋에 저장해야 합니다.
- 원활한 흐름: C++ 로 "게임 흐름"을 그려 보는 것은 복잡할 수 있으므로, 보통 블루프린트로 (또는 딱 그 용도로 설계된 비헤이비어 트리같은 커스텀 시스템으로) 구현하는 것이 낫습니다. 딜레이 및 비동기 노드는 C++ 델리게이트보다 흐름을 따라잡기 훨씬 쉽습니다.
- 유연한 편집: 별도의 기술 훈련을 받지 않은 디자이너와 아티스트도 블루프린트를 생성하고 편집할 수 있으므로, 엔지니어 이외에도 수정해야 하는 애셋은 블루프린트가 이상적입니다.
- 쉬운 데이터 사용: 블루프린트 클래스 안에 데이터를 저장하는 것은 C++ 안에 저장하는 것보다 훨씬 간단하고 안전합니다. 블루프린트는 데이터와 로직이 밀접하게 섞인 클래스에 좋습니다.
블루프린트에서 C++ 로 변환
블루프린트는 생성 및 반복처리 작업이 쉬우므로,
블루프린트에서 프로토타입을 만든 후 그 기능의 일부 또는 전부를 C++ 로 옮기는 것이 일반적입니다.
보통 시스템의 기본 기능을 입증했고 다른 사람들이 원활하게 사용할 수 있도록 "강화"하고자 하는 "리팩터링 지점"에서 이 작업을 하는 것이 좋습니다.
이 시점에서, 어떤 클래스, 함수, 변수는 C++ 로 옮기고 어떤 것은 블루프린트에 남겨둘지 결정해야 합니다.
그 결정을 내리기 전, C++ 로 리팩터링하기 위해 거쳐야 하는 프로세스를 이해하는 것이 좋습니다.
일반적으로 첫 단계는 블루프린트 클래스가 상속할 "베이스" C++ 클래스 세트를 만드는 것입니다.
게임의 베이스 네이티브 클래스를 만들었으면 프로토타입 블루프린트의 부모를 새로운 네이티브 클래스로 변경합니다. 그 작업을 완료하면 블루프린트 클래스의 변수와 함수를 네이티브 C++ 로 옮기기 시작할 수 있습니다.
네이티브 클래스의 변수 또는 함수가 블루프린트 변수와 유형 및 이름이 같다면, 블루프린트를 로드할 때 그 레퍼런스가 자동 변환됩니다. 하지만 블루프린트로의 외부 레퍼런스가 네이티브 베이스 클래스를 가리키도록 변경하고 싶을 수 있습니다. 예를 들어 ActionRPG 샘플 작업을 하는 도중, DefaultEngine.ini 파일에 다음과 같은 블록을 추가했습니다.
[CoreRedirects]
+ClassRedirects=(OldName="BP_Item_C", NewName="/Script/ActionRPG.RPGItem", OverrideClassName="/Script/CoreUObject.Class")
+ClassRedirects=(OldName="BP_Item_Potion_C", NewName="/Script/ActionRPG.RPGPotionItem", OverrideClassName="/Script/CoreUObject.Class")
+ClassRedirects=(OldName="BP_Item_Skill_C", NewName="/Script/ActionRPG.RPGSkillItem", OverrideClassName="/Script/CoreUObject.Class")
+ClassRedirects=(OldName="BP_Item_Weapon_C", NewName="/Script/ActionRPG.RPGWeaponItem", OverrideClassName="/Script/CoreUObject.Class")
위 블록은 코어 리디렉트 시스템을 사용하여,
Blueprint BP Item C 로의 레퍼런스를 전부 새로운 네이티브 클래스 RPGItem 으로 변환합니다.
OverrideClassName 옵션이 필요한 이유는 이제 UBlueprintGeneratedClass 가 아닌 UClass 임을 알리기 위해서입니다. 부모변경 및 수정을 처음 한 이후에는 느려진 블루프린트 컴파일 문제를 고치고 게임의 모든 블루프린트를 다시 저장해야 할 것입니다.
목표는 블루프린트 경고 없이 리팩터링을 완료하는 것으로, 그래야 새 이슈가 생겼을 때 추적이 쉽습니다.
모든 것이 정상 작동하면 이제 수정 프로세스 도중 추가한 CoreRedirect 를 제거하고 ini 파일을 정리하면 됩니다.
퍼포먼스 고려사항
C++ 가 블루프린트에 비해 갖는 강점은 퍼포먼스입니다.
하지만 많은 경우 사실 블루프린트 퍼포먼스가 문제되지는 않습니다.
대체로 C++ 코드 한 줄을 실행하는 것보다 블루프린트의 개별 노드 하나를 실행하는 속도가 느리다는 것이 큰 차이점인데, 한 노드에서 한 번 실행하는 것은 C++ 에서 호출하는 것만큼 빠릅니다.
예를 들어 값싼 최상위 노드 몇 개에 값비싼 Physics Trace 함수를 호출하는 블루프린트 클래스가 있다면,
C++ 로 변환해도 퍼포먼스 향상이 크지 않습니다. 하지만 블루프린트 클래스에 빽빽한 루프가 많거나 펼치면 노드가 수백개나 되는 중첩 매크로가 많은 경우라면, 그 코드는 C++ 로 옮길 것을 생각해야 합니다.
퍼포먼스 문제가 매우 심한 것은 Tick(틱) 함수입니다.
블루프린트 틱은 네이티브 틱보다 훨씬 느릴 수 있으므로,
인스턴스가 많은 클래스에는 절대 틱을 사용하지 말아야 합니다.
대신 타이머나 델리게이트를 사용하여 블루프린트 클래스가 필요할 때만 일을 하도록 해야 합니다.
블루프린트 클래스에 퍼포먼스 문제가 있는지 알아내는 가장 좋은 방법은 프로파일러 툴 을 사용하는 것입니다.
프로젝트의 퍼포먼스 상황을 파악하려면, 먼저 블루프린트 클래스의 퍼포먼스 스트레스가 심한 (적을 한 무더기 스폰하는 등의) 상황을 만든 뒤, 프로파일러 툴을 사용하여 프로파일을 캡처합니다.
프로파일러 툴을 사용하면 Game Thread Tick (게임 스레드 틱)을 자세히 분석해서 트리를 펼쳐가며 문제가 되는 블루프린트 클래스를 찾을 수 있습니다 (같은 클래스의 모든 인스턴스는 하나의 그룹으로 묶여 있으니, 참고하세요).
블루프린트 클래스 안에서, 시간이 걸리는 블루프린트 함수를 확인할 수 있습니다. 그 함수를 펼칩니다.
대부분의 시간이 Self 에서 발생한다면, 블루프린트 오버헤드로 인해 퍼포먼스 손실이 있는 것입니다.
하지만 대부분의 시간이 그 함수 안에 중첩된 다른 네이티브 이벤트에서 발생한다면, 블루프린트 오버헤드 문제는 아닙니다.
블루프린트 네이티브화 를 통해 이러한 문제를 줄일 수 있지만, 몇 가지 단점이 있습니다.
첫째, 쿠킹 워크플로를 변경하므로 쿠킹된 게임의 반복처리 작업이 느려질 수 있습니다. 또한,
네이티브화된 블루프린트의 런타임 로직은 보통 블루프린트와 다르므로 게임의 특성에 따라 다른 작동방식 또는 버그가 생길 수 있습니다. 대부분의 블루프린트 기능은 네이티브화에 완벽 지원되지만, 명확하지 않은 몇 가지 예외가 있을 수 있습니다.
마지막으로, C++ 를 직접 변환한 것에 비해 퍼포먼스 향상이 크지 않을 수 있습니다.
네이티브화가 퍼포먼스 이슈를 전부 해결하지는 못하지만, 해법이 될 수 있는지 조사해 볼 수는 있을 것입니다.
아키텍처 노트
블루프린트와 C++ 를 같이 사용하여 게임을 만들면,
게임의 규모가 커지고 복잡해짐에 따라 여러가지 문제가 생길 것입니다.
프로젝트가 커지기 시작할 때 유념해야 할 몇 가지 사항입니다.
- 비싼 블루프린트로의 형변환 금지: 블루프린트 클래스 BP_B 에서 BP_A 로 형변환( 또는 함수나 다른 블루프린트의 변수형으로 선언)할 때마다 해당 블루프린트의 로드 종속성이 생깁니다. 그러면 BP_A 가 커다란 스태틱 메시 4 개와 사운드 20 개를 레퍼런싱하는 경우, 형변환이 실패한다 하더라도 BP_B 를 로드할 때마다 커다란 스태틱 메시 4 개와 사운드 20 개를 로드합니다. 바로 그 이유로 네이티브 베이스 클래스 또는 최소한의 블루프린트 베이스 클래스에 중요 함수와 변수만 정의하는 것이 필수입니다. 거기서 비싼 블루프린트를 자손 클래스로 만들어야 합니다.
- 순환 블루프린트 레퍼런스 금지: 순환 레퍼런스(, 즉 한 클래스가 다른 클래스를 레퍼런스하는 데 거기서 첫 클래스를 레퍼런스하는 경우)는 C++ 의 경우 헤더 파일이 있어서 문제가 되지 않습니다. 하지만 과도한 순환 블루프린트 레퍼런스는 에디터 로드 및 컴파일 시간을 악화시킬 수 있습니다. 위와 마찬가지로 비싼 자손 블루프린트로 형변환( 또는 변수 레퍼런스를 생성)하는 대신 C++ 클래스 또는 값싼 베이스 블루프린트 클래스로 형변환하여 개선할 수 있습니다.
- C++ 클래스에서 애셋 레퍼런스 금지: C++ 생성자에서 FObjectFinder 및 FClassFinder 를 사용한 애셋 레퍼런스는 가능하지만, 가급적 피해야 합니다. 이런 식의 애셋 레퍼런스는 프로젝트 시작 시 로드되므로, 레퍼런스가 실제 필요치 않은 경우 로드 시간 및 메모리 이슈가 발생합니다. 또 생성자에서 레퍼런스된 애셋은 삭제나 이름변경도 쉽지 않습니다. 일반적으로 C++ 에서 특정 스태틱 메시 레퍼런스를 만드는 것보다는 Game Data (게임 데이터) 애셋 또는 블루프린트 유형을 조금 생성한 뒤 애셋 매니저나 구성 파일을 사용하여 로드하는 것이 좋습니다.
- 스트링으로 애셋 레퍼런스 금지: C++ 클래스에서 애셋을 로드할 때 발생하는 이슈를 피하려면, C++ 의 LoadObject 같은 함수로 디스크의 특정 애셋을 수동 로드할 수 있습니다. 하지만 이 레퍼런스는 쿠커가 전혀 추적하지 못하므로 패키지 게임에서 문제가 생길 수 있습니다. 그래서 대신 C++ 클래스에서 FSoftObjectPath 또는 TSoftObjectPtr 유형을 사용하고, ini 또는 블루프린트 클래스에서 설정한 뒤, 요청 시 로드 또는 비동기 로드를 통해 로드해야 합니다.
- 사용자 구조체 및 열거형 주의: C++ 에서 정의한 열거형과 구조체는 C++ 에서도 블루프린트에서도 사용할 수 있지만, 사용자 구조체/열거형은 C++ 에서 사용할 수 없고 세이브 게임 섹션의 설명대로 수동 고칠 수도 없습니다. 보통 게임플레이 로직을 조금씩 더 많이 C++ 로 옮기게 되므로, 중요한 열거형과 구조체는 C++ 에서 구현하는 것이 좋습니다. 기본적으로 블루프린트 둘 이상이 사용하는 것이면 아마 네이티브 C++ 로 구현해야 할 것입니다.
- 네트워크 아키텍처 고려: 게임의 네트워크 아키텍처 특성은 클래스 구조에 큰 영향을 줍니다. 일반적으로 프로토타입은 네트워킹을 염두에 두고 만들지 않으므로, 뭔가 "진짜로" 리팩터링을 시작할 때 어떤 액터가 어떤 데이터를 리플리케이트할지 고려하기 시작해야 합니다. 반복처리를 어렵게 만드는 부분에 대한 결정을 잘 내려야 리플리케이트되는 데이터의 원활한 흐름을 만들 수 있습니다.
- 비동기 로드 고려: 게임의 크기가 커지면 처음에 모든 것을 로드하던 것에서 필요할 때 애셋을 로드하도록 해야 합니다. 이 시점에 도달하면, 하드 레퍼런스 를 소프트 레퍼런스 또는 PrimaryAssetId (프라이머리 애셋 ID)로 변환하기 시작해야 합니다. AssetManager (애셋 매니저)는 애셋 비동기 로드를 손쉽게 해주는 함수를 여럿 제공하며, 로우 레벨 함수를 제공하는 StreamableManager (스트리머블 매니저)도 노출되어 있습니다.
https://docs.unrealengine.com/4.27/ko/Resources/SampleGames/ARPG/BalancingBlueprintAndCPP/
'Unreal > Study' 카테고리의 다른 글
언리얼 엔진 이해하기 (0) | 2019.06.07 |
---|---|
유니티 - 언리얼 차이점 (0) | 2019.06.07 |
12. 스마트 포인터와 메모리 관리+ GC (0) | 2019.06.03 |
11 직렬화=시리얼라이제이션 + 로직 +FArchive (0) | 2019.05.31 |
10. 언리얼 C++ 딜리게이트 + 종류 + 바인딩 (0) | 2019.05.30 |