728x90

스테이지 시스템으로 게임 완성하기
이번 강의에서는 지금까지 만든 모든 기능을 하나로 묶어
완전한 스테이지 기반 게임 구조를 완성합니다.
이 강의가 끝나면
이 게임은 더 이상 “벽돌깨기 예제”가 아니라
처음부터 끝까지 플레이 가능한 게임가 됩니다.
이번 강의에서 구현할 내용
이번 강의의 핵심은 다음과 같습니다.
- 스테이지 개념 도입
- 총 3개의 스테이지 구성
- 스테이지 클리어 시 SPACE로 다음 스테이지 진행
- 스테이지가 올라갈수록 패들 크기 감소
- 스테이지마다 벽돌 배치가 다르게 구성
게임 오버가 되면
언제나 R 키로 처음 스테이지부터 재시작합니다.
스테이지 변수 추가
현재 스테이지를 관리할 변수를 추가합니다.
stage = 1
MAX_STAGE = 3
스테이지별 패들 크기 설정
스테이지가 올라갈수록
패들이 작아지도록 설정합니다.
def get_paddle_width(stage):
if stage == 1:
return 100
if stage == 2:
return 80
return 60
스테이지별 벽돌 배치 구성
각 스테이지는
서로 다른 벽돌 배치를 가집니다.
STAGE_LAYOUTS = {
1: [
"##########",
"##########",
"##########",
"##########"
],
2: [
"##########",
"## #### ##",
"##########"
],
3: [
"## #### ##",
"##########",
"## #### ##",
"##########"
]
}
스테이지 기반 벽돌 생성
벽돌 생성 함수를
스테이지 기준으로 변경합니다.
def create_bricks(stage):
bricks.clear()
layout = STAGE_LAYOUTS[stage]
for row_idx, row in enumerate(layout):
for col_idx, cell in enumerate(row):
if cell == "#":
x = col_idx * (brick_width + brick_padding) + 35
y = row_idx * (brick_height + brick_padding) + 50
bricks.append({
"rect": pygame.Rect(x, y, brick_width, brick_height),
"color": random.choice(BRICK_COLORS)
})
스테이지 클리어 처리
모든 벽돌이 제거되면
게임 상태를 클리어 상태로 전환합니다.
if len(bricks) == 0:
if stage < MAX_STAGE:
game_state = "STAGE_CLEAR"
else:
game_state = "GAME_CLEAR"
다음 스테이지 진행 처리
스테이지 클리어 상태에서
SPACE 키를 누르면 다음 스테이지로 이동합니다.
if event.key == pygame.K_SPACE and game_state == "STAGE_CLEAR":
stage += 1
reset_stage()
game_state = "PLAY"
스테이지 리셋 함수
스테이지 전환 시
필요한 요소만 초기화합니다.
def reset_stage():
global ball_active, paddle_width
ball_active = False
paddle_width = get_paddle_width(stage)
paddle_rect.width = paddle_width
paddle_rect.x = (SCREEN_WIDTH - paddle_width) // 2
create_bricks(stage)
items.clear()
effects.clear()
게임 흐름 정리
이제 게임의 전체 흐름은 다음과 같습니다.
- TITLE → SPACE → STAGE 1
- PLAY → STAGE_CLEAR → SPACE → 다음 스테이지
- 마지막 스테이지 클리어 → GAME_CLEAR
- GAME_OVER → R → STAGE 1
이 구조는
상업 게임과 동일한 흐름입니다.

전체 코드
더보기
import pygame
import sys
import random
pygame.init()
pygame.mixer.init()
SCREEN_WIDTH = 800
SCREEN_HEIGHT = 600
screen = pygame.display.set_mode((SCREEN_WIDTH, SCREEN_HEIGHT))
pygame.display.set_caption("Breakout Game")
BLACK = (0, 0, 0)
WHITE = (255, 255, 255)
GREEN = (0, 200, 0)
EFFECT_COLOR = (255, 200, 200)
BRICK_COLORS = [
(255, 80, 80),
(80, 255, 80),
(80, 80, 255),
(255, 200, 80),
(200, 80, 255),
(80, 200, 255)
]
clock = pygame.time.Clock()
FPS = 60
font = pygame.font.SysFont(None, 30)
title_font = pygame.font.SysFont(None, 64)
info_font = pygame.font.SysFont(None, 28)
brick_sound = pygame.mixer.Sound("brick.wav")
paddle_sound = pygame.mixer.Sound("paddle.wav")
item_sound = pygame.mixer.Sound("item.wav")
game_state = "TITLE"
prev_state = None
ball_active = False
score = 0
lives = 3
stage = 1
MAX_STAGE = 3
paddle_speed = 7
brick_width = 60
brick_height = 20
brick_padding = 10
rows = 4
cols = 10
STAGE_LAYOUTS = {
1: [
"##########",
"##########",
"##########",
"##########"
],
2: [
"##########",
"## #### ##",
"##########"
],
3: [
"## #### ##",
"##########",
"## #### ##",
"##########"
]
}
def get_paddle_width(stage):
if stage == 1:
return 100
if stage == 2:
return 80
return 60
paddle_width = get_paddle_width(stage)
paddle_height = 15
paddle_rect = pygame.Rect(
(SCREEN_WIDTH - paddle_width) // 2,
SCREEN_HEIGHT - 40,
paddle_width,
paddle_height
)
ball_radius = 8
ball_x = paddle_rect.centerx
ball_y = paddle_rect.top - ball_radius
ball_speed_x = 5
ball_speed_y = -5
ball_rect = pygame.Rect(
ball_x - ball_radius,
ball_y - ball_radius,
ball_radius * 2,
ball_radius * 2
)
bricks = []
items = []
effects = []
item_speed = 2
def create_bricks(stage):
bricks.clear()
layout = STAGE_LAYOUTS[stage]
for r, row in enumerate(layout):
for c, cell in enumerate(row):
if cell == "#":
x = c * (brick_width + brick_padding) + 35
y = r * (brick_height + brick_padding) + 50
bricks.append({
"rect": pygame.Rect(x, y, brick_width, brick_height),
"color": random.choice(BRICK_COLORS)
})
def create_item(x, y):
items.append(pygame.Rect(x, y, 20, 20))
def reset_stage():
global ball_active, paddle_width
ball_active = False
paddle_width = get_paddle_width(stage)
paddle_rect.width = paddle_width
paddle_rect.x = (SCREEN_WIDTH - paddle_width) // 2
create_bricks(stage)
items.clear()
effects.clear()
def reset_game():
global score, lives, stage, ball_active
score = 0
lives = 3
stage = 1
ball_active = False
reset_stage()
pygame.mixer.music.load("title_bgm.mp3")
pygame.mixer.music.play(-1)
reset_stage()
running = True
while running:
clock.tick(FPS)
for event in pygame.event.get():
if event.type == pygame.QUIT:
running = False
if event.type == pygame.KEYDOWN:
if event.key == pygame.K_SPACE and game_state == "TITLE":
game_state = "PLAY"
elif event.key == pygame.K_SPACE and game_state == "PLAY" and not ball_active:
ball_active = True
elif event.key == pygame.K_SPACE and game_state == "STAGE_CLEAR":
stage += 1
reset_stage()
game_state = "PLAY"
if event.key == pygame.K_r and game_state in ("GAME_OVER", "GAME_CLEAR"):
reset_game()
game_state = "PLAY"
if game_state != prev_state:
if game_state == "TITLE":
pygame.mixer.music.load("title_bgm.mp3")
pygame.mixer.music.play(-1)
elif game_state == "PLAY":
pygame.mixer.music.load("game_bgm.mp3")
pygame.mixer.music.play(-1)
prev_state = game_state
if game_state == "TITLE":
screen.fill(BLACK)
screen.blit(title_font.render("BREAKOUT GAME", True, WHITE), (SCREEN_WIDTH // 2 - 200, 200))
screen.blit(info_font.render("Press SPACE to Start", True, WHITE), (SCREEN_WIDTH // 2 - 110, 300))
pygame.display.flip()
continue
if game_state == "STAGE_CLEAR":
screen.fill(BLACK)
screen.blit(title_font.render("STAGE CLEAR", True, WHITE), (SCREEN_WIDTH // 2 - 150, 200))
screen.blit(info_font.render("Press SPACE for Next Stage", True, WHITE), (SCREEN_WIDTH // 2 - 160, 300))
pygame.display.flip()
continue
if game_state in ("GAME_OVER", "GAME_CLEAR"):
screen.fill(BLACK)
text = "GAME OVER" if game_state == "GAME_OVER" else "GAME CLEAR!"
screen.blit(title_font.render(text, True, WHITE), (SCREEN_WIDTH // 2 - 130, 200))
screen.blit(info_font.render("Press R to Restart", True, WHITE), (SCREEN_WIDTH // 2 - 80, 300))
pygame.display.flip()
continue
keys = pygame.key.get_pressed()
if keys[pygame.K_LEFT]:
paddle_rect.x -= paddle_speed
if keys[pygame.K_RIGHT]:
paddle_rect.x += paddle_speed
paddle_rect.left = max(paddle_rect.left, 0)
paddle_rect.right = min(paddle_rect.right, SCREEN_WIDTH)
if not ball_active:
ball_x = paddle_rect.centerx
ball_y = paddle_rect.top - ball_radius
else:
ball_x += ball_speed_x
ball_y += ball_speed_y
ball_rect.x = ball_x - ball_radius
ball_rect.y = ball_y - ball_radius
if ball_active:
if ball_rect.left <= 0 or ball_rect.right >= SCREEN_WIDTH:
ball_speed_x *= -1
if ball_rect.top <= 0:
ball_speed_y *= -1
if ball_rect.colliderect(paddle_rect):
ball_speed_y *= -1
ball_rect.bottom = paddle_rect.top
ball_y = ball_rect.centery
paddle_sound.play()
for brick in bricks:
if ball_rect.colliderect(brick["rect"]):
overlap_left = ball_rect.right - brick["rect"].left
overlap_right = brick["rect"].right - ball_rect.left
overlap_top = ball_rect.bottom - brick["rect"].top
overlap_bottom = brick["rect"].bottom - ball_rect.top
min_overlap = min(overlap_left, overlap_right, overlap_top, overlap_bottom)
if min_overlap == overlap_left or min_overlap == overlap_right:
ball_speed_x *= -1
else:
ball_speed_y *= -1
if random.random() < 0.3:
create_item(brick["rect"].centerx - 10, brick["rect"].centery)
effects.append({"rect": pygame.Rect(brick["rect"].x, brick["rect"].y, brick["rect"].width, brick["rect"].height), "timer": 10})
brick_sound.play()
bricks.remove(brick)
score += 10
break
if len(bricks) == 0:
if stage < MAX_STAGE:
game_state = "STAGE_CLEAR"
else:
game_state = "GAME_CLEAR"
for item in items[:]:
item.y += item_speed
if item.colliderect(paddle_rect):
paddle_rect.width += 30
item_sound.play()
items.remove(item)
elif item.top > SCREEN_HEIGHT:
items.remove(item)
for effect in effects[:]:
effect["timer"] -= 1
if effect["timer"] <= 0:
effects.remove(effect)
if ball_rect.top > SCREEN_HEIGHT and ball_active:
lives -= 1
ball_active = False
if lives <= 0:
game_state = "GAME_OVER"
else:
reset_stage()
screen.fill(BLACK)
screen.blit(font.render(f"Score: {score}", True, WHITE), (10, 10))
screen.blit(font.render(f"Lives: {lives}", True, WHITE), (SCREEN_WIDTH - 100, 10))
screen.blit(font.render(f"Stage: {stage}", True, WHITE), (SCREEN_WIDTH // 2 - 40, 10))
pygame.draw.rect(screen, WHITE, paddle_rect)
pygame.draw.circle(screen, WHITE, (ball_x, ball_y), ball_radius)
for brick in bricks:
pygame.draw.rect(screen, brick["color"], brick["rect"])
for item in items:
pygame.draw.rect(screen, GREEN, item)
for effect in effects:
pygame.draw.rect(screen, EFFECT_COLOR, effect["rect"])
pygame.display.flip()
pygame.quit()
sys.exit()
이 강의를 끝으로
파이게임 벽돌깨기 게임 제작 과정을 모두 마쳤습니다.
지금까지 만든 구조를 바탕으로
아이디어를 더해 자신만의 게임으로 확장해보시기 바랍니다.
728x90
'⚙️ Python > 🎮 Pygame 실전' 카테고리의 다른 글
| [Pygame] 🧱 벽돌 깨기 게임 만들기 16강 | 시각적 연출 강화 (0) | 2026.02.27 |
|---|---|
| [Pygame] 🧱 벽돌 깨기 게임 만들기 15강 | 배경음악 추가 (0) | 2026.02.27 |
| [Pygame] 🧱 벽돌 깨기 게임 만들기 14강 | 재시작 기능 추가 (0) | 2026.02.26 |
| [Pygame] 🧱 벽돌 깨기 게임 만들기 13강 | 게임 시작 흐름 개선 (0) | 2026.02.26 |
| [Pygame] 🧱 벽돌 깨기 게임 만들기 12강 | 타이틀 화면 추가 (0) | 2026.02.25 |