C++로 게임 엔진을 직접 만들면서 얻은 깨달음

by DD
5개월 전
조회수 17

C++와 DirectX 9, Win32 API를 사용하여 3개월 동안 게임 엔진을 직접 제작하며 게임 개발의 핵심 원리(Core Principles)를 학습함

상태 패턴(State Pattern) 적용을 통해 게임 아키텍처를 개선하고, 코드의 유연성과 유지보수성을 확보함

Swept AABB 충돌 감지(Swept AABB Collision Detection)를 구현하여 고속 이동 객체의 충돌 문제를 해결하고, 물리 시뮬레이션의 정확성을 높임

DirectX 9의 렌더링 파이프라인(Rendering Pipeline)을 직접 구현하며 그래픽스 프로그래밍(Graphics Programming)에 대한 깊이 있는 이해를 얻음

게임 엔진 제작 과정을 통해 유니티(Unity)와 같은 기존 도구의 추상화(Abstraction) 뒤에 숨겨진 원리(Principles)를 이해하고, 문제 해결 능력을 향상시킴

아키텍처 설계의 중요성: 상태 패턴(State Pattern) 적용

본문에서는 게임 엔진 개발 초기에 아키텍처 설계(Architecture Design)의 중요성을 간과하여 겪었던 어려움을 설명한다. 초기에는 모든 게임 로직을 단일 클래스에 통합하여 코드의 복잡성이 증가하고, 새로운 기능을 추가하는 데 어려움을 겪었다. 이를 해결하기 위해 상태 패턴(State Pattern)을 적용하여 각 게임 화면(메뉴, 레벨, 게임 오버 등)을 별도의 클래스로 분리했다.

IGameState 인터페이스(Interface): 모든 게임 상태 클래스가 구현해야 하는 Update, Render, OnEnter, OnExit 메서드 정의

Game 클래스: 현재 상태를 관리하고, 상태 전환을 담당

결과: 코드의 모듈성(Modularity)과 유연성(Flexibility)을 확보하고, 유지보수성을 향상시킴

이러한 경험을 통해 저자는 설계 중심 개발(Design-First Development)의 중요성을 깨달았으며, 특히 1,000줄 이상의 프로젝트에서는 더욱 중요함을 강조한다.

DirectX 9 렌더링 파이프라인(Rendering Pipeline) 구현

저자는 DirectX 9를 사용하여 렌더링 파이프라인을 직접 구현하는 과정을 상세히 설명한다. DirectX 9는 오래되었지만, 렌더링의 기본 원리를 배우기에 적합하다는 점을 강조한다. 렌더링 파이프라인 초기화 과정은 다음과 같다.

Direct3D9 인터페이스 생성: 그래픽 드라이버(Graphics Driver)에 연결

디스플레이 기능 쿼리: 해상도(Resolution) 및 색상 형식(Color Format) 확인

프레젠테이션 파라미터 설정: 윈도우 모드(Windowed Mode), 백버퍼(Backbuffer) 설정

디바이스 생성: 하드웨어 가속(Hardware Acceleration) 또는 소프트웨어 처리(Software Processing) 선택

스프라이트 렌더러 생성: 2D 게임을 위한 헬퍼(Helper) 사용

각 프레임 렌더링 과정은 화면 지우기, 드로우 콜(Draw Call) 시작, 스프라이트 변환 적용, 텍스처(Texture) 드로우, 프레임 종료 순으로 진행된다. 특히, 더블 버퍼링(Double Buffering)을 통해 화면 찢김(Tearing) 현상을 방지한다.

Swept AABB 충돌 감지(Collision Detection) 구현

저자는 고속으로 움직이는 객체의 충돌 문제를 해결하기 위해 Swept AABB(Ray-swept Box) 충돌 감지 기법을 사용했다. 기존의 개별 프레임(Discrete Frame) 기반 충돌 감지 방식은 객체가 빠르게 움직일 경우, 객체가 벽을 통과하는 터널링(Tunneling) 현상이 발생할 수 있다.

Swept AABB: 객체의 이동을 레이(Ray)로 간주하여, 프레임 내에서 충돌 발생 여부를 판단

x, y 축별 충돌 시간 계산: 각 축에서 충돌 발생 시간을 계산하고, 가장 빠른 시간을 선택

충돌 지점 계산: 충돌 발생 시간(Time of Impact)을 사용하여 정확한 충돌 지점 계산

이러한 방식을 통해 저자는 터널링 현상을 해결하고, 물리 시뮬레이션의 정확성을 향상시켰다. Swept AABB 구현은 게임 내에서 빠르게 움직이는 객체(Fast-moving Objects)의 충돌을 처리하는 데 필수적이다.

충돌 감지(Detection)와 충돌 해결(Resolution)의 분리

저자는 충돌 감지와 충돌 해결을 분리하는 것이 중요하다고 강조한다. 초기에는 충돌 감지와 해결을 하나의 단계에서 처리하여, 버그(Bug) 발생 및 복잡성 증가를 경험했다.

충돌 감지: 두 객체가 충돌했는지 여부 판단

충돌 해결: 충돌 발생 시, 객체의 위치를 조정하고 반사(Reflection) 적용

분리된 단계: PhysicsManager에서 감지 및 해결을 분리하여, 복잡한 시나리오(Multiple Overlaps, Conveyor Belts) 처리

이러한 분리를 통해 저자는 코드의 유연성을 높이고, 다양한 충돌 시나리오를 효과적으로 처리할 수 있게 되었다. 특히, 물리 엔진(Physics Engine)을 설계할 때, 감지와 해결을 분리하는 것은 일반적인 설계 패턴이다.

게임 루프(Game Loop) 구조

저자는 게임 엔진의 핵심인 게임 루프(Game Loop) 구조를 설명한다. 게임 루프는 게임의 모든 동작을 제어하며, 프레임 독립적인 움직임을 보장하기 위해 델타 타임(Delta Time)을 사용한다.

델타 타임 계산: CalculateDeltaTime() 함수를 통해 프레임 간 시간 간격 계산

입력 업데이트: inputManager.Update()를 통해 사용자 입력 처리

게임 상태 업데이트: 현재 게임 상태(Menu, Level, GameOver 등) 업데이트

렌더링: renderer.BeginFrame(), gameState.Render(), renderer.EndFrame()을 통해 화면에 그리기

이러한 구조는 모듈성(Modularity), 테스트 용이성(Testability), 유지보수성(Maintainability), 확장성(Scalability)을 제공한다. 각 시스템은 하나의 역할만 담당하며, 새로운 게임 상태를 추가하는 것이 용이하다.

I Built a Game Engine from Scratch in C++ (Here's What I Learned)