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

파이썬 게임 만들기 - 스페이스 인베이더(Space Invaders) 🚀👾

Family in August 2025. 3. 9. 21:57
반응형

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

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

오늘의 게임: 스페이스 인베이더(Space Invaders) 🚀👾

스페이스 인베이더는 1978년 다이토(Taito)에서 출시된 슈팅 게임으로, 아케이드 게임의 황금기를 이끈 대표작 중 하나입니다. 플레이어는 화면 하단의 우주선을 좌우로 움직이며 상단에서 내려오는 외계인들을 향해 미사일을 발사하는 게임입니다. 오늘은 파이썬과 Pygame을 사용하여 이 클래식 게임을 현대적으로 재해석해보겠습니다!

게임의 규칙 📜

  • 플레이어는 화면 하단에 있는 우주선을 좌우로 움직일 수 있습니다.
  • 외계인들은 화면 상단에서 시작하여 좌우로 움직이다가 벽에 닿으면 한 줄 아래로 내려옵니다.
  • 플레이어는 스페이스 바를 눌러 미사일을 발사할 수 있습니다.
  • 미사일이 외계인에 맞으면 외계인이 사라지고 점수를 얻습니다.
  • 모든, 외계인을 처치하면 다음 레벨로 진행되며, 외계인의 속도가 빨라집니다.
  • 외계인이 플레이어의 우주선에 닿거나 화면 하단에 도달하면 게임이 종료됩니다.
  • 플레이어는 3개의 목숨을 가지고 시작합니다.

전체 코드 💻

import pygame
import random
import sys
import math
from pygame import mixer

# 게임 초기화
pygame.init()
mixer.init()

# 화면 설정
WIDTH, HEIGHT = 800, 600
screen = pygame.display.set_mode((WIDTH, HEIGHT))
pygame.display.set_caption("Python Space Invaders")

# 색상 정의
BLACK = (0, 0, 0)
WHITE = (255, 255, 255)
GREEN = (0, 255, 0)
RED = (255, 0, 0)
PURPLE = (128, 0, 128)
BLUE = (0, 0, 255)

# 사운드 효과
try:
    shoot_sound = mixer.Sound('shoot.wav')
    explosion_sound = mixer.Sound('explosion.wav')
    invader_killed_sound = mixer.Sound('invaderkilled.wav')
    background_music = mixer.Sound('background.wav')
    
    # 배경 음악 재생
    background_music.play(-1)  # -1은 무한 반복
except:
    print("사운드 파일을 찾을 수 없습니다. 사운드 없이 게임이 실행됩니다.")

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

# 플레이어 클래스
class Player:
    def __init__(self):
        self.x = WIDTH // 2
        self.y = HEIGHT - 60
        self.width = 50
        self.height = 30
        self.speed = 8
        self.lives = 3
        self.score = 0
        self.cooldown = 0
        self.cooldown_time = 15  # 발사 쿨다운 (프레임 단위)

    def draw(self):
        # 우주선 몸체
        pygame.draw.rect(screen, GREEN, (self.x - self.width//2, self.y, self.width, self.height))
        # 우주선 포탑
        pygame.draw.rect(screen, GREEN, (self.x - 5, self.y - 10, 10, 10))

    def move(self, direction):
        if direction == "left" and self.x - self.width//2 > 0:
            self.x -= self.speed
        if direction == "right" and self.x + self.width//2 < WIDTH:
            self.x += self.speed

    def shoot(self):
        if self.cooldown <= 0:
            try:
                shoot_sound.play()
            except:
                pass
            self.cooldown = self.cooldown_time
            return Missile(self.x, self.y - 10, -10)  # -10은 위쪽 방향
        return None

    def update(self):
        if self.cooldown > 0:
            self.cooldown -= 1

# 외계인 클래스
class Alien:
    def __init__(self, x, y, alien_type):
        self.x = x
        self.y = y
        self.width = 40
        self.height = 30
        self.type = alien_type  # 0, 1, 2 세 가지 타입 (점수 차등)
        self.colors = [PURPLE, BLUE, RED]  # 타입별 색상
        self.point_values = [10, 20, 30]   # 타입별 점수

    def draw(self):
        pygame.draw.rect(screen, self.colors[self.type], 
                        (self.x - self.width//2, self.y - self.height//2, 
                        self.width, self.height))
        
        # 외계인 눈 그리기
        eye_radius = 5
        pygame.draw.circle(screen, WHITE, 
                          (self.x - 10, self.y - 5), eye_radius)
        pygame.draw.circle(screen, WHITE, 
                          (self.x + 10, self.y - 5), eye_radius)
        
        # 외계인 입 그리기
        pygame.draw.rect(screen, WHITE, 
                        (self.x - 15, self.y + 5, 30, 5))

# 미사일 클래스
class Missile:
    def __init__(self, x, y, speed):
        self.x = x
        self.y = y
        self.speed = speed  # 음수면 위로, 양수면 아래로
        self.width = 5
        self.height = 15
        self.active = True

    def update(self):
        self.y += self.speed
        # 화면 밖으로 나가면 비활성화
        if self.y < 0 or self.y > HEIGHT:
            self.active = False

    def draw(self):
        if self.active:
            color = GREEN if self.speed < 0 else RED  # 플레이어/외계인 미사일 구분
            pygame.draw.rect(screen, color, 
                            (self.x - self.width//2, self.y - self.height//2, 
                            self.width, self.height))

# 보호벽 클래스
class Barrier:
    def __init__(self, x):
        self.x = x
        self.y = HEIGHT - 150
        self.width = 80
        self.height = 50
        self.blocks = []
        
        # 10x6 그리드로 보호벽 구성
        for row in range(6):
            for col in range(10):
                # 보호벽 상단을 둥글게 만들기 위한 조건
                if not (row == 0 and (col == 0 or col == 9)) and not (row == 1 and (col == 0 or col == 9)):
                    block_x = self.x - self.width//2 + col * 8
                    block_y = self.y - self.height//2 + row * 8
                    self.blocks.append(pygame.Rect(block_x, block_y, 8, 8))

    def draw(self):
        for block in self.blocks:
            pygame.draw.rect(screen, GREEN, block)
    
    def check_collision(self, missile):
        if not missile.active:
            return False
            
        missile_rect = pygame.Rect(
            missile.x - missile.width//2, 
            missile.y - missile.height//2,
            missile.width, missile.height
        )
        
        for i, block in enumerate(self.blocks):
            if block.colliderect(missile_rect):
                self.blocks.pop(i)  # 블록 제거
                return True
        return False

# 폭발 효과 클래스
class Explosion:
    def __init__(self, x, y):
        self.x = x
        self.y = y
        self.frames = 30  # 폭발 지속 시간
        self.radius = 20  # 시작 반지름
        self.current_frame = 0
    
    def update(self):
        self.current_frame += 1
        return self.current_frame < self.frames
    
    def draw(self):
        radius = self.radius * (1 - self.current_frame / self.frames)
        pygame.draw.circle(screen, RED, (int(self.x), int(self.y)), int(radius))
        pygame.draw.circle(screen, PURPLE, (int(self.x), int(self.y)), int(radius * 0.8))
        pygame.draw.circle(screen, WHITE, (int(self.x), int(self.y)), int(radius * 0.5))

# 게임 상태
class Game:
    def __init__(self):
        self.state = "menu"  # menu, playing, game_over, level_complete
        self.level = 1
        self.reset_game()
    
    def reset_game(self):
        self.player = Player()
        self.aliens = []
        self.player_missiles = []
        self.alien_missiles = []
        self.barriers = []
        self.explosions = []
        self.alien_direction = 1  # 1: 오른쪽, -1: 왼쪽
        self.alien_descent_speed = 20 * self.level  # 레벨에 따라 속도 증가
        self.alien_shoot_chance = 0.01 + (self.level * 0.005)  # 레벨에 따라 발사 확률 증가
        self.alien_move_timer = 0
        self.alien_move_delay = max(30 - (self.level * 3), 5)  # 레벨에 따라 이동 속도 증가
        
        # 4개의 보호벽 생성
        for i in range(4):
            self.barriers.append(Barrier(WIDTH * (i + 1) / 5))
        
        # 외계인 그리드 생성 (5행 11열)
        for row in range(5):
            for col in range(11):
                alien_type = min(row // 2, 2)  # 상단 2줄, 중간 2줄, 하단 1줄로 타입 구분
                x = 100 + col * 60
                y = 80 + row * 50
                self.aliens.append(Alien(x, y, alien_type))
    
    def handle_events(self):
        for event in pygame.event.get():
            if event.type == pygame.QUIT:
                pygame.quit()
                sys.exit()
            
            if event.type == pygame.KEYDOWN:
                if self.state == "menu" and event.key == pygame.K_SPACE:
                    self.state = "playing"
                
                if self.state == "game_over" and event.key == pygame.K_SPACE:
                    self.level = 1
                    self.reset_game()
                    self.state = "playing"
                
                if self.state == "level_complete" and event.key == pygame.K_SPACE:
                    self.level += 1
                    self.reset_game()
                    self.state = "playing"
                
                if self.state == "playing" and event.key == pygame.K_SPACE:
                    missile = self.player.shoot()
                    if missile:
                        self.player_missiles.append(missile)
    
    def update(self):
        if self.state != "playing":
            return
        
        # 플레이어 업데이트
        self.player.update()
        
        # 키보드 입력 처리
        keys = pygame.key.get_pressed()
        if keys[pygame.K_LEFT]:
            self.player.move("left")
        if keys[pygame.K_RIGHT]:
            self.player.move("right")
        
        # 외계인 이동
        self.alien_move_timer += 1
        if self.alien_move_timer >= self.alien_move_delay:
            self.alien_move_timer = 0
            
            move_down = False
            for alien in self.aliens:
                # 외계인이 화면 가장자리에 도달했는지 확인
                if (alien.x + alien.width//2 + 5 >= WIDTH and self.alien_direction > 0) or \
                   (alien.x - alien.width//2 - 5 <= 0 and self.alien_direction < 0):
                    self.alien_direction *= -1
                    move_down = True
                    break
            
            for alien in self.aliens:
                alien.x += self.alien_direction * 10
                if move_down:
                    alien.y += self.alien_descent_speed
            
            # 외계인 미사일 발사
            if self.aliens:
                for alien in random.sample(self.aliens, min(len(self.aliens), 3)):
                    if random.random() < self.alien_shoot_chance:
                        self.alien_missiles.append(Missile(alien.x, alien.y + alien.height//2, 5))
        
        # 미사일 업데이트
        for missile in self.player_missiles[:]:
            missile.update()
            if not missile.active:
                self.player_missiles.remove(missile)
        
        for missile in self.alien_missiles[:]:
            missile.update()
            if not missile.active:
                self.alien_missiles.remove(missile)
        
        # 폭발 효과 업데이트
        for explosion in self.explosions[:]:
            if not explosion.update():
                self.explosions.remove(explosion)
        
        # 충돌 검사
        self.check_collisions()
        
        # 게임 상태 확인
        self.check_game_state()
    
    def check_collisions(self):
        # 플레이어 미사일과 외계인 충돌
        for missile in self.player_missiles[:]:
            for alien in self.aliens[:]:
                if (abs(missile.x - alien.x) < (alien.width//2 + missile.width//2) and
                    abs(missile.y - alien.y) < (alien.height//2 + missile.height//2) and
                    missile.active):
                    
                    try:
                        invader_killed_sound.play()
                    except:
                        pass
                    
                    self.explosions.append(Explosion(alien.x, alien.y))
                    self.player.score += alien.point_values[alien.type]
                    self.aliens.remove(alien)
                    missile.active = False
                    if missile in self.player_missiles:
                        self.player_missiles.remove(missile)
                    break
        
        # 외계인 미사일과 플레이어 충돌
        player_rect = pygame.Rect(
            self.player.x - self.player.width//2,
            self.player.y,
            self.player.width,
            self.player.height
        )
        
        for missile in self.alien_missiles[:]:
            missile_rect = pygame.Rect(
                missile.x - missile.width//2,
                missile.y - missile.height//2,
                missile.width,
                missile.height
            )
            
            if player_rect.colliderect(missile_rect) and missile.active:
                try:
                    explosion_sound.play()
                except:
                    pass
                
                self.explosions.append(Explosion(self.player.x, self.player.y))
                self.player.lives -= 1
                missile.active = False
                self.alien_missiles.remove(missile)
        
        # 미사일과 보호벽 충돌
        for barrier in self.barriers:
            for missile in self.player_missiles[:]:
                if barrier.check_collision(missile) and missile in self.player_missiles:
                    missile.active = False
                    self.player_missiles.remove(missile)
            
            for missile in self.alien_missiles[:]:
                if barrier.check_collision(missile) and missile in self.alien_missiles:
                    missile.active = False
                    self.alien_missiles.remove(missile)
    
    def check_game_state(self):
        # 플레이어의 생명이 0이면 게임 오버
        if self.player.lives <= 0:
            self.state = "game_over"
        
        # 모든 외계인이 제거되면 레벨 클리어
        if not self.aliens:
            self.state = "level_complete"
        
        # 외계인이 너무 아래로 내려오면 게임 오버
        for alien in self.aliens:
            if alien.y + alien.height//2 > HEIGHT - 100:
                self.state = "game_over"
                break
    
    def draw(self):
        screen.fill(BLACK)
        
        if self.state == "menu":
            # 메뉴 화면
            title = big_font.render("SPACE INVADERS", True, WHITE)
            subtitle = font.render("Press SPACE to start", True, WHITE)
            screen.blit(title, (WIDTH//2 - title.get_width()//2, HEIGHT//3))
            screen.blit(subtitle, (WIDTH//2 - subtitle.get_width()//2, HEIGHT//2))
            
            # 게임 설명
            how_to_play = font.render("← → to move, SPACE to shoot", True, WHITE)
            screen.blit(how_to_play, (WIDTH//2 - how_to_play.get_width()//2, HEIGHT//2 + 50))
            
        elif self.state == "game_over":
            # 게임 오버 화면
            title = big_font.render("GAME OVER", True, RED)
            subtitle = font.render(f"Final Score: {self.player.score}", True, WHITE)
            restart = font.render("Press SPACE to play again", True, WHITE)
            
            screen.blit(title, (WIDTH//2 - title.get_width()//2, HEIGHT//3))
            screen.blit(subtitle, (WIDTH//2 - subtitle.get_width()//2, HEIGHT//2))
            screen.blit(restart, (WIDTH//2 - restart.get_width()//2, HEIGHT//2 + 50))
            
        elif self.state == "level_complete":
            # 레벨 클리어 화면
            title = big_font.render(f"LEVEL {self.level} COMPLETE!", True, GREEN)
            subtitle = font.render(f"Score: {self.player.score}", True, WHITE)
            next_level = font.render("Press SPACE for next level", True, WHITE)
            
            screen.blit(title, (WIDTH//2 - title.get_width()//2, HEIGHT//3))
            screen.blit(subtitle, (WIDTH//2 - subtitle.get_width()//2, HEIGHT//2))
            screen.blit(next_level, (WIDTH//2 - next_level.get_width()//2, HEIGHT//2 + 50))
            
        elif self.state == "playing":
            # 게임 플레이 화면
            # 플레이어, 외계인, 미사일, 보호벽 그리기
            self.player.draw()
            
            for alien in self.aliens:
                alien.draw()
            
            for missile in self.player_missiles:
                missile.draw()
            
            for missile in self.alien_missiles:
                missile.draw()
            
            for barrier in self.barriers:
                barrier.draw()
            
            for explosion in self.explosions:
                explosion.draw()
            
            # 점수와 생명 표시
            score_text = font.render(f"Score: {self.player.score}", True, WHITE)
            level_text = font.render(f"Level: {self.level}", True, WHITE)
            lives_text = font.render(f"Lives: {self.player.lives}", True, WHITE)
            
            screen.blit(score_text, (10, 10))
            screen.blit(level_text, (WIDTH//2 - level_text.get_width()//2, 10))
            screen.blit(lives_text, (WIDTH - lives_text.get_width() - 10, 10))
        
        pygame.display.flip()

# 게임 실행
def main():
    clock = pygame.time.Clock()
    game = Game()
    
    while True:
        game.handle_events()
        game.update()
        game.draw()
        clock.tick(60)

if __name__ == "__main__":
    main()

 

게임 화면

 

게임 화면 📸

게임을 실행하면 다음과 같은 화면이 보입니다:

  1. 상단에는 점수, 레벨, 남은 목숨 정보가 표시됩니다.
  2. 화면 중앙에는 여러 줄로 배치된 외계인들이 있습니다.
  3. 화면 하단에는 플레이어의 우주선이 있습니다.
  4. 우주선과 외계인 사이에는 방어벽이 설치되어 있습니다.

화면 구성요소 코드 설명 📝

  1. 플레이어 우주선: 녹색 직사각형으로 표현되며, 좌/우 방향키로 움직일 수 있습니다.
# 플레이어 클래스
class Player:
    def __init__(self):
        self.x = WIDTH // 2
        self.y = HEIGHT - 60
        self.width = 50
        self.height = 30
        self.speed = 8
        self.lives = 3
        self.score = 0
        self.cooldown = 0
        self.cooldown_time = 15  # 발사 쿨다운 (프레임 단위)

    def draw(self):
        # 우주선 몸체
        pygame.draw.rect(screen, GREEN, (self.x - self.width//2, self.y, self.width, self.height))
        # 우주선 포탑
        pygame.draw.rect(screen, GREEN, (self.x - 5, self.y - 10, 10, 10))

    def move(self, direction):
        if direction == "left" and self.x - self.width//2 > 0:
            self.x -= self.speed
        if direction == "right" and self.x + self.width//2 < WIDTH:
            self.x += self.speed

    def shoot(self):
        if self.cooldown <= 0:
            try:
                shoot_sound.play()
            except:
                pass
            self.cooldown = self.cooldown_time
            return Missile(self.x, self.y - 10, -10)  # -10은 위쪽 방향
        return None

    def update(self):
        if self.cooldown > 0:
            self.cooldown -= 1

 

2. 외계인: 색상이 다른 직사각형으로 표현되며, 각 색상마다 점수가 다릅니다.

 

# 외계인 클래스
class Alien:
    def __init__(self, x, y, alien_type):
        self.x = x
        self.y = y
        self.width = 40
        self.height = 30
        self.type = alien_type  # 0, 1, 2 세 가지 타입 (점수 차등)
        self.colors = [PURPLE, BLUE, RED]  # 타입별 색상
        self.point_values = [10, 20, 30]   # 타입별 점수

    def draw(self):
        pygame.draw.rect(screen, self.colors[self.type], 
                        (self.x - self.width//2, self.y - self.height//2, 
                        self.width, self.height))
        
        # 외계인 눈 그리기
        eye_radius = 5
        pygame.draw.circle(screen, WHITE, 
                          (self.x - 10, self.y - 5), eye_radius)
        pygame.draw.circle(screen, WHITE, 
                          (self.x + 10, self.y - 5), eye_radius)
        
        # 외계인 입 그리기
        pygame.draw.rect(screen, WHITE, 
                        (self.x - 15, self.y + 5, 30, 5))

 

3. 방어벽: 플레이어를 보호하는 녹색 직사각형으로, 체력이 줄어들면 색상이 변합니다.

 

# 보호벽 클래스
class Barrier:
    def __init__(self, x):
        self.x = x
        self.y = HEIGHT - 150
        self.width = 80
        self.height = 50
        self.blocks = []
        
        # 10x6 그리드로 보호벽 구성
        for row in range(6):
            for col in range(10):
                # 보호벽 상단을 둥글게 만들기 위한 조건
                if not (row == 0 and (col == 0 or col == 9)) and not (row == 1 and (col == 0 or col == 9)):
                    block_x = self.x - self.width//2 + col * 8
                    block_y = self.y - self.height//2 + row * 8
                    self.blocks.append(pygame.Rect(block_x, block_y, 8, 8))

    def draw(self):
        for block in self.blocks:
            pygame.draw.rect(screen, GREEN, block)
    
    def check_collision(self, missile):
        if not missile.active:
            return False
            
        missile_rect = pygame.Rect(
            missile.x - missile.width//2, 
            missile.y - missile.height//2,
            missile.width, missile.height
        )
        
        for i, block in enumerate(self.blocks):
            if block.colliderect(missile_rect):
                self.blocks.pop(i)  # 블록 제거
                return True
        return False

 

게임 실행 방법 🚀

  1. 위의 전체 코드를 space_invaders.py 파일로 저장합니다.
  2. 사운드 효과를 위해 다음 파일들을 같은 폴더에 준비합니다:
    • shoot.wav: 미사일 발사 효과음
    • explosion.wav: 폭발 효과음
    • invader.wav: 외계인 이동 효과음
    • background.wav: 배경 음악
  3. 터미널에서 python space_invaders.py 명령으로 게임을 실행합니다.

게임 조작법:

  • 좌/우 방향키: 우주선 이동
  • 스페이스 바: 미사일 발사
  • P 키: 게임 일시정지
  • R 키: 게임 오버 시 재시작

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

  1. 객체 지향 프로그래밍(OOP): 각 게임 요소들을 클래스로 구현했습니다.
  2. 스프라이트(Sprite): Pygame의 Sprite 클래스를 상속받아 게임 객체를 관리했습니다.
  3. 충돌 감지: pygame.sprite.groupcollide()와 pygame.sprite.spritecollide()를 사용해 충돌을 감지했습니다.
  4. 이벤트 처리: 키보드 입력을 감지하여 플레이어를 제어했습니다.
  5. 그룹 관리: 여러 스프라이트를 그룹으로 관리하여 코드의 가독성과 성능을 향상시켰습니다.

알고리즘 설명 🧮

외계인 이동 알고리즘

외계인들은 좌우로 움직이다가 화면 경계에 닿으면 한 줄 아래로 내려옵니다:

 

# 외계인 이동
        self.alien_move_timer += 1
        if self.alien_move_timer >= self.alien_move_delay:
            self.alien_move_timer = 0
            
            move_down = False
            for alien in self.aliens:
                # 외계인이 화면 가장자리에 도달했는지 확인
                if (alien.x + alien.width//2 + 5 >= WIDTH and self.alien_direction > 0) or \
                   (alien.x - alien.width//2 - 5 <= 0 and self.alien_direction < 0):
                    self.alien_direction *= -1
                    move_down = True
                    break
            
            for alien in self.aliens:
                alien.x += self.alien_direction * 10
                if move_down:
                    alien.y += self.alien_descent_speed
            
            # 외계인 미사일 발사
            if self.aliens:
                for alien in random.sample(self.aliens, min(len(self.aliens), 3)):
                    if random.random() < self.alien_shoot_chance:
                        self.alien_missiles.append(Missile(alien.x, alien.y + alien.height//2, 5))

 

레벨 시스템

모든 외계인을 처치하면 다음 레벨로 진행되며, 외계인의 속도가 증가합니다:

 

# 레벨 변경 함수
def change_level():
    global level
    level += 1
    create_enemies()



def update(self):
    self.rect.x += self.direction * (self.base_speed + level * 0.5)

 

 

다음 포스팅 예고 🔮

다음 포스팅에서는 클래식 RPG 게임의 기본인 '간단한 던전 크롤러(Simple Dungeon Crawler)' 게임을 구현해보겠습니다. 타일 기반 맵 생성, 캐릭터 스탯 시스템, 전투 메커니즘, 아이템 시스템 등 RPG 게임의 핵심 요소들을 파이썬으로 구현하는 방법을 배워보겠습니다.

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

반응형