Engine

Engine

2025년 7월 8일

기본개념 #

게임엔진 #

게임 엔진이라는것은 지나간 시간만큼 진행된 상태를 현재 화면에 시뮬레이션 하는것이다.
그래서 델타타임은 직전 프레임을 그리는동안 걸리는 시간인거고, 현재 시간에 맞춰 스냅샷해서 렌더링하는 것이다.

b0a6c09e-97a9-4bf1-8130-88ad7ca8457d

이전 프레임의 렌더링완료 시점에 다음 프레임의 상태가 결정된다.
정확히 말하자면 기준은 렌더링이 아니라 Tick이고, 더 자세히 말하면 프레임을 그릴 준비를 하고 스냅샷을 찍는 순간(델타타임 업데이트 등) 시점의 상태일 것이다.

어차피 눈으로 들어오는것, 뇌가 인지하는것 생각해보면 화면에 보이는 시점이 실제 동작한 시점과 차이난다고 하더라도 인간은 인지하지 못한다.


UEditorEngine #

FEngineLoop #

FEngineLoop은 전역적으로 관리되는 엔진 객체이다. 실질적으로 엔진이 한 틱에 실행하게되는 모든 작업들이 포함되며, GuardedMain의 메인루프 안에서 이 클래스의 Tick을 호출해준다.

 1ENGINE_API UEngine* GEngine = NULL;
 2
 3class FEngineLoop : public IEngineLoop
 4{
 5public:
 6    // GuardedMain -> EnginePreInit 에서 이 함수를 호출하여 GEngineLoop를 초기화한다.
 7    int32 PreInit(const TCHAR* CmdLine)
 8    {
 9        //...
10    }
11
12    // 9 - Foundation - Entry - FEngineLoop::Tick
13    virtual void Tick() override
14    {
15        {
16            // FApp::CurrentTime / FApp::DeltaTime / FApp::GameTime 을 상황에 맞게 갱신하는 작업을 한다.
17            // 프레임 속도에 다른 보정
18            // - 최대FPS보다 빨리돌아서 시간이 남았다면 슬립해서 스케줄러에게 양보
19            // - 고정FPS에서 늦었다면 논리시간을 한프레임만큼만 고정해버림 -> 유저는 슬로우모션처럼 느낌 -> 최적화 필요 신호
20            GEngine->UpdateTimeAndHandleMaxTickRate();
21        }
22
23        // main game engine tick (world, game objects, etc)
24        // 위에서 세팅한 델타타임을 엔진틱에 넘겨준다. (델타타임 만큼 데이터를 업데이트 해야하니)
25        // 상속구조: ULyraEditorEngine -> UUnrealEdEngine -> UEditorEngine -> UEngine -> UObject
26        // - 시작프로젝트로 lyra를 선택했기 때문에 엔진 상속구조가 있어서 각각의 생성자, Init, Tick이 있지만,
27        // - 건너뛰고 UEditorEngine::Tick 부터 진행한다.
28        GEngine->Tick(FApp::GetDeltaTime(), bIdleMode);
29    }
30};

UEditorEngine #

 1class UEditorEngine : public UEngine
 2{
 3public:
 4    /** returns the WorldContext for the editor world. for now, there will always be exactly 1 of these in the editor */
 5    // 16 - Foundation - Entry - GetEditorWorldContext
 6    FWorldContext& GetEditorWorldContext(bool bEnsureIsGWorld = false)
 7    {
 8        // WorldList 는 UEngine에 있다. 리스트에서 Editor WorldContext를 찾는다.
 9        for (int32 i = 0; i < WorldList.Num(); ++i)
10        {
11            if (WorldList[i].WorldType == EWorldType::Editor)
12            {
13                return WorldList[i];
14            }
15        }
16        // 여기로 오면 무조건 크래시. WorldContext는 UEngine::Init()에서 초기화해서 무조건있다.
17        check(false);
18        return CreateNewWorldContext(EWorldType::Editor);
19    }
20
21    // 10 - Foundation - Entry - UEditorEngine::Tick
22    // see UEditorEngine
23    virtual void Tick(float DeltaSeconds, bool bIdleMode) override
24    {
25        // 15 - Foundation - Entry
26        // GWorld는 UEngine의 Init에서 생성된다. 
27        UWorld* CurrentGWorld = GWorld;
28
29        // 에디터 WorldContext를 가져온다. 
30        FWorldContext& EditorContext = GetEditorWorldContext();
31    }
32};

UEngine #

 1// 12 - Foundation - Entry - UEngine
 2// UObject는 GC에 의해 관리되는 객체 
 3class UEngine : public UObject, public FExec
 4{
 5    // 월드컨텍스트 생성
 6    FWorldContext& CreateNewWorldContext(EWorldType::Type WorldType)
 7    {
 8        FWorldContext* NewWorldContext = new FWorldContext;
 9        WorldList.Add(NewWorldContext);
10        NewWorldContext->WorldType = WorldType;
11        NewWorldContext->ContextHandle = FName(*FString::Printf(TEXT("Context_%d"), NextWorldContextHandle++));
12        return *NewWorldContext;
13    }
14
15    // 17 - Foundation - Entry - UEngine::Init
16    // GuardedMain 에서 호출된다. (GuardedMain > EditorInit > EngineInit ...)
17    virtual void Init(IEngineLoop* InEngineLoop)
18    {
19        // 에디터인 경우
20        if (GIsEditor)
21        {
22            // Editor WorldContext를 만든다.
23            FWorldConext& InitialWorldContext = CreateNewWorldContext(EWorldType::Editor);
24
25            // Editor 타입의 World 도 생성해서 WorldContext 에 저장하고, GWorld에 전역으로 넣는다.
26            InitialWorldContext.SetCurrentWorld(UWorld::CreateWorld(EWorldType::Editor, true));
27            GWorld = InitialWorldContext.World();
28        }
29    }
30
31    // UEngine이 WorldContext 를 생성, 파괴하며 관리한다.
32    // TArray: 메모리상 연속된 객체 배열이고(캐시히트율 높음)
33    // TIndirectArray: 객체 포인터배열 인데 소유권까지 관리해서 누수걱정은없다.
34    TIndirectArray<FWorldContext> WorldList;
35    int32 NextWorldContextHandle;
36};

FWorldContext #

UEngine 계열 클래스 내부에서 UWorld*를 구분하거나 추적하기 위한 메타데이터 구조체이다.

언리얼엔진은 UWorld 객체로 하나의 월드를 표현하고 그 위에 레벨을 로드, 언로드 하는 방식으로 동작한다. 에디터, 게임, PlayInEditor, 멀티플레이 등의 모드나 로비에서 인게임 진입 시 여러개의 월드가 동시에 존재할 수 있다. 이때 관리하기 위함

FWorldContext는 UEngine(혹은 상속받은 라일라같은애들) 내부에서만 수정하고, 외부 코드(게임플레이, 플러그인)에서는 UWorld*만 사용한다(읽기전용 컨텍스트 조회는 가능).
외부에서 UWorld*를 엔진 함수로 전달해주면 엔진에서는 대응하는 FWorldContext를 역매핑해서 사용한다.

WorldContext 에 AddRef, SetCurrentWorld 로 UWorld*를 저장하고 동기화하기 때문에 엔진에서 업데이트한 월드를 외부 코드에서도 최신 월드를 참조할수 있게된다.

 1namespace EWorldType
 2{
 3    enum Type
 4    {
 5        None,
 6        Game,
 7        Editor,
 8        PIE,
 9    };
10}
11
12// 13 - Foundation - Entry - FWorldContext
13struct FWorldContext
14{
15    void SetCurrentWorld(UWorld* World)
16    {
17        UWorld* OldWorld = ThisCurrentWorld;
18        ThisCurrentWorld = World;
19
20        if (OwningGameInstance)
21        {
22            OwningGameInstance->OnWorldChanged(OldWorld, ThisCurrentWorld);
23        }
24    }
25
26    TEnumAsByte<EWorldType::Type> WorldType;
27    
28    // Context의 이름을 지정할 수 있다. UWorld::GetFName() 과는 다름.
29    FName ContextHandle;
30
31    // 이 WorldContext를 소유한 UGameInstance. 일반적으로 1:1 매핑된다.
32    TObjectPtr<class UGameInstance> OwningGameInstance;
33
34    // 이 Context가 참조하는 UWorld
35    TObjectPtr<UWorld> ThisCurrentWorld;
36};
comments powered by Disqus