Python Turtle을 활용한 클래식 게임 개발: 뱀 게임 만들기

이전 장에서는 Tkinter를 사용하여 그래픽 애플리케이션을 만드는 방법을 자세히 살펴보았습니다. 틱택토 게임과 그림판 애플리케이션을 만드는 과정을 통해 Tkinter의 다양한 위젯, 스타일링, 이벤트 처리, Canvas 그리기 등을 배웠습니다.

이번 장에서는 turtle 모듈로 다시 돌아가, 실제 게임 애플리케이션을 만들어 볼 것입니다. 바로 '뱀 게임'입니다. 이 게임은 간단하면서도 프로그래밍의 여러 개념을 적용해 볼 수 있어 좋은 학습 기회가 될 것입니다.

뱀 게임이란?

뱀 게임은 화면에 나타나는 뱀을 조종하여 음식을 먹고 몸집을 늘려가는 고전적인 게임입니다. 플레이어는 화살표 키를 사용하여 뱀의 머리 방향을 조절합니다. 화면에는 뱀 머리와 같은 크기의 빨간색 음식이 무작위 위치에 나타납니다. 뱀이 음식을 먹을 때마다 뱀의 몸이 길어지고, 새로운 음식이 다른 무작위 위치에 나타납니다. 뱀이 벽이나 자신의 몸에 부딪히면 게임이 종료됩니다.

최종적으로 완성될 게임의 모습은 다음과 같습니다 (그림 19-1).

뱀 게임 화면 예시

그림 19-1: 뱀 게임

이 게임을 만들기 위해 필요한 코드의 순서를 미리 걱정하지 마세요. 각 부분을 차근차근 설명하고, 최종적으로 모든 코드를 올바른 순서로 제공할 것입니다.

필요한 모듈 가져오기

이 게임을 만들기 위해 세 개의 모듈이 필요합니다:

  • turtle: 뱀, 점수, 음식을 그리는 데 사용됩니다.
  • random: 음식이 무작위 위치에 나타나도록 하는 데 사용됩니다.
  • time: 게임 루프의 속도를 조절하여 뱀이 눈에 보이게 움직이도록 하는 데 사용됩니다.
import turtle
import time
import random

화면 설정

먼저 turtle 화면을 설정합니다. 제목은 'Snake Game'으로 하고 배경색은 검은색으로 지정합니다. setup() 함수를 사용하여 화면의 너비와 높이를 픽셀 단위로 설정하여 모든 요소의 좌표를 명확하게 관리할 수 있도록 합니다.

s = turtle.Screen()
s.title('Snake Game')
s.bgcolor('Black')
s.setup(width=500, height=500)

tracer(0) 메서드를 사용하여 화면 업데이트 애니메이션을 비활성화합니다. 이는 게임 중에 많은 요소를 빠르게 그려야 할 때 성능을 향상시키는 데 도움이 됩니다.

s.tracer(0) # 애니메이션 비활성화

변수 초기화

게임에 필요한 여러 전역 변수를 초기화합니다:

  • snake: 뱀의 각 부분을 나타내는 turtle 객체를 저장할 리스트입니다.
  • size: 뱀의 각 부분(머리, 몸통)과 음식의 크기(너비와 높이)를 나타내는 변수입니다.
  • key: 현재 눌린 방향키를 저장하는 변수입니다. ('up', 'down', 'left', 'right' 또는 빈 문자열)
  • score: 현재 점수를 저장하는 변수입니다.
snake = []
size = 20
key = ''
score = 0

뱀 머리 생성

이제 뱀의 머리가 될 turtle 객체를 생성하고 초기화합니다. 속도는 0(가장 빠름), 모양은 'square', 색상은 'Green'으로 설정하고 화면 중앙(0, 0)으로 이동시킵니다. 이 머리를 snake 리스트에 추가합니다.

# 뱀 머리 그리기
head = turtle.Turtle()
head.speed(0)
head.shape('square')
head.color('Green')
head.penup()
head.goto(0, 0)
snake.append(head) # 첫 번째 머리 추가

게임 루프 및 화면 업데이트

게임이 계속 실행되는 동안 화면을 지속적으로 업데이트하기 위한 메인 게임 루프를 만듭니다. s.update()를 호출하여 화면에 그려진 모든 것을 새로 고칩니다.

while True:
    s.update()
    # 여기에 게임 로직 추가
    time.sleep(0.2) # 0.2초 지연

time.sleep(0.2)는 뱀의 움직임을 눈으로 볼 수 있도록 속도를 늦춥니다.

첫 번째 음식 생성

이제 첫 번째 음식을 무작위 위치에 생성합니다. 새로운 turtle 객체 'apple'을 생성하고, 모양은 'square', 색상은 'Red'로 설정합니다. 화면 내에서 무작위 위치를 생성하기 위해 random.randint(-11, 11) * size를 사용하여 size의 배수로 된 좌표를 얻습니다. 이는 뱀의 움직임과 정렬되도록 하기 위함입니다.

# 첫 번째 음식 그리기
apple = turtle.Turtle()
apple.speed(0)
apple.shape('square')
apple.color('Red')
apple.penup()

# 무작위 정수 생성 (20의 배수)
aX = random.randint(-11, 11) * size
aY = random.randint(-11, 11) * size
apple.goto(aX, aY)

방향 제어 설정

화면이 키보드 입력을 감지하도록 s.listen()을 호출합니다. 화살표 키 입력에 따라 set_up, set_down, set_left, set_right 함수를 호출하도록 onkeypress()를 설정합니다. 이 함수들은 전역 변수 key의 값을 변경하여 뱀의 이동 방향을 결정합니다. 뱀이 스스로를 물지 않도록 역방향 이동은 방지합니다.

# 이벤트 감지 및 처리
s.listen()
s.onkeypress(set_up, 'Up')
s.onkeypress(set_down, 'Down')
s.onkeypress(set_left, 'Left')
s.onkeypress(set_right, 'Right')

def set_up():
    global key
    if key != 'down':
        key = 'up'

def set_down():
    global key
    if key != 'up':
        key = 'down'

def set_left():
    global key
    if key != 'right':
        key = 'left'

def set_right():
    global key
    if key != 'left':
        key = 'right'

뱀 머리 이동

moveHead() 함수는 현재 key 값에 따라 뱀 머리의 x 또는 y 좌표를 size만큼 변경합니다. 이 함수는 메인 게임 루프에서 호출되어 뱀이 지정된 방향으로 계속 움직이도록 합니다.

# 설정된 방향에 따라 이동
def moveHead():
    if key == 'up':
        head.sety(head.ycor() + size)
    if key == 'down':
        head.sety(head.ycor() - size)
    if key == 'left':
        head.setx(head.xcor() - size)
    if key == 'right':
        head.setx(head.xcor() + size)

# 게임 루프 내에서 호출
# while True:
#     s.update()
#     moveHead()
#     time.sleep(0.2)

점수판 생성

점수를 표시하기 위해 새로운 turtle 객체 sc를 생성합니다. 화면 우측 상단(120, 220)에 'Score:0'을 Arial 폰트, 크기 20, bold 스타일로 표시합니다. 이 turtle은 화면에 보이지 않도록 숨깁니다.

# 점수판 그리기
sc = turtle.Turtle()
sc.speed(0)
sc.pencolor('White')
sc.penup()
sc.goto(120, 220)
sc.write('Score:0', font=('Arial', 20, 'bold'))
sc.hideturtle()

뱀 성장 및 음식 섭취

뱀 머리가 음식과 겹치는지 확인하는 로직을 게임 루프에 추가합니다. head.distance(apple) <= 0 조건을 사용하여 충돌을 감지합니다. 충돌 시:

  1. drawApple() 함수를 호출하여 새로운 음식을 무작위 위치에 생성합니다.
  2. drawSnake() 함수를 호출하여 snake 리스트에 새로운 몸통 부분을 추가하여 뱀을 성장시킵니다.
  3. 점수를 1 증가시키고 changeScore() 함수를 호출하여 점수판을 업데이트합니다.
# 먹었는지 확인
if head.distance(apple) <= 0: # 완전히 겹쳤을 때
    drawApple()
    # 새로운 몸통 부분 생성
    drawSnake() # 이전 머리를 몸통으로 유지
    score += 1
    changeScore(score)

# drawApple, drawSnake, changeScore 함수 정의 필요

drawApple() 함수는 이전에 음식을 생성했던 로직과 동일하게 작동합니다. drawSnake() 함수는 새로운 turtle 객체를 생성하여 snake 리스트에 추가합니다. changeScore(score) 함수는 점수판 turtle을 업데이트합니다.

몸통 움직임 구현

moveBody() 함수는 뱀의 모든 몸통 부분이 머리를 따라 움직이도록 합니다. 각 몸통 부분은 바로 앞 몸통 부분(또는 머리)의 이전 위치로 이동합니다. 이를 위해 현재 모든 turtle 객체의 x, y 좌표를 임시 리스트 temp에 저장한 후, 각 몸통 부분(`snake[i]` for i > 0)을 이전 몸통 부분(`temp[i-1]`)의 위치로 이동시킵니다.

def moveBody():
    temp = []
    # 현재 위치 리스트 생성
    for i in snake:
        x = i.xcor()
        y = i.ycor()
        temp.append({'x': x, 'y': y})

    # 전체 몸통 이동
    for i in range(1, len(snake)):
        snake[i].goto(temp[i-1]['x'], temp[i-1]['y'])

# 게임 루프에서 moveBody() 호출 필요
# moveBody()

moveBody() 함수는 moveHead() 함수 이전에 호출되어야, 머리가 이동하기 전에 몸통이 머리를 따라가는 것처럼 보이게 됩니다.

충돌 감지

게임 루프의 마지막 부분에서 checkCollision() 함수를 호출하여 충돌을 감지합니다. 이 함수는 다음을 확인합니다:

  • 벽 충돌: 뱀 머리의 x 또는 y 좌표가 화면 경계(-240 또는 240)를 벗어나는지 확인합니다.
  • 몸통 충돌: 뱀 머리의 좌표가 snake 리스트의 다른 몸통 부분과 겹치는지 확인합니다.

충돌이 감지되면 collision 변수를 True로 설정하고, 게임 루프를 중단시키기 위해 key를 빈 문자열로 설정합니다. 이후 1초간 대기하고, 모든 게임 요소를 화면 밖으로 이동시킨 후 'GAME OVER' 메시지를 표시합니다.

def checkCollision():
    global key
    collision = False
    # 벽 충돌
    if head.xcor() < -240 or head.xcor() > 240 or head.ycor() < -240 or head.ycor() > 240:
        collision = True
    # 몸통 충돌
    for i in range(1, len(snake)):
        if head.xcor() == snake[i].xcor() and head.ycor() == snake[i].ycor():
           collision = True

    if collision == True:
        key = '' # 움직임 일시 정지
        time.sleep(1) # 잠시 멈춤
        # 모든 요소를 화면 밖으로 이동
        for s in snake:
            s.goto(2000, 2000)
        apple.goto(2500, 2500)
        # 게임 오버 메시지
        game = turtle.Turtle()
        game.penup()
        game.goto(-170, 0)
        game.pencolor('white')
        game.write('GAME OVER!', font=('Arial', 40, 'bold'))
        game.hideturtle()
    return collision

# 게임 루프 내에서 호출
# if checkCollision():
#     break

while True 루프는 다음과 같이 업데이트됩니다:

while True:
    s.update()
    # 먹었는지 확인
    if head.distance(apple) <= 0:
        drawApple()
        drawSnake()
        score += 1
        changeScore(score)
    moveBody() # 몸통 먼저 이동
    moveHead() # 머리 이동
    # 다음 라운드 머리 이동 전에 충돌 확인
    if checkCollision():
        break
    time.sleep(0.2)

마지막으로, s.mainloop()를 호출하여 사용자가 창을 닫을 때까지 화면이 유지되도록 합니다.

s.mainloop()

전체 코드

앞서 설명한 모든 코드를 합친 전체 코드는 다음과 같습니다:

import turtle
import time
import random

# 화면 설정
s = turtle.Screen()
s.title('Snake Game')
s.bgcolor('Black')
s.setup(width=500, height=500)
s.tracer(0) # 애니메이션 비활성화

# 필요한 변수 생성 및 할당
snake = []
size = 20
key = ''
score = 0

# 뱀 머리 그리기
head = turtle.Turtle()
head.speed(0)
head.shape('square')
head.color('Green')
head.penup()
head.goto(0, 0)
snake.append(head) # 첫 번째 머리 추가

# 첫 번째 음식 그리기
apple = turtle.Turtle()
apple.speed(0)
apple.shape('square')
apple.color('Red')
apple.penup()
# 무작위 정수 생성 (20의 배수)
aX = random.randint(-11, 11) * size
aY = random.randint(-11, 11) * size
apple.goto(aX, aY)

# 점수판 그리기
sc = turtle.Turtle()
sc.speed(0)
sc.pencolor('White')
sc.penup()
sc.goto(120, 220)
sc.write('Score:0', font=('Arial', 20, 'bold'))
sc.hideturtle()

# 뱀 방향 변경 함수
def set_up():
    global key
    if key != 'down':
        key = 'up'

def set_down():
    global key
    if key != 'up':
        key = 'down'

def set_left():
    global key
    if key != 'right':
        key = 'left'

def set_right():
    global key
    if key != 'left':
        key = 'right'

# 뱀 머리 이동 함수
def moveHead():
    if key == 'up':
        head.sety(head.ycor() + size)
    if key == 'down':
        head.sety(head.ycor() - size)
    if key == 'left':
        head.setx(head.xcor() - size)
    if key == 'right':
        head.setx(head.xcor() + size)

# 뱀 몸통 이동 함수
def moveBody():
    temp = []
    for i in snake:
        x = i.xcor()
        y = i.ycor()
        temp.append({'x': x, 'y': y})
    # 전체 몸통 이동
    for i in range(1, len(snake)):
        snake[i].goto(temp[i-1]['x'], temp[i-1]['y'])

# 음식 다시 그리기 함수
def drawApple():
    aX = random.randint(-11, 11) * size
    aY = random.randint(-11, 11) * size
    apple.goto(aX, aY)

# 새로운 뱀 몸통 부분 생성 함수
def drawSnake():
    sBody = turtle.Turtle()
    sBody.speed(0)
    sBody.shape('square')
    sBody.color('Green')
    sBody.penup()
    snake.append(sBody) # 끝에 추가

# 점수 업데이트 함수
def changeScore(score):
    sc.goto(120, 220)
    sc.clear()
    string = 'Score: {}'.format(score)
    sc.write(string, font=('Arial', 20, 'bold'))
    sc.hideturtle()

# 충돌 감지 함수 (벽 및 몸통)
def checkCollision():
    global key
    collision = False
    # 벽 충돌
    if head.xcor() < -240 or head.xcor() > 240 or head.ycor() < -240 or head.ycor() > 240:
        collision = True
    # 몸통 충돌
    for i in range(1, len(snake)):
        if head.xcor() == snake[i].xcor() and head.ycor() == snake[i].ycor():
           collision = True
    if collision == True:
        key = '' # 움직임 일시 정지
        time.sleep(1) # 잠시 멈춤
        # 모든 요소를 화면 밖으로 이동
        for s in snake:
            s.goto(2000, 2000)
        apple.goto(2500, 2500)
        # 게임 오버 메시지
        game_over_turtle = turtle.Turtle()
        game_over_turtle.penup()
        game_over_turtle.goto(-170, 0)
        game_over_turtle.pencolor('white')
        game_over_turtle.write('GAME OVER!', font=('Arial', 40, 'bold'))
        game_over_turtle.hideturtle()
    return collision

# 이벤트 감지 및 처리
s.listen()
s.onkeypress(set_up, 'Up')
s.onkeypress(set_down, 'Down')
s.onkeypress(set_left, 'Left')
s.onkeypress(set_right, 'Right')

# 메인 게임 루프
while True:
    s.update()
    # 먹었는지 확인
    if head.distance(apple) <= 0:
        drawApple()
        drawSnake()
        score += 1
        changeScore(score)
    moveBody() # 몸통 먼저 이동
    moveHead() # 머리 이동
    # 다음 라운드 머리 이동 전에 충돌 확인
    if checkCollision():
        break
    time.sleep(0.2)

s.mainloop()

이 장에서는 turtle 모듈을 사용하여 뱀 게임을 만드는 과정을 살펴보았습니다. time 모듈을 사용하여 프로그램 일시 중지, 게임 루프 생성, turtle 객체의 위치 가져오기, 게임 캐릭터 이동, 충돌 감지, 게임 점수 관리 등 다양한 새로운 개념을 배웠습니다.

다음 장에서는 pygame 라이브러리를 사용하여 2D 게임 개발의 세계로 나아가 보겠습니다.

태그: python turtle 게임 개발 뱀 게임 프로그래밍 기초

7월 9일 23:43에 게시됨