
파이썬 지렁이(Snake) 게임 만들기 — 한글 OK · 스코어/최고점 · 랜덤 스킨 · 사운드
Pygame으로 클래식 지렁이 게임을 한 파일로 구현합니다. 한글 폰트 깨짐 방지, 점수·최고점 저장, 랜덤 스킨, 간단 사운드 이펙트까지 모두 포함했어요. 아래 가이드를 따라가면 설치 → 실행 → 커스터마이즈까지 바로 끝낼 수 있습니다.
목차
개요 & 목표


본 포스트는 Pygame을 사용해 지렁이(Snake) 게임을 만드는 과정을 정리했습니다. 단순한 완성본이 아니라, 블로그/수업/스터디에서 재사용하기 쉽도록 한글 폰트, 스코어/최고점, 랜덤 스킨, 사운드 등 “필수+재미” 요소를 기본 탑재했습니다.
핵심 기능 요약
- 한글 렌더링 자동 선택: Windows/맥/리눅스에서 가능한 한 적합한 한글 폰트를 자동 선택
- 점수 & 최고점 저장:
highscore.txt
로 최고점 자동 관리 - 랜덤 스킨: 시작 시 머리/몸/먹이 색상 랜덤, 가끔 먹을 때도 색 변환
- 사운드 이펙트: 먹기/일시정지/게임오버 음향 (외부 파일 없이 PCM 생성)
- 난이도 상승: 일정 점수마다 속도 증가
- 벽 옵션: 벽 충돌 게임오버(ON) vs 화면 반대편으로 이동(OFF)
개발 환경 준비
필수 설치
pip install pygame
참고: 일부 리눅스는 SDL 관련 패키지가 추가로 필요할 수 있습니다.
파일 이름
아래 “전체 코드”를 그대로 복사해 snake_kor.py
로 저장하세요.
전체 코드
# -*- coding: utf-8 -*-
# Snake (지렁이) - 한글 폰트/스코어/최고점/랜덤스킨/사운드 포함 단일 파일
# pip install pygame
import pygame
import random
import sys
import os
import math
import struct
from typing import List, Tuple, Optional
# ====== 전역 설정 ======
WIDTH, HEIGHT = 640, 480 # 창 크기
CELL = 20 # 격자 크기
GRID_W, GRID_H = WIDTH // CELL, HEIGHT // CELL
WALLS = True # True: 벽충돌=게임오버 / False: 반대편으로 랩어라운드
SHOW_GRID = False # 격자 표시
START_SPEED = 10 # 시작 FPS
SPEED_STEP_EVERY = 5 # n점마다 속도 +1
# 한글 폰트 경로 (원하면 직접 경로 지정) 예: r"C:\Windows\Fonts\malgun.ttf"
FONT_PATH: Optional[str] = None
# 폰트 이름 후보 (시스템 폰트에서 검색)
KOREAN_FONT_CANDIDATES = [
"malgungothic", "맑은 고딕",
"NanumGothic", "나눔고딕",
"AppleGothic", # macOS
"Noto Sans CJK KR", "Noto Sans KR"
]
BG_COLOR = (18, 18, 22) # 배경
GRID_COLOR = (40, 40, 48)
INFO_COLOR = (160, 160, 170)
TEXT_COLOR = (235, 235, 235)
# 방향 벡터
UP, DOWN, LEFT, RIGHT = (0, -1), (0, 1), (-1, 0), (1, 0)
# ====== 유틸 ======
def pick_korean_font(size: int) -> pygame.font.Font:
"""한글 렌더링 가능한 폰트를 최대한 자동으로 선택."""
if FONT_PATH and os.path.exists(FONT_PATH):
return pygame.font.Font(FONT_PATH, size)
# 시스템 폰트 탐색
for name in KOREAN_FONT_CANDIDATES:
try:
f = pygame.font.SysFont(name, size)
# 간단 확인: 한글 문자열 렌더가 되는지 테스트
test = f.render("가나다 한글 테스트", True, (255, 255, 255))
if test.get_width() > 0:
return f
except Exception:
continue
# 최후의 보루 (한글이 깨질 수 있음)
return pygame.font.Font(None, size)
def render_text_center(surf, text: str, size: int, color, center):
font = pick_korean_font(size)
s = font.render(text, True, color)
rect = s.get_rect(center=center)
surf.blit(s, rect)
def draw_grid(surf):
for x in range(0, WIDTH, CELL):
pygame.draw.line(surf, GRID_COLOR, (x, 0), (x, HEIGHT))
for y in range(0, HEIGHT, CELL):
pygame.draw.line(surf, GRID_COLOR, (0, y), (WIDTH, y))
def clamp_wrap(pos: Tuple[int,int]) -> Tuple[int,int]:
x, y = pos
x %= GRID_W
y %= GRID_H
return (x, y)
def random_empty_cell(snake: List[Tuple[int,int]]) -> Tuple[int,int]:
while True:
p = (random.randrange(GRID_W), random.randrange(GRID_H))
if p not in snake:
return p
def draw_cell(surf, pos, color):
x, y = pos
pygame.draw.rect(surf, color, (x*CELL, y*CELL, CELL, CELL), border_radius=4)
def load_high_score(path="highscore.txt") -> int:
try:
with open(path, "r", encoding="utf-8") as f:
return int(f.read().strip() or "0")
except Exception:
return 0
def save_high_score(score: int, path="highscore.txt"):
try:
with open(path, "w", encoding="utf-8") as f:
f.write(str(score))
except Exception:
pass
# ====== 사운드 (외부 파일 없이 톤 생성) ======
def init_mixer():
try:
# 44.1kHz, 16bit signed, mono
pygame.mixer.init(frequency=44100, size=-16, channels=1, buffer=512)
return True
except Exception:
return False
def make_tone(freq=880, ms=120, volume=0.3) -> Optional[pygame.mixer.Sound]:
"""사인파 톤 생성 (numpy 없이 raw PCM 생성)."""
if not pygame.mixer.get_init():
return None
sample_rate = 44100
length = int(sample_rate * (ms / 1000.0))
amp = int(32767 * max(0.0, min(1.0, volume)))
frames = bytearray()
for i in range(length):
t = i / sample_rate
val = int(amp * math.sin(2 * math.pi * freq * t))
frames += struct.pack("<h", val) # 16-bit little-endian
try:
sound = pygame.mixer.Sound(buffer=frames)
return sound
except Exception:
return None
# ====== 스킨 팔레트 ======
def random_skin():
"""머리/몸/먹이 색상 랜덤 팔레트 생성."""
# 기본 팔레트 후보 + 완전 랜덤 혼합
palettes = [
((60, 200, 255), (0, 170, 120), (250, 80, 60)), # 기본
((255, 90, 90), (220, 40, 40), (255, 210, 70)), # 레드
((120, 220, 120), (60, 180, 60), (255, 120, 0)), # 그린
((255, 160, 30), (230, 120, 20), (120, 200, 255)), # 오렌지/블루
((200, 120, 255), (140, 70, 220), (255, 120, 140)), # 퍼플/핑크
]
if random.random() < 0.7:
return random.choice(palettes)
# 30% 확률로 완전 랜덤 팔레트
def rc():
return (random.randint(40,255), random.randint(40,255), random.randint(40,255))
return (rc(), rc(), rc())
# ====== 메인 ======
def main():
pygame.init()
screen = pygame.display.set_mode((WIDTH, HEIGHT))
pygame.display.set_caption("지렁이 게임 (Snake) - 한글/스코어/사운드/스킨")
clock = pygame.time.Clock()
# 폰트 사전 준비(상단 정보창용)
info_font = pick_korean_font(18)
big_font = pick_korean_font(56)
mid_font = pick_korean_font(24)
# 사운드 초기화 및 기본 효과음
sound_ok = init_mixer()
s_eat = make_tone(freq=990, ms=90, volume=0.35) if sound_ok else None
s_over = make_tone(freq=200, ms=300, volume=0.4) if sound_ok else None
s_pause = make_tone(freq=660, ms=70, volume=0.25) if sound_ok else None
high_score = load_high_score()
def new_game():
snake = [(GRID_W//2, GRID_H//2)]
direction = RIGHT
food = random_empty_cell(snake)
score = 0
speed = START_SPEED
paused = False
head_color, body_color, food_color = random_skin()
return snake, direction, food, score, speed, paused, head_color, body_color, food_color
snake, direction, food, score, speed, paused, HEAD, BODY, FOOD = new_game()
pending_dir = direction
game_over = False
running = True
while running:
# ===== 입력 처리 =====
for event in pygame.event.get():
if event.type == pygame.QUIT:
running = False
elif event.type == pygame.KEYDOWN:
key = event.key
if key in (pygame.K_ESCAPE, pygame.K_q):
running = False
if game_over:
if key == pygame.K_r:
snake, direction, food, score, speed, paused, HEAD, BODY, FOOD = new_game()
pending_dir = direction
game_over = False
continue
if key in (pygame.K_UP, pygame.K_w) and direction != DOWN:
pending_dir = UP
elif key in (pygame.K_DOWN, pygame.K_s) and direction != UP:
pending_dir = DOWN
elif key in (pygame.K_LEFT, pygame.K_a) and direction != RIGHT:
pending_dir = LEFT
elif key in (pygame.K_RIGHT, pygame.K_d) and direction != LEFT:
pending_dir = RIGHT
elif key == pygame.K_p:
paused = not paused
if s_pause: s_pause.play()
elif key == pygame.K_r:
snake, direction, food, score, speed, paused, HEAD, BODY, FOOD = new_game()
pending_dir = direction
# ===== 업데이트 =====
if not game_over and not paused:
direction = pending_dir
hx, hy = snake[0]
dx, dy = direction
new_head = (hx + dx, hy + dy)
if WALLS:
if not (0 <= new_head[0] < GRID_W and 0 <= new_head[1] < GRID_H):
game_over = True
else:
new_head = clamp_wrap(new_head)
if not game_over:
if new_head in snake:
game_over = True
else:
snake.insert(0, new_head)
if new_head == food:
score += 1
food = random_empty_cell(snake)
# 속도 증가
if score % SPEED_STEP_EVERY == 0:
speed += 1
if s_eat: s_eat.play()
# 가끔 스킨을 살짝 섞어주기(먹을 때 색상 변화 효과)
if random.random() < 0.2:
HEAD, BODY, _ = random_skin()
else:
snake.pop()
# 최고점 갱신/사운드
if game_over:
if s_over: s_over.play()
if score > high_score:
high_score = score
save_high_score(high_score)
# ===== 그리기 =====
screen.fill(BG_COLOR)
if SHOW_GRID:
draw_grid(screen)
# 먹이
draw_cell(screen, food, FOOD)
# 뱀
for i, p in enumerate(snake):
draw_cell(screen, p, HEAD if i == 0 else BODY)
# 상단 정보줄
info_text = f"점수: {score} 최고점: {high_score} 속도: {speed} 벽: {'ON' if WALLS else 'OFF'} P:일시정지 R:재시작 Q/Esc:종료"
info_surf = info_font.render(info_text, True, INFO_COLOR)
screen.blit(info_surf, (10, 8))
if paused and not game_over:
render_text_center(screen, "일시정지", 48, TEXT_COLOR, (WIDTH//2, HEIGHT//2))
if game_over:
overlay = pygame.Surface((WIDTH, HEIGHT), pygame.SRCALPHA)
overlay.fill((0, 0, 0, 130))
screen.blit(overlay, (0, 0))
render_text_center(screen, "게임 오버", 56, TEXT_COLOR, (WIDTH//2, HEIGHT//2 - 40))
render_text_center(screen, f"이번 점수: {score} 최고점: {high_score}", 26, (230, 210, 210), (WIDTH//2, HEIGHT//2 + 5))
render_text_center(screen, "R: 다시 시작 Q/Esc: 종료", 22, INFO_COLOR, (WIDTH//2, HEIGHT//2 + 40))
pygame.display.flip()
clock.tick(speed if not game_over else 30)
pygame.quit()
sys.exit()
if __name__ == "__main__":
main()
코드 구조 해설
- 설정 상수: 창/격자/속도/벽 옵션 등 상단에서 한 번에 제어
- 폰트 선택:
pick_korean_font()
에서 OS별 한글 폰트를 자동 탐색 - 사운드:
init_mixer()
로 믹서 초기화,make_tone()
으로 사인파 PCM 생성 - 랜덤 스킨:
random_skin()
에서 팔레트 또는 랜덤 RGB를 뽑아 머리/몸/먹이 색 지정 - 게임 루프: 이벤트 처리 → 상태 업데이트 → 렌더링(정보줄·오버레이 포함)
- 최고점: 게임 오버 시
highscore.txt
갱신
실행 & 조작법
python snake_kor.py
- 방향키 또는 WASD : 이동
- P : 일시정지 / 해제
- R : 재시작
- Q 또는 ESC : 종료
커스터마이즈 가이드
룰 & 난이도
WALLS = False # 랩어라운드 모드
SHOW_GRID = True # 격자선 표시
START_SPEED = 8 # 시작 속도 조정
SPEED_STEP_EVERY = 4 # 4점마다 속도 +1
폰트 강제 지정
FONT_PATH = r"C:\Windows\Fonts\malgun.ttf" # Windows 예시
# macOS 예: FONT_PATH = "/System/Library/Fonts/AppleSDGothicNeo.ttc"
최고점 저장(파일) 설명
최고점은 실행 폴더의 highscore.txt
에 숫자 형태로 보관합니다. 권한 이슈가 있는 경로나 읽기전용 폴더에서는 기록이 실패할 수 있으니, 문제 발생 시 다른 폴더에서 실행해 보세요.
사운드 이펙트 작동 원리
외부 음원 파일 없이 사인파를 즉석 생성합니다. make_tone()
가 주파수·길이·볼륨을 받아 16비트 PCM을 만들어 pygame.mixer.Sound
로 재생합니다. 환경에 따라 오디오 장치 초기화가 안 되면 자동으로 무음 동작합니다(게임플레이는 정상).
자주 발생하는 이슈 해결
- ModuleNotFoundError: pygame →
pip install pygame
- 한글 깨짐 →
FONT_PATH
를 명시하거나, 시스템에 한글 폰트를 설치 - 사운드가 안 남 → 다른 오디오 장치를 선택하거나 관리자 권한으로 실행, 그래도 안 되면 무시해도 됨
- 격자/창 크기 변경 →
WIDTH
,HEIGHT
,CELL
이 서로 나누어떨어지도록
배포(EXE) 팁
pip install pyinstaller
pyinstaller -F -w snake_kor.py
dist/snake_kor.exe
가 생성됩니다. 배포 시 실행 방법과 조작법을 함께 안내하면 좋아요.
확장 아이디어
- 맵·장애물/스테이지(벽 레이아웃 추가)
- 랭킹 Top 5 파일 저장
- 스킨 선택 화면 & 테마 프리셋
- 효과음/배경음 다중 채널 믹싱
© 본 코드/글은 학습 및 개인 프로젝트용으로 자유롭게 활용하셔도 됩니다.
반응형