(3/10) 2D SRPG 만들기 선행학습 — 스프라이트와 애니메이션
Summary
2편에서 마름모 전장을 깔았으니, 이제 그 위에 설 캐릭터를 만들 차례예요. 파랜드 택틱스의 캐릭터들은 가만히 있어도 숨을 쉬고, 이동할 땐 방향에 맞는 걷기 모션을 보여줍니다. 오늘은 그 원리인 스프라이트와 애니메이션을 다룹니다.
미리 결론을 말하면, 2D 애니메이션은 여러 장의 그림을 시간에 맞춰 갈아끼우는 것이 전부예요. 그림을 어떻게 준비하고(스프라이트시트), 어떻게 자르고(슬라이싱), 언제 갈아끼울지(프레임 타이밍)만 알면 됩니다. 그림 실력이 없어도 괜찮아요 — 무료 에셋을 쓰는 법과, 에셋 없이 코드만으로 임시 캐릭터를 만드는 법을 둘 다 준비했습니다.
💡 이 글에서 다루는 것
- Surface 와 blit — pygame 이 그림을 다루는 방식
- 스프라이트시트 슬라이싱 — 한 장의 시트를 프레임 배열로 자르기
- 무료 에셋 구하는 곳 3곳 + 라이선스 확인 포인트
- 델타타임 기반 애니메이터 — fps 를 바꿔도 깨지지 않는 구조
- 발밑 기준점(anchor) — 캐릭터를 마름모 타일 위에 정확히 세우는 법
- 실습 — 4방향 걷기 애니메이션으로 전장을 걸어다니는 캐릭터
1. Surface 와 blit — pygame 의 그림 모델
pygame 에서 모든 그림은 Surface 라는 사각형 픽셀 덩어리입니다. 화면(screen)도 Surface 고, 불러온 이미지도 Surface 예요. 한 Surface 를 다른 Surface 위에 복사해 붙이는 동작이 blit 입니다.
sprite = pygame.image.load("hero.png").convert_alpha() # 파일 → Surface
screen.blit(sprite, (100, 80)) # (100, 80) 위치에 붙이기
여기서 convert_alpha() 를 빼먹으면 안 됩니다. 이미지를 화면 포맷에 미리 맞춰두는 함수인데, 이거 없이 매 프레임 blit 하면 픽셀 포맷 변환이 매번 일어나서 렌더링이 몇 배로 느려져요. 투명 배경(PNG 의 알파 채널)을 살리려면 convert() 가 아니라 convert_alpha() 쪽입니다.
✅ 이미지 로드 직후엔 무조건
convert_alpha()— 이유를 몰라도 일단 습관으로. 나중에 캐릭터 20명이 돌아다녀도 프레임이 안 떨어지는 비결이에요.
2. 스프라이트시트 — 한 장에 모든 동작을
캐릭터 그림을 프레임마다 파일 하나씩 두면 파일이 수백 개가 됩니다. 그래서 게임은 스프라이트시트 라는 큰 이미지 한 장에 모든 프레임을 격자로 배열해두고, 코드에서 잘라 씁니다. 보통 이렇게 배열해요.
┌────┬────┬────┬────┐
│ ↓0 │ ↓1 │ ↓2 │ ↓3 │ ← 1행: 아래 방향 걷기 4프레임
├────┼────┼────┼────┤
│ ←0 │ ←1 │ ←2 │ ←3 │ ← 2행: 왼쪽
├────┼────┼────┼────┤
│ →0 │ →1 │ →2 │ →3 │ ← 3행: 오른쪽
├────┼────┼────┼────┤
│ ↑0 │ ↑1 │ ↑2 │ ↑3 │ ← 4행: 위
└────┴────┴────┴────┘
자르기는 subsurface 로 합니다. 원본에서 사각형 영역만 참조하는 가벼운 Surface 를 만들어줘요.
def slice_sheet(sheet, frame_w, frame_h):
cols = sheet.get_width() // frame_w
rows = sheet.get_height() // frame_h
frames = []
for r in range(rows):
row = [sheet.subsurface((c * frame_w, r * frame_h, frame_w, frame_h))
for c in range(cols)]
frames.append(row)
return frames # frames[행][열] = Surface
# 128x128 시트를 32x32 프레임으로 자르면
sheet = pygame.image.load("hero_sheet.png").convert_alpha()
frames = slice_sheet(sheet, 32, 32)
print(len(frames), "행 x", len(frames[0]), "열")
4 행 x 4 열
frames[0] 이 아래 방향 4프레임, frames[3] 이 위 방향 4프레임 — 이런 식으로 행 인덱스가 방향, 열 인덱스가 애니메이션 프레임이 됩니다.
그림은 어디서 구하나
직접 도트를 찍지 않아도 연습용 에셋은 충분히 구할 수 있어요.
| 사이트 | 특징 | 주의점 |
|---|---|---|
| Kenney.nl | 전부 CC0(퍼블릭 도메인), 품질 균일 | 픽셀아트보단 깔끔한 벡터풍 |
| OpenGameArt.org | RPG 스프라이트시트 방대 | 에셋마다 라이선스 제각각 |
| itch.io | 인디 감성 픽셀아트 다수 | 무료여도 상업 이용 조건 확인 |
🚨 에셋은 다운로드 전에 라이선스부터 확인하세요. “개인 학습 OK, 상업 불가” 인 에셋으로 게임을 완성한 뒤에 알게 되면 그림을 전부 갈아엎어야 합니다. CC0 또는 CC-BY(출처 표기) 위주로 모으는 걸 추천드려요.
3. 에셋 없이 따라하기 — 코드로 만드는 임시 캐릭터
이 글을 실습할 때 에셋 다운로드가 귀찮다면, 코드로 스프라이트시트를 즉석 생성해버리면 됩니다. 빈 Surface 에 도형을 그려 넣는 방식인데, 실전에서도 에셋이 나오기 전에 임시 그림으로 개발을 진행하는 것은 아주 흔한 일이에요. 프로그래머 아트라고 부릅니다.
def make_placeholder_sheet(frame=32):
"""4방향 x 2프레임짜리 임시 캐릭터 시트를 만들어요."""
sheet = pygame.Surface((frame * 2, frame * 4), pygame.SRCALPHA)
body = {0: (94, 198, 214), 1: (94, 198, 214),
2: (94, 198, 214), 3: (94, 198, 214)}
eye_dx = {0: 0, 1: -5, 2: 5, 3: 0} # 방향별 눈 위치: 아래/왼/오른/위
for row in range(4): # 방향
for col in range(2): # 프레임 (0=서기, 1=걷기)
x0, y0 = col * frame, row * frame
bob = 2 if col == 1 else 0 # 걷기 프레임은 살짝 들썩
pygame.draw.rect(sheet, body[row],
(x0 + 10, y0 + 12 - bob, 12, 14)) # 몸통
pygame.draw.circle(sheet, (240, 220, 180),
(x0 + 16, y0 + 9 - bob), 6) # 머리
if row != 3: # 위 방향은 뒤통수
pygame.draw.circle(sheet, (30, 30, 46),
(x0 + 16 + eye_dx[row], y0 + 8 - bob), 2)
return sheet
sheet = make_placeholder_sheet()
print(sheet.get_size())
(64, 128)
64×128 시트가 나왔으니, 2장의 slice_sheet(sheet, 32, 32) 로 자르면 방향 4행 × 프레임 2열이 그대로 나와요. 나중에 진짜 에셋을 구하면 이 함수 한 줄만 pygame.image.load() 로 바꾸면 되고, 나머지 코드는 하나도 안 건드립니다. 이렇게 그림과 로직을 분리해두는 게 핵심이에요.
4. 애니메이터 — 시간이 프레임을 넘긴다
애니메이션의 원리는 책장 넘기기와 같습니다. 누적 시간을 재고, 일정 시간마다 다음 프레임으로 넘기면 돼요. 1편에서 배운 델타타임이 여기서 다시 등장합니다.
class Animator:
def __init__(self, fps=6):
self.fps = fps # 초당 프레임 전환 횟수
self.t = 0.0 # 누적 시간
def update(self, dt):
self.t += dt
def frame_index(self, n_frames):
return int(self.t * self.fps) % n_frames
# 6fps 애니메이터로 2프레임짜리 걷기를 돌리면
anim = Animator(fps=6)
for step in range(5):
anim.update(0.1) # 0.1초씩 5번 흐른다고 가정
print(f"{anim.t:.1f}초 → 프레임 {anim.frame_index(2)}")
0.1초 → 프레임 0
0.2초 → 프레임 1
0.3초 → 프레임 1
0.4초 → 프레임 0
0.5초 → 프레임 1
int(누적시간 × fps) % 프레임수 — 이 한 줄이 애니메이션의 심장입니다. 나머지 연산(%) 덕분에 마지막 프레임 다음엔 자동으로 첫 프레임으로 돌아가서 무한 반복돼요. 걷기는 6fps, 공격은 10fps 처럼 동작마다 속도를 다르게 줄 수도 있습니다.
💡 “게임은 60fps 인데 애니메이션은 6fps?” — 다른 개념이에요. 화면은 초당 60번 그리지만, 캐릭터 그림은 초당 6번만 갈아끼우는 겁니다. 도트 게임 특유의 착착 끊기는 감성이 바로 이 낮은 애니메이션 fps 에서 나와요.
5. 발밑 기준점 — 마름모 위에 정확히 세우기
캐릭터를 타일 위에 그릴 때 흔한 실수가, 스프라이트의 왼쪽 위 모서리를 타일 좌표에 맞추는 거예요. 그러면 캐릭터가 타일에서 붕 떠 보입니다. 캐릭터는 발밑 중앙이 마름모의 중앙에 오도록 맞춰야 해요.
def draw_unit(screen, sprite, gx, gy):
sx, sy = grid_to_screen(gx, gy) # 마름모 꼭대기 꼭짓점 (2편)
cx = sx # 마름모 중앙 x = 꼭대기와 같음
cy = sy + TILE_H // 2 # 마름모 중앙 y
w, h = sprite.get_size()
screen.blit(sprite, (cx - w // 2, cy - h + 6)) # 발밑을 중앙에, 6px 보정
cx - w // 2 로 가로 중앙을 맞추고, cy - h 로 그림의 바닥이 타일 중앙에 닿게 합니다. 마지막 +6 같은 미세 보정은 스프라이트마다 발 위치가 달라서 눈으로 보면서 잡는 값이에요. 이 기준점 개념은 나중에 키가 큰 캐릭터, 큰 몬스터를 얹을 때도 그대로 확장됩니다.
6. 실습 — 전장을 걸어다니는 캐릭터
2편의 쿼터뷰 전장 위에서, 방향키로 걸어다니는 캐릭터를 완성해봅시다. 이동 중일 때만 걷기 애니메이션이 돌고, 멈추면 서기 프레임으로 돌아옵니다.
import pygame
pygame.init()
screen = pygame.display.set_mode((960, 540))
pygame.display.set_caption("SRPG 선행학습 3편 — 걸어다니는 캐릭터")
clock = pygame.time.Clock()
ROWS = COLS = 8
TILE_W, TILE_H = 64, 32
ORIGIN_X, ORIGIN_Y = 480, 120
def grid_to_screen(gx, gy):
return ((gx - gy) * (TILE_W // 2) + ORIGIN_X,
(gx + gy) * (TILE_H // 2) + ORIGIN_Y)
def draw_tile(gx, gy, color):
sx, sy = grid_to_screen(gx, gy)
pts = [(sx, sy), (sx + TILE_W // 2, sy + TILE_H // 2),
(sx, sy + TILE_H), (sx - TILE_W // 2, sy + TILE_H // 2)]
pygame.draw.polygon(screen, color, pts)
pygame.draw.polygon(screen, (30, 30, 46), pts, 1)
# --- 3장의 임시 시트 + 2장의 슬라이서 (본문 함수 그대로) ---
def make_placeholder_sheet(frame=32):
sheet = pygame.Surface((frame * 2, frame * 4), pygame.SRCALPHA)
eye_dx = {0: 0, 1: -5, 2: 5, 3: 0}
for row in range(4):
for col in range(2):
x0, y0 = col * frame, row * frame
bob = 2 if col == 1 else 0
pygame.draw.rect(sheet, (94, 198, 214),
(x0 + 10, y0 + 12 - bob, 12, 14))
pygame.draw.circle(sheet, (240, 220, 180),
(x0 + 16, y0 + 9 - bob), 6)
if row != 3:
pygame.draw.circle(sheet, (30, 30, 46),
(x0 + 16 + eye_dx[row], y0 + 8 - bob), 2)
return sheet
def slice_sheet(sheet, fw, fh):
return [[sheet.subsurface((c * fw, r * fh, fw, fh))
for c in range(sheet.get_width() // fw)]
for r in range(sheet.get_height() // fh)]
frames = slice_sheet(make_placeholder_sheet(), 32, 32)
DIR_DOWN, DIR_LEFT, DIR_RIGHT, DIR_UP = 0, 1, 2, 3
class Animator:
def __init__(self, fps=6):
self.fps, self.t = fps, 0.0
def update(self, dt):
self.t += dt
def frame_index(self, n):
return int(self.t * self.fps) % n
# 캐릭터 상태 — 픽셀 좌표(마름모 중앙 기준)로 자유 이동
px, py = grid_to_screen(3, 3)
py += TILE_H // 2
direction, moving = DIR_DOWN, False
anim = Animator(fps=6)
running = True
while running:
dt = clock.tick(60) / 1000
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]
dy = keys[pygame.K_DOWN] - keys[pygame.K_UP]
moving = (dx != 0 or dy != 0)
if moving:
px += dx * 150 * dt
py += dy * 150 * dt
if dx > 0: direction = DIR_RIGHT
elif dx < 0: direction = DIR_LEFT
elif dy > 0: direction = DIR_DOWN
else: direction = DIR_UP
anim.update(dt)
frame_col = anim.frame_index(2) if moving else 0 # 멈추면 서기 프레임
sprite = frames[direction][frame_col]
screen.fill((30, 30, 46))
for gy in range(ROWS):
for gx in range(COLS):
draw_tile(gx, gy, (106, 168, 79))
w, h = sprite.get_size()
screen.blit(sprite, (int(px) - w // 2, int(py) - h + 6))
pygame.display.flip()
pygame.quit()
방향키를 누르면 캐릭터가 그 방향을 보며 들썩들썩 걷고, 손을 떼면 멈춰 섭니다. 2프레임짜리 엉성한 캐릭터지만 구조는 상용 게임과 동일해요 — 시트의 프레임 수를 4장, 8장으로 늘리고 그림만 갈아끼우면 그대로 부드러워집니다.
🎉 여기까지 하면 “전장 + 그 위에 살아있는 캐릭터”가 생겼습니다. 그런데 지금 캐릭터는 픽셀 단위로 자유 이동을 하죠. SRPG 는 타일 단위로, 정해진 범위 안에서만 움직이는 게임이에요. 그걸 만드는 게 다음 편입니다.
7. 정리
- Surface + blit +
convert_alpha()— pygame 그림의 3요소 - 스프라이트시트를
subsurface로 잘라frames[방향][프레임]배열로 - 무료 에셋은 Kenney/OpenGameArt/itch.io, 라이선스 먼저 확인
- 애니메이션 =
int(누적시간 x fps) % 프레임수한 줄 - 캐릭터는 발밑 중앙을 마름모 중앙에 — 기준점(anchor) 개념
일단 오늘은 여기까지….. 다음 글에서는 SRPG 의 정체성인 이동 범위 계산과 A* 길찾기를 정리해볼게요.
← 이전 글: (2/10) 2D SRPG 만들기 선행학습 — 타일맵과 쿼터뷰 좌표계 | 다음 글 →: (4/10) 2D SRPG 만들기 선행학습 — 이동 범위와 A* 길찾기