PyQt5 기반Tetris(테트리스) 게임앱

개요

이번 주제는 Python + PyQt5 로 만든 테스리스 게임입니다.

(키보드 화살표 ←, →, ↑(회전), ↓(빨리하강) 로 조작)


예전에 C++로 만들어 올려둔 게시물이 있는데, 구글에서 저작권 위반 신고가 들어와 게시물이 삭제(?)되어 현재는 게시글이 없어진 상태입니다.

제가 직접  짠 코드인데 이유는 정확히 모르지만 같이 넣어둔 게임 효과음이 원인이 아닐까 추측합니다.😓 

최근 심화반 학생들 중 테트리스 게임을 목표로 하는 학생이 있어 파이썬을 이용해 수업 참조 예제로 서둘러 만들어 보았습니다.  

테트리스는 난이도가 높은 예제이므로 게시물을 참조하여 포기하지 말고 끝까지 진행해 학습에 도움이 되었으면 하는 바램입니다.

버그가 있다면 댓글로 알려주시면 감사하겠습니다.

간단히 개발 과정을 요약해보면, 


  • 7가지 4x4 블럭을 생성, 블럭이 존재하는 부분은 값 (1) (2차원 Array)
    • 20x10의 배경맵(초기값 0)에 블럭을 겹치고 블럭이 존재하는 맵값을(1) 로 만들어 행 증가.
    • 행이 증가되면 블럭이 아래로 내려오고 이때, 저장해둔 이전 맵을 제거(값 1->0) 
    • 블럭 회전(방향키:↑) 은 회전된 블럭의 모양이 배경맵의 쌓인부분(2)과 겹치지 않으면 성공
      • 블럭의 다음행이 배경맵의 맨 아래(행 20) or 이미 쌓여진 부분(2)에 닿으면 값(2)변경
      • 다음 블럭을 새로 생성하고 다시 반복


      아래 그림을 참조바랍니다.

       

      개발환경 

      • Windows 11 Pro 64bit, Visual Studio 2022

      • Python 3.9 64bit, PyQt5 5.15.9

       

      소스코드

      모든 소스코드는 제가 직접 작성하였으며,

      코드 설명은 Google AI Gemini 에게 소스코드를 제공하고 설명을 부탁하였습니다.😁


      3개의 *.py 파일로 구성.

      • main.py (메인함수)

      • blocks.py (테트리스 블럭 및 추상화 클래스)

      • game.py (게임 진행 담당)

       

      Git Link : https://github.com/justdoit76/Tetris_Python

       

      main.py 소스코드

      소스코드의 메인 함수 입니다.

      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
      31
      32
      33
      34
      35
      36
      37
      38
      39
      40
      41
      42
      43
      44
      45
      46
      from PyQt5.QtWidgets import QApplication, QWidget, QMessageBox
      from PyQt5.QtGui import QCloseEvent, QKeyEvent, QPaintEvent, QPainter
      from game import Tetris
      import sys
       
      from PyQt5.QtCore import Qt
      QApplication.setAttribute(Qt.AA_EnableHighDpiScaling, True)
       
      class Window(QWidget):
           
          def __init__(self):
              super().__init__()
              self.setWindowTitle('Ocean Coding School')
              self.setFixedSize(340,640)
              self.tetris = Tetris(self)       
               
          def paintEvent(self, e) -> None:
              qp = QPainter()
              qp.begin(self)
              self.tetris.draw(qp)
              qp.end()
              return super().paintEvent(e)       
               
          def keyPressEvent(self, e) -> None:
              self.tetris.keyDown(e.key())
              return super().keyPressEvent(e)       
           
          def closeEvent(self, e) -> None:     
              self.tetris.run = False
              return super().closeEvent(e)   
           
          def gameOver(self):
              result = QMessageBox.information(self, 'Game Over!', 'Retry(Y), Exit(N)',
                                               QMessageBox.Yes| QMessageBox.No)
               
              if result==QMessageBox.Yes:
                  del(self.tetris)
                  self.tetris = Tetris(self)
              else:
                  self.close()       
               
      if __name__ == '__main__':
          app = QApplication(sys.argv)
          w = Window()
          w.show()
          sys.exit(app.exec_())

      [__init__]

      게임 창 제목, 크기 설정

      Tetris 객체 생성

      게임 시작

      [paintEvent] 

      QPainter 객체 생성

      Tetris 객체의 draw 함수 호출하여 게임 화면 그리기

      [keyPressEvent]

      눌린 키에 따라 Tetris 객체의 keyDown 함수 호출

      [closeEvent]

      Tetris 객체의 run 플래그 False로 설정하여 게임 종료

      [gameOver]

      게임 종료 메시지 출력

      사용자 선택에 따라 게임 재시작 또는 종료


      blocks.py 소스코드

      테트리스 블럭들의 공통 요소를 추상화 클래스(Abstract Base Class)로 하고 각 블럭들은 상속받는 형태로 구현.

      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
      31
      32
      33
      34
      35
      36
      37
      38
      39
      40
      41
      42
      43
      44
      45
      46
      47
      48
      49
      50
      51
      52
      53
      54
      55
      56
      57
      58
      59
      60
      61
      62
      63
      64
      65
      66
      67
      68
      69
      70
      71
      72
      73
      74
      75
      76
      77
      78
      79
      80
      81
      82
      83
      84
      85
      86
      87
      88
      89
      90
      91
      92
      93
      94
      95
      96
      97
      98
      99
      100
      101
      102
      103
      104
      105
      106
      107
      108
      109
      110
      111
      112
      113
      114
      115
      116
      117
      118
      119
      120
      121
      122
      123
      124
      125
      126
      127
      128
      129
      130
      131
      132
      133
      134
      135
      136
      137
      138
      139
      140
      141
      142
      143
      144
      145
      146
      147
      148
      149
      150
      151
      152
      153
      154
      155
      156
      157
      158
      159
      160
      161
      162
      163
      164
      165
      166
      167
      168
      169
      170
      171
      172
      173
      174
      175
      176
      177
      178
      179
      180
      181
      182
      183
      184
      185
      186
      187
      188
      189
      190
      191
      192
      193
      194
      195
      196
      197
      198
      199
      200
      201
      202
      203
      204
      205
      206
      207
      208
      209
      210
      211
      212
      213
      214
      215
      216
      217
      218
      219
      220
      221
      222
      223
      224
      225
      226
      227
      228
      229
      230
      231
      232
      233
      234
      235
      236
      237
      238
      239
      240
      241
      242
      243
      244
      245
      246
      247
      248
      249
      250
      251
      252
      253
      254
      255
      256
      257
      from abc import ABC, abstractmethod
      from enum import Enum
       
      class BType(Enum):
          O = 0
          I = 1
          S = 2
          Z = 3
          L = 4
          J = 5
          T = 6
       
      class Block(ABC):
       
          Size = 4
               
          @abstractmethod
          def __init__(self, type):
              super().__init__()       
              self.type = type
              self.arr = []
              self.idx = 0       
           
          def rotate_r(self):
              size = len(self.arr)
               
              if self.idx>=size-1:
                  self.idx = 0
              else:
                  self.idx += 1       
           
          def rotate_l(self):
              size = len(self.arr)
               
              if self.idx<=0:
                  self.idx = size-1
              else:
                  self.idx -= 1    
       
          def findTail(self):
              block = self.arr[self.idx]
              U = self.findUpperTail(block)
              D = self.findLowerTail(block)
              L = self.findLeftTail(block)
              R = self.findRightTail(block)
               
              return U, D, L, R
               
          def findUpperTail(self, block):       
              for r in range(Block.Size):
                  for c in range(Block.Size):                           
                      if block[r][c]:
                          return r
                       
          def findLowerTail(self, block):       
              for r in range(Block.Size-1, -1, -1):
                  for c in range(Block.Size):                           
                      if block[r][c]:
                          return r
                       
          def findLeftTail(self, block):       
              for c in range(Block.Size):
                  for r in range(Block.Size):                           
                      if block[r][c]:
                          return c
                       
          def findRightTail(self, block):       
              for c in range(Block.Size-1, -1, -1):
                  for r in range(Block.Size):                           
                      if block[r][c]:
                          return c
               
          def print(self):       
              print('-'*Block.Size)           
              for r in range(Block.Size):
                  for c in range(Block.Size):
                      if self.arr[self.idx][r][c]:
                          print('■', end='')
                      else:
                          print('□', end='')
                  print()
              print('-'*Block.Size)
       
      class BO(Block):   
          def __init__(self):
              super().__init__(BType.O)
              self.color = (255,255,0,255)
              # 0
              temp = [ [False, False, False, False] for _ in range(Block.Size) ]
              for r in range(1, 3):
                  for c in range(1, 3):
                      temp[r][c] = True
              self.arr.append(temp)
       
      class BI(Block):   
          def __init__(self):
              super().__init__(BType.I)
              self.color = (115,251,253,255)
              # 0
              temp =  [ [False, False, False, False] for _ in range(Block.Size) ]       
              for c in range(Block.Size):           
                  temp[1][c] = True
              self.arr.append(temp)
               
              # 1
              temp =  [ [False, False, False, False] for _ in range(Block.Size) ]
              for r in range(Block.Size):
                  temp[r][2] = True
              self.arr.append(temp)           
                   
      class BS(Block):   
          def __init__(self):
              super().__init__(BType.S)   
              self.color = (0,255,0,255)
              # 0
              temp =  [ [False, False, False, False] for _ in range(Block.Size) ]
              temp[1][2] = True
              temp[1][3] = True
              temp[2][1] = True
              temp[2][2] = True
              self.arr.append(temp)
               
              # 1
              temp =  [ [False, False, False, False] for _ in range(Block.Size) ]
              temp[0][2] = True
              temp[1][2] = True
              temp[1][3] = True
              temp[2][3] = True
              self.arr.append(temp)
               
      class BZ(Block):   
          def __init__(self):
              super().__init__(BType.Z)    
              self.color = (0,255,0,255)
              # 0
              temp =  [ [False, False, False, False] for _ in range(Block.Size) ]
              temp[2][2] = True
              temp[2][3] = True
              temp[1][1] = True
              temp[1][2] = True
              self.arr.append(temp)
               
              # 1
              temp =  [ [False, False, False, False] for _ in range(Block.Size) ]
              temp[0][3] = True
              temp[1][2] = True
              temp[1][3] = True
              temp[2][2] = True
              self.arr.append(temp)      
               
      class BL(Block):   
          def __init__(self):       
              super().__init__(BType.L)       
              self.color = (255,168,76,255)
              # 0       
              temp =  [ [False, False, False, False] for _ in range(Block.Size) ]       
              temp[1][1] = True
              temp[1][2] = True
              temp[1][3] = True
              temp[2][1] = True
              self.arr.append(temp)
               
              # 1       
              temp =  [ [False, False, False, False] for _ in range(Block.Size) ]       
              temp[0][2] = True
              temp[1][2] = True
              temp[2][2] = True
              temp[2][3] = True
              self.arr.append(temp)
               
              # 2       
              temp =  [ [False, False, False, False] for _ in range(Block.Size) ]       
              temp[0][3] = True
              temp[1][1] = True
              temp[1][2] = True
              temp[1][3] = True
              self.arr.append(temp)
               
              # 4       
              temp =  [ [False, False, False, False] for _ in range(Block.Size) ]       
              temp[0][1] = True
              temp[0][2] = True
              temp[1][2] = True
              temp[2][2] = True
              self.arr.append(temp)
               
      class BJ(Block):   
          def __init__(self):
              super().__init__(BType.J)
              self.color = (0,0,255,255)
              # 0       
              temp =  [ [False, False, False, False] for _ in range(Block.Size) ]       
              temp[1][1] = True
              temp[1][2] = True
              temp[1][3] = True
              temp[2][3] = True
              self.arr.append(temp)
               
              # 1       
              temp =  [ [False, False, False, False] for _ in range(Block.Size) ]       
              temp[0][2] = True
              temp[0][3] = True
              temp[1][2] = True
              temp[2][2] = True
              self.arr.append(temp)
               
              # 2       
              temp =  [ [False, False, False, False] for _ in range(Block.Size) ]       
              temp[0][1] = True
              temp[1][1] = True
              temp[1][2] = True
              temp[1][3] = True
              self.arr.append(temp)
               
              # 4       
              temp =  [ [False, False, False, False] for _ in range(Block.Size) ]       
              temp[0][2] = True
              temp[1][2] = True
              temp[2][1] = True
              temp[2][2] = True
              self.arr.append(temp)
               
      class BT(Block):   
          def __init__(self):
              super().__init__(BType.T)
              self.color = (255,0,255,255)
              # 0       
              temp =  [ [False, False, False, False] for _ in range(Block.Size) ]       
              temp[1][1] = True
              temp[1][2] = True
              temp[1][3] = True
              temp[2][2] = True
              self.arr.append(temp)
               
              # 1       
              temp =  [ [False, False, False, False] for _ in range(Block.Size) ]       
              temp[0][2] = True
              temp[1][2] = True
              temp[1][3] = True
              temp[2][2] = True
              self.arr.append(temp)
               
              # 2       
              temp =  [ [False, False, False, False] for _ in range(Block.Size) ]       
              temp[0][2] = True
              temp[1][1] = True
              temp[1][2] = True
              temp[1][3] = True
              self.arr.append(temp)
               
              # 4       
              temp =  [ [False, False, False, False] for _ in range(Block.Size) ]       
              temp[0][2] = True
              temp[1][1] = True
              temp[1][2] = True
              temp[2][2] = True
              self.arr.append(temp)     

      [__init__]

      블록 타입, 초기 모양, 회전 인덱스 설정

      [rotate_r]

      블록 시계 방향 회전

      [rotate_l]

      블록 반시계 방향 회전

      [findTail]

      블록의 상하좌우 가장 끝 셀 찾기

      [findUpperTail]

      블록 상단 가장 끝 셀 찾기

      [findLowerTail]

      블록 하단 가장 끝 셀 찾기

      [findLeftTail]

      블록 왼쪽 가장 끝 셀 찾기

      [findRightTail]

      블록 오른쪽 가장 끝 셀 찾기

      [print]

      블록 모양 출력

       

      game.py 소스코드

      실제 게임 진행을 담당하는 클래스, 별도의 실행흐름(Thread)을 생성한 후 아래와 같이 게임을 진행.

      1. Initialization of  Game Elements

      2. Generating Thread

      3. Moving and Rotating Blocks

      4. Block Stacking and Line Clearing

      이 후, 3, 4번을 쓰레드에서 반복

      이해하기 쉽도록 블럭들의 상태는 "0:없음, 1:이동중, 2:쌓기" 로 표시해 두었습니다.

      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
      31
      32
      33
      34
      35
      36
      37
      38
      39
      40
      41
      42
      43
      44
      45
      46
      47
      48
      49
      50
      51
      52
      53
      54
      55
      56
      57
      58
      59
      60
      61
      62
      63
      64
      65
      66
      67
      68
      69
      70
      71
      72
      73
      74
      75
      76
      77
      78
      79
      80
      81
      82
      83
      84
      85
      86
      87
      88
      89
      90
      91
      92
      93
      94
      95
      96
      97
      98
      99
      100
      101
      102
      103
      104
      105
      106
      107
      108
      109
      110
      111
      112
      113
      114
      115
      116
      117
      118
      119
      120
      121
      122
      123
      124
      125
      126
      127
      128
      129
      130
      131
      132
      133
      134
      135
      136
      137
      138
      139
      140
      141
      142
      143
      144
      145
      146
      147
      148
      149
      150
      151
      152
      153
      154
      155
      156
      157
      158
      159
      160
      161
      162
      163
      164
      165
      166
      167
      168
      169
      170
      171
      172
      173
      174
      175
      176
      177
      178
      179
      180
      181
      182
      183
      184
      185
      186
      187
      188
      189
      190
      191
      192
      193
      194
      195
      196
      197
      198
      199
      200
      201
      202
      203
      204
      205
      206
      207
      208
      209
      210
      211
      212
      213
      214
      215
      216
      217
      218
      219
      220
      221
      222
      223
      224
      225
      226
      227
      228
      229
      230
      231
      232
      233
      234
      235
      236
      237
      238
      239
      240
      241
      242
      243
      244
      245
      246
      247
      248
      249
      250
      251
      252
      253
      from PyQt5.QtCore import Qt, QRectF, QObject, pyqtSignal
      from PyQt5.QtGui import QBrush, QColor
      from blocks import BO,BI,BS,BZ,BL,BJ,BT, BType
      from threading import Thread, Lock
      from random import randint
      import time
       
      class Tetris(QObject):
           
          Row = 20
          Col = 10
          Blocks = (BO, BI, BS, BZ, BL, BJ, BT)
          update_signal = pyqtSignal()
          gameover_signal = pyqtSignal()
           
          def __init__(self, w):
              super().__init__()
              self.parent = w
              self.rect = w.rect()
               
              self.inrect = QRectF(self.rect)
              gap = 20
              self.inrect.adjust(gap, gap, -gap, -gap)       
       
              self.size = self.inrect.height() / Tetris.Row
               
              # create block
              self.initBlock()
               
              # create map
              self.initMap()
               
              # signal
              self.update_signal.connect(w.update)
              self.gameover_signal.connect(w.gameOver)
               
              # thread
              self.cs = Lock()
              self.t = Thread(target = self.threadFunc)
              self.run = True
              self.t.start()  
               
          def initBlock(self):
               # block       
              n = randint(BType.O.value, BType.T.value)
              print('Block Type:', n)
              self.block = Tetris.Blocks[n]()
               
              # standard row, col       
              self.cy = -1
              self.cx = Tetris.Col//2-self.block.Size//2
               
          def initMap(self):
              # color map
              self.cmaps = [[(0,0,0,0) for _ in range(Tetris.Col)] for _ in range(Tetris.Row)]
       
              # logical map (0:None, 1:Move block, 2:Stacked block)
              self.before = []
              self.maps = [[0 for _ in range(Tetris.Col)] for _ in range(Tetris.Row)]
               
              # displayed map       
              self.rects = []
              x = self.inrect.left()       
              y = self.inrect.top()
              for r in range(Tetris.Row):
                  temp = []
                  for c in range(Tetris.Col):
                      dx = x+c*self.size
                      dy = y+r*self.size
                      rect = QRectF(dx, dy, self.size, self.size)
                      temp.append(rect)
                  self.rects.append(temp)
               
          def draw(self, qp):
              self.drawBackground(qp)
              self.drawBlock(qp)     
               
          def drawBackground(self, qp):
              x = self.inrect.left()       
              y = self.inrect.top()
               
              x2 = self.inrect.right()
              y2 = self.inrect.top()
               
              x3 = self.inrect.left()
              y3 = self.inrect.bottom()
               
              for i in range(Tetris.Row+1):
                  qp.drawLine(x, y+i*self.size, x2, y2+i*self.size)
                  if i<Tetris.Col+1:
                      qp.drawLine(x+i*self.size, y, x3+i*self.size, y3)
           
          def drawBlock(self, qp):                  
              for r in range(Tetris.Row):
                  for c in range(Tetris.Col):
                      if self.maps[r][c]!=0:
                          if self.maps[r][c]==1:
                              R, G, B, A = self.block.color
                              b = QBrush(QColor(R,G,B,A))
                              qp.setBrush(b)
                          else:
                              R, G, B, A = self.cmaps[r][c]
                              b = QBrush(QColor(R,G,B,A))                       
                              qp.setBrush(b)
                      else:
                          qp.setBrush(Qt.NoBrush)                   
       
                      qp.drawRect(self.rects[r][c])
                      qp.drawText(self.rects[r][c], Qt.AlignCenter, f'{self.maps[r][c]}')
       
          def keyDown(self, key):
              self.cs.acquire()
               
              # find tail of blocks
              U, D, L, R = self.block.findTail()       
       
              if key==Qt.Key_Left:
                  if self.cx>0-L and self.isOverlapped(self.cx-1, self.cy)==False:
                      self.cx-=1
                      self.blockUpdate()               
              elif key==Qt.Key_Right:
                  if self.cx<Tetris.Col-1-R and self.isOverlapped(self.cx+1, self.cy)==False:
                      self.cx+=1
                      self.blockUpdate()               
              elif key==Qt.Key_Up:
                  self.block.rotate_r()
                  U, D, L, R = self.block.findTail()
                  # block has rotated off the screen or overlapped other block
                  if (self.cx<0-L or self.cx>Tetris.Col-1-R) or self.isOverlapped(self.cx, self.cy):
                      self.block.rotate_l()
                  self.blockUpdate()  
                  self.block.print()
              elif key==Qt.Key_Down:           
                  if self.cy-D<Tetris.Row-2:
                      self.cy+=1
                      self.blockUpdate()
                       
              self.cs.release()
               
          def isOverlapped(self, cx, cy):
              U, D, L, R = self.block.findTail()
              bl = self.block.arr[self.block.idx]
              for r in range(U, D+1):
                  for c in range(L, R+1):
                      if bl[r][c] and self.maps[cy-r-1][c+cx]==2:
                          return True
              return False
               
          def blockUpdate(self):
              bl = self.block.arr[self.block.idx]
              size = self.block.Size       
               
              # delete before blocks
              for r, c in self.before:
                  self.maps[r][c] = 0
              self.before.clear()
               
              # set current blocks
              for r in range(size):           
                  for c in range(size):
                      if bl[size-1-r][c]:
                          if self.cy-r>=0:
                              self.maps[self.cy-r][c+self.cx]=1
                              # remember current blocks
                              self.before.append( (self.cy-r, c+self.cx) )
                      
              # stack or not
              if not self.isMoveDown():
                  if self.cy<=1:
                      return False
                  self.stackBlock()
                  self.removeBlock()
                  self.initBlock()
               
              self.update_signal.emit()       
              return True
                               
          def isMoveDown(self):
              bl = self.block.arr[self.block.idx]
              size = self.block.Size
               
              for r in range(size):           
                  for c in range(size):
                      if bl[size-1-r][c]:                  
                          # bottom of map
                          if self.cy - r + 1 > Tetris.Row-1:
                              self.before.clear()
                              return False                   
                          elif self.cy-r >= 0:
                              # found stacked block
                              if self.maps[self.cy-r+1][c+self.cx]==2:                           
                                  self.before.clear()
                                  return False
              return True
           
          def stackBlock(self):
              bl = self.block.arr[self.block.idx]
              size = self.block.Size 
              color = self.block.color
               
              for r in range(size):           
                  for c in range(size):
                      if bl[size-1-r][c]:               
                          self.maps[self.cy-r][c+self.cx]=2
                          self.cmaps[self.cy-r][c+self.cx]=color
                           
          def removeBlock(self):       
              # find remove line
              lines = []       
              for r in range(Tetris.Row):
                  cnt = 0
                  for c in range(Tetris.Col):
                      if self.maps[r][c]==2:
                          cnt+=1
                      else:
                          break
       
                  if cnt == Tetris.Col:
                      lines.append(r)
                       
              if lines:           
                  # remove line       
                  for r in lines:
                      for c in range(Tetris.Col):
                          self.maps[r][c] = 0
                       
                      self.update_signal.emit()
                      time.sleep(0.2)              
                       
                      # fall blocks               
                      for rr in range(r-1, -1, -1):
                          for cc in range(Tetris.Col):
                              if self.maps[rr][cc]==2:
                                  self.maps[rr+1][cc] = 2
                                  self.maps[rr][cc] = 0
                                   
                                  self.update_signal.emit()
                                  time.sleep(0.1)
                      time.sleep(0.2)              
                       
       
          def threadFunc(self):
              while self.run:                           
                  self.cs.acquire()
                  self.cy+=1      
                  if not self.blockUpdate():
                      self.gameover_signal.emit()
                      break
                  #self.cy+=1      
                  self.cs.release()
                  print(self.cy, self.cx)
                  time.sleep(0.5)           
              print('thread finished...')

      [__init__]

      self.parent = w: 게임 화면 위젯을 저장

      게임 화면 영역 계산 (self.rect)

      내부 게임 영역 계산 (self.inrect)

      블록 크기 계산 (self.size)

      초기 블록 생성 (self.initBlock())

      맵 초기화 (self.initMap())

      시그널 연결 (게임 화면 업데이트, 게임 오버)

      게임 쓰레드 생성 및 시작 (self.t.start())

      [initBlock]

      랜덤하게 블록 타입 선택

      선택된 블록 클래스 생성

      블록 초기 위치 설정

      [initMap]

      맵을 나타내는 2차원 리스트 초기화 (빈 공간, 이동 가능, 쌓인 블록)

      화면 표시를 위한 QRectF 객체들의 리스트 생성

      [draw]

      게임 배경 그리기

      블록 그리기

      [drawBackground]

      가로 및 세로 선으로 게임 맵 그리기

      [drawBlock]

      맵 데이터를 확인하여 이동 가능한 블록, 쌓인 블록 구분하여 채우기

      블록 모양 및 맵 데이터 그리기

      [keyDown]

      눌린 키에 따라 블록 이동, 회전 처리

      락 (아래로 이동) 처리 시 블록 쌓임 및 라인 삭제 체크

      [isOverlapped]

      새로운 위치에서 블록과 기존 맵이 겹치는지 검사

      [blockUpdate]

      블록 이동 및 맵 업데이트

      이전 위치 블록 삭제

      새로운 위치 블록 표시

      쌓임 여부 확인

      쌓이면 블록 쌓기

      라인 삭제 필요 시 처리

      게임 화면 업데이트 시그널 발생

      [isMoveDown]

      블록이 아래로 이동 가능한지 검사

      [stackBlock]

      현재 위치 블록을 쌓기

      [removeBlock]

      완성된 라인 찾기

      라인 삭제 및 점수 계산 (아직 구현되지 않음)

      라인 삭제 후 블록 떨어뜨리기 효과

      [threadFunc]

      게임 쓰레드에서 반복적으로 수행

      블록을 한 칸 아래로 이동시키고 업데이트

      블록 이동 실패 시 게임 오버 시그널 발생

      일정 시간 간격으로 게임 진행

       


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

      마지막으로 왜 PyGame 을 사용하지 않고 PyQt를 사용했는지 물어보는 분들이 가끔씩 계십니다.

      그 이유는 파이썬에는 수많은 모듈이 존재하고 그 모듈의 사용법에 익숙해 지는데 많은 시간과 노력이 필요합니다.

      PyGame은 오직 게임에만 사용할 수 있다면, PyQt는 좀 더 넓은 사용처 (일반 앱, GUI, 다양한 플랫폼) 에 적용할 수 있으므로 활용도가 더 높다고 생각합니다.

      감사합니다.

      댓글

      이 블로그의 인기 게시물

      Qt Designer 설치하기

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