본문 바로가기

[Pygame] 🧱 벽돌 깨기 게임 만들기 16강 | 시각적 연출 강화

@도마22026. 2. 27. 22:00
728x90


시각적 연출 강화

 

이번 강의에서는 게임의 로직을 바꾸지 않고
시각적인 인상을 크게 바꾸는 게임 디자인 작업을 진행합니다.

구체적으로는
벽돌이 랜덤한 색상으로 생성되도록 변경합니다.

이 작업은 단순한 꾸미기가 아니라
플레이어의 인식과 체감 난이도에 직접적인 영향을 줍니다.


왜 벽돌 색상이 중요한가

지금까지의 벽돌은 모두 같은 색상이었습니다.
이 경우 게임에는 다음과 같은 한계가 있습니다.

  • 벽돌이 많아 보이지 않습니다
  • 충돌의 시각적 피드백이 약합니다
  • 스테이지 변화가 느껴지지 않습니다

벽돌 색상을 다양하게 주는 것만으로도
게임은 훨씬 풍부해 보이게 됩니다.


색상 리스트 준비

랜덤 색상을 사용하되
완전히 무작위 RGB 값은 사용하지 않습니다.

게임 디자인에서는
미리 정해진 색상 팔레트를 사용하는 것이 안정적입니다.

BRICK_COLORS = [
    (255, 80, 80),
    (80, 255, 80),
    (80, 80, 255),
    (255, 200, 80),
    (200, 80, 255),
    (80, 200, 255)
]

벽돌에 색상 정보 추가하기

기존에는 벽돌을 Rect 객체로만 관리했습니다.
이제는 사각형 + 색상을 함께 관리합니다.

bricks.append({
    "rect": pygame.Rect(brick_x, brick_y, brick_width, brick_height),
    "color": random.choice(BRICK_COLORS)
})

이 구조를 사용하면
각 벽돌은 서로 다른 색을 가질 수 있습니다.


벽돌 생성 함수 수정

벽돌 생성 함수에서
색상을 함께 설정하도록 변경합니다.

def create_bricks():
    bricks.clear()
    for row in range(rows):
        for col in range(cols):
            brick_x = col * (brick_width + brick_padding) + 35
            brick_y = row * (brick_height + brick_padding) + 50
            bricks.append({
                "rect": pygame.Rect(brick_x, brick_y, brick_width, brick_height),
                "color": random.choice(BRICK_COLORS)
            })

벽돌 그리기 방식 변경

이제 벽돌은
사각형과 색상을 함께 사용해 그립니다.

for brick in bricks:
    pygame.draw.rect(screen, brick["color"], brick["rect"])

충돌 처리 코드 수정

벽돌이 딕셔너리 구조로 바뀌었기 때문에
충돌 처리도 이에 맞게 수정합니다.

if ball_rect.colliderect(brick["rect"]):

이 외의 로직은
기존과 동일하게 유지됩니다.


현재 단계의 결과

이번 강의까지 구현된 결과는 다음과 같습니다.

  • 벽돌이 랜덤한 색상으로 생성됩니다
  • 화면이 훨씬 다채롭게 보입니다
  • 게임이 스테이지처럼 느껴지기 시작합니다

이제 게임은
기능적으로도, 시각적으로도 완성 단계에 들어왔습니다.

다음 강의에서는
본격적으로 스테이지 개념을 도입합니다.



전체 코드

더보기
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

paddle_width = 100
paddle_height = 15
paddle_speed = 7

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
)

brick_width = 60
brick_height = 20
brick_padding = 10
rows = 4
cols = 10

bricks = []
items = []
effects = []
item_speed = 2

def create_bricks():
    bricks.clear()
    for row in range(rows):
        for col in range(cols):
            brick_x = col * (brick_width + brick_padding) + 35
            brick_y = row * (brick_height + brick_padding) + 50
            bricks.append({
                "rect": pygame.Rect(brick_x, brick_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_game():
    global score, lives, ball_active, ball_speed_x, ball_speed_y
    score = 0
    lives = 3
    ball_active = False
    ball_speed_x = 5
    ball_speed_y = -5
    paddle_rect.width = paddle_width
    paddle_rect.x = (SCREEN_WIDTH - paddle_width) // 2
    create_bricks()
    items.clear()
    effects.clear()

create_bricks()

pygame.mixer.music.load("title_bgm.mp3")
pygame.mixer.music.play(-1)

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"
                ball_active = False

            elif event.key == pygame.K_SPACE and game_state == "PLAY" and not ball_active:
                ball_active = True

            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 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:
        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:
            paddle_rect.width = paddle_width
            paddle_rect.x = (SCREEN_WIDTH - paddle_width) // 2
            create_bricks()
            items.clear()
            effects.clear()

    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))

    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 게임 제작까지 차근차근 기록합니다. — 도마

목차