6 분 소요

🎮 2D SRPG 만들기 선행학습 6부작 — 파랜드 택틱스 같은 쿼터뷰 SRPG 를 직접 만들기 위한 기초 체력을 쌓습니다. 게임 루프부터 세이브/로드, 맵 에디터, UI, 사운드, 연출까지 편마다 실행되는 코드로 하나씩. 전체 10편.
  1. 게임 루프와 델타타임 — 모든 게임의 심장
  2. 타일맵과 쿼터뷰 좌표계 — 마름모 세상의 수학
  3. 스프라이트와 애니메이션 — 캐릭터에 생명 불어넣기
  4. 이동 범위와 A* 길찾기 — SRPG 의 파란 칸
  5. 턴제 전투와 상태머신 — 턴 큐·데미지·적 AI
  6. 데이터 설계와 세이브/로드 — 게임을 콘텐츠로 채우기
  7. Tiled 맵 에디터 — 배열 손편집 졸업하기
  8. UI 와 메뉴 — 정보를 보여주는 기술
  9. 사운드 — 타격감은 귀에서 나온다지금 글
  10. 연출 — 손맛을 만드는 잔기술

Summary

8편까지 만든 게임을 플레이해보면 화면은 제법 그럴듯한데 뭔가 허전합니다. 소리가 없어요. 공격이 명중해도, 메뉴를 눌러도, 턴이 바뀌어도 완전한 무음이죠. 게임의 체감에서 소리가 차지하는 비중은 생각보다 커서, 잘 만든 타격음 하나가 화면 연출보다 손맛을 더 크게 바꿉니다. 파랜드 택틱스의 검격음과 마법 영창음을 기억하신다면 무슨 말인지 아실 거예요.

다행히 사운드는 이 시리즈에서 가장 배우기 쉬운 부품입니다. pygame 의 mixer 모듈이 거의 다 해주거든요. 오늘은 BGM 과 효과음의 구조 차이, 초보가 반드시 밟는 지연(latency) 함정, 그리고 시리즈 전통대로 에셋 없이 코드로 효과음을 만드는 법까지 다룹니다.

💡 이 글에서 다루는 것

  • 소리의 두 종류 — 스트리밍되는 BGM 과 메모리에 올리는 효과음
  • 믹서 초기화 — 버퍼 크기가 만드는 “늦게 들리는 타격음” 함정
  • 에셋 없이 효과음 만들기 — 표준 라이브러리 wave 로 사인파 합성
  • 효과음 재생 원칙 — 소리는 상태 전이에 건다
  • BGM — 무한 루프, 페이드아웃, 전투/승리 전환
  • 무료 음원 구하는 곳 3곳 + 라이선스 확인 포인트



1. 소리의 두 종류 — BGM 과 효과음

pygame 의 오디오는 쓰임새에 따라 API 가 둘로 나뉘어 있습니다. 처음엔 헷갈리는데, 나뉜 이유를 알면 안 잊혀요.

  BGM (pygame.mixer.music) 효과음 (pygame.mixer.Sound)
재생 방식 디스크에서 스트리밍 메모리에 통째로 로드
동시 재생 한 곡만 기본 8채널 동시 재생
쓰임새 전장 음악, 승리 팡파레 타격음, 메뉴 클릭음
길이 몇 분짜리 0.05~1초짜리

BGM 은 3분짜리 파일을 메모리에 다 올리면 낭비라 조금씩 읽으며 재생하고, 그래서 동시에 한 곡뿐입니다. 효과음은 반대로 짧은 파일을 메모리에 올려두고 지연 없이, 여러 개를 겹쳐 터뜨려요. 적 세 명이 연달아 맞으면 타격음 세 개가 겹쳐 나야 하니까요.



2. 믹서 초기화 — 버퍼가 만드는 지연 함정

믹서는 pygame.init() 이 알아서 켜주지만, 기본 설정에 함정이 하나 있습니다. 오디오 버퍼가 커서 소리가 늦게 나요. 공격 모션과 타격음이 어긋나는 “싱크 밀림”의 주범입니다. 해결은 pre_init 으로 버퍼를 줄이는 것 — 반드시 pygame.init() 전에 불러야 해서 이름이 pre 입니다.

import pygame

pygame.mixer.pre_init(44100, -16, 2, 512)   # 샘플레이트, 16비트, 스테레오, 버퍼
pygame.init()
print(pygame.mixer.get_init(), "/ 채널", pygame.mixer.get_num_channels())
(44100, -16, 2) / 채널 8

버퍼 512 면 44100Hz 기준 약 12ms 지연이라 사람 귀에 즉각으로 느껴집니다. 기본값(환경에 따라 2048~4096)은 50~90ms 라 확실히 “늦게” 들려요.

⚠️ 버퍼를 무작정 줄이면(128 이하) 소리가 지글거리거나 끊길 수 있습니다. 512 가 게임용 표준 절충값이에요. 지글거림이 들리면 1024 로 올려보세요.



3. 에셋 없이 효과음 만들기 — 사인파 합성

효과음 파일이 없어도 시리즈 전통대로 코드로 만들 수 있습니다. 소리의 정체는 결국 초당 수만 개의 진폭 숫자라서, 사인파에 감쇠를 곱해 WAV 로 저장하면 그럴듯한 레트로 효과음이 나와요. 표준 라이브러리 wave 만 씁니다.

import wave, struct, math

def make_sound(path, freq=220, duration=0.15, volume=0.6, rate=22050):
    n = int(rate * duration)
    with wave.open(path, "w") as w:
        w.setnchannels(1)                          # 모노
        w.setsampwidth(2)                          # 16비트
        w.setframerate(rate)
        frames = bytearray()
        for i in range(n):
            t = i / rate
            decay = math.exp(-t * 18)              # 지수 감쇠 — 타격의 '탁' 느낌
            sample = volume * decay * math.sin(2 * math.pi * freq * t)
            frames += struct.pack("<h", int(sample * 32767))
        w.writeframes(frames)

make_sound("hit.wav", freq=180)                    # 낮고 둔탁한 타격음
make_sound("select.wav", freq=880, duration=0.06, volume=0.3)   # 높고 짧은 선택음

import os
print(os.path.getsize("hit.wav"), "bytes")
6658 bytes

7KB 도 안 되는 파일이지만 재생해보면 분명히 “탁” 소리가 납니다. 핵심은 두 파라미터예요. 주파수(freq) 가 낮으면 둔탁하고 높으면 쨍하며, 감쇠 속도(exp(-t * 18) 의 18)가 클수록 짧게 끊깁니다. 용도별 추천값을 정리하면:

용도 freq duration 느낌
타격음 150~220 0.15 둔탁한 “탁”
메뉴 선택음 700~1000 0.06 가벼운 “삑”
크리티컬 100 + 300 두 번 겹치기 0.25 묵직한 “쿵”
회복음 520 → 780 연속 재생 0.1 × 2 밝은 “띠링”

진짜 게임용 효과음을 만들고 싶어지면 jsfxr 같은 브라우저 레트로 효과음 생성기를 추천드려요. 슬라이더를 굴리다 마음에 드는 소리를 WAV 로 내려받으면 됩니다.



4. 효과음 재생 — 소리는 상태 전이에 건다

만든 WAV 는 Sound 로 올려서 play() 하면 끝입니다.

hit = pygame.mixer.Sound("hit.wav")
print(round(hit.get_length(), 2), "초, 볼륨", hit.get_volume())
hit.set_volume(0.7)
print(hit.get_volume())
0.15 초, 볼륨 1.0
0.6953125

set_volume(0.7) 을 넣었는데 0.6953 이 나온 게 버그가 아니에요 — 내부적으로 볼륨이 128단계로 양자화돼서 가장 가까운 눈금에 앉습니다. 귀로는 구분 안 되니 신경 쓰지 않아도 됩니다.

재생보다 중요한 건 어디서 재생하느냐입니다. 원칙은 하나예요.

소리는 상태 전이와 이벤트 확정 순간에 건다. 5편 상태머신의 전이 지점과 8편의 확정 지점이 곧 사운드 훅입니다 — 메뉴가 열릴 때(ACTION 진입), 항목을 고를 때, 공격이 확정될 때(try_attack 안), 턴이 바뀔 때. 반대로 매 프레임 도는 update/draw 안에서 play() 를 부르면 초당 60번 겹쳐 재생되는 소음 폭탄이 됩니다.

8편 코드 기준으로 연결 지점을 짚으면 이렇습니다.

SFX = {"hit": pygame.mixer.Sound("hit.wav"),
       "select": pygame.mixer.Sound("select.wav")}

# 8편의 확정 지점들에 한 줄씩
# ACTION 메뉴 항목 클릭 시     → SFX["select"].play()
# TARGETING 공격 확정 시       → SFX["hit"].play()      (add_popup 바로 옆)
# 턴 교대(ENEMY_TURN 진입) 시  → SFX["select"].play()

데미지 팝업(add_popup)과 타격음이 같은 줄에서 나가는 게 포인트예요. 눈과 귀가 같은 순간을 가리켜야 타격감이 삽니다.



5. BGM — 깔고, 반복하고, 페이드아웃

BGM 은 mixer.music 으로 다룹니다. API 가 단출해서 게임에 필요한 건 사실상 네 줄이에요.

pygame.mixer.music.load("battle_theme.ogg")
pygame.mixer.music.set_volume(0.4)      # BGM 은 효과음보다 낮게
pygame.mixer.music.play(-1)             # -1 = 무한 반복
# ... 전투 종료 시 ...
pygame.mixer.music.fadeout(1500)        # 1.5초에 걸쳐 서서히 종료

play(-1) 의 -1 이 무한 루프입니다. 전투가 끝나면 뚝 끊지 말고 fadeout 으로 잦아들게 한 뒤 승리 팡파레를 play() 하면, 그 흐름만으로 고전 SRPG 의 전투 종료 연출이 완성돼요. BGM 전환도 상태 전이에 거는 겁니다 — VICTORY 상태 진입이 곧 팡파레 재생 지점이에요.

💡 BGM 볼륨은 효과음보다 확실히 낮게(0.3~0.5) 잡는 게 정석입니다. BGM 이 크면 타격음이 묻혀서, 애써 만든 타격감이 사라져요.

음원은 어디서 구하나

BGM 은 합성으로 만들기 어려우니 무료 음원을 씁니다. 3편의 그림 에셋과 같은 요령이고, 라이선스 먼저 확인하는 것도 같아요.

사이트 특징 주의점
Kenney.nl 효과음 팩 CC0, 출처 표기도 불필요 BGM 은 적음
OpenGameArt.org 게임용 BGM·효과음 방대 트랙마다 라이선스 제각각
freesound.org 현실 녹음 효과음의 보고 CC-BY 가 많아 출처 표기 필요

🚨 BGM 은 특히 라이선스 사고가 잦은 영역입니다. “무료 다운로드”와 “게임에 넣어 배포 가능”은 다른 말이에요. CC0 또는 CC-BY 트랙만 쓰고, CC-BY 는 크레딧 화면에 출처를 적어두는 습관을 들이세요.



6. 실습 — 무음 전투에 소리 입히기

8편 미니 전투에 이번 편을 얹는 조립도입니다. 새 함수는 없고, 초기화 두 줄과 기존 확정 지점의 play() 한 줄씩이 전부예요.

import pygame, wave, struct, math

pygame.mixer.pre_init(44100, -16, 2, 512)      # ① init 전에 버퍼 축소
pygame.init()
screen = pygame.display.set_mode((960, 540))
clock = pygame.time.Clock()

# make_sound                                    ← 본문 3장 그대로
# 8편의 전투/UI 코드 전부                         ← 8편 그대로

make_sound("hit.wav", freq=180)
make_sound("select.wav", freq=880, duration=0.06, volume=0.3)
SFX = {k: pygame.mixer.Sound(f"{k}.wav") for k in ("hit", "select")}
SFX["hit"].set_volume(0.7)

# ② 8편 코드에서 딱 세 곳만 수정:
#   - ACTION 메뉴 클릭 처리 안에            SFX["select"].play()
#   - TARGETING 공격 확정(add_popup 옆)에   SFX["hit"].play()
#   - ENEMY_TURN 의 try_attack 안에도       SFX["hit"].play()

실행해서 공격을 확정하는 순간 — 데미지 숫자가 떠오르면서 “탁” 소리가 같이 터집니다. 코드로 합성한 7KB 짜리 소리인데도 손맛이 확 달라지는 게 느껴질 거예요. 소리가 없던 게임에 처음 소리를 넣는 순간은 마름모 격자가 처음 떴을 때만큼 짜릿합니다.



7. 정리

  • BGM 은 스트리밍(한 곡), 효과음은 메모리(8채널 동시) — 나뉜 이유부터 이해
  • pre_init 버퍼 512 — 타격음 싱크 밀림은 버퍼 탓
  • 효과음은 사인파 × 지수 감쇠로 합성 가능 — freq 와 감쇠가 소리의 성격
  • 소리는 상태 전이·확정 순간에만 — update/draw 안의 play() 는 소음 폭탄
  • 팝업과 타격음은 같은 줄에서 — 눈과 귀가 같은 순간을 가리키게
  • BGM 볼륨은 효과음보다 낮게, 라이선스는 배포 가능 여부까지 확인

눈에 정보가 보이고 귀에 소리가 들리니 게임이 제법 완성형에 가까워졌어요. 마지막 남은 한 끗이 연출입니다. 크리티컬이 터졌는데 화면이 미동도 없으면 아깝잖아요 — 화면이 흔들리고, 순간 멈추고, 부드럽게 넘어가는 잔기술들이 손맛을 완성합니다.

일단 오늘은 여기까지….. 다음 글에서는 로드맵의 4번, 연출을 정리해볼게요.


← 이전 글: (8/10) 2D SRPG 만들기 선행학습 — UI 와 메뉴, 정보를 보여주는 기술다음 글 →: (10/10) 2D SRPG 만들기 선행학습 — 연출, 손맛을 만드는 잔기술