12.1 뱀 게임 만들기
- 12.1.1 뱀 게임이란?
- 12.1.2 프로젝트 시작하기
- 12.1.3 외부 라이브러리 설치하기
- 12.1.4 파이게임 라이브러리 사용하기
- 12.1.5 게임 데이터 모델 정의하기
- 12.1.6 개체의 동작 정의하기
- 12.1.7 더 연습하기
프로그래밍 입문자들에게 실습 프로젝트로 간단한 게임 프로그램을 만들어 볼 것을 권한다. 비동기 처리(입력·출력·계산 등을 동시에 처리하는 것), 그래픽 출력, 논리적인 실행 과정 등 많은 것을 배우고 익힐 수 있기 때문이다. 게임을 좋아한다면 프로그래밍을 더 흥미롭게 연습할 수도 있다. 뱀 게임은 비교적 간단해서 파이썬 언어를 막 배운 여러분이 도전하기에 적절하다.
12.1.1 뱀 게임이란?
뱀 게임은 게임판 위에서 여러 개의 블록으로 이루어진 뱀을 조종해 사과를 먹도록 하는 게임이다. 뱀의 머리가 벽이나 몸에 부딛히면 게임이 끝나는데, 뱀이 사과를 먹을 때마다 뱀의 길이가 길어져서 점점 어려워진다.
그림 12-1 뱀 게임 (playsnake.org)
게임을 만들려면 어떤 게임인지 알아야 한다. 웹 브라우저로 https://playsnake.org 웹사이트에 접속해 플레이해 볼 수 있다. 간단한 게임이지만 직접 만들어보려고 하면 어떻게 해야 할지 막막할 것이다. 예제를 따라서 한 번 만들어 보면 그렇게 어렵지 않을 것이다. 그리고 배운 것을 응용해 다른 보드 게임도 직접 만들어볼 수 있을 것이다. 도전해보자!
12.1.2 프로젝트 시작하기
파이참에서 뱀 게임 프로젝트를 새로 만드는 것부터 시작하자. (1.3절 참고)
파이참에 예제 실습용 프로젝트(study)가 열려 있다면 ‘File -> Close Project’를 클릭해 닫아 두자.
그림 12-2 이전에 열어 둔 프로젝트 닫기
프로젝트를 닫으면 파이참 시작 화면이 나온다. ‘Create New Project’ 버튼을 클릭해 새 프로젝트를 생성한다.
그림 12-3 파이참 시작 화면
프로젝트 디렉터리를 ‘snake’라고 입력하고, ‘Create’ 버튼을 누르면 된다. ‘Project Interpreter’는 파이썬 인터프리터를 선택하는 것인데 ‘New Environment using Virtualenv’를 고르면 된다. (기본으로 선택되어 있을 것이다.)
그림 12-4 파이참 시작 화면
Virtualenv란?
프로젝트마다 실행 환경 구성(어떤 파이썬 인터프리터를 사용할 것인지, 어떤 라이브러리를 설치하여 이용할 것인지 등)을 다르게 설정해야 할 때가 있다. Virtualenv는 서로 독립적인 파이썬 실행 환경 구성을 설정할 수 있도록 도와주는 도구다. 파이참을 이용하면 따로 준비하지 않고도 Virtualenv를 간편하게 이용할 수 있다.
새 프로젝트를 만들었으면, 게임을 만드는 데 필요한 라이브러리를 설치하자.
12.1.3 외부 라이브러리 설치하기
그동안은 파이썬 인터프리터에 내장된 표준 라이브러리만 사용해 봤다. 게임을 만들려면 표준 라이브러리만으로는 어렵다. 외부 라이브러리를 가져와 사용하는 방법을 알아 보자.
파이게임 라이브러리
파이게임(Pygame)이라는 라이브러리를 이용해 뱀 게임을 만들어 볼 것이다. 파이게임은 게임 제작에 필요한 여러 가지 기능을 모아 놓은 라이브러리다. 뱀게임에서는 그래픽 출력 기능과 비동기 키 입력 처리 기능을 사용할 것이다.
- 그래픽 출력 기능: 그동안은
print()
함수를 이용해 텍스트 환경에서만 정보를 출력했다. 하지만 게임을 만들려면 그래픽을 출력할 수 있어야 한다. - 비동기 키 입력 처리 기능:
input()
함수로도 사용자의 키보드 입력을 받을 수 있다. 하지만input()
함수를 사용하면 사용자가 키를 누르고 엔터 키를 입력할 때까지 프로그램이 정지된다. 프로그램을 멈추지 않고(비동기적으로) 키를 입력받을 방법이 필요하다.
파이게임은 오늘날의 상업용 게임을 만들기에는 충분하지 않을 수 있지만, 간단한 프로그래밍 실습용 게임 제작에는 쓰기에는 아주 좋다. 파이게임에 대한 더 자세한 내용은 파이게임 공식 웹사이트(https://www.pygame.org)에서 알아볼 수 있다.
파이참으로 외부 라이브러리 설치하기
그런데 파이게임 라이브러리는 파이썬의 표준 라이브러리가 아니다. 별도로 설치해서 프로젝트에 포함시켜야 한다. 파이게임 라이브러리는 어디서 어떻게 다운로드할 수 있을까?
파이썬 소프트웨어 재단이 운영하는 파이썬 패키지 인덱스(Python Package Index, PyPI)라는 저장소에는 파이썬 프로그래머들이 만들어 둔 수많은(2019년 기준 약 17만 9천 개의) 라이브러리 패키지가 등록되어 있다. 저장소는 무료로 사용할 수 있고, 저장소에 등록된 라이브러리도 대부분 오픈소스 라이선스로 공개된 것이어서 자유롭게 사용할 수 있다. 파이참으로 저장소에서 라이브러리를 다운로드·설치할 수 있다.
파이참의 상단 메뉴에서 ‘File -> Settings’를 클릭해 설정 창을 연다.
그림 12-5 파이참 설정 창 열기
설정 창의 메뉴에서 ‘Project: snake -> Project Interpreter’를 찾아 클릭한다. 그러면 설정 창의 오른쪽 패널에서 프로젝트 환경 구성을 볼 수 있다. 지금은 ‘pip’와 ‘setuptools’라는 라이브러리만 기본으로 설치되어 있을 것이다. 오른쪽에 있는 ‘+’ 버튼을 눌러 라이브러리를 추가한다.
그림 12-6 파이참 설정 창의 프로젝트 환경 구성
라이브러리 검색 창이 나오면 ‘pygame’이라고 입력한다. 가장 위에 검색되는 pygame 패키지를 선택하고, 아래쪽의 ‘Install Package’ 버튼을 누른다.
그림 12-7 라이브러리 검색하고 설치하기
잠시 기다리면 설치가 완료되고 프로젝트 환경 구성에 ‘pygame’이 추가된 것을 확인할 수 있다. 설치가 되었으면 ‘OK’ 버튼을 눌러 설정 창을 닫으면 된다.
그림 12-8 라이브러리 검색하고 설치하기
12.1.4 파이게임 라이브러리 사용하기
이제 파이게임 라이브러리로 그래픽을 출력하고 사용자의 입력을 실시간으로 처리할 수 있다. 파이게임의 기초적인 사용법을 뱀 게임 만드는 데 필요한 만큼만 간단히 알아보자.
게임 화면 창 열기
뱀 게임은 텍스트 출력 창 대신 그래픽 창을 이용할 것이다. 뱀 게임 프로젝트에 snake.py
라는 파일을 만들고 다음 코드를 따라 입력하자.
코드 12-1 게임 화면 창 열기
import pygame # ❶ 파이게임 모듈 임포트하기
import time
SCREEN_WIDTH = 400 # ❷ 게임 화면의 너비
SCREEN_HEIGHT = 80 # 게임 화면의 높이
pygame.init() # ❸ 파이게임을 사용하기 전에 초기화한다.
# ❹ 지정한 크기의 게임 화면 창을 연다.
screen = pygame.display.set_mode((SCREEN_WIDTH, SCREEN_HEIGHT))
time.sleep(3) # ❺ 3초동안 기다린다
❶ 파이게임 라이브러리도 표준 라이브러리와 마찬가지로 임포트해야 사용할 수 있다. import 문으로 임포트하면 된다. ❸ 파이게임을 사용하려면 먼저 pygame.init()
메서드를 실행해 게임 실행에 필요한 설정을 초기화해야 한다. 파이게임 초기화를 한 뒤 ❹ pygame.display.set_mode()
함수로 게임 화면 창을 열 수 있다. 인자로는 게임 창의 너비와 높이를 담은 튜플을 전달한다. 이 함수는 화면 객체를 반환해 주는데, 화면에 무언가를 그릴 때 사용해야 하므로 screen
이라는 변수에 대입해 두자. 화면 크기는 ❷와 같이 이름 붙여 정의해두면 편리하다. 창을 연 뒤에 프로그램이 바로 종료되면 창이 열린 것을 확인할 수 없다. time 모듈의 sleep()
함수를 이용하면 프로그램이 지정한 초 동안 아무 것도 하지 않고 기다리도록 할 수 있다. ❺ 이 함수로 3초동안 기다렸다가 종료되도록 하자.
프로그램을 실행해보면 다음과 같이 너비 400 픽셀, 높이 80 픽셀 크기의 검은색 창이 열렸다가 3초 후에 닫힐 것이다.
그림 12-9 게임 화면 창 열기
RGB 색 모델
이제 화면에 무언가 다른 것을 출력해 볼 텐데, 그러기 위해서는 컴퓨터에서 색을 표현하는 방법을 알아야 한다.
컴퓨터에서 색을 나타낼 때는 일반적으로 ‘RGB 색 모델’을 이용한다. 사람의 눈은 적(Red)·녹(Green)·청(Blue) 세 가지 빛 수용체로 색을 인식한다. RGB 색 모델은 적·녹·청 세 가지 빛을 섞어 사람이 인식할 수 있는 다양한 색을 표현한다. 빛의 밝기는 가장 어두운 0부터 가장 밝은 255 사이의 범위로 지정한다.
다음은 여러 가지 색을 RGB 색 모델로 정의한 것이다. 각 색마다 세 가지 빛의 강도를 튜플로 묶었다.
코드 12-2 여러 가지 색
RED = 255, 0, 0 # 적색: 적 255, 녹 0, 청 0
GREEN = 0, 255, 0 # 녹색: 적 0, 녹 255, 청 0
BLUE = 0, 0, 255 # 청색: 적 0, 녹 0, 청 255
PURPLE = 127, 0, 127 # 보라색: 적 127, 녹 0, 청 127
BLACK = 0, 0, 0 # 검은색: 적 0, 녹 0, 청 0
GRAY = 127, 127, 127 # 회색: 적 127, 녹 127, 청 127
WHITE = 255, 255, 255 # 하얀색: 적 255, 녹 255, 청 255
이처럼 다양한 색을 정의할 수 있다. 뱀 게임에서는 적색, 녹색, 하얀색만 있으면 된다.
사각형 그리기
화면에 여러 가지 색의 사각형을 그려 보자. 사각형은 pygame.draw.rect()
함수로 그릴 수 있다. snake.py
소스코드를 다음 예제와 같이 수정하고 실행해보자.
코드 12-3 게임 화면 창 열기
# (...) 색 정의와 파이게임 초기화 코드 생략
# 화면 전체에 하얀 사각형 그리기
rect = pygame.Rect((0, 0), (SCREEN_WIDTH, SCREEN_HEIGHT)) # ❶
pygame.draw.rect(screen, WHITE, rect) # ❷
# 화면 왼쪽 위에 녹색 정사각형 그리기
rect = pygame.Rect((0, 0), (40, 40))
pygame.draw.rect(screen, GREEN, rect)
# 화면 오른쪽 아래에 적색 직사각형 그리기
rect = pygame.Rect((340, 60), (60, 20))
pygame.draw.rect(screen, RED, rect)
pygame.display.update() # ❸ 화면 새로고침
time.sleep(3)
프로그램을 실행하면 그림 12-3과 같이 하얀 배경에 녹색 정사각형과 적색 직사각형이 그려질 것이다. ❷ pygame.draw.rect()
함수는 인자로 사각형을 그릴 화면(pygame.display.set_mode()
함수가 반환한 것), 색, 사각형 정보를 전달받아 그린다. 사각형 객체는 ❶과 같이 pygame.Rect
클래스를 이용해 정의할 수 있다. pygame.Rect
인스턴스를 생성할 때는 사각형의 위치 좌표쌍(x, y)과 크기(너비, 높이)를 각각 튜플로 전달해주면 된다. 화면에 무언가를 그린 뒤에는 ❸과 같이 pygame.display.update()
함수로 화면을 갱신해주어야 출력된다.
그림 12-10 화면에 사각형 출력하기
블록 그리기
뱀 게임에서는 여러 개의 사각형 블록을 이용해 뱀과 사과를 표현한다. 여러 개의 블록을 출력할 수 있도록 게임 화면의 크기를 400 픽셀 x 400 픽셀로 늘리자. 그리고 배경과 블록을 그리는 함수를 정의해 두자.
코드 12-4 블록을 그리는 함수 정의하기
SCREEN_WIDTH = 400
SCREEN_HEIGHT = 400
BLOCK_SIZE = 20
def draw_background(screen):
"""게임의 배경을 그린다."""
background = pygame.Rect((0, 0), (SCREEN_WIDTH, SCREEN_HEIGHT))
pygame.draw.rect(screen, WHITE, background)
def draw_block(screen, color, position):
"""position 위치에 color 색깔의 블록을 그린다."""
block = pygame.Rect((position[1] * BLOCK_SIZE, position[0] * BLOCK_SIZE),
(BLOCK_SIZE, BLOCK_SIZE))
pygame.draw.rect(screen, color, block)
배경을 그리는 함수 draw_background()
는 앞서 작성해 본 코드와 동일하므로 이해하기 어렵지 않을 것이다. 블록을 그리는 함수 draw_block()
은 화면, 블록의 색, 블록의 위치(y, x 튜플)를 인자로 전달받아 블록을 그린다. 블록 크기는 (BLOCK_SIZE, BLOCK_SIZE)
로 고정된다. 블록 하나당 크기가 20 x 20이므로, 블록의 위치는 (position[1] * 20, position[0] * 20)
과 같이 계산하여 위치 포인트 하나당 20 픽셀씩 띄워 출력하도록 했다. 게임 화면의 크기가 400 x 400 이므로 400 개의 블록을 출력할 수 있다.
이 함수를 이용해 블록을 출력해 보자.
코드 12-5 블록 그리기
# (...) 함수 정의, 파이게임 초기화 코드 생략
draw_background(screen)
draw_block(screen, RED, (1, 1))
draw_block(screen, RED, (3, 1))
draw_block(screen, RED, (5, 1))
draw_block(screen, RED, (7, 1))
draw_block(screen, GREEN, (12, 10))
draw_block(screen, GREEN, (12, 11))
draw_block(screen, GREEN, (12, 12))
draw_block(screen, GREEN, (12, 13))
pygame.display.update()
time.sleep(3)
프로그램을 실행하면 그림 12-11과 같이 블록이 출력될 것이다.
그림 12-11 블록 그리기
사용자의 입력을 받고 게임 창 닫기
화면에 예쁜 블록을 출력할 수 있게 되긴 했지만 아직 부족한 점이 많다. 먼저, 게임 화면이 3초 후에 바로 닫혀 버리는 문제가 크다. 이래서는 게임을 할 수 없다. while 문을 이용해서 창을 열어둔 채로 무한히 계속 기다리게 하면 어떨까? 프로그램 마지막의 time.sleep(3)을 아래 코드로 바꾸어 보자.
코드 12-6 게임 화면 창을 무한히 계속 열어두기
# 무한히 계속, 아무 일도 하지 않는다.
while True:
pass
수정한 프로그램을 실행해보면, 창이 계속 열려 있긴 할 것이다. 하지만 그대로 멈춘 채 아무 것도 할 수 없다. 특히, 닫기 버튼을 눌러도 게임 창이 닫히지 않을 것이다. 프로그램이 사용자의 입력을 처리하지 않고 있기 때문이다. 닫기 버튼을 여러 번 누르면, 운영체제가 프로그램을 강제로 종료시켜 버린다.
닫기 버튼이 올바르게 동작하도록 하려면 ‘이벤트(event)’를 다룰 수 있어야 한다. 이벤트란 의미 있는 사건을 정의해 둔 것이다. 이벤트를 이용하면 특정한 사건이 일어났을 때 그에 맞는 코드를 실행하도록 준비해 둘 수 있다. 파이게임은 사용자의 키보드 입력, 마우스 움직임, 프로그램 종료 요청 등 다양한 상황에 대응하는 이벤트를 제공한다. 프로그램 마지막의 무한 반복 코드를 수정하여, 프로그램 종료 요청 이벤트가 발생했을 때 실행할 코드를 정의해 보자.
코드 12-7 게임 종료 이벤트 처리하기
# 종료 이벤트가 발생할 때까지 게임을 계속 진행한다
while True:
events = pygame.event.get() # ❶ 발생한 이벤트 목록을 읽어들인다
for event in events: # ❷ 이벤트 목록을 순회하며 각 이벤트를 처리한다
if event.type == pygame.QUIT: # ❸ 종료 이벤트가 발생한 경우
exit() # ❹ 게임을 종료한다
❶ pygame.event.get()
함수는 마지막으로 이 함수를 호출한 후 다시 호출될 때까지 발생한 이벤트를 리스트에 담아 반환한다. ❷ 이벤트들이 리스트에 들어 있으므로 for 문으로 순회하며 처리해야 한다. 이벤트는 키보드 입력, 마우스의 움직임 등 여러 가지 종류가 있다. 이벤트의 종류는 type
속성으로 확인할 수 있다. ❸ 발생한 이벤트의 종류가 종료 이벤트인 pygame.QUIT
인지 if 문으로 확인한다. ❹ 프로그램은 exit()
함수를 실행하여 종료할 수 있다. 무한히 반복되므로, 프로그램 실행 중 다양한 이벤트가 발생하더라도 계속 적절히 처리하며 실행할 수 있을 것이다. 수정한 프로그램을 실행해 보면 종료 버튼을 눌렀을 때 게임이 정상적으로 종료되는 것을 확인할 수 있다.
키보드 입력 이벤트 처리하기
키보드 입력 이벤트를 처리하면 게임의 조작을 구현할 수 있다. 블록을 하나만 남겨 두고, 키가 입력될 때마다 블록이 움직이도록 해 보자. 프로그램을 다음과 같이 수정한다.
코드 12-8 키보드 입력 이벤트 처리하기
# (...) 색 정의, 배경 그리기 함수, 블록 그리기 함수, 파이게임 초기화 코드 생략
block_position = [0, 0] # ❶ 블록의 위치 (y, x)
# 종료 이벤트가 발생할 때까지 게임을 계속 진행한다
while True:
events = pygame.event.get()
for event in events:
if event.type == pygame.QUIT:
exit()
if event.type == pygame.KEYDOWN: # ❷ 이벤트 종류가 키 입력 이벤트이면
block_position[1] += 1 # 블록을 오른쪽으로 한 칸 움직인다
# ❸ 화면을 계속 새로 그린다
draw_background(screen)
draw_block(screen, GREEN, block_position)
pygame.display.update()
수정된 부분을 살펴보자. ❶ 블록을 움직이려면 블록의 위치를 기억하는 변수가 필요하다. block_position
이라는 변수에 y, x 좌표를 담은 리스트로 정의했다. ❷ 이벤트를 처리할 때 키 입력 이벤트 종류인 pygame.KEYDOWN
도 처리하도록 했다. 키 입력 이벤트가 일어날 때마다 블록의 x좌표를 1씩 증가시킨다. 블록의 위치가 오른쪽으로 옮겨질 것이다. ❸ 키가 입력되면 화면에 그릴 내용이 변화하기 때문에, 블록이 움직이는 것을 보여주려면 화면을 계속 새로 그려야 한다. 배경을 그리는(화면을 지우는) 코드와 블록을 그리는 코드를 무한 반복 while 문 속으로 옮겼다. 이제 이벤트 처리와 화면 갱신이 무한히 계속 반복될 것이다. 프로그램을 실행해보면, 아무 키나 입력할 때마다 블록이 오른쪽으로 움직이는 것을 확인할 수 있다.
블록을 움직이는 것이 멋지긴 하지만, 한 방향으로만 움직이는 것은 마음에 들지 않는다. 입력된 키가 무엇인지 확인하고, 화살표 키일 때 그에 맞는 방향으로 블록을 움직이게 해 보자. 입력된 키는 이벤트 객체의 key
속성으로 확인할 수 있고, 화살표 키는 방향별로 pygame.K_UP
, pygame.K_DOWN
, pygame.K_LEFT
, pygame.K_RIGHT
로 정의되어 있다. pygame.KEYDOWN
이벤트를 처리하는 부분을 다음과 같이 수정하자.
코드 12-9 화살표 키로 블록 움직이기
while True:
events = pygame.event.get()
for event in events:
if event.type == pygame.QUIT:
exit()
if event.type == pygame.KEYDOWN:
if event.key == pygame.K_UP: # 입력된 키가 위쪽 화살표 키인 경우
block_position[0] -= 1 # 블록의 y 좌표를 1 뺀다
elif event.key == pygame.K_DOWN: # 입력된 키가 아래쪽 화살표 키인 경우
block_position[0] += 1 # 블록의 y 좌표를 1 더한다
elif event.key == pygame.K_LEFT: # 입력된 키가 왼쪽 화살표 키인 경우
block_position[1] -= 1 # 블록의 x 좌표를 1 뺀다
elif event.key == pygame.K_RIGHT: # 입력된 키가 왼쪽 화살표 키인 경우
block_position[1] += 1 # 블록의 x 좌표를 1 더한다
이제 화살표 키를 눌러 블록을 원하는 방향으로 움직일 수 있게 되었다. 프로그램을 실행해 확인해 보자. 초록색 블록이 잘 움직이는가?
그림 12-12 화살표 키로 블록 움직이기
일정한 시간마다 블록 움직이기
뱀 게임을 다시 한 번 해보고 뱀이 어떻게 움직이는지 살펴보자. 사용자가 키를 눌러 뱀을 조정하는 것은 뱀의 방향 뿐이다. 뱀은 키를 입력하지 않더라도 앞으로 계속 움직인다. 블록이 스스로 움직이게 하려면 어떻게 해야 할까? 시간은 저절로 흐르므로, 흐른 시간을 측정해서 일정한 시간마다 블록이 움직이게 하면 될 것 같다. 다음은 datetime
모듈을 이용해 시간을 측정하고 블록이 자동으로 움직이도록 프로그램을 수정한 것이다.
코드 12-10 일정한 시각마다 블록 움직이기
from datetime import datetime
from datetime import timedelta
# (...) 색 정의, 배경 그리기 함수, 블록 그리기 함수, 파이게임 초기화 코드 생략
block_position = [0, 0]
last_moved_time = datetime.now() # ❶ 마지막으로 블록을 움직인 때
while True:
events = pygame.event.get()
for event in events:
if event.type == pygame.QUIT:
exit()
if timedelta(seconds=1) <= datetime.now() - last_moved_time: # ❷ 블록을 움직이고 1초가 지났으면,
block_position[1] += 1 # 블록을 오른쪽으로 움직인다
last_moved_time = datetime.now() # ❸ 블록을 움직인 시각을 지금으로 갱신한다
draw_background(screen)
draw_block(screen, GREEN, block_position)
pygame.display.update()
❶ 마지막으로 블록을 움직인 때가 언제인지 기록하기 위한 변수를 정의한다. datetime.now()
메서드를 이용해 현재 시각으로 초기화했다. ❷ while 문 안에서 매 반복마다 datetime.now() - last_moved_time
을 계산해 시간이 얼마나 지났는지 확인한다. 시간이 1초(timedelta(seconds=1)
) 이상 지났으면 블록을 움직이도록 한다. 그리고 ❸ 블록을 움직인 시각을 갱신해 둔다. 그래야 다음 번에 시간이 얼마나 지났는지도 올바르게 확인할 수 있다. 프로그램을 위 코드와 같이 수정하고, 블록이 1초마다 오른쪽으로 저절로 움직이는지 확인해보자.
화살표 키를 눌러 블록이 움직일 방향을 지정할 수 있다면 더 좋겠다. 블록이 움직일 방향을 기억할 변수를 정의하고, 사용자의 키가 입력되었을 때 방향을 바꿀 수 있게 해 보자.
코드 12-11 일정한 시각마다 지정한 방향으로 블록 움직이기
# (...) 색 정의, 배경 그리기 함수, 블록 그리기 함수, 파이게임 초기화 코드 생략
# ❶ 방향키 입력에 따라 바꿀 블록의 방향
DIRECTION_ON_KEY = {
pygame.K_UP: 'north',
pygame.K_DOWN: 'south',
pygame.K_LEFT: 'west',
pygame.K_RIGHT: 'east',
}
block_direction = 'east' # ❷ 블록의 방향
block_position = [0, 0]
last_moved_time = datetime.now()
while True:
events = pygame.event.get()
for event in events:
if event.type == pygame.QUIT:
exit()
if event.type == pygame.KEYDOWN:
# ❸ 입력된 키가 화살표 키면,
if event.key in DIRECTION_ON_KEY:
# ❹ 블록의 방향을 화살표 키에 맞게 바꾼다
block_direction = DIRECTION_ON_KEY[event.key]
if timedelta(seconds=1) <= datetime.now() - last_moved_time:
if block_direction == 'north': # ❺ 1초가 지날 때마다
block_position[0] -= 1 # 블록의 방향에 따라
elif block_direction == 'south': # 블록의 위치를 변경한다
block_position[0] += 1
elif block_direction == 'west':
block_position[1] -= 1
elif block_direction == 'east':
block_position[1] += 1
last_moved_time = datetime.now()
draw_background(screen)
draw_block(screen, GREEN, block_position)
pygame.display.update()
코드가 조금 길어졌지만 충분히 이해할 수 있을 것이다. ❶ 입력된 키가 무엇인지에 따라 방향을 적절히 바꿀 수 있도록 화살표 키와 그에 대응하는 방향을 사전으로 정의했다. 그리고 ❷ 블록의 방향을 기억할 변수도 정의했다. ❸ 키보드 입력 이벤트가 발생했을 때, 입력된 키가 방향키인지 확인한다. 사전의 키 검사는 in
연산자로 확인할 수 있다. 방향키가 맞으면, ❹ 방향키에 대응하는 방향(사전의 값)을 블록의 방향 변수에 대입한다. 마지막으로, 1초마다 블록을 이동시키는 코드는 ❺ 블록의 방향에 따라서 적절히 위치를 변경하도록 한다. 프로그램을 실행하면 이제 블록이 1초에 한 번씩, 방향키로 입력한 방향을 향해 움직일 것이다.
이것으로 파이게임의 기초 사항을 모두 알아보았다. 코드 12-11의 while 문이 크게 세 부분으로 구성된 것을 확인하자. 이벤트를 처리하는 부분, 시간에 다른 게임 진행을 처리하는 부분, 게임 그래픽을 출력하는 부분. 게임 프로그램의 기본 절차는 모두 이 세 부분으로 구성된다. 남은 것은 게임에서 사용할 데이터 모델을 정의하고 게임의 규칙과 동작을 채워 넣는 것이다.
12.1.5 게임 데이터 모델 정의하기
개체 정의하기
게임에서 사용할 데이터 모델을 정의해 보자. 종이와 펜을 꺼내 뱀 게임을 구성하는 개체에 어떤 것이 있는지 적으면 된다. 물론, 뱀과 사과가 있어야 한다. 그리고 잘 보이지 않아서 빠트리기 쉽지만 뱀과 사과를 놓아 둘 게임판도 있어야 한다.
- 뱀
- 사과
- 게임판
이 개체들을 뱀 게임 소스 코드에 클래스로 정의해 두자.
코드 12-12 뱀 게임에서 사용할 데이터 모델 정의하기
class Snake:
"""뱀 클래스"""
pass
class Apple:
"""사과 클래스"""
pass
class GameBoard:
"""게임판 클래스"""
pass
속성 정의하기
게임을 구성하는 개체를 정했으면, 각 개체를 구성하는 속성을 생각나는 대로 써 보자. 뱀과 사과는 위치와 색이 있어야 한다. 뱀은 움직이는 방향도 필요하지만, 사과는 움직이지 않으니 방향이 필요하지 않다. 게임판은 가로·세로 넓이와 그 위에 올려둔 뱀과 사과를 속성으로 갖고 있어야 한다.
- 뱀
- 색
- 머리와 몸의 위치
- 이동 방향
- 사과
- 색
- 위치
- 게임판
- 가로 크기
- 세로 크기
- 올려둔 뱀
- 올려둔 사과
개체의 속성을 클래스의 속성으로 정의하자.
코드 12-13 뱀 게임에서 사용할 데이터 모델 정의하기
class Snake:
"""뱀 클래스"""
color = GREEN # 뱀의 색
def __init__(self):
self.positions = [(9, 6), (9, 7), (9, 8), (9, 9)] # 뱀의 위치
self.direction = 'north' # 뱀의 방향
class Apple:
"""사과 클래스"""
color = RED # 사과의 색
def __init__(self, position=(5, 5)):
self.position = position # 사과의 위치
class GameBoard:
"""게임판 클래스"""
width = 20 # 게임판의 너비
height = 20 # 게임판의 높이
def __init__(self):
self.snake = Snake() # 게임판 위의 뱀
self.apple = Apple() # 게임판 위의 사과
뱀의 색, 사과의 색, 게임판의 크기는 고정된 값이며 모든 인스턴스에서 동일하게 사용할 것이므로 클래스 속성으로 정의했다. 반면에 뱀의 위치와 방향, 사과의 위치, 게임판 위의 올라온 뱀과 사과는 계속 변하는 값이고 인스턴스마다 다를 수 있으므로 인스턴스 속성으로 정의했다. 인스턴스 속성들은 __init__()
함수에서 적당한 기본값을 정의해 두었다.
뱀의 위치(Snake.positions
)는 리스트로 정의한 것을 확인하자. 뱀이 여러 개의 블록으로 구성될 것이기 때문에 각 블록의 위치를 리스트에 넣어 표현했다. 뱀이 사과를 먹으면 리스트에 블록 위치를 더 추가해서 뱀의 길이를 늘릴 것이다.
12.1.6 개체의 동작 정의하기
게임 데이터 모델을 클래스로 정의해 보았다. 이제 이 클래스에 동작(메서드)을 정의하면 게임이 완성될 것이다. 각 클래스가 어떤 일을 해야 하는지 생각해보자.
- 뱀
- 자기 자신 그리기
- 현재 방향으로 움직이기
- 방향 바꾸기
- 자라나기
- 사과
- 자기 자신 그리기
- 게임판
- 자기 자신 그리기
- 자기 위에 뱀과 사과 놓기 (
__init__()
함수에 정의한 동작) - 사과가 없어지면 새로 놓기
- 게임을 한 차례 진행하기
개체 그리기
먼저 각 클래스에 공통적으로 존재하는 자기 자신을 화면에 그리는 메서드( draw()
)부터 정의해 보자.
코드 12-14 draw()
메서드 정의하기
class Snake:
# (...)
def draw(self, screen):
"""뱀을 화면에 그린다."""
for position in self.positions: # ❶ 뱀의 몸 블록들을 순회하며
draw_block(screen, self.color, position) # 각 블록을 그린다
class Apple:
# (...)
def draw(self, screen):
"""사과를 화면에 그린다."""
draw_block(screen, self.color, self.position) # ❷
class GameBoard:
# (...)
def draw(self, screen):
"""화면에 게임판의 구성요소를 그린다."""
self.apple.draw(screen) # ❸ 게임판 위의 사과를 그린다
self.snake.draw(screen) # 게임판 위의 뱀을 그린다
각 draw() 메서드는 화면 객체를 전달받아, draw_block()
함수를 이용해 화면에 블록을 그린다. ❶ 뱀은 여러 개의 블록으로 이루어져 있으므로 for 문으로 순회하며 그리도록 했다. ❷ 사과는 블록 한 개 뿐이어서 블록을 하나만 그리면 된다. ❸ 게임판은 자기 위에 올라와 있는 사과와 뱀을 그리는 역할을 한다. 따라서 게임판의 draw()
메서드를 한 번 호출할 때마다 게임 화면의 모든 구성요소를 그릴 수 있다.
게임 구성요소들을 생성하고 실제로 화면에 그려 보자. 앞서 정의한 움직이는 블록은 삭제하고, 그 대신 게임판 인스턴스를 만들어 화면에 그린다.
코드 12-15 게임의 구성 요소 그리기
# (...) 클래스 정의, 파이게임 초기화 코드 생략
game_board = GameBoard() # ❶ 게임판 인스턴스를 생성한다
while True:
events = pygame.event.get()
for event in events:
if event.type == pygame.QUIT:
exit()
draw_background(screen)
game_board.draw(screen) # ❷ 화면에 게임판을 그린다
pygame.display.update()
프로그램을 실행하면 다음과 같이 뱀과 사과가 놓인 게임판이 출력될 것이다.
그림 12-13 게임의 구성 요소 그리기
뱀 움직이기
이제 뱀을 움직일 수 있게 해 보자. 뱀이 한 칸 기어가면, 뱀의 몸을 구성하는 블록은 어떻게 이동하게 될까? 게임판 위에 뱀이 아래와 같이 놓여 있다고 생각해보자. 1이 뱀의 머리, 4가 뱀의 꼬리다.
그림 12-14 뱀의 위치
뱀이 동쪽(오른쪽)으로 한 칸씩 기어가면, 다음과 같이 움직일 것이다.
그림 12-15 뱀이 동쪽으로 기어갈 때
뱀이 북쪽으로 한 칸씩 기어가면, 다음과 같이 움직일 것이다.
1
1 2
1 => 2 => 3
4321 => 432 43 4
그림 12-16 뱀이 북쪽으로 기어갈 때
이 움직임에서 규칙을 찾을 수 있는가? 다음과 같이 정리할 수 있겠다.
- 뱀이 한 칸 기어갈 때마다, 뱀의 머리 블록은 뱀이 기어가는 방향으로 한 칸 움직인다.
- 뱀이 한 칸 기어갈 때마다, 뱀의 머리를 제외한 블록은 각각 자기 앞의 블록이 있던 위치로 이동한다.
예를 들어, 뱀 블록이 [(2, 3), (2, 4), (1, 4), (1, 5)]
와 같이 있을 때, 뱀이 북쪽으로 한 칸 움직인다면, [(1, 3), (2, 3), (2, 4), (1, 4)]
로 이동하게 될 것이다.
그림 12-17 뱀 블록의 위치 변화
그런데 게임에서 뱀의 머리와 몸통 블록은 위치만 다를 뿐 서로 구별되지는 않는다. 모두 초록색 블록으로 화면에 출력될 뿐이다. 그래서 좀 더 단순하게, 뱀의 꼬리 블록을 뱀의 머리가 움직여야 할 위치로 옮기기만 해도 된다. 즉, [(2, 3), (2, 4), (1, 4), (1, 5)]
에서 마지막의 (1, 5)
를 삭제하고 맨 앞에 새 머리 위치인 (1, 3)
을 추가하면 뱀의 새 위치인 [(1, 3), (2, 3), (2, 4), (1, 4)]
가 된다.
이것을 코드로 옮겨, 뱀 클래스에 crawl()
메서드를 정의할 수 있다.
코드 12-16 뱀의 몸을 움직이는 메서드 정의하기
class Snake:
# (...)
def crawl(self):
"""뱀이 현재 방향으로 한 칸 기어간다."""
head_position = self.positions[0]
y, x = head_position
if self.direction == 'north':
self.positions = [(y - 1, x)] + self.positions[:-1]
elif self.direction == 'south':
self.positions = [(y + 1, x)] + self.positions[:-1]
elif self.direction == 'west':
self.positions = [(y, x - 1)] + self.positions[:-1]
elif self.direction == 'east':
self.positions = [(y, x + 1)] + self.positions[:-1]
뱀이 향하고 있는 각 방향에 맞게 새로운 블록 위치를 추가하고, 마지막 블록 위치를 삭제하도록 했다.
아직까지는 프로그램을 실행해보아도 뱀이 움직이지는 않을 것이다. crawl()
메서드를 호출하는 곳이 없기 때문이다. 뱀이 움직이려면 게임이 진행되어야 한다. 게임판에 게임을 한 차례 진행시키는 메서드 process_turn()
메서드를 정의하여 한 차례 지날 때마다 crawl()
메서드를 호출하도록 하자.
코드 12-17 게임판에 게임 진행 메서드 정의하기
class GameBoard:
# (...)
def process_turn(self):
"""게임을 한 차례 진행한다."""
self.snake.crawl() # 뱀이 한 칸 기어간다.
게임 진행 프로세스를 수정해 0.3초가 지날 때마다 process_turn()
메서드를 호출하도록 하자.
코드 12-18 일정 시간마다 게임을 한 차례씩 진행하기
from datetime import datetime
from datetime import timedelta
import pygame
# (...) 클래스 정의, 파이게임 초기화 코드 생략
TURN_INTERVAL = timedelta(seconds=0.3) # ❶ 게임 진행 간격을 0.3초로 정의한다
while True:
events = pygame.event.get()
for event in events:
if event.type == pygame.QUIT:
exit()
# ❷ 시간이 TURN_INTERVAL만큼 지날 때마다 게임을 한 차례씩 진행한다
if TURN_INTERVAL < datetime.now() - last_turn_time:
game_board.process_turn()
last_turn_time += datetime.now()
draw_background(screen)
game_board.draw(screen)
pygame.display.update()
일정한 시간마다 게임을 한 턴씩 진행하는 것은 코드 12-10에서 블록을 움직여 본 방식과 똑같다. ❶ 한 차례를 진행할 때까지 기다려야 하는 시간을 timedelta
로 정의하고, ❷ 파이게임에서 이벤트를 처리하고 화면을 그리는 프로세스에서 그만큼의 시간이 지났는지 확인하고 게임을 진행하면 된다. 프로그램을 실행해보면 드디어 뱀이 살아 움직이는 모습을 볼 수 있다.
뱀 방향 바꾸기
뱀이 한쪽 방향으로만 움직인다는 것을 눈치챘을 것이다. 뱀 클래스에 방향을 바꾸는 turn()
메서드를 추가하고, 파이게임 이벤트 처리 코드에서 사용자의 키보드 입력에 따라 turn()
메서드를 호출하도록 해 보자. 코드 12-9를 참고해 직접 작성해보면 좋다. 다음과 같은 코드를 작성하게 될 것이다.
코드 12-19 키보드 입력에 따라 뱀 방향 바꾸기
class Snake:
# (...)
def turn(self, direction): # ❶
"""뱀의 방향을 바꾼다."""
self.direction = direction
# (...) 파이게임 초기화 코드 생략
while True:
events = pygame.event.get()
for event in events:
if event.type == pygame.QUIT:
exit()
if event.type == pygame.KEYDOWN: # ❷ 화살표 키가 입력되면 뱀의 방향을 바꾼다
if event.key in DIRECTION_ON_KEY:
game_board.snake.turn(DIRECTION_ON_KEY[event.key])
# (...) 게임 진행 코드 생략
# (...) 화면 출력 코드 생략
❶ turn()
메서드는 그냥 새 방향을 입력받아 인스턴스 속성에 대입하는 일을 할 뿐이다. ❷ 화살표 키를 입력받아 뱀의 방향을 바꾸는 코드는 코드 12-9에서 본 것과 같다. 블록의 방향 대신 뱀의 방향을 바꿀 뿐이다. 프로그램을 실행해서 뱀을 원하는대로 움직여 보자.
사과 먹기
이제 뱀이 사과를 먹고 자라는 것을 구현할 차례다. 그러려면 어떤 문제를 풀어야 하는지 정의해 보자.
- 뱀이 사과를 먹었다는 것을 어떻게 인식할 것인가?
- 사과를 먹은 뒤 뱀이 길어지게 하려면 어떻게 할 것인가?
- 사과를 먹은 뒤 새 사과를 어떻게 만들 것인가?
답을 한 번 생각해보고, 직접 구현할 수 있다면 해보는 것도 좋다.
첫번째, 뱀이 사과를 먹은 것을 인식하는 문제부터 해결해 보자. 뱀이 사과를 먹으려면, 뱀의 머리가 사과에 닿아야 한다. 즉, 뱀이 이동한 후에 뱀의 머리 위치와 사과의 위치가 똑같은 경우 뱀이 사과를 먹었다고 판단할 수 있다. 이것은 게임판의 게임 진행 메서드 process_turn()
에서 판단하도록 하자.
코드 12-20 뱀과 사과가 닿은 것을 확인하기
class GameBoard:
# (...)
def process_turn(self):
"""게임을 한 차례 진행한다."""
self.snake.crawl()
# ❶ 뱀의 머리와 사과가 닿았으면
if self.snake.positions[0] == self.apple.position:
self.snake.grow() # 뱀을 한 칸 자라게 한다
self.put_new_apple() # 사과를 새로 놓는다
process_turn()
메서드에서 뱀이 기어가도록 하고 있다. 뱀이 기어간 직후에 머리와 사과가 닿았는지 확인해야 한다. ❶ if 문으로 두 블록의 위치가 같은지 확인하고, 같으면 여기서 뱀이 자라나도록 하고 사과를 새로 만들면 된다. ❷ 뱀이 자라게 하는 메서드와 ❸ 사과를 새로 놓는 메서드는 아직 정의하지 않았지만 이렇게 호출할 것이라고 정해 두자.
두번째, 사과를 먹은 뒤 뱀이 길어지게 하는 문제는 뱀을 구성하는 블록을 하나 추가하는 것으로 해결할 수 있다. 뱀의 맨 앞이나 맨 뒤에 블록을 추가하면 되는데, 이미 한 칸 기어간 후이므로 맨 뒤에 추가하면 될 것 같다. 뱀 클래스에 다음과 같이 grow()
메서드를 정의하자.
코드 12-21 사과를 먹은 뱀이 자라도록 하기
class Snake:
# (...)
def grow(self):
"""뱀이 한 칸 자라나게 한다."""
tail_position = self.positions[-1]
y, x = tail_position
if self.direction == 'north':
self.positions.append((y - 1, x))
elif self.direction == 'south':
self.positions.append((y + 1, x))
elif self.direction == 'west':
self.positions.append((y, x - 1))
elif self.direction == 'east':
self.positions.append((y, x + 1))
crawl()
메서드와 유사하게 뱀의 진행 방향에 따라 알맞는 위치에 꼬리 블록을 하나 추가하도록 했다.
세번째, 사과를 새로 놓는 문제는 게임판에 놓은 사과를 새 것으로 바꾸면 해결할 수 있다. 새로 만들어지는 사과는 임의의 위치에 만들어져야 게임이 재미있을 것이다. 사과를 적절한 곳에 놓는 일은 게임판의 책임이므로, 사과 클래스가 아니라 게임판 클래스에 put_new_apple()
메서드를 추가해야 한다.
코드 12-22 사과를 임의의 위치에 새로 놓기
import random # ❶
# (...) 중간 코드 생략
class GameBoard:
# (...)
def put_new_apple(self):
"""게임판에 새 사과를 놓는다."""
self.apple = Apple((random.randint(0, 19), random.randint(0, 19))) # ❷
for position in self.snake.positions: # ❸ 뱀 블록을 순회하면서
if self.apple.position == position: # 사과가 뱀 위치에 놓인 경우를 확인해
self.put_new_apple() # 사과를 새로 놓는다
break
❶ 사과를 임의의 위치에 만들기 위해서는 random
모듈을 이용해야 한다. ❷ 사과를 y, x축 0 - 19 사이 임의의 위치에 놓도록 한다. 이걸로 다 됐을까? 사과를 임의의 위치에 놓다 보면 뱀의 위치와 겹칠 수 있다. 뱀의 위치에 사과가 놓이면 안되므로, 사과를 놓은 뒤 ❸ 뱀 블록을 순회하면서 사과의 위치와 겹치는지 확인하고 그런 경우 사과를 새로 놓는다.
이제 프로그램을 실행해서 뱀을 움직여 사과를 먹고 무럭무럭 자라도록 할 수 있다.
그림 12-18 사과를 먹고 자라는 뱀
자기 몸에 부딛히기
뱀이 움직이다가 자기 몸에 부딛히면 게임이 끝나도록 해야 한다. 뱀이 자기 몸에 부딛힌 경우는 예외적인 상황이라고 할 수 있으므로, 그에 해당하는 예외를 하나 정의해 두자.
코드 12-23 뱀 충돌 예외 정의하기
class SnakeCollisionException(Exception):
"""뱀 충돌 예외"""
pass
뱀 충돌 예외는 우리가 정의한 것이므로, 우리가 적절히 일으켜줘야 한다. 이 예외는 언제 일어나야 할까? 뱀이 충돌한 때다. 뱀의 충돌은 누가 확인해야 할까? 게임 진행을 담당하는 게임판이다. 게임판의 게임 진행 메서드 process_turn()
에서 뱀이 이동한 후 충돌을 확인해 예외를 일으키자.
코드 12-24 뱀 충돌을 확인하고 예외 일으키기
class GameBoard:
# (...)
def process_turn(self):
"""게임을 한 차례 진행한다."""
self.snake.crawl()
# 뱀의 머리가 뱀의 몸과 부딛혔으면
if self.snake.positions[0] in self.snake.positions[1:]:
raise SnakeCollisionException() # 뱀 충돌 예외를 일으킨다
if self.snake.positions[0] == self.apple.position:
self.snake.grow()
self.put_new_apple()
뱀의 머리가 몸과 부딛힌 것을 확인하려면, 뱀 머리의 위치(snake.positions[0]
)가 뱀의 나머지 블록 리스트(snake.positions[1:]
)에 들어있는지 연산자로 확인하면 된다. 이 예외가 발생하면 게임이 종료되겠지만, 처리되지 않은 예외로 인한 비정상적인 종료가 될 것이다. process_turn()
을 호출하는 파이게임 실행 프로세스에서 이 예외를 잡아 게임을 종료하도록 하자.
코드 12-25 뱀 충돌 예외가 일어났을 때 게임 종료하기
# (...) 클래스 정의, 파이게임 초기화 코드 생략
while True:
# (...) 이벤트 처리 코드 생략
if TURN_INTERVAL < datetime.now() - last_turn_time:
try:
game_board.process_turn()
except SnakeCollisionException:
exit()
last_turn_time = datetime.now()
# (...) 화면 출력 코드 생략
이제 뱀이 자기 몸에 부딛히면 게임이 정상적으로 종료된다. 더 개선할 점은 있지만, 이걸로 기본적인 뱀 게임을 완성했다.
12.1.7 더 연습하기
다음은 여러분이 더 실습할 수 있도록 준비한 과제다. 해결 방법을 책에서 알려주지는 않는다. 실험하고, 연구하고, 자료를 찾아 보면서 직접 해결해보기 바란다. 여기까지 따라온 여러분이라면 충분히 도전해 볼만하다.
더 구현해 볼 것
프로그램은 언제나 더 개선할 점이 있다. 뱀 게임은 어떤 점을 개선할 수 있을까?
- 뱀이 게임판 밖으로 나가면 게임이 끝나도록 해야 한다.
- 지금은 뱀이 180도 방향 전환을 할 수 있다. 한번에 90도씩만 방향을 바꿀 수 있어야 한다.
- 뱀이 계속 길어져서 사과를 더 이상 놓을 곳이 없을 때 게임이 올바르게 종료되도록 해야 한다.
- 사과를 먹으면 게임 속도가 점점 더 빨라지도록 해서 난이도를 높여 보자.
- 사과가 한 번에 여러 개씩 나오면 더 재미있을 것 같다.
- 사과가 놓인 뒤 일정한 시간이 지나면 사과의 색이 점점 흐려지다가 사과가 없어지도록 하자.
- 뱀의 머리 블록에서 꼬리 블록으로 갈 수록 색이 점점 변하게 하자.
다른 게임 만들어보기
뱀 게임을 만들면서 배운 노하우로 다른 보드 게임을 만들어보는 것도 좋다. 나는 테트리스를 만들어보기를 추천한다. 많은 초보 프로그래머들이 도전하는 게임이고 공부도 많이 되기 때문이다. 테트리스가 아니더라도 관심이 있는 것이면 무엇이든 좋다. 게임 프로그래밍에 관심이 있다면 다른 게임도 만들어보기 바란다.
파이썬으로 게임 프로그래밍을 하는 것이 재미가 있었다면 『나만의 Python Game 만들기(알 슈베이가르트 저, 김세희 역, 정보문화사)』라는 책이 기초적인 내용과 실습 과제를 많이 다루고 있어서 추천하고 싶다. 더 화려하고 본격적인 게임을 만들고 싶다면 C#(프로그래밍 언어)과 유니티(게임 개발 도구)를 학습해보면 좋다.
댓글 남기기