게임 엔진 개발기 #1

0편에서 말했 듯, 게임 엔진 개발에 가장 중요한게 프로젝트의 구조인 것 같습니다. 그래서 이번에는 구조를 좀 정해볼려고 하는데 결국은 언리얼의 모듈 방식이 가장 좋은 것 같습니다. 그래서 언리얼 방식으로 Target.cs 랑 Module.cs 을 분석해서 프로젝트를 구성하는 기능을 구현해봤습니다.

유틸리티 프로젝트 (a.k.a Framework Utility Tool)

유틸리티 프로젝트를 만들기 직전 고민했던 것 중 하나가 프로젝트를 UHT처럼 C#으로 만들지 아니면 기존 프로젝트에 사용했던 것처럼 C++로 만들지였습니다.

기능 유틸리티 프로젝트에 기능 명세로 생각한 것들은 Project 파일을 생성하고, Header를 분석해서 클래스 구조를 자동 생성하고, 프로젝트의 3rd party 라이브러리를 자동으로 세팅해주는 것 입니다.

결국 c#을 사용하기로 결정했는데, 이유는 이전 프로젝트에서 c++을 사용해서 위 기능들을 구현했었는데 다 좋고 clang도 연결해서 header 분석도 하고 했는데, 가장 큰 단점이 생산성이 낮고 빌드하는데 오래 걸린다는 점이었습니다. 그래서 c#을 알아보던 도중 nuget 에 clang wrapper 라이브러리들이 존재하는 것을 깨닫고 c#을 사용하기로 결정했습니다.

해당 구현은 다음 프로젝트 등을 참조했습니다.

프로젝트 생성

예시

using FrameworkUtilityTool.Configuration;

public class DX11RHI : ModuleRules
{
    public DX11RHI(TargetRules target) : base(target)
    {
        PublicIncludePaths.AddRange(new string[] { "Public" });
        PrivateIncludePaths.AddRange(new string[] { "Private" });

        PublicDependencyModuleNames.AddRange(new string[] {
            "Core",
            "RHI",
        });

        PrivateThirdPartyModuleNames.AddRange(new string[] {
            "DX11",
        });
    }
}

이런식으로 Module을 구성했습니다.

다만 Target.cs는 구현에 조금 복잡한 감이 있어서 일단은 Module.cs로 전체 프로젝트를 구성했습니다.

그래서 이런식으로 구조가 만들어지게 프로젝트를 구성했습니다.

Header 생성

QCLASS()
class RHI_API QTexture : public QObject {
protected:
    QPROPERTY()
    uint32_t Width = 0;

    QPROPERTY()
    uint32_t Height = 0;

    QPROPERTY()
    uint32_t Depth = 0;

    QPROPERTY()
    EPixelFormat Format = EPixelFormat::RGBA8;

    QPROPERTY()
    uint32_t RowPitch = 0;

    QPROPERTY()
    uint32_t SlicePitch = 0;
    ...
}

일단 예시로 Class에 Macro를 활용해서 유틸리티 툴이 클래스를 어떻게 파싱할지 표시해주는 방식을 구상해봤습니다. 기본 베이스는 언리얼의 그것이지만 자세하게 들어가면 달라질 것 같습니다.

#include "Texture.h"

QClass *Generated_Initializer_Class_ACube();
static FInitClassOnStart Generated_InitClassOnStart_Class_ACube(&Generated_Initializer_Class_ACube, &ACube::StaticClass, TEXT("ACube"), TEXT(""));

IMPLEMENT_CLASS(ACube)

QClass *Generated_Initializer_Class_ACube() {
    static QClass *Instance = nullptr;
    if (!Instance) {
        Instance = ACube::StaticClass();
        Instance->SetParentClass(QObject::StaticClass());
    }

    return Instance;
}

내부적으로는 이런식으로 정보들을 정의하게 하여 일단은 Class 정보를 가지고 있게 구상했습니다.

이렇게 구상 후 구현을 하던 도중 문제가 생겼는데 생각보다 clang의 분석에 시간이 오래 걸리고, 결정적으로 소스 코드까지 필요한 점이었습니다. 다행히도 소스 코드가 필요한 부분은 clang의 옵션에 -fsyntax-only를 주면 AST만 분석하게 만들 수 있습니다..

CppParserOptions Options = new CppParserOptions();
Options.ParseAsCpp = true;
Options.AdditionalArguments.Add("-xc++");
Options.AdditionalArguments.Add("-fsyntax-only"); // AST 만
Options.AdditionalArguments.Add("-std=c++17");

그리고 clang의 파싱이 오래 걸리는 문제는 미리 header를 분석해서 매크로가 있는 클래스만 뽑아내서 심볼로 가지고 있고 해당 클래스만 분석하도록 만들었습니다.

// 해더를 파싱해서 Class, Struct, Enum, Property 정보를 획득
SymbolParser.SymbolParser parser = new SymbolParser.SymbolParser(FilePath);
List<Symbol> symbols = parser.Run(source, file);

if (symbols == null)
{
    Console.WriteLine($"'{file}': Error: Parsing failed.");
    return;
}
else if (symbols.Count == 0)
{
    return;
}

// Generator에 symbol을 넘겨주고 generator에서 symbols 에 있는 요소들만 분석하도록 적용
generator.Symbols = symbols;

3rdParty library setup

마지막으로 3자 라이브러리를 세팅하는 기능입니다. 해당 기능은 다음 라이브러리에서 아이디어를 얻어서 구현했습니다.

해당 라이브러리처럼 자동으로 소스를 다운받고 빌드하게 하였습니다.

0 replies

Leave a Reply

Want to join the discussion?
Feel free to contribute!

Leave a Reply

Your email address will not be published. Required fields are marked *