(ShellEngine의 에디터)
요즘에는 에디터를 지원하지 않는 엔진은 거의 없다. GUI없이 코드만으로 게임을 만드려 하면 끔찍할 것이다.
그래서 엔진을 처음 만들때 부터 에디터를 고려하고 만들기 시작했다.
그렇게 시작한 에디터가 2년 가까이 지나는 동안 월드 편집기, 에셋 파이프라인, 핫리로드, 빌드 시스템, 멀티 패스 렌더러, undo/redo까지 붙은 하나의 제작 환경이 됐다. 지금 돌아보면 에디터를 만든 것이 아니라 에디터가 엔진을 만든 것 같다.
2024.06 커밋
가장 먼저 한 일은 에디터 코드를 Game 모듈에서 떼어내는 것이었다. 기능이 더 붙기 전에 분리하지 않으면 나중에 훨씬 힘들어질 게 눈에 보였다. Editor 모듈을 나누고, ImGui를 통해 Project와 파일 탐색기 UI를 만들었다.
뷰포트도 만들었다. 뷰포트에 화면을 띄우려면 어떻게 해야할까? 카메라를 통해 렌더링 한 이미지를 뷰포트로 띄워야 할 것이다. 그래서 이 때 오프스크린 렌더링 기능도 구현했었다.
에디터는 처음부터
World를 상속한EditorWorld안에서 돌아가게 설계했다. 에디터만의 특수 기능이 필요했기 때문이다.
그리고 ImGui는 싱글 스레드 기반 라이브러리라 ShellEngine의 멀티스레드 구조와 맞추느라 고생을 좀 했다. 자세한 내용은 Github 문서에 따로 정리해뒀다.

2024.10 커밋
에디터의 핵심이라고 볼 수 있는 피킹을 구현했다. 방법은 두 가지 중 하나를 골라야 했다.
ray 방식은 에디터에서 정확한 클릭을 구현하기엔 부적절하다고 생각하여 오프스크린 방식을 택했다.
피킹 카메라가 렌더 ID 버퍼를 읽어 어떤 오브젝트를 클릭했는지 판별한다.
선택 상태를 컴포넌트로 표현했다
물체가 피킹이 됐으면 해당 오브젝트에EditorControl컴포넌트와OutlineComponent컴포넌트를 안 보이게 부착 시켰다. 별도의 선택 상태 변수를 관리하는 대신 편집 기능 자체를 컴포넌트로 만든 것이다. 이 설계 덕분에 확장에 유연해졌다.
Hierarchy와 Inspector가 독립 패널로 분리됐고, 드래그드롭으로 오브젝트 순서를 바꾸거나 부모를 재설정할 수 있게 됐다. 메시를 씬에 드래그해서 넣는 것도 이때 생겼다.
드래그드롭은 엔진의 리플렉션 시스템을 활용해 구현했다. 현재 선택한 오브젝트의 클래스명을 가져와서, 드롭을 요구하는 클래스명과 같을 때만 받아들이는 방식이다.
// 드래그로 받는 객체의 타입 이름 == 드래그 중인 객체의 타입 이름이면 받음
const ImGuiPayload* payload = ImGui::AcceptDragDropPayload(propertyTypeName.c_str());
auto currentPayload = ImGui::GetDragDropPayload();
if (payload)
{
// 프로퍼티 값을 변경하고 저장...
}
에디터에 에셋을 불러오는 과정은 이렇게 구현했다.
이 방식을 사용하면 에셋을 쉽게 빼고 추가 할 수 있다. 언리얼 방식으로 구현할까 생각했지만 직관적으로 프로젝트 폴더에서 어떤 에셋이 존재하는지를 보고 싶었기 때문에 유니티의 방식을 채택했다.
2024.11 커밋
아직까진 편집이 됐지만 저장이 안 됐다. 에디터를 끄면 모든 게 사라졌다.
AssetDatabase를 구현해 현재 프로젝트의 모든 에셋을 관리 할 수 있게 됐고, 불러온 에셋은 전부 SObject의 파생 클래스이므로 가상 함수를 추가해 에셋별로 직렬화와 역직렬화를 구현했다.
월드도 하나의 에셋으로 취급하게 해 월드를 저장하고 다시 열 수 있게 됐다.
Serialize 함수에서 현재 월드에 존재하는 모든 오브젝트를 json에 저장했다
Deserialize함수에서 json을 읽어 오브젝트를 다시 생성하고, 오브젝트간 관계를 재설정하게 했다.
이렇게 월드와 에셋의 저장과 불러오기 기능을 완성했다.
게임이 실행 될 때는 EditorWorld가 World가 된다.
따라서 게임 빌드에서는 에디터에만 존재하고 의존하는 오브젝트는 역직렬화 과정에서 배제되게 했다.
타입별로 전용 Inspector UI를 등록할 수 있게 됐다. 사용자가 직접 만든 컴포넌트에 전용 Inspector를 붙일 수 있다는 것이다. 에디터가 엔진 내부 타입 전용 UI에서 사용자 확장 가능한 UI로 바뀐 시점이었다.
class TextureInspector : public CustomInspector
{
INSPECTOR(TextureInspector, render::Texture)
public:
SH_EDITOR_API void RenderUI(const std::vector<core::SObject*>& objs, int idx) override;
}
2025년 4월, 이 달에만 46개의 커밋이 들어갔다. 가장 시각적으로 달라진 것은 멀티 패스 아웃라인이었다. 선택된 오브젝트 주변에 아웃라인이 생겼고, 피킹도 별도 패스로 분리됐다.
아웃라인은 두개의 패스를 통해 그려진다.
OutlineComponent를 이용해 아웃라인 패스로 그릴 물체를 전달한다.EditorOutlinePass에서 현재 선택한 오브젝트를 단순히 흰색으로 렌더링 하고 렌더 텍스쳐로 내보낸다.EditorPostOutlinePass에서 EditorOutlinePass에서 렌더링 한 물체를 SobelFilter를 사용해 외곽선을 추출하고 전체 화면에 외곽선을 그린다.그리고 렌더링 외에 구조적으로 중요한 변화가 있었다.
EditorUI가 전역 UI 로직에서 컴포넌트로 바뀌었는데, 에디터 UI가 엔진 루프 바깥에서 따로 돌지 않고 월드 수명주기 안으로 들어오게 됐다.
Awake()에서 창들을 생성하고, Update()에서 ImGui를 렌더링했다.
같은 달에 프로젝트 템플릿과 핫리로드 DLL도 들어왔다. 사용자 코드를 빌드하고, 에디터 안에서 다시 읽고, 월드 상태를 복원하는 흐름이 처음 완성됐다. 해당 내용은 여기에 글을 써놨다!
2025.07 커밋
에셋 시스템을 다시 손봤다. Asset, AssetBundle, AssetImporter, AssetExporter등의 클래스들이 정리되면서 텍스처, 모델, 셰이더, 머티리얼, 프리팹이 각각 타입별 로더를 갖게 됐고, 로딩 순서도 의존성을 고려해 정해졌다.
그다음 빌드 시스템이 들어왔다. 시작 월드에서 직렬화된 json에서 모든 참조 UUID를 수집하고, assets.bundle과 gameManager.bin을 만들게 했다.
assets.bundle에는 필요한 에셋 파일인 .asset파일들을 압축해서 보관하고 게임에서 필요한 것을 불러온다.
gameManager.bin에는 제일 처음에 불러와야 하는 게임의 설정을 들어가게 만들었다.
후반에는 생산성을 높여주는 기능들이 붙었다. Prefab과 ScriptableObject, 멀티 오브젝트 편집, 폰트 편집기 같은 것들이다. 틈틈이 에디터 안정성을 높이는 작업도 계속했다.
이 글을 쓰는 지금(2026-04)에는 undo/redo기능을 구현중이다.EditorCommand와 CommandHistory를 만들었고, 이름 변경, 선택, 트랜스폼 조작을 즉시 반영하지 않고 커맨드 형태로 만들어서 스택에 쌓는 구조다. 여러 물체를 조작하는 커맨드는 transaction 기반으로 하나로 묶게 만들었다.
아직 모든 편집 행위를 커맨드화 하진 않았다. 일부 삭제나 계층 패널 조작은 여전히 즉시 반영 중심이라 앞으로 더 구현해야 할 영역이 남아 있다.
지금의 ShellEngine 에디터는 완성된 상용 툴이 아니다. 꽤 강한 구조를 얻었지만 아직 다듬어야 할 곳이 많은 성장 중인 제작 환경이다. 그리고 솔직히 말하면, 엔진을 만들기 시작했을 때 에디터가 이렇게까지 커질 줄은 몰랐다..