콘텐츠 대표 이미지 - C언어 그래픽 프로그래밍과 SDL · OpenGL을 활용한 실전 게임 개발 완전 가이드
🎮 프로그램개발 · C언어

C언어 그래픽 프로그래밍과 SDL · OpenGL을 활용한 실전 게임 개발 완전 가이드

SDL2 + OpenGL + C언어로 나만의 게임 만들기 🕹️ | 기초부터 실전까지 팩트 폭격 🚀

🎯 💻 🖥️ 🎨 🔧 🚀
#include <SDL2/SDL.h> #include <GL/gl.h> int main(int argc, char* argv[]) { SDL_Init(SDL_INIT_VIDEO); } C언어 기반 그래픽 프로그래밍 SDL2 + OpenGL 실전 게임 개발 렌더링 파이프라인 셰이더 · 텍스처 · 물리 C언어 SDL2 OpenGL 게임개발 그래픽스

야 솔직히 말하자면... C언어로 게임 만든다고 하면 주변에서 "그거 왜 해? 유니티 쓰면 되잖아ㅋㅋ" 이런 반응 많이 들을 거임 😅
근데 진짜 게임 엔진이 어떻게 돌아가는지 이해하고 싶다면? 그래픽 파이프라인이 뭔지 뼛속까지 느끼고 싶다면? C언어 + SDL + OpenGL 조합이 답이야.

이 글에서는 C언어로 그래픽 프로그래밍을 시작하는 방법부터, SDL2로 윈도우 띄우고, OpenGL로 3D 렌더링하고, 실제 게임 루프 짜는 것까지 팩트만 빡세게 다룰 거임. 준비됐지? 🔥


🧱 1. C언어와 그래픽 프로그래밍 — 왜 굳이 C야?

먼저 "왜 C언어냐?"부터 짚고 넘어가자. 파이썬도 있고 C++도 있는데 왜 C냐고?
이유는 명확함. C언어는 하드웨어와 가장 가까운 고수준 언어이고, 대부분의 그래픽 라이브러리(OpenGL, Vulkan, DirectX)가 C 인터페이스를 기본으로 제공함.
SDL2도 마찬가지로 순수 C API야. 즉, C를 알면 그래픽 프로그래밍의 진짜 뼈대를 이해할 수 있음 💪

📌 C언어 그래픽 프로그래밍의 핵심 장점

① 메모리 직접 제어 — 픽셀 버퍼를 직접 조작 가능. 렌더링 최적화의 끝판왕
② 이식성(Portability) — Windows, Linux, macOS, 임베디드까지 동일 코드 동작
③ 성능 — 오버헤드 없는 직접 API 호출. 게임 엔진 내부 구조와 동일
④ 학습 효과 — 그래픽 파이프라인, 버퍼, 셰이더를 직접 다루며 원리 이해

실제로 Quake, Doom, Wolfenstein 3D 같은 레전드 게임들이 전부 C언어로 만들어졌음. 존 카맥(John Carmack)이 C로 짠 코드들이 지금도 오픈소스로 공개돼 있는데, 진짜 예술임 🎨
현대 게임 엔진들도 코어 부분은 C/C++로 작성되어 있고, 그래픽 드라이버 자체도 C로 구현됨.

🔍 C언어 그래픽 프로그래밍의 역사적 흐름

시대 기술 특징
1980년대 직접 메모리 접근 VGA 버퍼(0xA0000)에 직접 픽셀 쓰기
1990년대 Mode 13h, VESA 256색 팔레트, 320×200 해상도
2000년대 OpenGL 1.x~2.x 고정 파이프라인, 하드웨어 가속
2010년대 OpenGL 3.x~4.x, SDL2 프로그래머블 셰이더, 크로스플랫폼
현재 Vulkan, OpenGL 4.6, SDL3 저수준 GPU 제어, 멀티스레드 렌더링
💡 팁! C언어 그래픽 프로그래밍을 배우면 나중에 Unity나 Unreal Engine 쓸 때 "아 이게 내부적으로 이렇게 돌아가는 거구나" 하는 깨달음이 옴. 진짜 레벨업 느낌 ㄹㅇ

🎮 2. SDL2 완전 정복 — Simple DirectMedia Layer의 모든 것

SDL(Simple DirectMedia Layer)은 Sam Lantinga가 1998년에 만든 크로스플랫폼 멀티미디어 라이브러리야.
현재 버전은 SDL2(2013년 출시)이고, SDL3도 나왔지만 SDL2가 아직 업계 표준으로 많이 쓰임.
Steam 게임의 상당수가 SDL2를 사용하고, Valve 자체도 SDL 개발에 기여하고 있음 👍

🏗️ SDL2가 제공하는 기능들

윈도우 관리 OpenGL 컨텍스트 키보드/마우스 입력 오디오 재생 파일 I/O 타이머 스레드 네트워크(SDL_net) 이미지 로딩(SDL_image) 폰트(SDL_ttf)

📦 SDL2 설치 및 환경 설정

환경 설정이 제일 귀찮은 부분인데 ㅋㅋ 차근차근 해보자.

🪟 Windows (MinGW/GCC 기준)

터미널
# MSYS2 사용 시
pacman -S mingw-w64-x86_64-SDL2
pacman -S mingw-w64-x86_64-SDL2_image
pacman -S mingw-w64-x86_64-SDL2_ttf

🐧 Linux (Ubuntu/Debian)

터미널
sudo apt-get install libsdl2-dev
sudo apt-get install libsdl2-image-dev
sudo apt-get install libsdl2-ttf-dev

🍎 macOS (Homebrew)

터미널
brew install sdl2
brew install sdl2_image
brew install sdl2_ttf

🖥️ SDL2 기본 윈도우 생성 — Hello, SDL!

자 이제 진짜 코드 보자. SDL2로 윈도우 띄우는 가장 기본적인 코드임:

main.c
#include <SDL2/SDL.h>
#include <stdio.h>

int main(int argc, char* argv[]) {
    // SDL 초기화 — 비디오 서브시스템 활성화
    if (SDL_Init(SDL_INIT_VIDEO) != 0) {
        printf("SDL 초기화 실패: %s\n", SDL_GetError());
        return 1;
    }

    // 윈도우 생성
    SDL_Window* window = SDL_CreateWindow(
        "나의 첫 SDL 게임",          // 제목
        SDL_WINDOWPOS_CENTERED,      // X 위치 (중앙)
        SDL_WINDOWPOS_CENTERED,      // Y 위치 (중앙)
        800,                         // 너비
        600,                         // 높이
        SDL_WINDOW_SHOWN             // 플래그
    );

    if (!window) {
        printf("윈도우 생성 실패: %s\n", SDL_GetError());
        SDL_Quit();
        return 1;
    }

    // 렌더러 생성 (하드웨어 가속)
    SDL_Renderer* renderer = SDL_CreateRenderer(
        window, -1, SDL_RENDERER_ACCELERATED
    );

    // 게임 루프
    int running = 1;
    SDL_Event event;

    while (running) {
        // 이벤트 처리
        while (SDL_PollEvent(&event)) {
            if (event.type == SDL_QUIT) {
                running = 0;
            }
        }

        // 화면 지우기 (검정색)
        SDL_SetRenderDrawColor(renderer, 30, 30, 46, 255);
        SDL_RenderClear(renderer);

        // 빨간 사각형 그리기
        SDL_SetRenderDrawColor(renderer, 255, 100, 100, 255);
        SDL_Rect rect = {300, 200, 200, 150};
        SDL_RenderFillRect(renderer, &rect);

        // 화면 업데이트
        SDL_RenderPresent(renderer);
    }

    // 정리
    SDL_DestroyRenderer(renderer);
    SDL_DestroyWindow(window);
    SDL_Quit();
    return 0;
}
💡 컴파일 명령어
gcc main.c -o game -lSDL2 -lm
Windows MinGW:
gcc main.c -o game.exe -lmingw32 -lSDL2main -lSDL2

🔄 SDL2 게임 루프의 구조

게임 루프(Game Loop)는 모든 게임의 심장임. 이 구조를 이해하는 게 진짜 중요해:

SDL2 게임 루프 구조 초기화 SDL_Init / 윈도우 생성 이벤트 처리 SDL_PollEvent / 키보드 · 마우스 게임 로직 업데이트 물리 · 충돌 · AI · 상태 갱신 렌더링 SDL_RenderClear / 그리기 / Present 루프 반복 종료 / 정리 SDL_QUIT ⏱️ 프레임 제어 SDL_GetTicks() SDL_Delay() 목표: 60 FPS

⏱️ 고정 타임스텝(Fixed Timestep) 게임 루프

단순 루프는 컴퓨터 성능에 따라 게임 속도가 달라지는 문제가 있음. 그래서 고정 타임스텝 방식을 써야 함:

game_loop.c
#define TARGET_FPS 60
#define FRAME_TIME (1000 / TARGET_FPS)  // 약 16ms

Uint32 frame_start, frame_time;

while (running) {
    frame_start = SDL_GetTicks();

    // 이벤트 처리
    handle_events();

    // 게임 로직 업데이트
    update_game();

    // 렌더링
    render();

    // 프레임 속도 제한
    frame_time = SDL_GetTicks() - frame_start;
    if (frame_time < FRAME_TIME) {
        SDL_Delay(FRAME_TIME - frame_time);
    }
}
🎯 델타 타임(Delta Time) 방식

더 정교한 방법은 델타 타임을 사용하는 것임.
float delta = (current_time - last_time) / 1000.0f;

이렇게 하면 position.x += speed * delta; 처럼 시간 기반 이동이 가능해서
어떤 FPS에서도 동일한 게임 속도를 유지할 수 있음 👌

🔺 3. OpenGL 완전 정복 — 3D 그래픽의 세계로

OpenGL(Open Graphics Library)은 1992년 Silicon Graphics(SGI)가 개발한 크로스플랫폼 그래픽 API야.
현재는 Khronos Group이 관리하고 있고, 최신 버전은 OpenGL 4.6(2017년 출시)임.
모바일에서는 OpenGL ES, 웹에서는 WebGL이 OpenGL 기반으로 파생됨 🌐

📊 OpenGL 버전별 주요 특징

OpenGL 1.x — 고정 파이프라인. glBegin/glEnd 방식. 레거시
OpenGL 2.x — GLSL 셰이더 도입. 프로그래머블 파이프라인 시작
OpenGL 3.x — VAO/VBO 필수화. 고정 파이프라인 deprecated
OpenGL 4.x — 테셀레이션, 컴퓨트 셰이더, 간접 렌더링 등 고급 기능

🔗 SDL2 + OpenGL 컨텍스트 설정

SDL2와 OpenGL을 연결하는 게 핵심임. 이렇게 하면 됨:

sdl_opengl_init.c
#include <SDL2/SDL.h>
#include <GL/glew.h>   // GLEW: OpenGL 확장 로더
#include <stdio.h>

int main(int argc, char* argv[]) {
    SDL_Init(SDL_INIT_VIDEO);

    // OpenGL 버전 설정 (3.3 Core Profile)
    SDL_GL_SetAttribute(SDL_GL_CONTEXT_MAJOR_VERSION, 3);
    SDL_GL_SetAttribute(SDL_GL_CONTEXT_MINOR_VERSION, 3);
    SDL_GL_SetAttribute(SDL_GL_CONTEXT_PROFILE_MASK,
                        SDL_GL_CONTEXT_PROFILE_CORE);

    // 더블 버퍼링 활성화
    SDL_GL_SetAttribute(SDL_GL_DOUBLEBUFFER, 1);
    // 깊이 버퍼 24비트
    SDL_GL_SetAttribute(SDL_GL_DEPTH_SIZE, 24);

    // OpenGL 플래그로 윈도우 생성
    SDL_Window* window = SDL_CreateWindow(
        "SDL2 + OpenGL",
        SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED,
        800, 600,
        SDL_WINDOW_OPENGL | SDL_WINDOW_SHOWN
    );

    // OpenGL 컨텍스트 생성
    SDL_GLContext gl_context = SDL_GL_CreateContext(window);

    // GLEW 초기화 (OpenGL 함수 포인터 로드)
    glewExperimental = GL_TRUE;
    GLenum glew_err = glewInit();
    if (glew_err != GLEW_OK) {
        printf("GLEW 초기화 실패: %s\n", glewGetErrorString(glew_err));
        return 1;
    }

    // VSync 활성화 (수직 동기화)
    SDL_GL_SetSwapInterval(1);

    printf("OpenGL 버전: %s\n", glGetString(GL_VERSION));
    printf("GLSL 버전: %s\n", glGetString(GL_SHADING_LANGUAGE_VERSION));

    // ... 게임 루프 ...

    SDL_GL_DeleteContext(gl_context);
    SDL_DestroyWindow(window);
    SDL_Quit();
    return 0;
}

🎨 OpenGL 렌더링 파이프라인 이해하기

OpenGL 렌더링 파이프라인 ① 버텍스 데이터 VBO / VAO ② 버텍스 셰이더 위치 변환 (GLSL) ③ 프리미티브 어셈블리 삼각형 구성 ④ 지오메트리 셰이더 선택적 / 프리미티브 생성 ⑤ 래스터화 3D → 2D 픽셀 변환 ⑥ 프래그먼트 셰이더 색상 · 텍스처 · 조명 계산 ⑦ 테스트 & 블렌딩 깊이 · 스텐실 · 알파 ⑧ 프레임버퍼 최종 픽셀 저장 ⑨ 화면 출력 SDL_GL_SwapWindow() 🖊️ 프로그래머블 단계 (GLSL 셰이더 작성) 버텍스 셰이더 · 지오메트리 셰이더 · 프래그먼트 셰이더 ⚙️ 고정 기능 단계 (GPU 자동 처리) 프리미티브 어셈블리 · 래스터화 · 테스트 * OpenGL 3.3 Core Profile 기준 파이프라인

✍️ GLSL 셰이더 작성하기

셰이더는 GPU에서 실행되는 작은 프로그램임. GLSL(OpenGL Shading Language)로 작성해:
C언어랑 문법이 비슷해서 금방 익힐 수 있음 ㅋㅋ

버텍스 셰이더 (vertex_shader.glsl)

GLSL
#version 330 core

// 입력: 버텍스 속성
layout (location = 0) in vec3 aPos;      // 위치
layout (location = 1) in vec3 aColor;    // 색상
layout (location = 2) in vec2 aTexCoord; // 텍스처 좌표

// 출력: 프래그먼트 셰이더로 전달
out vec3 ourColor;
out vec2 TexCoord;

// 유니폼: CPU에서 전달하는 변환 행렬
uniform mat4 model;       // 모델 변환
uniform mat4 view;        // 뷰 변환 (카메라)
uniform mat4 projection;  // 투영 변환

void main() {
    // MVP 변환: 3D 좌표 → 클립 공간
    gl_Position = projection * view * model * vec4(aPos, 1.0);
    ourColor = aColor;
    TexCoord = aTexCoord;
}

프래그먼트 셰이더 (fragment_shader.glsl)

GLSL
#version 330 core

in vec3 ourColor;
in vec2 TexCoord;

out vec4 FragColor;  // 최종 픽셀 색상

uniform sampler2D texture1;  // 텍스처 샘플러
uniform float time;          // 시간 (애니메이션용)

void main() {
    // 텍스처 색상과 버텍스 색상 혼합
    vec4 texColor = texture(texture1, TexCoord);

    // 시간에 따른 색상 변화 효과
    float pulse = (sin(time * 2.0) + 1.0) * 0.5;

    FragColor = texColor * vec4(ourColor, 1.0) * vec4(pulse, 1.0, 1.0, 1.0);
}

📐 VBO, VAO, EBO — 버텍스 데이터 관리

OpenGL 3.3+에서는 이 세 가지가 핵심임. 헷갈리는 사람 많은데 정리해줄게:

개념 풀네임 역할
VBO Vertex Buffer Object 버텍스 데이터(위치, 색상, UV)를 GPU 메모리에 저장
VAO Vertex Array Object VBO의 데이터 레이아웃 정보를 기억하는 컨테이너
EBO Element Buffer Object 인덱스 배열 저장. 버텍스 재사용으로 메모리 절약
vao_vbo_setup.c
// 삼각형 버텍스 데이터
float vertices[] = {
    // 위치(x,y,z)      색상(r,g,b)
    -0.5f, -0.5f, 0.0f,  1.0f, 0.0f, 0.0f,  // 왼쪽 아래 (빨강)
     0.5f, -0.5f, 0.0f,  0.0f, 1.0f, 0.0f,  // 오른쪽 아래 (초록)
     0.0f,  0.5f, 0.0f,  0.0f, 0.0f, 1.0f   // 위 (파랑)
};

unsigned int VAO, VBO;

// VAO 생성 및 바인딩
glGenVertexArrays(1, &VAO);
glBindVertexArray(VAO);

// VBO 생성 및 데이터 업로드
glGenBuffers(1, &VBO);
glBindBuffer(GL_ARRAY_BUFFER, VBO);
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);

// 버텍스 속성 설정
// 위치 속성 (location = 0)
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE,
                      6 * sizeof(float), (void*)0);
glEnableVertexAttribArray(0);

// 색상 속성 (location = 1)
glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE,
                      6 * sizeof(float), (void*)(3 * sizeof(float)));
glEnableVertexAttribArray(1);

// 렌더링 시
glBindVertexArray(VAO);
glDrawArrays(GL_TRIANGLES, 0, 3);
⚠️ 주의! OpenGL Core Profile에서는 VAO 없이 렌더링하면 에러남. 반드시 VAO를 먼저 바인딩하고 VBO 설정해야 함. 이거 모르고 삽질하는 사람 진짜 많음 ㅋㅋㅋ

🗺️ 4. 수학 기초 — 게임 그래픽의 뼈대

게임 그래픽 프로그래밍에서 수학은 피할 수 없음. 근데 겁먹지 마 ㅋㅋ 필요한 것만 알면 됨 💪
특히 선형대수(벡터, 행렬)가 핵심이야.

📊 벡터(Vector) 연산

vector_math.c
#include <math.h>

typedef struct {
    float x, y, z;
} Vec3;

// 벡터 덧셈
Vec3 vec3_add(Vec3 a, Vec3 b) {
    return (Vec3){a.x + b.x, a.y + b.y, a.z + b.z};
}

// 벡터 스칼라 곱
Vec3 vec3_scale(Vec3 v, float s) {
    return (Vec3){v.x * s, v.y * s, v.z * s};
}

// 내적(Dot Product) — 각도 계산에 사용
float vec3_dot(Vec3 a, Vec3 b) {
    return a.x*b.x + a.y*b.y + a.z*b.z;
}

// 외적(Cross Product) — 법선 벡터 계산에 사용
Vec3 vec3_cross(Vec3 a, Vec3 b) {
    return (Vec3){
        a.y*b.z - a.z*b.y,
        a.z*b.x - a.x*b.z,
        a.x*b.y - a.y*b.x
    };
}

// 정규화(Normalize) — 단위 벡터로 변환
Vec3 vec3_normalize(Vec3 v) {
    float len = sqrtf(v.x*v.x + v.y*v.y + v.z*v.z);
    if (len == 0.0f) return (Vec3){0, 0, 0};
    return (Vec3){v.x/len, v.y/len, v.z/len};
}

🔢 4x4 변환 행렬 — MVP 변환

3D 그래픽에서 MVP(Model-View-Projection) 변환은 필수 개념임:

🎯 MVP 변환 이해하기

Model 행렬 — 오브젝트의 위치, 회전, 크기 변환 (로컬 → 월드 공간)
View 행렬 — 카메라 시점 변환 (월드 → 카메라 공간)
Projection 행렬 — 원근감 적용 (카메라 → 클립 공간)

gl_Position = Projection × View × Model × vertex_position
matrix.c
typedef float Mat4[16];  // 4x4 행렬 (열 우선 순서)

// 단위 행렬
void mat4_identity(Mat4 m) {
    memset(m, 0, sizeof(Mat4));
    m[0] = m[5] = m[10] = m[15] = 1.0f;
}

// 투영 행렬 (원근 투영)
void mat4_perspective(Mat4 m, float fov, float aspect,
                      float near, float far) {
    float f = 1.0f / tanf(fov * 0.5f * M_PI / 180.0f);
    memset(m, 0, sizeof(Mat4));
    m[0]  = f / aspect;
    m[5]  = f;
    m[10] = (far + near) / (near - far);
    m[11] = -1.0f;
    m[14] = (2.0f * far * near) / (near - far);
}

// 이동 행렬
void mat4_translate(Mat4 m, float tx, float ty, float tz) {
    mat4_identity(m);
    m[12] = tx;
    m[13] = ty;
    m[14] = tz;
}

// Y축 회전 행렬
void mat4_rotate_y(Mat4 m, float angle) {
    float c = cosf(angle);
    float s = sinf(angle);
    mat4_identity(m);
    m[0]  =  c;
    m[2]  =  s;
    m[8]  = -s;
    m[10] =  c;
}
💡 팁! 행렬 수학이 복잡하면 cglm이나 linmath.h 같은 C용 수학 라이브러리를 쓰는 것도 좋은 방법임. 특히 linmath.h는 헤더 파일 하나로 끝나서 편함 👍

🖼️ 5. 텍스처 로딩과 적용 — 게임을 예쁘게 만들기

텍스처 없는 게임은 맛없는 라면 같은 거임 ㅋㅋ 텍스처 로딩부터 OpenGL에 적용하는 것까지 알아보자.

📸 SDL_image로 이미지 로딩

texture_loader.c
#include <SDL2/SDL.h>
#include <SDL2/SDL_image.h>
#include <GL/glew.h>

// 텍스처 로드 함수
unsigned int load_texture(const char* filepath) {
    // SDL_image로 이미지 로드
    SDL_Surface* surface = IMG_Load(filepath);
    if (!surface) {
        printf("이미지 로드 실패: %s\n", IMG_GetError());
        return 0;
    }

    // RGBA 포맷으로 변환
    SDL_Surface* converted = SDL_ConvertSurfaceFormat(
        surface, SDL_PIXELFORMAT_RGBA32, 0
    );
    SDL_FreeSurface(surface);

    // OpenGL 텍스처 생성
    unsigned int texture_id;
    glGenTextures(1, &texture_id);
    glBindTexture(GL_TEXTURE_2D, texture_id);

    // 텍스처 파라미터 설정
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER,
                    GL_LINEAR_MIPMAP_LINEAR);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);

    // 픽셀 데이터 업로드
    glTexImage2D(
        GL_TEXTURE_2D, 0, GL_RGBA,
        converted->w, converted->h, 0,
        GL_RGBA, GL_UNSIGNED_BYTE, converted->pixels
    );

    // 밉맵 자동 생성 (성능 최적화)
    glGenerateMipmap(GL_TEXTURE_2D);

    SDL_FreeSurface(converted);
    return texture_id;
}
🔍 텍스처 필터링 옵션 설명

GL_NEAREST — 가장 가까운 픽셀 선택. 픽셀아트 게임에 적합
GL_LINEAR — 주변 픽셀 보간. 부드러운 텍스처
GL_LINEAR_MIPMAP_LINEAR — 밉맵 + 선형 보간. 원거리 텍스처 품질 향상

밉맵(Mipmap)이란? 텍스처를 여러 해상도로 미리 저장해두는 기법.
멀리 있는 오브젝트에는 작은 텍스처를 써서 성능 향상 + 앨리어싱 방지 🎯

🎯 6. 실전 게임 개발 — 2D 슈팅 게임 만들기

이제 진짜 게임을 만들어보자! SDL2 렌더러(OpenGL 없이)로 간단한 2D 슈팅 게임 구조를 짜볼 거임.
이 구조를 이해하면 어떤 게임이든 만들 수 있음 💪

🏗️ 게임 아키텍처 설계

2D 슈팅 게임 아키텍처 Game 메인 컨트롤러 Player 이동 · 발사 · HP Enemy AI · 패턴 · 스폰 Bullet 위치 · 속도 · 충돌 Renderer SDL 렌더링 처리 Input 키보드 · 마우스 Collision AABB · 원형 충돌 Score / UI 점수 · 레벨 · HUD

🚀 플레이어 구조체와 이동 구현

player.h
#ifndef PLAYER_H
#define PLAYER_H

#include <SDL2/SDL.h>

#define PLAYER_SPEED 300.0f   // 픽셀/초
#define PLAYER_WIDTH  48
#define PLAYER_HEIGHT 48
#define MAX_BULLETS   50

typedef struct {
    float x, y;          // 위치
    float vx, vy;        // 속도
    int hp;              // 체력
    int score;           // 점수
    SDL_Texture* sprite; // 스프라이트 텍스처
    float shoot_cooldown; // 발사 쿨다운
} Player;

void player_init(Player* p, float x, float y);
void player_update(Player* p, float delta);
void player_render(Player* p, SDL_Renderer* renderer);
void player_shoot(Player* p);

#endif
player.c
#include "player.h"
#include <SDL2/SDL.h>

void player_init(Player* p, float x, float y) {
    p->x = x;
    p->y = y;
    p->vx = 0;
    p->vy = 0;
    p->hp = 3;
    p->score = 0;
    p->shoot_cooldown = 0.0f;
}

void player_update(Player* p, float delta) {
    const Uint8* keys = SDL_GetKeyboardState(NULL);

    p->vx = 0;
    p->vy = 0;

    if (keys[SDL_SCANCODE_LEFT]  || keys[SDL_SCANCODE_A])
        p->vx = -PLAYER_SPEED;
    if (keys[SDL_SCANCODE_RIGHT] || keys[SDL_SCANCODE_D])
        p->vx =  PLAYER_SPEED;
    if (keys[SDL_SCANCODE_UP]    || keys[SDL_SCANCODE_W])
        p->vy = -PLAYER_SPEED;
    if (keys[SDL_SCANCODE_DOWN]  || keys[SDL_SCANCODE_S])
        p->vy =  PLAYER_SPEED;

    // 대각선 이동 시 속도 정규화
    if (p->vx != 0 && p->vy != 0) {
        p->vx *= 0.7071f;  // 1/sqrt(2)
        p->vy *= 0.7071f;
    }

    p->x += p->vx * delta;
    p->y += p->vy * delta;

    // 화면 경계 처리
    if (p->x < 0) p->x = 0;
    if (p->x > 800 - PLAYER_WIDTH) p->x = 800 - PLAYER_WIDTH;
    if (p->y < 0) p->y = 0;
    if (p->y > 600 - PLAYER_HEIGHT) p->y = 600 - PLAYER_HEIGHT;

    // 발사 쿨다운 감소
    if (p->shoot_cooldown > 0)
        p->shoot_cooldown -= delta;
}

💥 충돌 감지 — AABB 방식

AABB(Axis-Aligned Bounding Box)는 가장 기본적이고 빠른 충돌 감지 방법임:

collision.c
typedef struct {
    float x, y, w, h;
} AABB;

// AABB 충돌 감지
int aabb_check(AABB a, AABB b) {
    return (a.x < b.x + b.w &&
            a.x + a.w > b.x &&
            a.y < b.y + b.h &&
            a.y + a.h > b.y);
}

// 원형 충돌 감지 (더 정확한 경우 사용)
int circle_check(float x1, float y1, float r1,
                 float x2, float y2, float r2) {
    float dx = x2 - x1;
    float dy = y2 - y1;
    float dist_sq = dx*dx + dy*dy;
    float r_sum = r1 + r2;
    return dist_sq <= r_sum * r_sum;
}

// 총알-적 충돌 처리 예시
void check_bullet_enemy_collision(Bullet* bullets, int bullet_count,
                                   Enemy* enemies, int enemy_count,
                                   int* score) {
    for (int i = 0; i < bullet_count; i++) {
        if (!bullets[i].active) continue;
        for (int j = 0; j < enemy_count; j++) {
            if (!enemies[j].active) continue;

            AABB b_box = {bullets[i].x, bullets[i].y, 8, 8};
            AABB e_box = {enemies[j].x, enemies[j].y,
                          enemies[j].w, enemies[j].h};

            if (aabb_check(b_box, e_box)) {
                bullets[i].active = 0;
                enemies[j].hp--;
                if (enemies[j].hp <= 0) {
                    enemies[j].active = 0;
                    *score += 100;
                }
            }
        }
    }
}
💡 성능 팁! 오브젝트가 많아지면 충돌 감지가 O(n²)이 되어 느려짐.
이럴 때는 공간 분할(Spatial Partitioning) 기법인 쿼드트리(Quadtree)나 그리드(Grid) 방식을 사용하면 성능을 크게 향상시킬 수 있음 🚀

🔊 7. SDL_mixer로 사운드 추가하기

게임에 소리 없으면 반쪽짜리임 ㅋㅋ SDL_mixer로 BGM이랑 효과음 넣는 방법 알아보자:

audio.c
#include <SDL2/SDL_mixer.h>

// 오디오 초기화
int audio_init() {
    if (Mix_OpenAudio(44100, MIX_DEFAULT_FORMAT, 2, 2048) < 0) {
        printf("SDL_mixer 초기화 실패: %s\n", Mix_GetError());
        return 0;
    }
    // 채널 수 설정 (동시 재생 가능한 효과음 수)
    Mix_AllocateChannels(16);
    return 1;
}

// BGM 재생 (무한 반복)
Mix_Music* bgm = Mix_LoadMUS("assets/bgm.ogg");
Mix_PlayMusic(bgm, -1);  // -1 = 무한 반복
Mix_VolumeMusic(64);     // 볼륨 0~128

// 효과음 재생
Mix_Chunk* shoot_sfx = Mix_LoadWAV("assets/shoot.wav");
Mix_PlayChannel(-1, shoot_sfx, 0);  // -1 = 자동 채널 선택

// 정리
Mix_FreeChunk(shoot_sfx);
Mix_FreeMusic(bgm);
Mix_CloseAudio();
🎵 지원 오디오 포맷

WAV MP3 OGG FLAC MOD MIDI

게임 효과음은 WAV (낮은 레이턴시), BGM은 OGG (작은 파일 크기) 추천!

⚡ 8. 성능 최적화 — 빠른 게임 만들기

게임이 버벅이면 아무리 재밌어도 망함 ㅋㅋ C언어 게임 개발에서 성능 최적화 핵심 포인트들을 정리해줄게:

🏎️ 렌더링 최적화

① 스프라이트 시트(Sprite Sheet) 사용
여러 이미지를 하나의 텍스처에 모아서 텍스처 바인딩 횟수를 줄임.
SDL_RenderCopy의 srcRect 파라미터로 특정 영역만 렌더링 가능.

② 배치 렌더링(Batch Rendering)
같은 텍스처를 쓰는 오브젝트들을 한 번에 그리기.
SDL2에서는 SDL_RenderCopyEx 대신 SDL_RenderGeometry 활용.

③ 더티 렉탱글(Dirty Rectangle)
변경된 영역만 다시 그리는 기법. 정적 배경이 많은 게임에 효과적.

④ 오프스크린 렌더링
SDL_Texture를 렌더 타겟으로 사용해서 복잡한 장면을 미리 렌더링.

💾 메모리 최적화

memory_pool.c
// 오브젝트 풀(Object Pool) 패턴
// 동적 할당/해제 반복을 피하기 위해 미리 메모리 할당

#define MAX_BULLETS 200

typedef struct {
    float x, y;
    float vx, vy;
    int active;
} Bullet;

// 전역 풀 — 한 번만 할당
Bullet bullet_pool[MAX_BULLETS];
int bullet_pool_size = MAX_BULLETS;

// 비활성 총알 재사용
Bullet* bullet_spawn(float x, float y, float vx, float vy) {
    for (int i = 0; i < MAX_BULLETS; i++) {
        if (!bullet_pool[i].active) {
            bullet_pool[i].x = x;
            bullet_pool[i].y = y;
            bullet_pool[i].vx = vx;
            bullet_pool[i].vy = vy;
            bullet_pool[i].active = 1;
            return &bullet_pool[i];
        }
    }
    return NULL;  // 풀이 가득 찬 경우
}

// 총알 비활성화 (메모리 해제 없음)
void bullet_destroy(Bullet* b) {
    b->active = 0;
}
최적화 기법 효과 적용 난이도
오브젝트 풀 malloc/free 제거, 캐시 효율 향상 ⭐⭐
스프라이트 시트 텍스처 바인딩 횟수 감소 ⭐⭐
공간 분할 충돌 감지 O(n²) → O(n log n) ⭐⭐⭐
LOD (Level of Detail) 원거리 오브젝트 단순화 ⭐⭐⭐
프러스텀 컬링 화면 밖 오브젝트 렌더링 스킵 ⭐⭐⭐⭐
⚠️ C언어 메모리 관리 주의사항!
게임 루프 안에서 malloc/free를 반복 호출하면 메모리 단편화(fragmentation)가 발생해서 성능이 저하됨.
오브젝트 풀이나 아레나 할당자(Arena Allocator)를 사용하는 것이 좋음.

🌟 9. 고급 OpenGL 기법 — 빛과 그림자

기본을 다 익혔다면 이제 고급 기법으로 넘어가자. 게임을 진짜 예쁘게 만드는 기술들임 ✨

💡 퐁 조명 모델(Phong Lighting)

3D 게임에서 조명은 현실감의 핵심임. 퐁 조명 모델은 세 가지 성분으로 구성됨:

🔆 퐁 조명 3요소

Ambient(주변광) — 모든 방향에서 균일하게 오는 빛. 그림자 영역도 완전히 검게 되지 않도록
Diffuse(확산광) — 빛의 방향과 표면 법선 벡터의 각도에 따른 밝기. 가장 중요한 성분
Specular(반사광) — 거울 반사. 광택 효과. 시점에 따라 달라짐
phong_fragment.glsl
#version 330 core

in vec3 FragPos;    // 프래그먼트 월드 위치
in vec3 Normal;     // 법선 벡터
in vec2 TexCoord;

out vec4 FragColor;

uniform vec3 lightPos;    // 광원 위치
uniform vec3 viewPos;     // 카메라 위치
uniform vec3 lightColor;  // 광원 색상
uniform sampler2D texture1;

void main() {
    vec3 texColor = texture(texture1, TexCoord).rgb;

    // 1. Ambient (주변광)
    float ambientStrength = 0.15;
    vec3 ambient = ambientStrength * lightColor;

    // 2. Diffuse (확산광)
    vec3 norm = normalize(Normal);
    vec3 lightDir = normalize(lightPos - FragPos);
    float diff = max(dot(norm, lightDir), 0.0);
    vec3 diffuse = diff * lightColor;

    // 3. Specular (반사광)
    float specularStrength = 0.5;
    vec3 viewDir = normalize(viewPos - FragPos);
    vec3 reflectDir = reflect(-lightDir, norm);
    float spec = pow(max(dot(viewDir, reflectDir), 0.0), 32.0);
    vec3 specular = specularStrength * spec * lightColor;

    // 최종 색상 = (주변광 + 확산광 + 반사광) × 텍스처 색상
    vec3 result = (ambient + diffuse + specular) * texColor;
    FragColor = vec4(result, 1.0);
}

🌑 그림자 매핑(Shadow Mapping)

그림자는 두 패스(Pass)로 렌더링함:

1
깊이 패스 — 광원 시점에서 씬을 렌더링해서 깊이 맵(Depth Map) 생성. 프레임버퍼 오브젝트(FBO) 사용
2
렌더링 패스 — 카메라 시점에서 렌더링할 때, 각 프래그먼트가 깊이 맵에서 광원에 보이는지 확인. 보이지 않으면 그림자 처리
💡 그림자 매핑 팁!
Shadow Acne(그림자 노이즈) 문제는 Shadow Bias로 해결.
float bias = max(0.05 * (1.0 - dot(normal, lightDir)), 0.005);

PCF(Percentage Closer Filtering)로 그림자 경계를 부드럽게 만들 수 있음 🎨

🛠️ 10. 개발 도구와 디버깅

C언어 게임 개발에서 디버깅은 진짜 중요함. 좋은 도구 알아두면 개발 속도가 2배는 빨라짐 ㅋㅋ

🔍 필수 개발 도구

도구 용도 플랫폼
GDB C/C++ 디버거. 브레이크포인트, 스택 추적 Linux/macOS/Windows
Valgrind 메모리 누수 감지. malloc/free 오류 탐지 Linux/macOS
RenderDoc OpenGL/Vulkan 프레임 디버거. GPU 상태 분석 Windows/Linux
gprof 성능 프로파일링. 병목 지점 찾기 Linux/macOS
AddressSanitizer 메모리 오류 런타임 감지 (GCC/Clang 내장) Linux/macOS/Windows
컴파일 플래그
# 디버그 빌드 (최적화 없음, 디버그 심볼 포함)
gcc -g -O0 -Wall -Wextra -fsanitize=address main.c -o game_debug -lSDL2 -lGL -lGLEW -lm

# 릴리즈 빌드 (최적화 최대)
gcc -O3 -DNDEBUG main.c -o game -lSDL2 -lGL -lGLEW -lm

📊 OpenGL 에러 처리

gl_debug.c
// OpenGL 에러 체크 매크로
#define GL_CHECK(stmt) do { \
    stmt; \
    GLenum err = glGetError(); \
    if (err != GL_NO_ERROR) { \
        printf("OpenGL 에러 0x%x at %s:%d\n", err, __FILE__, __LINE__); \
    } \
} while(0)

// 사용 예시
GL_CHECK(glBindTexture(GL_TEXTURE_2D, texture_id));
GL_CHECK(glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, w, h, 0,
                      GL_RGBA, GL_UNSIGNED_BYTE, pixels));

// OpenGL 4.3+ 디버그 콜백 (더 강력한 방법)
void GLAPIENTRY gl_debug_callback(GLenum source, GLenum type,
    GLuint id, GLenum severity, GLsizei length,
    const GLchar* message, const void* userParam) {
    if (severity != GL_DEBUG_SEVERITY_NOTIFICATION) {
        printf("[OpenGL 디버그] %s\n", message);
    }
}

// 초기화 시 등록
glEnable(GL_DEBUG_OUTPUT);
glDebugMessageCallback(gl_debug_callback, NULL);

📚 11. 실전 프로젝트 구조 — 제대로 된 게임 만들기

이제 진짜 프로젝트 구조를 잡아보자. 코드가 커질수록 구조가 중요해짐 💡
그리고 이런 기술들을 배우고 싶은데 혼자 하기 어렵다면, 재능넷에서 C언어 게임 개발 전문가를 찾아 1:1 멘토링을 받는 것도 좋은 방법임 🎯

📁 프로젝트 디렉토리 구조

프로젝트 구조
my_game/
├── src/
│   ├── main.c          # 진입점, 게임 루프
│   ├── game.c/h        # 게임 상태 관리
│   ├── player.c/h      # 플레이어 로직
│   ├── enemy.c/h       # 적 AI
│   ├── bullet.c/h      # 총알 시스템
│   ├── renderer.c/h    # 렌더링 추상화
│   ├── input.c/h       # 입력 처리
│   ├── audio.c/h       # 사운드 시스템
│   ├── collision.c/h   # 충돌 감지
│   ├── math/
│   │   ├── vec2.c/h    # 2D 벡터
│   │   ├── vec3.c/h    # 3D 벡터
│   │   └── mat4.c/h    # 4x4 행렬
│   └── utils/
│       ├── pool.c/h    # 오브젝트 풀
│       └── log.c/h     # 로깅 시스템
├── assets/
│   ├── sprites/        # 스프라이트 이미지
│   ├── sounds/         # 효과음/BGM
│   ├── fonts/          # 폰트 파일
│   └── shaders/        # GLSL 셰이더 파일
├── include/            # 외부 헤더
├── lib/                # 외부 라이브러리
├── Makefile            # 빌드 스크립트
└── README.md

🔧 Makefile 작성

Makefile
CC = gcc
CFLAGS = -Wall -Wextra -std=c11 -I./include
LDFLAGS = -lSDL2 -lSDL2_image -lSDL2_ttf -lSDL2_mixer -lGL -lGLEW -lm

# 소스 파일 자동 수집
SRCS = $(wildcard src/*.c) $(wildcard src/math/*.c) $(wildcard src/utils/*.c)
OBJS = $(SRCS:.c=.o)

TARGET = my_game

# 기본 빌드
all: $(TARGET)

$(TARGET): $(OBJS)
	$(CC) $(OBJS) -o $(TARGET) $(LDFLAGS)

%.o: %.c
	$(CC) $(CFLAGS) -c $< -o $@

# 디버그 빌드
debug: CFLAGS += -g -O0 -DDEBUG -fsanitize=address
debug: LDFLAGS += -fsanitize=address
debug: $(TARGET)

# 릴리즈 빌드
release: CFLAGS += -O3 -DNDEBUG
release: $(TARGET)

# 정리
clean:
	rm -f $(OBJS) $(TARGET)

.PHONY: all debug release clean

🎮 게임 상태 머신(State Machine)

게임에는 여러 상태가 있음. 메인 메뉴, 게임 플레이, 일시정지, 게임 오버 등.
상태 머신으로 깔끔하게 관리하자:

game_state.c
typedef enum {
    STATE_MAIN_MENU,
    STATE_PLAYING,
    STATE_PAUSED,
    STATE_GAME_OVER,
    STATE_QUIT
} GameState;

typedef struct {
    GameState current;
    GameState previous;
    void (*update)(float delta);
    void (*render)(void);
    void (*on_enter)(void);
    void (*on_exit)(void);
} GameStateManager;

// 상태 전환
void state_change(GameStateManager* sm, GameState new_state) {
    if (sm->on_exit) sm->on_exit();
    sm->previous = sm->current;
    sm->current = new_state;

    // 새 상태에 맞는 함수 포인터 설정
    switch (new_state) {
        case STATE_MAIN_MENU:
            sm->update = menu_update;
            sm->render = menu_render;
            sm->on_enter = menu_enter;
            break;
        case STATE_PLAYING:
            sm->update = game_update;
            sm->render = game_render;
            sm->on_enter = game_enter;
            break;
        // ... 기타 상태들
    }

    if (sm->on_enter) sm->on_enter();
}

🚀 12. 배포와 크로스플랫폼 빌드

게임 만들었으면 배포해야지 ㅋㅋ SDL2의 가장 큰 장점이 크로스플랫폼이라는 거임!

🌍 플랫폼별 빌드 방법

🪟 Windows 배포
MinGW로 컴파일 후 SDL2.dll, SDL2_image.dll 등 DLL 파일을 exe와 같은 폴더에 포함.
NSIS나 Inno Setup으로 인스톨러 제작 가능.

🐧 Linux 배포
AppImage 형식으로 배포하면 대부분의 Linux 배포판에서 실행 가능.
Flatpak, Snap 패키지도 좋은 선택.

🍎 macOS 배포
.app 번들 형식으로 패키징. SDL2 프레임워크를 번들에 포함.
코드 서명(Code Signing) 필요.

🎮 Steam 배포

SDL2로 만든 게임은 Steam에 올리기 좋음. Valve가 SDL2를 공식 지원하기 때문임.
Steamworks SDK를 C API로 사용 가능하고, Steam Input API도 SDL2와 잘 통합됨.
실제로 Hollow Knight, Celeste, Hades 같은 인디 게임들도 SDL 기반으로 개발됨 🎯

💡 itch.io 배포 팁!
인디 게임 플랫폼 itch.io에 올리면 HTML5(Emscripten)로 웹 버전도 만들 수 있음.
SDL2는 Emscripten을 공식 지원해서 C 코드를 WebAssembly로 컴파일 가능!
emcc main.c -s USE_SDL=2 -o game.html

📖 13. 학습 로드맵과 추천 리소스

C언어 게임 개발을 처음 시작하는 사람들을 위한 학습 로드맵을 정리해줄게.
재능넷에서도 C언어 그래픽 프로그래밍 관련 강의나 멘토링 서비스를 찾을 수 있으니 참고해봐 😊

C언어 게임 개발 학습 로드맵 🌱 1단계: C언어 기초 (1~2개월) 포인터 · 구조체 · 동적 메모리 · 파일 I/O · 함수 포인터 · 전처리기 🎮 2단계: SDL2 기초 (1~2개월) 윈도우 생성 · 이벤트 처리 · 2D 렌더링 · 텍스처 · 사운드 · 게임 루프 🔺 3단계: OpenGL 기초 (2~3개월) VAO/VBO · GLSL 셰이더 · 텍스처 · 변환 행렬 · 카메라 · 조명 🎯 4단계: 게임 개발 실전 (3~6개월) 충돌 감지 · 물리 엔진 · AI · 파티클 · 사운드 · 게임 상태 관리 · 최적화 🚀 5단계: 고급 기법 (지속적 학습) 그림자 · PBR · 포스트 프로세싱 · Vulkan · 멀티스레딩 · 네트워크

📚 추천 학습 자료

📖 책
• Game Programming Patterns — Robert Nystrom (무료 온라인 공개)
• Real-Time Rendering — Akenine-Möller 외 (그래픽스 바이블)
• OpenGL Programming Guide (Red Book) — 공식 OpenGL 가이드

🌐 온라인 자료
• learnopengl.com — OpenGL 최고의 무료 튜토리얼 사이트 (한국어 번역본 있음)
• lazyfoo.net — SDL2 튜토리얼 시리즈
• github.com/id-Software — Quake, Doom 오픈소스 코드

🎥 유튜브
• The Cherno — C++ 게임 엔진 개발 시리즈
• javidx9 (OneLoneCoder) — C++ 콘솔 게임 엔진
🏆 실력 향상 프로젝트 아이디어

초급 — Pong, Snake, Tetris (SDL2 2D 렌더러)
중급 — 2D 플랫포머, 탑다운 슈팅, 퍼즐 게임 (SDL2 + 물리)
고급 — 3D 미로 게임, 간단한 FPS (OpenGL + 셰이더)
도전 — 미니 게임 엔진 제작, Wolfenstein 3D 클론

🎊 마무리 — C언어 게임 개발의 매력

여기까지 왔으면 진짜 대단한 거임 ㅋㅋ 내용이 많았는데 핵심만 정리해줄게:

✅ 핵심 정리

① C언어 — 하드웨어에 가장 가까운 언어. 메모리 직접 제어. 그래픽 API의 기반
② SDL2 — 크로스플랫폼 멀티미디어 라이브러리. 윈도우, 입력, 오디오, OpenGL 컨텍스트
③ OpenGL — 크로스플랫폼 3D 그래픽 API. GLSL 셰이더로 GPU 프로그래밍
④ 게임 루프 — 이벤트 처리 → 로직 업데이트 → 렌더링의 반복
⑤ 수학 — 벡터, 행렬, MVP 변환이 3D 그래픽의 핵심
⑥ 최적화 — 오브젝트 풀, 스프라이트 시트, 공간 분할로 성능 향상
⑦ 구조 — 상태 머신, 모듈화, 오브젝트 풀로 유지보수 가능한 코드 작성

C언어로 게임을 만드는 건 분명히 쉽지 않음. 근데 이걸 해내면 진짜 게임 개발의 본질을 이해하게 됨.
유니티나 언리얼 엔진을 쓸 때도 "아 이게 내부적으로 이렇게 돌아가는 거구나" 하는 깨달음이 생기고,
성능 최적화나 그래픽 버그 해결도 훨씬 잘 할 수 있게 됨 💪

처음엔 삼각형 하나 그리는 것도 어렵겠지만, 하나씩 쌓아가다 보면 어느새 자기만의 게임이 화면에 돌아가고 있을 거임.
그 순간의 성취감은 진짜 말로 표현 못 함 🎮✨

자, 이제 코드 에디터 열고 시작해봐! 화이팅!! 🚀🔥

🎮 💻 🔥 ✨ 🚀 🎯 💪 🌟
댓글 작성

이 글에 대한 여러분의 생각을 들려주세요

댓글 0
댓글을 불러오는 중입니다.

아직 댓글이 없습니다.