| 일 | 월 | 화 | 수 | 목 | 금 | 토 |
|---|---|---|---|---|---|---|
| 1 | 2 | 3 | 4 | |||
| 5 | 6 | 7 | 8 | 9 | 10 | 11 |
| 12 | 13 | 14 | 15 | 16 | 17 | 18 |
| 19 | 20 | 21 | 22 | 23 | 24 | 25 |
| 26 | 27 | 28 | 29 | 30 | 31 |
- 메타버스
- 알고리즘
- hanbit.co.kr
- 이득우
- C#
- 데이터 통신과 컴퓨터 네트워크
- C++
- HANBIT Academy
- BOJ
- 박기현
- unity6
- https://insightbook.co.kr/
- 생능출판
- 입출력과 사칙연산
- 주우석
- Shimon Schocken
- 김진홍 옮김
- (주)책만
- 백준
- 이득우의 게임수학
- 밑바닥부터 만드는 컴퓨팅 시스템 2판
- booksr.co.kr
- C
- The Elements of Computing Systems 2/E
- 일기
- 잡생각 정리글
- JavaScript
- 게임 수학
- 전공자를 위한 C언어 프로그래밍
- Noam Nisan
- Today
- Total
cyphen156
CyphenEngine 개발기 #3 에디터 빌드를 고려하자 본문
오랜만에 엔진 개발기를 쓴다.
사실 한동안 이것저것 손을 못 댔는데, 다시 들여다보다 보니 지금 구조에서 놓치고 있던 부분이 보였다.
정확히는 처음 설계할 때 "게임 엔진"을 만든다고 생각했는데, 내가 진짜 만들고 싶은 건 "게임 엔진 에디터"에 가까웠다.
이 차이가 생각보다 컸다.
처음에는 다음과 같이 설계했었다

당시 설계는 명백히 타겟 플랫폼에서 직접 구동되는 런타임 엔진을 중심으로 하고 있다.
Windows API 위에 Window Manager가 있고, 그 안에서 Core가 Time, Input, Event, File, Path 같은 기반 기능을 관리하며, Modules와 AddOn이 붙는 구조다.
이 구조는 게임 실행을 위한 엔진 코어로는 충분히 의미가 있다.
하지만 "게임을 제작하는 도구"로 보자면 치명적으로 빠진 게 있다.
씬 편집도, 리소스 관리도, 플랫폼별 빌드 생성도 없다. 게임을 만들려면 결국 코드를 직접 수정해야 한다.
이건 Unity나 Unreal처럼 에디터로 게임을 만드는 방식이 아니라,
자체 엔진을 쓰는 스튜디오에서 개발자가 직접 엔진 코드를 건드리며 작업하는 방식에 가깝다.
이것은 내가 지향하는 모듈화 엔진 구조와도 맞지 않았다.
모듈을 붙이고 떼는 선택권을 사용자에게 주려면, 그 모듈을 선택하고 검증하고 빌드에 반영할 수 있는 에디터 또는 관리 계층이 필요하기 때문이다.
즉, 단순히 엔진 내부를 모듈화하는 것만으로는 부족했다. 사용자가 그 모듈 구조를 직접 다루고 선택할 수 있는 게임 엔진 에디터가 필요하다는 것을 깨달았다.
실행 흐름부터 다시 보기
먼저 실행 관점에서 보면 구조는 다음과 같다.
exe 실행
↓
플랫폼별 Main 함수 실행
↓
Platform Launch
↓
Common Bootstrap
↓
CyphenEngine 초기화
↓
Runtime Loop 동작
Windows라면 wWinMain, Linux라면 main, macOS라면 해당 플랫폼의 앱 진입점이 먼저 실행된다.
이 부분은 공통 엔진 코드로 완전히 밀어 넣을 수 없다.
애플리케이션의 최초 진입점은 플랫폼이 정한다.
따라서 wWinMain 이전에 공통 EngineMain 같은 함수가 먼저 실행되는 구조를 일반적인 애플리케이션 방식으로 만들 수는 없다.
가능한 것은 그 다음이다.
플랫폼별 진입점은 각 플랫폼 Launch 코드에 남겨두고, 그 안에서 필요한 네이티브 초기화를 수행한 뒤 공통 엔진 실행 흐름으로 넘긴다.
Platform Main
- Windows: wWinMain
- Linux: main
- macOS: App Entry
↓
Platform Launch
- 창 생성
- 네이티브 핸들 확보
- 메시지 / 이벤트 펌프 준비
↓
Common Bootstrap
- Path
- File
- Time
- Logger
- Platform 정보
- Renderer 설정 검증
↓
CyphenEngine
- 기본 프레임 루프 본체
- Editor / Game Runtime 분기
이렇게 보면 플랫폼별 Main 함수는 피할 수 없다.
다만 그 이후의 실행 흐름은 공통화할 수 있다.
Main 이전에 존재할 수 있는 것과 실행될 수 있는 것은 다르다
여기서 한 가지를 구분해야 했다.
헤더, 매크로, enum, 인터페이스, static 저장 공간, 전역 포인터 선언 같은 것들은 wWinMain보다 논리적으로 먼저 존재할 수 있다.
예를 들면 define.h, 플랫폼 매크로, 렌더러 매크로, 문자열 타입 규칙, GEngine 선언 같은 것들이다.
이들은 실행 흐름을 갖는 코드라기보다, 엔진이 올라오기 전에 이미 깔려 있어야 하는 인프라 레벨의 구조다.
하지만 Time, Path, File, Logger 같은 시스템은 조금 다르다.
헤더와 타입 선언은 미리 존재할 수 있지만, 실제 초기화는 플랫폼 진입점 이후에 이루어져야 한다.
예를 들어 Path는 실행 파일 위치, 작업 디렉터리, 플랫폼별 경로 규칙을 알아야 한다.
Time은 Windows에서는 QueryPerformanceCounter 계열을 쓸 수 있고, Linux에서는 다른 API를 써야 한다.
Logger도 출력 경로나 콘솔, 파일 시스템 상태가 준비된 뒤 초기화하는 편이 자연스럽다.
따라서 이들은 Main 이전의 실행 계층이 아니라, Platform Launch 이후 CyphenEngine Runtime 이전에 초기화되어야 하는 Common Bootstrap 계층으로 보는 것이 맞다.
내가 기존에 Time, Path, File 같은 것을 먼저 만들었던 이유도 결국 여기에 있었다.
이들은 게임 로직보다 아래에 있는 기반 기능이다.
다만 이제는 그 위치를 더 명확히 해야 한다.
Platform Launch
↓
Common Bootstrap
- Path
- File
- Time
- Logger
↓
CyphenEngine Init
GEngine은 유지해도 된다
이번에 다시 보면서 GEngine에 대해서도 생각을 정리했다.
처음에는 전역 포인터를 쓰는 구조가 너무 단순한 방식이 아닌가 싶었다.
하지만 엔진 인스턴스의 성격을 생각하면 꼭 그렇지는 않았다.
CyphenEngine은 필요할 때마다 가져오는 리소스가 아니다.
프레임 루프가 도는 동안 항상 먼저 참조되는 루트 실행 컨텍스트다.
즉, 엔진 인스턴스는 앱 실행 중 계속 살아 있어야 한다.
현재 구조처럼 GEngine을 전역 포인터로 두는 이유도 여기에 있다.
GEngine
= 현재 실행 중인 CyphenEngine 인스턴스를 가리키는 전역 루트 포인터
실제 CyphenEngine 객체는 new CyphenEngine()으로 생성되므로 힙에 존재한다.
하지만 이 객체는 매 프레임 새로 찾거나 생성하는 대상이 아니다.
한 번 생성된 뒤 런타임 루프가 끝날 때까지 계속 참조된다.
따라서 힙에 있다는 이유만으로 매 프레임마다 큰 메모리 비용이 발생한다고 보기는 어렵다.
중요한 것은 매 프레임 엔진 인스턴스를 탐색하거나 획득하는 절차를 만들지 않는 것이다.
가능하다면 루프 진입 전에 엔진 포인터나 참조를 고정해 사용할 수 있다.
CyphenEngine* const engine = GEngine;
while (platformApplication.PumpEvents())
{
engine->Run();
}
또는 참조로 잡을 수도 있다.
CyphenEngine& engine = *GEngine;
while (platformApplication.PumpEvents())
{
engine.Run();
}
방식은 엔진 인스턴스를 매번 다시 찾는 구조가 아니라, 루프 동안 사용할 엔진 주소를 고정하는 구조다.
물론 캐시 상주 여부는 CPU와 실행 상황에 따라 달라진다.
하지만 매 프레임 가장 먼저 접근되는 루트 객체라면 hot path에 놓이는 것이 자연스럽다.
CyphenEngine은 바로 그런 위치에 있어야 하는 객체다.
CyphenEngine의 의미를 다시 잡기
여기서 CyphenEngine 자체의 의미도 다시 잡아야 했다.
처음에는 CyphenEngine을 게임 실행 런타임에 가까운 것으로 생각했다.
하지만 에디터 구조로 보면 조금 다르다.
CyphenEngine은 Game Runtime 그 자체도 아니고, Editor Runtime 그 자체도 아니다.
플랫폼 루프에서 매 프레임 호출되는 기본 엔진 본체에 가깝다.
그 아래에서 현재 실행 모드에 따라 Editor Runtime과 Game Runtime이 갈라진다.
CyphenEngine
- 기본 프레임 루프 본체
- Time / Input / Event / Renderer / Resource 같은 공통 시스템 관리
- 현재 실행 모드에 따라 EditorRuntime 또는 GameRuntime 실행
에디터 실행 중이라면 흐름은 다음과 같다.
CyphenEngine::Run()
↓
EditorRuntime Tick
↓
Editor UI / Scene View / Tool 처리
↓
PIE 실행 중이면 GameRuntime Tick
↓
Render
패키징된 게임 실행이라면 흐름은 더 단순하다.
CyphenEngine::Run()
↓
GameRuntime Tick
↓
Render
즉, 에디터와 게임 런타임을 완전히 별개의 최상위 루프로 두는 것이 아니라, 둘을 총괄하는 기본 엔진 루프가 하나 있고, 그 안에서 실행 모드에 따라 분기되는 형태가 자연스럽다.
Platform Launch
↓
GEngine / CyphenEngine
↓
EditorRuntime
↓
PIE GameRuntime
또는
Platform Launch
↓
GEngine / CyphenEngine
↓
GameRuntime
이 구조가 에디터에도 맞다.
에디터도 결국 앱이다.
입력도 받아야 하고, 화면도 갱신해야 하고, 내부에서 게임 실행 상태도 관리해야 한다.
다만 게임 실행 파일에서는 Game Runtime이 중심이고, 에디터에서는 Editor Runtime이 중심이 된다.
다시 보니 플랫폼 처리 책임도 흘러나와 있었다
실행 흐름과 CyphenEngine의 위치를 다시 정리하고 나니, 다른 문제가 보였다.
플랫폼 처리 책임이 Runtime 소스코드 안까지 흘러나와 있었다.
대표적인 예가 File과 Path다.
겉으로 보면 File은 공통 API처럼 보인다.
File::OpenFile
File::Exists
File::ReadAll
File::WriteAll
File::Delete
하지만 구현 안에서는 Windows와 Linux 처리가 직접 갈라진다.
Windows
_wfopen_s
_wremove
Linux
fopen
remove
Path도 비슷하다.
Windows
GetCurrentDirectory
Linux
getcwd
API 자체가 문제는 아니다.
File::ReadAll()이나 Path::Root() 같은 함수는 공통 API로 유지해도 된다.
문제는 그 공통 구현 파일 안에 플랫폼 처리 규약이 들어와 있다는 점이다.
즉, Runtime 소스코드가 플랫폼 규칙을 직접 알고 있다.
이건 구조상 좋지 않다.
Runtime은 이미 정리된 공통 API를 제공해야 하고, 플랫폼별 처리 방식은 그 아래 계층으로 내려가야 한다.
공통 선언은 유지하고, 구현은 플랫폼별로 나눈다
처음에는 PlatformFile::Open() 같은 중간 계층을 둘 수도 있다고 생각했다.
File
↓
PlatformFile
↓
Windows / Linux API
하지만 생각해보니 꼭 그럴 필요는 없다.
공통 호출 계층을 하나 더 둘 필요 없이, File의 선언은 공통으로 두고 구현 파일만 플랫폼별로 나누면 된다.
구조는 이렇게 잡는다.
Runtime/Public/File.h
공통 File API 선언
Platform/Windows/Private/File.cpp
Windows용 File 구현
Platform/Linux/Private/File.cpp
Linux용 File 구현
사용자 코드나 Runtime 코드는 계속 같은 API를 본다.
하지만 실제 구현은 플랫폼별 cpp에서 갈라진다.
Windows 빌드에서는 Windows용 File.cpp만 포함하고, Linux 빌드에서는 Linux용 File.cpp만 포함한다.
이 방식의 장점은 API 계약이 한 곳에 남는다는 점이다.
공통 선언이 있으면 “이 엔진에서 File API는 이렇게 생겼다”는 기준이 생긴다.
플랫폼별 구현 cpp는 그 선언을 구현해야 한다.
반대로 공통 선언 없이 Windows용 File.h, Linux용 File.h를 각각 두면, 각 플랫폼 헤더가 우연히 같은 함수 이름과 시그니처를 유지해야 한다.
그건 장기적으로 위험하다.
그래서 결론은 다음과 같다.
공통 선언
유지
공통 구현
제거
플랫폼별 구현
추가
빌드 시스템이 구현 선택을 책임진다
이 방식의 단점도 있다.
빌드 단계에서 현재 플랫폼에 맞는 cpp만 포함해야 한다.
Windows 빌드
Platform/Windows/Private/File.cpp 포함
Platform/Linux/Private/File.cpp 제외
Linux 빌드
Platform/Linux/Private/File.cpp 포함
Platform/Windows/Private/File.cpp 제외
둘 다 포함하면 중복 정의가 난다.
하나도 포함하지 않으면 링크 에러가 난다.
하지만 이건 오히려 맞는 책임 분리다.
플랫폼별 소스를 고르는 책임은 Runtime 코드가 아니라 빌드 시스템과 플랫폼 설정 쪽에 있어야 한다.
Runtime의 File.cpp 안에서 #if defined(PLATFORM_WINDOWS)로 분기하는 방식은 당장 편하지만, 결국 플랫폼 규약이 공통 코드 안으로 새어 나온다.
빌드 타겟이 Windows라면 Windows 구현만 빌드하고, Linux라면 Linux 구현만 빌드하는 것이 더 자연스럽다.
Define과 Framework의 역할도 다시 정리한다
이 과정에서 define.h와 framework.h의 역할도 다시 봐야 했다.
기존 define.h는 너무 많은 일을 하고 있었다.
define.h
- NOMINMAX
- LOG_INTERNAL
- PLATFORM_WINDOWS / PLATFORM_LINUX
- TSTRING / TCHAR / TTEXT
- TSLASH / TSLASH_STR
- TO_TSTRING
- LARGEINTEGER
- SINGLE
처음에는 한 파일에서 전부 처리하는 것이 편했다.
하지만 플랫폼 감지, 문자열 정책, 경로 구분자, 시간 타입, 로그 매크로, 싱글톤 매크로가 모두 한 파일에 들어가 있으니 책임이 흐려졌다.
반대로 framework.h에 이런 것들을 넣으면, framework.h가 플랫폼 실행 뼈대뿐 아니라 문자열·파일·시간 정책까지 흡수하게 된다.
이것도 좋지 않다.
따라서 역할은 다음처럼 나누기로 했다.
PlatformDefine
- 현재 빌드 플랫폼 감지
CommonInclude
- 공통 표준 헤더 포함
Define
- 엔진 공통 정의
- 타입 별칭
- 리터럴 매크로
- 공통 매크로
Framework
- 플랫폼별 Launch / Window 계층 선택
Types
- 엔진 공통 enum / struct
define.h는 매크로 전용 파일일 필요는 없다.
이름 그대로 “정의 헤더”라면 using, constexpr, 최소한의 매크로가 같이 들어갈 수 있다.
중요한 것은 모든 플랫폼 정책을 한 파일에 우겨넣지 않는 것이다.
타입은 using, 리터럴은 매크로
문자열 쪽도 다시 정리해야 한다.
기존 구조의 의도 자체는 틀리지 않았다.
Windows에서는 wide 문자열을 쓰고, Linux에서는 narrow 문자열을 쓰려면, 코드에 직접 박히는 리터럴도 플랫폼 문자 체계에 맞춰 바뀌어야 한다.
예를 들어 Windows에서 문자열 타입이 std::wstring이라면 다음 코드는 맞지 않는다.
TString text = "감사합니다";
오른쪽 "감사합니다"는 여전히 const char[]이기 때문이다.
그래서 코드에 직접 쓰는 문자열 리터럴은 다음처럼 감싸야 한다.
TString text = TTEXT("감사합니다");
이 경우 Windows에서는 L"감사합니다"로, Linux에서는 "감사합니다"로 치환된다.
이 부분은 매크로가 필요하다.
문자열 리터럴 앞의 L은 타입 별칭으로 해결할 수 없다.
하지만 자료형까지 매크로일 필요는 없다.
자료형은 전처리 텍스트 치환에서 제외하고 플랫폼별 처리에서 컴파일러가 이해할 수 있는 using으로 포함하는 방향으로 바꾸기로 했다.
따라서 방향은 이렇게 잡는다.
TString / TChar
using
TTEXT
macro
TSLASH / TSLASH_STR
macro 또는 constexpr 정책
파일 / 경로 / 시간 동작
함수 구현
다시 정리한 구조
이제 큰 흐름 보면 아래와 같이 정리된다.

Source
↓
Engine Build Target
↓
Platform Main
↓
Platform Launch
↓
Common Bootstrap
↓
CyphenEngine
↓
EditorRuntime / GameRuntime
↓
Game Build Target
↓
Game Runtime
여기서 첫 번째 빌드 타겟은 CyphenEngine 자체를 어떤 플랫폼용 에디터로 빌드할 것인가이다.
두 번째 빌드 타겟은 CyphenEngine 안에서 사용자가 만든 게임을 어떤 플랫폼용으로 빌드할 것인가이다.
즉, 빌드 타겟은 두 번 존재한다.
1차 빌드
CyphenEngine Editor 자체의 실행 플랫폼 결정
2차 빌드
사용자가 만든 Game Runtime의 실행 플랫폼 결정
이 구분이 중요하다.
처음에는 단순히 런타임 엔진을 만든다고 생각했기 때문에 이 구분이 흐릿했다.
하지만 에디터를 만들겠다고 보면, Engine Build Target과 Game Build Target은 명확히 나뉘어야 한다.
현재 1차 목표
현재 1차 목표는 거창하게 잡지 않기로 했다.
먼저 Windows 빌드와 Linux 빌드를 기준으로, 에디터 안에 게임 씬 하나가 있다고 가정하고, 그 안에서 사각형 하나가 움직이는 것에만 집중하려 한다.
별도의 씬 편집기, 인스펙터, 리소스 브라우저 없이도 괜찮다.
중요한 것은 CyphenEngine이 플랫폼별로 빌드되어 실행되고, 공통 Bootstrap을 거쳐, Editor Runtime 안에서 PIE 형태로 최소한의 Game Runtime이 실제로 도는 것이다.
사각형 하나가 움직이는 것만으로도 다음을 확인할 수 있다.
- Platform Launch가 정상적으로 동작하는가
- Common Bootstrap이 저수준 기반 기능을 초기화하는가
- File / Path / Time 같은 기반 기능이 플랫폼별 구현을 통해 동작하는가
- Renderer 모듈이 연결되는가
- CyphenEngine이 기본 프레임 루프 본체로 동작하는가
- EditorRuntime이 올라오는가
- PIE 안에서 GameRuntime이 Tick / Render 되는가
최종 정리
이번에 다시 정리한 결론은 다음과 같다.
- 플랫폼별 Main 함수는 피할 수 없다.
- Main 이후의 실행 흐름은 공통화할 수 있다.
- CyphenEngine은 Editor도 Game도 아니라 기본 프레임 루프 본체다.
- GEngine은 현재 실행 중인 CyphenEngine 인스턴스를 가리키는 전역 루트 포인터로 유지한다.
- Time, Path, File, Logger는 Main 이전 실행 계층이 아니라 Common Bootstrap에서 초기화되는 기반 시스템이다.
- File / Path / Time의 공통 API 선언은 유지한다.
- 공통 구현 파일 안의 플랫폼 분기는 제거한다.
- 실제 구현 cpp는 플랫폼별로 나눈다.
- 빌드 시스템이 현재 플랫폼에 맞는 cpp만 선택한다.
- 자료형은 using으로 정의하고, 문자열 리터럴은 TTEXT 매크로로 처리한다.
- TCHAR / CCHAR 같은 Windows SDK 이름은 피한다.
- Runtime 코드는 이미 정리된 공통 API만 사용한다.
공통 API는 유지한다.
플랫폼 규약은 Runtime 소스에 두지 않는다.
플랫폼별 동작은 플랫폼별 cpp로 내려보낸다.
'프로젝트 > CyphenEngine' 카테고리의 다른 글
| 자체엔진 개발 중간 기록 : Dx11.dll -> Vulkan.dll 렌더러 모듈 교체 테스트 (0) | 2026.07.01 |
|---|---|
| 저장용 리눅스 명령어 (0) | 2026.06.29 |
| 자체엔진 개발 중간 기록 : 이미지 두장 교체해서 렌더링하기 (0) | 2026.06.25 |
| CyphenEngine 개발기 #2 5개월만에 돌아온 엔진개발 근황 풀이 (0) | 2025.07.17 |
| CyphenEngine 개발기 #1 프로젝트 설계하기 (0) | 2025.02.11 |
