일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
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 |
- predictive analysis
- Python
- 알고리즘
- 파이썬
- 포인터
- 티스토리챌린지
- OOP
- Data Science
- 반복문
- vscode
- string
- baekjoon
- pass by reference
- 문자열
- 함수
- 백준
- raw data
- 오블완
- 배열
- function
- pointer
- array
- C++
- const
- Object Oriented Programming
- programming
- Pre-processing
- Class
- assignment operator
- Deep Learning
- Today
- Total
Channi Studies
[python] Turtle graphics로 뱀게임 만들기 본문
Turtle graphics Module을 사용해서 뱀 게임을 만들어보자.
먹이를 먹으면 꼬리가 한 칸씩 자라고, 꼬리나 창 끝에 부딪히면 죽는 게임이다.
main.py, snake.py, scoreBoard.py, food.py 총 4가지 파일로 이루어져 있다.

# main.py
import time
from turtle import Screen, Turtle
from snake import Snake
from food import Food
from scoreBoard import ScoreBoard
screen = Screen()
screen.setup(width=600, height=600)
screen.bgcolor("black")
screen.title("Snake Game")
screen.tracer(0)
snake = Snake()
food = Food()
score = ScoreBoard()
screen.listen()
screen.onkey(snake.up, "Up")
screen.onkey(snake.down, "Down")
screen.onkey(snake.left, "Left")
screen.onkey(snake.right, "Right")
game_is_on = True
while game_is_on:
screen.update()
time.sleep(0.08)
snake.move()
# detect collision with food
if snake.head.distance(food) < 15:
food.refresh()
score.increase_score()
snake.extend()
# detect collision with wall
if snake.head.xcor() > 280 or snake.head.xcor() < -280 or snake.head.ycor() > 280 or snake.head.ycor() < -280:
game_is_on = False
score.game_over()
# detect collision with it's own tail
for segment in snake.segments[1:]:
if snake.head.distance(segment) < 10:
game_is_on = False
score.game_over()
screen.exitonclick()
sceen을 설정하고 snake, food, scoreboard 객체를 만들었다.
파일 맨 끝에 screen.exitonclick()은 화면이 켜지자마자 꺼지지 않게 하기 위해 필수적이다.
screen 객체는 screen.listen() 메소드로 키 입력을 감지하게 되고, 화살표 키에 따라 움직이게 된다.
움직이는 메소드는 snake.py에 포함되어 있다.
반복문 내에 screen.update()를 넣은 이유는 상단에서 screen의 tracer 값이 0으로 지정되었기 때문인데, 이는 turtle graphic의 화면이 업데이트되는 시간을 통제한다. 0으로 설정하면 업데이트가 되지 않기 때문에, screen.update()로 반복문이 돌 때만 업데이트 되게끔 설정했다. 이유는 뱀의 움직임에 대한 시각적 효과에 있다.
time.sleep()은 업데이트 사이에 시간적 간격을 조절하는데, 다르게 말하면 게임의 속도와 난이도를 바꿀 수 있다.
속도가 줄어들수록 화면의 업데이트가 빨라지고, 업데이트가 빨라질 수록 뱀이 빠르게 움직이기 때문에 난이도가 올라간다.
snake.move()의 알고리즘은 snake.py에서 확실하게 볼 수 있다.
밑에서 다시 자세하게 설명하겠다.
이후는 음식(또는 점수)와의 충돌, 화면 테두리와의 충돌, 스스로의 꼬리와의 충돌을 차례로 해결한다.
음식 충돌에서,
turtle.distance()는 특정 turtle 객체와 또다른 turtle 객체, 또는 특정 좌표와의 거리를 계산해서 알려준다.
food는 10x10 px, snake는 20x20px인데, 10/2 + 20/2 = 15보다 거리가 적어졌을 때가 가장 시각적으로 적절한 충돌 상태로 느껴진다.
내부의 3가지 메소드는 각 파일에서 설명하겠다.
테두리 충돌에서,
turtle.xcor()는 turtle 객체의 x-coordinate, 즉 x 좌표를 반환하고, turtle.ycor()는 y 좌표를 반환한다.
아까 말했듯이 snake 객체의 크기가 20px x 20px 이므로 300(창 크기) - 20(객체 크기) = 280이 정확한 충돌 상태이다.
충돌할 시 게임 오버가 되야 하므로, 반복문을 중단시키고 game_over 메소드를 출력한다.
이 또한 scoreBoard에서 설명하겠다.
꼬리와의 충돌에서,
segments는 각 snake 객체, snake.segments는 snake 객체를 모아둔 리스트, snake.head는 뱀의 머리, 즉 가장 앞의 snake 객체이다.
뱀의 머리와 각 객체간의 거리가 10보다 적어졌을 때, 테두리 충돌과 동일한 방식으로 게임 오버 시킨다.
하지만 segments에는 snake.head도 포함되어 있으므로, snake.head이후의 객체에 대해서만 충돌 검사를 진행한다.
# snake.py
from turtle import Turtle
STARTING_POSITIONS = [(0, 0), (-20, 0), (-40, 0)]
MOVE_DISTANCE = 20
UP = 90
DOWN = 270
LEFT = 180
RIGHT = 0
class Snake:
def __init__(self) -> None:
self.segments = []
self.create_snake()
self.head = self.segments[0]
def create_snake(self):
for position in STARTING_POSITIONS:
new_segment = Turtle(shape="square")
new_segment.color("green")
new_segment.penup()
new_segment.goto(position)
self.segments.append(new_segment)
def move(self):
for seg_num in range(len(self.segments) - 1, 0, -1):
new_x = self.segments[seg_num - 1].xcor()
new_y = self.segments[seg_num - 1].ycor()
self.segments[seg_num].goto(new_x, new_y)
self.head.forward(MOVE_DISTANCE)
def up(self):
if self.head.heading() != DOWN:
self.head.setheading(UP)
def down(self):
if self.head.heading() != UP:
self.head.setheading(DOWN)
def left(self):
if self.head.heading() != RIGHT:
self.head.setheading(LEFT)
def right(self):
if self.head.heading() != LEFT:
self.head.setheading(RIGHT)
def add_segment(self, position):
new_segment = Turtle(shape="square")
new_segment.color("green")
new_segment.penup()
new_segment.goto(position)
self.segments.append(new_segment)
def extend(self):
# add a new segment into a snake
self.add_segment(self.segments[-1].position())
snake.py에는 여러 상수가 사용된다.
STARTING_POSITION은 시작할때 생성되는 3개의 snake 객체의 위치를 의미한다
MOVE_DISTANCE는 한번 움직일때 움직이는 거리, 20px을 뜻한다
UP, DOWN, LEFT, RIGHT는 turtle 객체의 heading 방향을 나타낸다.
self.segments에는 생성된 뱀 객체들이 저장된다.
self.head는 뱀 객체들 중 가장 선두 객체를 의미한다.
create_snake는 게임 시작 시 3개의 뱀을 생성하는 역할을 한다.
move는 뱀이 지속적으로 움직이는 가장 중요한 알고리즘인데, 이 알고리즘 때문에 main.py에서 screen.update를 굳이 사용한 것이다.

뱀의 길이는 정사각형 객체 여러개로 이루어져있기 때문에, 뱀이 이동하는 효과를 위해서는 각 객체 동일한 양만큼 이동해야 한다.
1번 turtle 객체를 머리로 설정했을 때, 1번은 상수 MOVE_DISTANCE = 10 만큼 항상 전진한다.
여기서 전진하는 방향(동 서 남 북)은 위에서 설명한 screen.listen()과 screen.onkey() 메소드로 조절된다.
하지만 1번이 먼저 전진했을 시, 1번의 기존 위치를 따로 변수에 할당한 후, 2번이 해당 위치로 움직이고, 2번에게 변수를 할당하고, 3번이 움직이는 식으로 비효율적이게 되니, 방법을 바꾼다.
4번이 3번으로 이동하고, 3번이 2번으로 이동하고, 2번이 1번으로 이동한 뒤, 1번이 forward(MOVE_DISTANCE)를 취한다.
역시 전진하는 방향은 동일하게 조절된다.
이렇게 될 시 문제는 시각적 효과가 뱀이 애벌레처럼 모였다가 늘어나고 모였다가 늘어나는 것이 보인다는 것이다.
그것은 우리가 원하는 효과가 아니기 때문에, tracer와 update 메소드를 사용함으로써 뱀의 모든 turtle 객체들이 한번씩 움직임을 마치고 난 뒤에 update를 함으로써 뱀이 모두 동시에 앞으로 가는 것 처럼 보이게 하는 효과를 낸다.
다음은 키 입력이 감지되었을 때, 뱀의 머리 방향을 바꾸는 메소드 4가지이다.
하지만 여기서 가고 있던 방향의 180도 반대 방향으로는 향할 수 없기 때문에, 이를 신경쓰며 작성했다.
add_segment는 입력받은 position위치로, 새로운 뱀을 생성하는 메소드이다.
position의 위치는 뱀의 최후방의 뱀 객체의 좌표일 것이다.
extend는 add_segment에 해당 뱀의 position을 입력해주는 메소드이다.
# food.py
from turtle import Turtle
import random
class Food(Turtle):
def __init__(self) -> None:
super().__init__()
self.shape("circle")
self.penup()
self.shapesize(stretch_len=0.5, stretch_wid=0.5)
self.color("orange")
self.speed("fastest")
random_x = random.randint(-280, 280)
random_y = random.randint(-280, 280)
self.goto(x=random_x, y=random_y)
def refresh(self):
random_x = random.randint(-280, 280)
random_y = random.randint(-280, 280)
self.goto(x=random_x, y=random_y)
food.py에는 음식, 또는 점수 객체에 대한 내용이 있다.
점수 객체는 Turtle을 상속받고, 전부 오렌지색 원이다.
원 또한 기본 픽셀이 20 x 20이므로 0.5배를 취해서 10x10 px로 고정시킨다.
speed("fastest")의 이유는 원의 생성 이후 아래 3줄의 코드에 따라 랜덤 좌표로 이동하는데, 이동하는 모습이 시각적으로 보이면 이상하기 때문이다.
즉, 이동하고 난 이후만 보여서 플레이 할 때 해당 위치에서 생겨난 것 처럼 보여준다.
refresh 메소드는 먹이를 먹었을 때의 상황으로, 새로운 먹이를 생성한다.
# scoreBoard.py
from turtle import Turtle
FONT = ('Courier', 25, 'normal')
class ScoreBoard(Turtle):
def __init__(self) -> None:
super().__init__()
self.score = 0
self.color("white")
self.penup()
self.hideturtle()
self.goto(0,270)
self.write(arg=f"Score: {self.score}", align="center", font=FONT)
def increase_score(self):
self.score += 1
self.clear()
self.write(arg=f"Score: {self.score}", align="center", font=FONT)
def game_over(self):
self.goto(0,0)
self.write("Game Over", align="center", font=('Courier', 35, 'normal'))
scoreBoard에는 점수판, 그리고 게임오버 시 게임오버 텍스트가 뜨는 객체들을 관리한다.
FONT는 폰트 정보를 담은 상수인데, 이는 특별한 이유는 없고 잘 어울리는 폰트와 크기를 정하면 된다.
(600x600의 화면에서는 25정도가 적절해보인다)
ScoreBoard객체 역시 Turtle을 상속받고, self.score를 0으로 설정한 뒤 food에서 했던것과 비슷하게 진행한다.
여기서 self.write는 직관적으로 해당 문구를 화면에 띄우는 효과를 한다.
increase_score메소드는 뱀이 음식을 먹었을때, 즉 뱀의 최선두 객체인 snake.head의 좌표가 food 객체와 distance < 15의 차이를 보일 때, 발동한다.
점수를 1 올리고, 기존의 텍스트를 삭제한 뒤 업데이트된 점수로 텍스트를 출력한다.
self.clear()를 하지 않으면 기존의 텍스트 위에 또 텍스트가 얹어지면서 읽을 수 없게 된다.
game_over메소드는 게임 오버 상황시 사용하기 위한 메소드인데, (0,0), 즉 화면 정중앙에 Game Over 텍스트를 띄우는 역할이다.
본 포스트는 udemy 강좌
'100 Days of Code: The Complete Python Pro Bootcamp for 2023' Section 20 - 21
의 내용을 정리한 것 입니다.
'python' 카테고리의 다른 글
[programmers] 문자열 여러번 뒤집기 (python) (8) | 2023.10.11 |
---|---|
[python] tkinter miles km converter / tkinter 마일 킬로미터 변환기 (3) | 2023.10.10 |
[python] *args 과 **kwargs (6) | 2023.10.10 |
[python] list comprehension 리스트 컴프리헨션 (0) | 2023.10.09 |
[python] code-runner extension 파일 실행 시 터미널을 초기화, file path 출력 안되게 변경 (0) | 2023.10.08 |