OpenGL 게임 프로젝트 기본 뼈대 분석: 창 생성부터 렌더 루프, 2D 스프라이트 기반 확장 구조까지

OpenGL로 게임을 만든다고 하면 많은 사람이 가장 먼저 셰이더 문법이나 버퍼 객체부터 떠올린다. 하지만 실제로 프로젝트를 시작해 보면 제일 먼저 필요한 것은 화려한 그래픽 기술이 아니라, “게임을 올려둘 수 있는 안정적인 바닥”이다. 창을 만들고, OpenGL 컨텍스트를 초기화하고, 매 프레임마다 갱신과 렌더링을 분리해서 호출하고, 입력과 시간 정보를 관리하고, 이후 텍스처나 스프라이트 같은 상위 개념을 차곡차곡 쌓아 올릴 수 있어야 한다.

이번 글에서는 현재 이 OpenGLGame 프로젝트의 기본 뼈대를 바탕으로, 이 프로젝트가 어떤 책임 분리를 가지고 설계되어 있는지, 왜 이런 구조가 초기 게임 프레임워크로 괜찮은 출발점이 되는지, 그리고 앞으로 어떤 방향으로 확장하면 좋은지를 아주 자세하게 정리해 보려고 한다. 이 글은 단순한 파일 설명이 아니라, “이 프로젝트가 지금 어느 단계에 있고 앞으로 무엇을 담을 수 있는가”를 중심으로 읽으면 더 잘 이해된다.

OpenGL Game Project

OpenGL Game Project 사이트를 아래에 링크했습니다.

OpenGL Game Project Github

1. 이 프로젝트를 한 문장으로 요약하면

현재 프로젝트는 Windows 환경의 Visual Studio 기반 C++ OpenGL 게임 프로젝트이며, GLFW와 GLAD를 사용해 OpenGL 컨텍스트를 만들고, GLM으로 수학 연산을 처리하며, OpenGLWindow라는 베이스 클래스 위에 MainWindow를 올려 실제 게임 장면을 구성할 수 있게 만든 최소 실행 프레임워크라고 볼 수 있다.

조금 더 풀어서 말하면 다음과 같다.

  • 실행 진입점은 main이다.
  • 실제 창 생성과 메인 루프는 OpenGLWindow가 담당한다.
  • 프로젝트별 장면 초기화와 렌더링은 MainWindow가 오버라이드한다.
  • 2D 렌더링에 필요한 사각형 버퍼, 텍스처, 변환 행렬, 스프라이트 표현이 별도 클래스로 분리되어 있다.
  • Windows 리소스 접근과 문자열 인코딩 보조 유틸리티도 포함되어 있다.

즉, 지금 단계의 프로젝트는 “게임 완성본”이라기보다 “게임을 올리기 위한 엔진 초안”에 가깝다. 그리고 바로 이 점이 중요하다. 초반 프로젝트에서 가장 큰 가치는 기능 개수보다 구조의 방향성이기 때문이다.

2. 왜 이런 기본 뼈대가 중요한가

많은 OpenGL 입문 프로젝트는 예제 하나를 띄우는 데에는 성공하지만, 코드가 전부 main.cpp에 몰려 있거나, 입력 처리와 렌더링과 리소스 초기화가 뒤섞여 있어서 조금만 기능이 늘어나도 유지보수가 빠르게 어려워진다. 반면 이 프로젝트는 아직 단순하지만, 적어도 다음과 같은 분리 의식을 가지고 있다.

  • 창과 컨텍스트 관리는 OpenGLWindow
  • 실제 장면 로직은 MainWindow
  • 텍스처 로딩은 Texture2D
  • 기하 버퍼는 VertexBuffer2D
  • 오브젝트 배치와 회전, 반전은 Transform2D
  • 렌더 가능한 단위는 Sprite

이 구조는 게임 개발에서 매우 중요하다. 게임은 시간이 지날수록 기능이 폭발적으로 늘어난다. 플레이어, 적, UI, 이펙트, 오디오, 씬 전환, 리소스 캐시, 충돌 판정, 애니메이션, 상태 머신 등이 계속 추가된다. 처음부터 모든 것을 완벽하게 설계할 수는 없지만, 적어도 “무엇이 어디에 들어가야 하는가”에 대한 감각이 있으면 프로젝트가 훨씬 오래 버틴다.

이 프로젝트는 그런 의미에서 출발점이 괜찮다. 코드 양은 적지만, 관심사의 분리가 이미 시작되어 있기 때문이다.

3. 프로젝트 전체 구조: 어떤 계층으로 이루어져 있는가

현재 코드베이스를 보면 대략 다음 네 층으로 나눠서 이해할 수 있다.

3-1. 애플리케이션 시작 계층

이 계층의 핵심은 main.cpp다. 여기서는 프로그램 시작 시 디버그 메모리 누수 검사를 켜고, OpenGL 버전을 지정하고, MainWindow 인스턴스를 생성한 뒤 창 생성과 앱 실행을 요청한다.

핵심 흐름은 아주 단순하다.

  1. OpenGL 버전 지정
  2. 메인 윈도우 객체 생성
  3. 창 생성 시도
  4. 실패 시 종료
  5. 성공 시 메인 루프 진입

이 단순함은 장점이다. main이 복잡해지기 시작하면, 프로그램 생명주기와 게임 로직이 얽히기 쉽다. 현재 구조는 main을 최대한 얇게 유지하고, 실제 책임을 윈도우 클래스에 넘기고 있다.

또 하나 눈에 띄는 부분은 디버그 메모리 체크다. _CrtSetDbgFlag(_CRTDBG_ALLOC_MEM_DF | _CRTDBG_LEAK_CHECK_DF);를 통해 프로그램 종료 시 메모리 누수를 확인하도록 해 두었다. 이 설정 하나만으로도 “이 프로젝트가 최소한 디버그 편의성을 고려하고 있다”는 신호를 준다. 작은 프로젝트일수록 이런 습관이 쌓이면 이후 확장에서 큰 차이를 만든다.

4. OpenGLWindow: 이 프로젝트의 실제 엔진 코어

이 프로젝트에서 가장 중요한 클래스는 사실 MainWindow보다 OpenGLWindow다. MainWindow는 내용이 거의 비어 있지만, OpenGLWindow는 창 생성부터 루프 실행, FPS 계산, 입력 보조, 콜백 연결, 투영 행렬 재계산까지 담당하고 있다.

이 클래스를 이해하면 프로젝트의 절반 이상을 이해한 것과 같다.

4-1. 창 생성과 OpenGL 컨텍스트 초기화

createOpenGLWindow는 GLFW 초기화와 컨텍스트 생성의 중심 함수다. 여기서 다음 일을 한다.

  • GLFW 초기화
  • OpenGL 메이저/마이너 버전 지정
  • Core Profile 지정
  • 창 생성
  • 컨텍스트 활성화
  • GLAD 로더 초기화
  • 창 크기 콜백, 드래그 앤 드롭 콜백 등록
  • 마우스 버튼, 휠 콜백 등록
  • 초기 화면 크기 반영
  • 윈도우 스타일 일부 수정
  • 아이콘 이미지 로딩

이 함수 하나만 봐도 이 프로젝트가 단순히 OpenGL 함수만 호출하는 것이 아니라, 실제 데스크톱 애플리케이션으로서의 사용성을 어느 정도 의식하고 있음을 알 수 있다.

특히 인상적인 부분은 두 가지다.

첫 번째는 창 크기 변경 시 투영 행렬과 뷰포트를 함께 갱신하는 구조다. 이는 해상도 변화가 생겼을 때 화면 출력이 깨지지 않도록 하는 기본 장치다.

두 번째는 정적 콜백과 객체 인스턴스를 연결하는 방식이다. GLFW 콜백은 C 스타일 함수 포인터 기반이기 때문에 객체 멤버 함수에 바로 연결하기 어렵다. 이 프로젝트는 map<GLFWwindow*, OpenGLWindow*> _windows를 두고, 정적 콜백에서 다시 실제 객체를 찾아 멤버 함수를 호출하는 우회 구조를 사용한다. 아주 전형적이면서도 실전적인 패턴이다.

즉, OpenGLWindow는 단순한 창 래퍼가 아니라, C 스타일 라이브러리와 C++ 객체지향 구조를 접합하는 어댑터 역할도 수행하고 있다.

4-2. 메인 루프의 구조

runApp의 흐름은 게임 루프의 기본 원형을 잘 보여 준다.

  1. 수직 동기화 설정
  2. 투영 행렬 계산
  3. 장면 초기화
  4. 이전 프레임 시간 기록
  5. 창이 닫힐 때까지 반복
  6. 델타 타임과 FPS 갱신
  7. 렌더 호출
  8. 게임 로직 업데이트 호출
  9. 종료 시 장면 해제
  10. 창 파괴 및 GLFW 종료

중요한 점은 렌더링과 업데이트가 별도 함수로 나뉘어 있다는 것이다. 지금은 updateScene이 비어 있지만, 이 자리에 게임 상태 변경, 애니메이션 진척, 입력 기반 이동, 충돌 검사, 스폰 처리 등을 넣을 수 있다.

여기서 눈여겨볼 부분은 루프의 호출 순서다. 현재는 다음 순서로 돈다.

  • 시간 계산
  • 렌더
  • 업데이트

일반적으로는 업데이트 후 렌더 순서를 사용하는 경우가 많지만, 현재 구조가 틀렸다고 볼 수는 없다. 다만 추후 게임 상태를 프레임마다 정확히 반영하고 싶다면 updateScene() 이후 renderScene()으로 바꾸는 것이 더 직관적일 수 있다. 지금 단계에서는 큰 문제는 아니지만, 구조를 성장시킬 때 한 번 재검토할 만한 포인트다.

4-3. 시간 관리와 FPS 계산

게임 루프에서 시간이 중요한 이유는 간단하다. 프레임 속도가 달라도 게임 속도는 같아야 하기 때문이다.

이 프로젝트는 glfwGetTime()을 기반으로 프레임 간 시간 차이를 _timeDelta에 저장한다. 그리고 sof라는 함수로 속도값에 델타 타임을 곱하는 보조 기능도 제공한다. 이름은 짧지만 의도는 분명하다. 예를 들어 초당 300픽셀 이동 속도를 프레임 독립적으로 적용하려면 position += sof(300.0f) 같은 형태로 활용할 수 있다.

이 구조는 매우 유용하다. 초보 프로젝트에서 흔히 나오는 문제는 “내 컴퓨터에서는 빠르고 다른 컴퓨터에서는 느린 게임”인데, 델타 타임을 초기에 구조에 포함해 두면 이런 문제를 피하기 쉬워진다.

FPS 계산도 별도 카운터로 관리한다. _nextFPS를 프레임마다 증가시키고, 1초가 지나면 _FPS에 반영하는 단순한 구조다. 디버그 HUD를 붙이거나 성능 로그를 찍을 때 이 정보는 바로 활용할 수 있다.

4-4. 입력과 마우스 좌표 처리

keyPressedkeyPressedOnce는 GLFW 입력을 한 단계 감싼다.

  • keyPressed는 현재 프레임 기준 눌림 상태 확인
  • keyPressedOnce는 한 번만 반응해야 하는 입력 처리

이 구분은 게임에서 매우 중요하다. 예를 들어 캐릭터 이동은 키를 누르고 있는 동안 계속 반응해야 하지만, 메뉴 열기나 일시정지는 한 번만 트리거되어야 한다. 이런 차이를 초반부터 API 수준에서 분리한 점은 좋은 선택이다.

또한 getOpenGLCursorPosition은 일반 윈도우 좌표계를 OpenGL 스타일 좌표계로 뒤집어서 반환한다. 윈도우 시스템에서는 보통 좌상단이 원점이지만, OpenGL 2D 배치에서는 좌하단 원점을 쓰는 경우가 많다. 이 함수는 그 차이를 흡수해 준다. 나중에 UI 클릭 처리나 타일 선택 같은 기능을 넣을 때 꽤 유용하다.

4-5. 투영 행렬 관리

이 프로젝트는 두 개의 투영 행렬을 내부에 유지한다.

  • 원근 투영 행렬 getProjectionMatrix
  • 직교 투영 행렬 getOrthoProjectionMatrix

이 선택은 앞으로 2D와 3D 양쪽 가능성을 모두 열어 둔다는 점에서 흥미롭다. 현재 VertexBuffer2D, Sprite, Transform2D의 구조를 보면 실제 관심은 2D에 더 가까워 보이지만, 창 클래스 차원에서 원근 투영까지 준비해 둔 것은 이후 3D 오브젝트나 카메라 연출로 확장할 여지를 남긴다.

특히 직교 투영 행렬을 같이 제공하는 것은 UI, 2D 배경, 타일 기반 장면, 스프라이트 렌더링에 적합하다. 다시 말해 이 프로젝트는 이름은 OpenGLGame이지만, 실제 구조는 2D 게임 프레임워크에 매우 잘 어울린다.

5. MainWindow: 비어 있지만 가장 중요한 확장 포인트

MainWindow는 현재 구현만 보면 꽤 단순하다.

  • initializeScene에서 OpenGL 상태 초기화
  • renderScene에서 색상 및 깊이 버퍼 클리어
  • updateScene, releaseScene, onWindowSizeChanged, onMouseButtonPressed는 비어 있음

처음 보면 “기능이 거의 없네”라고 느낄 수 있다. 하지만 사실 이 클래스는 이 프로젝트에서 가장 중요한 사용자 확장 지점이다. 베이스 프레임워크인 OpenGLWindow가 운영체제와 OpenGL 컨텍스트를 관리한다면, MainWindow는 “이 게임이 실제로 무엇을 그릴 것인가”를 책임지는 곳이다.

initializeScene에서 설정하는 OpenGL 상태를 보면 앞으로의 의도가 보인다.

  • 배경색 지정
  • 알파 블렌딩 활성화
  • 시저 테스트 활성화
  • 깊이 테스트 활성화
  • 컬링 활성화
  • 멀티샘플 관련 옵션 활성화

이 설정은 단순한 삼각형 하나가 아니라, 스프라이트나 UI와 3D 요소가 뒤섞일 수도 있는 일반적인 게임 씬을 염두에 둔 초기 상태라고 볼 수 있다. 즉, 지금 장면은 비어 있지만 무대 장치는 이미 켜져 있는 셈이다.

이 클래스에 앞으로 들어갈 만한 책임은 다음과 같다.

  • 셰이더 생성과 보관
  • 텍스처 로딩
  • 플레이어, 배경, UI 오브젝트 생성
  • 입력 처리에 따른 게임 상태 변경
  • 오브젝트 갱신
  • 렌더 순서 제어
  • 씬 종료 시 리소스 정리

결국 MainWindow는 “한 게임의 첫 번째 씬”이 될 수도 있고, 더 발전하면 씬 매니저 아래에서 메뉴 씬과 플레이 씬으로 분리되기 전의 단일 게임 월드 역할을 할 수도 있다.

6. 2D 렌더링 기반: VertexBuffer2D, Texture2D, Transform2D, Sprite

이 프로젝트의 현재 재미있는 부분은 창만 만들고 끝나는 것이 아니라, 이미 2D 오브젝트를 그릴 수 있는 최소 구성 요소가 들어 있다는 점이다. 이 네 클래스를 함께 보면 하나의 렌더링 파이프라인이 보인다.

6-1. VertexBuffer2D: “그릴 사각형”을 만든다

게임에서 2D 이미지를 출력할 때 사실 대부분은 텍스처를 입힌 사각형을 그리는 것이다. VertexBufferSystem2D::Generate()는 바로 그 사각형을 위한 버퍼를 만든다.

구성은 다음과 같다.

  • 정점 4개
  • 인덱스 6개
  • 색상 버퍼
  • UV 버퍼
  • VAO, VBO, EBO 생성 및 속성 연결

사각형을 두 개의 삼각형으로 나누어 그리는 전형적인 방식이며, 좌표는 0~1 범위의 정규화된 로컬 공간으로 잡혀 있다. 이 방식의 장점은 위치와 크기와 회전을 모두 Transform2D 쪽에서 처리할 수 있다는 점이다.

즉, 버퍼 자체는 “기본 단위 사각형”이고, 실제 화면상의 배치와 모양은 변환 행렬로 결정된다. 이 패턴은 매우 확장성이 좋다. 스프라이트, 버튼, 체력바, 타일, 파티클 대부분이 같은 버퍼 구조를 재사용할 수 있기 때문이다.

6-2. Texture2D: 이미지 데이터를 GPU 텍스처로 변환한다

TextureSystem::Generate는 파일 경로 또는 메모리 상의 이미지 데이터를 받아 OpenGL 텍스처를 생성한다. 내부적으로 stbi_load 또는 stbi_load_from_memory를 사용하고, 텍스처 파라미터도 함께 설정한다.

이 클래스의 의미는 크다. 왜냐하면 게임에서는 “파일을 읽는다”와 “GPU에 올린다”가 같은 일이 아니기 때문이다. 이미지 파일은 디스크 자원이고, 텍스처는 GPU 자원이다. 두 단계를 분리해서 생각해야 나중에 로딩, 해제, 캐싱, 비동기 처리까지 확장할 수 있다.

현재 구현에서 보이는 특징은 다음과 같다.

  • 모든 이미지를 RGBA 형태로 통일해서 읽는다.
  • 밉맵을 생성한다.
  • 래핑은 GL_REPEAT
  • 최소/최대 필터는 GL_LINEAR

이 선택은 부드러운 일반 2D 출력에는 무난하다. 추후 픽셀 아트 스타일 게임이라면 GL_NEAREST로 바꾸는 선택지도 생긴다. 즉, 지금 설정은 일단 “깨지지 않고 부드럽게 보이는 기본값”이라고 이해하면 된다.

또한 삭제 함수가 별도로 있어 OpenGL 리소스 수명 관리가 클래스 레벨에서 어느 정도 의식되고 있다는 점도 중요하다.

6-3. Transform2D: 위치, 크기, 회전, 반전을 행렬로 묶는다

Transform2D는 아주 작지만, 게임 오브젝트 표현의 핵심이다. 내부에는 다음 상태가 있다.

  • 위치 mPosition
  • 스케일 mScale
  • 각도 mAngle
  • 좌우 반전 mFlipX
  • 상하 반전 mFlipY

그리고 Get() 함수가 이를 하나의 모델 행렬로 합성한다.

이 클래스가 좋은 이유는 “그리는 대상”과 “배치 방식”을 분리하기 때문이다. 텍스처나 버퍼는 오브젝트의 모양에 가까운 정보고, 트랜스폼은 그 오브젝트가 어디에 어떤 자세로 존재하는지에 대한 정보다. 이 둘을 분리하면 애니메이션, 충돌 박스, 부모-자식 계층, 카메라 오프셋 같은 시스템을 나중에 얹기 쉬워진다.

특히 반전 처리 로직이 별도 bool로 관리되는 점은 2D 게임에서 실용적이다. 캐릭터가 왼쪽을 보느냐 오른쪽을 보느냐 같은 표현은 텍스처를 두 장 준비하는 대신 스케일 부호를 바꿔 해결하는 경우가 많다. 이 프로젝트는 그 기반을 이미 갖고 있다.

6-4. Sprite: 렌더 가능한 게임 오브젝트의 최소 단위

Sprite는 지금 프로젝트에서 가장 “게임 오브젝트답게” 보이는 클래스다. 셰이더, 텍스처, 버퍼, 트랜스폼을 묶어 실제 화면에 그릴 수 있는 객체를 만든다.

Draw()를 보면 의도가 명확하다.

  1. 보이는 상태인지 확인
  2. 셰이더 활성화
  3. 깊이값 전달
  4. 모델 행렬 전달
  5. 텍스처 바인딩
  6. 버퍼 드로우

이건 곧 “렌더링 1단위”를 캡슐화한 것이다. 상위 시스템은 스프라이트에게 위치를 바꾸고, 회전시키고, 보이게 하거나 숨기고, 마지막에 Draw()만 호출하면 된다.

또한 이 클래스에는 단순 렌더링 외에 게임 로직을 위한 작은 보조 기능도 있다.

  • AABB 기반 충돌 검사
  • 이동 여부 확인
  • 화면 영역 내 존재 여부 확인
  • 마지막 위치 기록

이 부분이 흥미롭다. 현재 프로젝트는 아직 순수 렌더러에 머무르지 않고, 이미 “오브젝트 단위 게임성”을 조금씩 담고 있다. 물론 이후 더 정교한 컴포넌트 시스템이나 물리 계층이 생길 수 있겠지만, 초기 단계에서는 이렇게 렌더 객체에 최소 게임 속성을 얹는 방식도 충분히 실용적이다.

7. Shader: 작은 프로젝트에서 아주 현실적인 선택

Shader 클래스는 헤더 파일 하나에 구현이 함께 들어 있는 구조다. 보통 더 큰 프로젝트에서는 선언과 구현을 분리하지만, 현재 규모에서는 이 선택도 충분히 이해 가능하다.

이 클래스는 두 가지 방식의 생성자를 제공한다.

  • 셰이더 파일 경로로부터 읽는 방식
  • 문자열 소스 코드 자체를 받아 컴파일하는 방식

이중 두 번째 방식은 특히 ResourceManager와 결합될 때 의미가 생긴다. 예를 들어 셰이더 소스를 외부 파일이 아니라 Windows 리소스나 패키징된 문자열에서 읽어 올 수도 있다. 즉, 단순한 유틸리티 같지만 나중에 배포 구조를 바꿀 때 꽤 유연하게 작동할 여지가 있다.

또한 setBool, setInt, setFloat, setVec*, setMat* 계열을 모두 제공하므로, 셰이더 uniform 전달 인터페이스가 이미 깔끔하게 추상화되어 있다. 이 클래스 덕분에 상위 코드에서는 OpenGL의 로우 레벨 uniform 호출을 직접 반복하지 않아도 된다.

작은 프로젝트에서 이런 래퍼는 엄청난 생산성 차이를 만든다. 결국 게임 로직 코드는 셰이더 핸들보다 “무엇을 그릴 것인가”에 더 집중해야 하기 때문이다.

8. ResourceManagerEncoding: 플랫폼 친화적인 보조 도구

겉으로는 덜 눈에 띄지만, 이 프로젝트의 성격을 보여 주는 두 파일이 있다. ResourceManagerEncoding이다.

8-1. ResourceManager: 실행 파일 내부 리소스 접근을 위한 준비

ResourceManager는 Windows API 기반으로 리소스를 읽어 오는 헬퍼다. 문자열 형태로도 읽을 수 있고, 메모리 포인터와 크기 형태로도 받을 수 있다. 리소스 종류도 셰이더, PNG, 폰트, WAV, MP3 등으로 구분할 수 있게 되어 있다.

이것이 중요한 이유는 게임 배포 방식 때문이다. 초기에는 파일 경로에서 직접 불러오는 편이 쉽지만, 나중에는 다음 같은 고민이 생긴다.

  • 실행 파일과 리소스를 어떻게 함께 배포할 것인가
  • 셰이더 파일 누락을 어떻게 막을 것인가
  • 외부 파일 경로 의존성을 줄일 수 있는가

이때 Windows 리소스 시스템을 사용하면 일정 부분 해결 가능하다. 물론 현재 코드에서 이 매니저가 본격적으로 사용되고 있지는 않지만, 구조상 이미 포함되어 있다는 사실은 프로젝트 확장 방향을 보여 준다.

8-2. Encoding: 드래그 앤 드롭 파일 경로 처리에서 빛나는 유틸리티

Encoding 유틸리티는 UTF-8과 ACP 코드페이지 간 변환을 제공한다. 이것이 왜 필요할까? Windows 환경에서는 파일 경로와 문자열 인코딩 문제가 생각보다 자주 등장한다. 특히 한글 경로, 시스템 로캘, 외부 입력 문자열 등이 섞이면 파일 처리에서 예상치 못한 문제가 발생한다.

현재 drop_callback에서 드래그 앤 드롭된 파일 경로를 utf8_to_acp로 변환하는 부분을 보면, 이 프로젝트가 단순 렌더링 예제를 넘어 실제 데스크톱 사용 시나리오를 생각하고 있다는 점을 알 수 있다.

게임 개발에서는 이런 “눈에 잘 안 띄는 안정성 코드”가 의외로 중요하다. 화려한 그래픽보다 먼저 개발 생산성을 무너뜨리는 것은 종종 인코딩과 경로 처리 문제이기 때문이다.

9. 이 프로젝트가 현재 보여 주는 기술 스택과 개발 철학

이 프로젝트의 구성 요소를 보면 기술 선택도 비교적 명확하다.

  • C++20
  • Visual Studio 기반 Windows 데스크톱 프로젝트
  • GLFW로 창 및 입력 처리
  • GLAD로 OpenGL 함수 로딩
  • GLM으로 벡터/행렬 계산
  • stb_image로 이미지 로딩
  • Windows API와 런타임 디버그 도구 일부 사용

여기서 읽히는 철학은 “필요한 외부 라이브러리는 쓰되, 게임 구조 자체는 직접 쌓는다”에 가깝다. 완성형 엔진을 가져다 쓰는 대신, 창 생성과 렌더 루프, 스프라이트 처리 등 핵심 구조를 학습 가능한 수준으로 직접 통제하고 있다.

이런 방식의 장점은 명확하다.

  • 그래픽 파이프라인을 직접 이해할 수 있다.
  • 원하는 구조를 직접 설계할 수 있다.
  • 엔진 내부를 블랙박스로 두지 않아도 된다.

반면 단점도 있다.

  • 해야 할 일이 많다.
  • 리소스 관리, 씬 관리, 오디오, UI 등을 모두 직접 설계해야 한다.
  • 작은 구조적 실수가 나중에 큰 부채가 될 수 있다.

그럼에도 불구하고, 학습과 커스텀 게임 제작 관점에서는 매우 좋은 방향이다. 특히 지금 프로젝트처럼 기본 뼈대를 먼저 만들고 그 위에 시스템을 단계적으로 얹는 방식은 실전적으로도 꽤 건강한 출발이다.

10. 지금 단계의 강점

현재 프로젝트의 강점을 냉정하게 정리해 보면 다음과 같다.

10-1. 책임 분리가 이미 시작되어 있다

main이 얇고, 창 관리와 장면 로직이 분리되어 있으며, 텍스처와 버퍼, 트랜스폼, 스프라이트가 별도 단위로 나뉘어 있다. 작은 프로젝트에서 이 정도 분리만 되어 있어도 이후 유지보수 난도가 크게 달라진다.

10-2. 2D 게임으로 확장하기 좋은 형태다

직교 투영, 사각형 버퍼, 텍스처, 스프라이트, 트랜스폼 구조를 보면 타일맵, 액션 게임, 퍼즐 게임, UI 기반 게임으로 확장하기 좋다. “무언가를 그릴 수 있는 상태”를 넘어 “오브젝트 단위로 관리할 수 있는 상태”에 이미 가까워져 있다.

10-3. 프레임 독립 업데이트를 위한 시간 기반 구조가 있다

델타 타임과 FPS 계산이 이미 들어가 있어서, 이후 이동 속도나 애니메이션 속도를 프레임에 덜 종속적으로 설계할 수 있다.

10-4. 플랫폼 실무 감각이 조금 들어 있다

윈도우 아이콘, 드래그 앤 드롭, 인코딩 변환, 메모리 누수 점검 같은 요소는 흔한 튜토리얼 코드보다 한 단계 실용적이다.

11. 지금 단계의 한계와 앞으로 보완하면 좋은 점

좋은 출발점이라는 말은 아직 완성형은 아니라는 뜻이기도 하다. 오히려 지금은 구조를 다듬기 가장 좋은 시점이다. 이 프로젝트에서 앞으로 손보면 특히 좋아질 부분을 자세히 짚어 보자.

11-1. 게임 상태와 씬 구조가 아직 없다

현재는 MainWindow 하나가 모든 장면을 담당하는 구조다. 초기에는 괜찮지만, 메뉴 화면, 로딩 화면, 플레이 화면, 결과 화면 등이 생기면 씬 분리가 필요해진다.

가장 자연스러운 다음 단계는 다음과 같은 흐름이다.

  • Scene 인터페이스 도입
  • initialize, update, render, release 분리
  • SceneManager로 현재 씬 교체
  • MainWindow는 씬 매니저를 소유하고 위임만 수행

이렇게 가면 창 클래스는 창 역할에 더 집중하고, 게임 콘텐츠는 씬으로 분리된다.

11-2. 리소스 캐싱 구조가 아직 약하다

지금은 Sprite 생성 시 텍스처와 버퍼를 직접 만든다. 이 방식은 간단하지만, 같은 이미지 파일을 여러 번 읽거나 같은 사각형 버퍼를 중복 생성할 가능성이 높다.

추후에는 다음 같은 구조가 좋다.

  • 텍스처 매니저에서 경로별 캐싱
  • 셰이더 매니저에서 이름별 캐싱
  • 공용 쿼드 버퍼 재사용
  • 참조 카운트 혹은 소유권 정책 정리

작은 게임도 오브젝트 수가 늘어나면 중복 로딩 비용이 금방 커진다.

11-3. 렌더링과 로직의 경계가 더 선명해질 필요가 있다

현재 Sprite는 렌더링과 충돌 보조 기능을 함께 가진다. 이 정도는 나쁘지 않지만, 프로젝트가 커지면 다음 선택이 필요해진다.

  • Sprite를 순수 렌더 컴포넌트로 유지할지
  • 게임 오브젝트가 Transform, Collider, Renderer 같은 컴포넌트를 조합하는 구조로 갈지

초기에는 단순 클래스 하나가 빠르지만, 기능이 많아지면 역할 분리가 다시 필요해진다.

11-4. 예외 처리와 실패 처리의 일관성이 더 필요하다

예를 들어 텍스처 로딩 실패나 GLAD 초기화 실패, 아이콘 파일 누락, 셰이더 파일 읽기 실패 등은 현재 로그 출력 수준에서 머무는 경우가 있다. 이 부분은 나중에 안정성을 위해 다음 중 하나로 통일하는 것이 좋다.

  • 실패 시 즉시 종료
  • 실패 상태를 반환하고 상위에서 판단
  • 기본 대체 리소스를 사용

게임 프로젝트는 리소스 하나가 빠져도 전체가 이상하게 보일 수 있으므로, 실패 전략을 정해 두는 것이 중요하다.

11-5. 플랫폼 종속 코드가 명시적으로 분리되지 않았다

현재는 Windows API 호출이 OpenGLWindow, ResourceManager, Encoding 등 여러 위치에 자연스럽게 섞여 있다. Windows 전용 프로젝트라면 괜찮지만, 장기적으로 플랫폼 이식성을 고려한다면 래퍼 계층을 두는 편이 좋다.

물론 지금 단계에서는 과도한 추상화보다 빠른 개발이 더 중요할 수 있다. 다만 구조가 커지기 전에 어디까지를 플랫폼 전용으로 둘지 기준을 세워 두면 이후 훨씬 수월하다.

12. 이 뼈대를 바탕으로 어떤 게임을 만들기 좋은가

현재 구조를 기준으로 보면 다음 장르가 특히 잘 맞는다.

12-1. 2D 퍼즐 게임

직교 투영, 스프라이트, 격자 기반 배치, 마우스 입력 처리 구조와 잘 맞는다. 블록 퍼즐, 매치 퍼즐, 카드 퍼즐 등은 지금 구조에서 비교적 자연스럽게 확장할 수 있다.

12-2. 2D 액션 또는 아케이드 게임

스프라이트 이동, 충돌 판정, 입력 처리, 델타 타임 구조가 이미 있어 슈팅 게임이나 간단한 플랫폼 액션의 시작점으로도 적합하다.

12-3. UI 중심 시뮬레이션 게임

버튼, 패널, 아이콘, 수치 표시 등도 결국 2D 렌더링과 입력 처리 위에서 동작하므로, 적절한 UI 레이어만 추가하면 충분히 대응 가능하다.

반면 지금 상태에서 바로 하기 어려운 영역도 있다.

  • 대규모 3D 씬 관리
  • 복잡한 애니메이션 파이프라인
  • ECS 중심 대형 프로젝트 구조
  • 멀티스레드 리소스 스트리밍

즉, 이 프로젝트는 “작고 명확한 게임”을 만들기에 특히 좋은 출발점이며, 대규모 엔진 수준 기능은 앞으로 점진적으로 쌓아 올려야 한다.

13. 만약 내가 이 프로젝트를 다음 단계로 확장한다면

이 글을 마무리하기 전에, 이 뼈대를 기반으로 실제 개발을 계속한다면 어떤 순서로 확장할지 제안해 보겠다.

1단계. 장면에 실제 오브젝트 하나를 올린다

  • 셰이더 준비
  • 텍스처 하나 로드
  • Sprite 하나 생성
  • 위치와 크기 설정
  • renderScene에서 그리기

이 단계의 목표는 “빈 배경을 넘어서 실제 화면 요소 하나가 보이는 상태”다.

2단계. 입력과 이동을 붙인다

  • 키 입력으로 위치 이동
  • 델타 타임 기반 속도 적용
  • 화면 밖 제한 처리

여기서 게임으로서의 감각이 생기기 시작한다.

3단계. 다수 오브젝트와 업데이트 루프를 정리한다

  • 오브젝트 컨테이너 관리
  • 업데이트 루프에서 일괄 갱신
  • 렌더 순서와 깊이값 정리

이 단계부터는 사실상 작은 게임 프레임워크가 된다.

4단계. 리소스 매니저와 씬 시스템을 도입한다

  • 텍스처/셰이더 캐싱
  • 씬 전환 구조
  • 메뉴와 플레이 장면 분리

이때부터 프로젝트가 “예제”에서 “실제 게임”으로 넘어가기 시작한다.

5단계. 충돌, 애니메이션, UI, 오디오를 붙인다

  • 충돌 계층 분리
  • 스프라이트 시트 애니메이션
  • 텍스트/UI 렌더링
  • 사운드 재생 시스템

현재 프로젝트 설정에 FMOD 관련 라이브러리 흔적도 보이므로, 오디오 시스템을 연결하는 방향도 충분히 고려할 수 있다.

14. 마무리: 이 프로젝트는 작지만 방향이 좋다

이 OpenGLGame 프로젝트는 아직 기능이 많지 않다. 화면에는 지금 사실상 배경색만 그려지고, MainWindow의 대부분은 비어 있으며, 게임 콘텐츠는 아직 본격적으로 올라오지 않았다. 하지만 이 프로젝트를 단순히 “아직 아무것도 없는 상태”라고 보는 것은 정확하지 않다.

오히려 더 정확한 평가는 이렇다.

이 프로젝트는 이미 게임이 자라날 자리와 규칙을 어느 정도 갖춘 상태다.

창 생성, OpenGL 초기화, 메인 루프, 시간 관리, 입력 처리, 투영 행렬, 2D 버퍼, 텍스처, 스프라이트, 리소스 보조 유틸리티까지, 게임 프로젝트의 뼈대를 이루는 핵심들이 작지만 분명하게 들어 있다. 무엇보다 중요한 것은 관심사 분리가 시작되었다는 점이다. 이 한 가지 사실만으로도 이후 확장 가능성은 크게 높아진다.

결국 좋은 기본 뼈대란 처음부터 모든 기능을 가진 프로젝트가 아니다. 오히려 새로운 기능이 들어와도 어디에 놓아야 할지 감이 오는 프로젝트가 좋은 뼈대다. 그런 관점에서 보면, 현재 이 프로젝트는 화려하지는 않지만 꽤 건강한 출발선에 서 있다.

앞으로 여기에 첫 스프라이트가 올라가고, 입력으로 움직이기 시작하고, 충돌과 애니메이션과 씬 전환이 더해진다면, 이 작은 뼈대는 금세 하나의 게임다운 구조로 성장할 수 있다. 그리고 바로 그런 성장 가능성이 이 프로젝트의 가장 큰 가치다.

Leave a comment