참고영상: https://www.youtube.com/watch?v=SfPWCKTHzE4
소스코드 미리보기
import pygame
import random
colors = [
(0,0,0),
(0,255,255), #I - Cyan
(0,0,255), #Reverse L - Blue
(255,127,0), #L - Orange
(255,255,0), #Block - Yellow
(0,255,0), #S - Green
(128,0,128), #T - Purple
(255,0,0), #Reverse S - Red
]
class Figure:
x = 0
y = 0
Figures = [
[[1,5,9,13], [4,5,6,7]], #I
[[0,1,4,8], [0,4,5,6], [1,5,9,8], [4,5,6,10]], #Reverse L
[[0,1,5,9], [4,5,6,8], [0,4,8,9], [2,4,5,6]], #L
[[1,2,5,6]], #Block
[[6,7,9,10], [1,5,6,10]], #S
[[1,4,5,6], [1,4,5,9], [4,5,6,9], [1,5,6,9]], #T
[[4,5,9,10], [2,6,5,9]], #Reverse S
]
def __init__(self, x_coord, y_coord):
self.x = x_coord
self.y = y_coord
self.type = random.randint(0, len(self.Figures)-1)
self.color = colors[self.type+1]
self.rotation = 0
def image(self):
return self.Figures[self.type][self.rotation]
def rotate(self):
self.rotation = (self.rotation + 1) % len(self.Figures[self.type])
class Tetris:
height = 0
width = 0
field = []
score = 0
state = "start"
Figure = None
Next = None
def __init__(self, _height, _width):
self.height = _height
self.width = _width
self.field = []
self.score = 0
self.state = "start"
for i in range(_height):
new_line = []
for j in range(_width):
new_line.append(0)
self.field.append(new_line)
self.new_next()
self.new_figure()
def new_figure(self):
self.Figure = Figure(6,0)
self.Figure.type = self.Next.type
self.Figure.color = colors[self.Figure.type+1]
self.new_next()
def new_next(self):
self.Next = Figure(30,0)
#self.Next.type = random.randint(0, len(self.Next.Figures)-1)
def go_down(self):
self.Figure.y += 1
if self.intersects():
self.Figure.y -= 1
self.freeze()
def side(self, dx):
old_x = self.Figure.x
edge = False
for i in range(4):
for j in range(4):
p = i*4 + j
if p in self.Figure.image():
if j + self.Figure.x + dx > self.width -1 or \
j + self.Figure.x + dx < 0:
edge = True
if not edge:
self.Figure.x += dx
if self.intersects():
self.Figure.x = old_x
def left(self):
self.side(-1)
def right(self):
self.side(1)
def down(self):
while not self.intersects():
self.Figure.y += 1
self.Figure.y -= 1
self.freeze()
def rotate(self):
old_rotation = self.Figure.rotation
self.Figure.rotate()
if self.intersects():
self.Figure.rotation = old_rotation
def intersects(self):
intersection = False
try:
for i in range(4):
for j in range(4):
p = i*4 + j
if p in self.Figure.image():
if i+self.Figure.y > self.height-1 or \
i+self.Figure.y < 0 or \
self.field[i+self.Figure.y][j+self.Figure.x] > 0:
intersection = True
except:
intersection = True
return intersection
def freeze(self):
for i in range(4):
for j in range(4):
p = i*4 + j
if p in self.Figure.image():
self.field[i+self.Figure.y][j+self.Figure.x] = self.Figure.type+1
self.break_lines()
self.new_figure()
if self.intersects():
self.state = "gameover"
def break_lines(self):
lines = 0
for i in range(1, self.height):
zeros = 0
for j in range(self.width):
if self.field[i][j] == 0:
zeros += 1
if zeros == 0:
lines += 1
for i2 in range(i, 1, -1):
for j in range(self.width):
self.field[i2][j] = self.field[i2-1][j]
self.score += lines ** 2
def paused():
pause = True
while pause:
text_pause = gameover_font.render("Paused", True, (255,215,0))
screen.blit(text_pause, [90,250])
pygame.display.flip()
for event in pygame.event.get():
if event.type == pygame.KEYDOWN:
if event.key == pygame.K_ESCAPE:
pause = False
pygame.init()
screen = pygame.display.set_mode((380,670))
pygame.display.set_caption("Tetris")
done = False
pause = False
fps = 5
clock = pygame.time.Clock()
zoom = 20
downTick = 0
downCooldown = 0
game = Tetris(30,15)
pressing_down = False
pressing_left = False
pressing_right = False
BLACK = (0,0,0)
WHITE = (255,255,255)
GRAY = (128,128,128)
while not done:
if game.state == "start":
downTick += clock.get_time()
if downTick > 500:
downTick = 0
if downTick == 0:
game.go_down()
for event in pygame.event.get():
if event.type == pygame.QUIT:
done = True
if event.type == pygame.KEYDOWN:
if event.key == pygame.K_UP:
game.rotate()
if event.key == pygame.K_DOWN:
game.down()
if event.key == pygame.K_LEFT:
game.left()
pressing_left = True
if event.key == pygame.K_RIGHT:
game.right()
pressing_right = True
if event.key == pygame.K_ESCAPE:
if game.state == "gameover":
done = True
else:
paused()
if event.type == pygame.KEYUP:
if event.key == pygame.K_DOWN:
pressing_down = False
if event.key == pygame.K_LEFT:
pressing_left = False
if event.key == pygame.K_RIGHT:
pressing_right = False
if pressing_left:
game.left()
if pressing_right:
game.right()
screen.fill(color=BLACK)
for i in range(game.height):
for j in range(game.width):
if game.field[i][j] == 0:
color = GRAY
border = 1
else:
color = colors[game.field[i][j]]
border = 0
pygame.draw.rect(screen, color, [30+j*zoom, 30+i*zoom, zoom, zoom], border)
pygame.draw.rect(screen, GRAY, [30+j*zoom, 30+i*zoom, zoom, zoom], 1)
if game.Figure is not None:
for i in range(4):
for j in range(4):
p = i*4 + j
#pygame.draw.rect(screen, WHITE, [30+(j+game.Figure.x)*zoom, 30+(i+game.Figure.y)*zoom, zoom, zoom], 1)
if p in game.Figure.image():
pygame.draw.rect(screen, game.Figure.color, [30+(j+game.Figure.x)*zoom, 30+(i+game.Figure.y)*zoom, zoom, zoom])
#pygame.draw.rect(screen, GRAY, [30+(j+game.Figure.x)*zoom, 30+(i+game.Figure.y)*zoom, zoom, zoom], 1)
if game.Next is not None:
for i in range(4):
for j in range(4):
p = i*4 + j
if p in game.Next.image():
pygame.draw.rect(screen, game.Next.color, [30+(j+game.Next.x)*zoom/2, 30+(i+game.Next.y)*zoom/2, zoom/2, zoom/2])
gameover_font = pygame.font.SysFont("Clibri", 65, True, False)
text_gameover = gameover_font.render("Game Over!", True, (255,215,0))
text_pressEsc = gameover_font.render("Press Esc", True, (255,215,0))
if game.state == "gameover":
screen.blit(text_gameover, [30,250])
screen.blit(text_pressEsc, [60,315])
score_font = pygame.font.SysFont("Clibri", 25, True, False)
text_score = score_font.render("Score: " + str(game.score), True, WHITE)
screen.blit(text_score, [0,0])
pygame.display.flip()
clock.tick(fps)
pygame.quit()
서론
Pygame 공부하기 세 번째 Tetris 만들기
참고한 영상에서의 Tetris 구현방식을 요약하면 다음과 같다.
- 블록의 데이터를 4x4 행렬과 같이 표시
- 블록을 그리는 것은 정확한 좌표가 아닌 Grid를 채우는 방식
- Grid의 칸에 색 데이터를 저장 ex) 0=빈칸, 1=하늘색...
- 충돌판정은 Grid의 색 데이터가 0이 아니면 블록이 있는 것으로 판정
- 줄을 없앨 때는 위쪽 줄의 데이터를 아래쪽으로 내림
코드 설명
Array: colors
colors = [
(0,0,0),
(0,255,255), #I - Cyan
(0,0,255), #Reverse L - Blue
(255,127,0), #L - Orange
(255,255,0), #Block - Yellow
(0,255,0), #S - Green
(128,0,128), #T - Purple
(255,0,0), #Reverse S - Red
]
Figure에 들어갈 색깔을 저장한다.
Class: Figure
class Figure:
x = 0
y = 0
Figures = [
[[1,5,9,13], [4,5,6,7]], #I
[[0,1,4,8], [0,4,5,6], [1,5,9,8], [4,5,6,10]], #Reverse L
[[0,1,5,9], [4,5,6,8], [0,4,8,9], [2,4,5,6]], #L
[[1,2,5,6]], #Block
[[6,7,9,10], [1,5,6,10]], #S
[[1,4,5,6], [1,4,5,9], [4,5,6,9], [1,5,6,9]], #T
[[4,5,9,10], [2,6,5,9]], #Reverse S
]
def __init__(self, x_coord, y_coord):
self.x = x_coord
self.y = y_coord
self.type = random.randint(0, len(self.Figures)-1)
self.color = colors[self.type+1]
self.rotation = 0
def image(self):
return self.Figures[self.type][self.rotation]
def rotate(self):
self.rotation = (self.rotation + 1) % len(self.Figures[self.type])
블록의 모양에 대한 Class
Array: Figures
각 블록의 회전된 모양에 대한 데이터를 갖고 있는 배열
아래와 같은 4x4 행렬의 모습으로 생각할 수 있음
Figures[type][rotation]과 같은 모양
0 | 1 | 2 | 3 |
4 | 5 | 6 | 7 |
8 | 9 | 10 | 11 |
12 | 13 | 14 | 15 |
Func: __init__
Class의 init 함수
초기 x, y 정보를 받고 (x,y는 Grid에서의 좌표)
type을 임의로 설정
color를 colors의 type+1번째로 설정 (colors의 첫번째 element는 (0,0,0); 빈칸)
rotation을 기본(0)으로 설정
Func: image
Figure의 모양에 대한 배열을 반환
Func: rotate
rotation을 바꿈
나머지 연산을 이용해서 각 type에 따라 rotation을 변경할 수 있도록 함
Class: Tetris
class Tetris:
height = 0
width = 0
field = []
score = 0
state = "start"
Figure = None
Next = None
def __init__(self, _height, _width):
self.height = _height
self.width = _width
self.field = []
self.score = 0
self.state = "start"
for i in range(_height):
new_line = []
for j in range(_width):
new_line.append(0)
self.field.append(new_line)
self.new_next()
self.new_figure()
def new_figure(self):
self.Figure = Figure(6,0)
self.Figure.type = self.Next.type
self.Figure.color = colors[self.Figure.type+1]
self.new_next()
def new_next(self):
self.Next = Figure(30,0)
#self.Next.type = random.randint(0, len(self.Next.Figures)-1)
def go_down(self):
self.Figure.y += 1
if self.intersects():
self.Figure.y -= 1
self.freeze()
def side(self, dx):
old_x = self.Figure.x
edge = False
for i in range(4):
for j in range(4):
p = i*4 + j
if p in self.Figure.image():
if j + self.Figure.x + dx > self.width -1 or \
j + self.Figure.x + dx < 0:
edge = True
if not edge:
self.Figure.x += dx
if self.intersects():
self.Figure.x = old_x
def left(self):
self.side(-1)
def right(self):
self.side(1)
def down(self):
while not self.intersects():
self.Figure.y += 1
self.Figure.y -= 1
self.freeze()
def rotate(self):
old_rotation = self.Figure.rotation
self.Figure.rotate()
if self.intersects():
self.Figure.rotation = old_rotation
def intersects(self):
intersection = False
try:
for i in range(4):
for j in range(4):
p = i*4 + j
if p in self.Figure.image():
if i+self.Figure.y > self.height-1 or \
i+self.Figure.y < 0 or \
self.field[i+self.Figure.y][j+self.Figure.x] > 0:
intersection = True
except:
intersection = True
return intersection
def freeze(self):
for i in range(4):
for j in range(4):
p = i*4 + j
if p in self.Figure.image():
self.field[i+self.Figure.y][j+self.Figure.x] = self.Figure.type+1
self.break_lines()
self.new_figure()
if self.intersects():
self.state = "gameover"
def break_lines(self):
lines = 0
for i in range(1, self.height):
zeros = 0
for j in range(self.width):
if self.field[i][j] == 0:
zeros += 1
if zeros == 0:
lines += 1
for i2 in range(i, 1, -1):
for j in range(self.width):
self.field[i2][j] = self.field[i2-1][j]
self.score += lines ** 2
사실상 본체
Variables
height: 테트리스 칸의 행 갯수
width: 테트리스 칸의 열 갯수
field: 테트리스 칸 정보를 저장하는 배열 (Grid)
score: 점수
state: 현재 게임 상태 (start, gameover)
Figure: 현재 조작하는 블록
Next: 다음에 나올 블록
Func: __init__
각 변수를 초기화하고
반복문으로 field를 모두 0으로 만든다. (모두 빈칸)
현재 블록과 다음 블록을 생성한다.
Func: new_figure
새로운 블록을 중간쯤에 생성하는 함수; 영상과는 조금 다르다
type은 처음에 생성했던 다음 블록의 type을 복사하고 이에 따른 color를 지정한다.
이후 다시 한번 다음 블록을 생성(new_next)
Func: new_next
새로운 다음 블록을 생성하는 함수; 영상에는 없는 새로운 함수
Func: go_down
프레임마다 실행되는 블록을 한칸 내리는 함수
intersects가 True이면 블록이 겹쳤다는 것이니 한 칸 올리고 블록을 고정한다(freeze).
Func: side
현재 블록을 좌우로 옮기는 함수
옮길 수 없을 때를 위해 old_x를 저장
현재 블록의 모든 사각형에 대하여 움직였을 때의 x좌표가 field를 벗어난다면 edge = True로 움직이지 않게 한다.
또한 intersects로 다른 블록과의 충돌 판정으로 움직일지 말지 결정한다.
Func: left, right
Input이 감지되었을 때 실행되는 함수
Func: down
Hard Drop의 일종을 구현하는 함수
intersects(충돌판정)가 True가 되기 전까지 블록을 내린다.
intersects가 True이면 블록이 겹쳤다는 것이니 한 칸 올리고 블록을 고정한다(freeze).
Func: rotate
블록을 회전시키는 함수
side와 마찬가지로 회전할 수 없을 때를 위해 old_rotation을 저장
Class Figure의 rotate 함수를 실행하고 intersects=True라면 원래 회전상태로 돌아온다.
Func: intersects
블록 간 충돌 판정을 반환하는 함수
현재 블록의 모든 사각형에 대하여 y좌표가 height-1보다 크거나 (바닥)
y좌표가 0보다 작거나 (천장)
해당 field의 색 0이 아니면 (블록이 있음)
intersection=True로 한다.
Func: freeze
블록을 고정하고 각 데이터를 맞는 field의 x,y에 저장하는 함수
이때 줄을 없앨지 확인하고
새로운 블록을 생성한다.
만약 intersects=True이면 (블록을 놓을 곳이 없어서 생성된 블록이 충돌하는 경우)
gamestate=gameover로 만든다.
Func: break_lines
줄을 없애고 점수를 추가하는 함수
한 줄의 모든 field의 값이 0이 아니면 field 전체를 한칸 내린다. (field[i][j] = field[i-1][j])
점수는 없앤 줄의 제곱만큼 추가하도록 했다. (변경가능)
Func: pause
def paused():
pause = True
while pause:
text_pause = gameover_font.render("Paused", True, (255,215,0))
screen.blit(text_pause, [90,250])
pygame.display.flip()
for event in pygame.event.get():
if event.type == pygame.KEYDOWN:
if event.key == pygame.K_ESCAPE:
pause = False
영상에는 없는 새로운 함수; 일시정지하는 함수
Esc를 누르면 pause=True로 하고 Paused Text를 띄운다.
다시 Esc를 누르면 pause=False로 한다.
Program Initialize
pygame.init() - pygame 시작
screen = pygame.display.set_mode((380,670)) - screen의 크기를 지정
pygame.display.set_caption - 창의 제목
done - Program Loop 변수
pause - 일시정지 변수
fps - 초당 프레임
clock = pygame.time.Clock() - 시간 관련
zoom - 테트리스 칸의 크기
downTick - 이만큼의 ms가 지나면 블록이 한칸 내려감
game = Tetris(30,15) - 인스턴스 생성
키를 계속 누르고 있음을 표현
pressing_down
pressing_left
pressing_right
BLACK, GRAY, WHITE - 미리 지정해둔 color 배열
Program Loop
done=True일 때까지 반복한다.
블록 한칸 내리기
game의 state가 start일 때 downTick에 time을 더하고 500이 넘으면 초기화한 후 블록을 한칸 내림
if game.state == "start":
downTick += clock.get_time()
if downTick > 500:
downTick = 0
if downTick == 0:
game.go_down()
Keyboard Input
for event in pygame.event.get():
if event.type == pygame.QUIT:
done = True
if event.type == pygame.KEYDOWN:
if event.key == pygame.K_UP:
game.rotate()
if event.key == pygame.K_DOWN:
game.down()
if event.key == pygame.K_LEFT:
game.left()
pressing_left = True
if event.key == pygame.K_RIGHT:
game.right()
pressing_right = True
if event.key == pygame.K_ESCAPE:
if game.state == "gameover":
done = True
else:
paused()
if event.type == pygame.KEYUP:
if event.key == pygame.K_DOWN:
pressing_down = False
if event.key == pygame.K_LEFT:
pressing_left = False
if event.key == pygame.K_RIGHT:
pressing_right = False
if pressing_left:
game.left()
if pressing_right:
game.right()
KEYDOWN이면 pressing=True로 하여 계속 누르고 있을 때 행동을 지속
KEYUP 이면 pressing=False로 하여 행동을 멈춤
Grid 그리기
field에 저장되어 있는 데이터에 따라 rect를 그려 표현
screen.fill(color=BLACK)
for i in range(game.height):
for j in range(game.width):
if game.field[i][j] == 0:
color = GRAY
border = 1
else:
color = colors[game.field[i][j]]
border = 0
pygame.draw.rect(screen, color, [30+j*zoom, 30+i*zoom, zoom, zoom], border)
pygame.draw.rect(screen, GRAY, [30+j*zoom, 30+i*zoom, zoom, zoom], 1)
현재 블록 그리기
if game.Figure is not None:
for i in range(4):
for j in range(4):
p = i*4 + j
#pygame.draw.rect(screen, WHITE, [30+(j+game.Figure.x)*zoom, 30+(i+game.Figure.y)*zoom, zoom, zoom], 1)
if p in game.Figure.image():
pygame.draw.rect(screen, game.Figure.color, [30+(j+game.Figure.x)*zoom, 30+(i+game.Figure.y)*zoom, zoom, zoom])
#pygame.draw.rect(screen, GRAY, [30+(j+game.Figure.x)*zoom, 30+(i+game.Figure.y)*zoom, zoom, zoom], 1)
다음 블록 그리기
if game.Next is not None:
for i in range(4):
for j in range(4):
p = i*4 + j
if p in game.Next.image():
pygame.draw.rect(screen, game.Next.color, [30+(j+game.Next.x)*zoom/2, 30+(i+game.Next.y)*zoom/2, zoom/2, zoom/2])
Gameover Text
gameover_font = pygame.font.SysFont("Clibri", 65, True, False)
text_gameover = gameover_font.render("Game Over!", True, (255,215,0))
text_pressEsc = gameover_font.render("Press Esc", True, (255,215,0))
if game.state == "gameover":
screen.blit(text_gameover, [30,250])
screen.blit(text_pressEsc, [60,315])
Score Text
score_font = pygame.font.SysFont("Clibri", 25, True, False)
text_score = score_font.render("Score: " + str(game.score), True, WHITE)
screen.blit(text_score, [0,0])
Screen update by fps
pygame.display.flip()
clock.tick(fps)
'작업일지 > Pygame' 카테고리의 다른 글
Pygame #2 - Space Invader (0) | 2022.05.05 |
---|---|
Pygame #1 - Installation (0) | 2022.05.02 |