PROJECT REPORT
PROJECT REPORT // WINAPI_DONT_STARVE
C++, WinAPI를 이용한 Don't Starve 모사 및 성능 최적화
게임 엔진에 의존하지 않고 C++와 WinAPI만을 이용해 완전 밑바닥부터 게임을 구축하는 데 도전한 프로젝트입니다. 게임 엔진의 추상화된 기능 뒤에 숨겨진 전체적인 실행 파이프라인을 깊이 있게 이해하고 스스로 설계해보고자 시작하였으며, 기존 게임인 'Don't Starve'의 핵심 시스템을 참고하여 제작했습니다.
인게임 핵심 시스템 & 스크린샷 미리보기
캐릭터 선택부터 오픈월드 생태계, 크래프팅 및 인벤토리, 하운드/거미 전투, 거대 보스 거미 여왕 레이드까지 주요 인게임 화면을 넘겨가며 옆으로 스크롤하여 확인할 수 있습니다.
1. 주요 시스템 아키텍처
1.1 컴포넌트 기반 객체 설계 (Component-Based)
단일 상속의 복잡성을 피하고 객체 지향적인 유연성을 극대화하기 위해 컴포넌트 기반 구조를 채택했습니다. 모든 게임 객체(GameObject)는 기능 단위의 컴포넌트를 소유하며, 실행 시점에 필요한 기능을 동적으로 탈부착할 수 있습니다.
GameObject (엔티티 컨테이너)
ENTITY CONTAINER고유 식별자(ID) 및 타입 관리, `std::vector<std::unique_ptr<Component>>` 소유 및 컴포넌트 생명주기 관리
이러한 구조를 통해 새로운 객체를 생성할 때 상속 계층에 얽매이지 않고 유연하게 기능을 조립할 수 있습니다.
코드 보기: GameObject.h - 컴포넌트 추가 및 획득 핵심 로직
// GameObject.h - 컴포넌트 추가 및 획득 핵심 로직
template <typename T, typename... Args>
T* AddComponent(Args&&... args) {
auto newComponent = std::make_unique<T>(this, std::forward<Args>(args)...);
T* componentPtr = newComponent.get();
m_components.push_back(std::move(newComponent));
return componentPtr;
}
template <typename T>
T* GetComponent() const {
for (const auto& component : m_components) {
if (!component) continue;
T* target = dynamic_cast<T*>(component.get());
if (target) return target;
}
return nullptr;
}
1.2 중앙 집중식 매니저 패턴 (Singleton Managers)
시스템의 전역적인 상태 관리와 자원 공유를 위해 싱글톤 기반 매니저들을 구축했습니다.
Engine Subsystem Core (Singleton Services)
GLOBAL SERVICES HUBObjectManager
ENTITY월드 엔티티 생명주기 및 2D 공간 분할(Spatial Partitioning) 제어
-
▸
모든
GameObject동적 생성·소멸 및 ID 고유성 보장 - ▸ 256×256 정적 그리드 배열 인덱싱 실시간 업데이트
- ▸ 가시 범위 내 활성 객체 풀링(Pooling) 및 순회 최적화
RenderManager
RENDER커맨드 패턴 기반 지연 렌더링 및 발밑 Pivot Y-Sorting 정렬
-
▸
프레임별
DrawCommand생성 및 레이어 큐 버퍼링 -
▸
깊이(Depth) 우선순위 Pivot Y 기준
std::stable_sort정렬 - ▸ 배경·월드·UI 레이어 단위 GDI+ 더블 버퍼링 일괄 Flush
ResourceManager
ASSETGDI+ 비트맵 및 스프라이트 시트 해시맵 캐싱 및 중복 로드 방지
- ▸ 파일 경로 해시 기반 비트맵 메모리 캐싱 (Zero Duplicate)
- ▸ 애니메이션 스프라이트 시트 프레임 Rect 메타데이터 관리
- ▸ 씬 전환 및 프로그램 종료 시 안전한 GDI+ 리소스 일괄 해제
CameraManager
CAMERA플레이어 시야 추적, 뷰포트 컬링 및 월드-스크린 좌표계 변환
- ▸ 플레이어 중심 선형 보간(Lerp) 부드러운 타깃 추적
-
▸
ObjectManager에서 가시 범위 객체를 추적하여 렌더링 - ▸ World Coordinates ↔ Screen DC 좌표계 선형 변환 행렬
2. 렌더링 시스템 및 파이프라인
GDI+ 환경에서 대규모 오브젝트를 효율적으로 출력하기 위해 커맨드 패턴 기반의 렌더링 파이프라인을 구축했습니다. 매 프레임 모든 객체를 즉시 그리는 대신, 렌더링 명령을 레이어별로 수집하고 정렬한 뒤 일괄 실행(Flush)하는 방식을 취합니다.
2.1 렌더링 파이프라인 흐름
카메라 뷰포트 컬링부터 발밑 Pivot Y 깊이 정렬, GDI+ 백버퍼 일괄 출력(Flush)까지의 4단계 렌더링 파이프라인 실행 순서 전개도입니다.
1. 시야 연산 & 공간 쿼리 (CameraManager & ObjectManager)
SPATIAL QUERY
CameraManager가 현재 플레이어 중심의 유효 뷰포트(AABB)를 산출하면, 2D 공간 분할 그리드를 관리하는 ObjectManager가 화면 밖 객체를 O(1) 인덱싱으로 배제하고 가시 영역 내 후보군 선별
2. 선형 탐색 없는 책임 위임 (GameObject & SpriteRenderer)
POLYMORPHIC DISPATCH
선별된 가시 객체가 다형성 Render()를 호출받아, 무거운 컴포넌트 검색 없이 자신이 소유한 SpriteRenderer로 렌더링 책임을 즉각 위임
3. 지연 드로우 커맨드 버퍼링 (SpriteRenderer & RenderManager)
COMMAND BUFFERING
화면에 비트맵을 즉시 그리지 않고, 발밑 접지면(Foot Pivot Y) 깊이 정보를 포함한 지연 그리기 명령(DrawCommand)을 생성하여 중앙 RenderManager 큐에 적재
4. 발밑 깊이 정렬 및 일괄 출력 (RenderManager)
Y-SORT & FLUSH
적재된 명령들을 발밑 깊이 오름차순으로 일괄 정렬(Y-Sorting)한 뒤, GDI+ 백버퍼에 단 한 번의 순차 Flush로 출력하여 깊이 왜곡 없는 자연스러운 2.5D 입체감 구현
2.2 렌더링 파이프라인 속 서브시스템 협력
단일 책임 원칙(SRP)에 따라 각 매니저와 컴포넌트가 명확히 책임을 분담하며, 유기적인 상호작용을 통해 렌더링 파이프라인을 구동합니다.
| 서브시스템 | 주요 역할 (Role) | 파이프라인 내 책임 (Responsibility) |
|---|---|---|
CameraManager | 시야 연산 & 뷰포트 제어 | 타깃(플레이어) 추적, 가시 뷰포트 산출 및 가시 객체 렌더링 루프 총괄 |
ObjectManager | 객체 수명 & 공간 색인 | 2D 공간 분할 그리드 인덱싱 및 뷰포트 교차 객체 선별 컬링 |
GameObject | 다형성 렌더링 진입점 | 렌더링 다형성 호출 수신 및 소유 렌더러 컴포넌트로 O(1) 위임 |
SpriteRenderer | 스프라이트 변환 & 커맨드화 | 발밑 피벗(Foot Pivot) 기준 깊이값 계산 및 지연 그리기 명령(DrawCommand) 생성 |
RenderManager | 지연 렌더 파이프라인 총괄 | 드로우 커맨드 큐 버퍼링, 발밑 깊이 오름차순 정렬(Y-Sorting) 및 GDI+ 백버퍼 일괄 Flush |
3. 최적화 및 핵심 기술
3.1 공간 분할 기법: 그리드 기반 컬링 (Grid-based Culling)
월드에 수천 개의 오브젝트가 존재할 때, 모든 객체에 대해 매 프레임 업데이트와 충돌 검사(Brute-force, O(N))를 수행하면 심각한 성능 저하가 발생합니다. 이를 방지하기 위해 맵 전체를 256x256 픽셀 크기의 정적 2D 그리드 배열(Spatial Partitioning)로 분할했습니다.
1. 광역 그리드 셀 선별 (Broad Phase)
O(1) // INDEXING
전체 맵의 2D 그리드 배열 중 카메라 뷰포트가 걸쳐 있는 활성 셀 범위([startX~endX], [startY~endY])를 즉시 산출하여 화면 밖 수천 개 객체를 1차 배제
2. 개별 객체 정밀 AABB 컬링 (Narrow Phase)
O(K) // CULLING
선별된 가시 셀 내부의 객체들만 순회하며, 개별 오브젝트의 실제 바운딩 박스(AABB)와 뷰포트 교차 여부를 최종 판정하여 가시 버퍼(visibleBuffer)에 수집
3.1.1 동적 오브젝트의 그리드 이동 처리
플레이어나 몬스터처럼 실시간으로 월드를 이동하는 동적 객체는 좌표가 변경될 때마다 자신이 속한 공간 분할 그리드 셀의 소속을 동기화해야 합니다.
하지만 매 프레임 무조건 기존 셀에서 객체를 빼내고 새 셀로 재등록하게 되면, 빈번한 탐색과 재할당으로 인해서 비효율적입니다. 특히 256×256 픽셀 크기의 그리드 셀은 캐릭터의 1프레임 이동량에 비해 매우 넓기 때문에, 대부분의 프레임 동안 객체는 동일한 셀 내부에서 머무릅니다.
따라서 객체가 셀의 경계를 실제로 넘어섰을 때만 다른 셀로 배치시켜 렌더링 판단을 하도록 설계했습니다.
3.1.2 오브젝트 그리드 추가/제거 및 선언 구조 (Spatial Lifecycle Management)
오브젝트가 새로 생성되거나 소멸할 때, 해당 오브젝트를 그리드 시스템에 안전하게 등록하고 지워주는 과정이 필수적입니다. 또한, 월드의 크기와 타일 크기에 맞추어 그리드의 너비와 높이를 런타임 최적화를 위해 상수로 정의했습니다.
코드 보기: ObjectManager.h - 공간 분할 시스템 헤더 정의
// ObjectManager.h - 공간 분할 시스템의 선언부
class ObjectManager {
public:
void UpdateObjectGrid(GameObject* pObj);
void AddToGrid(GameObject* pObj);
void RemoveFromGrid(GameObject* pObj);
private:
// 256px 정사각형 셀 기준의 정적 그리드 크기 정의
static constexpr int GRID_CELL_SIZE = 256;
static constexpr int GRID_WIDTH = (MAP_WIDTH * TILE_SIZE / GRID_CELL_SIZE) + 2;
static constexpr int GRID_HEIGHT = (MAP_HEIGHT * TILE_SIZE / GRID_CELL_SIZE) + 2;
// 2D 공간 분할 그리드 (각 셀마다 속한 오브젝트 포인터 벡터 관리)
std::vector<GameObject*> m_spatialGrid[GRID_WIDTH][GRID_HEIGHT];
uint32_t m_spatialQueryStamp = 1;
};
코드 보기: ObjectManager.cpp - 그리드 추가 및 삭제 구현
// ObjectManager.cpp - 객체 생성 시 그리드 등록
void ObjectManager::AddToGrid(GameObject* pObj)
{
if (!pObj || pObj->GetType() == GO_TYPE_UI) return;
Gdiplus::RectF bounds = pObj->GetBounds();
int x = (int)floor((bounds.X + bounds.Width * 0.5f) / GRID_CELL_SIZE);
int y = (int)floor((bounds.Y + bounds.Height * 0.5f) / GRID_CELL_SIZE);
x = (std::max<int>)(0, (std::min<int>)(GRID_WIDTH - 1, x));
y = (std::max<int>)(0, (std::min<int>)(GRID_HEIGHT - 1, y));
m_spatialGrid[x][y].push_back(pObj);
pObj->SetGridCell(x, y);
}
// ObjectManager.cpp - 객체 소멸 시 그리드 제거
void ObjectManager::RemoveFromGrid(GameObject* pObj)
{
if (!pObj) return;
int x = pObj->GetGridCellX();
int y = pObj->GetGridCellY();
if (x >= 0 && x < GRID_WIDTH && y >= 0 && y < GRID_HEIGHT) {
std::vector<GameObject*>& cell = m_spatialGrid[x][y];
cell.erase(std::remove(cell.begin(), cell.end(), pObj), cell.end());
pObj->SetGridCell(-1, -1);
}
}
4. 애니메이션 시스템 및 이벤트 콜백
Animator와 AnimationClip을 분리하여 고도로 제어 가능한 프레임 기반 애니메이션 시스템을 구축했습니다. 특히, 애니메이션의 시각적 흐름과 게임의 논리적 흐름(데미지 판정, 사운드 재생 등)을 일치시키기 위해 프레임 이벤트 콜백(Frame Event Callback) 시스템을 도입했습니다.
4.1 애니메이션 시스템 구조 및 핵심 객체 관계
애니메이션 재생과 렌더링 책임을 분리하여, 객체의 상태 전이(FSM)와 시각적 프레임 재생, 실제 화면 출력이 느슨하게 결합된(Decoupled) 컴포넌트 아키텍처를 구축했습니다. 각 컴포넌트는 다음과 같이 역할을 분담합니다.
Sprite
DATA
렌더링에 필요한 단일 프레임 이미지와, 시트의 절단 영역(sourceRect) 및 피벗 좌표를 보관
AnimationClip
SEQUENCE
특정 동작을 이루는 스프라이트 시퀀스와 재생 속도, 프레임별 이벤트 데이터를 보관
Animator
CONTROLLER
캐릭터 상태에 알맞은 클립을 선택하고, 경과 시간에 맞춰 현재 재생할 스프라이트를 제어 및 동기화
SpriteRenderer
RENDER
지정된 스프라이트의 절단 영역을 가져와, 발밑 피벗 기준 깊이 정렬 후 화면에 최종 래스터 출력
매 프레임 이미지 참조 및 렌더링 파이프라인
게임 루프가 동작하는 동안 Update 단계(시간 누적 및 스프라이트 교체)와 Render 단계(커맨드 버퍼링 및 일괄 출력)를 거쳐, 각 객체들이 어떻게 상호작용하며 화면에 프레임을 렌더링하는지 나타낸 실행 순서도입니다.
1. 시간 누적 및 순번 도출
ANIMATOR // UPDATE
Animator::Update(dt)가 재생 경과 시간(m_elapsed)을 누적하고, 클립 총 재생 시간 대비 현재 출력할 프레임 순번(currentFrameIndex)을 도출
2. 스프라이트 추출 및 주입
SYNC // SET-SPRITE
Animator가 데이터 컨테이너인 AnimationClip에서 해당 순번의 Sprite 객체를 꺼내, 타깃 SpriteRenderer::SetSprite()로 최신 스프라이트를 동기화 주입
3. 원본 비트맵 참조 & 커맨드 적재
ZERO-COPY // COMMAND
SpriteRenderer::Render()가 주입받은 Sprite의 sourceRect와 원본 비트맵 포인터를 참조하고, 월드 위치(Transform) 및 발밑 피벗을 결합하여 RenderManager 큐에 지연 그리기 명령 등록
4. Y-Sorting 정렬 및 일괄 출력
BLIT // FLUSH
RenderManager::Flush()가 큐에 수집된 명령들을 발밑 접지면(Y) 오름차순으로 일괄 정렬(Y-Sorting)한 뒤, 백버퍼에 원본 시트 비트맵 영역만 순차 래스터 렌더링
4.2 스프라이트 시트 슬라이싱 파싱 (Pig 몬스터 사례)
게임 내 수십 종의 몬스터와 캐릭터 애니메이션을 효율적으로 처리하기 위해 SpriteSheet 클래스를 통한 그리드 기반 자동 슬라이싱 및 비트맵 공유 시스템을 구축했습니다. 프로젝트 내 실제 돼지 몬스터(GOID_MONSTER_PIG)의 리소스 파싱 과정을 통해 스프라이트 시트가 어떻게 메모리에 적재되고 프레임 단위로 분할되는지 살펴봅니다.
pig_pigman_idle_loop_down.png
IDLE // 4x9 (33 FRAMES)
실제 하향 대기(Idle) 애니메이션 시트 (4열 × 9행, 총 33프레임으로 패킹된 고해상도 아틀라스)
pig_pigman_walk_loop_down.png
WALK // 4x11 (41 FRAMES)
실제 하향 이동(Walk) 애니메이션 시트 (4열 × 11행, 총 41프레임으로 패킹된 고해상도 아틀라스)
스프라이트 시트 3단계 파싱 파이프라인
1. 프레임 규격 자동 슬라이싱
AUTO-SIZE // SLICE
RegisterAnimation 호출 시 원본 해상도와 행/열 정보를 바탕으로 1프레임 크기를 자동 계산 및 분할
2. 단일 비트맵 공유 아키텍처
ZERO-COPY // MEMORY
원본 비트맵 1장만 메모리에 유지하고, 분할된 모든 프레임은 잘라낼 좌표 영역(sourceRect)만 참조
3. 수평 반전 사전 캐싱
PRE-BAKE // FLIP
좌측 방향(DIR_LEFT) 비트맵을 로딩 단계에서 미리 반전 캐싱하여 런타임 행렬 연산 없이 즉시 출력
- 프레임 규격 자동 산출 및 슬라이싱 (
RegisterAnimation): 돼지 몬스터처럼 상태마다 프레임 수(대기 33프레임, 이동 41프레임 등)가 다를 때, 프레임의 가로·세로 크기를 일일이 수동 입력하지 않아도 엔진이 원본 이미지 해상도와 열 개수를 읽어 1프레임 크기를 자동으로 역산합니다. 이를 통해 새로운 모션 시트가 추가되더라도 코드 수정 없이 유연하게 애니메이션을 등록할 수 있습니다. - 단일 비트맵 공유 아키텍처 (Zero Duplicate Memory): 수십 개의 애니메이션 프레임을 개별 이미지로 잘라내어 복사하면 메모리가 급증합니다. 따라서 원본 비트맵 1장만을 메모리에 유지(
std::shared_ptr)하고, 각 프레임은 원본 비트맵 안에서 오려낼 사각형 좌표(sourceRect)와 중심점(pivot) 정보만 참조하게 하여 메모리 복제 비용을 0바이트로 실현했습니다. - 수평 반전(Horizontal Flip) 사전 캐싱: 좌우 대칭 형태의 모션을 런타임에 매 프레임 변환 행렬로 뒤집으면 WinAPI 소프트웨어 렌더링에 적지 않은 CPU 오버헤드가 발생합니다. 따라서 좌측 방향(
DIR_LEFT) 애니메이션은 로딩 시점에 비트맵을 미리 수평 반전하여 캐싱해 두고, 렌더링 루프에서는 추가 연산 없이 초고속 래스터 출력(BitBlt)을 유지하도록 설계했습니다.
코드 보기: SpriteSheet.cpp - 자동 슬라이싱 및 프레임 파싱 핵심 구현
// SpriteSheet.cpp - CreateFromFile 핵심 슬라이싱 및 비트맵 공유 로직
std::shared_ptr<SpriteSheet> SpriteSheet::CreateFromFile(
const std::wstring& filePath, int frameWidth, int frameHeight,
int cols, int totalFrames, float pivotX, float pivotY, bool flipHorizontal)
{
auto sheet = std::shared_ptr<SpriteSheet>(new SpriteSheet());
auto bitmap = std::make_shared<Gdiplus::Bitmap>(filePath.c_str());
if (bitmap->GetLastStatus() != Gdiplus::Ok) return nullptr;
int sheetWidth = bitmap->GetWidth();
int sheetHeight = bitmap->GetHeight();
// frameWidth/frameHeight가 0이면 cols와 totalFrames로 동적 자동 계산
int finalFrameWidth = frameWidth;
int finalFrameHeight = frameHeight;
if (finalFrameWidth <= 0 && cols > 0) {
finalFrameWidth = sheetWidth / cols;
}
if (finalFrameHeight <= 0 && cols > 0 && totalFrames > 0) {
int totalRows = (totalFrames + cols - 1) / cols;
finalFrameHeight = sheetHeight / totalRows;
}
// 단일 원본 비트맵 포인터를 공유하며 33개 프레임의 UV sourceRect만 생성
sheet->m_bitmap = bitmap;
for (int i = 0; i < totalFrames; ++i) {
int col = i % cols;
int row = i / cols;
Gdiplus::Rect sourceRect(col * finalFrameWidth, row * finalFrameHeight,
finalFrameWidth, finalFrameHeight);
sheet->m_sprites.push_back(std::make_shared<Sprite>(bitmap, sourceRect, pivotX, pivotY));
}
return sheet;
}
4.3 람다 캡처를 활용한 이벤트 바인딩 및 프레임 이벤트 콜백 실행
객체(Player 등) 초기화 시점에 RegisterAnimation을 통해 애니메이션 클립을 생성하고, 특정 프레임 번호에 이벤트 키 문자열(eventName)을 등록합니다. 애니메이션 진행 중 해당 프레임에 도달하면 std::function 람다 캡처로 바인딩된 콜백 함수가 즉시 발동하여 타격 판정(OnChopHit), 효과음 재생(SoundManager::PlaySFX), 상태 전이(OnChopEnd) 등의 게임 로직을 유기적으로 연계합니다.
코드 보기: Player.cpp - 도끼질(Chop) 애니메이션 이벤트 바인딩 및 콜백 실행 예시
// Player.cpp - 도끼질 애니메이션 설정 및 이벤트 콜백 바인딩
const int CHOP_HIT_FRAME = 4; // 도끼가 나무에 맞는 프레임
// 클립에 이벤트 프레임 등록
AnimationClip* clip = m_animator->GetAnimationClip((int)PlayerState::CHOP, dir);
if (clip) {
clip->AddEventFrame(CHOP_HIT_FRAME, L"chop_hit");
clip->AddEventFrame(CHOP_LAST_FRAME, L"chop_end");
// 이벤트 콜백 람다 바인딩: 프레임 이벤트 검사 시 바인딩된 콜백 자동 실행
clip->SetEventCallback([this](int frameIndex, const std::wstring& eventName) {
if (eventName == L"chop_hit") {
SoundManager::GetInstance()->PlaySFX(L"Resource/Sound/PlayerSound/Chop_tree.wav");
this->OnChopHit(); // 타겟 나무에 데미지 적용
}
else if (eventName == L"chop_end") {
this->OnChopEnd(); // 상태 전이
}
});
}
5. 자체 제작 에디터와 데이터 파이프라인 (DontStarve_Editor & Data Pipeline)
방대한 오픈월드 맵과 수십 종의 오브젝트, 충돌체, 애니메이션 피벗을 하드코딩 없이 기획·제작하기 위해 독립 WinAPI 프로젝트로 전용 비주얼 에디터인 DontStarve_Editor를 자체 개발했습니다. 에디터에서 시각적으로 조정한 오브젝트 및 맵 데이터는 파일 시스템을 통해 게임 클라이언트(DontStarve_Client)에 재컴파일 없이 즉각 동기화되는 완전한 데이터 주도(Data-Driven) 파이프라인을 제공합니다.
5.1 듀얼 모드 에디터 런처 (EditorLauncher)
단일 WinAPI 프로세스 내에서 IEditorScreen 인터페이스 다형성을 활용하여, 윈도우 파괴 및 재생성 없이 맵 에디터(MapEditor)와 오브젝트 에디터(ObjectEditor)를 즉각 스위칭할 수 있는 런처 환경을 제공합니다.
EditorLauncher 실행 화면
LAUNCHER // WINAPI_GUI
GDI+ 더블 버퍼링 기반의 버튼 인터페이스로 Map Editor(맵 타일/배치)와 Object Editor(피벗/콜라이더)를 원클릭 전환
5.2 오브젝트 에디터: 시각적 피벗 & 콜라이더 기즈모 편집 (ObjectEditor)
엔티티의 스프라이트 원화를 뷰포트에 실시간으로 띄우고, 마우스 드래그 및 정밀 입력 다이얼로그를 통해 발밑 피벗(Foot Pivot)과 충돌체(Box/Circle Collider)를 직관적으로 편집할 수 있는 비주얼 툴셋입니다.
ObjectEditor 메인 작업 인터페이스
VIEWPORT // INSPECTOR
우측 오브젝트 팔레트에서 돼지 몬스터(GOID_MONSTER_PIG)를 선택한 상태로, 현재 피벗 (0.5, 1) 및 박스 콜라이더 오프셋과 단축키 HUD를 실시간 디스플레이
1. 발밑 피벗(Foot Pivot) 시각적 편집
Top-Down 쿼터뷰에서 캐릭터와 장애물 간의 앞뒤 렌더링 순서(Y-Sorting)는 '발이 바닥에 닿는 접지 기준점(Foot Pivot)'에 의해 결정됩니다. 에디터에서는 마우스 드래그로 초록색 십자선 기즈모를 자유롭게 이동시키거나, P 키를 눌러 $0.0 \sim 1.0$ 정규화 수치를 직접 입력할 수 있습니다.
발밑 피벗 기즈모 및 정밀 입력 다이얼로그
PIVOT // Y-SORTING_BASE
스프라이트 최하단 접지선에 맞춰 Pivot X: 0.500, Pivot Y: 1.000을 설정하여 완벽한 Y-Sorting 기준점 확립
2. Box 및 Circle 콜라이더 기즈모 편집
오브젝트의 실제 물리적 부피와 피격 판정 범위를 설정하기 위해 사각형(Box)과 원형(Circle) 기즈모를 실시간 드래그로 조절합니다.
Box Collider 8방향 리사이즈
AABB // 4-CORNER_HANDLE
붉은색 스프라이트 외곽선 내부에 초록색 박스 콜라이더를 배치하고, 4개 모서리 핸들을 드래그하여 실제 몸체 영역에 피팅
Circle Collider 반경 조절
CIRCLE // RADIUS_HANDLE
B 키로 원형 충돌체로 전환 후, 중심점 핸들과 외곽 반지름 핸들을 드래그하여 회전 대칭 충돌체 설정
3. GameData 오버라이드 텍스트 직렬화
오브젝트 에디터에서 시각적으로 조정한 발밑 피벗(Foot Pivot) 좌표와 콜라이더(Box/Circle Collider)의 오프셋 및 크기 데이터는 GameData/object_resource_overrides.txt 파일로 직렬화되어 디스크에 영구 저장됩니다. 상단 [저장하기] 버튼 또는 Ctrl+S를 누르면 EditorResourceManager::SaveAllObjectResourceOverrides()가 호출되어 아래와 같은 텍스트 규격으로 기록됩니다:
# object_resource_overrides.txt - pivot and collider from Object Editor
# IDName pivotX pivotY hasCollider colliderType offsetX offsetY width height centerX centerY radius
GOID_MONSTER_PIG 0.5 1 1 0 -80 -205 161 205 0 0 0
5.3 맵 에디터: 타일/오브젝트 배치와 맵 입출력 (.dsm) (MapEditor)
넓은 월드 맵을 구축하고 게임 내 지형 타일과 몬스터·오브젝트 스폰 위치를 직관적으로 설계할 수 있는 비주얼 맵 빌더입니다.
1. 맵 만들기 (타일 및 오브젝트 팔레트 배치)
우측 수직 팔레트 인터페이스를 통해 상단 지형 타일(흙, 잔디, 늪지대 등)과 하단 게임 오브젝트(풀, 묘목, 상록수, 돼지, 돼지집, 베리덤불 등)를 마우스로 자유롭게 선택하여 맵 위에 실시간으로 배치할 수 있습니다.
우측 팔레트 타일 선택 & 3x3 지형 페인팅
PALETTE // TILE_PAINT
우측 팔레트 상단에서 흙 타일(TILEID_DIRT_00)을 선택한 후, 초록색 3x3 브러시 영역으로 넓은 면적의 지형을 신속하게 칠하고 플레이어 시작 지점(초록 원 P)을 지정
오브젝트 팔레트 선택 & 자연물 군집 배치
PALETTE // PROP_PLACEMENT
우측 팔레트에서 풀(GOID_PROP_GRASS)과 상록수(GOID_PROP_EVERGREEN)를 선택해 칠해진 흙 타일 위에 원하는 밀도로 식생을 자연스럽게 배치한 모습
2. 맵 세이브 & 로드하기 (.dsm 파일 입출력 및 보스 씬 복원)
에디터 상단 메뉴 파일(F) -> 저장 / 열기를 통해 완성된 맵을 독자적인 포맷인 Don't Starve Map(.dsm) 파일로 디스크에 저장하거나 언제든 다시 불러올 수 있습니다. WinAPI 표준 공용 대화상자(GetSaveFileName, GetOpenFileName)를 기반으로 입출력 스트림을 제어합니다.
.dsm 맵 파일 저장 대화상자
FILE_SAVE // DSM_EXPORT
작업한 맵의 타일 배열, 플레이어 스폰 위치, 배치된 모든 오브젝트 좌표를 Map_Example.dsm이라는 파일명으로 직렬화하여 저장
.dsm 맵 파일 열기 대화상자
FILE_LOAD // DSM_IMPORT
저장된 Map_Example.dsm을 비롯해 기존에 사전 제작된 필드 및 보스 스테이지 맵 파일 목록을 선택하여 즉시 역직렬화 로드
실제 맵 에디터로 설계 및 저장된 뒤 불러와 플레이되는 대표적인 인게임 씬의 예시로 하운드 습격 필드(01_BossHound.dsm)와 거미 여왕 보스 아레나(02_BossSpiderQueen.dsm)가 있습니다:
하운드 무리 인카운터 필드 (01_BossHound.dsm)
SCENE_EXAMPLE // HOUND_FIELD
플레이어 스폰 마커(P)를 중심으로 노멀 하운드, 레드 하운드, 아이스 하운드가 긴장감 있게 분산 배치된 맵을 로드한 화면
거미 여왕 보스 아레나 (02_BossSpiderQueen.dsm)
SCENE_EXAMPLE // SPIDER_QUEEN
중앙의 보스 거미 여왕(GOID_MONSTER_SPIDERQUEEN)과 외곽에 배치된 복수의 거미집(코쿤) 스폰 기지 구조가 완벽하게 복원 로드된 화면
5.4 인게임 클라이언트 데이터 로드 파이프라인 (Client Data Binding)
에디터에서 저장한 object_resource_overrides.txt와 .dsm 맵 파일은 클라이언트 소스 코드를 단 한 줄도 재컴파일하지 않고 게임 실행 시 아래의 3단계 파이프라인을 통해 인게임 월드로 완벽하게 구체화됩니다.
1. DataManager::Init()
OVERRIDESobject_resource_overrides.txt 로드 ➔ 각 ID별 에디터에서 편집된 피벗 및 콜라이더 규격을 메모리 테이블에 덮어쓰기
2. SceneManager::LoadAllMapData()
PARSE .DSM등록된 씬 테이블의 .dsm 파일 스트림 로드 ➔ # TILES(타일맵), # PLAYER(스폰), # OBJECTS(엔티티 좌표) 섹션 분리
3. ParseMapFileInto 동적 씬 바인딩
WORLD INSTANTIATE맵 오브젝트 생성 시 DataManager의 갱신된 피벗/콜라이더를 즉시 전달받아 ObjectManager 공간 그리드에 안전 등록
코드 보기: DataManager.cpp - 오브젝트 오버라이드 데이터 덮어쓰기 로직
// DataManager.cpp - GameData/object_resource_overrides.txt 로드 및 id별 피벗/콜라이더 주입
void DataManager::Init()
{
// 1. 코드 기본 테이블 로드
for (size_t i = 0; i < DataTable::ObjectResourceCount; ++i) {
const auto& entry = DataTable::ObjectResourceTable[i];
m_objectResources[entry.id] = { entry.id, entry.baseDir, entry.imageName };
}
// 2. 에디터에서 저장된 오버라이드 텍스트 파일을 파싱하여 실시간 덮어쓰기
ResourcePathUtils::ParseObjectResourceOverridesFile(L"GameData/object_resource_overrides.txt",
[this](GameObjectID id, const ResourcePathUtils::ObjectResourceDef& overrideDef) {
auto it = m_objectResources.find(id);
if (it != m_objectResources.end()) {
it->second.pivotX = overrideDef.pivotX;
it->second.pivotY = overrideDef.pivotY;
it->second.hasCollider = overrideDef.hasCollider;
it->second.colliderType = overrideDef.colliderType;
it->second.colliderOffsetX = overrideDef.colliderOffsetX;
it->second.colliderOffsetY = overrideDef.colliderOffsetY;
it->second.colliderWidth = overrideDef.colliderWidth;
it->second.colliderHeight = overrideDef.colliderHeight;
it->second.colliderRadius = overrideDef.colliderRadius;
}
});
}
코드 보기: SceneManager.cpp & Function.h - .dsm 맵 파싱 및 오버라이드 결합
// SceneManager.cpp - DataManager의 리소스 정보를 전달하며 맵 파싱 수행
bool SceneManager::ParseMapFile(const std::wstring& mapFileName, MapData& outMapData)
{
auto getObjectResourceInfo = [](GameObjectID id) -> const ResourcePathUtils::ObjectResourceDef* {
return DataManager::GetInstance()->GetObjectResourceInfo(id);
};
return ResourcePathUtils::ParseMapFileInto(mapFileName, outMapData, getObjectResourceInfo);
}
// Function.h - ParseMapFileInto: .dsm 텍스트 섹션 파싱 및 오브젝트 스폰
template<typename GetObjectResourceInfoFunc>
inline bool ParseMapFileInto(const std::wstring& mapFileName, MapData& outMapData, GetObjectResourceInfoFunc getObjectResourceInfo)
{
std::wifstream file(mapFileName);
if (!file.is_open()) return false;
std::wstring line;
enum Section { NONE, METADATA, PLAYER, TILES, OBJECTS } section = NONE;
while (std::getline(file, line)) {
if (line.empty() || line[0] == L'#') {
if (line.find(L"# PLAYER") != std::wstring::npos) section = PLAYER;
else if (line.find(L"# TILES") != std::wstring::npos) section = TILES;
else if (line.find(L"# OBJECTS") != std::wstring::npos) section = OBJECTS;
continue;
}
if (section == OBJECTS) {
std::wstringstream ss(line);
std::wstring idStr, xStr, yStr;
if (std::getline(ss, idStr, L',') && std::getline(ss, xStr, L',') && std::getline(ss, yStr, L',')) {
GameObjectID objID = EnumTables::GetGameObjectID(idStr.c_str());
if (objID != GOID_NONE) {
const auto* pRes = getObjectResourceInfo(objID); // 에디터에서 수정한 피벗/콜라이더 획득
outMapData.gameObjects.push_back({ objID, std::stof(xStr), std::stof(yStr), pRes });
}
}
}
}
file.close();
return true;
}