Skip to content

Latest commit

 

History

History
431 lines (379 loc) · 15.3 KB

211224.md

File metadata and controls

431 lines (379 loc) · 15.3 KB

Day 79

파이썬으로 배우는 게임 개발 입문편

블록 격파 게임 완성

from PyQt5.QtWidgets import *
from PyQt5.QtGui import *
from PyQt5.QtCore import *
import sys
import random
import time
from threading import Thread, Lock


class Ball_Bar_Block:
    def __init__(self, parent):
        # 공
        self.ball_x = 0 # 공의 X 좌표
        self.ball_y = 0 # 공의 Y 좌표
        self.ball_xp = 0
        self.ball_yp = 0
        # 바
        self.bar_x = 0 # 바의 X 좌표
        self.bar_y = 540 # 바의 Y 좌표
        # 블록
        self.block = []
        for i in range(5):
            self.block.append([1] * 10)
        for i in range(10):
            self.block.append([0] * 10)
        # 점수, 스테이지
        self.score = 0
        self.stage = 0

        self.parent = parent

    def move_ball(self):
        self.ball_x = self.ball_x + self.ball_xp
        if self.ball_x < 20:
            self.ball_x = 20
            self.ball_xp = -self.ball_xp
        if self.ball_x > 780:
            self.ball_x = 780
            self.ball_xp = -self.ball_xp
        x = int(self.ball_x / 80)
        y = int(self.ball_y / 40)
        if self.block[y][x] == 1:
            self.block[y][x] = 0
            self.ball_xp = -self.ball_xp
            self.score = self.score + 10

        self.ball_y = self.ball_y + self.ball_yp
        if self.ball_y >= 600:
            self.parent.idx = 2
            self.parent.tmr = 0
            return
        if self.ball_y < 20:
            self.ball_y = 20
            self.ball_yp = -self.ball_yp
        x = int(self.ball_x / 80)
        y = int(self.ball_y / 40)
        if self.block[y][x] == 1:
            self.block[y][x] = 0
            self.ball_yp = -self.ball_yp
            self.score = self.score + 10

        if self.bar_y - 40 <= self.ball_y and self.ball_y <= self.bar_y:
            if self.bar_x - 80 <= self.ball_x and self.ball_x <= self.bar_x + 80:
                self.ball_yp = -10
                self.score = self.score + 1
            elif self.bar_x - 100 <= self.ball_x and self.ball_x <= self.bar_x - 80:
                self.ball_yp = -10
                self.ball_xp = random.randint(-20, -10)
                self.score = self.score + 2
            elif self.bar_x + 80 <= self.ball_x and self.ball_x <= self.bar_x + 100:
                self.ball_yp = -10
                self.ball_xp = random.randint(10, 20)
                self.score = self.score + 2
    
    def key_press(self, key): # 키보드 이벤트 처리
        if key == Qt.Key_Space and self.parent.bGame == False and self.parent.idx == 0: # 게임 시작
            self.parent.thread.start()
            self.parent.bGame = True

        if key == Qt.Key_Backspace: # 게임 종료
            self.parent.idx = 4

        if key == Qt.Key_Space and self.parent.stage_clear == True: # 다음 스테이지로
            for y in range(5):
                for x in range(10):
                    self.block[y][x] = 1
            self.parent.idx = 0
            self.parent.tmr = 1
            self.stage = self.stage + 1
            self.parent.stage_clear = False

        if key == Qt.Key_R and self.parent.game_over == True: # 재도전
            self.score = 0
            self.parent.idx = 0
            self.parent.tmr = 1
            self.parent.game_over = False

        if key == Qt.Key_N and self.parent.game_over == True: # 새 게임
            for y in range(5):
                for x in range(10):
                    self.block[y][x] = 1
            self.parent.idx = 0
            self.parent.tmr = 0
            self.parent.game_over = False
        
        if key == Qt.Key_Left and self.bar_x > 80: # 바를 왼쪽으로
            self.bar_x = self.bar_x - 40
        if key == Qt.Key_Right and self.bar_x < 720: # 바를 오른쪽으로
            self.bar_x = self.bar_x + 40

    def block_color(self, x, y): # 블럭의 색깔을 동적으로 정하는 함수
        color_hex = '#{0:x}{1:x}{2:x}'.format(15 - x - int(y / 3), x + 1, y * 3 + 3) # 16진수 RGB 값으로 변환
        col = QColor(color_hex)
        return col

class CMap:
    def __init__(self, parent):
        super().__init__()

        self.thread = Thread(target = self.playgame)
        self.bRun = True
        self.bGame = False
        self.is_clr = True
        self.game_over = False
        self.stage_clear = False
        self.idx = 0
        self.tmr = 0
        self.ball = Ball_Bar_Block(self)
        self.parent = parent

        
    def draw(self, qp): # 블럭, 텍스트 같은 그래픽 요소들을 화면에 표시
        qp.setFont(QFont('Times New Roman', 20, QFont.Bold))
        if self.bGame == False: # 타이틀 화면
            qp.setPen(QPen(Qt.cyan))
            self.start_txt = 'Press [Space] to Play'
        else:
            self.start_txt = ''
        qp.drawText(275, 300, self.start_txt)

        if self.game_over == True: # 게임 오버 화면
            self.gameover_txt = 'GAME OVER'
            self.replay_txt = '[R]eplay'
            self.newgame_txt = '[N]ew game'
        else:
            self.gameover_txt = ''
            self.replay_txt = ''
            self.newgame_txt = ''
        qp.setPen(QPen(Qt.red))
        qp.drawText(325, 260, self.gameover_txt)
        qp.setPen(QPen(Qt.cyan))
        qp.drawText(200, 340, self.replay_txt)
        qp.setPen(QPen(Qt.yellow))
        qp.drawText(500, 340, self.newgame_txt)
        
        if self.stage_clear == True: # 스테이지 클리어 화면
            self.stageclear_txt = 'STAGE CLEAR'
            self.nextstage_txt = 'NEXT [SPACE]'
        else:
            self.stageclear_txt = ''
            self.nextstage_txt = ''
        qp.setPen(QPen(QColor(204, 255, 0)))
        qp.drawText(325, 260, self.stageclear_txt)
        qp.setPen(QPen(Qt.cyan))
        qp.drawText(325, 340, self.nextstage_txt)

        self.is_clr = True
        for y in range(15): # 블럭과 스테이지, 점수 표시
            for x in range(10):
                gx = x * 80
                gy = y * 40
                if self.ball.block[y][x] == 1:
                    qp.setBrush(self.ball.block_color(x, y))
                    qp.setPen(QPen(Qt.black))
                    qp.drawRect(gx + 1, gy + 4, 78, 31)
                    self.is_clr = False
        qp.setFont(QFont('Times New Roman', 20, QFont.Bold))
        qp.setPen(QPen(Qt.white))
        self.stage_txt = 'STAGE ' + str(self.ball.stage)
        qp.drawText(150, 30, self.stage_txt)
        self.score_txt = 'SCORE ' + str(self.ball.score)
        qp.drawText(550, 30, self.score_txt)

        # 공 그리기
        qp.setBrush(QColor(255, 228, 73))
        qp.drawEllipse(self.ball.ball_x - 20, self.ball.ball_y - 20, 40, 40)
        qp.setPen(QColor(255, 235, 65))
        qp.setBrush(QColor(255, 244, 50))
        qp.drawEllipse(self.ball.ball_x - 16, self.ball.ball_y - 16, 28, 28)

        # 바 그리기
        qp.setBrush(QColor(181, 181, 189))
        qp.drawRect(self.ball.bar_x - 80, self.ball.bar_y - 12, 160, 24)
        qp.setBrush(QColor(181, 181, 189))
        qp.drawRect(self.ball.bar_x - 78, self.ball.bar_y - 14, 156, 28)
        qp.setBrush(QColor(230, 230, 233))
        qp.drawRect(self.ball.bar_x - 78, self.ball.bar_y - 12, 156, 24)

    def playgame(self): # 게임의 진행과 흐름을 총괄하는 함수, thread로 처리
        while self.bRun:
            if self.idx == 0: # 게임 시작 준비
                self.tmr = self.tmr + 1
                if self.tmr == 1:
                    self.ball.stage = 1
                    self.ball.score = 0
                if self.tmr == 2:
                    self.ball.ball_x = 160
                    self.ball.ball_y = 240
                    self.ball.ball_xp = 10
                    self.ball.ball_yp = 10
                    self.ball.bar_x = 400
                    self.idx = 1
            elif self.idx == 1: # 게임 진행
                self.ball.move_ball()
                if self.is_clr == True:
                    self.idx = 3
                    self.tmr = 0
            elif self.idx == 2: # 게임 오버
                self.tmr = self.tmr + 1
                if self.tmr == 1:
                    self.game_over = True
            elif self.idx == 3: # 스테이지 클리어
                self.tmr = self.tmr + 1
                if self.tmr == 1:
                    self.stage_clear = True
            elif self.idx == 4: # 게임 종료
                self.bGame = False
                self.bRun = False
                break
            self.parent.update()
            time.sleep(0.03)
        if not self.bGame:
            self.parent.endSignal.emit()

class WindowClass(QWidget):
    endSignal = pyqtSignal()
    def __init__(self):
        super().__init__()

        self.initUI()

    def initUI(self):
        self.setWindowTitle('Block game')
        self.setGeometry(100, 100, 800, 600)
        pal = QPalette()
        pal.setColor(QPalette.Background, QColor(0, 0, 0))
        self.setAutoFillBackground(True)
        self.setPalette(pal)

        self.game = CMap(self)
        self.endSignal.connect(self.ExitGame)

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

    def keyPressEvent(self, e):
        self.game.ball.key_press(e.key())

    def ExitGame(self):
        self.close()

if __name__ == "__main__":
    app = QApplication(sys.argv)
    main_W = WindowClass()
    main_W.show()
    sys.exit(app.exec_())

블럭의 색 부분은 책에서 16진수 RGB 코드로 변환하는 과정이 있었는데 내가 해당 부분을 처리하지 않았기 때문에 일어난 일이었다.

def block_color(self, x, y): # 블럭의 색깔을 동적으로 정하는 함수
    color_hex = '#{0:x}{1:x}{2:x}'.format(15 - x - int(y / 3), x + 1, y * 3 + 3) # 16진수 RGB 값으로 변환
    col = QColor(color_hex)
    return col

이와 같이 변환시켜주니까 해결되었다.

이번에 하면서 추가로 알게된 것들이 있었다. 우선 main_window쪽 클래스에서 모든 것을 처리하려고 하지 않아도 된다는 것이다. 지금까지는 클래스 하나로 처리하려고 해왔었는데 이번에는 클래스를 분할해서 사용하니까 처리하기가 한결 수월했다. 추가로 qpainter로 그린 것을 지우는 것이 항상 문제였는데 변수로 텍스트를 관리하면 해결할 수 있었다.

if self.bGame == False: # 타이틀 화면
    qp.setPen(QPen(Qt.cyan))
    self.start_txt = 'Press [Space] to Play'
else:
    self.start_txt = ''
qp.drawText(275, 300, self.start_txt)

이 부분이 대표적인 예시이다. self.start_txt로 게임이 시작되기 전에는 'Press [Space] to Play'라는 문장을 저장하고 화면에 그린다. 하지만 게임이 시작되면 ''을 저장하고 출력하는데 아무것도 없으니 출력되지 않는다.

Threading과 Multiprocessing

이번 것을 하면서 threading과 multiprocessing이 다르다는 것을 친구에게 들었다.
우선 python의 threading은 GIL 정책이 있다.
GIL이란 여러 쓰레드가 있어도 CPU 자원을 사용할 수 있는 쓰레드는 하나만 허용하는 것이다. 그러니까 실질적으로 동시에 실행 처리가 되는 것이 아니라서 IO 대기를 하는 것이 아니면 손실이 있다는 것이다.
하지만 cpu 동작을 많지 않고 I/O 동작이 더 많은 프로그램에서는 충분히 효과를 얻을 수 있다.

이에 반해 multiprocessing은 프로세스를 여러개 두어 병렬 처리를 하기 때문에 CPU 사용이 많은 작업이면 threading보다 효율적으로 동작한다는 것이다. 하지만 더 많은 메모리를 사용한다.

Threading 예제

# 0부터 100,000,000 까지의 합을 구한다.
from threading import Thread

def work(id, start, end, result):
    total = 0
    for i in range(start, end):
        total += i
    result.append(total)
    return

if __name__ == '__main__':
    START, END = 0, 100000000
    result = list()
    th1 = Thread(target=work, args=(1, START, END, result))

    th1.start()
    th1.join()

print(f'Result : {sum(result)}')

Multiprocessing 예제

# 0부터 100,000,000 까지의 합을 구한다.
from multiprocessing import Process, Queue

def work(id, start, end, result):
    total = 0
    for i in range(start, end):
        total += i
    result.put(total)
    return

if __name__ == "__main__":
    START, END = 0, 100000000
    result = Queue()
    th1 = Process(target=work, args=(1, START, END//2, result))
    th2 = Process(target=work, args=(2, END//2, END, result))
    
    th1.start()
    th2.start()
    th1.join()
    th2.join()

    result.put('STOP')
    total = 0
    while True:
        tmp = result.get()
        if tmp == 'STOP':
            break
        else:
            total += tmp
    print(f"Result: {total}")

파이썬으로 배우는 게임 개발 실전편

Chapter 1

Walking dog

from PyQt5.QtWidgets import *
from PyQt5.QtGui import *
from PyQt5.QtCore import *
import sys
import time
from threading import Thread

class WindowClass(QWidget):
    def __init__(self):
        super().__init__()
        self.img_dog = [
            QPixmap('Python_workspace\python_game\image\walking_dog\dog0.png'),
            QPixmap('Python_workspace\python_game\image\walking_dog\dog1.png'),
            QPixmap('Python_workspace\python_game\image\walking_dog\dog2.png'),
            QPixmap('Python_workspace\python_game\image\walking_dog\dog3.png')
        ]
        self.img_bg = QPixmap('Python_workspace\python_game\image\walking_dog\park.png').scaled(480, 380)
        self.initUI()
        self.worker = animating(self)
        self.worker.start()
        self.show()

    def initUI(self):
        self.setGeometry(100, 100, 480, 380)
        self.setWindowTitle('Walking dog')
        self.background_left = QLabel(self)
        self.background_left.setPixmap(self.img_bg)
        self.background_left.move(-480, 0)
        self.background_middle = QLabel(self)
        self.background_middle.setPixmap(self.img_bg)
        self.background_middle.move(0, 0)
        self.background_right = QLabel(self)
        self.background_right.setPixmap(self.img_bg)
        self.background_right.move(480, 0)
        self.dog = QLabel(self)
        self.dog.move(240, 200)
        self.dog.setPixmap(self.img_dog[0])

class animating(QThread):
    def __init__(self, parent):
        super().__init__()
        self.parent = parent

    def run(self):
        self.x = 0
        self.ani = 0
        background_loc = [self.parent.background_left, self.parent.background_middle, self.parent.background_right]
        while True:
            self.x = self.x + 4
            if self.x % 240 == 0:
                background_loc[2].move(-480, 0)
                tmp = background_loc.pop()
                background_loc.insert(0, tmp)
                

            background_loc[0].move(self.x - 480, 0)
            background_loc[1].move(self.x, 0)
            background_loc[2].move(self.x + 480, 0)
            self.ani = (self.ani + 1) % 4
            self.parent.dog.setPixmap(self.parent.img_dog[self.ani])
            time.sleep(0.2)


if __name__ == "__main__":
    app = QApplication(sys.argv)
    main_W = WindowClass()
    sys.exit(app.exec_())

이번에도 tkinter로 되어있는 것을 PyQt로 만들려고 했으나 배경이 자연스럽게 넘어가지지 않는 상황이다.