TECH.ART.FLOW.IO

[번역][연재물] 언리얼 엔진 개발 가이드. C++

jplee 2025. 4. 16. 02:59

Italink 군의 멋진 연재글을 공유 해 봅니다. 원문 링크는 글 맨 하단에 있습니다.


언리얼 엔진 C++ 프로그래밍 문서

서문

언리얼 엔진은 C++로 작성된 강력한 엔진이지만, 빌드 도구(UBT)와 리플렉션 컴파일러(UHT)의 존재로 인해 C++ 표준과는 독립적인 문법을 가지고 있어, 개발자들 사이에서는 U++라고 불립니다.
문법상의 차이뿐만 아니라 언리얼 엔진의 개발 프로세스도 일반적인 C++ 개발 프로세스와는 매우 다릅니다. 예를 들어, STL 표준 라이브러리는 도구 상자(Toolkit)와 같아서 우리가 이를 사용해 개발하는 반면, 언리얼 엔진은 개발 플랫폼(Platform)에 가까워서 우리가 이를 기반으로 개발합니다.
현재 U++ 관련 튜토리얼과 서적이 많지 않은데, 개발자들에게 유용한 주요 시리즈 글들은 다음과 같습니다:

  • 형태 없는 거인: 언리얼 엔진 프로그래밍 분석
  • UE4 개발 가이드 - Gamedev Guide
  • 순적연구실 - 차리펑
  • 언리얼 렌더링 시스템 분석 - 샹왕
  • InsideUE - 다자오

언리얼 엔진의 부족한 문서와 난해한 코드는 C++ 입문자들에게 넘을 수 없는 장벽과도 같습니다. 이는 언리얼 엔진과 같은 산업용 프로젝트에는 수많은 개발자가 참여하고 있으며, 코드의 규모가 방대하고 빠르게 변화하기 때문에 그 발전 과정을 정리하고 체계화하는 것이 매우 어려운 일이기 때문입니다.
저자는 몇몇 U++ 영상 강좌들을 살펴보았는데, 내용이 파편적이고 대부분 UE를 겉핥기식으로 다룬 C++ 기초 강좌에 불과했습니다. 소개된 내용도 제한적이고 많은 사각지대가 존재해서 학습 효과가 그다지 크지 않았습니다.

좋은 강좌를 발견하시면 댓글로 추천해 주세요~

언리얼 엔진을 다루기 위해서는 대규모 C++ 프로젝트의 엔지니어링 능력이 필요한데, 분명히 말씀드리자면 C++ 입문자에게는 적합하지 않습니다
만약 당신이 학생이고 우연히 언리얼 엔진이라는 놀라운 것을 발견해서 화려한 그래픽에 매료되어 U++ 개발을 배우고 싶다면, 문제 풀이와 논문 작성 외에도 다음과 같은 능력을 갖추고 있는지 생각해 보아야 합니다:

  • C++ 프로젝트 구축에 익숙함
  • 엔진의 각 서브모듈에 대한 이해가 있으며, 대규모 프로젝트 소스 코드를 읽을 수 있는 능력

만약 그렇지 않다면, 무작정 덤비기보다는 차분히 기초를 다지는 것을 추천드립니다
기초는 오랜 시간 갈고닦아야 단단해지며, 기술도 한 걸음 한 걸음 천천히 쌓아가야 점진적으로 발전할 수 있습니다. 지름길을 찾으려 하거나, 누군가가 이런저런 사람들을 자랑하는 것을 듣고 대충 시도해 보면 기술이 대단해질 것이라 생각하면 안 됩니다. 실제로, 산업계에는 노력 없는 천재가 없습니다. 적어도 제가 존경하는 사람들 중에는 예외가 없었으며, 기술에는 우열이 없고 대부분의 경우 도를 깨닫는 데는 선후가 있고, 기술은 각자의 전문 분야가 있을 뿐입니다
제 경험을 예로 들자면, 3년 전에 U++를 잠시 공부했었는데, 당시에는 공식 문서를 한 번 훑어보고 다음 튜토리얼을 따라해봤습니다:

완료 후에는 많은 코드가 이해가 안 되고 모호한 부분이 너무 많아서 포기했습니다. 차라리 직접 보고 만질 수 있는 것들을 배우는 게 낫겠다 싶어서 Qt, CMake, OpenGL, 일부 그래픽스 지식과 디지털 신호 관련 내용을 공부했고, 다음과 같은 것을 만들었습니다:

이러한 기술들을 능숙하게 다룰 수 있게 된 후에 다시 언리얼 엔진을 보니, 와, 모든 것이 명확해졌습니다~
UBT(Unreal Build Tool)를 봤을 때, 아, 이것은 CMake와 같은 용도로 프로젝트 빌드를 관리하는구나, 이걸로 이런 작업들을 할 수 있겠다고 생각했습니다:

  • 빌드 대상에 라이브러리(Library), 포함 디렉토리(Include Directories), 매크로 정의(Definitions) 추가
  • 컴파일러 설정
  • 빌드 스크립트 실행

UHT(Unreal Header Tool)를 보았을 때는 Qt의 Moc(Meta Object Compiler)가 떠올랐습니다. 작업 흐름이 코드 헤더 파일을 스캔하여 정보를 얻고, 새로운 코드 파일을 생성하여 빌드 대상에 추가하는 것이죠. 이를 통해 UE의 코드 리플렉션을 구현하고, 리플렉션을 통해 타입 기반 위젯, 객체 기반 패널을 구현할 수 있으며, 리플렉션 데이터를 직접 구성하여 블루프린트와 같은 시각적 프로그래밍 스크립트를 구현할 수 있습니다.
Slate를 봤을 때는 문서가 없어도 추측할 수 있었습니다:

  • FSlateApplication은 전역 싱글톤으로, 모든 UI의 디스패치 센터이며, 여기서 전체 애플리케이션의 상태를 가져오고 설정할 수 있으며 유용한 작업들을 제공합니다
  • SWindow는 최상위 윈도우로, 크로스 플랫폼 윈도우 인스턴스(FGenericWindow)를 보유하고 있으며 윈도우 관련 설정과 작업을 제공합니다.
  • SWidget은 윈도우 영역을 분할하고 자신의 영역 내 상호작용과 그리기 이벤트를 처리합니다. 이것을 봤을 때 첫 반응은 어떤 속성을 설정할 수 있는지, 어떤 이벤트 가상 함수를 오버라이드할 수 있는지, 그리고 PaintEvent를 어떻게 처리하는지 찾아보는 것이었습니다

기본적인 GUI 개념들도 매핑해보면:

  • 윈도우의 기본 상태: 활성화(Active), 포커스(Focus), 가시성(Visible), 모달(Modal), 변환(Transform)
  • 레이아웃 전략 및 관련 개념:
    • 박스 레이아웃(HBox, VBox), 플로우 레이아웃(Flow), 그리드 레이아웃(Grid), 앵커 레이아웃(Anchors), 오버랩 레이아웃(Overlap), 캔버스(Canvas)
    • 내부 여백(Margin), 외부 여백(Padding), 간격(Spacing), 정렬(Alignment)
  • 스타일 관리: 아이콘(Icon), 스타일(Style), 브러시(Brush)
  • 폰트 관리: 폰트 패밀리(Font Family), 텍스트 너비 측정(Font Measure)
  • 크기 계산: 위젯 크기 계산 전략
  • 상호작용 이벤트: 마우스, 키보드, 드래그 앤 드롭, 포커스 이벤트, 이벤트 처리 메커니즘, 마우스 캡처
  • 그리기 이벤트: 그리기 요소, 영역 클리핑
  • 기본 위젯: 레이블(Label), 버튼(Button), 체크박스(Check), 콤보박스(Combo), 슬라이더(Slider), 스크롤바(Scroll Bar), 텍스트박스(Text), 대화상자(Dialog), 컬러피커(Color), 메뉴바(Menu Bar), 메뉴(Menu), 상태바(Status Bar), 스크롤 패널(Scroll), 스택(전환) 패널(Stack/Switcher), 리스트 패널(List), 트리 패널(Tree), 도킹 윈도우(Dock), ...
  • 국제화: 텍스트 현지화 번역(Localization)

좋습니다, 이제 Slate로 상상할 수 있는 모든 인터페이스 효과를 구현할 수 있다는 것을 알았습니다.
RHI(Rendering Hardware Interface)를 보면서, OpenGL/Vulkan의 어떤 작업들과 대응되는지 연상할 수 있었고, 커스텀 확장을 하려면 결국 비슷한 방식으로 하면 된다는 것을 알았습니다.
Niagara를 접했을 때, GPU 파티클이 단순히 Compute Pipeline을 통해 상호작용 체인에서 파티클 운동을 시뮬레이션하고, 원자 연산과 간접 렌더링을 활용하여 파티클을 재활용하며, 최종적으로 파티클 데이터를 인스턴스화 데이터로 파티클 렌더러의 파라미터에 바인딩하여 파티클 이펙트를 렌더링한다는 것을 알았습니다. 그리고 Niagara의 Module은 단지 Compute Shader 코드와 파이프라인 리소스를 구성하는 것이라는 것도요. 파티클 시스템의 구현 원리를 알고 있었기에 Niagara의 워크플로우와 성능에 대해 매우 민감하게 인지할 수 있었습니다.
World, Actor, Component, Controller ... 등을 보면서, 게임플레이 아키텍처가 이렇게 많은 구조를 사용하여 코드의 책임을 분리할 수 있다는 것을 깨달았습니다. 제가 반쪽짜리 실력으로 했던 것보다 훨씬 더 잘 되어 있네요~
이런 식으로, 일부 문서와 튜토리얼을 맹목적으로 따라가는 것을 포기한 후에, 오히려 일반적인 프레임워크를 통해 쌓은 지식 체계가 언리얼 엔진을 다시 볼 때 "특별한" 시각을 갖게 해주었습니다 - 엔진에는 어떤 것들이 있어야 하는지, 아, 언리얼 엔진에도 있구나, 그것도 아주 잘 만들어져 있네요.
따라서, 아직 대규모 C++ 프로젝트의 엔지니어링 능력이 없다면, Qt나 문서가 잘 갖춰진 오픈소스의 경량 엔진을 학습해보시기를 추천드립니다. 직접 하나를 만들어보면 더 좋겠죠~
탄탄한 기초 지식 체계를 구축한 후에는 앞으로의 학습 중점은 주로 접근 방식이 될 것입니다
본 문서는 기본 개발의 주요 노선을 설명하는 것이 목적이며, 단순한 개발자 문서일 뿐 튜토리얼이 아닙니다.

프로젝트 구조

표준 언리얼 엔진 프로젝트의 파일 구조는 다음과 같습니다:

  • Config: 프로젝트의 각종 설정 파일들이 저장됨 (게임 설정, 엔진 설정, 에디터 설정, 플러그인 설정 등)
  • Content: 프로젝트의 리소스 파일들이 저장됨
  • Saved: 임시 저장소로, 프로젝트 개발 과정에서 생성되는 파일들이 여기에 저장됨 (로그, 크래시 기록, 라이트맵 베이킹, 로컬 에디터 설정 등)
  • Source: 프로젝트의 소스 코드 파일들이 저장됨
  • MyProj.uproject: 프로젝트 파일

uproject

  • .uproject 파일에는 프로젝트의 기본 정보가 저장되며, 초기 구조는 다음과 같습니다:
{
    "FileVersion": 3,
    "EngineAssociation": "5.2",     
    "Category": "",
    "Description": "",
    "Modules": [                    
        {
            "Name": "MyProj",
            "Type": "Runtime",
            "LoadingPhase": "Default"
        }
    ],
    "Plugins": [
        {
            "Name": "ModelingToolsEditorMode",
            "Enabled": true,
            "TargetAllowList": [
                "Editor"
            ]
        }
    ]
}

이 파일의 주요 매개변수는 다음과 같습니다:

  • EngineAssociation: 엔진 버전
  • Modules: 프로젝트가 보유한 코드 모듈
  • Plugins: 프로젝트에서 활성화된 내장 플러그인

이러한 매개변수들은 수동으로 수정할 수 있지만, 대부분의 경우 UE 에디터에서 변경하는 것이 더 안전하고 유연합니다.

엔진 버전 전환

*.uproject 파일의 우클릭 메뉴에서 현재 프로젝트의 엔진 버전을 전환할 수 있습니다:

만약 엔진이 선택 상자의 드롭다운 목록에 나타나지 않는다면, 아래 디렉토리에서 UnrealVersionSelector-Win64-Shipping.exe를 사용하여 등록해야 합니다:

내장 플러그인 활성화

엔진에서 내장 플러그인을 활성화하면, 에디터가 자동으로 *.uproject 파일의 Plugins 섹션에 새로운 플러그인 항목을 추가합니다:

프로젝트 모듈 추가

모듈(Modules)은 언리얼 엔진(UE)의 소프트웨어 아키텍처의 기본 구성 요소입니다. 독립적인 코드 단위에서 프로그래밍 도구, 런타임 기능, 라이브러리 또는 기타 기능들을 하나로 통합하여 캡슐화합니다.
모듈식 프로그래밍을 사용하면 다음과 같은 이점이 있습니다:

  • 모듈은 우수한 코드 분리를 강제하며, 기능을 캡슐화하고 코드의 내부 구성 요소를 숨길 수 있습니다.
  • 모듈은 개별 컴파일 단위로 컴파일됩니다. 이는 변경된 모듈만 다시 컴파일하면 되므로, 대규모 프로젝트의 컴파일 시간이 크게 단축됩니다.
  • 모듈은 종속성 그래프에서 서로 연결되어 있으며, Include What You Use (IWYU) 표준에 따라 실제 사용되는 코드 패키지만 헤더 파일을 포함할 수 있습니다. 이는 프로젝트에서 사용되지 않는 모듈이 빌드에서 안전하게 제외된다는 것을 의미합니다.
  • 런타임에 언제든지 모듈을 로드하고 언로드하는 것을 제어할 수 있습니다. 이를 통해 어떤 시스템이 사용 가능하고 활성화되어 있는지 관리하여 프로젝트의 성능을 최적화할 수 있습니다.
  • 특정 조건(예: 프로젝트가 어떤 플랫폼용으로 작성되었는지)에 따라 프로젝트에 모듈을 포함하거나 제외할 수 있습니다.

모든 프로젝트와 플러그인은 기본적으로 자체 메인 모듈을 가지고 있습니다
UE 에디터 메인 패널의 도구(Tools) - 디버그(Debug) - 모듈(Modules)에서 현재 프로젝트에서 활성화된 모든 모듈을 확인할 수 있습니다:

모듈 생성에 대해서는 다음을 참조하십시오:

문서를 읽는 것 외에도 C++ 프로젝트 빌드의 기본 개념을 이해해야 합니다:

  • .Build.cs는 UE 모듈의 빌드 파일로, 이 파일에는 해당 모듈의 빌드 규칙이 정의되어 있으며, 여기에는 포함 경로, 종속 라이브러리, 컴파일 옵션 등이 포함됩니다. (CMake의 CMakeLists.txt와 유사)

기본적인 *.Build.cs 구조는 다음과 같습니다:

using UnrealBuildTool;
using System.IO; // for Path
public class ModuleName : ModuleRules
{
    public ModuleName(ReadOnlyTargetRules Target) : base(Target)
    {
        PCHUsage = ModuleRules.PCHUsageMode.UseExplicitOrSharedPCHs;
        PublicIncludePaths.AddRange(
            new string[] {
                // ... add public include paths required here ...
            }
        );
        PrivateIncludePaths.AddRange(
            new string[] {
                // ... add other private include paths required here ...
            }
        );
        PublicDependencyModuleNames.AddRange(
            new string[]
            {
                "Core",
                "CoreUObject",
                "Engine",
                // ... add other public dependencies that you statically link with here ...
            }
        );
        PrivateDependencyModuleNames.AddRange(
            new string[]
            {
                // ... add private dependencies that you statically link with here ...  
            }
        );
    }
}
  • UE의 빌드 도구(UBT)는 .Build.cs를 기반으로 프로젝트의 프로젝트 파일(VS 프로젝트의 .sln 파일)을 생성합니다. 따라서 .Build.cs의 내용이나 모듈의 코드 파일 구조를 수정한 경우, UBT를 사용하여 프로젝트 파일을 다시 생성해야 IDE가 변경된 코드와 종속성 관계를 분석할 수 있습니다.
  • 포함 경로와 종속 라이브러리는 C++ 프로젝트 빌드의 기본 개념입니다.
    • 포함 경로는 헤더 파일 정의의 검색 경로를 추가하는 데 사용됩니다.
    • 종속 라이브러리는 현재 모듈에 외부 모듈의 구현을 링크(Link)하는 데 사용됩니다.

프로젝트에 D:/Unreal Projects/MyProj/Source/MyProj/MyHeader.h라는 C++ 헤더 파일이 있다고 가정할 때, 이 파일의 코드 정의를 사용하려면 다음과 같이 직접 사용할 수 있습니다:
#include "D:/Unreal Projects/MyProj/Source/MyProj/MyHeader.h"
*.Build.cs의 IncludePaths에 "D:/Unreal Projects/MyProj/Source/MyProj"를 추가하면 다음과 같이 간단하게 작성할 수 있습니다:
#include "MyHeader.h"
현재 모듈에서 다른 모듈의 코드를 사용하려면 *.Build.cs의 DependencyModuleNames에 대상 모듈을 추가하기만 하면 됩니다

이 단계를 수행하지 않으면 컴파일 시 링크 오류(Link Error)가 발생합니다. 이 경우 사용하고자 하는 구조체의 코드 파일을 찾아 IDE를 통해 해당 파일이 어떤 *.Build.cs에 속해 있는지 확인한 후, 해당 모듈 이름을 DependencyModuleNames에 추가하면 문제가 해결됩니다.

Public과 Private의 차이점

간단히 말해서, Public은 전파 가능을 의미하고, Private은 자체 사용만을 의미합니다.
A, B, C라는 세 개의 모듈이 있다고 가정하고, 이들의 코드 파일 구조는 다음과 같습니다:

  • 폴더 A
    • A.Build.cs
    • Public 폴더
      • a.h
    • Private 폴더
      • A.h
  • 폴더 B
    • B.Build.cs
    • Public 폴더
      • b.h
    • Private 폴더
      • B.h
  • 폴더 C
    • C.Build.cs
    • Public 폴더
      • c.h
    • Private 폴더
      • C.h
  • .Build.cs의 의사 코드는 다음과 같습니다:
public class A(ReadOnlyTargetRules Target) : base(Target){
}
public class B(ReadOnlyTargetRules Target) : base(Target){
    PrivateDependencyModuleNames.AddRange( new string[] { "A"});
}
public class C(ReadOnlyTargetRules Target) : base(Target){
    PublicDependencyModuleNames.AddRange( new string[] { "B"});
}

c.h에서 모듈 A와 B의 파일로 이동하면 다음과 같은 결과가 나타납니다:

#include "C.h"
//【编译报错0】,Private文件夹并不是模块C的搜索路径。
#include "b.h"
//编译正常,UE会将模块自身的 Public目录 自动加入到 Build.cs 的 PublicIncludePaths 中,又由于 模块C的公有依赖 中加入了 模块B
//所以 模块B的PublicIncludePaths 也会传递给 模块C,因此模块C中可以正常访问B中的Public目录
#include "B.h"  
//【编译报错1】,模块C无法访问到模块B的Private目录
#include "A.h"          
//【编译报错2】,由于 模块C的公有依赖 中加入了 模块B,而 模块B 却只是在私有依赖 中加入了 模块A
//因此B中可以正常使用A中的Public内容,但C不能使用A
#include "a.h"          
//【编译报错3】,模块C无法访问到模块A的任何内容

위의 오류를 제거하려면 *.Build.cs 구조를 다음과 같이 변경하면 됩니다:

public class A(ReadOnlyTargetRules Target) : base(Target){
    PublicIncludePaths.AddRange( new string[] { "Private"});    //修复【编译报错3】,将模块A的Private文件夹添加到公有包含路径中
}
public class B(ReadOnlyTargetRules Target) : base(Target){
    PublicDependencyModuleNames.AddRange( new string[] { "A"}); //修复【编译报错2,3】,将模块A作为模块B的公用依赖,表示可传递依赖
    PublicIncludePaths.AddRange( new string[] { "Private"});    //修复【编译报错1】,将模块B的Private文件夹添加到公有包含路径中
}
public class C(ReadOnlyTargetRules Target) : base(Target){      
    PublicDependencyModuleNames.AddRange( new string[] { "B"});
    PublicIncludePaths.AddRange( new string[] { "Private"});    //修复【编译报错0】,将模块C的Private文件夹添加到公有包含路径中
}

위의 예시를 보면 Public과 Private의 차이를 이해하기는 어렵지 않습니다:

  • Public은 전파 가능함을 의미
  • Private은 자체 사용만을 의미

독자들은 아마도 궁금할 것입니다. 모두 Public으로 통일하면 많은 작업을 줄일 수 있지 않을까? 그런데 왜 전부 Public을 사용하지 않을까요? 그것은 다음과 같은 문제들이 발생하기 때문입니다:

  • 정의 충돌: 가져온 모듈에서 중복된 정의(클래스, 함수, 전역 변수)가 있을 경우 컴파일 오류가 발생합니다.
  • 컴파일 속도 저하: 만약 모듈 B가 모듈 A를 가져왔을 때, 모듈 A의 코드가 변경되면 B 모듈도 재컴파일이 발생하게 됩니다. 따라서 불필요한 의존성이 많으면 컴파일 속도가 크게 저하될 수 있습니다. 예를 들어 #include "CoreMinimal.h"도 이러한 문제를 야기할 수 있습니다.

모듈의 컴파일 충돌 위험과 컴파일 속도 저하 문제를 방지하기 위해, 모듈을 작성할 때는 가능한 한 Private을 사용하고, 모듈이 외부로 전파되어야 할 때만 Public 사용을 고려해야 합니다.

객체 시스템

언리얼 엔진의 로직 구조는 객체 지향 설계 패러다임을 채택했으며, 모든 클래스(Class)가 통일된 관리 방식을 가질 수 있도록 다른 객체 지향 프레임워크처럼 자체 기본 클래스인 UObject를 제공합니다.

구조

그 구조 계층은 다음과 같습니다:

이는 주로 3단계 구조로 구성되어 있으며, 각 단계는 서로 다른 책임을 가지고 있습니다:

  • UObjectBase: 데이터 계층으로, UObject의 모든 기본 데이터를 정의하며 다음을 포함합니다:
    • ObjectFlags: Object의 각종 식별자
    • InternalIndex: UE의 모든 Object 포인터는 하나의 배열에 저장되며, 여기에서는 GC를 위해 배열 내 빠른 위치 파악을 위한 인덱스를 기록합니다
    • ClassPrivate: Object의 메타 타입 - UClass: 해당 클래스의 리플렉션 데이터를 저장합니다
    • NamePrivate: Object의 이름
    • OuterPrivate: 해당 Object를 보유하는 객체(직렬화와 관련이 있으며, 생명주기와는 무관)
  • UObjectBaseUtility: 데이터 인터페이스 계층으로, 데이터 계층을 위한 다양한 처리 인터페이스를 제공합니다.
  • UObject: 기능 계층으로, 객체 시스템의 다양한 기본 함수 인터페이스를 제공합니다.

UObjectBase의 생성자에는 다음과 같은 코드가 있습니다:

UObjectBase::UObjectBase(UClass* InClass, EObjectFlags InFlags, EInternalObjectFlags InInternalFlags, UObject *InOuter, FName InName)
:   ObjectFlags         (InFlags)
,   InternalIndex       (INDEX_NONE)
,   ClassPrivate        (InClass)
,   OuterPrivate        (InOuter)
{
    check(ClassPrivate);
    // Add to global table.
    AddObject(InName, InInternalFlags);
}

Runtime\\CoreUObject\\Private\\UObject\\UObjectHash.cpp
여기서 AddObject는 새로 생성된 UObject의 주소를 전역 변수 GUObjectArray에 저장하며, 이는 다음 위치에 있습니다:

// Global UObject array instance
FUObjectArray GUObjectArray;

FUObjectArray의 정의는 Runtime\CoreUObject\Public\UObject\UObjectArray.h에 위치해 있으며, 내부적으로 모든 Object, 클러스터, GC를 관리하는 데 사용되며, 주요 데이터 멤버는 다음과 같습니다:

class FUObjectArray{
    //typedef TStaticIndirectArrayThreadSafeRead<UObjectBase, 8 * 1024 * 1024 /* Max 8M UObjects */, 16384 /* allocated in 64K/128K chunks */ > TUObjectArray;
    typedef FChunkedFixedUObjectArray TUObjectArray;
    // note these variables are left with the Obj prefix so they can be related to the historical GObj versions
    /** First index into objects array taken into account for GC.                           */
    int32 ObjFirstGCIndex;
    /** Index pointing to last object created in range disregarded for GC.                  */
    int32 ObjLastNonGCIndex;
    /** Maximum number of objects in the disregard for GC Pool */
    int32 MaxObjectsNotConsideredByGC;
    /** If true this is the intial load and we should load objects int the disregarded for GC range.    */
    bool OpenForDisregardForGC;
    /** Array of all live objects.                                          */
    TUObjectArray ObjObjects;
    /** Synchronization object for all live objects.                                            */
    mutable FCriticalSection ObjObjectsCritical;
    /** Available object indices.                                           */
    TArray<int32> ObjAvailableList;
    /**
     * Array of things to notify when a UObjectBase is created
     */
    TArray<FUObjectCreateListener* > UObjectCreateListeners;
    /**
     * Array of things to notify when a UObjectBase is destroyed
     */
    TArray<FUObjectDeleteListener* > UObjectDeleteListeners;
};

또 하나의 핵심 구조는 FUObjectHashTables입니다. 이는 싱글톤 클래스로, 빠른 검색을 위한 다수의 매핑을 저장하고 있으며, 그 코드 구조는 다음과 같습니다:

struct FHashBucket{
    void *ElementsOrSetPtr[2];  //可能是UObjectBase* ,也可能是 TSet<UObjectBase*>*
};
template <typename T>
class TBucketMap : private TMap<T, FHashBucket>{
    //...
};
class FUObjectHashTables
{
    /** 线程锁 */
    FCriticalSection CriticalSection;
public:
    TBucketMap<int32> Hash;                 //Id到Object的映射
    TMultiMap<int32, uint32> HashOuter;      //Id到父对象ID的映射
    TBucketMap<UObjectBase*> ObjectOuterMap;    //Object 到 子对象集合 的映射
    TBucketMap<UClass*> ClassToObjectListMap;   //UClass 到 其所有实例 的映射
    TMap<UClass*, TSet<UClass*> > ClassToChildListMap;  //UClass 到 其派生Class 的映射
    TAtomic<uint64> ClassToChildListMapVersion;         
    TBucketMap<UPackage*> PackageToObjectListMap;       //包 到 对象集 的映射
    TMap<UObjectBase*, UPackage*> ObjectToPackageMap;   //对象 到 包 的映射
    static FUObjectHashTables& Get()
    {
        static FUObjectHashTables Singleton;
        return Singleton;
    }
    //...
};

생성

간단한 UObject 클래스 정의는 다음과 같습니다:

#pragma once
#include "UObject/Object.h"
#include "CustomObject.generated.h"     //如果存在 #include "{文件名}.generated.h" ,UE则会使用UHT生成该文件的反射数据
UCLASS()
class UCustomObject :public UObject {
    GENERATED_BODY()    //GENERATED_BODY() 反射的入口宏,UHT会生成该宏的定义,里面定义了一些结构塞到UCustomObject的类定义中
public:
    UCustomObject() {}
};

UObject의 객체 인스턴스를 생성할 때는 일반적으로 NewObject&lt;&gt;() 함수를 사용합니다. 예를 들어 NewObject&lt;UCustomObject&gt;()

/**
 * Convenience template for constructing a gameplay object
 *
 * @param   Outer       the outer for the new object.  If not specified, object will be created in the transient package.
 * @param   Class       the class of object to construct
 * @param   Name        the name for the new object.  If not specified, the object will be given a transient name via MakeUniqueObjectName
 * @param   Flags       the object flags to apply to the new object
 * @param   Template    the object to use for initializing the new object.  If not specified, the class's default object will be used
 * @param   bCopyTransientsFromClassDefaults    if true, copy transient from the class defaults instead of the pass in archetype ptr (often these are the same)
 * @param   InInstanceGraph                     contains the mappings of instanced objects and components to their templates
 * @param   ExternalPackage                     Assign an external Package to the created object if non-null
 *
 * @return  a pointer of type T to a new object of the specified class
 */
template< class T >
FUNCTION_NON_NULL_RETURN_START
    T* NewObject(UObject* Outer,
                 const UClass* Class, 
                 FName Name = NAME_None,
                 EObjectFlags Flags = RF_NoFlags,
                 UObject* Template = nullptr, 
                 bool bCopyTransientsFromClassDefaults = false,
                 FObjectInstancingGraph* InInstanceGraph = nullptr,
                 UPackage* ExternalPackage = nullptr)
{
    if (Name == NAME_None)
    {
        FObjectInitializer::AssertIfInConstructor(Outer, TEXT("NewObject with empty name can't be used to create default subobjects (inside of UObject derived class constructor) as it produces inconsistent object names. Use ObjectInitializer.CreateDefaultSubobject<> instead."));
    }
#if DO_CHECK
    // Class was specified explicitly, so needs to be validated
    CheckIsClassChildOf_Internal(T::StaticClass(), Class);
#endif
    FStaticConstructObjectParameters Params(Class);
    Params.Outer = Outer;
    Params.Name = Name;
    Params.SetFlags = Flags;
    Params.Template = Template;
    Params.bCopyTransientsFromClassDefaults = bCopyTransientsFromClassDefaults;
    Params.InstanceGraph = InInstanceGraph;
    Params.ExternalPackage = ExternalPackage;
    return static_cast<T*>(StaticConstructObject_Internal(Params));
}

오브젝트 생성에 관해 주목해야 할 몇 가지 사항이 있습니다:

  • Outer는 Object의 수명 주기와 전혀 관련이 없습니다: Outer 매개변수는 현재 오브젝트가 저장 측면에서 어떤 부모 오브젝트에 속하는지를 지정하기 위한 것으로, 이는 에셋 직렬화와 관련이 있습니다.
  • UClass를 통해 오브젝트를 생성할 수 있습니다: NewObject<UCustomObject>()는 NewObject<UObject>(GetTransientPackage(), UCustomObject::StaticClass())와 동일합니다

일반적으로 언리얼 엔진에서 접하는 대부분의 오브젝트는 UObject이며, 코드 레벨에서 UObject를 생성할 때 다음과 같은 방식을 사용합니다:

UObject* MyObject = NewObject<UObject>();
UTexture2D* MyTexture = NewObject<UTexture2D>();
UBlueprint* MyBlueprint = NewObject<UBlueprint>();
UStaticMesh* MyStaticMesh = NewObject<UStaticMesh>();
UNiagaraSystem* MyNiagaraSystem = NewObject<UNiagaraSystem>();
AActor* MyActor = NewObject<AActor>();
UStaticMeshComponent* MyStaticMeshComponent = NewObject<StaticMeshComponent>();
UWorld* World = NewObject<UWorld>();
...

이러한 명령문들은 언리얼에서 문법적 오류가 없으며 컴파일도 정상적으로 됩니다.
하지만 언리얼에 익숙한 분들은 아시겠지만, 이런 방식으로 직접 오브젝트를 생성할 수 있긴 하지만 일부 메커니즘이 제대로 작동하지 않을 수 있습니다. 이는 해당 타입들이 특정한 생성 데이터와 로직을 동반하기 때문이며, 엔진은 보통 이를 새로운 인터페이스로 감싸서 제공합니다. 예를 들면:

  • UWorld::CreateWorld(...)를 사용하여 UWorld를 생성
  • UWorld::SpawnActor( ... )를 사용하여 해당 월드에서 Actor를 생성
  • 에디터에서 블루프린트(UBlueprint)를 생성할 때는 FKismetEditorUtilities::CreateBlueprint(...) 사용
  • Actor의 생성자에서 UObject::CreateDefaultSubobject(...)를 사용하여 현재 Actor에 속하는 컴포넌트 구조를 생성
  • ...

따라서 NewObject()로 생성한 오브젝트의 일부 기능이 제대로 작동하지 않는다면, 엔진이 특별한 생성 로직을 캡슐화했을 가능성이 높습니다. 해당 로직을 찾을 수 없다면, 초기화와 등록을 위한 로직을 찾아서 수동으로 실행해볼 수 있습니다.

제거

UObject 오브젝트의 생성 외에도, 그것의 제거, 즉 생명주기 관리도 매우 중요합니다.
잘 알려져 있듯이, 언리얼은 자동 가비지 컬렉션 시스템을 갖추고 있으며, 이는 오브젝트 참조를 기반으로 한 정기적이고 정량적인 수거 방식입니다.
개발자에게 있어서, 보통은 이 메커니즘의 실행 원리를 깊이 파고들 필요는 없고, 기본적인 사용법만 숙지하면 됩니다. 주요 사용법은 다음과 같습니다:

  • 엔진은 어떻게 오브젝트가 가비지인지 아닌지를 판단할까요?
  • GC를 어떻게 설정하고 사용할까요?

UE에서 객체가 가비지로 취급되지 않도록 하는 직접적인 방법에는 네 가지가 있습니다:

  • NewObject 생성 시 EObjectFlags::RF_Standalone 플래그를 사용하면, 해당 플래그가 지정된 객체는 에디터에서 GC에 의해 수집되지 않습니다(자세한 내용은 GarbageCollection.h line28 참조)
  • void UObject::AddToRoot() 함수를 사용하여 객체를 루트 객체 집합에 추가하면 객체가 해제되지 않으며, void UObject::RemoveFromRoot()를 사용하여 루트 객체 집합에서 제거할 수 있습니다.
  • FGCObject 클래스를 상속받는 방법으로, 이는 UE의 여러 모듈에서 객체 수명 주기 관리에 널리 사용됩니다

UE는 GC 실행 시 모든 FGCObject 객체의 virtual void AddReferencedObjects( FReferenceCollector& Collector ) 함수를 호출하며, Collector는 작업 진입점으로, Collector에 추가된 객체는 해당 GC 과정에서 해제되지 않습니다.
FGCObject의 생성자에서는 해당 객체를 UE 전역의 FGCObject 객체 목록에 등록하므로, FGCObject 클래스를 상속받은 객체는 루트 객체로 취급됩니다.
따라서 개발자는 자신의 기본 구조 정의에 public FGCObject를 추가하고 관련 함수를 구현하기만 하면 됩니다:

class FMyManager : public FGCObject {
    virtual void AddReferencedObjects(FReferenceCollector& Collector) override {
        //Collector.AddReferencedObject(...)
        //..
    }
    virtual FString GetReferencerName() const override{ return TEXT("FMyManager"); }   
};
  • 객체를 저장하기 위해 스마트 포인터 TStrongObjectPtr를 사용할 수 있습니다. 이는 본질적으로 여전히 FGCObject를 사용하는 것이지만, 구조가 매우 무거워서 대규모로 사용하는 것은 권장하지 않습니다. 자세한 내용은 다음을 참조하세요:

간접적인 방법은 다음과 같습니다:

  • 현재 객체가 가비지가 아닌 경우, 해당 객체가 참조하는 객체들도 가비지가 아닙니다.

UE에서는 주로 리플렉션을 통해 객체 간의 참조를 구축하고 분석합니다. 예를 들면 다음과 같은 코드가 있습니다:

UCLASS()
class UCustomObject :public UObject {
public:
    UCustomObject() {}
    UPROPERTY()         
    UObject* Prop = nullptr;
};
int main() {
    auto A = NewObject<UCustomObject>();
    auto B = NewObject<UCustomObject>();
    A->Prop = B;        
    A->AddToRoot();
    // Engine Loop
    return 0;
}
  • 객체 A가 루트 객체 집합에 추가되었기 때문에 해제되지 않으며, UE가 리플렉션을 통해 객체 A의 Prop 속성이 객체 B를 참조한다고 판단하므로 B도 해제되지 않습니다

사용에 관해서는 다음 함수들을 통해 직접 객체를 수동으로 파괴할 수 있습니다:

  • bool UObject::ConditionalBeginDestroy(): GC를 기다리지 않고 객체를 직접 파괴
  • void UObject::MarkAsGarbage(): 가비지로 표시하여 다음 GC 때 해제되도록 함

또한 다음 코드를 사용하여 현재 프레임에서 수동으로 GC를 실행할 수 있습니다:
bool bForceGarbageCollectionPurge = true; //전체 GC 여부 GEngine->ForceGarbageCollection(bForceGarbageCollectionPurge);
추가로 특별히 주의해야 할 점은, UE의 객체 시스템과 리플렉션 시스템이 상호 보완적이며, 여러 이유로 인해 UObject 객체가 소멸자를 사용하지 않고 리소스 해제 로직을 수행하므로, 소멸 로직을 구현할 때는 UObject의 가상 함수를 오버라이드하시기 바랍니다:

  • virtual void BeginDestroy()

프로젝트 설정에서 GC 매개변수 세부 사항 조정에 관하여:

응용

고유 객체 이름 생성:

FName MakeUniqueObjectName(UObject* Parent,
                           const UClass* Class,
                           FName InBaseName/*=NAME_None*/,
                           EUniqueObjectNameOptions Options /*= EUniqueObjectNameOptions::None*/)

UObject의 생성과 소멸 감시:

class FUObjectCreateListener
{
public:
    virtual ~FUObjectCreateListener() {}
    virtual void NotifyUObjectCreated(const class UObjectBase *Object, int32 Index)=0;
    virtual void OnUObjectArrayShutdown()=0;
};
class FUObjectDeleteListener
{
public:
    virtual ~FUObjectDeleteListener() {}
    virtual void NotifyUObjectDeleted(const class UObjectBase *Object, int32 Index)=0;
    virtual void OnUObjectArrayShutdown() = 0;
};

개발자는 이 두 가지 리스너를 상속받아 커스텀 구현할 수 있으며, GUObjectArray의 다음 함수들을 호출하여 UObject를 전역적으로 모니터링하거나 수정할 수 있습니다(UnLua가 바로 이런 방식으로 구현되어 있습니다):

class FUObjectArray{
    void AddUObjectCreateListener(FUObjectCreateListener* Listener);
    void RemoveUObjectCreateListener(FUObjectCreateListener* Listener);
    void AddUObjectDeleteListener(FUObjectDeleteListener* Listener);
    void RemoveUObjectDeleteListener(FUObjectDeleteListener* Listener);
};

Runtime\CoreUObject\Public\UObject\UObjectHash.h는 FUObjectHashTables의 데이터를 관리하고 접근하기 위한 몇 가지 정적 메서드를 제공합니다.

UObject* StaticFindObjectFastInternal(const UClass* Class, const UObject* InOuter, FName InName, bool ExactClass = false, bool AnyPackage = false, EObjectFlags ExclusiveFlags = RF_NoFlags, EInternalObjectFlags ExclusiveInternalFlags = EInternalObjectFlags::None);
UObject* StaticFindObjectFastExplicit(const UClass* ObjectClass, FName ObjectName, const FString& ObjectPathName, bool bExactClass, EObjectFlags ExcludeFlags = RF_NoFlags);
COREUOBJECT_API void GetObjectsWithOuter(const class UObjectBase* Outer, TArray<UObject *>& Results, bool bIncludeNestedObjects = true, EObjectFlags ExclusionFlags = RF_NoFlags, EInternalObjectFlags ExclusionInternalFlags = EInternalObjectFlags::None);
COREUOBJECT_API void ForEachObjectWithOuterBreakable(const class UObjectBase* Outer, TFunctionRef<bool(UObject*)> Operation, bool bIncludeNestedObjects = true, EObjectFlags ExclusionFlags = RF_NoFlags, EInternalObjectFlags ExclusionInternalFlags = EInternalObjectFlags::None);
inline void ForEachObjectWithOuter(const class UObjectBase* Outer, TFunctionRef<void(UObject*)> Operation, bool bIncludeNestedObjects = true, EObjectFlags ExclusionFlags = RF_NoFlags, EInternalObjectFlags ExclusionInternalFlags = EInternalObjectFlags::None)
{
    ForEachObjectWithOuterBreakable(Outer, [Operation](UObject* Object) { Operation(Object); return true; }, bIncludeNestedObjects, ExclusionFlags, ExclusionInternalFlags);
}
COREUOBJECT_API class UObjectBase* FindObjectWithOuter(const class UObjectBase* Outer, const class UClass* ClassToLookFor = nullptr, FName NameToLookFor = NAME_None);
COREUOBJECT_API void GetObjectsWithPackage(const class UPackage* Outer, TArray<UObject *>& Results, bool bIncludeNestedObjects = true, EObjectFlags ExclusionFlags = RF_NoFlags, EInternalObjectFlags ExclusionInternalFlags = EInternalObjectFlags::None);
COREUOBJECT_API void ForEachObjectWithPackage(const class UPackage* Outer, TFunctionRef<bool(UObject*)> Operation, bool bIncludeNestedObjects = true, EObjectFlags ExclusionFlags = RF_NoFlags, EInternalObjectFlags ExclusionInternalFlags = EInternalObjectFlags::None);
COREUOBJECT_API void GetObjectsOfClass(const UClass* ClassToLookFor, TArray<UObject *>& Results, bool bIncludeDerivedClasses = true, EObjectFlags ExcludeFlags = RF_ClassDefaultObject, EInternalObjectFlags ExclusionInternalFlags = EInternalObjectFlags::None);
COREUOBJECT_API void ForEachObjectOfClass(const UClass* ClassToLookFor, TFunctionRef<void(UObject*)> Operation, bool bIncludeDerivedClasses = true, EObjectFlags ExcludeFlags = RF_ClassDefaultObject, EInternalObjectFlags ExclusionInternalFlags = EInternalObjectFlags::None);
COREUOBJECT_API void ForEachObjectOfClasses(TArrayView<const UClass*> ClassesToLookFor, TFunctionRef<void(UObject*)> Operation, EObjectFlags ExcludeFlags = RF_ClassDefaultObject, EInternalObjectFlags ExclusionInternalFlags = EInternalObjectFlags::None);
/*获取UClass所有派生类的UClass*/
COREUOBJECT_API void GetDerivedClasses(const UClass* ClassToLookFor, TArray<UClass *>& Results, bool bRecursive = true);
COREUOBJECT_API TMap<UClass*, TSet<UClass*>> GetAllDerivedClasses();
COREUOBJECT_API bool ClassHasInstancesAsyncLoading(const UClass* ClassToLookFor);
void HashObject(class UObjectBase* Object);
void UnhashObject(class UObjectBase* Object);
void HashObjectExternalPackage(class UObjectBase* Object, class UPackage* Package);
void UnhashObjectExternalPackage(class UObjectBase* Object);
UPackage* GetObjectExternalPackageThreadSafe(const class UObjectBase* Object);
UPackage* GetObjectExternalPackageInternal(const class UObjectBase* Object);

Editor\\UnrealEd\\Public\\ObjectTools.h의 ObjectTools 네임스페이스는 에디터 환경에서 Object를 안전하게 조작하는 다양한 메서드를 제공합니다. 이와 유사하게 ThumbnailTools, AssetTools 등도 있습니다...

namespace ObjectTools
{
    UNREALED_API bool IsObjectBrowsable( UObject* Obj );
    UNREALED_API void DuplicateObjects( const TArray<UObject*>& SelectedObjects, const FString& SourcePath = TEXT(""), const FString& DestinationPath = TEXT(""), bool bOpenDialog = true, TArray<UObject*>* OutNewObjects = NULL );
    UNREALED_API UObject* DuplicateSingleObject(UObject* Object, const FPackageGroupName& PGN, TSet<UPackage*>& InOutPackagesUserRefusedToFullyLoad, bool bPromptToOverwrite = true, TMap<TSoftObjectPtr<UObject>, TSoftObjectPtr<UObject>>* DuplicatedObjects = nullptr);
    UNREALED_API FConsolidationResults ConsolidateObjects( UObject* ObjectToConsolidateTo, TArray<UObject*>& ObjectsToConsolidate, bool bShowDeleteConfirmation = true );
    UNREALED_API FConsolidationResults ConsolidateObjects(UObject* ObjectToConsolidateTo, TArray<UObject*>& ObjectsToConsolidate, TSet<UObject*>& ObjectsToConsolidateWithin, TSet<UObject*>& ObjectsToNotConsolidateWithin, bool bShouldDeleteAfterConsolidate, bool bWarnAboutRootSet = true);
    UNREALED_API void CompileBlueprintsAfterRefUpdate(const TArray<UObject*>& ObjectsConsolidatedWithin);
    UNREALED_API void CopyReferences( const TArray< UObject* >& SelectedObjects ); // const
    UNREALED_API void ShowReferencers( const TArray< UObject* >& SelectedObjects ); // const
    UNREALED_API void ShowReferenceGraph( UObject* ObjectToGraph );
    UNREALED_API void ShowReferencedObjs( UObject* Object, const FString& CollectionName = FString(), ECollectionShareType::Type ShareType = ECollectionShareType::CST_Private);
    UNREALED_API void SelectActorsInLevelDirectlyReferencingObject( UObject* RefObj );
    UNREALED_API void SelectObjectAndExternalReferencersInLevel( UObject* Object, const bool bRecurseMaterial );
    UNREALED_API void AccumulateObjectReferencersForObjectRecursive( UObject* Object, TArray<UObject*>& Referencers, const bool bRecurseMaterial );
    bool ShowDeleteConfirmationDialog ( const TArray<UObject*>& ObjectsToDelete );
    UNREALED_API void CleanupAfterSuccessfulDelete ( const TArray<UPackage*>& ObjectsDeletedSuccessfully, bool bPerformReferenceCheck = true );
    UNREALED_API int32 DeleteObjects( const TArray< UObject* >& ObjectsToDelete, bool bShowConfirmation = true, EAllowCancelDuringDelete AllowCancelDuringDelete = EAllowCancelDuringDelete::AllowCancel);
    UNREALED_API int32 DeleteObjectsUnchecked( const TArray< UObject* >& ObjectsToDelete );
    UNREALED_API int32 DeleteAssets( const TArray<FAssetData>& AssetsToDelete, bool bShowConfirmation = true );
    UNREALED_API bool DeleteSingleObject( UObject* ObjectToDelete, bool bPerformReferenceCheck = true );
    UNREALED_API int32 ForceDeleteObjects( const TArray< UObject* >& ObjectsToDelete, bool ShowConfirmation = true );
    UNREALED_API void ForceReplaceReferences(UObject* ObjectToReplaceWith, TArray<UObject*>& ObjectsToReplace);
    UNREALED_API void ForceReplaceReferences(UObject* ObjectToReplaceWith, TArray<UObject*>& ObjectsToReplace, TSet<UObject*>& ObjectsToReplaceWithin);
    UNREALED_API void AddExtraObjectsToDelete(TArray< UObject* >& ObjectsToDelete);
    UNREALED_API bool ComposeStringOfReferencingObjects( TArray<FReferencerInformation>& References, FString& RefObjNames, FString& DefObjNames );
    UNREALED_API void DeleteRedirector(UObjectRedirector* Redirector);
    UNREALED_API bool GetMoveDialogInfo(const FText& DialogTitle, UObject* Object, bool bUniqueDefaultName, const FString& SourcePath, const FString& DestinationPath, FMoveDialogInfo& InOutInfo);
    UNREALED_API bool RenameObjectsInternal( const TArray<UObject*>& Objects, bool bLocPackages, const TMap< UObject*, FString >* ObjectToLanguageExtMap, const FString& SourcePath, const FString& DestinationPath, bool bOpenDialog );
    UNREALED_API bool RenameSingleObject(UObject* Object, FPackageGroupName& PGN, TSet<UPackage*>& InOutPackagesUserRefusedToFullyLoad, FText& InOutErrorMessage, const TMap< UObject*, FString >* ObjectToLanguageExtMap = NULL, bool bLeaveRedirector = true);
    UNREALED_API void AddLanguageVariants( TArray<UObject*>& OutObjects, TMap< UObject*, FString >& OutObjectToLanguageExtMap, USoundWave* Wave );
    UNREALED_API bool RenameObjects( const TArray< UObject* >& SelectedObjects, bool bIncludeLocInstances, const FString& SourcePath = TEXT(""), const FString& DestinationPath = TEXT(""), bool bOpenDialog = true );
    UNREALED_API FString SanitizeObjectName(const FString& InObjectName);
    UNREALED_API FString SanitizeObjectPath(const FString& InObjectPath);
    UNREALED_API FString SanitizeInvalidChars(const FString& InText, const FString& InvalidChars);
    UNREALED_API FString SanitizeInvalidChars(const FString& InText, const TCHAR* InvalidChars);
    UNREALED_API void SanitizeInvalidCharsInline(FString& InText, const TCHAR* InvalidChars);
    UNREALED_API void GenerateFactoryFileExtensions( const UFactory* InFactory, FString& out_Filetypes, FString& out_Extensions, TMultiMap<uint32, UFactory*>& out_FilterIndexToFactory );
    UNREALED_API void GenerateFactoryFileExtensions( const TArray<UFactory*>& InFactories, FString& out_Filetypes, FString& out_Extensions, TMultiMap<uint32, UFactory*>& out_FilterIndexToFactory );
    UNREALED_API void AppendFactoryFileExtensions( UFactory* InFactory, FString& out_Filetypes, FString& out_Extensions );
    UNREALED_API void AppendFormatsFileExtensions(const TArray<FString>& InFormats, FString& out_FileTypes, FString& out_Extensions);
    UNREALED_API void AssembleListOfExporters(TArray<UExporter*>& OutExporters);
    UNREALED_API void TagInUseObjects( EInUseSearchOption SearchOption, EInUseSearchFlags InUseSearchFlags = EInUseSearchFlags::None);
    UNREALED_API TSharedPtr<SWindow> OpenPropertiesForSelectedObjects( const TArray<UObject*>& SelectedObjects );
    UNREALED_API void RemoveDeletedObjectsFromPropertyWindows( TArray<UObject*>& DeletedObjects );
    UNREALED_API bool IsAssetValidForPlacing(UWorld* InWorld, const FString& ObjectPath);
    UNREALED_API bool IsClassValidForPlacing(const UClass* InClass);
    UNREALED_API bool IsClassRedirector( const UClass* Class );
    UNREALED_API bool AreObjectsOfEquivalantType( const TArray<UObject*>& InProposedObjects );
    UNREALED_API bool AreClassesInterchangeable( const UClass* ClassA, const UClass* ClassB );
    UNREALED_API void GatherObjectReferencersForDeletion(UObject* InObject, bool& bOutIsReferenced, bool& bOutIsReferencedByUndo, FReferencerInformationList* OutMemoryReferences = nullptr, bool bInRequireReferencingProperties = false);
    UNREALED_API void GatherSubObjectsForReferenceReplacement(TSet<UObject*>& InObjects, TSet<UObject*>& ObjectsToExclude, TSet<UObject*>& OutObjectsAndSubObjects);
};

 

리플렉션 시스템

리플렉션은 현대 프로그램 개발에서 새롭게 부상하는 개념으로, 코드를 메타적 관점에서 바라보며 코드 내의 열거형, 클래스, 구조체, 함수 등을 런타임에 접근하고 조작할 수 있는 자산으로 다루는 것을 목표로 합니다.이는 본질적으로 C++ 코드의 자기 성찰입니다.
리플렉션의 개념과 기본 구현에 대한 자세한 내용은 다음을 참조하세요:

  • 현대 그래픽 엔진 입문 가이드 (5) - 매크로, 템플릿, 리플렉션

구조

UE에서 비교적 완전한 리플렉션 마커 예시는 다음과 같습니다:

/*CustomStruct.h*/
#pragma once        
#include "UObject/Object.h"
#include "CustomStruct.generated.h"
UCLASS()
class UCustomClass :public UObject {
    GENERATED_BODY()
public:
    UCustomClass() {}
public:
    UFUNCTION()
    static int Add(int a, int b) {
        UE_LOG(LogTemp, Warning, TEXT("Hello World"));
        return a + b;
    }
private:
    UPROPERTY(meta = (Keywords = "This is keywords"))
    int SimpleValue = 123;
    UPROPERTY()
    TMap<FString, int> ComplexValue = { {"Key0",0},{"Key1",1} };
};
UENUM()
enum class ECustomEnum : uint8 {
    One UMETA(DisplayName = "This is 0"),
    Two UMETA(DisplayName = "This is 1"),
    Tree UMETA(DisplayName = "This is 2")
};
USTRUCT()
struct FCustomStruct
{
    GENERATED_BODY()
    UPROPERTY()
    int Value = 123;
};
//Interface的固定结构定义
UINTERFACE()
class UCustomInterface :public UInterface {
    GENERATED_BODY()
};
class ICustomInterface
{
    GENERATED_BODY()
public:
    UFUNCTION()
    virtual void SayHello() {}
};

위의 매크로 표시된 구조에 대해 UE는 해당 메타데이터를 저장하기 위한 데이터 구조를 생성합니다

  • UCLASS() : UClass, 클래스의 설명 정보를 저장
  • USTRUCT() : UScriptStruct, 구조체의 설명 정보를 저장
  • UENUM() : UEnum, 열거형의 설명 정보를 저장
  • UPROPERTY() : FProperty, 속성의 설명 정보를 저장
  • UFUNCTION() : UFunction, 함수의 설명 정보를 저장
  • UINTERFACE() : 없음

이러한 매크로의 괄호 안에 관련 시스템이 사용할 수 있는 매개변수를 추가할 수 있으며, 자세한 설정은 다음을 참조하십시오:

다음은 리플렉션 사용의 비교적 완전한 예시입니다:

UClass* MetaClassFromGlobalStatic = StaticClass<UCustomClass>();
UClass* MetaClassFromMemberStatic = UCustomClass::StaticClass();            //根据类型通过静态方法获取UClass
UCustomClass* Instance = NewObject<UCustomClass>();
UClass* MetaClassFromInstance = Instance->GetClass();                       //根据实例获取UClass,当实例指针退化为父类时依然有效
UClass* SuperMetaClass = MetaClassFromInstance->GetSuperClass();            //获取父类的UClass
bool IsCustomClass = MetaClassFromInstance->IsChildOf(UCustomClass::StaticClass()); //判断继承关系
UFunction* FuncAdd = MetaClassFromInstance->FindFunctionByName("Add");      //获取函数
UFunction* SuperFunc = FuncAdd->GetSuperFunction();                         //获取父函数,此时为null
/*调用函数*/
int A = 5, B = 10;
uint8* Params = (uint8*)FMemory::Malloc(FuncAdd->ReturnValueOffset);
FMemory::Memcpy((void*)Params, (void*)&A, sizeof(int));
FMemory::Memcpy((void*)(Params + sizeof(A)), (void*)&B, sizeof(int));
FFrame Frame(Instance, FuncAdd, Params, 0, FuncAdd->ChildProperties);
int Result;
FuncAdd->Invoke(Instance, Frame, &Result);
UE_LOG(LogTemp, Warning, TEXT("FuncAdd: a + b = %d"), Result);
/*覆盖函数*/
FuncAdd->SetNativeFunc(execCallSub);
Frame = FFrame(Instance, FuncAdd, Params, 0, FuncAdd->ChildProperties);
FuncAdd->Invoke(Instance, Frame, &Result);
UE_LOG(LogTemp, Warning, TEXT("FuncSub: a - b = %d"), Result);
/*读写属性信息*/
FIntProperty* SimpleProperty = FindFProperty<FIntProperty>(UCustomClass::StaticClass(), "SimpleValue");
int* SimplePropertyPtr = SimpleProperty->ContainerPtrToValuePtr<int>(Instance);   //根据复合结构的地址,得到实际属性的地址
*SimplePropertyPtr = 789;
FMapProperty* ComplexProperty = FindFProperty<FMapProperty>(UCustomClass::StaticClass(), "ComplexValue");
TMap<FString, int>* ComplexPropertyPtr = ComplexProperty->ContainerPtrToValuePtr<TMap<FString, int>>(Instance);
ComplexPropertyPtr->FindOrAdd("Key02", 2);
bool HasBlueprintVisible = SimpleProperty->HasAllPropertyFlags(EPropertyFlags::CPF_BlueprintVisible);
FString MetaData = SimpleProperty->GetMetaData("Keywords");
/*枚举相关*/
UEnum* MetaEnum = StaticEnum<ECustomEnum>();            
UEnum* MetaEnumFromValueName = nullptr; 
UEnum::LookupEnumName("EMetaEnum::One", &MetaEnumFromValueName);        //根据枚举元素的名称获取
for (int i = 0; i < MetaEnum->NumEnums(); i++) {
    UE_LOG(LogTemp, Warning, TEXT("%s : %d - %s"), *(MetaEnum->GetNameByIndex(i).ToString()), MetaEnum->GetValueByIndex(i), *MetaEnum->GetMetaData(TEXT("DisplayName"), i));
}
/*UStruct相关*/
UStruct* MetaStructFromGlobalStatic = StaticStruct<FCustomStruct>();
UStruct* MetaStructFromMemberStatic = FCustomStruct::StaticStruct();
FCustomStruct* StructInstance = new FCustomStruct;

 

CDO(클래스 기본 객체)

UE의 리플렉션 시스템은 UObject에 Class Default Object라는 메커니즘을 제공합니다. 즉, 각 클래스마다 하나의 기본 객체를 가집니다
CDO가 존재함으로써 각 클래스는 전역 싱글톤을 보유하게 됩니다
다음 함수를 통해 CDO를 얻을 수 있습니다:

UObject* UClass::GetDefaultObject(bool bCreateIfNeeded = true) const
{
    if (ClassDefaultObject == nullptr && bCreateIfNeeded)   // 不存在则新建
    {
        UE_TRACK_REFERENCING_PACKAGE_SCOPED((GetOutermost()), PackageAccessTrackingOps::NAME_CreateDefaultObject);
        const_cast<UClass*>(this)->CreateDefaultObject();
    }
    return ClassDefaultObject;
}

UE는 편리한 가져오기 방법도 제공합니다:
const UObject* ConstCDO = GetDefault<UObject>(); //CDO의 Const 포인터 가져오기 UObject* MutableCDO = GetMutableDefault<UObject>(); //수정 가능한 CDO 포인터 가져오기
UE는 NewObject 생성 시 CDO를 템플릿으로 사용하므로, CDO의 속성값을 수정하여 새로운 객체의 초기 속성값을 조정할 수 있습니다.
UE의 디테일 패널은 CDO를 기준으로 현재 객체의 어떤 속성이 CDO와 차이가 있는지 확인합니다:

또한, CDO는 UE의 Config 메커니즘과 밀접하게 연관되어 있습니다:

UCLASS(config = CustomSettings, defaultconfig)
class UCustomSettings : public UObject {
    GENERATED_BODY()
public:
    UPROPERTY(EditAnywhere, Config)
    FString ConfigValue;
};

CDO의 글로벌 싱글톤 특성으로 인해 이를 설정 저장소로 활용할 수 있습니다. UCLASS와 UPROPERTY에 Config 관련 식별자를 추가하면 다음 코드를 통해 프로젝트 디렉토리에 관련 설정을 저장할 수 있습니다:

CDO->SaveConfig(); 
//Instance->LoadConfig(); 
//Instance->TryUpdateDefaultConfigFile();

설정 파일을 에디터의 프로젝트 설정에서도 표시하고 싶다면, 플러그인 시작 시점에 다음 코드를 사용하여 설정 파일을 등록할 수 있습니다:

ISettingsModule* SettingsModule = FModuleManager::GetModulePtr<ISettingsModule>("Settings");
if (SettingsModule) {
    auto Settings = GetMutableDefault<UCustomSettings>();
    SettingsModule->RegisterSettings("Project", 
                                     TEXT("CustomSection"),
                                     TEXT("CustomSettingsName"),
                                     LOCTEXT("CustomSettingsName", "CustomSettingsName"),
                                     LOCTEXT("CCustomSettingsTooltip", "Custom Settings Tooltip"),
                                     Settings);
}

또한 주의해야 할 점은, CDO도 UObject의 생성자를 실행하므로 리소스 소모가 큰 작업을 수행할 때는 CDO의 실행 여부를 고려해야 합니다:

UCustomObject::UCustomObject(const FObjectInitializer& ObjectInitializer){
    if (HasAnyFlags(EObjectFlags::RF_ClassDefaultObject)) {     //判断当前对象是否是CDO
    }
}

직렬화(Serialize)

UObject는 직렬화와 역직렬화를 지원하므로, 모든 UObject는 에셋(Asset)으로 사용될 수 있습니다.
UE의 직렬화 인터페이스는 다음과 같습니다:

class UObject{
    /** 
     * Handles reading, writing, and reference collecting using FArchive.
     * This implementation handles all FProperty serialization, but can be overridden for native variables.
     */
    virtual void Serialize(FArchive& Ar);
};

UObject는 비Transient 리플렉션 속성을 자동으로 직렬화하며, Serialize 함수를 오버라이드하여 FArchive로 비리플렉션 데이터를 추가할 수 있습니다.
FObjectReaderFObjectWriter를 사용하여 UObject와 ByteArray를 상호 변환할 수 있습니다.
리플렉션 속성만 직렬화하려면 다음 코드를 사용할 수 있습니다:

TArray<uint8> Buffer;
FMemoryWriter PropertiesAr(Buffer, true);
CustomObject->SerializeScriptProperties(PropertiesAr);

직렬화에 대한 자세한 내용은 다음을 참조하세요:

  • UE 직렬화 소개 및 소스코드 분석

블루프린트(Blueprint)

언리얼 엔진 리플렉션의 가장 강력한 점은 개발자가 코드 스캔을 통해 리플렉션 데이터를 생성할 수 있을 뿐만 아니라, 직접 리플렉션 데이터 구조를 생성하고 구성할 수 있다는 것입니다
다음은 코드를 사용하여 UClass를 구성하는 예시입니다:

`DEFINE_FUNCTION(execIntToString)                //自定义本地函数
{
    P_GET_PROPERTY(FIntProperty, IntVar);       //获取参数
    P_FINISH;                                   //获取结束
    *((FString*)RESULT_PARAM) = FString::FromInt(IntVar);       //设置返回值
}`

`UClass* CustomClass = NewObject<UClass>(GetTransientPackage(),"CustomClass");       //自定义类
CustomClass->ClassFlags |= CLASS_EditInlineNew;         //设置类标识
CustomClass->SetSuperStruct(UObject::StaticClass());    //设置父类
FIntProperty* IntProp = new FIntProperty(CustomClass, TEXT("IntProp"), EObjectFlags::RF_NoFlags);//创建Int属性
FStrProperty* StringProp = new FStrProperty(CustomClass, TEXT("StringProp"), EObjectFlags::RF_NoFlags);
UPackage* CoreUObjectPkg = FindObjectChecked<UPackage>(nullptr, TEXT("/Script/CoreUObject"));   //创建Vector结构体属性
UScriptStruct* VectorStruct = FindObjectChecked<UScriptStruct>(CoreUObjectPkg, TEXT("Vector"));
FStructProperty* VectorProp = new FStructProperty(CustomClass, "VectorProp", RF_NoFlags);
VectorProp->Struct = VectorStruct;
CustomClass->ChildProperties = IntProp;                 //CustomClass->ChildProperties 是一个链表节点,通过链表来链接属性
IntProp->Next = StringProp;                 
StringProp->Next = VectorProp;
UFunction* CustomFucntion = NewObject<UFunction>(CustomClass, "IntToString", RF_Public);
CustomFucntion->FunctionFlags = FUNC_Public | FUNC_Native;  //本地函数指的是本地C++函数,否则是指蓝图脚本中的函数代码
FIntProperty* IntParam = new FIntProperty(CustomFucntion, "IntParam", EObjectFlags::RF_NoFlags);
IntParam->PropertyFlags = CPF_Parm;                         //用作函数参数
FStrProperty* StringReturnParam = new FStrProperty(CustomFucntion, "StringReturnParam", EObjectFlags::RF_NoFlags);
StringReturnParam->PropertyFlags = CPF_Parm | CPF_ReturnParm;       // 用作函数返回值(必须包含CPF_Parm)
CustomFucntion->ChildProperties = IntParam;                         // 函数返回值必须是最后的节点,用于计算后续的参数偏移
IntParam->Next = StringReturnParam;
CustomFucntion->SetNativeFunc(execIntToString);                     // 设置本地函数
CustomFucntion->Bind();                                                 
CustomFucntion->StaticLink(true);                                   // 链接所有属性,此操作会生成Property的内存大小、偏移值..
CustomFucntion->Next = CustomClass->Children;                       // 将Function添加Class Children链表的头部,以便Field访问
CustomClass->Children = CustomFucntion;
CustomClass->AddFunctionToFunctionMap(CustomFucntion, CustomFucntion->GetFName());  //将自定义函数添加到自定义类的函数表中
CustomClass->Bind();
CustomClass->StaticLink(true);                          // 链接所有属性
CustomClass->AssembleReferenceTokenStream();            // 关联属性的GC,完成自定义类的构建
UObject* CDO = CustomClass->GetDefaultObject();         
// 对CDO中的属性进行赋值
void* IntPropPtr = IntProp->ContainerPtrToValuePtr<void*>(CDO);
IntProp->SetPropertyValue(IntPropPtr, 789);
FVector Vector(1, 2, 3);
void* VectorPropPtr = VectorProp->ContainerPtrToValuePtr<void>(CDO);
VectorProp->CopyValuesInternal(VectorPropPtr, &Vector, 1);
// 调用自定义函数
int InputParam = 12345;
UFunction* Func = CustomClass->FindFunctionByName("IntToString");
uint8* Params = (uint8*)FMemory::Malloc(Func->ReturnValueOffset);       //分配参数缓冲区
FMemory::Memcpy((void*)Params, (void*)&InputParam, sizeof(int));
FFrame Frame(CDO, Func, Params, 0, Func->ChildProperties);
FString ReturnParam;
Func->Invoke(CDO, Frame, &ReturnParam);`

이후에는 직접 만든 UClass를 사용하여 UObject를 생성할 수도 있습니다:
UObject* CustomObject = NewObject<UObject>(GetTransientPackage(), CustomClass);
그리고 언리얼 엔진의 블루프린트는 위의 작업들을 래핑하고 관련 에디터를 제공한 것에 불과합니다.
언리얼 엔진의 블루프린트는 UBlueprint라는 데이터 구조에 대응되며, 블루프린트 에디터에서 수행하는 대부분의 작업(속성, 함수 조작, 노드 편집, 클래스 설정 등)은 기본적으로 UBlueprint의 매개변수를 수정하는 것입니다:

다음 인터페이스를 사용하여 블루프린트를 생성할 수 있습니다:

static UBlueprint* FKismetEditorUtilities::CreateBlueprint(
    UClass* ParentClass,
    UObject* Outer, 
    const FName NewBPName,
    enum EBlueprintType BlueprintType,
    TSubclassOf<UBlueprint> BlueprintClassType,
    TSubclassOf<UBlueprintGeneratedClass> BlueprintGeneratedClassType, 
    FName CallingContext = NAME_None);

매개 변수의 의미는 다음과 같습니다.:

  • ParentClass : 부모 클래스, 생성된 블루프린트가 이 클래스의 속성과 메서드를 상속받게 됩니다
  • Outer : 생성될 블루프린트의 Outer(소유자)
  • NewBPname : 생성될 블루프린트의 이름
  • BlueprintType : 블루프린트의 유형, 다음 값들 중 하나가 될 수 있습니다:
enum EBlueprintType
{
    /** Normal blueprint. */
    BPTYPE_Normal               UMETA(DisplayName="Blueprint Class"),
    /** Blueprint that is const during execution (no state graph and methods cannot modify member variables). */
    BPTYPE_Const                UMETA(DisplayName="Const Blueprint Class"),
    /** Blueprint that serves as a container for macros to be used in other blueprints. */
    BPTYPE_MacroLibrary         UMETA(DisplayName="Blueprint Macro Library"),
    /** Blueprint that serves as an interface to be implemented by other blueprints. */
    BPTYPE_Interface            UMETA(DisplayName="Blueprint Interface"),
    /** Blueprint that handles level scripting. */
    BPTYPE_LevelScript          UMETA(DisplayName="Level Blueprint"),
    /** Blueprint that serves as a container for functions to be used in other blueprints. */
    BPTYPE_FunctionLibrary      UMETA(DisplayName="Blueprint Function Library"),
    BPTYPE_MAX,
};

 

  • BlueprintClassType : 블루프린트 클래스의 기본 템플릿으로, 기본값으로 UBlueprint::StaticClass()를 사용할 수 있으며, 이를 통해 블루프린트 설정을 커스터마이징할 수 있습니다
  • BlueprintGeneratedClassType : 블루프린트 생성 클래스의 기본 템플릿으로, 기본값으로 UBlueprintGeneratedClass::StaticClass()를 사용할 수 있으며, 생성 전략을 커스터마이징하는 데 사용됩니다
  • CallingContext : 호출 컨텍스트로, 기본값은 비어 있습니다

블루프린트 생성 로직은 다음과 같이 간단히 설명할 수 있습니다:

UBlueprint* FKismetEditorUtilities::CreateBlueprint(
    UClass* ParentClass,
    UObject* Outer,
    TSubclassOf<UBlueprint> BlueprintClassType,
    TSubclassOf<UBlueprintGeneratedClass> BlueprintGeneratedClassType)
{
    UBlueprint* NewBP = NewObject<UBlueprint>(Outer,*BlueprintClassType);   //根据BlueprintClassType创建实例
    NewBP->ParentClass = ParentClass;
    UBlueprintGeneratedClass* NewGenClass =                                 //根据BlueprintGeneratedClassType创建实例
        NewObject<BlueprintGeneratedClassType>(NewBP->GetOutermost(),*BlueprintGeneratedClassType);
    NewGenClass->SetSuperStruct(ParentClass)            //设置NewGenClass的父类为ParentClass,这样就能访问其属性和方法
    NewBP->GeneratedClass = NewGenClass;                //将新生成的UClass赋值给NewBP->GeneratedClass 
}

블루프린트 에디터에서 컴파일 버튼을 클릭할 때

실제로 호출되는 함수는 다음과 같습니다:

static void FKismetEditorUtilities::CompileBlueprint(UBlueprint* BlueprintObj, 
                             EBlueprintCompileOptions CompileFlags = EBlueprintCompileOptions::None,
                             class FCompilerResultsLog* pResults = nullptr );

이 함수의 목적은 블루프린트 에디터의 속성 목록과 로직 그래프, 그리고 ParentClass를 기반으로 UBlueprint의 멤버 변수 GeneratedClass를 생성하는 것입니다
일반적으로 C++ 측에서 블루프린트를 사용하는 단계는 다음과 같습니다:

UClass * BlueprintGenClass = LoadObject<UClass>(nullptr, "/Game/NewBlueprint.NewBlueprint_C");      //获取蓝图中的生成类
UObject* Instance = NewObject<UObject>(GetTransientPackage(),Blueprint->GeneratedClass);            //根据类来创建对象实例
UFunction* Func = Blueprint->GeneratedClass->FindFunctionByName("FuncName");     // 在类反射数据中查找名称为[FuncName]的函数
Instance->ProcessEvent(Func, nullptr);                                           // 调用蓝图中定义的函数
FProperty* Prop = FindFProperty<FProperty>(Blueprint->GeneratedClass, "IntProp");// 在类反射数据中查找名称为[IntProp]的属性
int value = 0;
Prop->GetValue_InContainer(Instance,&value);        //获取属性
Prop->SetValue_InContainer(Instance,&value);        //设置属性

/Game/NewBlueprint.NewBlueprint_C를 사용하는 것은 UBlueprint의 멤버 변수 GeneratedClass를 얻는 것입니다. 여기서 /Game/NewBlueprint는 패키지 경로이고, NewBlueprint_C는 멤버 변수 GeneratedClass의 객체 이름입니다.
UBlueprint에서 생성된 UClass의 경우, UClass의 멤버 변수 ClassGeneratedBy를 통해 이를 생성한 UBlueprint를 얻을 수 있으며, 다음 함수를 사용할 수도 있습니다:

static UBlueprint* UBlueprint::GetBlueprintFromClass(const UClass* InClass);

블루프린트의 작동 방식을 이해하고 나면 블루프린트를 사용하는 것이 어렵지 않습니다. 다음은 코드를 사용하여 블루프린트를 만들고 편집기를 여는 예제입니다:

UClass* ParentClass = NewObject<UClass>();      //创建ParentClass
ParentClass->SetSuperStruct(UObject::StaticClass());
ParentClass->ClassFlags = CLASS_Abstract | CLASS_MatchedSerializers | CLASS_Native | CLASS_ReplicationDataIsSetUp | CLASS_RequiredAPI | CLASS_TokenStreamAssembled | CLASS_HasInstancedReference  | CLASS_Constructed;
//为Class添加事件函数
UFunction* BPEvent = NewObject<UFunction>(ParentClass, "BlueprintEvent", RF_Public | RF_Transient);
BPEvent->FunctionFlags = FUNC_Public | FUNC_Event | FUNC_BlueprintEvent;
BPEvent->Bind();
BPEvent->StaticLink(true);
BPEvent->Next = ParentClass->Children;              //将函数添加到Parent的Field中
ParentClass->Children = BPEvent;
ParentClass->AddFunctionToFunctionMap(BPEvent, "BlueprintEvent");
ParentClass->Bind();
ParentClass->StaticLink(true);
ParentClass->AssembleReferenceTokenStream(true);
UBlueprint* NewBP = FKismetEditorUtilities::CreateBlueprint(                        //创建蓝图
    ParentClass,
    GetTransientPackage(),
    "NewBP",
    EBlueprintType::BPTYPE_Normal,
    UBlueprint::StaticClass(),
    UBlueprintGeneratedClass::StaticClass());
int32 NodePositionY = 0;
//在图表中创建事件节点
FKismetEditorUtilities::AddDefaultEventNode(NewBP, NewBP->UbergraphPages[0], "BlueprintEvent", ParentClass, NodePositionY); 
//当事件不存在时会创建自定义事件
FKismetEditorUtilities::AddDefaultEventNode(NewBP, NewBP->UbergraphPages[0], "CustomEvent", ParentClass, NodePositionY);    
//打开蓝图编辑器
FBlueprintEditorModule& BlueprintEditorModule = FModuleManager::LoadModuleChecked<FBlueprintEditorModule>("Kismet");
BlueprintEditorModule.CreateBlueprintEditor(EToolkitMode::Standalone, nullptr, NewBP);

.... 파트 2에서 다시...


원문

0 - 基础编程 - Modern Graphics Engine Guide

Unreal Engine C++ 编程文档 前言 Unreal Engine 是一个由 C++ 编写的 强大引擎,但由于 构建工具(UBT) 和 反射编译器(UHT) 的存在 ,导致它有着独立于C++标准的语法,因此网友们也戏称它为 U++ 。 不

italink.github.io