파이썬 예제 (T-Rex Game)

개요

안녕하세요. 요즘 자전거 타는 재미에 빠져 오랜만에 글을 올리는것 같네요.

이번시간에는 구글 크롬 웹브라우저에 내장되어 인터넷 연결이 원활하지 않을때 실행되는 T-Rex Game을 파이썬으로 만들어 보았습니다.

심화반 수업을 듣는 학원생 중, 이 게임을 주제로 진행하는 친구들이 많은데 참조바랍니다.

[ T-Rex Game 실행화면 ]

실행화면은 아래 동영상 참조 바랍니다.

 

개발환경

  • Windows 11 Pro 64bit, Visual Studio 2022

  • Python 3.9 (64bit)

  • PyQt5 5.15.6

 

소스코드

이 프로젝트는 다음과 같이 구성되어 있습니다.

두 파일을 같은 위치에 두고 main.py를 시작파일로 실행하면 됩니다.

 

  • main.py, game.py 2개의 파이썬 파일

  • 2종류의 공룡 그림, 4종류의 선인장 그림 파일 - 링크

    (rex0.png, rex1.png, cac0.png, cac1.png, cac2.png, cac3.png)
[이미지 출처 : 구글에서 캡쳐]

소스코드는 아래에서 설명하겠으며, 해당 그림파일은 다운로드 후, 압축을 풀고 소스코드와 같은 경로에 존재해야 실행이 가능합니다.

당연히 main.py 가 시작파일 입니다.


main.py 코드 분석

from PyQt5.QtWidgets import QApplication, QWidget, QMessageBox
from PyQt5.QtGui import QPainter
from PyQt5.QtCore import Qt
import sys
from game import Game

QApplication.setAttribute(Qt.AA_EnableHighDpiScaling, True)

class myWidget(QWidget):

    def __init__(self):
        super().__init__()
        self.setWindowTitle('Ocean Coding School')
        self.setFixedSize(800,300)
        self.game = Game(self)
        self.game.startGame()

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

    def keyPressEvent(self, e):
        self.game.keyPress(e.key())

    def closeEvent(self, e):
        self.game.endGame()

    def gameOver(self):
        res = QMessageBox.information(self, 'Game Over!', 'Retry(Yes), Exit(No)', QMessageBox.Yes | QMessageBox.No)
        if res==QMessageBox.Yes:
            del(self.game)
            self.game = Game(self)
            self.game.startGame()
        else:
            self.close()

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


main.py 는 위젯을 생성해 화면을 표시하고 Game 클래스의 객체를 생성하는 역할을 담당.

즉, 게임과는 관련이 없고 창 (위젯 or 윈도우)을 생성해 표시.


[라인 1~7]

필요한 모듈, 패키지를 불러오는 코드.


[라인 9~16]

QWidget에서 상속받은 myWidget 클래스의 시작.

생성자 함수에서 창의 제목, 크기 조정 후 뒤에 소개할 game.py 파일의 Game 클래스 객체 생성.


[라인 18~22]

QWidget 클래스의 paintEvent 함수 오버라이딩.

이 함수는 위젯을 새로 그려야 할 필요가 있을 때 마다 호출되는 함수이며, QPainter 객체를 생성해 Game 클래스의 객체로 전달.



[라인 24~25]
QWidget 클래스의 keyPressEvent 함수 오버라이딩.

이 함수는 위젯에 키보드 입력이 발생하면 호출되는 함수이며, 눌러진 키값을 Game 클래스의 객체로 전달.

위젯에서 직접 Paint, Keyboard 시그널을 처리하지 않고, Game 클래스에 전달하는 이유는 Widget과 Game Class로 코드를 분산하고, 역할을 나누기 위한 설계.

 

[라인  27~28]

QWidget 클래스의 closeEvent 함수 오버라이딩.

이 함수는 위젯을 닫을때 호출되는 함수이며, Game 클래스의 endGame() 함수를 호출해 게임을 중지하는 역할을 담당.

game.py 코드 분석

from PyQt5.QtCore import QObject, QRect, pyqtSignal, Qt
from PyQt5.QtGui import QPixmap, QFont
from threading import Thread
import time
import random

class Cactus:
    def __init__(self, i, r):
        self.idx = i
        self.rect = r
        self.dead = False

class Game(QObject):

    update_signal = pyqtSignal()
    end_signal = pyqtSignal()

    def __init__(self, w):
        super().__init__()
        self.parent = w
        self.rect = w.rect()

        # ground y pos
        self.gy = int( self.rect.height()*0.8 )

        # ground rocks
        self.gap = 6
        bt  = self.rect.bottom()        
        self.gr = []
        for i in range(self.rect.width()//self.gap):
            y = random.randint(self.gy, bt)
            self.gr.append(y)      

        # t-rex
        self.trex = QRect()
        self.jump = False
        self.jh = 0
        self.cnt = 0
        self.idx = 0
        self.rImgs = []
        for i in range(2):            
            img = QPixmap(f'rex{i}.png')
            self.rImgs.append(img)

        sz =  int( self.rect.height()*0.2 )
        x = int( self.rect.width()*0.1 )
        y = self.gy - sz - self.jh
        self.trex = QRect(x, y, sz, sz)

        # cactus
        self.cImgs = []
        for i in range(4):            
            img = QPixmap(f'cac{i}.png')
            w = img.width()/2
            h = img.height()/2
            img = img.scaled(w, h, Qt.KeepAspectRatio, Qt.SmoothTransformation)
            self.cImgs.append(img)     

        self.cs = []

        self.score = 0

        # signal
        self.update_signal.connect(self.parent.update)
        self.end_signal.connect(self.parent.gameOver)

    def startGame(self):
        self.t = Thread(target=self.threadFunc)
        self.run = True
        self.t.start()

    def endGame(self):
        self.run = False

    def draw(self, qp):
        # ground
        qp.drawLine(0, self.gy, self.rect.width(), self.gy)
        for x in range(self.rect.width()//self.gap ):
            qp.drawPoint( x*self.gap, self.gr[x] )

        # t-rex       
        qp.drawPixmap( self.trex, self.rImgs[self.idx], self.rImgs[self.idx].rect() )

        # cactus
        for c in self.cs:
            qp.drawPixmap( c.rect, self.cImgs[c.idx], self.cImgs[c.idx].rect() )

        # score
        f = QFont('arial', 15)
        qp.setFont(f)
        qp.drawText(self.rect, Qt.AlignTop|Qt.AlignHCenter, f'{self.score:003}')

    def keyPress(self, key):
        if key==Qt.Key_Space and self.jump==False:
            self.jump = True
            self.jh = 10
            t = Thread(target=self.jumping)
            t.start()

    def moveGround(self):
        y = random.randint(self.gy, self.rect.bottom())
        self.gr.pop(0)
        self.gr.append(y)

    def jumping(self):
        while True:
            self.trex.adjust(0, -self.jh, 0, -self.jh)
            self.jh -= 0.5

            if self.trex.bottom()>self.gy:
                self.trex.moveBottom(self.gy)
                self.jump = False
                self.update_signal.emit()
                break

            time.sleep(0.01)

    def createCactus(self):
        n = random.randint(1, 100)
        if n>=100:
            idx = random.randint( 0, len(self.cImgs)-1 )
            x = self.rect.right()
            y = self.gy-self.cImgs[idx].height()
            w = self.cImgs[idx].width()
            h = self.cImgs[idx].height()
            rect = QRect(x, y, w, h)
            cs = Cactus(idx, rect)

            if self.cs:
                gap = self.rect.right() - self.cs[-1].rect.right()
                if gap>self.rect.width()*0.2:
                    self.cs.append(cs)
            else:
                self.cs.append(cs)            

    def moveCactus(self):
        sp = self.gap
        result = 1
        for c in self.cs:
            if c.rect.intersects(self.trex):                
                result = 0
                break
            elif c.rect.right()<0:
                c.dead = True
                self.score += 1
                break
            else:
                c.rect.adjust(-sp, 0, -sp, 0)

        # delete cactus
        self.cs = [c for c in self.cs if c.dead==False]
        return result

    def threadFunc(self):
        while self.run:
            self.moveGround()
            self.createCactus()
            if( self.moveCactus() == False):
                self.end_signal.emit()
                self.run = False

            if self.cnt%5 == 0:
                self.idx = int( not bool(self.idx) )                
            self.cnt += 1
            
            self.update_signal.emit()
            time.sleep(0.01)


game.py 파일은 T-Rex 게임을 실제 생성하고 관리하는 Game 클래스로 구성.

[라인 1~5]

코드에 필요한 모듈 불러오기.

 

[라인 7~11]

선인장 클래스이며 선인장 번호 (int, 이미지 3가지 종류), 선인장 사각형 위치 (QRect) ,  선인장 생존여부(bool, 화면밖으로 나갔는지) 를 3가지 객체 변수로 가짐.


[라인 13~65]

Game 클래스의 시작.

화면 갱신, 게임 종료 처리용 SIGNAL 클래스변수 선언.

생성자에서 main.py 의 위젯 정보 및 위젯의 사각형 정보를 저장.

지면 높이 (gy) 및 지면 이동효과 (우측->좌측, 즉 X좌표 감소) 를 표시하기 위한 변수 (gr) 생성.

(실제 게임의 이동은 공룡이 아닌 지면의 X좌표가 감소하며 구현됨)

공룡 이미지 처리용 리스트 (rImgs), 위치 표시용 변수 (trex) 생성.

선인장 이미지 처리용 리스트 (cImgs), Cactus 클래스 객체 저장용 리스트 (cs) 생성.

[ 지면 높이, 이동효과 구현 변수 ]

 

[라인 67~70]

Thread 생성 및 시작.


[라인 72~73]

Thread 의 while 무한 루프를 제어하는 self.run 변수를 False 처리해 쓰레드 종료.


[라인 75~91]

main.py 파일에 있는 Form 클래스 (위젯) 를 새로 그려야 할 필요가 있을때 마다 호출되는 함수.

그 때 QPainter 객체인 qp를 전달인자로 받아 그림을 그리는 역할.

지면에 표시되는 점들이 저정된 self.gr 리스트의 x좌표를 감소시켜 지면 그리기.

T-Rex이미지 2장을 번갈아 표시하여 공룡이 뛰어다니는것 처럼 그리기.

선인장 및 점수 표시.


[라인 93~98]

키보드 눌러짐을 감지.

스페이스 키가 눌러지면 점프 높이를 설정하고, 점프 처리를 위한 쓰레드를 생성.


[라인 100~103]

지면을 표시하는 점 리스트의 첫부분을 제거(POP)하고 맨 마지막에 새로운 지면 Y좌표를 추가(APPEND).

 

[라인 105~116]

공룡의 점프를 구현한 쓰레드 함수.

실제 공룡의 점프는 Y좌표를 감소하는 방식으로 구현되어 있으며, 가속도를 표현하기 위해 감소폭이 점차 작아지다 정점에 도달하면 부호가 바뀌며 다시 내려오는 방식으로 구현.

 

[라인 118~134]

선인장을 생성하는 함수.

화면 오른쪽 끝 넘어 보이지 않는 부분에 선인장을 생성하고, 생성되는 선인장들의 최소 간격을 조절.


[라인 136~152]

선인장 리스트에 저장된 선인장의 X좌표를 감소시켜 이동.

공룡과 충돌하거나, 화면밖을 나간 경우는 선인장 삭제.

공룡과 선인장의 충돌은 코드 복잡도를 줄이기 위해 단순 사각형의 교집합이 잡히는지 검출하는 방식이므로 개선 (N개의 폴리곤 처리 등) 이 필요.

[공룡과 선인장 충돌처리 예시]

[라인 154~167]

쓰레드에서 호출되는 함수이며 게임 진행을 담당하는 함수.

무한 루프를 돌며, 지면 이동, 선인장 생성, 화면 갱신 등을 처리.


이상으로 모든 설명을 마칩니다. 감사합니다.

댓글

댓글 쓰기

이 블로그의 인기 게시물

Qt Designer 설치하기

C++ 예제 (소켓 서버, 이미지, 파일전송)