(1/10) 2D SRPG 만들기 선행학습 — 게임 루프와 델타타임, 모든 게임의 심장
Summary
어릴 때 밤새 하던 파랜드 택틱스 같은 게임을 직접 만들어보고 싶다는 생각, 한 번쯤 해보셨을 거예요. 저도 그랬습니다. 그런데 막상 시작하려고 하면 “유니티부터 배워야 하나?”, “도트는 어떻게 찍지?”, “턴제 전투는 뭘로 만들지?” 같은 질문이 한꺼번에 쏟아지면서 손이 안 움직여요.
이 시리즈는 그 막막함을 걷어내는 선행학습 코스입니다. 쿼터뷰 2D SRPG 를 만들기 위해 반드시 알아야 하는 기초 개념 열 덩어리를, 편마다 직접 실행해볼 수 있는 코드와 함께 하나씩 쌓아요. 1편은 모든 게임의 심장인 게임 루프와, 어떤 컴퓨터에서든 같은 속도로 움직이게 해주는 델타타임부터 시작합니다.
💡 이 글에서 다루는 것
- 파랜드 택틱스를 구성요소로 분해하기 — 무엇을 배워야 하는지 지도 그리기
- 도구 선택 — 왜 엔진(유니티·고도)이 아니라 pygame 으로 시작하는지
- 게임 루프의 3박자 — 입력 처리 → 상태 업데이트 → 화면 그리기
- 델타타임 — 60fps 컴퓨터와 144fps 컴퓨터에서 똑같이 움직이게 만들기
- 첫 실습 — 방향키로 움직이는 캐릭터(사각형) 만들기
1. 만들려는 것부터 분해하기
무작정 코드부터 치면 반드시 길을 잃어요. 먼저 파랜드 택틱스 같은 게임이 어떤 부품으로 이루어져 있는지 분해해봅시다. 실제로 게임을 하나 떠올리면서 화면에 보이는 것, 일어나는 일을 쪼개보면 이렇게 나와요.
| 구성요소 | 게임에서 보이는 모습 | 배우는 편 |
|---|---|---|
| 게임 루프 | 게임이 멈추지 않고 계속 반응하는 것 자체 | 1편 (지금) |
| 타일맵 + 쿼터뷰 좌표계 | 마름모 타일이 깔린 전장 맵 | 2편 |
| 스프라이트 + 애니메이션 | 걷고, 공격하고, 쓰러지는 캐릭터 | 3편 |
| 이동 범위 + 길찾기 | 유닛 선택 시 파랗게 표시되는 이동 가능 칸 | 4편 |
| 턴제 전투 시스템 | 아군 턴 → 적군 턴, 데미지 계산, 적 AI | 5편 |
| 데이터 설계 + 세이브 | 유닛 능력치, 스킬, 저장하고 이어하기 | 6편 |
| 맵 에디터 (Tiled) | 배열 손편집을 졸업한 실전 맵 제작 | 7편 |
| UI 와 메뉴 | 정보창, 행동 메뉴, 전투 예측창 | 8편 |
| 사운드 | BGM, 타격 효과음 | 9편 |
| 연출 | 화면 흔들림, 페이드, 히트스톱 | 10편 |
이렇게 놓고 보면 “게임을 만든다”는 거대한 목표가 열 개의 학습 가능한 주제로 바뀝니다. 각 주제는 독립적으로 연습할 수 있고, 마지막에 합치면 게임의 뼈대가 돼요.
✅ 이 시리즈의 목표는 완성된 상용 게임이 아니라, 열 부품을 각각 이해하고 돌려본 상태를 만드는 거예요. 부품을 이해하고 나면 그걸 조립해 자기만의 게임을 만드는 건 시간 문제입니다.
2. 도구 선택 — 왜 pygame 인가
게임 개발 도구는 크게 세 갈래입니다. 결론부터 말하면 이 시리즈는 Python + pygame 으로 갑니다.
| 도구 | 장점 | 단점 | 이 시리즈 관점 |
|---|---|---|---|
| Unity / Unreal | 상용 수준, 에셋스토어, 취업 시장 |
배울 것이 방대, 2D SRPG 엔 과함 |
원리가 엔진 뒤에 숨어서 학습에 불리 |
| Godot | 가볍고 2D 강함, 무료 | GDScript 별도 학습 | 좋은 선택지, 원리 학습 후 넘어가면 최적 |
| pygame (Python) | 원리가 전부 드러남, Python 그대로 |
상용 배포엔 약함 | 선행학습에 최적 |
엔진을 쓰면 타일맵도, 애니메이션도, 길찾기도 버튼 몇 번에 해결돼요. 편하지만, 그 밑에서 무슨 일이 일어나는지 모르는 채로 조립만 하게 됩니다. 그러다 엔진이 제공하지 않는 기능(SRPG 의 이동 범위 계산 같은 것)이 필요해지는 순간 막혀요. SRPG 는 장르 특성상 이런 커스텀 로직이 유난히 많은 장르라서, 원리를 아는 것이 곧 개발 속도가 됩니다.
pygame 은 반대로 아무것도 안 해줍니다. 창 하나, 이미지 그리기, 입력 읽기가 전부예요. 그래서 게임 루프·좌표 변환·길찾기를 전부 내 손으로 짜게 되고, 그 과정이 그대로 선행학습이 됩니다. 여기서 익힌 개념은 나중에 Godot 이나 Unity 로 갈아타도 이름만 바뀐 채 그대로 다시 만나요.
설치는 한 줄입니다. 커뮤니티가 이어받아 활발히 관리하는 pygame-ce(Community Edition)를 추천드려요.
pip install pygame-ce
설치 확인은 파이썬에서 임포트 한 번이면 됩니다.
python -c "import pygame; print(pygame.version.ver)"
2.5.7
버전이 찍히면 준비 끝이에요.
3. 게임 루프 — 게임은 거대한 while 문이다
웹 개발이나 데이터 분석을 하다가 게임 코드를 처음 보면 낯선 점이 하나 있어요. 게임은 요청이 올 때만 실행되는 프로그램이 아니라, 초당 수십 번씩 도는 무한 루프라는 점입니다.
모든 게임은 본질적으로 이 세 박자를 무한 반복해요.
┌─────────────────────────────────┐
│ 1. 입력 처리 (키보드/마우스 읽기) │
│ 2. 상태 업데이트 (위치·HP·턴 갱신) │
│ 3. 화면 그리기 (현재 상태를 렌더링) │
└──────────── 반복 (초당 60회) ─────┘
파랜드 택틱스에서 커서를 움직이면 즉시 반응하는 것도, 대기 중인 캐릭터가 제자리에서 숨쉬는 애니메이션을 반복하는 것도 전부 이 루프가 돌고 있기 때문이에요. 최소한의 게임 루프를 pygame 으로 옮기면 이렇습니다.
import pygame
pygame.init()
screen = pygame.display.set_mode((960, 540)) # 게임 창
pygame.display.set_caption("SRPG 선행학습 1편")
clock = pygame.time.Clock() # 프레임 조절 타이머
running = True
while running: # ← 이게 게임 루프
# 1. 입력 처리
for event in pygame.event.get():
if event.type == pygame.QUIT: # 창 닫기 버튼
running = False
# 2. 상태 업데이트 (아직 없음)
# 3. 화면 그리기
screen.fill((30, 30, 46)) # 배경색으로 지우고
pygame.display.flip() # 그린 결과를 화면에 반영
clock.tick(60) # 초당 60회로 제한
pygame.quit()
실행하면 짙은 남색 창이 하나 뜹니다. 아무것도 없어 보이지만, 이 순간에도 루프는 초당 60번씩 돌면서 입력을 기다리고 있어요. 이 빈 창이 모든 게임의 출발점입니다.
💡
clock.tick(60)이 없으면 루프가 CPU 가 허락하는 최대 속도로 돌아서 팬이 굉음을 냅니다. 60은 초당 프레임 수(FPS) 상한이에요. 파랜드 택틱스 시절 게임들은 30fps 도 많았지만, 요즘은 60이 기본값입니다.
여기서 꼭 익혀야 할 사고방식이 하나 있어요. “그리기”는 매 프레임 처음부터 다시 한다는 것. 이전 프레임의 그림을 지우고(fill), 현재 상태를 기준으로 전부 다시 그립니다. “움직인 부분만 고쳐 그린다”가 아니라 “매번 백지에서 다시 그린다”가 기본이에요. 상태(데이터)와 그리기(렌더링)를 분리하는 이 습관이 뒤 편들의 토대가 됩니다.
4. 델타타임 — 컴퓨터가 달라도 같은 속도로
이제 화면에서 뭔가를 움직여봅시다. 그런데 여기서 초보 때 거의 모두가 밟는 함정이 있어요. 이렇게 쓰는 겁니다.
# ❌ 나쁜 예 — 프레임마다 5픽셀씩 이동
x += 5
이 코드는 프레임이 한 번 돌 때마다 5픽셀 움직여요. 60fps 컴퓨터에선 초당 300픽셀, 144fps 게이밍 모니터에선 초당 720픽셀. 같은 게임인데 컴퓨터에 따라 캐릭터 속도가 2배 넘게 달라집니다. 옛날 게임을 최신 PC 에서 돌리면 미친 속도로 빨라지는 게 바로 이 구조 때문이에요.
해결책이 델타타임(delta time, dt) 입니다. “지난 프레임에서 이번 프레임까지 걸린 시간(초)”을 재서, 이동량을 시간 기준으로 계산하는 거예요.
# ✅ 좋은 예 — 초당 300픽셀 이동 (프레임레이트 무관)
x += 300 * dt
60fps 면 dt ≈ 0.0167초라서 프레임당 5픽셀, 144fps 면 dt ≈ 0.0069초라서 프레임당 2.08픽셀. 어느 쪽이든 1초 뒤엔 정확히 300픽셀 이동해 있습니다.
pygame 에서 dt 는 clock.tick() 의 반환값으로 얻어요. 반환 단위가 밀리초라서 1000으로 나눠 초로 바꿉니다.
dt = clock.tick(60) / 1000 # 예: 60fps 면 약 0.016 이 나와요
print(f"{dt:.4f}초, 이번 프레임 이동량 = {300 * dt:.2f}px")
0.0166초, 이번 프레임 이동량 = 4.99px
⚠️ 앞으로 이 시리즈의 모든 움직임(캐릭터 이동, 애니메이션, 카메라)은 dt 기반으로 씁니다. “초당 얼마”로 생각하는 습관을 지금 들여두세요. 나중에 고치려면 코드 전체를 뒤져야 합니다.
5. 첫 실습 — 방향키로 움직이는 캐릭터
배운 걸 전부 합쳐서, 방향키로 움직이는 캐릭터를 만들어봅시다. 아직 스프라이트(그림)가 없으니 캐릭터는 사각형으로 대신해요. 3편에서 진짜 캐릭터로 바꿉니다.
한 가지 새로 등장하는 것은 pygame.key.get_pressed() 입니다. 이벤트 큐(event.get())는 “눌리는 순간”을 잡는 데 좋고, get_pressed() 는 “지금 눌려 있는가”를 매 프레임 확인하는 데 좋아요. 꾹 누르고 있는 동안 계속 움직여야 하는 이동에는 후자가 맞습니다.
import pygame
pygame.init()
screen = pygame.display.set_mode((960, 540))
pygame.display.set_caption("SRPG 선행학습 1편 — 움직이는 캐릭터")
clock = pygame.time.Clock()
# 캐릭터 상태 — 위치는 float 로 관리 (dt 곱하면 소수가 나오니까)
player_x, player_y = 480.0, 270.0
SPEED = 300 # 초당 300픽셀
SIZE = 32 # 캐릭터 크기(px)
running = True
while running:
dt = clock.tick(60) / 1000
# 1. 입력 처리
for event in pygame.event.get():
if event.type == pygame.QUIT:
running = False
keys = pygame.key.get_pressed()
dx = keys[pygame.K_RIGHT] - keys[pygame.K_LEFT] # -1, 0, 1
dy = keys[pygame.K_DOWN] - keys[pygame.K_UP]
# 2. 상태 업데이트
player_x += dx * SPEED * dt
player_y += dy * SPEED * dt
# 화면 밖으로 못 나가게 가두기
player_x = max(0, min(960 - SIZE, player_x))
player_y = max(0, min(540 - SIZE, player_y))
# 3. 화면 그리기
screen.fill((30, 30, 46))
pygame.draw.rect(screen, (94, 198, 214),
(int(player_x), int(player_y), SIZE, SIZE))
pygame.display.flip()
pygame.quit()
실행하고 방향키를 눌러보세요. 청록색 사각형이 부드럽게 움직이면 성공입니다. 대각선(→ 와 ↓ 동시)도 자연스럽게 되는데, dx/dy 를 각각 계산해서 더하는 구조 덕분이에요.
🎉 축하드려요. 방금 만든 이 100줄짜리 코드에 이미 입력 → 업데이트 → 렌더라는 모든 게임의 골격이 들어 있습니다. 파랜드 택틱스도, 요즘 나오는 대작도 이 골격 위에 살을 붙인 것뿐이에요.
작은 디테일 하나를 짚고 갈게요. 위치를 float 로 들고 있다가 그릴 때만 int() 로 바꿉니다. dt 를 곱하면 이동량이 4.99px 같은 소수가 되는데, 저장을 int 로 하면 소수점 아래가 매 프레임 버려져서 느린 이동이 아예 멈춰버리는 버그가 생겨요. 상태는 정밀하게(float), 그리기는 픽셀 단위로(int) — 이것도 앞으로 계속 쓰는 원칙입니다.
6. 정리 — 오늘 쌓은 기초 체력
체크리스트로 정리해볼게요.
- 파랜드 택틱스를 열 개 부품(루프·타일맵·스프라이트·길찾기·전투·데이터·맵에디터·UI·사운드·연출)으로 분해했다
- 원리 학습용 도구로 pygame 을 골랐고 설치했다
- 게임 루프의 3박자 — 입력 처리 → 상태 업데이트 → 화면 그리기
- 델타타임으로 프레임레이트와 무관하게 같은 속도를 내는 법
- 상태는 float, 그리기는 int — 방향키로 움직이는 첫 캐릭터 완성
다음 편에서는 이 빈 화면에 파랜드 택틱스의 상징인 마름모 타일 전장을 깝니다. 배열로 맵을 정의하고, 직교 좌표를 쿼터뷰(아이소메트릭) 화면 좌표로 바꾸는 수학을 다뤄요. 이 시리즈에서 제일 “아하!” 소리가 나오는 편입니다.
일단 오늘은 여기까지….. 다음 글에서는 타일맵과 쿼터뷰 좌표계를 정리해볼게요.