[파이썬 간단한 게임 만들기] 2. 스네이크 게임
유치한 게임에 오신 것을 환영합니다.
이번에는 스네이크 게임을 만들어 보겠습니다.
아래와 같은 순서로 배워보겠습니다.
1. 목표
이번 장에서는 1. 기본기 쌓기를 활용하여 실제 플레이가 가능한 게임을 만들어 보도록 하겠습니다.
이번에 만들 게임은 스네이크 게임입니다. 스네이크 게임의 규칙은 다음과 같습니다.
화면에 랜덤으로 사과가 생성되고 뱀이 사과를 먹으면 성장합니다. 뱀의 머리가 자신의 몸통에 닿게 되면 게임은 종료됩니다.
사과를 최대한 많이 먹어 몸통을 늘리는 것이 게임의 목표입니다.
PyGame을 활용하여 이 스네이크 게임을 구현하고, 캐릭터를 제어하고 여러 상태변화에 대한 이벤트 처리까지 배워보도록 하겠습니다.
2. 사전 준비
2-1) 파이썬 문법 이해
1. Class 이해 2. list 다루기 > list append > list의 덧셈 연산 |
2-2) 파이썬 게임 기본 구조 준비 (흰색 게임판 그리기)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22 |
import pygame # 1. pygame 선언
pygame.init() # 2. pygame 초기화
# 3. pygame에 사용되는 전역변수 선언
WHITE = (255,255,255)
size = [400,300]
screen = pygame.display.set_mode(size)
done= False
clock= pygame.time.Clock()
# 4. pygame 무한루프
def runGame():
global done
while not done:
clock.tick(10)
screen.fill(WHITE)
pygame.display.update() runGame()
pygame.quit()
|
cs |
이전 장( 1. 기본기 쌓기)에서 학습한 pygame의 기본 구조를 기반으로 작성하도록 하겠습니다.
- 게임판 구성
게임을 진행할 화면을 다음과 같이 설정하도록 하겠습니다.
7번 라인(화면 색) : green(RGB - 0,255,0)
8번 라인(화면 크기) : 넓이(width) - 400, 높이(height) - 400
import pygame # 1. pygame 선언
pygame.init() # 2. pygame 초기화
# 3. pygame에 사용되는 전역변수 선언
WHITE = (255,255,255)
GREEN = (0, 255,0)
size = [400,400]
screen = pygame.display.set_mode(size)
done= False
clock= pygame.time.Clock()
# 4. pygame 무한루프
def runGame():
global done
while not done:
clock.tick(10)
screen.fill(GREEN)
for event in pygame.event.get():
if event.type == pygame.QUIT:
done = True
break
pygame.display.update()
runGame()
pygame.quit()
3. 소스 코드 (전체)
파일명 : snake.py
drive.google.com/drive/folders/1P7-ibtlJKUEet1lbqkSAg7N8RyxOza_o?usp=sharing
4. 사전 지식
1) 스네이크 게임에 필요한 요소 정의
스네이크 게임에는 세 가지가 필요합니다.
- 게임판
- 뱀
- 사과
간단한 구현을 위해 뱀과 사과는 정사각형 도형을 통해 구현하고, 각각의 요소는 Class를 통해 모델을 정의를 하겠습니다.
1
2
3
4
5
6
7
8
9
10
11
|
class Snake:
"""뱀 클래스"""
pass
class Apple:
"""사과 클래스"""
pass
|
cs |
각각 Snake, Apple, GameBoard를 통해 게임 요소를 정의합니다.
해당 클레스에 대한 기능 정의는 구현을 하면서 함께 진행하도록 하겠습니다.
2) PyGame 물체 출력(그리기)
뱀과 사과를 정사각형 도형으로 표현해보겠습니다.
사과는 정사각형 1개로, 뱀은 정사각형을 5개 연결해서 길게 표현해 보겠습니다.
PyGame에서 도형을 그리는 방법은 다음과 같습니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
RED = (255,0,0)
GREEN = (0,255,0)
def draw_block(screen, color, position):
block = pygame.Rect((position[0] * 20, position[1] * 20),
(20, 20))
pygame.draw.rect(screen, color, block)
draw_block(screen, RED, (8, 3))
draw_block(screen, GREEN, (10, 12))
draw_block(screen, GREEN, (11, 12))
draw_block(screen, GREEN, (12, 12))
draw_block(screen, GREEN, (13, 12))
draw_block(screen, GREEN, (14, 12))
pygame.display.update()
|
cs |
구현된 코드를 자세히 살펴보겠습니다.
4번 라인에서 사각형 도형을 그려주는 draw_block() 함수를 만들었습니다.
draw_block() 함수의 input parameter는 다음과 같습니다.
- screen : 물체가 출력될 스크린(screen = pygame.display.set_mode(size))
- color : 물체의 색 (RGB 색상을 나타내는 튜플 형태)
- position : 물체가 그려질 위치 (X, Y 좌표를 나타내는 튜플 형태)
pygame 라이브러리는 사각형 도형을 그릴 수 있도록 pygame.Rect()라는 함수를 제공합니다. 사각형을 그린다고 생각하면, 어떤정보가 필요할거 같나요? 시작위치와 크기 정보겠죠.
- 시작위치는 X, Y 좌표가 될 것이고
- 크기는 width, height로 설정 되겠죠.
프로그램도 마찬가지로 이렇게 4가지 값으로 사각형을 그릴 수 있습니다.
input을 주는 방법은 여러가지가 있는데요, 코드에서는 2개의 튜플로 input parameter를 설정하였습니다.
즉,
- 첫 번째 인자를 통해 사각형이 출력될 위치(x, y좌표)를 지정하고,
- 두 번째 인자로 사각형의 사이즈를 지정해줍니다. 우리는 사각형을 20x20의 크기를 갖는 정사각형으로 지정하였습니다.
다음으로 pygame.draw.rect() 함수를 통해서 물체를 화면(screen)에 출력하도록 합니다.
5라인에서는 x, y 좌표에 *20을 했습니다. 이 부분은 어떤 의미를 지닐까요? 정사각형이 20x20의 크기를 가지므로, 정사각형 다음에 딱 붙는 정사각형은 20을 띄고 그려져야 합니다. 그래서 *20 연산이 있는거지요.
9~16번 라인까지 각기 다른 위치에 도형을 출력하도록 하였고
17번 라인의 pygame.display.update()를 통해 물체가 출력된 화면(screen)을 업데이트합니다.
pygame을 통해 사각형을 출력하는 방법을 배웠으니 이제 게임을 만들어보도록 하겠습니다.
5. 구현 순서
구현 순서는 다음과 같습니다.
Step1 | 뱀 움직이기(키보드 이벤트 처리, 자동 이동) |
Step2 | 클래스 별 이벤트 처리(뱀, 사과) |
Step3 | 뱀이 사과를 먹을 때 이벤트 처리 |
Step4 | 뱀 충돌 처리 |
Step1) 뱀 움직이기(키보드 이벤트 처리, 자동 이동)
먼저 게임판에서 뱀을 조종하도록 해보겠습니다.
- 키보드 방향키를 통해 뱀의 이동방향을 제어하고,
- 일정 시간이 지날 때마다 뱀은 진행방향으로 자동으로 이동하도록 설정
해보겠습니다.
import pygame # 1. pygame 선언
pygame.init() # 2. pygame 초기화
# 3. pygame에 사용되는 전역변수 선언
WHITE = (255,255,255)
RED = (255,0,0)
GREEN = (0,255,0)
size = [400,400]
screen = pygame.display.set_mode(size)
done= False
clock= pygame.time.Clock()
block_position = [0, 0] # (y,x)
def draw_block(screen, color, position):
block = pygame.Rect((position[0] * 20, position[1] * 20),
(20, 20))
pygame.draw.rect(screen, color, block)
# 4. pygame 무한루프
def runGame():
global done
while not done:
clock.tick(10)
screen.fill(WHITE)
for event in pygame.event.get():
if event.type == pygame.QUIT:
done=True
if event.type == pygame.KEYDOWN:
if event.key == pygame.K_UP:
block_position[1] -= 1 # 블록의 y 좌표를 1 뺀다
elif event.key == pygame.K_DOWN:
block_position[1] += 1 # 블록의 y 좌표를 1 더한다
elif event.key == pygame.K_LEFT:
block_position[0] -= 1 # 블록의 x 좌표를 1 뺀다
elif event.key == pygame.K_RIGHT:
block_position[0] += 1 # 블록의 x 좌표를 1 더한다
draw_block(screen, GREEN, block_position)
pygame.display.update()
runGame()
pygame.quit()
15 번 라인은 움직일 뱀의 위치를 지정하는 변수입니다. 첫 시작은 (0,0)에서 시작됩니다.
33~41번 라인은 키보드 입력 이벤트를 처리하는 코드입니다. 이전 장에서 키보드 상하에 대한 이벤트 처리를 구성한 것에서 좌, 우 입력에 대한 처리를 추가하여 키보드 방향키에 대한 모든 이벤트 처리를 작성합니다.
43,44번 라인을 통해서 이벤트 처리를 통해 변경한 뱀(블록)의 위치 재설정 해줍니다.
다음으로 키보드가 이동한 방향으로 시간에 따라 자동으로 이동하도록 처리하겠습니다.
시간과 관련된 라이브러리를 선언합니다.
import pygame # 1. pygame 선언
from datetime import datetime
from datetime import timedelta
... 상단 코드 생략(색 정의, 스크린 설정 등)
block_position = [0, 0] # (y,x)
last_moved_time = datetime.now()
moving_direction = '' #East, West, South, North
def draw_block(screen, color, position):
block = pygame.Rect((position[0] * 20, position[1] * 20),
(20, 20))
pygame.draw.rect(screen, color, block)
# 4. pygame 무한루프
def runGame():
global done, last_moved_time, moving_direction
while not done:
clock.tick(10)
screen.fill(WHITE)
for event in pygame.event.get():
if event.type == pygame.QUIT:
done=True
if event.type == pygame.KEYDOWN:
if event.key == pygame.K_UP:
block_position[1] -= 1 # 블록의 y 좌표를 1 뺀다
moving_direction = 'N'
last_moved_time = datetime.now()
elif event.key == pygame.K_DOWN:
block_position[1] += 1 # 블록의 y 좌표를 1 더한다
moving_direction = 'S'
last_moved_time = datetime.now()
elif event.key == pygame.K_LEFT:
block_position[0] -= 1 # 블록의 x 좌표를 1 뺀다
moving_direction = 'W'
last_moved_time = datetime.now()
elif event.key == pygame.K_RIGHT:
block_position[0] += 1 # 블록의 x 좌표를 1 더한다
moving_direction = 'E'
last_moved_time = datetime.now()
if timedelta(seconds=0.5) <= datetime.now() - last_moved_time:
if moving_direction == 'N':
block_position[1] -= 1
elif moving_direction == 'S':
block_position[1] += 1
elif moving_direction == 'W':
block_position[0] -= 1
elif moving_direction == 'E':
block_position[0] += 1
last_moved_time = datetime.now()
draw_block(screen, GREEN, block_position)
pygame.display.update()
runGame()
pygame.quit()
8번 라인은 시간 경과에 따라 뱀을 이동하기 위해 필요한 시간 변수입니다.
9번 라인은 자동으로 움직일 방향을 담을 변수입니다. 이전 키보드 방향을 저장하여 시간에 따라 직전에 이동한 방향으로 이동할 수 있도록 합니다.
30,31번 라인은 기존의 키보드 이벤트 처리에서 9번 라인의 변수에 현재 이동 방향을 알 수 있도록 변수값을 설정해주며, 시간 값을 리셋하여 키보드 입력 시 새롭게 시간을 검증하도록 합니다. 해당 부분에 대해서는 하단에서 자세히 설명하도록 하겠습니다.
45~54번 라인은 위에서 설정된 '시간'과 '방향'을 기반으로 자동 이동을 검증하고 수행하는 부분입니다. 먼저 45번 라인에서 'datetime.now() - last_moved_time'을 통해 현재 시간을 기준으로 경과된 시간을 계산하여 해당 시간이 0.5초 이상이면 자동 이동을 수행합니다. 즉, 뱀은 0.5초마다 진행방향으로 자동으로 이동한다는 의미입니다.
다음 라인부터는 해당 방향을 각각 검증하여(N, S, W, E) 방향 값에 맞추어 뱀의 위치를 수정해주도록 합니다.
Step2) 클래스 별 이벤트 처리(뱀, 사과)
이번에는 앞서 준비한 클래스(뱀, 사과)에 대한 액션을 정의하고 해당 이벤트를 처리하겠습니다.
먼저 뱀은 다음과 같은 동작을 가집니다.
- 움직이기
- 꼬리가 따라 이동
- 스크린에 출력
다음으로 사과는 다음과 같은 동작을 가집니다.
- 스크린에 출력
먼저, 뱀이 이동하는 로직에 대해 설명드리겠습니다. 일반적으로 스네이크 게임에서 뱀은 자신이 이동했던 위치를 따라 몸통부터 꼬리까지 따라 움직이게 되어 있습니다.
이를 쉽게 구현하는 방법은 초당 뱀이 이동하는 방식을 '턴'으로 이해하면 쉽습니다. 뱀의 머리가 진행 방향으로 이동하게 되면 머리가 앞으로 나가는 것이 아니라 뱀의 꼬리가 머리가 이동할 위치로 가면 자연스럽게 턴마다 뱀이 이동하는 것처럼 보일 수 있습니다.
이를 기반으로 각 클래스(뱀, 사과)별 기능과 이벤트 처리를 작성하면 다음과 같습니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
|
import pygame # 1. pygame 선언
from datetime import datetime
from datetime import timedelta
pygame.init() # 2. pygame 초기화
# 3. pygame에 사용되는 전역변수 선언
WHITE = (255,255,255)
RED = (255,0,0)
GREEN = (0,255,0)
size = [400,400]
screen = pygame.display.set_mode(size)
done= False
clock= pygame.time.Clock()
last_moved_time = datetime.now()
KEY_DIRECTION = {
pygame.K_UP: 'N',
pygame.K_DOWN: 'S',
pygame.K_LEFT: 'W',
pygame.K_RIGHT: 'E',
}
def draw_block(screen, color, position):
block = pygame.Rect((position[0] * 20, position[1] * 20),
(20, 20))
pygame.draw.rect(screen, color, block)
class Snake:
def __init__(self):
self.positions = [(2,0),(1, 0),(0,0)] # 뱀의 위치, (2,0이 머리)
self.direction = ''
def draw(self):
for position in self.positions:
draw_block(screen, GREEN, position)
def move(self):
head_position = self.positions[0]
y, x = head_position
if self.direction == 'N':
self.positions = [(y - 1, x)] + self.positions[:-1]
elif self.direction == 'S':
self.positions = [(y + 1, x)] + self.positions[:-1]
elif self.direction == 'W':
self.positions = [(y, x - 1)] + self.positions[:-1]
elif self.direction == 'E':
self.positions = [(y, x + 1)] + self.positions[:-1]
class Apple:
def __init__(self, position=(5, 5)):
self.position = position
def draw(self):
draw_block(screen, RED, self.position)
# 4. pygame 무한루프
def runGame():
global done, last_moved_time
#게임 시작 시, 뱀과 사과를 초기화
snake = Snake()
apple = Apple()
while not done:
clock.tick(10)
screen.fill(WHITE)
for event in pygame.event.get():
if event.type == pygame.QUIT:
done=True
if event.type == pygame.KEYDOWN:
if event.key in KEY_DIRECTION:
snake.direction = KEY_DIRECTION[event.key]
if timedelta(seconds=0.5) <= datetime.now() - last_moved_time:
snake.move()
snake.draw()
apple.draw()
pygame.display.update()
runGame()
pygame.quit()
|
cs |
소스코드의 양이 많아졌으나 하나하나 씩 살펴보도록 하겠습니다.
30~49번 라인은 뱀(Snake) 클래스에 대한 내용입니다. 먼저 뱀을 게임판에 표시해주는 draw() 함수와 앞서 설명드린 꼬리가 따라 이동 할 수 있도록 move() 함수를 정의하였습니다.
move() 함수에서는 먼저 머리의 위치(head_position)와 진행방향(direction)을 가져와 현재 꼬리 위치를 머리가 움직일 위치로 이동하여 뱀이 움직일 수 있도록 합니다.
52~57번 라인은 사과(Apple) 클래스에 대한 내용입니다. 사과는 간단하게 정해진 위치에 출력될 수 있도록 정의되며 추후에 이벤트 처리를 추가하여 랜덤 위치에 생성될 수 있도록 합니다.
63,64번 라인은 게임 시작 시 클래스로 정의한 각 뱀, 사과 객체를 만들어 게임에 활용할 수 있도록 합니다.
73~75번 라인은 이전에 작성된 키보드 입력에 대한 이벤트 처리 부분이며 18~23번 라인에 따라 각 키보드 입력값에 대한 캐릭터 값(N,S,W,E)을 지정하여 뱀의 이동 뱡향을 지정해줍니다.
77,78번 라인은 시간을 검증하여 매번 0.5초마다 자동으로 뱀이 이동할 수 있도록 합니다.
80,81번 라인은 앞서 위치 값을 기반으로 각 뱀과 사과를 게임판에 그려주도록 하는 코드입니다.
Step3) 뱀이 사과를 먹을 때 이벤트 처리
먼저 뱀이 사과를 먹었을 때 발생할 수 있는 이벤트는 2가지입니다.
1. 뱀의 길이가 증가한다.(+1)
2. 기존 사과가 사라지며 새로운 사과가 생긴다.
그렇다면 먼저 뱀이 사과에 닿은 것을 확인하는 것이 필요합니다. 이를 코드로 작성하면 다음과 같습니다.
if snake.positions[0] == apple.position:
snake.grow()
apple.position = (random.randint(0, 19), random.randint(0, 19))
뱀의 머리 부분과 사과의 위치 값이 동일 할 때 ,
따라서 뱀(Snake) 클래스에 뱀의 길이를 늘려주는 grow() 함수를 호출하고, 사과의 위치가 바뀌도록 합니다
Snake 클래스는 grow()함수를 정의하면 되겠죠?
class Snake:
(...)
def grow(self):
tail_position = self.positions[-1] # [(2,0),(1, 0),(0,0)] 라면, (0, 0)를 의미함
x, y = tail_position
if self.direction == 'N':
# positions.append()는 추가하는 값이 뒤에 추가된다.
# positions가 [(2, 0), (1, 0), (0, 0)] 라면, positions.append((10, 20))의 결과는 [(2, 0), (1, 0), (0, 0), (10, 20)]이 된다.
self.positions.append((x, y - 1))
elif self.direction == 'S':
self.positions.append((x, y + 1))
elif self.direction == 'W':
self.positions.append((x - 1, y))
elif self.direction == 'C':
self.positions.append((x + 1, y))
위와 같이 Snake 클래스에 grow() 함수를 추가하여 진행 방향에 따라 뱀의 위치를 담고 있는 postions 리스트에 새로운 위치 값을 추가합니다.
다음으로 사과를 새롭게 생성할 수 있도록 앞서 작성한 코드를 수정합니다.
random.randint()를 활용하여 랜덤한 정수값을 x, y좌표로 설정하여 뱀이 사과를 먹게 되면 사과의 위치가 바뀌며 마치 새롭게 생성되도록 추가합니다.
Step4) 뱀 충돌 처리
마지막으로 게임의 종료인 뱀의 머리가 자신의 몸통과 충돌했을 때의 처리를 추가하도록 하겠습니다.
충돌 처리는 앞서 작성한 것보다 매우 간단하게 구현할 수 있습니다. 뱀의 머리 위치와 뱀의 위치가 포함된 positions의 리스트의 값 중 일치하는 좌표가 있으면 게임을 종료시키도록 합니다.
1
2
|
if self.snake.positions[0] in self.snake.positions[1:]:
done = True
|
cs |
충돌 이벤트가 발생하면 done 값을 True로 변경하여 게임 루프가 더 이상 돌지 않도록 하게 되면 게임은 끝나게 됩니다.
6. 정리
이번 장에서는 앞서 배운 pygame의 기본 구조와 게임 루프, 이벤트 처리를 기반으로 스네이크 게임을 구현해보았습니다. 먼저, 게임에 필요한 요소 정의를 하고 물체를 화면에 출력할 수 있도록 사전학습을 한 뒤 진행을 했으며, Step 1) 뱀 움직이기 -> 키보드 이벤트 처리, 자동 이동(시간의 흐름에 따라 변화) Step2) 클래스 별 이벤트 처리 -> 뱀, 사과 클래스 및 메서드 정의, 클래스별 이벤트 처리 Step 3) 뱀이 사과를 먹을 때 이벤트 처리 -> 게임에서 발생할 수 있는 변수 상황 처리, random 패키지를 활용한 이벤트 처리 Step 4) 벰 충돌 처리 -> 게임 종료 액션을 배웠습니다.
이번 장에는 pygame과 클래스를 함께 사용하며 여러 상황에 맞는 이벤트 처리를 주로 다뤘습니다. 이를 통해 조금 더 색다른 게임을 만들어 보세요.
7. 확장하기
뱀의 충돌 처리를 추가해보세요. 양쪽 벽에 부딪히면 게임이 끝나도록 말이죠.
단계가 올라감에 따라, 사과의 수를 늘려보면 어떨까요?
ㅁ 참고
도움이 되셨다면, 좋아요 / 구독 버튼 눌러주세요~
저작물의 저작권은 작성자에게 있습니다.
공유는 자유롭게 하시되 댓글 남겨주세요~
상업적 용도로는 무단 사용을 금지합니다.
끝까지 읽어주셔서 감사합니다^^