슈팅게임은 날아 오는 적을 무찌르는 게임이다.

 

캐릭터가 좌우로 움직이는게 있고 동서남북 방향으로 움직이는 게임도 있다.

 

아래에서 위를 보는 것을 종스크롤, 왼쪽에서 오른쪽으로 가는 것을 횡스크롤이라고 한다.

 

 

Shoot 'em up - Wikipedia

Subgenre of shooter game Shoot 'em up (also known as shooter or STG[1][2]) is a subgenre of video games within the shooter subgenre in the action genre. There is no consensus as to which design elements compose a shoot 'em up. Some restrict the definition

en.wikipedia.org

시초는 1978년작 스페이스인베이더이다.

 

스페이스 인베이더

한국에서도 많은 사랑을 받은 장르이다. 슈팅게임은 상업용으로 개발되었는데 오락실에서 동전을 넣고 한판을 플레이하는 방식으로 운영되었다. 슈팅게임은 생각보다 고도의 반사신경과 동체시력을 필요로 해서 고수들이 생긴 매니아들의 게임이기도 했다. 나중에는 너무 매니아적으로 발달해서 일반인들이 다가가기 힘들어진 게임들도 생겨났다.

 

 

아케이드 게임 시절을 생각하면 소닉윙즈나 트윈비 정도가 생각난다.

 

생각해보면 모든 2D슈팅게임은 같은 구조를 하고 있다. 플레이어는 캐릭터를 이동시켜서 다가오는 적을 피하고 미사일이나 무기를 발사해서 적을 물리쳐야 한다. 대부분의 무기는 직선 무기다. 가끔 폭탄을 사용해서 화면상의 적들을 섬멸시키기도 한다.

 

동일한 동작을 반복하기에 등장하는 캐릭터나 적이 다르게 생기지 않으면 게임을 구분할 방법이 없다. 그래서 게임의 스토리라는 것도 필요하다.

 

스테이지의 마지막에는 보스가 등장해서 좀 더 고생을 해서 깨야하고 보통 7-8 스테이지 정도를 클리어하면 엔딩과 함께 게임이 종료한다.

 

아주 심플한 구조다. 이번 포스팅에서는 파이썬으로 슈팅게임의 뼈대를 만들어 본다.

 

게임은 가장 기본적인 도형으로 만들 것이다. 도형으로 구현해보는 것은 중요하다. 도형에 적용되는 알고리즘은 캐릭터 스프라이트에도 적용이 되기 때문이다.

 

먼저 전체 소스코드의 흐름을 보고 넘어간다. 부분적으로 설명이 필요하다.

 

슈팅게임 - 빨간 도형이 캐릭터이다. 이동할 수 있고 총알을 발사할 수 있다.

import pygame
import random
import os
import sys
import time

# ----- 게임창 위치설정 -----

win_posx = 700
win_posy = 300
os.environ['SDL_VIDEO_WINDOW_POS'] = "%d,%d" % (win_posx, win_posy)

# ----- 전역 -----

SCREEN_WIDTH = 700
SCREEN_HEIGHT = 500
FPS = 60

score = 0
playtime = 1

# ----- 색상 -----

BLACK = 0, 0, 0
WHITE = 255,255,255
RED = 255, 0, 0
GREEN1 = 25, 102, 25
GREEN2 = 51, 204, 51
GREEN3 = 233, 249, 185
BLUE = 17, 17, 212
BLUE2 = 0, 0, 255
YELLOW = 255, 255, 0
LIGHT_PINK1 = 255, 230, 255
LIGHT_PINK2 = 255, 204, 255

def initialize_game(width, height):
    pygame.init()
    surface = pygame.display.set_mode((width, height))
    pygame.display.set_caption("Pygame Shmup")
    return surface

def game_loop(surface):
    clock = pygame.time.Clock()
    sprite_group = pygame.sprite.Group()
    mobs = pygame.sprite.Group()
    bullets = pygame.sprite.Group()
    player = PlayerShip()
    global player_health
    player_health= 100
    global score
    score = 0
    sprite_group.add(player)
    for i in range(7):
        enemy = Mob()
        sprite_group.add(enemy)
        mobs.add(enemy)

    running = True
    while running:
        for event in pygame.event.get():
            if event.type == pygame.QUIT:
                 running = False
            if event.type == pygame.KEYDOWN:
                if event.key == pygame.K_q:
                    running = False
                if event.key == pygame.K_SPACE:
                    player.shoot(sprite_group, bullets)
            if event.type == pygame.MOUSEBUTTONDOWN:
                player.shoot(sprite_group, bullets)


        sprite_group.update()

        hits = pygame.sprite.groupcollide(mobs, bullets, True, True)
        for hit in hits:
            mob = Mob()
            sprite_group.add(mobs)
            mobs.add(mob)
            score += 10

        hits = pygame.sprite.spritecollide(player, mobs, False)
        if hits:
            print('a mob hits player!')
            player_health -= 1
            if player_health < 0:
                gameover(surface)
                close_game()
                restart()

        surface.fill(LIGHT_PINK1)
        sprite_group.draw(surface)
        score_update(surface)
        pygame.display.flip()
        clock.tick(FPS)
    pygame.quit()
    print('game played: ',playtime)

def score_update(surface):
    font = pygame.font.SysFont('malgungothic',35)
    image = font.render(f'  점수 : {score}  HP: {player_health} ', True, BLUE2)
    pos = image.get_rect()
    pos.move_ip(20,20)
    pygame.draw.rect(image, BLACK,(pos.x-20, pos.y-20, pos.width, pos.height), 2)
    surface.blit(image, pos)

def gameover(surface):
    font = pygame.font.SysFont('malgungothic',50)
    image = font.render('GAME OVER', True, BLACK)
    pos = image.get_rect()
    pos.move_ip(50, int(SCREEN_HEIGHT/2))
    surface.blit(image, pos)
    pygame.display.update()
    time.sleep(2)

def close_game():
    pygame.quit()
    print('Game closed')

def restart():
    screen = initialize_game(SCREEN_WIDTH,SCREEN_HEIGHT)
    game_loop(screen)
    close_game()

class PlayerShip(pygame.sprite.Sprite):
    def __init__(self):
        pygame.sprite.Sprite.__init__(self)
        self.image = pygame.Surface((40,30))
        self.image.fill(RED)
        self.rect = self.image.get_rect()
        self.rect.centerx = int(SCREEN_WIDTH / 2)
        self.rect.centery = SCREEN_HEIGHT - 20
        self.speedx = 0
        self.speedy = 0

    def update(self):
        self.speedx = 0
        self.speedy = 0
        keystate = pygame.key.get_pressed()
        if keystate[pygame.K_a]:
            self.speedx = -10
        if keystate[pygame.K_d]:
            self.speedx = 10
        if keystate[pygame.K_w]:
            self.speedy = -10
        if keystate[pygame.K_s]:
            self.speedy = 10
        self.rect.x += self.speedx
        self.rect.y += self.speedy
        if self.rect.right > SCREEN_WIDTH:
            self.rect.right = SCREEN_WIDTH
        if self.rect.left < 0:
            self.rect.left = 0
        if self.rect.bottom > SCREEN_HEIGHT:
            self.rect.bottom = SCREEN_HEIGHT
        if self.rect.top < 0:
            self.rect.top = 0

    def shoot(self, all_sprites,bullets):
        bullet = Bullet(self.rect.centerx, self.rect.top)
        all_sprites.add(bullet)
        bullets.add(bullet)


class Mob(pygame.sprite.Sprite):
    def __init__(self):
        pygame.sprite.Sprite.__init__(self)
        self.image = pygame.Surface((30,30))
        self.color = random.choice([BLACK, BLUE, RED, GREEN1, YELLOW])
        self.image.fill(self.color)
        self.rect = self.image.get_rect()
        self.rect.x = random.randrange(SCREEN_WIDTH - self.rect.width)
        self.rect.y = random.randrange(-100, -40)
        self.speedy = random.randrange(1, 8)
        self.speedx = random.randrange(-3, 3)
        self.direction_change = False

    def update(self):
        self.rect.x += self.speedx
        self.rect.y += self.speedy

        if self.rect.top > SCREEN_HEIGHT + 10 or self.rect.left < -25 or self.rect.right > SCREEN_WIDTH + 20:
            self.rect.x = random.randrange(SCREEN_WIDTH - self.rect.width)
            self.rect.y = random.randrange(-100, -40)
            self.speedy = random.randrange(3, 8)

class Bullet(pygame.sprite.Sprite):
    def __init__(self, player_x, player_y):
        pygame.sprite.Sprite.__init__(self)
        self.image = pygame.Surface((10,20))
        self.image.fill(GREEN1)
        self.rect = self.image.get_rect()
        self.rect.bottom = player_y
        self.rect.centerx = player_x
        self.speedy = - 10

    def update(self):
        self.rect.y += self.speedy
        if self.rect.bottom < 0:
            self.kill()

if __name__ == '__main__':
    screen = initialize_game(SCREEN_WIDTH,SCREEN_HEIGHT)
    game_loop(screen)
    sys.exit()


 

 

 

 

 

1. 모듈, 전역 변수 등

 

import pygame
import random
import os
import sys
import time

# ----- 게임창 위치설정 -----

win_posx = 700
win_posy = 300
os.environ['SDL_VIDEO_WINDOW_POS'] = "%d,%d" % (win_posx, win_posy)

# ----- 전역 -----

SCREEN_WIDTH = 700
SCREEN_HEIGHT = 500
FPS = 60

score = 0

# ----- 색상 -----

BLACK = 0, 0, 0
WHITE = 255,255,255
RED = 255, 0, 0
GREEN1 = 25, 102, 25
GREEN2 = 51, 204, 51
GREEN3 = 233, 249, 185
BLUE = 17, 17, 212
BLUE2 = 0, 0, 255
YELLOW = 255, 255, 0
LIGHT_PINK1 = 255, 230, 255
LIGHT_PINK2 = 255, 204, 255

파이게임을 사용할 거니까 pygame 을 import 한다. random, os, sys, time 은 파이썬의 기본 라이브러리이다.

 

파이게임의 설치에 관하여는 아래 포스팅을 참고한다.

 

파이썬 게임만들기 1 | 파이게임 설치, 게임창 띄우기 | 게임 루프 , 이벤트 핸들링 | 파이게임 개

파이게임은 파이썬의 멀티미디어 라이브러리로 이 모듈을 설치하면 파이썬으로 게임을 만들 수 있다. 멀티미디어에 최적화된 SDL(Simple DirectMedia Layer)를 파이썬으로 감싸는(Wrapper) 라이브러리이

digiconfactory.tistory.com

os.environ 은 윈도우창의 위치를 설정한다.

 

전역변수에는 윈도우의 사이즈 FPS (1초당 표시되는 프레임의 수), game의 점수가 있다. 게임의 공통적인 사항을 관리할 때 전역변수를 쓴다. 색상 코드는 RGB 색상으로 전부다 사용하지 않아도 몇가지 쓸만한 색들을 모아놓으면 편하다.

 

2. 초기화

def initialize_game(width, height):
    pygame.init()
    surface = pygame.display.set_mode((width, height))
    pygame.display.set_caption("Pygame Shmup")
    return surface

초기화는 pygame의 초기화를 시킨다.

 

화면의 크기를 설명하는 set_mode 는 surface (표면)을 반환한다. 이 surface 가 윈도우의 표면으로 게임의 그래픽은 이 surface를 기본으로 그리게 된다. surface 가 어느 위치에 있느냐를 추적하다 보면 자연스럽게 흐름을 읽을 수 있다.

 

3. 게임루프

 

def game_loop(surface):
    clock = pygame.time.Clock()
    sprite_group = pygame.sprite.Group()
    mobs = pygame.sprite.Group()
    bullets = pygame.sprite.Group()
    player = PlayerShip()
    global player_health
    player_health= 100
    global score
    score = 0
    sprite_group.add(player)
    for i in range(7):
        enemy = Mob()
        sprite_group.add(enemy)
        mobs.add(enemy)

    running = True
    while running:
        for event in pygame.event.get():
            if event.type == pygame.QUIT:
                 running = False
            if event.type == pygame.KEYDOWN:
                if event.key == pygame.K_q:
                    running = False
                if event.key == pygame.K_SPACE:
                    player.shoot(sprite_group, bullets)
            if event.type == pygame.MOUSEBUTTONDOWN:
                player.shoot(sprite_group, bullets)


        sprite_group.update()

        hits = pygame.sprite.groupcollide(mobs, bullets, True, True)
        for hit in hits:
            mob = Mob()
            sprite_group.add(mobs)
            mobs.add(mob)
            score += 10

        hits = pygame.sprite.spritecollide(player, mobs, False)
        if hits:
            print('a mob hits player!')
            player_health -= 1
            if player_health < 0:
                gameover(surface)
                close_game()
                restart()

        surface.fill(LIGHT_PINK1)
        sprite_group.draw(surface)
        score_update(surface)
        pygame.display.flip()
        clock.tick(FPS)
    pygame.quit()
    print('game played: ',playtime)

 

게임 루프안에서 게임이 돌아간다. 모든 게임은 다 루프를 돈다. 루프를 돈다는 것은 게임이 진행되고 있는 것이고 사용자의 입력을 대기하고 있다는 말이다. 사용자의 입력으로 어떤 일이 발생하는데 이 일을 event 이벤트라고 한다. 그 이벤트가 맞다. 게임 루프안에서는 모든 것이 이벤트로 실시간으로 업데이트되고 있다. 업데이트라고 한 것은 이벤트가 저장되지는 않기 때문이다.

 

게임루프의 세부사항은 처음부터 작성될 수 없다. 보통은 객체를 만들고 게임루프에 추가하는 방식으로 진행한다.

 

while 문을 보면 종료 이벤트가 발생하기 전까지 무한 루프를 하고 있음을 알 수 있다.

 

게임루프부터 이해할려고 하지 말고 먼저 객체를 봐야 한다.

 

이 포스팅에서는 일단 순서대로 나열하지만 게임을 작성하기 위해서는 게임루프와 클래스를 오가면서 해야 한다.

 

게임 루프의 세부사항들에 대하여는 아래 파이게임에 대한 포스팅들을 참고한다.

 

 

'프레임워크(FRAMEWORK)/파이게임(PYGAME)' 카테고리의 글 목록

컴퓨터 코딩과 언어학 * 컴퓨터: 파이썬, 자바, C언어 외 * 자연어: 수학

digiconfactory.tistory.com

 

4. 전역함수

 

def score_update(surface):
    font = pygame.font.SysFont('malgungothic',35)
    image = font.render(f'  점수 : {score}  HP: {player_health} ', True, BLUE2)
    pos = image.get_rect()
    pos.move_ip(20,20)
    pygame.draw.rect(image, BLACK,(pos.x-20, pos.y-20, pos.width, pos.height), 2)
    surface.blit(image, pos)

def gameover(surface):
    font = pygame.font.SysFont('malgungothic',50)
    image = font.render('GAME OVER', True, BLACK)
    pos = image.get_rect()
    pos.move_ip(50, int(SCREEN_HEIGHT/2))
    surface.blit(image, pos)
    pygame.display.update()
    time.sleep(2)

def close_game():
    pygame.quit()
    print('Game closed')
    
def restart():
    screen = initialize_game(SCREEN_WIDTH,SCREEN_HEIGHT)
    game_loop(screen)
    close_game()

게임에 공통으로 사용되는 전역함수들이다.

 

점수와 체력을 표시하는 것과 게임오버, 게임 종료, 재시작에 관한 함수들이다.

 

파이게임에서 한글이 지원되는 폰트가 별로 없는데 무료 폰트를 받아서 동일한 폴더에 넣고 사용할 수 있다. 단 폰트는 저작권에 민감하니 배포시에는 주의해야 한다.

 

5. 플레이어 쉽(우주선)

class PlayerShip(pygame.sprite.Sprite):
    def __init__(self):
        pygame.sprite.Sprite.__init__(self)
        self.image = pygame.Surface((40,30))
        self.image.fill(RED)
        self.rect = self.image.get_rect()
        self.rect.centerx = int(SCREEN_WIDTH / 2)
        self.rect.centery = SCREEN_HEIGHT - 20
        self.speedx = 0
        self.speedy = 0

    def update(self):
        self.speedx = 0
        self.speedy = 0
        keystate = pygame.key.get_pressed()
        if keystate[pygame.K_a]:
            self.speedx = -10
        if keystate[pygame.K_d]:
            self.speedx = 10
        if keystate[pygame.K_w]:
            self.speedy = -10
        if keystate[pygame.K_s]:
            self.speedy = 10
        self.rect.x += self.speedx
        self.rect.y += self.speedy
        if self.rect.right > SCREEN_WIDTH:
            self.rect.right = SCREEN_WIDTH
        if self.rect.left < 0:
            self.rect.left = 0
        if self.rect.bottom > SCREEN_HEIGHT:
            self.rect.bottom = SCREEN_HEIGHT
        if self.rect.top < 0:
            self.rect.top = 0

    def shoot(self, all_sprites,bullets):
        bullet = Bullet(self.rect.centerx, self.rect.top)
        all_sprites.add(bullet)
        bullets.add(bullet)

우주선의 동작에 관한 클래스이다.

 

pygame 의 Sprite 클래스를 상속받아서 사용한다. 저런식으로 Sprite 클래스의 __init__ 초기화를 해줘야 스프라이트를 사용할 수 있다. 사용법이 조금 아리송하지만 상당히 편리한 기능들이 들어있는 클래스다. 몇번 사용하다 보면 금방 익숙해질 것이다.

 

update 에서 키조작에 관한 정의를 해두었다. 캐릭터를 조작할 때 기본은 동서남북으로 이동하는 것이고 윈도우 창을 벗어나지 않는 것이다. 그 내용이 update 에 들어 있다. 이것은 캐릭터가 이동하는 게임을 만들때 반복해서 사용가능하다.

 

shoot 은 총을 쏠 때 처리하는 함수이다.

 

6. 몹 클래스

 

흔히 온라인 게임 유저들이 몹을 사냥하러 떠난다. 몹은 게임의 필드에 나타나는 적들이다. 주인공을 잡으러 오기 때문에 이쪽에서 먼저 해치워야 한다.

class Mob(pygame.sprite.Sprite):
    def __init__(self):
        pygame.sprite.Sprite.__init__(self)
        self.image = pygame.Surface((30,30))
        self.color = random.choice([BLACK, BLUE, RED, GREEN1, YELLOW])
        self.image.fill(self.color)
        self.rect = self.image.get_rect()
        self.rect.x = random.randrange(SCREEN_WIDTH - self.rect.width)
        self.rect.y = random.randrange(-100, -40)
        self.speedy = random.randrange(1, 8)
        self.speedx = random.randrange(-3, 3)
        self.direction_change = False

    def update(self):
        self.rect.x += self.speedx
        self.rect.y += self.speedy

        if self.rect.top > SCREEN_HEIGHT + 10 or self.rect.left < -25 or self.rect.right > SCREEN_WIDTH + 20:
            self.rect.x = random.randrange(SCREEN_WIDTH - self.rect.width)
            self.rect.y = random.randrange(-100, -40)
            self.speedy = random.randrange(3, 8)

 

몹클래스의 초기화는 플레이어쉽 클래스와 비슷하다. 캐릭터가 같는 특성을 공유하기 때문이다. 이 클래스의 특징은 몹의 출현위치를 randomize 한다는 것이다.

 

random 모듈을 가져온 것 이 때문이다. 아마 많은 게임들에서 random 함수를 사용한다. 매번 실행시 위치가 바뀌는 것이 게임을 더 흥미롭게 한다. 매번 똑같은 게임은 질리기가 쉽다.

 

7. 총알 클래스

 

class Bullet(pygame.sprite.Sprite):
    def __init__(self, player_x, player_y):
        pygame.sprite.Sprite.__init__(self)
        self.image = pygame.Surface((10,20))
        self.image.fill(GREEN1)
        self.rect = self.image.get_rect()
        self.rect.bottom = player_y
        self.rect.centerx = player_x
        self.speedy = - 10

    def update(self):
        self.rect.y += self.speedy
        if self.rect.bottom < 0:
            self.kill()

총알 클래스는 적을 공격하는 무기이다. 적에게 맞으면 적은 사라지고 점수는 올라간다.

 

이것도 거의 동일한 __init__ 형태를 하고 있다.

 

update 메서드는 매 프레임마다 실행되는 코드이다. 총알이 발사되면 앞으로 나가야 한다. speedy의 속도로 y축을 향해 나아간다. 총알이 y 축 0을 넘어가면, 즉 화면 상단에 총알의 아래 self.rect.bottom 를 통과하면 객체가 소멸한다. self.kill()

 

8. 게임 실행

if __name__ == '__main__':
    screen = initialize_game(SCREEN_WIDTH,SCREEN_HEIGHT)
    game_loop(screen)
    sys.exit()

 

함수를 불러와 게임을 실행한다.

 

sys.exit() 는 system에 모든 자원을 반환하고 프로그램을 종료한다. sys.exit()를 사용하면 그 뒤의 코드는 사용할 수 없으니 넣는 순서에 주의한다.

    running = True
    while running:
        for event in pygame.event.get():
            if event.type == pygame.QUIT:
                 running = False
            if event.type == pygame.KEYDOWN:
                if event.key == pygame.K_q:
                    running = False
                if event.key == pygame.K_SPACE:
                    player.shoot(sprite_group, bullets)
            if event.type == pygame.MOUSEBUTTONDOWN:
                player.shoot(sprite_group, bullets)


        sprite_group.update()

        hits = pygame.sprite.groupcollide(mobs, bullets, True, True)
        for hit in hits:
            mob = Mob()
            sprite_group.add(mobs)
            mobs.add(mob)
            score += 10

        hits = pygame.sprite.spritecollide(player, mobs, False)
        if hits:
            print('a mob hits player!')
            player_health -= 1
            if player_health < 0:
                gameover(surface)
                close_game()
                restart()

        surface.fill(LIGHT_PINK1)
        sprite_group.draw(surface)
        score_update(surface)
        pygame.display.flip()
        clock.tick(FPS)

* 마지막으로 게임 루프의 while 루프를 다시한번 보자.

 

event 부분은 딱히 설명이 필요없다. 무기 발사 버튼을 두개 설정했다. 스페이스 키와 마우스 버튼이다. 오른쪽, 왼쪽 다 해당한다.

 

스프라이트 클래스의 특징은 그룹으로 묶어서 사용가능하다는 것인데 적이 총알에 맞았을 때, 플레이어 캐릭터가 몹에 부딪혔을 때를 각각 if 문으로 구현한다. 스프라이트 콜라이드( spritecollide) 기능은 적과의 충돌을 감지해준다. 그룹간의 비교도 가능하다. 매 프레임 마다 화면상의 모든 몹들과 총알 그리고 플레이어의 충돌 여부를 확인한다.

 

우리에게 잘 보이지 않지만 컴퓨터는 짧은 시간에 이렇게 많은 일을 하고 있는 것이다. 따라서 프로그래밍을 잘하려면 역설적으로 시간을 느리게 봐야 한다. 매 프레임에 일어나는 일들을 하나씩 다 지켜봐야 한다.

 

 


이 포스팅은 슈팅게임의 뼈대를 만들어 봤다. 이 다음에 배경과 스프라이트를 입히는 것은 아래 다음 포스팅을 참고한다

 

digiconfactory.tistory.com/301

 

파이썬 게임 만들기 | 슈팅게임 (SHMUP) | 스프라이트 | 효과음, 배경음넣기 | 스프라이트 입히기

이전의 포스트에서 슈팅게임의 뼈대를 만들어 두었다. 이제 그 위에 그럴듯한 옷을 입힐 차례다. 파이썬 게임 만들기 | 슈팅 게임 만들기 (SHMUP) | 파이게임 | 슈팅게임의 뼈대 | Sprite 객체 사용하

digiconfactory.tistory.com

 

 

*참고한 유튜브 채널

 

KidsCanCode

Everyone should learn to code! Learning to program a computer is fun, rewarding, and empowering. Find out more at http://kidscancode.org/

www.youtube.com

 

공유하기

facebook twitter kakaoTalk kakaostory naver band