이전 시간에는 mino를 경계선 안에서 자유자재로 움직일 수 있도록 했습니다. 하지만 다음 블럭이 나오는 일은 없었죠.
이번 글에서는, 블럭이 놓이는 조건을 작성하고, 블럭이 놓인 후 다음 블럭이 나올 수 있도록 해보겠습니다.
블럭이 놓이는 조건이 뭘까요? 문서를 찾아보니 Lock Delay라는 문서가 있습니다.
미노가 놓인다고 바로 멈추는 것이 아니라, 적절한 시간이 지나는 동안에도 움직임이 없으면 (보통은 0.5초) 멈춥니다.
이 동작을 만들기 위해서는
- 타이머를 동작시킬 조건 (더 이상 밑으로 내려가지 못함)
- 타이머가 필요 (0.5초를 재야 함)
- 타이머가 0.5초동안 움직인 후 다음 블럭 생성
위 3동작이 있어야 할 것입니다. 바로 작성해 봅시다.
1. 더 이상 밑으로 내려가지 못하는 조건 생성
먼저, 현재의 블럭이 더 이상 밑으로 내려가지 못하는지 상태인지 확인해 봅시다.
def execute(self):
self._init()
while self._running:
self._event()
self._system()
self._update()
self._quit()
반복문에 system 함수를 추가하도록 합시다. system 에서 _enable_down()이란 함수를 추가하여 현재 미노가 밑으로 못 내려가는지 check하고 결과를 print 합니다.
밑으로 못내려 가는지에 대한 기준을 우선 y축 position을 기준으로 잡았습니다.
def _system(self):
if not self._enable_down():
print("not enable")
def _enable_down(self):
is_enable = True
for block in self._current_mino.blocks:
if block[1] + 1 >= self._field_h:
is_enable = False
break;
return is_enable
이렇게 코딩하면 미노가 바닥에 도달해 내려갈 수 없을 때 "not enable"이라는 print가 동작할 것입니다.
print를 통해 self._enable_down()이라는 함수가 잘 동작하는 것을 확인했습니다.
이제는 self._enable_down()함수가 True가 되면, 타이머가 동작하게 하면 됩니다.
2. Timer를 사용해 보자.
pygame 공식문서에서 timer로 검색해봅시다. pygame.time이라는 method가 눈에 띕니다.
이 중에서 눈에 띄는 함수가 있으니, 바로 pygame.time.set_timer입니다. 이 함수를 이용해서 5초 후에 trigger를 일으키는 동작을 짜달라고 ChatGPT에게 물어보았습니다.
ChatGPT는 좋은 예제 코드를 작성해 주었습니다.
import pygame
pygame.init()
# Set up the window
win = pygame.display.set_mode((500, 500))
pygame.display.set_caption("Timer Example")
# Set up the flags
flag1 = True
flag2 = False
# Define an event to trigger every 5 seconds
TIMER_EVENT = pygame.USEREVENT + 1
pygame.time.set_timer(TIMER_EVENT, 5000) # 5000 milliseconds = 5 seconds
# Main game loop
running = True
while running:
for event in pygame.event.get():
if event.type == pygame.QUIT:
running = False
# Check if the timer event has triggered
if event.type == TIMER_EVENT:
flag1 = False
flag2 = True
# Draw the flags
if flag1:
pygame.draw.rect(win, (255, 0, 0), (50, 50, 50, 50))
elif flag2:
pygame.draw.rect(win, (0, 255, 0), (50, 50, 50, 50))
pygame.display.update()
# Clean up
pygame.quit()
* 출처 : ChatGPT
ChatGPT가 작성한 타이머 예제를 보면 다음과 같이 동작합니다.
- pygame.USEREVENT를 통해서 고유한 TIMER_EVENT를 만들고
- pygame.timer.set_timer로 타이머를 발동합니다.
- 시간이 지나면 pygame.event.get()에서 event.type == TIMER_EVENT인 event를 받을 수 있습니다.
이런 동작을 활용하여, 필자도 타이머를 활용한 코드를 작성해 보았습니다.
먼저 DROP_EVENT를 작성합니다.
class Tetris:
DROP_EVENT = pg.USEREVENT + 1 ## 작성된 부분
def __init__(self, width: int, height: int,
field_w:int=10, field_h:int=20, tile_size:int=10):
self._size = self.width, self.height = width, height
self._field_w, self._field_h, self._t_size = field_w, field_h, tile_size
self._display_surf = None
self._running = False
self._mino_list = [
Mino("I", [[-2, 0], [-1, 0], [ 0, 0], [ 1, 0]], (54, 167, 141)),
Mino("O", [[-1,-1], [ 0,-1], [-1, 0], [ 0, 0]], (166, 166, 81)),
Mino("S", [[-1, 0], [ 0, 0], [ 0,-1], [ 1,-1]], (177, 86, 90)),
Mino("Z", [[-1,-1], [ 0,-1], [ 0, 0], [ 1, 0]], (143, 173, 60)),
Mino("L", [[-1, 0], [ 0, 0], [ 1, 0], [ 1,-1]], (73, 94, 158)),
Mino("J", [[-1,-1], [-1, 0], [ 0, 0], [ 1, 0]], (202, 121, 65)),
Mino("T", [[-1, 0], [ 0, 0], [ 0,-1], [ 1, 0]], (183, 92, 165))
]
self._current_mino = None
그리고 event에 DROP_EVENT를 감지하는 코드를 작성합니다.
def _event(self):
for event in pg.event.get():
if event.type == pg.QUIT:
self._running = False
elif event.type == pg.KEYDOWN:
if event.key == pg.K_UP:
self._move( 0,-1)
elif event.key == pg.K_DOWN:
self._move( 0, 1)
elif event.key == pg.K_RIGHT:
self._move( 1, 0)
elif event.key == pg.K_LEFT:
self._move(-1, 0)
elif event.type == self.DROP_EVENT: ## 작성된 부분
print("dropped") ## 작성된 부분
그리고 system에 set_timer를 set, reset 하는 함수를 작성합니다. reset은 millisecond를 0으로 두면 reset 된다고 pygame에서 설명하고 있습니다.
def _system(self):
if self._enable_down():
pg.time.set_timer(self.DROP_EVENT, 0)
else:
pg.time.set_timer(self.DROP_EVENT, 500)
이러면 동작을 할까요 안할까요?
이번에도 실패!
무엇이 문제일까요? pg.time.set_timer(self.DROP_EVENT, 500)이 문제가 있나 해서 debug stop을 걸어보았습니다.
이상합니다. set 함수까지는 잘 동작하는데 말이죠. 그리고 debugging을 풀자마자 터미널에서는 dropped가 엄청 적혔습니다.
이유는 바로 set_timer를 동작시키고, 다음 루프에서 또 set_timer를 동작시켰기 때문입니다. 그리고 몰려있던 set_timer가 debugger를 통해 코드 동작이 멈춰있는 사이, event queue에는 self.DROP_EVENT가 잔뜩 쌓여있게 되고, debugger를 풀면 print("dropped")가 동작하게 되는 것입니다.
이런 동작을 막기 위해서는 set_timer가 동작했을 때, 다시 동작하지 않도록 막아줘야 합니다.
Flag를 통해 set_timer 중복 호출을 방지
먼저 중복을 방지할 flag역할을 할 drop_delay_status를 작성합니다.
class Tetris:
DROP_EVENT = pg.USEREVENT + 1
def __init__(self, width: int, height: int,
field_w:int=10, field_h:int=20, tile_size:int=10):
self._size = self.width, self.height = width, height
self._field_w, self._field_h, self._t_size = field_w, field_h, tile_size
self._display_surf = None
self._running = False
self._mino_list = [
Mino("I", [[-2, 0], [-1, 0], [ 0, 0], [ 1, 0]], (54, 167, 141)),
Mino("O", [[-1,-1], [ 0,-1], [-1, 0], [ 0, 0]], (166, 166, 81)),
Mino("S", [[-1, 0], [ 0, 0], [ 0,-1], [ 1,-1]], (177, 86, 90)),
Mino("Z", [[-1,-1], [ 0,-1], [ 0, 0], [ 1, 0]], (143, 173, 60)),
Mino("L", [[-1, 0], [ 0, 0], [ 1, 0], [ 1,-1]], (73, 94, 158)),
Mino("J", [[-1,-1], [-1, 0], [ 0, 0], [ 1, 0]], (202, 121, 65)),
Mino("T", [[-1, 0], [ 0, 0], [ 0,-1], [ 1, 0]], (183, 92, 165))
]
self._current_mino = None
self._drop_delay_status = False ## 작성된 부분
그리고 _system에서 flag를 적극 활용합니다.
def _system(self):
if self._enable_down():
self._drop_delay_status = False
pg.time.set_timer(self.DROP_EVENT, 0)
elif not self._drop_delay_status:
self._drop_delay_status = True
pg.time.set_timer(self.DROP_EVENT, 500)
이렇게 하면 제대로 동작할까요?
드디어 정상적인 timer가 동작하는 것을 확인할 수 있었습니다. 이제는 drop이 되면 새로운 mino를 소환할 차례입니다.
Drop 후 다음 블럭 생성 확인
drop event가 일어나면 print("dropped") 대신 실제로 새로운 미노를 나타내도록 함수를 작성합니다. 이전에 사용되었던 _set_mino_init_position을 작성해서 새로운 미노가 등장하도록 합니다.
def _event(self):
for event in pg.event.get():
if event.type == pg.QUIT:
self._running = False
elif event.type == pg.KEYDOWN:
if event.key == pg.K_UP:
self._move( 0,-1)
elif event.key == pg.K_DOWN:
self._move( 0, 1)
elif event.key == pg.K_RIGHT:
self._move( 1, 0)
elif event.key == pg.K_LEFT:
self._move(-1, 0)
elif event.type == self.DROP_EVENT:
self._current_mino = self._set_mino_init_position(1) ## 작성된 부분
그러면 미노가 drop 되었을 때, 다른 미노가 등장하게 됩니다. 하지만 원래 있던 기다란 미노가 사라졌군요.
Drop된 미노가 사라졌습니다. 왜냐하면 미노가 drop 된 후 field 남아있는 기능을 작성한 적이 없기 때문입니다.
사실, 지금 코드에는 field가 없습니다. field의 길이만큼 rect만 반복해서 그릴뿐이죠.
def _draw_field_rect_list(self):
""" field 크기만 정의되었을 뿐, field 내 블럭 단위로 정의된 부분이 없다 """
for i in range(self._field_w):
for j in range(self._field_h):
_new_rect = pg.Rect(i*self._t_size, j*self._t_size, self._t_size, self._t_size)
pg.draw.rect(self._display_surf, (255, 255, 255), _new_rect, 1)
그래서 다음 글에서는 field를 제대로 제작해 보도록 하겠습니다. 이왕이면 tetris_wiki에 있던 field의 크기인 10 * 40으로 제작하고, 실제 보이는 것은 10 * 20으로 해보는 것도 좋겠죠??
지금까지의 코드는 아래와 같습니다. 점점 Magic number들이 넘처나죠? 슬슬 정리할 때가 되었습니다.
import pygame as pg
from typing import List
class Mino:
def __init__(self, name:str, blocks:List[List], color:tuple):
self.name = name
self.blocks = blocks
self.color = color
class Tetris:
DROP_EVENT = pg.USEREVENT + 1
def __init__(self, width: int, height: int,
field_w:int=10, field_h:int=20, tile_size:int=10):
self._size = self.width, self.height = width, height
self._field_w, self._field_h, self._t_size = field_w, field_h, tile_size
self._display_surf = None
self._running = False
self._mino_list = [
Mino("I", [[-2, 0], [-1, 0], [ 0, 0], [ 1, 0]], (54, 167, 141)),
Mino("O", [[-1,-1], [ 0,-1], [-1, 0], [ 0, 0]], (166, 166, 81)),
Mino("S", [[-1, 0], [ 0, 0], [ 0,-1], [ 1,-1]], (177, 86, 90)),
Mino("Z", [[-1,-1], [ 0,-1], [ 0, 0], [ 1, 0]], (143, 173, 60)),
Mino("L", [[-1, 0], [ 0, 0], [ 1, 0], [ 1,-1]], (73, 94, 158)),
Mino("J", [[-1,-1], [-1, 0], [ 0, 0], [ 1, 0]], (202, 121, 65)),
Mino("T", [[-1, 0], [ 0, 0], [ 0,-1], [ 1, 0]], (183, 92, 165))
]
self._current_mino = None
self._drop_delay_status = False
def _init(self):
pg.init()
self._display_surf = pg.display.set_mode(self._size)
self._draw_field_rect_list()
self._running = True
self._current_mino = self._set_mino_init_position(0)
def _set_mino_init_position(self, index):
curr_mino = self._mino_list[index]
for b in curr_mino.blocks:
b[0] += self._field_w//2
return curr_mino
def _draw_field_rect_list(self):
for i in range(self._field_w):
for j in range(self._field_h):
_new_rect = pg.Rect(i*self._t_size, j*self._t_size, self._t_size, self._t_size)
pg.draw.rect(self._display_surf, (255, 255, 255), _new_rect, 1)
def _event(self):
for event in pg.event.get():
if event.type == pg.QUIT:
self._running = False
elif event.type == pg.KEYDOWN:
if event.key == pg.K_UP:
self._move( 0,-1)
elif event.key == pg.K_DOWN:
self._move( 0, 1)
elif event.key == pg.K_RIGHT:
self._move( 1, 0)
elif event.key == pg.K_LEFT:
self._move(-1, 0)
elif event.type == self.DROP_EVENT:
self._current_mino = self._set_mino_init_position(1)
def _system(self):
if self._enable_down():
self._drop_delay_status = False
pg.time.set_timer(self.DROP_EVENT, 0)
elif not self._drop_delay_status:
self._drop_delay_status = True
pg.time.set_timer(self.DROP_EVENT, 500)
def _enable_down(self):
is_enable = True
for block in self._current_mino.blocks:
if block[1] + 1 >= self._field_h:
is_enable = False
break;
return is_enable
def _update(self):
self._display_surf.fill((0, 0, 0))
self._draw_field_rect_list()
for b in self._current_mino.blocks:
_rect = pg.Rect(b[0]*self._t_size, b[1]*self._t_size, self._t_size, self._t_size)
pg.draw.rect(self._display_surf, (self._current_mino.color), _rect)
pg.display.flip()
def _move(self, x, y):
move_enable = True
for block in self._current_mino.blocks:
if 0 > block[0] + x or block[0] + x >= self._field_w or block[1] + y >= self._field_h:
move_enable = False
break;
if not move_enable:
return False
for block in self._current_mino.blocks:
block[0] += x
block[1] += y
def _quit(self):
pg.quit()
def execute(self):
self._init()
while self._running:
self._event()
self._system()
self._update()
self._quit()
def main():
tetris = Tetris(640, 400)
tetris.execute()
if __name__ == "__main__" :
main()
* reference
https://tetris.fandom.com/wiki/Lock_delay
https://www.pygame.org/docs/ref/time.html#pygame.time.set_timer
'Development > Tetris' 카테고리의 다른 글
Python - 테트리스(Tetris) 만들기 (9) - Refactoring (0) | 2023.03.09 |
---|---|
Python - 테트리스(Tetris) 만들기 (8) - Field (0) | 2023.03.09 |
Python - 테트리스(Tetris) 만들기 (6) - Tetriminos (2) | 2023.03.07 |
Python - 테트리스(Tetris) 만들기 (5) - Event (0) | 2023.03.05 |
Python - 테트리스(Tetris) 만들기 (4) - Draw Rect (0) | 2023.03.05 |