본문 바로가기

[Pygame] 🧱 벽돌 깨기 게임 만들기 17강 | 스테이지 시스템으로 게임 완성하기

@도마22026. 2. 28. 19:00
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
도마2
@도마2 :: 도마의 코드노트

초보자를 위한 코딩 강의를 정리합니다. 파이썬부터 C#, Unity 게임 제작까지 차근차근 기록합니다. — 도마

목차