파이썬으로 구현한 스네이크 게임

이번 예제는 Python 과 PyQt5를 이용한 스네이크 게임(Snake Game)입니다.

[게임 화면 캡쳐]



구글에 "스네이크 게임"으로 검색하면 나오는 고전게임이며, 키보드 상,하,좌,우 화살표 키를 이용해 뱀을 이동시켜 먹이를 먹는 게임입니다.

[구글 스네이크 게임 화면]

뱀이 먹이를 먹으면 한 마디씩 몸통이 늘어나는 방식이며, 뱀 머리가 자신의 몸통에 닿거나 맵의 바깥쪽으로 나가면 게임이 종료 됩니다.

예전에 저도 C++과 MFC를 이용해 만들어 본 적이 있지만(홈페이지 예제 참조),  이번에는 파이썬을 이용해 만들어 보았습니다.

스네이크 게임은 프로그래밍 공부하기 참 좋은 주제입니다.

파이썬을 공부하고 있다면 꼭 한번 도전해 보기 바랍니다.


전체 게임 소스코드는 4개의 파이썬 파일(*.py)로 구성되어 있습니다.

1. main.py
앱을 시작시키는 메인함수.

2. window.py
프로그램 창(윈도우)를 생성하는 역할을 담당.

3. snake.py
뱀 마디 클래스(CNode)와 이를 조합한 뱀클래스(CSnake) 로 구성.

4. map.py
뱀이 이동하는 맵을 생성하고, 게임의 진행을 담당.


이 게임을 만드는데 사용된 주요 파이썬 문법 및 모듈은 아래와 같습니다.

1. if 조건문, for, while 반복문, 기본 자료형(int, bool, list 등)

2. class, function(함수), thread 및 동기화

3. PyQt5를 이용한 GUI 처리, Keyboard 입력처리


대략적인 개요를 설명드렸고, 이제 코드를 한번 살펴 볼까요.

코드는 위에서 언급한 4가지 파이썬 파일을 하나씩 분석해 보려고 합니다.


1. snake.py 소스코드

from PyQt5.QtCore import Qt

class CNode:
    def __init__(self, x=0, y=0):
        self.x = x
        self.y = y

    def __eq__(self, other):
        if self.x == other.x and self.y == other.y:
            return True
        else:
            return False        


class CSnake:
    def __init__(self, lines):

        # 뱀 마디 리스트
        self.node = []

        # 뱀 방향
        self.dir = Qt.Key_Right

        # 뱀 마디 추가
        self.bAdd = False

        self.init(lines)

    def init(self, lines):
        cx = lines//2
        cy = lines//2
        # 기본 뱀 3마디 생성
        for i in range(3):
            self.node.append(CNode(cx, cy))
            cx-=1

    def changeDir(self, key):      
        
        if self.isChangeDir(key)==False:
            return None

        self.dir = key
        
    def isChangeDir(self, key):
        # 현재 방향과 이동할 방향이 반대인지? 
        if self.dir == Qt.Key_Left and key == Qt.Key_Right:
            return False
        elif self.dir == Qt.Key_Right and key == Qt.Key_Left:
            return False
        elif self.dir == Qt.Key_Up and key == Qt.Key_Down:
            return False
        elif self.dir == Qt.Key_Down and key == Qt.Key_Up:
            return False
        else:
            return True

    def isCrach(self):

        if self.nodeCount() < 5:
            return False

        # 뱀 머리
        head = CNode(self.node[0].x, self.node[0].y)       

        # 몸통은 4번째 마디부터 충돌가능하므로
        bodylist = self.node[4:]

        for body in bodylist:
            if head == body:
                return True

        return False        

    def move(self):

        # 머리, 몸통 충돌 검사
        if self.isCrach():
            return False

        # 뱀 머리
        head = CNode(self.node[0].x, self.node[0].y)       

        if self.dir == Qt.Key_Left:
            head.x-=1
        elif self.dir == Qt.Key_Right:
            head.x+=1
        elif self.dir == Qt.Key_Up:
            head.y-=1
        elif self.dir == Qt.Key_Down:
            head.y+=1

        # 이동방향으로 뱀머리 추가
        self.node.insert(0,head)        

        # 이동시 밥 먹었으면 꼬리 유지, 아니면 제거
        if self.bAdd:
            self.bAdd = False            
        else:
            self.node.pop()

        return True

    def addNode(self):
        self.bAdd = True

    # 뱀 길이 얻기     
    def nodeCount(self):
        return len(self.node)

1번 라인은 뱀 이동 키보드 이벤트 처리를 위해 PyQt5의 모듈을 불러오는 구문입니다.

3번 라인의 CNode 클래스는 뱀의 한 마디를 의미하는 클래스 입니다. 맵의 2차원 배열에서 마디의 위치를 저장하는 정수형 변수 x, y (배열의 인덱스)를 가집니다.

8번 라인은 CNode 클래스의 같음을 비교하는 __eq__ 연산자 정의 입니다. 차후 뱀의 머리 마디가 먹이와 겹쳤는지 (먹이를 먹었는지) 비교하는 용도로 사용할 계획입니다.

15번 라인은 주인공인 뱀에 대한 CSnake 클래스 입니다.

생성자의 전달인자로 맵의 크기를 (15X15) 받아 옵니다. 뱀이 처음 만들어질 위치를 맵의 중간으로 하기 위함입니다.

생성자에서 객체 변수로, 뱀의 마디(CNode)들을 저장할 리스트, 뱀의 현재 진행방향, 먹이를 먹었는지 여부를 저장할 변수를 선언합니다.

이후 init() 함수를 통해 처음 생성되는 3마디 길이의 뱀을 생성하는데, x좌표를 감소시켜 뱀 머리의 왼쪽으로 다음 마디들을 생성합니다.

37번 라인의 changeDir() 함수는 뱀을 이동시키는 키보드 입력이 발생시 호출되는 함수이며, 뱀의 이동 방향을 변경합니다.

단, 게임 규칙상 뱀이 자신이 이동하는 방향의 반대(우->좌, 상->하) 로 이동하는 것은 불가하므로 isChangeDir() 함수를 호출해, 현재 자신의 이동방향과 반대가 아닌지 검사 한 후 방향을 변경시킵니다.

44번 라인의 isChangeDir() 함수는 뱀의 방향 변경이 가능한지 체크 하는 함수로, 새로 바뀌어야 할 뱀의 방향이 현재 이동방향의 반대 방향이 아니면 참을, 반대라면 거짓을 리턴합니다.

57번 라인의 isCrash() 함수는 뱀 머리가 자신의 몸통 마디와 충돌하는지 검사하는 함수이며, 이를 위해 뱀 머리를 대상으로 자신의 마디 길이 만큼 반복하면서 검사합니다.

단, 뱀 머리는 구조상 머리부터 4번째 마디부터 몸통과 충돌 가능하므로, 뱀의 마디가 5마디 보다 작다면 검사를 수행하지 않도록 합니다.

74번 라인의 move()  함수는 뱀을 이동시키는 함수이며, isCrash() 함수를 호출해 충돌하지 않았다면 뱀을 현재 진행방향대로 1칸 이동시킵니다.

뱀을 이동시키는 알고리즘이 재미있는데, 모든 마디를 이동시키는 것이 아니라 뱀의 머리와 꼬리 2개의 마디만 변경하면 뱀의 이동이 구현가능합니다.

이는 뱀의 현재 머리 좌표(x,y)를 구한 후 왼쪽 이동인 경우 x좌표만 -1 감소시켜 뱀머리를 기존의 뱀 리스트에 머리로 추가한 후, 먹이를 먹은 상태라면 꼬리는 그대로 둡니다. 결과적으로 이동방향으로 뱀의 머리만 추가되어 마디가 +1 되는 원리입니다.


먹이를 먹은 상태가 아니라면, 리스트의 pop() 함수를 통해 맨 마지막 요소(꼬리)를 잘라내어 삭제합니다. 머리가 추가되어 마디가 +1 이지만 꼬리 마디가 -1 되어 결과적으로 같은 길이를 가지며, 뱀 이동좌표는 반영됩니다.

와우 멋지군요.

103번 라인의 addNode() 함수는 게임을 진행하는 map.py 파일에서 뱀머리와 먹이의 좌표가 같은 경우(먹이를 먹은경우), 꼬리가 추가되어야 함을 self.bAdd 변수에 저장하기 위한 용도입니다.

107번 라인의 nodeCount() 함수는 뱀의 전체 마디 길이를 돌려주는 함수입니다. 현재 뱀의 마디 길이를 알고싶을때 사용하면 되겠지요.

여기까지가 snake.py 파일 코드 분석 입니다.

이제 뱀이 그려 질까요?

아닙니다.

우리는 이제 뱀의 위치좌표를 저장하고 처리하는 클래스를 완성하였을 뿐, 그림을 그려주는 것의 별개의 문제 입니다.


이제 뱀이 이동하는 공간인 맵을 담당하는 코드를 살펴보겠습니다.

2. map.py 소스코드

from snake import *
from PyQt5.QtCore import *
from PyQt5.QtGui import *
from threading import Thread, Lock
import time
from random import randint

class CMap:    

    def __init__(self, parent):
        super().__init__()
        
        # 쓰레드(실행흐름) 생성
        self.thread = Thread(target = self.playgame)
        # 쓰레드 동기화 락
        self.lock = Lock()        
        
        # 쓰레드 동작
        self.bRun = True

        #키보드 2회 연속 방지
        self.bMove = True

        # 게임 진행 상태
        self.bGame = False
        
        # 밥 먹은 횟수
        self.foodcnt = 0

        # 맵의 줄수
        self.lines = 15
        # 뱀 생성
        self.snake = CSnake(self.lines)
        # 뱀 먹이
        self.food = CNode(-1,-1)
        # 부모 윈도우 저장
        self.parent = parent
        # 부모 윈도우 크기 저장
        self.outrect = parent.rect()
        # 내부 맵 여백 조정
        gap = 20
        self.inrect = self.outrect.adjusted(gap,gap,-gap,-gap)

        # 맵 한칸의 크기
        self.wsize = self.inrect.width()/self.lines
        self.hsize = self.inrect.height()/self.lines

        # 맵 사각형 좌표 저장
        self.left = self.inrect.left()
        self.top = self.inrect.top()
        self.right = self.inrect.right()
        self.bottom = self.inrect.bottom()

        # 맵 사각형 저장 2차원 배열 생성 [ [열] 행]
        self.rect = [[QRectF for _ in range(self.lines)] for _ in range(self.lines)]

        # 생성된 사각형 배열 좌표 저장
        topleft = QPoint(self.left, self.top)
        size = QSize(self.wsize, self.hsize)

        for i in range(self.lines):
            for j in range(self.lines):                
                self.rect[i][j] = QRect(self.left+(j*self.wsize)
                                        , self.top+(i*self.hsize)
                                        , self.wsize
                                        , self.hsize)  
                # 사각형 크기 줄이기
                self.rect[i][j].adjust(2,2,-2,-2)

    def reStart(self):
        
        del(self.thread)
        self.thread = Thread(target = self.playgame)
        
        self.bRun = True
        self.bGame = False
        self.bMove = True
        self.foodcnt = 0
        
        del(self.snake)         
        self.snake = CSnake(self.lines)
        
        del(self.food)        
        self.food = CNode(-1,-1)


    def draw(self, qp):        
        
        # 맵 그리기
        for i in range(self.lines+1):
            # 가로선
            qp.drawLine(self.left, self.top+(i*self.hsize), self.right, self.top+(i*self.hsize))
            # 세로선
            qp.drawLine(self.left+(i*self.wsize), self.top, self.left+(i*self.wsize), self.bottom)

        # 뱀 그리기
        i=0
        self.lock.acquire()
        for node in self.snake.node:
            if i==0:
                qp.setBrush(QColor(33,53,194))
            else:
                qp.setBrush(QColor(83,103,255,128))
            if self.bRun:
                qp.drawRect(self.rect[node.y][node.x])           
            i+=1
        self.lock.release()

        self.lock.acquire()
        # 밥 그리기
        if self.food.x != -1 and self.food.y != -1:
            qp.setBrush(QColor(255,0,0))
            qp.drawEllipse(self.rect[self.food.y][self.food.x])
        self.lock.release()

        qp.drawText(self.outrect, Qt.AlignTop|Qt.AlignLeft, '점수:'+str(self.foodcnt))

        # 게임 도움말
        if not self.bGame:
            qp.setFont(QFont('맑은 고딕', 20))
            qp.drawText(self.outrect, Qt.AlignCenter, '키보드 방향키를 누르면 시작')


    def keydown(self, key):

        # 게임 시작 (키누름 and 미시작 상태)
        if (key == Qt.Key_Right or key == Qt.Key_Up or key == Qt.Key_Down) and self.bGame==False:
            self.bGame = True
            self.snake.changeDir(key)
            self.thread.start()

        # 게임 진행중 키 변경
        if (key == Qt.Key_Left or key == Qt.Key_Right or key == Qt.Key_Up or key == Qt.Key_Down) and self.bGame==True:
            if self.bMove:
                self.snake.changeDir(key)
                self.bMove = False

    def makeFood(self):
        # 밥이 있으면 리턴
        if self.food.x != -1 and self.food.y != -1:
            return

        cnt = 0
        while True:
            x = randint(0,self.lines-1)
            y = randint(0,self.lines-1)
            node = CNode(x,y)
            
            bDiff = False
                        
            for snode in self.snake.node:
                if node == snode:
                    bDiff = True
                    break                    

            if not bDiff:
                self.food = node
                break

            # 밥을 놓을 공간이 없다면 탈출
            if cnt>=self.lines*self.lines:
                break

            cnt+=1

    def isEat(self, node):

        if self.food == node:
            return True
        else:
            return False

    def isOut(self, head):
        if head.x < 0 or head.x > = self.lines:
            return True
        elif head.y < 0 or head.y > = self.lines:
            return True
        else:
            return False

    def playgame(self):
        
        while self.bRun:            
            self.lock.acquire()
            
            # 뱀 이동, 머리,몸통 충돌시 종료 or 맵 나가면 종료
            if not self.snake.move() or self.isOut(self.snake.node[0]):
                self.parent.update()
                self.bRun = False
                self.bGame = False
                self.lock.release()
                break

            # 밥 생성
            self.makeFood()

            # 밥 먹었는지?
            bEat = self.isEat(self.snake.node[0])            

            # 밥먹었으면 마디 추가, 밥좌표 초기화(없음으로)
            if bEat:
                self.snake.addNode()
                self.foodcnt+=1
                self.food.x = -1
                self.food.y = -1

            self.lock.release()

            # 키보드 2중 누름 방지
            self.bMove = True

            self.parent.update()
            time.sleep(0.3)

        if not self.bGame:         
            self.parent.endSignal.emit()

게임 배경을 만들고, 진행하고, 그림을 그리는 만큼 코드가 깁니다.

위 코드의 174, 176 번 라인의 > = 은 >= 로 붙여서 작성(실제 파이썬 코드에서)해야 합니다. 웹 페이지 소스코드 강조 모듈이 붙여쓰니 코드를 오류로 인식해 띄워두었습니다.

이제 하나씩 차근차근 분석해 보겠습니다.

1번라인은 모듈을 불러오는 코드입니다. 앞에서 만들어 둔 snake.py 와 PyQt5, thread, time, random 등 게임에 필요한 모듈입니다.

PyQt는 그림을 그리기 위해(GUI), thread는 앱의 실행흐름을 별도로 제작해 게임 동작과 관련한 모든 함수(뱀이동, 먹이 생성, 충돌 여부 등)를 반복적으로 실행합니다.

time 모듈은 thread의 sleep() 지연을 위해, random은 먹이를 맵에 랜덤하게 만들어 내기 위해 필요합니다.

map.py 파일은 8번 라인부터 시작하는 하나의 클래스로 구성되어 있습니다.

먼저 클래스의 생성자에서 각종 객체 변수를 선언합니다.

코드에 주석을 달아 두었으니 꼭 한번 살펴 보시기 바랍니다.

유심히 보아야 할 객체 변수 선언 부분은 36번 라인 부터입니다.

[맵 사이즈]
self.outrect 라는 사각형의 크기를 저장하는 QRect 클래스의 변수는 윈도우 창의 사각형 좌표(붉은색 사각형)를 저장합니다.

self.inrect 는 실제 맵이 그려지는 안쪽 파란색 사각형의 좌표를 저장합니다.

self.gap 은 outrect 와 inrect 사이의 여백 입니다.

self.wsize와 hsize는 맵의 inrect 사각형 크기를 lines(15칸) 로 나눈  한칸(주황색 사각형)의 너비와 높이를 의미합니다.

즉, 사용자에게 보여주기 위해 이용해 선을 긋고, 사각형을 그려 배경과 뱀을 시각적으로 표현 하기 위한 그래픽 좌표 처리 용도의 변수들 입니다.

55번 라인은 2차원 리스트를 생성하는 부분으로,  15X15 = 225 칸의 사각형 좌표를 저장하는 리스트를 생성합니다.

C++, MFC 라면 CRect rect[15][15] 의 형태이겠죠.

57번 라인부터 68번 라인까지는 위에서 생성된 225개의 사각형 좌표를 저장하는 변수에 2중 반복문을 통해 실제 윈도우의 좌표를 저장하는 부분입니다.

맨 위 왼쪽 사각형부터 차례대로 wsize, hsize 만큼 이동하여 225개의 실제 사각형 좌표를 2차원 배열에 대입하는 부분입니다.

68번 라인은 뱀이 사각형에 꽉 차게 그려지지 않도록 하기 위해 실제 사각형의 좌상단 좌표에서 +2 픽셀만큼 더하고 우하단 좌표에서 -2 만큼 빼서 저장되는 사각형의 크기를 작아지게 하는 부분입니다.


70번 라인 reStart() 함수는 게임 종료시 재시작을 담당하며, 쓰레드 삭제, 재시작 및 각종 변수를 초기화 하는 역할을 담당합니다.

87번 라인의 draw() 함수는 그림을 그리는 역할 (사각형 맵, 뱀, 먹이 등)을 담당하는 함수이며, CMap의 생성자에서 전달인자로 넘겨 받은 부모 윈도우(QWidget) 의 paintEvent() 함수에 의해 호출 됩니다.

 Qt의 QWidget 클래스는 그림을 새로 그려야 할 필요가 있을때 마다 Paint 시그널과 연결된 슬롯인 paintEvent() 함수가 호출되는데, 이때 CMap의 draw() 함수를 호출하도록 하였습니다. 차후 window.py 에서 코드를 살펴 보겠습니다.

draw 함수의 전달인자로 넘어오는 QPainter의 변수인 qp를 이용해 맵을 구성하는 선을 line의 수 15 + 1 개 만큼 그립니다. +1인 이유는 2개의 칸을 그리기 위해서는 3번의 선을 그어야 하기 때문입니다.

QPainter의 drawLine() 함수는 시작점(x, y) , 끝점(x, y) 좌표를 전달인자로 입력하면 해당 점을 연결하는 선을 그립니다.

뱀을 그리는 부분은 각 마디들이 맵에서 표시되는 인덱스를 갖는 리스트 snake.node 를 반복하며 그리는데, 첫 마디가 0이면 머리이므로 다른 색으로 표시하도록 합니다.

QPainter의 drawRect() 함수는 QRect 클래스 타입을 전달인자로 받아 사각형을 그리는 함수입니다. 물론 다른 오버로딩의 형태도 존재합니다.

뱀의 사각형을 그릴 때 중요한 부분이 있는데, 뱀 마디를 저장하는 snake.node 리스트는 별도의 thread에서 동시에 해당 리스트에 접근될 수도 있으므로, 동기화 처리를 반드시 해야 합니다.

self.lock.acquire()는 비유하자면, 공유자원(화장실 변기)에 진입한 후, 문을 걸어 잠그는 행위이며, self.lock.release()는 공유자원 사용을 마친 후 문을 열어 다른 사람도 사용하도록 하는 것입니다.

쓰레드 동기화는 하고 싶은 말이 많지만, 다 쓰려면 별도의 주제로 다루어야 할 정도로 내용이 방대하므로 여기서는 자세히 다루지 않겠습니다.

이어서 먹이를 그리는 코드는 사각형을 그리는 것과 동일하나 ellipse()함수를 이용해 타원 모양으로 그립니다.

Qt의 ellipse() 함수는 사각형을 그리는 것과 전달인자가 같지만, 해당 사각형 좌표에 내접하는 타원을 그려줍니다.


124번 라인의 keydown()함수는 QWidget 클래스(차후 설명할 windows.py 파일에 포함)의 키보드 눌러짐이 감지(keyPressEvent)되면 호출되는 함수입니다.

처음 게임 정지상태에서 시작하기 위해, 키보드 상,하,좌,우 중 하나의 화살표 키가 눌러지고, 그리고(bool and 연산) 게임이 시작상태가 아니라면 self.thread.start()함수를 통해 쓰레드를 실행시키고, 게임을 시작합니다.

만약 시작상태라면, 뱀의 방향만 변경합니다.

여기서 self.bMove라는 bool 타입 변수 값을 변경하는데, 이는 연속으로 키보드를 눌러 반대방향으로 뱀이 이동하는 버그를 막기 위해 사용됩니다.

self.bMove는 한번 키보드의 방향이 변경되면 상태 플래그를 변경시켜, 뱀이 한번 이동 한 후에만 키보드를 입력할 수 있도록 막아주는 역할을 담당합니다.


138번 라인의 makeFood() 함수는 이름 그대로 맵에서 생성되는 먹이의 좌표 인덱스를 생성하는 역할을 담당합니다.

무작위로 생성된 먹이의 위치는 뱀 위치와 중복되어 생성되지 않도록 처리가 필요합니다.


166번 라인의 isEat() 함수는 뱀이 먹이를 먹었는지, 즉 뱀 머리와 먹이 위치 인덱스가 일치하는지 검사 (CNode의 __eq__() 비교연산자 사용)해서 먹이를 먹었느지 여부를 리턴합니다.


173번 라인의 isOut() 함수는 뱀의 머리가 맵을 벗어 나지 않았는지 판단하는 역할입니다.

뱀머리의 x, y 좌표 인덱스가 0 보다 작거나, 15보다 크거나 같다면 뱀이 맵의 밖으로 나갔으므로 참을, 아니면 거짓을 리턴합니다.

참고로, 맵의 2차원 배열은 15X15개의 칸으로 구성되어 있으며 왼쪽 위는 Y:0, X:0 이며, 우 하단은 Y:14, X:14로 구성됩니다. (Y는 행, X는 열을 의미, 0번 부터 시작)



181번 라인의 playgame() 함수는 쓰레드에 의해 호출되는 함수이며, 무한 루프로 구성되어 있습니다.

1. 뱀이동, 2.먹이생성, 3.먹이 먹음 여부 판단해 4.마디 추가를 수행하는 함수를 차례로 매 0.3초 간격으로 반복호출해 전체 게임을 진행하는 역할을 담당합니다.

만약 게임이 종료되는 상황이라면, 무한루프를 탈출해 QWidget 윈도우로 게임종료 시그널(endSignal) 을 보냅니다.


정말 긴 코드였습니다. 여기까지가 map.py 파일 분석입니다.



3. window.py 소스코드

다음은 윈도우 창(QWidget)을 생성하는 windows.py 파일 코드입니다.

from map import * 
from PyQt5.QtWidgets import QWidget, QApplication, QMessageBox

class CWidget(QWidget):

    # 시그널 생성
    endSignal = pyqtSignal() 

    def __init__(self):       
        super().__init__()
        
        self.initUI()

    def __del__(self):
        pass

    def initUI(self):          
        self.setGeometry(100,100,500,500)
        self.setWindowTitle('스네이크 게임') 
        self.setFixedSize(self.rect().size())
        
        self.map = CMap(self)

        self.endSignal.connect(self.ExitGame)
       

    def paintEvent(self, e):
        qp = QPainter()
        qp.begin(self)
        self.map.draw(qp)
        qp.end()     


    def keyPressEvent(self, e):        
        self.map.keydown(e.key())
        
    def ExitGame(self):
        result = QMessageBox.information(self
                                         ,'게임 종료'
                                         , '다시시작 : Yes, 종료 : No'
                                         , QMessageBox.Yes | QMessageBox.No)

        if result == QMessageBox.Yes:            
            self.map.reStart()
        else:
            self.close()

1번 라인은 앞서 설명드린 map.py 파일을 모듈로 불러오는 코드입니다.

이어서 QWidget과 QApplication, 메시지 박스를 띄우기 위한 QMessageBox 클래스를 불러옵니다.

4번 라인은 QWidget에서 상속받은 나만의 클래스 CWidget에 대한 코드입니다.

7번 라인에서 게임 종료 시그널을 클래스 변수로 생성하고 있습니다. (객체 변수 X)

9번 라인의 생성자 함수는 CWidget의 부모인 QWidget의 생성자를 호출하고 initUI() 함수를 통해 윈도우 창의 시작위치와 크기를 설정(setGeometry() 함수)하고, 창 크기를 변경하지 못하도록(setFixedSize() 함수) 설정합니다.

이 코드의 핵심 클래스인 CMap 클래스 변수를 선언하는 코드가 이어지며, 클래스 변수로 선언한 사용자 정의 시그널을 슬롯(ExitGame() 함수) 과 연결합니다.


27번 라인 paintEvent()함수는 QWidget클래스의 함수 재정의 (오버라이딩)이며, 윈도우를 새로 그릴 필요가 있을때 마다 호출됩니다.

여기서 QPainter 클래스의 변수인 qp를 선언해 CMap클래스의 draw() 함수로 전달인자를 넘김으로서, 그리기 권한을 위임하는 형식으로 구성되어 있습니다.

QPainter는 비유하자면 그림을 그리는 팔과 도화지 역할을 하는 주체이며, 이를 전달 받은  CMap 클래스에서 맵, 뱀, 먹이 등을 그리게 됩니다.

참고로 QPainter는 MFC의 CDC클래스와 역할이 비슷합니다.


34번 라인의 keyPressEvent() 함수 또한, QWidget 클래스 함수 오버라이딩이며, 키보드를 누르면 그 처리를 CMap의 keydown() 함수를 호출해 위임하고 있습니다.


37번 라인 ExitGame() 함수는 CMap에서 게임 종료 조건이 충족되면 CWidget으로 보내는 시그널과 연결된 슬롯 함수입니다.

QMessageBox 클래스를 이용해 게임을 다시 할지, 종료할지를 물어보고 그에 맞는 처리를  함수 호출을 통해 진행합니다.




4. main.py 소스코드

import sys
from window import *

QApplication.setAttribute(Qt.AA_EnableHighDpiScaling, True)

if __name__ == '__main__':
    app = QApplication(sys.argv)
    w = CWidget()
    w.show()
    sys.exit(app.exec_())

따로 파일을 작성해야 할 필요가 있나 싶을 정도로 간단한 코드입니다.

바로 이곳이 프로그램의 첫 시작점 (메인 함수)입니다.

4번 라인은 4k 모니터를 위한 고해상도 모드 설정입니다.

6번 라인은 해당 코드가 모듈로 실행되는 것을 방지하기 위한 조건문입니다.

Qt에서 QWidget을 생성하기 위해서 반드시 선행되어야 하는 부분이 QApplication을 만드는 일입니다.

QApplicaiton이 모든 응용프로그램이 가져야 할 이벤트 루프를 생성하고 처리하는 역할을 담당하기 때문입니다.

코드를 보면 QApplication과  QWidget은 전혀 관계나, 연결이 없어보이지만, QWidget은 QApplication에서 생성되는 전역 변수 qApp 참조하는 형태로 내부적으로 구성되어 있습니다.



이상으로 전체 코드 분석을 마칩니다.

감사합니다.

pyinstaller 로 제작한 실행파일 링크 : 스네이크 게임 실행파일

  • 개발 환경
  1. 운영체제 : MS Windows 10 Pro
  2. 개발 언어 : Python 3.7 (32bit), PyQt5 (5.11.3)
  3. 개발 도구 : MS Visual Studio 2017 Pro

댓글

  1. 안녕하세요! 좋은 강의글 잘 봤습니다! 다만 한가지 질문이 있어서 여쭤봅니다. 마지막에 pyinstaller를 통해서 exe 응용프로그램으로 실행할 수 있도록 변환하셨는데 구글링을 해도 어떻게 해야할지 잘 감이 안잡혀서...혹시 참고 할 수 있는 링크 혹은 간단하게라도 설명해주시면 정말 감사하겠습니다!!

    답글삭제
    답글
    1. 안녕하세요.

      pyinstaller는 python 의 기본 모듈이 아니라 따로 설치해야 합니다.

      블로그의 게시물 중 "pyqt5 설치" 글을 참조해서 모듈 이름만 pyinstaller 로 바꾸어 설치하면 됩니다.

      모듈 설치 후 파이썬 코드이름이 main.py 라면,
      1. 해당 프로젝트의 경로로 cmd 창을 열고,
      2. pyinstaller -F -w main.py 라는 명령어를 실행하면 dist 폴더가 생성되고 여기에 *.exe 파일이 만들어 집니다.
      3. 자세한 내용은 https://www.pyinstaller.org/ 의 documentations를 참조하세요.

      삭제

댓글 쓰기

이 블로그의 인기 게시물

Qt Designer 설치하기

PyQt5 기반 동영상 플레이어앱 만들기