파이썬 기초문법/파이썬 게임 만들기

파이썬 게임 만들기 - 브레이크아웃 게임 🧱🏓

Family in August 2025. 3. 8. 11:43
반응형

 

파이썬으로 시작하는 게임 개발의 세계 🐍🎮

안녕하세요! Python Game Dev 블로그의 여섯 번째 포스팅입니다. 이전에는 숫자 맞추기, 퀴즈, 행맨, 틱택토, 그리고 스네이크 게임을 만들어보았는데요, 오늘은 클래식 아케이드 게임인 브레이크아웃(Breakout) 게임을 만들어보겠습니다.

오늘의 게임: 브레이크아웃 게임 🧱🏓

브레이크아웃은 1976년 아타리에서 출시된 게임으로, 게임 역사에서 중요한 위치를 차지하고 있습니다. 플레이어는 화면 하단의 패들을 좌우로 움직여 공을 튕겨 블록을 부수는 게임입니다. 이번 포스팅에서는 파이썬과 Pygame을 이용해 이 클래식 게임을 재현해보겠습니다.

게임의 규칙 📜

  • 플레이어는 화면 하단에 있는 패들을 좌우로 움직일 수 있습니다.
  • 공은 계속 움직이며 벽, 블록, 패들에 부딪히면 튕겨나갑니다.
  • 블록을 맞추면 점수를 얻고 블록이 사라집니다.
  • 모든 블록을 제거하면 승리합니다.
  • 공이 화면 하단으로 떨어지면 생명이 줄어들고, 모든 생명을 잃으면 게임 오버됩니다.

전체 코드 💻

import pygame
import sys
import random
import math

# 초기화
pygame.init()
pygame.font.init()

# 색상 정의
WHITE = (255, 255, 255)
BLACK = (0, 0, 0)
BLUE = (0, 0, 255)
RED = (255, 0, 0)
GREEN = (0, 255, 0)
YELLOW = (255, 255, 0)
ORANGE = (255, 165, 0)

# 게임 설정
WIDTH, HEIGHT = 800, 600
PADDLE_WIDTH, PADDLE_HEIGHT = 100, 15
BALL_RADIUS = 10
BLOCK_WIDTH, BLOCK_HEIGHT = 80, 30
BLOCK_ROWS = 5
BLOCK_COLS = 10
FPS = 60

# 창 설정
screen = pygame.display.set_mode((WIDTH, HEIGHT))
pygame.display.set_caption('Python Breakout Game')
clock = pygame.time.Clock()

# 폰트 설정
font = pygame.font.SysFont('malgungothic', 30)
big_font = pygame.font.SysFont('malgungothic', 50)

class Paddle:
    def __init__(self, x, y, width, height, color, speed):
        self.x = x
        self.y = y
        self.width = width
        self.height = height
        self.color = color
        self.speed = speed
        self.rect = pygame.Rect(x, y, width, height)
    
    def draw(self, surface):
        pygame.draw.rect(surface, self.color, self.rect)
        # 패들에 하이라이트 추가
        pygame.draw.rect(surface, WHITE, (self.x, self.y, self.width, 5))
    
    def move(self, direction):
        if direction == 'left':
            self.x = max(0, self.x - self.speed)
        elif direction == 'right':
            self.x = min(WIDTH - self.width, self.x + self.speed)
        
        self.rect = pygame.Rect(self.x, self.y, self.width, self.height)

class Ball:
    def __init__(self, x, y, radius, color, speed_x, speed_y):
        self.x = x
        self.y = y
        self.radius = radius
        self.color = color
        self.speed_x = speed_x
        self.speed_y = speed_y
        self.active = False
    
    def draw(self, surface):
        pygame.draw.circle(surface, self.color, (self.x, self.y), self.radius)
        # 볼에 하이라이트 추가
        pygame.draw.circle(surface, WHITE, (self.x - 3, self.y - 3), 3)
    
    def move(self):
        if self.active:
            self.x += self.speed_x
            self.y += self.speed_y
    
    def bounce_wall(self):
        # 좌우 벽 충돌
        if self.x <= self.radius or self.x >= WIDTH - self.radius:
            self.speed_x *= -1
            play_sound('wall')
        
        # 천장 충돌
        if self.y <= self.radius:
            self.speed_y *= -1
            play_sound('wall')
    
    def bounce_paddle(self, paddle):
        if (self.y + self.radius >= paddle.y and
            self.x >= paddle.x and
            self.x <= paddle.x + paddle.width):
            
            # 패들의 어느 부분에 맞았는지에 따라 반사각 조절
            relative_x = (self.x - (paddle.x + paddle.width / 2)) / (paddle.width / 2)
            bounce_angle = relative_x * (math.pi / 3)  # 최대 60도 각도
            
            speed = math.sqrt(self.speed_x**2 + self.speed_y**2)
            self.speed_x = speed * math.sin(bounce_angle)
            self.speed_y = -speed * math.cos(bounce_angle)
            
            play_sound('paddle')
    
    def check_bottom(self):
        if self.y >= HEIGHT + self.radius:
            return True
        return False
    
    def reset(self, paddle):
        self.x = paddle.x + paddle.width // 2
        self.y = paddle.y - self.radius - 1
        self.active = False
        self.speed_x = random.choice([-4, 4])
        self.speed_y = -5

class Block:
    def __init__(self, x, y, width, height, color, points):
        self.x = x
        self.y = y
        self.width = width
        self.height = height
        self.color = color
        self.points = points
        self.rect = pygame.Rect(x, y, width, height)
        self.active = True
    
    def draw(self, surface):
        if self.active:
            pygame.draw.rect(surface, self.color, self.rect)
            # 블록에 테두리 추가
            pygame.draw.rect(surface, WHITE, self.rect, 2)
            # 하이라이트 추가
            pygame.draw.line(surface, WHITE, (self.x, self.y), (self.x + self.width, self.y), 3)
            pygame.draw.line(surface, WHITE, (self.x, self.y), (self.x, self.y + self.height), 3)
    
    def check_collision(self, ball):
        if not self.active:
            return False
        
        # 충돌 감지를 위한 임시 사각형 (공의 중심을 기준으로)
        ball_rect = pygame.Rect(ball.x - ball.radius, ball.y - ball.radius, 
                               ball.radius * 2, ball.radius * 2)
        
        if self.rect.colliderect(ball_rect):
            # 충돌 방향 파악 (수직, 수평)
            # 공과 블록 중심 사이의 거리 계산
            dx = ball.x - max(self.x, min(ball.x, self.x + self.width))
            dy = ball.y - max(self.y, min(ball.y, self.y + self.height))
            
            if abs(dx) > abs(dy):
                ball.speed_x *= -1  # 좌우 충돌
            else:
                ball.speed_y *= -1  # 상하 충돌
            
            self.active = False
            play_sound('block')
            return True
        
        return False

def create_blocks():
    blocks = []
    colors = [RED, ORANGE, YELLOW, GREEN, BLUE]
    points = [50, 40, 30, 20, 10]  # 위에서부터 점수 배분
    
    for row in range(BLOCK_ROWS):
        for col in range(BLOCK_COLS):
            block_x = col * (BLOCK_WIDTH + 5) + 50
            block_y = row * (BLOCK_HEIGHT + 5) + 50
            blocks.append(Block(block_x, block_y, BLOCK_WIDTH, BLOCK_HEIGHT, 
                               colors[row], points[row]))
    
    return blocks

def play_sound(sound_type):
    # 실제 게임에 사운드를 추가할 때는 아래 주석을 해제하고 사운드 파일을 준비해야 합니다
    # 이 예제에서는 사운드 파일 없이 함수만 준비합니다
    pass
    # if sound_type == 'wall':
    #     pygame.mixer.Sound('wall.wav').play()
    # elif sound_type == 'paddle':
    #     pygame.mixer.Sound('paddle.wav').play()
    # elif sound_type == 'block':
    #     pygame.mixer.Sound('block.wav').play()
    # elif sound_type == 'gameover':
    #     pygame.mixer.Sound('gameover.wav').play()

def show_message(message, y_pos, color=WHITE, size='normal'):
    font_to_use = big_font if size == 'big' else font
    text = font_to_use.render(message, True, color)
    text_rect = text.get_rect(center=(WIDTH//2, y_pos))
    screen.blit(text, text_rect)

def start_screen():
    screen.fill(BLACK)
    show_message("BREAKOUT", HEIGHT//3, YELLOW, 'big')
    show_message("시작하려면 SPACE를 누르세요", HEIGHT//2 + 30)
    show_message("종료하려면 ESC를 누르세요", HEIGHT//2 + 70)
    
    pygame.display.update()
    
    waiting = True
    while waiting:
        for event in pygame.event.get():
            if event.type == pygame.QUIT:
                pygame.quit()
                sys.exit()
            if event.type == pygame.KEYDOWN:
                if event.key == pygame.K_SPACE:
                    waiting = False
                elif event.key == pygame.K_ESCAPE:
                    pygame.quit()
                    sys.exit()

def game_over_screen(score, won=False):
    screen.fill(BLACK)
    
    if won:
        show_message("승리했습니다!", HEIGHT//3 - 50, GREEN, 'big')
    else:
        show_message("게임 오버", HEIGHT//3 - 50, RED, 'big')
    
    show_message(f"최종 점수: {score}", HEIGHT//2)
    show_message("다시 시작하려면 SPACE를 누르세요", HEIGHT//2 + 50)
    show_message("종료하려면 ESC를 누르세요", HEIGHT//2 + 90)
    
    pygame.display.update()
    
    waiting = True
    while waiting:
        for event in pygame.event.get():
            if event.type == pygame.QUIT:
                pygame.quit()
                sys.exit()
            if event.type == pygame.KEYDOWN:
                if event.key == pygame.K_SPACE:
                    waiting = False
                    return True  # 재시작
                elif event.key == pygame.K_ESCAPE:
                    pygame.quit()
                    sys.exit()

def main():
    # 게임 객체 초기화
    paddle = Paddle((WIDTH - PADDLE_WIDTH) // 2, HEIGHT - 30, 
                   PADDLE_WIDTH, PADDLE_HEIGHT, BLUE, 8)
    ball = Ball(WIDTH // 2, HEIGHT - 50, BALL_RADIUS, WHITE, 0, 0)
    blocks = create_blocks()
    
    # 게임 상태 변수
    score = 0
    lives = 3
    game_started = False
    
    # 시작 화면
    start_screen()
    
    # 게임 루프
    running = True
    while running:
        # 이벤트 처리
        for event in pygame.event.get():
            if event.type == pygame.QUIT:
                running = False
            elif event.type == pygame.KEYDOWN:
                if event.key == pygame.K_SPACE and not ball.active:
                    ball.active = True
                    if not game_started:
                        game_started = True
        
        # 키 상태 확인
        keys = pygame.key.get_pressed()
        if keys[pygame.K_LEFT]:
            paddle.move('left')
        if keys[pygame.K_RIGHT]:
            paddle.move('right')
        
        # 공이 활성화되지 않았으면 패들 위에 위치시키기
        if not ball.active:
            ball.reset(paddle)
        
        # 공 이동
        ball.move()
        ball.bounce_wall()
        ball.bounce_paddle(paddle)
        
        # 블록 충돌 확인
        active_blocks = 0
        for block in blocks:
            if block.active:
                active_blocks += 1
                if block.check_collision(ball):
                    score += block.points
        
        # 공이 바닥에 떨어졌는지 확인
        if ball.check_bottom():
            lives -= 1
            if lives <= 0:
                if game_over_screen(score):
                    # 재시작
                    return main()
                else:
                    running = False
            else:
                ball.reset(paddle)
        
        # 모든 블록이 제거되었는지 확인
        if active_blocks == 0:
            if game_over_screen(score, True):
                # 재시작
                return main()
            else:
                running = False
        
        # 화면 그리기
        screen.fill(BLACK)
        
        # 블록 그리기
        for block in blocks:
            block.draw(screen)
        
        # 패들과 공 그리기
        paddle.draw(screen)
        ball.draw(screen)
        
        # 점수와 생명 표시
        show_message(f"점수: {score}", 20)
        show_message(f"생명: {lives}", HEIGHT - 20)
        
        # 시작 안내 메시지
        if not game_started:
            show_message("SPACE를 눌러 시작하세요", HEIGHT // 2)
        
        pygame.display.update()
        clock.tick(FPS)
    
    pygame.quit()
    sys.exit()

if __name__ == "__main__":
    main()

게임 화면

게임 화면 📸

게임을 실행하면 다음과 같은 화면이 나타납니다:

  1. 시작 화면 ![시작 화면](게임 화면 1)
  2. 게임 진행 화면 ![게임 진행 화면](게임 화면 2)
  3. 게임 오버 화면 ![게임 오버 화면](게임 화면 3)

코드 설명 📝

1. 클래스 기반 구조

이번 게임에서는 객체 지향 프로그래밍(OOP)을 활용하여 게임 요소들을 클래스로 구현했습니다.

Paddle 클래스

class Paddle:
    def __init__(self, x, y, width, height, color, speed):
        self.x = x
        self.y = y
        self.width = width
        self.height = height
        self.color = color
        self.speed = speed
        self.rect = pygame.Rect(x, y, width, height)
    
    def draw(self, surface):
        pygame.draw.rect(surface, self.color, self.rect)
        # 패들에 하이라이트 추가
        pygame.draw.rect(surface, WHITE, (self.x, self.y, self.width, 5))
    
    def move(self, direction):
        if direction == 'left':
            self.x = max(0, self.x - self.speed)
        elif direction == 'right':
            self.x = min(WIDTH - self.width, self.x + self.speed)
        
        self.rect = pygame.Rect(self.x, self.y, self.width, self.height)

패들을 관리하는 클래스로, 위치, 크기, 색상, 속도 등의 속성과 화면에 그리는 draw 메서드, 이동하는 move 메서드를 가지고 있습니다.

Ball 클래스

class Ball:
    def __init__(self, x, y, radius, color, speed_x, speed_y):
        self.x = x
        self.y = y
        self.radius = radius
        self.color = color
        self.speed_x = speed_x
        self.speed_y = speed_y
        self.active = False
    
    def draw(self, surface):
        pygame.draw.circle(surface, self.color, (self.x, self.y), self.radius)
        # 볼에 하이라이트 추가
        pygame.draw.circle(surface, WHITE, (self.x - 3, self.y - 3), 3)
    
    def move(self):
        if self.active:
            self.x += self.speed_x
            self.y += self.speed_y
    
    def bounce_wall(self):
        # 좌우 벽과 천장 충돌 처리
        # ...
    
    def bounce_paddle(self, paddle):
        # 패들과의 충돌 처리
        # ...

공을 관리하는 클래스로, 위치, 반지름, 색상, 속도 등의 속성과 여러 메서드를 가지고 있습니다. 특히 bounce_paddle 메서드에서는 패들의 어느 부분에 맞았는지에 따라 반사각을 조절하여 게임성을 높였습니다.

Block 클래스

class Block:
    def __init__(self, x, y, width, height, color, points):
        self.x = x
        self.y = y
        self.width = width
        self.height = height
        self.color = color
        self.points = points
        self.rect = pygame.Rect(x, y, width, height)
        self.active = True
    
    def draw(self, surface):
        # 블록 그리기
        # ...
    
    def check_collision(self, ball):
        # 공과의 충돌 확인
        # ...

블록을 관리하는 클래스로, 위치, 크기, 색상, 점수 등의 속성과 그리기, 충돌 확인 메서드를 가지고 있습니다. check_collision 메서드에서는 공이 블록의 어느 면에 부딪혔는지 계산하여 적절한 반사를 구현했습니다.

2. 물리 시뮬레이션

브레이크아웃 게임의 핵심은 공의 움직임과 충돌 물리입니다.

def bounce_paddle(self, paddle):
    if (self.y + self.radius >= paddle.y and
        self.x >= paddle.x and
        self.x <= paddle.x + paddle.width):
        
        # 패들의 어느 부분에 맞았는지에 따라 반사각 조절
        relative_x = (self.x - (paddle.x + paddle.width / 2)) / (paddle.width / 2)
        bounce_angle = relative_x * (math.pi / 3)  # 최대 60도 각도
        
        speed = math.sqrt(self.speed_x**2 + self.speed_y**2)
        self.speed_x = speed * math.sin(bounce_angle)
        self.speed_y = -speed * math.cos(bounce_angle)
        
        play_sound('paddle')

패들의 중앙에 맞으면 수직으로 튕겨나가고, 왼쪽이나 오른쪽 가장자리에 맞을수록 해당 방향으로 각도가 커지는 물리 시뮬레이션을 구현했습니다. 이렇게 하면 플레이어가 패들로 공의 방향을 어느 정도 제어할 수 있습니다.

3. 게임 상태 관리

# 게임 상태 변수
score = 0
lives = 3
game_started = False

# 게임 루프 내에서의 상태 관리
# 공이 바닥에 떨어졌는지 확인
if ball.check_bottom():
    lives -= 1
    if lives <= 0:
        if game_over_screen(score):
            # 재시작
            return main()
        else:
            running = False
    else:
        ball.reset(paddle)

# 모든 블록이 제거되었는지 확인
if active_blocks == 0:
    if game_over_screen(score, True):
        # 재시작
        return main()
    else:
        running = False

게임의 다양한 상태(시작, 진행 중, 게임 오버, 승리)를 관리하고, 생명, 점수 등의 게임 변수를 추적합니다.

게임 실행 방법 🚀

  1. 필요한 라이브러리 설치
 
pip install pygame
  1. 위의 코드를 복사하여 텍스트 에디터에 붙여넣기
  2. 파일을 breakout.py와 같은 이름으로 저장
  3. 파이썬이 설치된 환경에서 터미널(명령 프롬프트)를 열고 파일을 저장한 디렉토리로 이동
  4. python breakout.py 명령어를 입력하여 게임 실행

확장 가능성 🌱

이 브레이크아웃 게임을 다음과 같은 방향으로 확장해볼 수 있습니다:

  1. 다양한 레벨 구현
    • 여러 레벨 설계, 레벨마다 다른 블록 배치
    • 난이도 증가 (공 속도 증가, 패들 크기 감소 등)
  2. 파워업 아이템 추가
    • 패들 크기 증가/감소
    • 공 속도 증가/감소
    • 멀티볼 (여러 개의 공 동시 사용)
    • 레이저 (패들에서 발사하여 블록 파괴)
  3. 시각적 개선
    • 스프라이트 이미지 사용
    • 파티클 효과 (블록 파괴 시)
    • 애니메이션 효과
  4. 사운드 추가
    • 배경 음악
    • 다양한 효과음 (충돌, 게임 오버, 승리 등)
  5. 게임 기능 확장
    • 점수 저장 및 하이스코어 시스템
    • 커스텀 레벨 에디터

게임에서 사용된 프로그래밍 개념 📚

이번 게임을 통해 다음과 같은 파이썬 개념들을 활용해보았습니다:

  1. 객체 지향 프로그래밍 (OOP)
    • 클래스를 활용한 게임 객체 모델링
    • 상속, 캡슐화 등의 OOP 개념
  2. 충돌 감지 및 물리
    • 사각형-원 충돌 감지
    • 반사각 계산
    • 속도와 방향 벡터 처리
  3. 게임 상태 관리
    • 다양한 게임 상태 간 전환
    • 조건 분기를 통한 게임 로직 구현
  4. 사용자 인터페이스
    • 텍스트 렌더링
    • 상태 표시 (점수, 생명 등)
  5. 이벤트 처리
    • 키보드 입력 확인
    • 게임 이벤트 처리 (충돌, 게임 오버 등)

알고리즘 설명 🧮

브레이크아웃 게임의 핵심 알고리즘들을 살펴보겠습니다:

반사각 계산 알고리즘

공이 패들에 부딪혔을 때 반사각을 계산하는 알고리즘은 다음과 같습니다:

  1. 패들 중심에서 공까지의 상대적 위치를 계산 (범위: -1.0 ~ 1.0)
  2. 이 상대적 위치를 기반으로 반사각 계산 (최대 60도)
  3. 삼각함수를 이용해 새로운 속도 벡터(x, y) 계산
relative_x = (self.x - (paddle.x + paddle.width / 2)) / (paddle.width / 2)
bounce_angle = relative_x * (math.pi / 3)  # 최대 60도 각도

speed = math.sqrt(self.speed_x**2 + self.speed_y**2)
self.speed_x = speed * math.sin(bounce_angle)
self.speed_y = -speed * math.cos(bounce_angle)

블록-공 충돌 감지 알고리즘

공이 블록과 충돌했을 때 어느 면에서 충돌했는지 판단하는 알고리즘입니다:

  1. 공과 블록의 충돌 여부 확인
  2. 충돌 시, 공의 중심에서 블록의 가장 가까운 점까지의 x, y 거리 계산
  3. x 거리가 y 거리보다 크면 좌우 충돌로 판단하고 x 속도 반전
  4. 그렇지 않으면 상하 충돌로 판단하고 y 속도 반전
# 충돌 방향 파악 (수직, 수평)
dx = ball.x - max(self.x, min(ball.x, self.x + self.width))
dy = ball.y - max(self.y, min(ball.y, self.y + self.height))

if abs(dx) > abs(dy):
    ball.speed_x *= -1  # 좌우 충돌
else:
    ball.speed_y *= -1  # 상하 충돌

다음 포스팅 예고 🔮

다음 포스팅에서는 두 명의 플레이어가 대결할 수 있는 클래식 게임 "퐁(Pong)" 게임을 만들어 볼 예정입니다. 멀티플레이어 게임 구현과 AI 대전 상대 프로그래밍에 대해 배워보겠습니다.

파이썬 게임 개발 여정, 계속 지켜봐 주세요! 궁금한 점이나 제안사항이 있으시면 댓글로 남겨주세요. 행복한 코딩 되세요! 🐍✨

반응형