역자의 글.
봄이 오는가 싶더니 여전히 춥기도 하고 이게 지금 봄이 되는건지 겨울이 되는건지 알 길이 없을만큼 참 다이나믹한 날씨가 계속 되고 있어요. 그런데 말이죠. 생각해보면 우리들의 인생은 예전부터 계속 이래왔던게 아닌가 싶습니다. ㅎㅎ 갑자기 좀 센치해져봤어요.
여전히 Italink 군의 멋진 정리글을 공유 해 봅니다.
에셋 시스템
NewObject의 함수 원형은 다음과 같습니다:
template< class T >
T* NewObject(UObject* Outer = (UObject*)GetTransientPackage());
여기서 Outer는 부모 객체로 간단히 이해할 수 있으며, 주의할 점은 Outer가 UObject의 GC와는 관련이 없고, 에셋 저장 구조상의 관계를 지정하는 역할을 한다는 것입니다.
UE에서 에셋 객체는 물리적 파일과 동일하지 않습니다. 실제로 디스크상의 *.uasset과 *.umap 파일은 UPackage 객체에 대응되는 것으로 이해할 수 있습니다.
UE에서 에셋을 생성할 때는 먼저 실제 물리적 파일 매핑으로 UPackage를 생성하고, 그 다음 NewObject를 사용하여 실제 에셋을 생성합니다. 이때 에셋의 Outer를 Package로 지정하며, Package를 저장할 때 해당 Package가 보유한 모든 UObject를 물리적 파일로 직렬화합니다. 이 과정은 다음과 같이 보입니다:
// 创建一个新Package,/Game/CustomPackage为包路径,其中/Game代表工程的Content目录
UPackage* CustomPackage = CreatePackage(nullptr, TEXT("/Game/CustomPackage"));
// 创建一个新的资产,可以是任意UObject(这里使用的UObject是抽象类,会有警告)
UObject* CustomInstance = NewObject<UObject>(CustomPackage, UObject::StaticClass(),
TEXT("CustomInstance"),
EObjectFlags::RF_Public | EObjectFlags::RF_Standalone);
// 获取Package的本地磁盘路径
FPackagePath PackagePath = FPackagePath::FromPackageNameChecked(CustomPackage->GetName());
const FString PackageFileName = PackagePath.GetLocalFullPath();
// 将包存储到磁盘上
const bool bSuccess = UPackage::SavePackage(CustomPackage,
CustomInstance,
RF_Public | RF_Standalone,
*PackageFileName,
GError, nullptr, false, true, SAVE_NoError);
이 작업은 프로젝트의 Content 디렉토리에 CustomPackage.uasset 파일을 생성합니다.

디스크에 있는 패키지와 에셋 객체는 다음 코드를 통해 불러올 수 있습니다:
UPackage* LoadedPackage = LoadPackage(nullptr, TEXT("/Game/CustomPackage"), LOAD_None);
UObject* LoadedAsset = LoadObject<UObject>(nullptr,TEXT("/Game/CustomPackage.CustomInstance"));
해당 에셋을 UE 에디터에서 생성하고 미리보기하려면 다음이 필요합니다:
- UFactory를 상속받아 에셋 객체의 생성 팩토리를 정의
- FAssetTypeActions_Base를 상속받아 에디터에서의 미리보기, 메뉴 등의 동작을 정의
에셋 관리에 대한 자세한 내용은 다음을 참조하세요:
게임플레이 프레임워크
여기 매우 상세한 글들이 있어 더 이상의 설명은 생략하겠습니다:
- 지후 - 하이거우 님 | UE 게임플레이 프레임워크: UObject, Actor, Component
- 지후 - 하이거우 님 | UE 게임플레이 프레임워크: WorldContext, GameInstance, Engine, UGameplayStatics
- 지후 - 하이거우 님 | UE 게임플레이 프레임워크: World, Level, WorldSetting, Level Blueprint
- 지후 - 하이거우 님 | UE 게임플레이 프레임워크: Pawn, Controller, APlayerState
- 지후 - 하이거우 님 | UE 게임플레이 프레임워크: GameMode, GameState
이러한 구조 체계를 이해하고 나면, 다음과 같은 과감한 코드도 작성해볼 수 있습니다. 다음은 정적 모델의 오프라인 렌더링 예제입니다:
void Render(UStaticMesh* StaticMesh, FString OutFileName, FTransform CameraTransform, int32 InResolution)
{
EWorldType::Type WorldType = EWorldType::Editor;
UWorld* World = UWorld::CreateWorld(WorldType, false, "RenderWorld"); // 手动创建UWorld
UWorld* LastWorld = GWorld;
GWorld = World;
FWorldContext& EditorContext = GEditor->GetEditorWorldContext();
EditorContext.SetCurrentWorld(World); // Sky Light 的更新依赖于 GEgnine 的 Tick,它需要保证 GWorld == EditorContext.World()
const FURL URL;
World->InitializeActorsForPlay(URL);
World->BeginPlay(); // 手动执行UWorld的开始事件
FakeEngineTick(World, 0.03f);
AStaticMeshActor* Actor = World->SpawnActor<AStaticMeshActor>();
UStaticMeshComponent* StaticMeshComp = Actor->GetStaticMeshComponent();
ASkyAtmosphere* SkyAtmosphereActor = World->SpawnActor<ASkyAtmosphere>();
AExponentialHeightFog* ExponentialHeightFogActor = World->SpawnActor<AExponentialHeightFog>();
ADirectionalLight* DirectionalLightActor = World->SpawnActor<ADirectionalLight>();
UDirectionalLightComponent* DirectionalLightComponent = DirectionalLightActor->GetComponent();
DirectionalLightComponent->SetIntensity(12);
ASkyLight* SkyLightActor = World->SpawnActor<ASkyLight>();
USkyLightComponent* SkyLightComponent = SkyLightActor->GetLightComponent();
SkyLightComponent->SetMobility(EComponentMobility::Movable);
SkyLightComponent->bLowerHemisphereIsBlack = false;
SkyLightComponent->SetIntensity(2);
SkyLightComponent->RecaptureSky(); // 请求刷新天光
FakeEngineTick(World, 0.03f); // 执行等待渲染更新的Tick完成
UTextureRenderTarget2D* RenderTarget = NewObject<UTextureRenderTarget2D>(); // 初始化RT的数据
RenderTarget->RenderTargetFormat = ETextureRenderTargetFormat::RTF_RGBA16f;
RenderTarget->InitAutoFormat(InResolution, InResolution);
RenderTarget->UpdateResourceImmediate(true);
FakeEngineTick(World, 0.03f); // 等待RT的资源提交
ETextureRenderTargetFormat RenderTargetFormat = RenderTarget->RenderTargetFormat;
FTextureRenderTargetResource* RenderTargetResource = RenderTarget->GameThread_GetRenderTargetResource();
ASceneCapture2D* SceneCaptureActor = World->SpawnActor<ASceneCapture2D>();
USceneCaptureComponent2D* SceneCaptureComp = SceneCaptureActor->GetCaptureComponent2D();
SceneCaptureComp->TextureTarget = RenderTarget;
SceneCaptureComp->bCaptureEveryFrame = false;
SceneCaptureComp->bOverride_CustomNearClippingPlane = true;
SceneCaptureComp->CustomNearClippingPlane = 0.01f;
SceneCaptureComp->CaptureSource = ESceneCaptureSource::SCS_FinalColorLDR;
SceneCaptureComp->ShowFlagSettings.Add(FEngineShowFlagsSetting{ TEXT("DistanceFieldAO"), false });
SceneCaptureComp->SetShowFlagSettings(SceneCaptureComp->ShowFlagSettings);
FakeEngineTick(World); // 等待SceneCapture的状态更新
SceneCaptureActor->SetActorTransform(CameraTransform); // 执行捕获
SceneCaptureComp->CaptureScene();
TArray<FLinearColor> Colors;
FReadSurfaceDataFlags ReadPixelFlags(RCM_MinMax);
FIntRect IntRegion(0, 0, RenderTarget->SizeX, RenderTarget->SizeY);
RenderTargetResource->ReadLinearColorPixels(Colors, ReadPixelFlags, IntRegion);
FImageView ImageView(Colors.GetData(), RenderTarget->SizeX, RenderTarget->SizeY);
FImageUtils::SaveImageByExtension(*OutFileName, ImageView); // 保存图像
World->DestroyWorld(false); // 还原状态
GEngine->DestroyWorldContext(World);
GWorld = LastWorld;
}
// 手动执行引擎的Tick更新
static void FakeEngineTick(UWorld* InWorld, float InDelta = 0.03f)
FApp::SetDeltaTime(InDelta);
GEngine->EmitDynamicResolutionEvent(EDynamicResolutionStateEvent::EndFrame);
GEngine->Tick(InDelta, false); // 其中包含了当前World的更新
FSlateApplication::Get().PumpMessages();
FSlateApplication::Get().Tick();
GFrameCounter++;
bool bIsTicking = FSlateApplication::Get().IsTicking();
if (!bIsTicking && GIsRHIInitialized) {
if (FSceneInterface* Scene = InWorld->Scene) {
ENQUEUE_RENDER_COMMAND(BeginFrame)([](FRHICommandListImmediate& RHICmdList) {
GFrameNumberRenderThread++;
GFrameCounterRenderThread++;
FCoreDelegates::OnBeginFrameRT.Broadcast();
});
ENQUEUE_RENDER_COMMAND(EndFrame)([](FRHICommandListImmediate& RHICmdList) {
FCoreDelegates::OnEndFrameRT.Broadcast();
RHICmdList.EndFrame();
});
FlushRenderingCommands();
}
ENQUEUE_RENDER_COMMAND(VirtualTextureScalability_Release)([](FRHICommandList& RHICmdList) {
GetRendererModule().ReleaseVirtualTexturePendingResources();
});
}
FTaskGraphInterface::Get().ProcessThreadUntilIdle(ENamedThreads::GameThread);
FSlateApplication::Get().GetRenderer()->Sync();
FThreadManager::Get().Tick();
FTSTicker::GetCoreTicker().Tick(InDelta);
GEngine->TickDeferredCommands();
GEngine->EmitDynamicResolutionEvent(EDynamicResolutionStateEvent::EndFrame);
FAssetCompilingManager::Get().FinishAllCompilation();
}
기타
델리게이트
UE에서는 델리게이트(Delegate) 개념을 사용하여 객체 간 통신을 수행하며, 이를 통해 범용적이고 타입 안전한 방식으로 C++ 함수를 호출할 수 있습니다.
Qt에 익숙하다면, 델리게이트는 Qt의 시그널-슬롯(Signal-Slot) 중 시그널에 해당한다고 볼 수 있습니다. 다만 델리게이트가 시그널보다 더 강력하지만, 사용법은 더 복잡해졌죠.가장 아쉬운 점은 이름인데, 만약 Signal이라고 불렀다면 개발자들이 더 쉽게 연상하여 올바른 사용법을 떠올릴 수 있었을 것입니다.
다음은 델리게이트를 사용하는 예시입니다(Signal이라는 이름을 사용한 의사 코드):
class Student{
Signal nameChanged(string OldName, string NewName);
public:
void setName(string inName){
string oldName = mName;
mName = inName;
nameChanged.Emit(oldName,mName);
}
private:
string mName;
};
class Tearcher{
public:
void OnStudentNameChanged(string OldName, string NewName){
// do something
}
};
class Class{
protected:
void connectEveryone(){
for(auto Student:mStudents){
Student.nameChanged.Connect(mTeacher,&Tearcher::OnStudentNameChanged);
}
}
private:
Teacher mTeacher;
TArray<Student> mStudents;
};
이 예시에서 connectEveryone 함수가 수행하는 기능은 다음과 같이 볼 수 있습니다: "선생님"이 교실에서 학생들에게 "이름을 바꾸면 저에게 알려주세요"라고 말하는 것입니다.
여기서는 Teacher와 Student의 구조를 직접 수정하지 않고, 제3자(교실)에서 두 객체의 관계를 연결함으로써 코드의 결합도를 낮추었습니다.
만약 델리게이트를 사용하지 않고 같은 기능을 구현한다면, 다음과 같은 지저분한 코드가 될 수 있습니다:
class Student{
public:
Student(Class* InClass) //需要传递上一级逻辑域的指针
: mClass(InClass){}
void setName(string inName){
string oldName = mName;
mName = inName;
//不安全的链式调用,且上下文跨度较大的调用会让后续的维护和迭代变得困难
mClass->mTeacher->OnStudentNameChanged(OldName,mName);
}
private:
string mName;
Class* mClass
};
class Tearcher{
public:
void OnStudentNameChanged(string OldName, string NewName){
// do something
}
};
class Class{
friend class Student;
private:
Teacher mTeacher;
TArray<Student> mStudents;
};
UE 델리게이트 사용법에 대한 자세한 내용은 다음을 참조하세요:
파트 3 에서 Slate 부터 다시 시작...
'TECH.ART.FLOW.IO' 카테고리의 다른 글
게임 투자자분들의 진솔한 이야기 2 (0) | 2025.04.17 |
---|---|
[번역][흥미로운]Laplacian Mesh Smoothing by Throwing Vertices (0) | 2025.04.16 |
[번역][연재물] 언리얼 엔진 개발 가이드 Part-1 (0) | 2025.04.16 |
[번역] The Eras of GPU Development (0) | 2025.04.13 |
Unity Learning Materials by Unity Japan. (0) | 2025.04.09 |