SDL2 실전 가이드 | 게임 개발 입문

이 글의 핵심

게임 엔진 없이 게임을 만들려면 좌표계, 프레임레이트, 델타 타임, Surface와 Texture 차이 같은 기초를 먼저 알아야 합니다. CMake 환경 설정에서 시작해 입력·렌더링·오디오를 하나씩 붙이고, 스프라이트 애니메이션·타일맵·파티클 시스템과 OpenGL 컨텍스트 연동까지 확장해 보는 구성입니다.

이 글의 핵심

SDL2로 창을 띄우고 입력을 받고, 렌더러로 그리고, 믹서로 소리까지 내는 흐름을 한 번에 정리합니다. 게임 엔진 없이 직접 루프를 돌릴 때 자주 마주치는 지점 위주로 설명합니다.

SDL을 쓰는 이유는 운영체제마다 전혀 다른 창 생성·입력·오디오 API를 하나로 감싸 주기 때문입니다. Windows의 Win32/DirectX, macOS의 Cocoa/Metal, Linux의 X11·Wayland를 직접 다루지 않아도 같은 코드가 여러 플랫폼에서 돌아가고, Valve의 Steam 클라이언트와 여러 상용 게임의 포팅 레이어로도 쓰일 만큼 검증된 라이브러리입니다. 반면 SDL은 “엔진”이 아니라 얇은 플랫폼 계층이라서, 씬 관리·물리·에디터 같은 것은 모두 직접 만들어야 합니다. 이 글의 예제가 길어지는 이유도 그 때문입니다. 참고로 2025년에 SDL3가 정식 출시되어 API 이름과 반환값 규칙이 일부 바뀌었지만, 게임 루프와 렌더링의 개념은 그대로이므로 SDL2로 익힌 내용을 그대로 옮겨 갈 수 있습니다.


사전 지식 (초보자를 위한 기초)

게임 개발의 기본 구조

게임은 보통 입력 → 갱신 → 그리기를 무한 루프로 반복합니다.

게임 루프:
┌─────────────────────┐
│  1. 입력 처리       │ ← 키보드, 마우스
│     (Input)         │
├─────────────────────┤
│  2. 게임 로직       │ ← 물리, AI, 충돌
│     (Update)        │
├─────────────────────┤
│  3. 화면 그리기     │ ← 렌더링
│     (Render)        │
└─────────────────────┘
        ↓
    (반복)

입력 단계에서 플레이어의 의도를 받고, 업데이트 단계에서 위치·충돌·AI를 갱신한 뒤, 렌더링 단계에서 화면에 반영합니다. 세 단계를 분리해 두는 것이 중요한 이유는, 렌더링 코드 안에서 게임 상태를 바꾸기 시작하면 프레임레이트가 바뀔 때 게임 속도까지 달라지고 버그를 재현하기 어려워지기 때문입니다.

좌표계

SDL 좌표계:

(0,0) ────────→ X (오른쪽)
  │
  │
  │
  ↓
  Y (아래)
800×600 창을 예로 들면:
- 좌측 상단: (0, 0)
- 우측 상단: (800, 0)
- 좌측 하단: (0, 600)
- 우측 하단: (800, 600)
- 중앙: (400, 300)

프레임레이트 (FPS)

FPS (Frames Per Second)는 초당 화면을 갱신하는 횟수입니다.

60 FPS:
- 1초에 60번 화면 갱신
- 1프레임 = 16.67ms
- 부드러운 움직임
30 FPS:
- 1초에 30번 화면 갱신
- 1프레임 = 33.33ms
- 일반적
15 FPS:
- 1초에 15번 화면 갱신
- 1프레임 = 66.67ms
- 끊김 현상

델타 타임 (Delta Time)

델타 타임은 직전 프레임과 이번 프레임 사이의 시간 간격입니다. 이동량을 “프레임당 몇 픽셀”이 아니라 “초당 몇 픽셀 × 경과 시간”으로 계산해야 144Hz 모니터와 60Hz 모니터에서 캐릭터가 같은 속도로 움직입니다.

// ❌ 잘못된 방법 (FPS에 따라 속도 다름)
x += 5;  // 60 FPS면 초당 300 픽셀, 30 FPS면 150 픽셀
// ✅ 올바른 방법 (FPS 무관)
x += 300 * deltaTime;  // 항상 초당 300 픽셀
// deltaTime 계산
float currentTime = SDL_GetTicks() / 1000.0f;
float deltaTime = currentTime - lastTime;
lastTime = currentTime;

Surface vs Texture

Surface:

  • CPU 메모리에 저장
  • 느림
  • 소프트웨어 렌더링 Texture:
  • GPU 메모리에 저장
  • 빠름
  • 하드웨어 가속
Surface (느림):
CPU ─→ Surface ─→ 화면
Texture (빠름):
CPU ─→ Texture ─→ GPU ─→ 화면

SDL이란?

SDL (Simple DirectMedia Layer)는 크로스 플랫폼 멀티미디어 라이브러리입니다.

SDL의 역할

창·렌더러·입력(키보드·마우스·패드)·오디오·타이머 등을 하나의 API로 묶어 줍니다. 순수 C API라서 C++, Python, Rust, C# 등 여러 언어의 바인딩이 있습니다.

SDL vs 다른 라이브러리

SDL은 데스크톱부터 Android·iOS까지 지원 범위가 넓습니다. SFML은 클래스 기반 C++ API라 C++에 익숙한 사람에게 더 깔끔하게 느껴질 수 있고, Raylib는 초보자가 몇 줄로 그림을 띄울 수 있을 만큼 단순한 API를 지향합니다. Unity나 Unreal 같은 엔진은 에디터·에셋 파이프라인·물리가 포함되어 생산성이 높지만 무겁고 학습량도 큽니다.

SDL 모듈

SDL2:           핵심 (윈도우, 렌더링, 이벤트)
SDL2_image:     이미지 로딩 (PNG, JPG, BMP)
SDL2_ttf:       트루타입 폰트
SDL2_mixer:     오디오 믹싱
SDL2_net:       네트워킹

개발 환경 설정

설치

macOS:

brew install sdl2
brew install sdl2_image sdl2_ttf sdl2_mixer

Ubuntu/Debian:

sudo apt update
sudo apt install libsdl2-dev
sudo apt install libsdl2-image-dev libsdl2-ttf-dev libsdl2-mixer-dev

Windows (vcpkg):

vcpkg install sdl2
vcpkg install sdl2-image sdl2-ttf sdl2-mixer

Windows (수동):

1. https://www.libsdl.org/download-2.0.php
2. Development Libraries 다운로드
3. 압축 해제
4. Visual Studio 프로젝트 설정:
   - Include Directories: SDL2/include
   - Library Directories: SDL2/lib/x64
   - Linker Input: SDL2.lib, SDL2main.lib

CMake 프로젝트

CMakeLists.txt:

cmake_minimum_required(VERSION 3.10)
project(SDLGame)
set(CMAKE_CXX_STANDARD 17)
# SDL2 찾기
find_package(SDL2 REQUIRED)
find_package(SDL2_image REQUIRED)
find_package(SDL2_ttf REQUIRED)
find_package(SDL2_mixer REQUIRED)
# 실행 파일
add_executable(game
    src/main.cpp
    src/Game.cpp
    src/Texture.cpp
    src/Player.cpp
)
# 라이브러리 링크
target_link_libraries(game
    SDL2::SDL2
    SDL2::SDL2main
    SDL2_image::SDL2_image
    SDL2_ttf::SDL2_ttf
    SDL2_mixer::SDL2_mixer
)
# 인클루드 디렉토리
target_include_directories(game PRIVATE
    ${CMAKE_CURRENT_SOURCE_DIR}/include
)

빌드:

mkdir build
cd build
cmake ..
make
./game

첫 번째 윈도우

기본 윈도우

#include <SDL2/SDL.h>
#include <iostream>
const int SCREEN_WIDTH = 800;
const int SCREEN_HEIGHT = 600;
int main(int argc, char* argv[]) {
    // SDL 초기화
    if (SDL_Init(SDL_INIT_VIDEO) < 0) {
        std::cerr << "SDL 초기화 실패: " << SDL_GetError() << std::endl;
        return -1;
    }
    
    // 윈도우 생성
    SDL_Window* window = SDL_CreateWindow(
        "SDL Tutorial",
        SDL_WINDOWPOS_CENTERED,
        SDL_WINDOWPOS_CENTERED,
        SCREEN_WIDTH,
        SCREEN_HEIGHT,
        SDL_WINDOW_SHOWN
    );
    
    if (!window) {
        std::cerr << "윈도우 생성 실패: " << SDL_GetError() << std::endl;
        SDL_Quit();
        return -1;
    }
    
    // 렌더러 생성
    SDL_Renderer* renderer = SDL_CreateRenderer(
        window,
        -1,
        SDL_RENDERER_ACCELERATED | SDL_RENDERER_PRESENTVSYNC
    );
    
    if (!renderer) {
        std::cerr << "렌더러 생성 실패: " << SDL_GetError() << std::endl;
        SDL_DestroyWindow(window);
        SDL_Quit();
        return -1;
    }
    
    // 메인 루프
    bool running = true;
    SDL_Event event;
    
    while (running) {
        // 이벤트 처리
        while (SDL_PollEvent(&event)) {
            if (event.type == SDL_QUIT) {
                running = false;
            }
        }
        
        // 렌더링
        SDL_SetRenderDrawColor(renderer, 100, 149, 237, 255);  // 코른플라워 블루
        SDL_RenderClear(renderer);
        SDL_RenderPresent(renderer);
    }
    
    // 정리
    SDL_DestroyRenderer(renderer);
    SDL_DestroyWindow(window);
    SDL_Quit();
    
    return 0;
}

이 뼈대에서 초보자가 가장 많이 놓치는 부분은 while (SDL_PollEvent(&event)) 루프입니다. 이벤트 큐를 비우지 않으면 운영체제는 프로그램이 응답하지 않는다고 판단해, Windows에서는 제목 표시줄에 “응답 없음”이 뜨고 macOS에서는 창이 아예 그려지지 않기도 합니다. 그래서 당장 입력을 처리하지 않더라도 매 프레임 큐를 끝까지 비워야 합니다. SDL_RenderClear → 그리기 → SDL_RenderPresent는 더블 버퍼링 구조로, 보이지 않는 뒷버퍼에 한 프레임을 다 그린 뒤 Present에서 화면과 교체합니다. Present를 빼먹으면 아무리 그려도 검은 화면만 보입니다. 또 모든 SDL 함수는 실패하면 NULL이나 음수를 반환하고 이유는 SDL_GetError()에 남기므로, 예제처럼 생성 직후 확인하고 실패 시 이미 만든 자원을 역순으로 해제하는 습관이 중요합니다. 렌더러 생성 실패는 GPU 드라이버가 없는 원격 데스크톱이나 가상 머신에서 자주 일어나며, 이때는 SDL_RENDERER_SOFTWARE로 대체해 볼 수 있습니다.


이벤트 처리

키보드 입력

bool running = true;
SDL_Event event;
while (running) {
    while (SDL_PollEvent(&event)) {
        if (event.type == SDL_QUIT) {
            running = false;
        }
        
        // 키 눌림
        if (event.type == SDL_KEYDOWN) {
            switch (event.key.keysym.sym) {
                case SDLK_ESCAPE:
                    running = false;
                    break;
                case SDLK_UP:
                    std::cout << "위쪽 화살표" << std::endl;
                    break;
                case SDLK_DOWN:
                    std::cout << "아래쪽 화살표" << std::endl;
                    break;
                case SDLK_LEFT:
                    std::cout << "왼쪽 화살표" << std::endl;
                    break;
                case SDLK_RIGHT:
                    std::cout << "오른쪽 화살표" << std::endl;
                    break;
                case SDLK_SPACE:
                    std::cout << "스페이스바" << std::endl;
                    break;
            }
        }
        
        // 키 떼어짐
        if (event.type == SDL_KEYUP) {
            std::cout << "키 떼어짐" << std::endl;
        }
    }
}

연속 입력 (키 누르고 있기):

// 키보드 상태 배열
// 변수 선언 및 초기화
const Uint8* keyState = SDL_GetKeyboardState(nullptr);
// 게임 루프
while (running) {
    // 이벤트 처리
    while (SDL_PollEvent(&event)) {
        if (event.type == SDL_QUIT) running = false;
    }
    
    // 연속 입력 확인
    if (keyState[SDL_SCANCODE_W]) {
        playerY -= 5;  // 위로 이동
    }
    if (keyState[SDL_SCANCODE_S]) {
        playerY += 5;  // 아래로 이동
    }
    if (keyState[SDL_SCANCODE_A]) {
        playerX -= 5;  // 왼쪽으로 이동
    }
    if (keyState[SDL_SCANCODE_D]) {
        playerX += 5;  // 오른쪽으로 이동
    }
    
    // 렌더링...
}

마우스 입력

while (SDL_PollEvent(&event)) {
    // 마우스 버튼 클릭
    if (event.type == SDL_MOUSEBUTTONDOWN) {
        if (event.button.button == SDL_BUTTON_LEFT) {
            int x = event.button.x;
            int y = event.button.y;
            std::cout << "왼쪽 클릭: (" << x << ", " << y << ")" << std::endl;
        }
        if (event.button.button == SDL_BUTTON_RIGHT) {
            std::cout << "오른쪽 클릭" << std::endl;
        }
    }
    
    // 마우스 이동
    if (event.type == SDL_MOUSEMOTION) {
        int x = event.motion.x;
        int y = event.motion.y;
        int dx = event.motion.xrel;  // 상대 이동
        int dy = event.motion.yrel;
    }
    
    // 마우스 휠
    if (event.type == SDL_MOUSEWHEEL) {
        if (event.wheel.y > 0) {
            std::cout << "휠 위로" << std::endl;
        } else if (event.wheel.y < 0) {
            std::cout << "휠 아래로" << std::endl;
        }
    }
}
// 현재 마우스 위치
int mouseX, mouseY;
Uint32 mouseState = SDL_GetMouseState(&mouseX, &mouseY);
if (mouseState & SDL_BUTTON(SDL_BUTTON_LEFT)) {
    std::cout << "왼쪽 버튼 누르고 있음" << std::endl;
}

게임패드 입력

// 게임패드 초기화
SDL_Init(SDL_INIT_VIDEO | SDL_INIT_GAMECONTROLLER);
// 게임패드 열기
SDL_GameController* controller = nullptr;
for (int i = 0; i < SDL_NumJoysticks(); i++) {
    if (SDL_IsGameController(i)) {
        controller = SDL_GameControllerOpen(i);
        if (controller) {
            std::cout << "게임패드 연결: " << SDL_GameControllerName(controller) << std::endl;
            break;
        }
    }
}
// 이벤트 처리
while (SDL_PollEvent(&event)) {
    // 버튼
    if (event.type == SDL_CONTROLLERBUTTONDOWN) {
        if (event.cbutton.button == SDL_CONTROLLER_BUTTON_A) {
            std::cout << "A 버튼" << std::endl;
        }
        if (event.cbutton.button == SDL_CONTROLLER_BUTTON_START) {
            std::cout << "Start 버튼" << std::endl;
        }
    }
    
    // 아날로그 스틱
    if (event.type == SDL_CONTROLLERAXISMOTION) {
        if (event.caxis.axis == SDL_CONTROLLER_AXIS_LEFTX) {
            float value = event.caxis.value / 32767.0f;  // -1.0 ~ 1.0
            std::cout << "왼쪽 스틱 X: " << value << std::endl;
        }
    }
}
// 정리
SDL_GameControllerClose(controller);

렌더링

기본 도형 그리기

// 배경색 설정
SDL_SetRenderDrawColor(renderer, 0, 0, 0, 255);  // 검정
SDL_RenderClear(renderer);
// 점
SDL_SetRenderDrawColor(renderer, 255, 255, 255, 255);  // 흰색
SDL_RenderDrawPoint(renderer, 400, 300);
// 선
SDL_SetRenderDrawColor(renderer, 255, 0, 0, 255);  // 빨강
SDL_RenderDrawLine(renderer, 100, 100, 700, 500);
// 사각형 (테두리)
SDL_Rect rect = {100, 100, 200, 150};
SDL_SetRenderDrawColor(renderer, 0, 255, 0, 255);  // 초록
SDL_RenderDrawRect(renderer, &rect);
// 사각형 (채우기)
SDL_SetRenderDrawColor(renderer, 0, 0, 255, 255);  // 파랑
SDL_RenderFillRect(renderer, &rect);
// 화면 업데이트
SDL_RenderPresent(renderer);

여러 도형 그리기

// 여러 점
SDL_Point points[] = {
    {100, 100}, {150, 120}, {200, 100}, {250, 150}
};
SDL_RenderDrawPoints(renderer, points, 4);
// 여러 선 (연결된)
SDL_RenderDrawLines(renderer, points, 4);
// 여러 사각형
SDL_Rect rects[] = {
    {100, 100, 50, 50},
    {200, 100, 50, 50},
    {300, 100, 50, 50}
};
SDL_RenderDrawRects(renderer, rects, 3);
SDL_RenderFillRects(renderer, rects, 3);

이미지 및 텍스처

SDL_image 사용

#include <SDL2/SDL_image.h>
// SDL_image 초기화
int imgFlags = IMG_INIT_PNG | IMG_INIT_JPG;
if (!(IMG_Init(imgFlags) & imgFlags)) {
    std::cerr << "SDL_image 초기화 실패: " << IMG_GetError() << std::endl;
    return -1;
}
// 이미지 로딩
SDL_Texture* loadTexture(const char* path, SDL_Renderer* renderer) {
    SDL_Surface* surface = IMG_Load(path);
    if (!surface) {
        std::cerr << "이미지 로딩 실패: " << IMG_GetError() << std::endl;
        return nullptr;
    }
    
    SDL_Texture* texture = SDL_CreateTextureFromSurface(renderer, surface);
    SDL_FreeSurface(surface);
    
    if (!texture) {
        std::cerr << "텍스처 생성 실패: " << SDL_GetError() << std::endl;
        return nullptr;
    }
    
    return texture;
}
// 사용
SDL_Texture* playerTexture = loadTexture("player.png", renderer);
// 렌더링
SDL_Rect destRect = {100, 100, 64, 64};
SDL_RenderCopy(renderer, playerTexture, nullptr, &destRect);
// 정리
SDL_DestroyTexture(playerTexture);
IMG_Quit();

스프라이트 시트

// 스프라이트 시트에서 특정 영역 잘라내기
SDL_Rect srcRect = {0, 0, 32, 32};  // 시트에서의 위치
SDL_Rect destRect = {100, 100, 64, 64};  // 화면에서의 위치
SDL_RenderCopy(renderer, spriteSheet, &srcRect, &destRect);
// 애니메이션
int frameWidth = 32;
int frameHeight = 32;
int currentFrame = 0;
int totalFrames = 8;
// 프레임 업데이트 (60 FPS 기준, 0.1초마다)
if (SDL_GetTicks() % 100 < 16) {
    currentFrame = (currentFrame + 1) % totalFrames;
}
// 현재 프레임 렌더링
SDL_Rect srcRect = {
    currentFrame * frameWidth,
    0,
    frameWidth,
    frameHeight
};
SDL_RenderCopy(renderer, spriteSheet, &srcRect, &destRect);

회전 및 플립

// 회전
double angle = 45.0;  // 각도
SDL_Point center = {32, 32};  // 회전 중심
SDL_RenderCopyEx(renderer, texture, nullptr, &destRect, angle, &center, SDL_FLIP_NONE);
// 좌우 반전
SDL_RenderCopyEx(renderer, texture, nullptr, &destRect, 0, nullptr, SDL_FLIP_HORIZONTAL);
// 상하 반전
SDL_RenderCopyEx(renderer, texture, nullptr, &destRect, 0, nullptr, SDL_FLIP_VERTICAL);
// 회전 + 반전
SDL_RenderCopyEx(renderer, texture, nullptr, &destRect, angle, &center, SDL_FLIP_HORIZONTAL);

텍스트 렌더링

SDL_ttf 사용

#include <SDL2/SDL_ttf.h>
// TTF 초기화
if (TTF_Init() == -1) {
    std::cerr << "SDL_ttf 초기화 실패: " << TTF_GetError() << std::endl;
    return -1;
}
// 폰트 로딩
TTF_Font* font = TTF_OpenFont("arial.ttf", 24);
if (!font) {
    std::cerr << "폰트 로딩 실패: " << TTF_GetError() << std::endl;
    return -1;
}
// 텍스트 렌더링
SDL_Color textColor = {255, 255, 255, 255};  // 흰색
SDL_Surface* textSurface = TTF_RenderText_Solid(font, "Hello SDL!", textColor);
SDL_Texture* textTexture = SDL_CreateTextureFromSurface(renderer, textSurface);
SDL_FreeSurface(textSurface);
// 텍스트 크기 확인
int textWidth, textHeight;
TTF_SizeText(font, "Hello SDL!", &textWidth, &textHeight);
// 렌더링
SDL_Rect textRect = {100, 100, textWidth, textHeight};
SDL_RenderCopy(renderer, textTexture, nullptr, &textRect);
// 정리
SDL_DestroyTexture(textTexture);
TTF_CloseFont(font);
TTF_Quit();

고품질 텍스트:

// Solid (빠름, 낮은 품질)
SDL_Surface* surface = TTF_RenderText_Solid(font, text, color);
// Shaded (중간 품질, 배경색 포함)
SDL_Color bgColor = {0, 0, 0, 255};
SDL_Surface* surface = TTF_RenderText_Shaded(font, text, color, bgColor);
// Blended (느림, 최고 품질, 안티앨리어싱)
SDL_Surface* surface = TTF_RenderText_Blended(font, text, color);
// UTF-8 지원
SDL_Surface* surface = TTF_RenderUTF8_Blended(font, "안녕하세요", color);

오디오

SDL_mixer 사용

#include <SDL2/SDL_mixer.h>
// SDL_mixer 초기화
if (Mix_OpenAudio(44100, MIX_DEFAULT_FORMAT, 2, 2048) < 0) {
    std::cerr << "SDL_mixer 초기화 실패: " << Mix_GetError() << std::endl;
    return -1;
}
// 음악 로딩
Mix_Music* music = Mix_LoadMUS("background.mp3");
if (!music) {
    std::cerr << "음악 로딩 실패: " << Mix_GetError() << std::endl;
}
// 효과음 로딩
Mix_Chunk* jumpSound = Mix_LoadWAV("jump.wav");
if (!jumpSound) {
    std::cerr << "효과음 로딩 실패: " << Mix_GetError() << std::endl;
}
// 음악 재생
Mix_PlayMusic(music, -1);  // -1: 무한 반복
// 음악 제어
Mix_VolumeMusic(64);  // 볼륨 (0~128)
Mix_PauseMusic();
Mix_ResumeMusic();
Mix_HaltMusic();
// 효과음 재생
Mix_PlayChannel(-1, jumpSound, 0);  // -1: 자동 채널 선택, 0: 반복 없음
// 효과음 제어
Mix_Volume(-1, 64);  // 모든 채널 볼륨
Mix_Volume(0, 128);  // 채널 0 볼륨
// 정리
Mix_FreeChunk(jumpSound);
Mix_FreeMusic(music);
Mix_CloseAudio();

게임 루프

기본 게임 루프

class Game {
private:
    SDL_Window* window;
    SDL_Renderer* renderer;
    bool running;
    
    Uint32 lastTime;
    float deltaTime;
    
public:
    Game() : window(nullptr), renderer(nullptr), running(false), lastTime(0), deltaTime(0) {}
    
    bool init() {
        if (SDL_Init(SDL_INIT_VIDEO | SDL_INIT_AUDIO) < 0) {
            std::cerr << "SDL 초기화 실패" << std::endl;
            return false;
        }
        
        window = SDL_CreateWindow(
            "Game",
            SDL_WINDOWPOS_CENTERED,
            SDL_WINDOWPOS_CENTERED,
            800, 600,
            SDL_WINDOW_SHOWN
        );
        
        if (!window) {
            std::cerr << "윈도우 생성 실패" << std::endl;
            return false;
        }
        
        renderer = SDL_CreateRenderer(window, -1, SDL_RENDERER_ACCELERATED | SDL_RENDERER_PRESENTVSYNC);
        
        if (!renderer) {
            std::cerr << "렌더러 생성 실패" << std::endl;
            return false;
        }
        
        running = true;
        lastTime = SDL_GetTicks();
        
        return true;
    }
    
    void handleEvents() {
        SDL_Event event;
        while (SDL_PollEvent(&event)) {
            if (event.type == SDL_QUIT) {
                running = false;
            }
            
            if (event.type == SDL_KEYDOWN) {
                if (event.key.keysym.sym == SDLK_ESCAPE) {
                    running = false;
                }
            }
        }
    }
    
    void update() {
        // 델타 타임 계산
        Uint32 currentTime = SDL_GetTicks();
        deltaTime = (currentTime - lastTime) / 1000.0f;
        lastTime = currentTime;
        
        // 게임 로직
        // 플레이어 이동, 충돌 감지, AI 등
    }
    
    void render() {
        // 화면 지우기
        SDL_SetRenderDrawColor(renderer, 0, 0, 0, 255);
        SDL_RenderClear(renderer);
        
        // 게임 오브젝트 그리기
        // ...
        
        // 화면 업데이트
        SDL_RenderPresent(renderer);
    }
    
    void run() {
        while (running) {
            handleEvents();
            update();
            render();
        }
    }
    
    void clean() {
        SDL_DestroyRenderer(renderer);
        SDL_DestroyWindow(window);
        SDL_Quit();
    }
};
int main(int argc, char* argv[]) {
    Game game;
    
    if (!game.init()) {
        return -1;
    }
    
    game.run();
    game.clean();
    
    return 0;
}

FPS 제한

const int FPS = 60;
const int frameDelay = 1000 / FPS;  // 정수 나눗셈이라 16ms (실제로는 약 62.5 FPS)
Uint32 frameStart;
int frameTime;
while (running) {
    frameStart = SDL_GetTicks();
    
    handleEvents();
    update();
    render();
    
    frameTime = SDL_GetTicks() - frameStart;
    
    if (frameDelay > frameTime) {
        SDL_Delay(frameDelay - frameTime);
    }
}

이 방식은 간단하지만 정밀하지 않습니다. SDL_GetTicks()는 밀리초 단위라 16.67ms를 표현할 수 없고, SDL_Delay는 운영체제 스케줄러에 의존하므로 요청한 시간보다 몇 ms 더 잘 수 있습니다. 특히 Windows에서는 타이머 해상도 때문에 과거에 15ms 가까이 오차가 나는 경우도 있었습니다. 그래서 실제 게임에서는 두 가지를 조합합니다. 첫째, 렌더러를 SDL_RENDERER_PRESENTVSYNC로 만들어 모니터 주사율에 맞춰 SDL_RenderPresent가 대기하게 하고, 둘째, 게임 로직은 앞에서 설명한 델타 타임으로 계산해 프레임 간격이 흔들려도 속도가 일정하게 유지되게 합니다. 물리 계산처럼 결과가 매번 같아야 하는 부분은 누적 시간을 1/60초 단위로 잘라 여러 번 갱신하는 고정 타임스텝 방식이 널리 쓰입니다. 가변 델타 타임을 그대로 물리에 넣으면 프레임이 잠깐 끊겼을 때 한 번에 큰 이동이 일어나 벽을 뚫고 지나가는 버그가 생기기 쉽습니다.


충돌 감지

AABB 충돌

AABB (Axis-Aligned Bounding Box):

bool checkCollision(SDL_Rect a, SDL_Rect b) {
    // 왼쪽 끝
    int leftA = a.x;
    int leftB = b.x;
    
    // 오른쪽 끝
    int rightA = a.x + a.w;
    int rightB = b.x + b.w;
    
    // 위쪽 끝
    int topA = a.y;
    int topB = b.y;
    
    // 아래쪽 끝
    int bottomA = a.y + a.h;
    int bottomB = b.y + b.h;
    
    // 충돌 확인
    if (bottomA <= topB) return false;  // A가 B 위에
    if (topA >= bottomB) return false;  // A가 B 아래에
    if (rightA <= leftB) return false;  // A가 B 왼쪽에     if (leftA >= rightB) return false;  // A가 B 오른쪽에     
    return true;  // 충돌!
}
// 또는 SDL 내장 함수
bool collision = SDL_HasIntersection(&rectA, &rectB);

원 충돌

struct Circle {
    int x, y;
    int radius;
};
bool checkCollision(Circle a, Circle b) {
    // 두 원의 중심 거리
    int dx = a.x - b.x;
    int dy = a.y - b.y;
    int distanceSquared = dx * dx + dy * dy;
    
    // 반지름 합
    int radiusSum = a.radius + b.radius;
    
    // 거리가 반지름 합보다 작으면 충돌
    return distanceSquared < (radiusSum * radiusSum);
}

점과 사각형 충돌

bool pointInRect(int x, int y, SDL_Rect rect) {
    return (x >= rect.x && x <= rect.x + rect.w &&
            y >= rect.y && y <= rect.y + rect.h);
}
// 마우스 클릭 확인
if (event.type == SDL_MOUSEBUTTONDOWN) {
    int mouseX = event.button.x;
    int mouseY = event.button.y;
    
    if (pointInRect(mouseX, mouseY, buttonRect)) {
        std::cout << "버튼 클릭!" << std::endl;
    }
}

실전 프로젝트

프로젝트 1: Pong 게임

#include <SDL2/SDL.h>
#include <iostream>
const int SCREEN_WIDTH = 800;
const int SCREEN_HEIGHT = 600;
const int PADDLE_WIDTH = 15;
const int PADDLE_HEIGHT = 100;
const int BALL_SIZE = 15;
class Pong {
private:
    SDL_Window* window;
    SDL_Renderer* renderer;
    bool running;
    
    // 패들
    SDL_Rect leftPaddle;
    SDL_Rect rightPaddle;
    int leftPaddleVel;
    int rightPaddleVel;
    
    // 공
    SDL_Rect ball;
    int ballVelX;
    int ballVelY;
    
    // 점수
    int leftScore;
    int rightScore;
    
public:
    Pong() : running(false), leftScore(0), rightScore(0) {
        // 패들 초기화
        leftPaddle = {50, SCREEN_HEIGHT / 2 - PADDLE_HEIGHT / 2, PADDLE_WIDTH, PADDLE_HEIGHT};
        rightPaddle = {SCREEN_WIDTH - 50 - PADDLE_WIDTH, SCREEN_HEIGHT / 2 - PADDLE_HEIGHT / 2, PADDLE_WIDTH, PADDLE_HEIGHT};
        leftPaddleVel = 0;
        rightPaddleVel = 0;
        
        // 공 초기화
        resetBall();
    }
    
    void resetBall() {
        ball = {SCREEN_WIDTH / 2 - BALL_SIZE / 2, SCREEN_HEIGHT / 2 - BALL_SIZE / 2, BALL_SIZE, BALL_SIZE};
        ballVelX = (rand() % 2 == 0) ? -5 : 5;
        ballVelY = (rand() % 10 - 5);
    }
    
    bool init() {
        if (SDL_Init(SDL_INIT_VIDEO) < 0) return false;
        
        window = SDL_CreateWindow("Pong", SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED, SCREEN_WIDTH, SCREEN_HEIGHT, SDL_WINDOW_SHOWN);
        if (!window) return false;
        
        renderer = SDL_CreateRenderer(window, -1, SDL_RENDERER_ACCELERATED | SDL_RENDERER_PRESENTVSYNC);
        if (!renderer) return false;
        
        running = true;
        return true;
    }
    
    void handleEvents() {
        SDL_Event event;
        while (SDL_PollEvent(&event)) {
            if (event.type == SDL_QUIT) {
                running = false;
            }
        }
        
        // 키보드 상태
        const Uint8* keyState = SDL_GetKeyboardState(nullptr);
        
        // 왼쪽 패들 (W/S)
        leftPaddleVel = 0;
        if (keyState[SDL_SCANCODE_W]) leftPaddleVel = -6;
        if (keyState[SDL_SCANCODE_S]) leftPaddleVel = 6;
        
        // 오른쪽 패들 (Up/Down)
        rightPaddleVel = 0;
        if (keyState[SDL_SCANCODE_UP]) rightPaddleVel = -6;
        if (keyState[SDL_SCANCODE_DOWN]) rightPaddleVel = 6;
    }
    
    void update() {
        // 패들 이동
        leftPaddle.y += leftPaddleVel;
        rightPaddle.y += rightPaddleVel;
        
        // 패들 경계 체크
        if (leftPaddle.y < 0) leftPaddle.y = 0;
        if (leftPaddle.y + PADDLE_HEIGHT > SCREEN_HEIGHT) leftPaddle.y = SCREEN_HEIGHT - PADDLE_HEIGHT;
        if (rightPaddle.y < 0) rightPaddle.y = 0;
        if (rightPaddle.y + PADDLE_HEIGHT > SCREEN_HEIGHT) rightPaddle.y = SCREEN_HEIGHT - PADDLE_HEIGHT;
        
        // 공 이동
        ball.x += ballVelX;
        ball.y += ballVelY;
        
        // 공 벽 충돌 (위/아래)
        if (ball.y <= 0 || ball.y + BALL_SIZE >= SCREEN_HEIGHT) {
            ballVelY = -ballVelY;
        }
        
        // 공 패들 충돌
        if (SDL_HasIntersection(&ball, &leftPaddle) || SDL_HasIntersection(&ball, &rightPaddle)) {
            ballVelX = -ballVelX;
        }
        
        // 득점
        if (ball.x < 0) {
            rightScore++;
            resetBall();
        }
        if (ball.x > SCREEN_WIDTH) {
            leftScore++;
            resetBall();
        }
    }
    
    void render() {
        // 배경
        SDL_SetRenderDrawColor(renderer, 0, 0, 0, 255);
        SDL_RenderClear(renderer);
        
        // 중앙선
        SDL_SetRenderDrawColor(renderer, 255, 255, 255, 255);
        for (int y = 0; y < SCREEN_HEIGHT; y += 20) {
            SDL_Rect line = {SCREEN_WIDTH / 2 - 2, y, 4, 10};
            SDL_RenderFillRect(renderer, &line);
        }
        
        // 패들
        SDL_RenderFillRect(renderer, &leftPaddle);
        SDL_RenderFillRect(renderer, &rightPaddle);
        
        // 공
        SDL_RenderFillRect(renderer, &ball);
        
        SDL_RenderPresent(renderer);
    }
    
    void run() {
        while (running) {
            handleEvents();
            update();
            render();
            SDL_Delay(16);  // ~60 FPS
        }
    }
    
    void clean() {
        SDL_DestroyRenderer(renderer);
        SDL_DestroyWindow(window);
        SDL_Quit();
    }
};
int main(int argc, char* argv[]) {
    Pong game;
    
    if (!game.init()) {
        return -1;
    }
    
    game.run();
    game.clean();
    
    return 0;
}

프로젝트 2: 플랫포머 게임

class Player {
public:
    SDL_Rect rect;
    float velocityX;
    float velocityY;
    bool onGround;
    
    Player(int x, int y) : velocityX(0), velocityY(0), onGround(false) {
        rect = {x, y, 32, 48};
    }
    
    void handleInput(const Uint8* keyState) {
        velocityX = 0;
        
        if (keyState[SDL_SCANCODE_LEFT]) {
            velocityX = -200;  // 픽셀/초
        }
        if (keyState[SDL_SCANCODE_RIGHT]) {
            velocityX = 200;
        }
        if (keyState[SDL_SCANCODE_SPACE] && onGround) {
            velocityY = -500;  // 점프
            onGround = false;
        }
    }
    
    void update(float deltaTime) {
        // 중력
        velocityY += 1000 * deltaTime;  // 픽셀/초²
        
        // 위치 업데이트
        rect.x += velocityX * deltaTime;
        rect.y += velocityY * deltaTime;
        
        // 바닥 충돌
        if (rect.y + rect.h >= 550) {
            rect.y = 550 - rect.h;
            velocityY = 0;
            onGround = true;
        }
        
        // 화면 경계
        if (rect.x < 0) rect.x = 0;
        if (rect.x + rect.w > 800) rect.x = 800 - rect.w;
    }
    
    void render(SDL_Renderer* renderer) {
        SDL_SetRenderDrawColor(renderer, 255, 0, 0, 255);
        SDL_RenderFillRect(renderer, &rect);
    }
};
class Platform {
public:
    SDL_Rect rect;
    
    Platform(int x, int y, int w, int h) {
        rect = {x, y, w, h};
    }
    
    void render(SDL_Renderer* renderer) {
        SDL_SetRenderDrawColor(renderer, 0, 255, 0, 255);
        SDL_RenderFillRect(renderer, &rect);
    }
};
class Platformer {
private:
    SDL_Window* window;
    SDL_Renderer* renderer;
    bool running;
    
    Player player;
    std::vector<Platform> platforms;
    
    Uint32 lastTime;
    float deltaTime;
    
public:
    Platformer() : player(100, 100), lastTime(0), deltaTime(0) {
        // 플랫폼 생성
        platforms.push_back(Platform(0, 550, 800, 50));      // 바닥
        platforms.push_back(Platform(200, 450, 150, 20));
        platforms.push_back(Platform(400, 350, 150, 20));
        platforms.push_back(Platform(600, 250, 150, 20));
    }
    
    bool init() {
        if (SDL_Init(SDL_INIT_VIDEO) < 0) return false;
        
        window = SDL_CreateWindow("Platformer", SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED, 800, 600, SDL_WINDOW_SHOWN);
        if (!window) return false;
        
        renderer = SDL_CreateRenderer(window, -1, SDL_RENDERER_ACCELERATED);
        if (!renderer) return false;
        
        running = true;
        lastTime = SDL_GetTicks();
        
        return true;
    }
    
    void handleEvents() {
        SDL_Event event;
        while (SDL_PollEvent(&event)) {
            if (event.type == SDL_QUIT) running = false;
        }
        
        const Uint8* keyState = SDL_GetKeyboardState(nullptr);
        player.handleInput(keyState);
    }
    
    void update() {
        Uint32 currentTime = SDL_GetTicks();
        deltaTime = (currentTime - lastTime) / 1000.0f;
        lastTime = currentTime;
        
        // 플레이어 업데이트
        player.update(deltaTime);
        
        // 플랫폼 충돌 체크
        for (auto& platform : platforms) {
            if (SDL_HasIntersection(&player.rect, &platform.rect)) {
                // 위에서 착지
                if (player.velocityY > 0 && player.rect.y + player.rect.h - 10 < platform.rect.y) {
                    player.rect.y = platform.rect.y - player.rect.h;
                    player.velocityY = 0;
                    player.onGround = true;
                }
            }
        }
    }
    
    void render() {
        SDL_SetRenderDrawColor(renderer, 135, 206, 235, 255);  // 하늘색
        SDL_RenderClear(renderer);
        
        // 플랫폼 렌더링
        for (auto& platform : platforms) {
            platform.render(renderer);
        }
        
        // 플레이어 렌더링
        player.render(renderer);
        
        SDL_RenderPresent(renderer);
    }
    
    void run() {
        while (running) {
            handleEvents();
            update();
            render();
        }
    }
    
    void clean() {
        SDL_DestroyRenderer(renderer);
        SDL_DestroyWindow(window);
        SDL_Quit();
    }
};
int main(int argc, char* argv[]) {
    Platformer game;
    
    if (!game.init()) return -1;
    
    game.run();
    game.clean();
    
    return 0;
}

Python에서 SDL 사용

PySDL2 설치

pip install PySDL2 PySDL2-dll

기본 윈도우 (Python)

import sdl2
import sdl2.ext
import sys
def main():
    # SDL 초기화
    sdl2.ext.init()
    
    # 윈도우 생성
    window = sdl2.ext.Window("SDL Python", size=(800, 600))
    window.show()
    
    # 렌더러 생성
    renderer = sdl2.ext.Renderer(window)
    
    # 메인 루프
    running = True
    while running:
        # 이벤트 처리
        events = sdl2.ext.get_events()
        for event in events:
            if event.type == sdl2.SDL_QUIT:
                running = False
            
            if event.type == sdl2.SDL_KEYDOWN:
                if event.key.keysym.sym == sdl2.SDLK_ESCAPE:
                    running = False
        
        # 렌더링
        renderer.clear(sdl2.ext.Color(100, 149, 237))  # 코른플라워 블루
        renderer.present()
    
    # 정리
    sdl2.ext.quit()
    return 0
if __name__ == '__main__':
    sys.exit(main())

간단한 게임 (Python)

import sdl2
import sdl2.ext
import random
class Ball:
    def __init__(self, x, y):
        self.x = x
        self.y = y
        self.vx = random.randint(-5, 5)
        self.vy = random.randint(-5, 5)
        self.size = 20
        self.color = sdl2.ext.Color(255, 255, 255)
    
    def update(self):
        self.x += self.vx
        self.y += self.vy
        
        # 벽 충돌
        if self.x <= 0 or self.x >= 800 - self.size:
            self.vx = -self.vx
        if self.y <= 0 or self.y >= 600 - self.size:
            self.vy = -self.vy
    
    def draw(self, renderer):
        rect = sdl2.SDL_Rect(int(self.x), int(self.y), self.size, self.size)
        renderer.fill((rect,), self.color)
def main():
    sdl2.ext.init()
    
    window = sdl2.ext.Window("Bouncing Balls", size=(800, 600))
    window.show()
    
    renderer = sdl2.ext.Renderer(window)
    
    # 공 생성
    balls = [Ball(random.randint(0, 780), random.randint(0, 580)) for _ in range(10)]
    
    running = True
    clock = sdl2.ext.time.Clock()
    
    while running:
        # 이벤트
        events = sdl2.ext.get_events()
        for event in events:
            if event.type == sdl2.SDL_QUIT:
                running = False
            
            # 클릭 시 공 추가
            if event.type == sdl2.SDL_MOUSEBUTTONDOWN:
                balls.append(Ball(event.button.x, event.button.y))
        
        # 업데이트
        for ball in balls:
            ball.update()
        
        # 렌더링
        renderer.clear(sdl2.ext.Color(0, 0, 0))
        
        for ball in balls:
            ball.draw(renderer)
        
        renderer.present()
        
        # FPS 제한
        clock.tick(60)
    
    sdl2.ext.quit()
if __name__ == '__main__':
    main()

SDL + OpenGL

OpenGL 컨텍스트 생성

#include <SDL2/SDL.h>
#include <glad/glad.h>
int main(int argc, char* argv[]) {
    SDL_Init(SDL_INIT_VIDEO);
    
    // OpenGL 속성 설정
    SDL_GL_SetAttribute(SDL_GL_CONTEXT_MAJOR_VERSION, 4);
    SDL_GL_SetAttribute(SDL_GL_CONTEXT_MINOR_VERSION, 1);
    SDL_GL_SetAttribute(SDL_GL_CONTEXT_PROFILE_MASK, SDL_GL_CONTEXT_PROFILE_CORE);
    SDL_GL_SetAttribute(SDL_GL_DOUBLEBUFFER, 1);
    SDL_GL_SetAttribute(SDL_GL_DEPTH_SIZE, 24);
    
    // 윈도우 생성 (OpenGL 플래그)
    SDL_Window* window = SDL_CreateWindow(
        "SDL + OpenGL",
        SDL_WINDOWPOS_CENTERED,
        SDL_WINDOWPOS_CENTERED,
        800, 600,
        SDL_WINDOW_OPENGL | SDL_WINDOW_SHOWN
    );
    
    // OpenGL 컨텍스트 생성
    SDL_GLContext context = SDL_GL_CreateContext(window);
    
    // GLAD 초기화
    if (!gladLoadGLLoader((GLADloadproc)SDL_GL_GetProcAddress)) {
        std::cerr << "GLAD 초기화 실패" << std::endl;
        return -1;
    }
    
    // VSync 활성화
    SDL_GL_SetSwapInterval(1);
    
    // OpenGL 설정
    glViewport(0, 0, 800, 600);
    glEnable(GL_DEPTH_TEST);
    
    // 메인 루프
    bool running = true;
    SDL_Event event;
    
    while (running) {
        while (SDL_PollEvent(&event)) {
            if (event.type == SDL_QUIT) running = false;
        }
        
        // OpenGL 렌더링
        glClearColor(0.2f, 0.3f, 0.3f, 1.0f);
        glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
        
        // 여기에 OpenGL 렌더링 코드...
        
        SDL_GL_SwapWindow(window);
    }
    
    // 정리
    SDL_GL_DeleteContext(context);
    SDL_DestroyWindow(window);
    SDL_Quit();
    
    return 0;
}

고급 렌더링

알파 블렌딩

// 텍스처에 알파 블렌딩 활성화
SDL_SetTextureBlendMode(texture, SDL_BLENDMODE_BLEND);
// 텍스처 투명도 설정
SDL_SetTextureAlphaMod(texture, 128);  // 0~255 (128 = 50% 투명)
// 색상 모드 설정
SDL_SetTextureColorMod(texture, 255, 0, 0);  // 빨간색 틴트

렌더 타겟

// 텍스처를 렌더 타겟으로 생성
SDL_Texture* targetTexture = SDL_CreateTexture(
    renderer,
    SDL_PIXELFORMAT_RGBA8888,
    SDL_TEXTUREACCESS_TARGET,
    800, 600
);
// 렌더 타겟 변경
SDL_SetRenderTarget(renderer, targetTexture);
// 이 텍스처에 렌더링
SDL_SetRenderDrawColor(renderer, 255, 0, 0, 255);
SDL_RenderClear(renderer);
// ... 더 많은 렌더링 ...
// 기본 렌더 타겟으로 복원
SDL_SetRenderTarget(renderer, nullptr);
// 렌더링된 텍스처 사용
SDL_RenderCopy(renderer, targetTexture, nullptr, nullptr);

스케일 및 논리 크기

// 논리 크기 설정 (자동 스케일링)
SDL_RenderSetLogicalSize(renderer, 800, 600);
// 이제 윈도우 크기가 변해도 800×600으로 렌더링됨
// 자동으로 스케일링됨
// 스케일 품질 설정
SDL_SetHint(SDL_HINT_RENDER_SCALE_QUALITY, "1");  // 선형 필터링
// "0": 최근접 이웃 (픽셀 아트)
// "1": 선형 (부드러움)
// "2": 이방성 (최고 품질)

타이머 및 시간

기본 타이머

// 시작 시간
Uint32 startTime = SDL_GetTicks();  // 밀리초
// 경과 시간
Uint32 elapsed = SDL_GetTicks() - startTime;
std::cout << "경과: " << elapsed << "ms" << std::endl;
// 대기
SDL_Delay(1000);  // 1초 대기
// 고해상도 타이머
Uint64 start = SDL_GetPerformanceCounter();
// ... 작업 ...
Uint64 end = SDL_GetPerformanceCounter();
double elapsedSec = (end - start) / (double)SDL_GetPerformanceFrequency();
std::cout << "경과: " << elapsedSec << "초" << std::endl;

FPS 카운터

class FPSCounter {
private:
    Uint32 frameCount;
    Uint32 lastTime;
    float fps;
    
public:
    FPSCounter() : frameCount(0), lastTime(SDL_GetTicks()), fps(0) {}
    
    void update() {
        frameCount++;
        
        Uint32 currentTime = SDL_GetTicks();
        Uint32 elapsed = currentTime - lastTime;
        
        // 1초마다 FPS 계산
        if (elapsed >= 1000) {
            fps = frameCount / (elapsed / 1000.0f);
            frameCount = 0;
            lastTime = currentTime;
        }
    }
    
    float getFPS() const {
        return fps;
    }
};
// 사용
FPSCounter fpsCounter;
while (running) {
    // ...
    
    fpsCounter.update();
    
    // FPS 표시
    if (SDL_GetTicks() % 1000 < 16) {
        std::cout << "FPS: " << fpsCounter.getFPS() << std::endl;
    }
}

파티클 시스템

struct Particle {
    float x, y;
    float vx, vy;
    Uint8 r, g, b, a;
    float life;
    float maxLife;
};
class ParticleSystem {
private:
    std::vector<Particle> particles;
    int maxParticles;
    
public:
    ParticleSystem(int max) : maxParticles(max) {
        particles.reserve(maxParticles);
    }
    
    void emit(float x, float y, int count) {
        for (int i = 0; i < count && particles.size() < maxParticles; i++) {
            Particle p;
            p.x = x;
            p.y = y;
            
            // 랜덤 속도
            float angle = (rand() % 360) * 3.14159f / 180.0f;
            float speed = (rand() % 100 + 50) / 10.0f;
            p.vx = cos(angle) * speed;
            p.vy = sin(angle) * speed;
            
            // 랜덤 색상
            p.r = rand() % 256;
            p.g = rand() % 256;
            p.b = rand() % 256;
            p.a = 255;
            
            p.life = 1.0f;
            p.maxLife = 1.0f;
            
            particles.push_back(p);
        }
    }
    
    void update(float deltaTime) {
        for (auto it = particles.begin(); it != particles.end();) {
            it->life -= deltaTime;
            
            if (it->life <= 0) {
                it = particles.erase(it);
            } else {
                // 위치 업데이트
                it->x += it->vx * deltaTime * 60;
                it->y += it->vy * deltaTime * 60;
                
                // 중력
                it->vy += 9.8f * deltaTime * 60;
                
                // 페이드 아웃
                it->a = (Uint8)(255 * (it->life / it->maxLife));
                
                ++it;
            }
        }
    }
    
    void render(SDL_Renderer* renderer) {
        for (const auto& p : particles) {
            SDL_SetRenderDrawColor(renderer, p.r, p.g, p.b, p.a);
            SDL_Rect rect = {(int)p.x, (int)p.y, 4, 4};
            SDL_RenderFillRect(renderer, &rect);
        }
    }
};
// 사용
ParticleSystem particles(1000);
// 마우스 클릭 시 파티클 방출
if (event.type == SDL_MOUSEBUTTONDOWN) {
    particles.emit(event.button.x, event.button.y, 50);
}
// 업데이트 및 렌더링
particles.update(deltaTime);
particles.render(renderer);

타일맵

타일맵 클래스

class TileMap {
private:
    std::vector<std::vector<int>> map;
    SDL_Texture* tileset;
    int tileSize;
    int mapWidth;
    int mapHeight;
    
public:
    TileMap(const std::string& filename, SDL_Texture* tileset, int tileSize)
        : tileset(tileset), tileSize(tileSize) {
        loadMap(filename);
    }
    
    void loadMap(const std::string& filename) {
        std::ifstream file(filename);
        std::string line;
        
        while (std::getline(file, line)) {
            std::vector<int> row;
            std::stringstream ss(line);
            int tile;
            
            while (ss >> tile) {
                row.push_back(tile);
                if (ss.peek() == ',') ss.ignore();
            }
            
            map.push_back(row);
        }
        
        mapHeight = map.size();
        mapWidth = map[0].size();
    }
    
    void render(SDL_Renderer* renderer, int cameraX, int cameraY) {
        for (int y = 0; y < mapHeight; y++) {
            for (int x = 0; x < mapWidth; x++) {
                int tileID = map[y][x];
                
                if (tileID == 0) continue;  // 빈 타일
                
                // 타일셋에서 타일 위치 계산
                int tilesPerRow = 8;  // 타일셋 가로 타일 수
                int srcX = (tileID % tilesPerRow) * tileSize;
                int srcY = (tileID / tilesPerRow) * tileSize;
                
                SDL_Rect srcRect = {srcX, srcY, tileSize, tileSize};
                SDL_Rect destRect = {
                    x * tileSize - cameraX,
                    y * tileSize - cameraY,
                    tileSize,
                    tileSize
                };
                
                SDL_RenderCopy(renderer, tileset, &srcRect, &destRect);
            }
        }
    }
    
    int getTile(int x, int y) const {
        int tileX = x / tileSize;
        int tileY = y / tileSize;
        
        if (tileX < 0 || tileX >= mapWidth || tileY < 0 || tileY >= mapHeight) {
            return 0;
        }
        
        return map[tileY][tileX];
    }
};
// 맵 파일 (map.txt)
/*
1,1,1,1,1,1,1,1,1,1
1,0,0,0,0,0,0,0,0,1
1,0,2,2,0,0,3,3,0,1
1,0,0,0,0,0,0,0,0,1
1,1,1,1,1,1,1,1,1,1
*/
// 사용
TileMap tileMap("map.txt", tilesetTexture, 32);
// 카메라
int cameraX = playerX - 400;
int cameraY = playerY - 300;
tileMap.render(renderer, cameraX, cameraY);

애니메이션

스프라이트 애니메이션 클래스

class Animation {
private:
    SDL_Texture* spriteSheet;
    std::vector<SDL_Rect> frames;
    int currentFrame;
    float frameTime;
    float elapsed;
    bool loop;
    
public:
    Animation(SDL_Texture* sheet, const std::vector<SDL_Rect>& frames, float frameTime, bool loop = true)
        : spriteSheet(sheet), frames(frames), currentFrame(0), frameTime(frameTime), elapsed(0), loop(loop) {}
    
    void update(float deltaTime) {
        elapsed += deltaTime;
        
        if (elapsed >= frameTime) {
            elapsed = 0;
            currentFrame++;
            
            if (currentFrame >= frames.size()) {
                if (loop) {
                    currentFrame = 0;
                } else {
                    currentFrame = frames.size() - 1;
                }
            }
        }
    }
    
    void render(SDL_Renderer* renderer, int x, int y, int width, int height, SDL_RendererFlip flip = SDL_FLIP_NONE) {
        SDL_Rect destRect = {x, y, width, height};
        SDL_RenderCopyEx(renderer, spriteSheet, &frames[currentFrame], &destRect, 0, nullptr, flip);
    }
    
    void reset() {
        currentFrame = 0;
        elapsed = 0;
    }
    
    bool isFinished() const {
        return !loop && currentFrame == frames.size() - 1;
    }
};
// 사용
std::vector<SDL_Rect> walkFrames = {
    {0, 0, 32, 48},
    {32, 0, 32, 48},
    {64, 0, 32, 48},
    {96, 0, 32, 48}
};
Animation walkAnimation(playerSheet, walkFrames, 0.1f, true);
// 게임 루프
walkAnimation.update(deltaTime);
walkAnimation.render(renderer, playerX, playerY, 64, 96);

상태 머신

게임 상태 관리

enum class GameState {
    MENU,
    PLAYING,
    PAUSED,
    GAME_OVER
};
class Game {
private:
    GameState currentState;
    
public:
    void handleEvents(SDL_Event& event) {
        switch (currentState) {
            case GameState::MENU:
                handleMenuEvents(event);
                break;
            case GameState::PLAYING:
                handlePlayingEvents(event);
                break;
            case GameState::PAUSED:
                handlePausedEvents(event);
                break;
            case GameState::GAME_OVER:
                handleGameOverEvents(event);
                break;
        }
    }
    
    void update(float deltaTime) {
        switch (currentState) {
            case GameState::MENU:
                updateMenu(deltaTime);
                break;
            case GameState::PLAYING:
                updatePlaying(deltaTime);
                break;
            case GameState::PAUSED:
                // 일시정지 중에는 업데이트 안함
                break;
            case GameState::GAME_OVER:
                updateGameOver(deltaTime);
                break;
        }
    }
    
    void render() {
        switch (currentState) {
            case GameState::MENU:
                renderMenu();
                break;
            case GameState::PLAYING:
                renderPlaying();
                break;
            case GameState::PAUSED:
                renderPlaying();  // 게임 화면
                renderPauseOverlay();  // 일시정지 오버레이
                break;
            case GameState::GAME_OVER:
                renderGameOver();
                break;
        }
    }
    
    void changeState(GameState newState) {
        currentState = newState;
    }
};

실전 게임: Space Shooter

#include <SDL2/SDL.h>
#include <SDL2/SDL_image.h>
#include <SDL2/SDL_mixer.h>
#include <vector>
#include <iostream>
const int SCREEN_WIDTH = 800;
const int SCREEN_HEIGHT = 600;
class Entity {
public:
    float x, y;
    int width, height;
    SDL_Texture* texture;
    
    Entity(float x, float y, int w, int h, SDL_Texture* tex)
        : x(x), y(y), width(w), height(h), texture(tex) {}
    
    SDL_Rect getRect() const {
        return {(int)x, (int)y, width, height};
    }
    
    void render(SDL_Renderer* renderer) {
        SDL_Rect rect = getRect();
        SDL_RenderCopy(renderer, texture, nullptr, &rect);
    }
};
class Player : public Entity {
public:
    float speed;
    int health;
    
    Player(float x, float y, SDL_Texture* tex)
        : Entity(x, y, 64, 64, tex), speed(300), health(100) {}
    
    void handleInput(const Uint8* keyState, float deltaTime) {
        if (keyState[SDL_SCANCODE_LEFT]) x -= speed * deltaTime;
        if (keyState[SDL_SCANCODE_RIGHT]) x += speed * deltaTime;
        if (keyState[SDL_SCANCODE_UP]) y -= speed * deltaTime;
        if (keyState[SDL_SCANCODE_DOWN]) y += speed * deltaTime;
        
        // 화면 경계
        if (x < 0) x = 0;
        if (x + width > SCREEN_WIDTH) x = SCREEN_WIDTH - width;
        if (y < 0) y = 0;
        if (y + height > SCREEN_HEIGHT) y = SCREEN_HEIGHT - height;
    }
};
class Bullet : public Entity {
public:
    float speed;
    
    Bullet(float x, float y, SDL_Texture* tex)
        : Entity(x, y, 8, 16, tex), speed(500) {}
    
    void update(float deltaTime) {
        y -= speed * deltaTime;
    }
    
    bool isOffScreen() const {
        return y + height < 0;
    }
};
class Enemy : public Entity {
public:
    float speed;
    
    Enemy(float x, float y, SDL_Texture* tex)
        : Entity(x, y, 48, 48, tex), speed(100) {}
    
    void update(float deltaTime) {
        y += speed * deltaTime;
    }
    
    bool isOffScreen() const {
        return y > SCREEN_HEIGHT;
    }
};
class SpaceShooter {
private:
    SDL_Window* window;
    SDL_Renderer* renderer;
    bool running;
    
    Player* player;
    std::vector<Bullet*> bullets;
    std::vector<Enemy*> enemies;
    
    SDL_Texture* playerTexture;
    SDL_Texture* bulletTexture;
    SDL_Texture* enemyTexture;
    
    Mix_Chunk* shootSound;
    Mix_Chunk* explosionSound;
    Mix_Music* bgMusic;
    
    Uint32 lastTime;
    float deltaTime;
    
    Uint32 lastShootTime;
    Uint32 shootCooldown;
    
    Uint32 lastEnemySpawn;
    Uint32 enemySpawnInterval;
    
    int score;
    
public:
    SpaceShooter() : running(false), lastTime(0), deltaTime(0),
                     lastShootTime(0), shootCooldown(200),
                     lastEnemySpawn(0), enemySpawnInterval(1000),
                     score(0) {}
    
    bool init() {
        if (SDL_Init(SDL_INIT_VIDEO | SDL_INIT_AUDIO) < 0) return false;
        
        window = SDL_CreateWindow("Space Shooter", SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED, SCREEN_WIDTH, SCREEN_HEIGHT, SDL_WINDOW_SHOWN);
        if (!window) return false;
        
        renderer = SDL_CreateRenderer(window, -1, SDL_RENDERER_ACCELERATED);
        if (!renderer) return false;
        
        // SDL_image 초기화
        IMG_Init(IMG_INIT_PNG);
        
        // SDL_mixer 초기화
        Mix_OpenAudio(44100, MIX_DEFAULT_FORMAT, 2, 2048);
        
        // 리소스 로딩
        playerTexture = IMG_LoadTexture(renderer, "player.png");
        bulletTexture = IMG_LoadTexture(renderer, "bullet.png");
        enemyTexture = IMG_LoadTexture(renderer, "enemy.png");
        
        shootSound = Mix_LoadWAV("shoot.wav");
        explosionSound = Mix_LoadWAV("explosion.wav");
        bgMusic = Mix_LoadMUS("background.mp3");
        
        // 플레이어 생성
        player = new Player(SCREEN_WIDTH / 2 - 32, SCREEN_HEIGHT - 100, playerTexture);
        
        // 음악 재생
        Mix_PlayMusic(bgMusic, -1);
        
        running = true;
        lastTime = SDL_GetTicks();
        
        return true;
    }
    
    void handleEvents() {
        SDL_Event event;
        while (SDL_PollEvent(&event)) {
            if (event.type == SDL_QUIT) {
                running = false;
            }
            
            if (event.type == SDL_KEYDOWN) {
                if (event.key.keysym.sym == SDLK_ESCAPE) {
                    running = false;
                }
            }
        }
        
        // 연속 입력
        const Uint8* keyState = SDL_GetKeyboardState(nullptr);
        player->handleInput(keyState, deltaTime);
        
        // 발사 (스페이스바)
        if (keyState[SDL_SCANCODE_SPACE]) {
            Uint32 currentTime = SDL_GetTicks();
            if (currentTime - lastShootTime >= shootCooldown) {
                bullets.push_back(new Bullet(player->x + player->width / 2 - 4, player->y, bulletTexture));
                Mix_PlayChannel(-1, shootSound, 0);
                lastShootTime = currentTime;
            }
        }
    }
    
    void update() {
        Uint32 currentTime = SDL_GetTicks();
        deltaTime = (currentTime - lastTime) / 1000.0f;
        lastTime = currentTime;
        
        // 총알 업데이트
        for (auto it = bullets.begin(); it != bullets.end();) {
            (*it)->update(deltaTime);
            
            if ((*it)->isOffScreen()) {
                delete *it;
                it = bullets.erase(it);
            } else {
                ++it;
            }
        }
        
        // 적 생성
        if (currentTime - lastEnemySpawn >= enemySpawnInterval) {
            int x = rand() % (SCREEN_WIDTH - 48);
            enemies.push_back(new Enemy(x, -48, enemyTexture));
            lastEnemySpawn = currentTime;
        }
        
        // 적 업데이트
        for (auto it = enemies.begin(); it != enemies.end();) {
            (*it)->update(deltaTime);
            
            if ((*it)->isOffScreen()) {
                delete *it;
                it = enemies.erase(it);
            } else {
                ++it;
            }
        }
        
        // 충돌 감지 (총알 vs 적)
        for (auto bulletIt = bullets.begin(); bulletIt != bullets.end();) {
            bool bulletHit = false;
            
            for (auto enemyIt = enemies.begin(); enemyIt != enemies.end();) {
                if (SDL_HasIntersection(&(*bulletIt)->getRect(), &(*enemyIt)->getRect())) {
                    // 충돌!
                    Mix_PlayChannel(-1, explosionSound, 0);
                    score += 10;
                    
                    delete *enemyIt;
                    enemyIt = enemies.erase(enemyIt);
                    
                    bulletHit = true;
                    break;
                } else {
                    ++enemyIt;
                }
            }
            
            if (bulletHit) {
                delete *bulletIt;
                bulletIt = bullets.erase(bulletIt);
            } else {
                ++bulletIt;
            }
        }
        
        // 충돌 감지 (플레이어 vs 적)
        for (auto& enemy : enemies) {
            if (SDL_HasIntersection(&player->getRect(), &enemy->getRect())) {
                player->health -= 10;
                std::cout << "피격! 체력: " << player->health << std::endl;
                
                if (player->health <= 0) {
                    std::cout << "게임 오버! 점수: " << score << std::endl;
                    running = false;
                }
            }
        }
    }
    
    void render() {
        // 배경
        SDL_SetRenderDrawColor(renderer, 0, 0, 20, 255);
        SDL_RenderClear(renderer);
        
        // 플레이어
        player->render(renderer);
        
        // 총알
        for (auto& bullet : bullets) {
            bullet->render(renderer);
        }
        
        // 적
        for (auto& enemy : enemies) {
            enemy->render(renderer);
        }
        
        SDL_RenderPresent(renderer);
    }
    
    void run() {
        while (running) {
            handleEvents();
            update();
            render();
        }
    }
    
    void clean() {
        // 엔티티 정리
        delete player;
        for (auto& bullet : bullets) delete bullet;
        for (auto& enemy : enemies) delete enemy;
        
        // 텍스처 정리
        SDL_DestroyTexture(playerTexture);
        SDL_DestroyTexture(bulletTexture);
        SDL_DestroyTexture(enemyTexture);
        
        // 오디오 정리
        Mix_FreeChunk(shootSound);
        Mix_FreeChunk(explosionSound);
        Mix_FreeMusic(bgMusic);
        Mix_CloseAudio();
        
        // SDL 정리
        SDL_DestroyRenderer(renderer);
        SDL_DestroyWindow(window);
        IMG_Quit();
        SDL_Quit();
    }
};
int main(int argc, char* argv[]) {
    SpaceShooter game;
    
    if (!game.init()) {
        return -1;
    }
    
    game.run();
    game.clean();
    
    return 0;
}

네트워킹 (SDL_net)

서버

#include <SDL2/SDL_net.h>
// SDL_net 초기화
SDLNet_Init();
// 서버 소켓 생성
IPaddress ip;
SDLNet_ResolveHost(&ip, nullptr, 1234);  // 포트 1234
TCPsocket server = SDLNet_TCP_Open(&ip);
if (!server) {
    std::cerr << "서버 소켓 생성 실패: " << SDLNet_GetError() << std::endl;
    return -1;
}
std::cout << "서버 시작 (포트 1234)" << std::endl;
// 클라이언트 연결 대기
TCPsocket client = SDLNet_TCP_Accept(server);
if (client) {
    std::cout << "클라이언트 연결됨" << std::endl;
    
    // 데이터 수신
    char buffer[512];
    int received = SDLNet_TCP_Recv(client, buffer, 512);
    if (received > 0) {
        buffer[received] = '\0';
        std::cout << "받음: " << buffer << std::endl;
    }
    
    // 데이터 송신
    const char* message = "Hello from server!";
    SDLNet_TCP_Send(client, message, strlen(message) + 1);
    
    SDLNet_TCP_Close(client);
}
SDLNet_TCP_Close(server);
SDLNet_Quit();

클라이언트

// 서버 연결
IPaddress ip;
SDLNet_ResolveHost(&ip, "127.0.0.1", 1234);
TCPsocket client = SDLNet_TCP_Open(&ip);
if (!client) {
    std::cerr << "서버 연결 실패: " << SDLNet_GetError() << std::endl;
    return -1;
}
std::cout << "서버 연결됨" << std::endl;
// 데이터 송신
const char* message = "Hello from client!";
SDLNet_TCP_Send(client, message, strlen(message) + 1);
// 데이터 수신
char buffer[512];
int received = SDLNet_TCP_Recv(client, buffer, 512);
if (received > 0) {
    buffer[received] = '\0';
    std::cout << "받음: " << buffer << std::endl;
}
SDLNet_TCP_Close(client);
SDLNet_Quit();

최적화

텍스처 아틀라스

// ❌ 느림 (텍스처 바인딩 많음)
for (auto& sprite : sprites) {
    SDL_RenderCopy(renderer, sprite.texture, nullptr, &sprite.rect);
}
// ✅ 빠름 (텍스처 아틀라스)
SDL_Texture* atlas = loadTexture("atlas.png", renderer);
for (auto& sprite : sprites) {
    SDL_RenderCopy(renderer, atlas, &sprite.srcRect, &sprite.destRect);
}

오브젝트 풀링

template<typename T>
class ObjectPool {
private:
    std::vector<T*> pool;
    std::vector<T*> active;
    
public:
    ObjectPool(int size) {
        for (int i = 0; i < size; i++) {
            pool.push_back(new T());
        }
    }
    
    T* acquire() {
        if (pool.empty()) {
            return new T();
        }
        
        T* obj = pool.back();
        pool.pop_back();
        active.push_back(obj);
        return obj;
    }
    
    void release(T* obj) {
        auto it = std::find(active.begin(), active.end(), obj);
        if (it != active.end()) {
            active.erase(it);
            pool.push_back(obj);
        }
    }
    
    ~ObjectPool() {
        for (auto obj : pool) delete obj;
        for (auto obj : active) delete obj;
    }
};
// 사용
ObjectPool<Bullet> bulletPool(100);
// 총알 생성
Bullet* bullet = bulletPool.acquire();
bullet->init(x, y);
// 총알 제거
if (bullet->isOffScreen()) {
    bulletPool.release(bullet);
}

디버깅

렌더링 디버그

// FPS 표시
void renderDebugInfo(SDL_Renderer* renderer, TTF_Font* font, float fps) {
    char text[64];
    sprintf(text, "FPS: %.1f", fps);
    
    SDL_Color white = {255, 255, 255, 255};
    SDL_Surface* surface = TTF_RenderText_Solid(font, text, white);
    SDL_Texture* texture = SDL_CreateTextureFromSurface(renderer, surface);
    
    SDL_Rect rect = {10, 10, surface->w, surface->h};
    SDL_RenderCopy(renderer, texture, nullptr, &rect);
    
    SDL_FreeSurface(surface);
    SDL_DestroyTexture(texture);
}
// 충돌 박스 표시
void renderCollisionBox(SDL_Renderer* renderer, const SDL_Rect& rect) {
    SDL_SetRenderDrawColor(renderer, 255, 0, 0, 255);
    SDL_RenderDrawRect(renderer, &rect);
}
// 그리드 표시
void renderGrid(SDL_Renderer* renderer, int gridSize) {
    SDL_SetRenderDrawColor(renderer, 50, 50, 50, 255);
    
    for (int x = 0; x < SCREEN_WIDTH; x += gridSize) {
        SDL_RenderDrawLine(renderer, x, 0, x, SCREEN_HEIGHT);
    }
    
    for (int y = 0; y < SCREEN_HEIGHT; y += gridSize) {
        SDL_RenderDrawLine(renderer, 0, y, SCREEN_WIDTH, y);
    }
}

트러블슈팅

일반적인 문제

1) “SDL.h: No such file or directory”

# 원인: SDL 헤더 경로 문제
# 해결: 컴파일 시 인클루드 경로 추가
# macOS/Linux
g++ main.cpp -I/usr/local/include/SDL2 -L/usr/local/lib -lSDL2
# 또는 pkg-config 사용
g++ main.cpp `pkg-config --cflags --libs sdl2`

2) “Undefined reference to SDL_main”

// 원인: SDL_main 매크로 문제
// 해결 1: main 함수 시그니처 변경
int main(int argc, char* argv[]) {
    // ...
}
// 해결 2: SDL_main 비활성화
#define SDL_MAIN_HANDLED
#include <SDL2/SDL.h>

3) 검은 화면

// 원인 1: RenderPresent 누락
SDL_RenderPresent(renderer);  // 필수!
// 원인 2: 텍스처가 화면 밖
// 좌표 확인
// 원인 3: 알파 블렌딩 문제
SDL_SetTextureBlendMode(texture, SDL_BLENDMODE_BLEND);

4) 느린 렌더링

// 참고: VSync를 켜면 모니터 주사율에 맞춰 대기하므로 CPU를 100% 쓰며 도는 루프를 막을 수 있음
SDL_CreateRenderer(window, -1, SDL_RENDERER_ACCELERATED | SDL_RENDERER_PRESENTVSYNC);
// 원인 2: 소프트웨어 렌더링
// SDL_RENDERER_ACCELERATED 플래그 확인
// 원인 3: 불필요한 렌더링
// 변경된 부분만 렌더링 (더티 렉트)

트러블슈팅 항목 중 가장 자주 겪는 것은 2번 링크 에러입니다. SDL은 Windows와 일부 플랫폼에서 main을 매크로로 SDL_main으로 바꾸고, 실제 진입점은 SDL2main 라이브러리가 제공합니다. 그래서 int main()처럼 인자 없는 시그니처로 쓰거나, CMake에서 SDL2::SDL2main을 링크하지 않으면 “undefined reference to SDL_main'" 또는 MSVC의 "unresolved external symbol main" 에러가 납니다. 저도 처음 Visual Studio로 옮겼을 때 Linux에서 멀쩡히 빌드되던 코드가 이 에러로 막혀 한참 헤맨 적이 있는데, 대부분 main(int argc, char* argv[])` 시그니처와 SDL2main 링크 두 가지로 해결됩니다.

느린 렌더링의 원인으로 흔한 것은 매 프레임 TTF_RenderText_*와 SDL_CreateTextureFromSurface로 텍스트 텍스처를 새로 만드는 코드입니다. 텍스처 생성은 CPU에서 GPU로 데이터를 올리는 비싼 작업이라, 점수처럼 값이 바뀔 때만 다시 만들고 나머지 프레임에는 캐시된 텍스처를 그려야 합니다. 같은 이유로 매 프레임 IMG_Load를 호출하는 것도 피해야 하며, 반대로 만든 텍스처를 SDL_DestroyTexture로 해제하지 않으면 GPU 메모리가 계속 늘어납니다.


모바일 (Android/iOS)

Android 빌드

# SDL2 Android 프로젝트 생성
# 1. SDL2 소스 다운로드
# 2. android-project 폴더 복사
# 3. jni/src에 코드 복사
# Android.mk
LOCAL_PATH := $(call my-dir)
include $(CLEAR_VARS)
LOCAL_MODULE := main
LOCAL_SRC_FILES := main.cpp
LOCAL_SHARED_LIBRARIES := SDL2
LOCAL_LDLIBS := -lGLESv1_CM -lGLESv2 -llog
include $(BUILD_SHARED_LIBRARY)
# 빌드
ndk-build
ant debug
adb install -r bin/MyGame-debug.apk

iOS 빌드

# Xcode 프로젝트 생성
# 1. SDL2.framework 추가
# 2. main.cpp 추가
# 3. Bridging Header 설정
# main.cpp는 동일하게 사용 가능
# iOS 특화 기능 (터치, 가속도계)은 SDL 이벤트로 처리

FAQ

Q1. SDL 1.2와 SDL2는? SDL 1.2는 유지보수가 끊긴 지 오래되었고, SDL2가 GPU 가속 렌더러·게임 컨트롤러·모바일 지원을 정리한 세대입니다. 2025년에는 SDL3가 정식 출시되었으므로, 새 프로젝트라면 SDL3도 검토할 만합니다. 다만 튜토리얼과 바인딩, 예제 코드는 아직 SDL2 기준이 훨씬 많아서 학습용으로는 SDL2가 여전히 편합니다. Q2. SDL과 SFML은 어떻게 고르나? SDL은 C 중심 API이고 여러 언어의 바인딩이 있으며, SFML은 C++에 맞춘 클래스 기반 설계입니다. 모바일 지원이 필요하면 SDL 쪽이 자료가 많으므로, 대상 플랫폼을 먼저 정하고 고르는 경우가 많습니다.

Q3. SDL만으로 3D를 하나? SDL 자체 렌더러(SDL_Renderer)는 2D 전용입니다. 3D는 13장처럼 SDL로 창과 OpenGL·Vulkan 컨텍스트만 만들고 그리기는 해당 그래픽 API로 직접 하는 방식을 씁니다.

Q4. 엔진과 SDL은? 엔진은 에디터와 워크플로가 포함되어 생산성이 좋고, SDL은 가볍지만 렌더링·물리·오디오 파이프라인을 직접 구성해야 합니다. 학습 목적, 작은 게임, 엔진이 맞지 않는 특수한 요구에는 SDL을, 출시 일정이 빠듯한 제품에는 엔진을 고르는 경우가 많습니다.

Q5. 어떤 언어로 SDL을 쓰나? SDL 자체는 C로 작성되었고 C++·Python·Rust·C# 등의 바인딩이 있습니다. 성능이 중요하면 C/C++, 실험이나 도구 제작이면 Python을 고르는 경우가 흔합니다.


같이 보면 좋은 글