【ゲーム制作】pygameのイベントキューでハマったお話

## 目次
- [はじめに](#はじめに)
- [何にそんなに悩んでいたのか](#何にそんなに悩んでいたのか)
- [解決策！（失敗）](#解決策失敗)
- [解決策！（成功）](#解決策成功)
- [最後に](#最後に)

## はじめに
　普段pygameでゲーム制作を行っている筆者ですが、学習を始めてからずっと悩んでいた問題を、本格的に解決しようと思ったので、その過程を書いていきます。同じような悩みで時間を浪費する人が、一人でも少なくなるように願っています。ちなみに筆者が使っているPCはSurface Pro9です。

## 何にそんなに悩んでいたのか
　そもそも何にそんなに悩んでいたのかですが、タイトルの通りイベントキューに関することです。この話をするにあたって、キー入力を検知したら、キーの名前を出力し2秒待機するコードを置いておきます。
```python:test.py
import pygame, sys
from pygame.locals import *
pygame.init()
screen = pygame.display.set_mode((512, 512))
clock = pygame.time.Clock()

def main():
    loop = True
    while loop:
        events = pygame.event.get()
        for event in events:
            if event.type == pygame.QUIT:
                pygame.quit()
                sys.exit()
            if event.type == pygame.KEYDOWN:
                print(pygame.key.name(event.key))
                pygame.time.wait(2000)
        clock.tick(30)
        pygame.display.update()
if __name__ == '__main__':
    main()
```
　筆者の場合はキーの名前の出力に当たる部分が、「画面にメッセージを表示する」だったのですが、そこはこの問題の本質ではないので置いておきます。じゃあ問題は何か、それは

 「待機中にイベントキューが蓄積してしまう」

です

これの何が嫌だったかというと、実際に上のコードを動かしてもらえばわかるのですが、連打したら、押した回数だけキーの名前を出して2秒待機してを繰り返すんですよね。筆者の場合は、メッセージウィンドウの表示中に十字キーを連打すると、メッセージ終了後に勝手に移動し始めるという、わけ分からない仕様が実装されてしまったわけです。これでは非常に困ります。

## 解決策！（失敗）
　解決しようとしてダメだった方法を紹介します。興味がない人は次の成功パターンまで飛ばしてください。
### case1
　待機中にイベントキューが蓄積するのが問題なのだから、待機後にイベントキューを空にすればいいんでしょ？
 ```python
 pygame.time.wait(2000)
 pygame.event.clear()
 ```
 →失敗

### case2
　じゃ、じゃあ待機中にイベントキューが蓄積しないようにすればいいんでしょ？
 ```python
 pygame.event.set_blocked(KEYDOWN)
 pygame.time.wait(2000)
 pygame.event.set_allowed(KEYDOWN)
 ```
 →失敗

 ### case3
 　こうなったら、そもそもイベントキューの先頭だけとってきて、残りはすべて消去するようにしてやる！
```diff_python
-events = pygame.event.get()
-for event in events:
+event = pygame.event.poll()
+pygame.event.clear()
+if event:
```
→失敗

　ここらへんで気づいたのですが、待機直後のイベントキューにはイベントがまだ溜まっていなくて、そのあとループを回してるときに、時間差でイベントキューが溜まってるっぽいんですよね。

 ```python:test.py
 import pygame, sys
from pygame.locals import *
pygame.init()
screen = pygame.display.set_mode((512, 512))
clock = pygame.time.Clock()

def main():
    loop = True
    while loop:
        print('次のループ開始')
        pygame.event.set_blocked(None)
        pygame.event.set_allowed(KEYDOWN)
        pygame.event.set_allowed(QUIT)
        events = pygame.event.get()
        for event in events:
            if event.type == pygame.QUIT:
                pygame.quit()
                sys.exit()
            if event.type == pygame.KEYDOWN:
                print(pygame.key.name(event.key))
                print('wait開始')
                pygame.time.wait(2000)
                print(pygame.event.get())
                print('wait終了')
        clock.tick(30)
        pygame.display.update()
if __name__ == '__main__':
    main()
 ```
 気になる人はこのコードをコピペして、待機中にキーを連打してみてください

 余談ですが、```pygame.time.wait```のほかにも処理を止める方法はいくつかありますよね。以下にダメだったものを挙げておきます。
 
 - ```pygame.event.delay```
 - ```time.sleep```

 ## 解決策！（成功）
 　いろいろ失敗を経て、イベントキューを何とかするよりも、イベントを受け取る側を何とかする方がよいと気づきました。

```python:test.py
import pygame, sys
from pygame.locals import *
pygame.init()
screen = pygame.display.set_mode((512, 512))
clock = pygame.time.Clock()

def main():
    loop = True
    flag = False
    start_time = None
    wait_time = 2000
    while loop:
        events = pygame.event.get()
        if start_time is not None and pygame.time.get_ticks() - start_time > wait_time:
            flag = False
            start_time = None
            print('wait終了')
        for event in events:
            if event.type == pygame.QUIT:
                pygame.quit()
                sys.exit()
            if event.type == pygame.KEYDOWN and not flag:
                print(pygame.key.name(event.key))
                print('wait開始')
                start_time = pygame.time.get_ticks()
                flag = True
        clock.tick(30)
        pygame.display.update()
if __name__ == '__main__':
    main()
```
　このように、前回イベントを処理してから、指定時間経つまでは無視するようにすれば、イベントを検知してしまっても、条件分岐ではじかれてくれるので、期待通りの動作が得られました。もしも```pygame.time.wait```のような感じで、処理を止めたい場所に1行挿入する形で使いたい！という人がいれば、以下のように自作の待機関数を作るのがいいでしょう。
```python
def my_wait(wait_time):
    loop = True
    start_time = pygame.time.get_ticks()
    while loop:
        if pygame.time.get_ticks() - start_time > wait_time:
            loop = False
        for event in pygame.event.get():
            if event.type == pygame.QUIT:
                pygame.quit()
                sys.exit()
```

## 最後に
　ここまで読んで下さりありがとうございました。筆者は、自作の待機関数をつかうのが快適に感じたので、そこに落ち着きました。待機中にイベントを検知したい時と、したくない時で、```pygame.time.wait```と使い分けようと思います。今回扱った現象の詳しい原因や、よりスマートな解決策をご存じの方はコメントで教えていただけるとありがたいです。