PygameとPygletを同時に動かす（＆初めてのPyglet備忘録）


　pygletでキー入力やら表示やら出来るようになった後、アクションゲーム的なものを作成。最終的にpygame(CarRacing_v2)と組み合わせるまで。

# 目次
- [動機](#いや何の意味があるねん)
- [初めてのpyglet](#初めてのpyglet)
    - [とりあえず文字を表示して、四角形を動かす](#とりあえずpygletで文字を表示し四角形を動かす)
    - [バッチを使う（Batch Rendering）](#バッチを使う)
        - [Appendのパフォーマンスについて](#appendについて)
    - [コーヒーブレイク１：pyglet.app.run()を使う](#コーヒーブレイク１pygletapprunを使う)
    - [コーヒーブレイク２：OpenGL方式の描画](#コーヒーブレイク２opengl方式の描画)
    - [アクションゲーム的なものを作ってみる](#アクションゲーム的なものを作ってみる)
- [pygameと組み合わせる](#pygameと組み合わせる)
    - [pygameのインストール](#とその前にpygameをインストールしなきゃ)
    - [pygameの基礎](#pygameの基本)
    - [pygletにpygameを合体](#がっちゃんこpyglet-with-pygame)
    - [OpenAIGymとpygletを同時にやってみる](#openaigymとやってみる)
        - [CarRacingのインストール](#carracingをまずはインストールする)
        - [CarRacingを動かす](#carracingを動かす)
        - [CarRacingにpygletを合体](#pygletを接続)
- [おまけ（エラーで残されたpygletのwindowをまとめて閉じるコード）](#おまけpygletのwindowを閉じるコード)

# **PygameとPygletを同時に動かす動機**

　pygameは一プロセス一つのウィンドウしか表示できないからまだしも、pygletは複数ウィンドウが可能なのに、一体何の意味があるのか、と思われるのも当然だろう。

　ごもっともだが、まあ、待ってほしい。

　pygameといっても、私の言うpygameとはOpenAIGymのことだ。
OpenAIGymで機械学習だとかをしながら、その内部の状態とか諸々をリアルタイムで表示したいという話である。

　当然のようにpygameで二窓できるかと思い込み、自作タスクのほうで表示用のプログラムを組んだというのに、出来ないと知ったときはショックだった。

　pygletは初めてなので、pygletの使い方の備忘録もかねて残す。（尚、OpenGLについてはC++、ベタ打ちでQIXを作ったことがある程度の経験。pygletを選択した理由はだいたいこれに帰する）

# 初めてのpyglet

　まずは```!pip install -U pyglet```でpygletをインストール。
　
 　!はpythonコード上でインストールするためのもの。ターミナルでインストールする場合は```pip install -U pyglet```で問題ない。

## とりあえずpygletで文字を表示し、四角形を動かす
　大前提としてpygameと一緒に動かしたい、という目的がある。
　つまり、**pyglet.app.run()** 君はお呼びではない。ループ処理はpygameと共有してもらわないと困る。
  
　一応app.run()方式にも触れはするが、基本は手動でループを組むものを使う。この方が自由度（こちらの制御範囲）も高いだろう。

　というわけで、pygletの[ドキュメント](https://pyglet.readthedocs.io/en/latest/programming_guide/quickstart.html)を見たりしながら、まずは馴れ親しんだ（？）ループ型の奴を組んでみる。

　もちろんキー入力やウィンドウを閉じる機能も完備。イベントじゃないほうでキー入力を取得するときは```window.push_handlers(keys)```を忘れずに（一敗）
　
　真ん中に"Hello, world"を表示し、下に左右の矢印キーで操作できる青い四角形を設置する。ゲームを作るのなら基礎の基礎となるだろう。

```python:MyFirstPyglet.py
import pyglet

config = pyglet.gl.Config(double_buffer=True)
#visible=Falseで作成してすぐにはウィンドウを表示しない
window = pyglet.window.Window(640, 480, visible=False, config=config, resizable=False)
window.set_caption('My first pyglet') #ウィンドウのタイトル

keys = pyglet.window.key.KeyStateHandler() #キー入力状態の判定用

label = pyglet.text.Label('Hello, world',
                          font_name='Times New Roman',
                          font_size=36,
                          x=window.width//2, y=window.height//2,
                          anchor_x='center', anchor_y='center') #文字

square = pyglet.shapes.Rectangle(x=window.width//2-50, y=50,
                                 width=100, height=100,
                                 color=(55, 55, 255)) #四角形 x,y,width,heightは明示的に書いているが省略も可能。pyglet.shapes.Rectangle(window.width//2-50, 50, 100, 100, color=(55, 55, 255))

dt = 0 #ループ毎のΔt
LoopFlag = True #メインループの終了判定用フラグ
action = 0 #キー入力による移動用

@window.event
def on_close(): #ウィンドウの×ボタンがクリックされたときのイベント
    global LoopFlag #外の変数にアクセスするために必要
    LoopFlag = False #ループフラグを折ってループから抜ける。

@window.event
def on_draw(): #windowのイベントとして描画処理を書く
    window.clear()
    label.draw() #文字の描写
    square.draw() #四角形の描写

@window.event
def on_key_press(symbol, modifiers): #押されたときのみイベントが発生する
    global LoopFlag
    if symbol == pyglet.window.key.ESCAPE:
        LoopFlag = False #ESCキーが押されたらループフラグを折ってループから抜ける。

window.set_visible() #ここでウィンドウを表示
while LoopFlag: #メインループ
    dt = pyglet.clock.tick() #前回tick()を呼び出してから経った時間（秒）を返す

    #きちんと更新されているのか、四角形を動かせるようにして確認する
    window.push_handlers(keys) #キー入力の状態を取得
    if keys[pyglet.window.key.LEFT]:
        square.x -= 150.0*dt #左に移動
    if keys[pyglet.window.key.RIGHT]:
        square.x += 150.0*dt #右に移動

    windowlist = list(pyglet.app.windows)
    for w in windowlist: #現在存在するwindow全てについて更新
        w.switch_to()
        w.dispatch_events()
        w.dispatch_event('on_draw')
        w.flip()

window.close() #ループを抜けたら終了
```

![ezgif-7-50f4318110.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3800150/9cb6dd9e-a153-6e9a-10c7-6f8a53001727.gif)

　とりあえずこれで基本は抑えられただろう。

　尚、いちいち```pyglet.```と頭につけるのが面倒な人は、```from pyglet import window, gl, shapes```と最初にインポートしておけば、```pyglet.```を省ける。他のライブラリとの干渉には注意しないといけないが。


## バッチを使う（図形の描画）

　先ほどは説明を省いていたが、簡単な図形に関しては[pyglet.shapes](https://pyglet.readthedocs.io/en/latest/modules/shapes.html)を使って描画する。

　パフォーマンスのためには、いちいちdraw()を呼ばずに、まとめて描画したほうがよいとのこと。そのためのBatchクラスが存在している。（[Batched Rendering](https://pyglet.readthedocs.io/en/latest/modules/graphics/index.html#pyglet.graphics.Batch)）

　使い方はかなり簡単。```batch = pyglet.graphics.Batch()```と頭で宣言して、図形(Shape)でも画像(Sprite)でも```batch=batch```と引数を取ればOKの模様。あとは```batch.draw()```で全部描画してくれる。

　ついでに、空のリストを作ってappendしていけば、pygameやopenGL的な書き方ができるのでその方法もやってみよう。

```diff_python:MyFirstPyglet2.py
config = pyglet.gl.Config(double_buffer=True)
#visible=Falseで作成してすぐにはウィンドウを表示しない
window = pyglet.window.Window(640, 480, visible=False, config=config, resizable=False)
window.set_caption('My first pyglet v2') #ウィンドウのタイトル

+ batch = pyglet.graphics.Batch() #バッチの定義
+ shapesList = [] #図形登録用

+ #appendで図形を登録
+ shapesList.append(pyglet.shapes.Circle(300, 50, 50, color=(50, 225, 30), batch=batch))
+ shapesList.append(pyglet.shapes.Triangle(100, 300, 320, 400, 540, 300, color=(220, 100, 100), batch=batch))
+ shapesList.append(pyglet.shapes.Line(20, 40, 620, 40, color=(255, 255, 255), thickness=20, batch=batch))

keys = pyglet.window.key.KeyStateHandler() #キー入力状態の判定用

label = pyglet.text.Label('Hello, world',
                          font_name='Times New Roman',
                          font_size=36,
                          x=window.width//2, y=window.height//2,
                          anchor_x='center', anchor_y='center', batch=batch) #文字

square = pyglet.shapes.Rectangle(x=window.width//2-50, y=50,
                                 width=100, height=100,
                                 color=(55, 55, 255), batch=batch) #四角形

dt = 0 #ループ毎のΔt
LoopFlag = True #メインループの終了判定用フラグ
action = 0 #キー入力による移動用

@window.event
def on_close(): #ウィンドウの×ボタンがクリックされたときのイベント
    global LoopFlag #外の変数にアクセスするために必要
    LoopFlag = False #ループフラグを折ってループから抜ける。

@window.event
def on_draw(): #windowのイベントとして描画処理を書く
    window.clear()
+    batch.draw() #バッチでまとめて描画
-    label.draw() #文字の描写
-    square.draw() #四角形の描写

@window.event
def on_key_press(symbol, modifiers): #押されたときのみイベントが発生する
    global LoopFlag
    if symbol == pyglet.window.key.ESCAPE:
        LoopFlag = False #ESCキーが押されたらループフラグを折ってループから抜ける。

window.set_visible() #ここでウィンドウを表示
while LoopFlag: #メインループ
    dt = pyglet.clock.tick() #前回tick()を呼び出してから経った時間（秒）を返す

    #きちんと更新されているのか、四角形を動かせるようにして確認する
    window.push_handlers(keys) #キー入力の状態を取得
    if keys[pyglet.window.key.LEFT]:
        square.x -= 150.0*dt #左に移動
    if keys[pyglet.window.key.RIGHT]:
        square.x += 150.0*dt #右に移動

    windowlist = list(pyglet.app.windows)
    for w in windowlist: #現在存在するwindow全てについて更新
        w.switch_to()
        w.dispatch_events()
        w.dispatch_event('on_draw')
        w.flip()

window.close() #ループを抜けたら終了
```

![screenshot240612-1615.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3800150/63ca7b78-5c37-8cc1-44a7-2ba0d4d2d7fe.jpeg)


~~小学生でももっとましなものが作れる。~~

### Appendについて
　余談だが、上のコードの```shapesList.append```について、大量に行う場合は```Appender = shapesList.append```のようにしてメソッドを変数に入れて使うとよい。（[参考](https://qiita.com/hi-asano/items/aa2976466739f280b887#%E5%95%8F%E9%A1%8C1-%E3%83%AA%E3%82%B9%E3%83%88%E7%94%9F%E6%88%90)）
　この差が意外と馬鹿にならない。3万個shapeを生成しようが、描画は1msec程度で済む。一方で、その数のappendを行うために200msecかかっていたりするため、描画速度について気にするよりも先に、描画前の演算部分の速度が問題となるだろう。

## コーヒーブレイク１：pyglet.app.run()を使う

　ループを自分で用意する代わりに、毎回呼び出す関数を用意して、```shedule_interval```で設定する。
　その後、```pyglet.app.run()```をすれば、画面の更新やら、イベントの処理とかをやってくれる。ついでに、上のサンプルから、改行を含めて文字を表示し、リアルタイムでFPSを表示させている。
　ただ、```pyglet.app.run()```をJupyterノートブックで使う場合、何度も繰り返し実行すると、ちゃんと```pyglet.app.exit()```していてもパフォーマンスが落ちる場合がある。マニュアル方式なら起きないのでそちらが無難。

```python:
import pyglet

config = pyglet.gl.Config(double_buffer=True)
#visible=Falseで作成してすぐにはウィンドウを表示しない
window = pyglet.window.Window(640, 480, visible=False, config=config, resizable=False)
window.set_caption('My first pyglet') #ウィンドウのタイトル
keys = pyglet.window.key.KeyStateHandler() #キー入力状態の判定用

batch = pyglet.graphics.Batch() #バッチの定義

shapesList = [] #図形登録用

#appendで図形を登録（update関数内で定義することも可能）
shapesList.append(pyglet.shapes.Circle(300, 50, 50, color=(50, 225, 30), batch=batch))
shapesList.append(pyglet.shapes.Triangle(100, 300, 320, 400, 540, 300, color=(220, 100, 100), batch=batch))
shapesList.append(pyglet.shapes.Line(20, 40, 620, 40, color=(255, 255, 255), thickness=20, batch=batch))

label = pyglet.text.Label('Hello, world',
                          font_name='Times New Roman',
                          font_size=36,
                          x=window.width//2-100, y=window.height//2,
                          anchor_x='left', anchor_y='center', batch=batch, multiline=True, width = 400) #文字（複数行）

square = pyglet.shapes.Rectangle(x=window.width//2-50, y=50,
                                 width=100, height=100,
                                 color=(55, 55, 255), batch=batch) #四角形

def PygletExit(): #終了用の関数
    pyglet.clock.unschedule(update) #updateのスケジュールを停止する。次の行でexitするので必要性はない。
    pyglet.app.exit() #runしたものは必ず終了させる。ノートブックにおいては特に、そのまま裏で動いたままになりパフォーマンスに影響する。
    window.close() #ウィンドウを閉じる

@window.event
def on_close(): #ウィンドウの×ボタンがクリックされたときのイベント
    PygletExit()

@window.event
def on_draw(): #windowのイベントとして描画処理を書く
    window.clear()
    batch.draw() #バッチでまとめて描画

@window.event
def on_key_press(symbol, modifiers): #押されたときのみイベントが発生する
    if symbol == pyglet.window.key.ESCAPE: #ESCAPEでコードがなくても閉じるが、app.exit()をするために必要。
        PygletExit()

def update(dt):
    #きちんと更新されているのか、四角形を動かせるようにして確認する
    window.push_handlers(keys) #キー入力の状態を取得
    if keys[pyglet.window.key.LEFT]:
        square.x -= 150.0*dt #左に移動
    if keys[pyglet.window.key.RIGHT]:
        square.x += 150.0*dt #右に移動

    label.text = f"Hello World \nFPS={1./dt:.1f}" #HelloWorldと改行してFPSを表示する。

window.set_visible() #ここでウィンドウを表示

pyglet.clock.schedule_interval(update, 1/60.0) #1/60は60Hzでupdateを呼ぶ
pyglet.app.run() #pygletの実行

```

## コーヒーブレイク２：OpenGL方式の描画

　pygletはOpenGLを使っているため、OpenGLのインターフェイスを直接使うことができる。

```python:
pyglet.gl.glClearColor(0.5,0,0.5,1) #ウィンドウの背景 Red,Green,Blue,Alpha 0.0~1.0で指定
```

![screenshot.9.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3800150/2e2d96cc-74fa-7798-c98e-72a9c91d45e5.jpeg)


　この通り、上のコードを```window.set_caption```とかの初期化の項に続けて加えておけば、ウィンドウの背景色（正確にはclearするときの塗りつぶし色）を設定できる。この```glClearColor```はOpenGLの関数である。
 
　同様に、描画についても、OpenGLにおける```GL_LINE_LOOP```とか```GL_TRIANGLES```とかそういうやつらを使って描画できるという。（[参考](https://docs.pyglet.org/en/pyglet-1.5-maintenance/programming_guide/graphics.html)）
　```GL_LINE_LOOP```ってなんだよ、って人は[OpenGLについての解説](https://tkengo.github.io/blog/2015/01/03/opengl-es-2-2d-knowledge-2/)を見るとよい。
 
　参考先の公式ドキュメント読むと......
 ```python:
 #pyglet.graphics.draw( [頂点数], [OpenGLの描画モード], (頂点達) )という形式
 pyglet.graphics.draw(2, pyglet.gl.GL_POINTS, ('v2i', (10, 15, 30, 35)) )
 #v2iはベクトル(vector)は2d（2Dで）上の点でintという意味
 pyglet.graphics.draw(2, pyglet.gl.GL_POINTS, ('v3f', (10.0, 15.0, 0.0, 30.0, 35.0, 0.0)) )
 #v3fはベクトル(vector)は3d（3Dで）上の点でfloatという意味
 pyglet.graphics.draw_indexed(4, pyglet.gl.GL_TRIANGLES,
    [0, 1, 2, 0, 2, 3], #下の頂点情報を再利用したり、好きな順番で使う方法
    ('v2i', (100, 100,
             150, 100,
             150, 150,
             100, 150))
)
```
　ただし、参考先のpygletのバージョンは1.5。pyglet(バージョン2.0.15)では動かなかった。

　どうやらこれらの方法は古い様子。```pyglet.shapes```を使った方がよいだろう。```pyglet.shapes```の方が簡単だし、まとめてbatchで描画したほうが速度も優れている。

## アクションゲーム的なものを作ってみる
　pygletのコードの書き方はわかったので、アクションゲーム的なものを試しに作ってみよう。プレイヤーの操作する自機と、登れる障害物をクラスで作る。これに衝突判定を実装して、描画する。
 
 　[スプライト](https://pyglet.readthedocs.io/en/latest/modules/sprite.html#)を使えば画像を動かせるし、[アニメーション](https://pyglet.readthedocs.io/en/latest/programming_guide/image.html#image-grids)もできるようだが、私は横着して図形のままでいく。

```python: Action game by pyglet.py
import pyglet
from dataclasses import dataclass
import copy
import time

config = pyglet.gl.Config(double_buffer=True)
#visible=Falseで作成してすぐにはウィンドウを表示しない
window = pyglet.window.Window(640, 480, visible=False, config=config, resizable=False)
window.set_caption('Action game by pyglet') #ウィンドウのタイトル

batch = pyglet.graphics.Batch() #バッチの定義

keys = pyglet.window.key.KeyStateHandler() #キー入力状態の判定用

def sign(a):
    return (a > 0) - (a < 0) #aが正なら1,負なら-1

@dataclass
class Object: #位置と大きさ用の構造体を設定
    x: float
    y: float
    width: float
    height: float

class wall(): #壁とかそういうやつ
    drawObj :pyglet.shapes.Rectangle
    shape :Object = Object(0, 0, 0, 0)
    collisionCountDown :float = 0.0 #衝突すると一定値が入り、衝突してない間は0まで時間とともに低下する
    
    def __init__(self,x,y,width,height):
        self.shape = Object(x, y, width, height) #きちんとインスタンス変数として設定すること
        self.drawObj = pyglet.shapes.Rectangle(self.shape.x-self.shape.width//2, self.shape.y-self.shape.height//2,
                                                self.shape.width, self.shape.height, color=(255, 255, 255), batch=batch)
    def collision(self, expectedState : Object):
        distanceConstrait = ((self.shape.width + expectedState.width) / 2, (self.shape.height + expectedState.height) / 2) #接近できる限界距離
        horizontalOverlap = abs(self.shape.x - expectedState.x) - distanceConstrait[0] #接近限界距離を下回っているか
        verticalOverlap = abs(self.shape.y - expectedState.y) - distanceConstrait[1]
        collisionDir :int = 2
        if verticalOverlap < 0 and horizontalOverlap < 0: #衝突
            if verticalOverlap < horizontalOverlap: #縦方向の重なりが大きいとき（負数なので不等号は逆）
                collisionDir = 0 #横方向の衝突
                expectedState.x = self.shape.x + distanceConstrait[0] * sign(expectedState.x - self.shape.x)
            else:
                collisionDir = 1 #縦方向の衝突
                expectedState.y = self.shape.y + distanceConstrait[1] * sign(expectedState.y - self.shape.y)
            self.collisionCountDown = 0.7 #衝突時にリセット
        return collisionDir, expectedState
    
    def update(self, dt):
        self.collisionCountDown = max(0.0, self.collisionCountDown - dt) #時間経過とともに減少

    def draw(self):
        self.drawObj.color = (255, 100, 100) if self.collisionCountDown > 0 else (255, 255, 255) #衝突カウントダウンがまだ残っている場合は赤くなる

class Player(): #自機
    drawObj :pyglet.shapes.Rectangle
    shape :Object = Object(0, 0, 0, 0)
    velocity :list[float] = [0.0, 0.0]
    Cv = [0.3, 0.1] #粘性抵抗値（速度比例）
    onObject :float = 0.0
    def __init__(self, x, y, width, height):
        self.shape = Object(x, y, width, height)
        self.drawObj = pyglet.shapes.Rectangle(self.shape.x-self.shape.width//2, self.shape.y-self.shape.height//2, 
                                                self.shape.width, self.shape.height, color=(150, 150, 255), batch=batch)
    def update(self, dt, walls : list[wall]):
        window.push_handlers(keys)
        if keys[pyglet.window.key.LEFT]:
            self.velocity[0] -= 4000.0 * dt #左に力 (m*dv/dt = F より dv = (F/m)*dt)
        if keys[pyglet.window.key.RIGHT]:
            self.velocity[0] += 4000.0 * dt #右に力
        if keys[pyglet.window.key.SPACE] and self.onObject > 0.0: #ジャンプ用のチャージがある限り加速
            self.velocity[1] += 12000.0 * dt
        self.velocity[1] -= 4000 * dt #重力
        self.velocity[0] = (1.0 - self.Cv[0]) * self.velocity[0]
        self.velocity[1] = (1.0 - self.Cv[1]) * self.velocity[1]

        expectedState = copy.deepcopy(self.shape) #いったんコピー
        expectedState.x += self.velocity[0] * dt #移動先で衝突判定は考える
        expectedState.y += self.velocity[1] * dt
        self.onObject = max(0.0, self.onObject - dt)
        for wall in walls:
            colDir, expectedState = wall.collision(expectedState) #壁ごとに衝突判定
            if colDir <= 1: #衝突していたら
                self.velocity[int(colDir)] = 0.0 #衝突方向の速度を0にする
                if colDir == 1 and wall.shape.y < self.shape.y:
                    self.onObject = 0.2 #乗っかっているとジャンプ用のチャージをフルに
        self.shape = expectedState #最終的な位置を代入
        self.shape.y = max(self.shape.width//2, self.shape.y) #一定高さより下には落ちない

    def draw(self):
        self.drawObj.x = self.shape.x - self.shape.width // 2
        self.drawObj.y = self.shape.y - self.shape.height // 2

dt = 0 #ループ毎のΔt
LoopFlag = True #メインループの終了判定用フラグ
action = 0 #キー入力による移動用

JIKI = Player(window.width//2,window.height//2,50,50) #自機の設定
wallList :list[wall] = [] #壁用の配列
wallList.append(wall(window.width//2,5,window.width,10)) #床の追加
wallList.append(wall(5,210,10,400)) #壁の追加
wallList.append(wall(window.width-5,210,10,400)) #壁の追加
wallList.append(wall(200,100,100,10)) #台の追加
wallList.append(wall(350,200,200,20))
wallList.append(wall(440,240,20,60))
wallList.append(wall(150,300,300,20))
wallList.append(wall(520,300,40,40))
wallList.append(wall(360,400,10,10))

@window.event
def on_close(): #ウィンドウの×ボタンがクリックされたときのイベント
    global LoopFlag #外の変数にアクセスするために必要
    LoopFlag = False

@window.event
def on_draw(): #windowのイベントとして描画処理を書く
    window.clear()
    batch.draw() #バッチでまとめて描画

@window.event
def on_key_press(symbol, modifiers): #押されたときのみイベントが発生する
    global LoopFlag
    if symbol == pyglet.window.key.ESCAPE:
        LoopFlag = False #ESCキーが押されたらループフラグを折ってループから抜ける。

window.set_visible() #ここでウィンドウを表示
while LoopFlag: #メインループ
    dt = pyglet.clock.tick() #前回tick()を呼び出してから経った時間（秒）を返す

    JIKI.update(dt, wallList) #状態の更新
    JIKI.draw() #描画図形の定義の更新
    for eachWall in wallList:
        eachWall.update(dt)
        eachWall.draw()

    windowlist = list(pyglet.app.windows)
    for w in windowlist: #現在存在するwindow全てについて更新
        w.switch_to()
        w.dispatch_events()
        w.dispatch_event('on_draw') #実際の描画
        w.flip()
    time.sleep(0.02)

window.close() #ループを抜けたら終了
```

![ezgif-7-6f26bff2b5.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3800150/4b41f0ee-d220-7e46-bf74-f2fe4fa04251.gif)

　ジャンプの強弱は、接地時点から0.2秒間だけスペースキーが効いて、上方向に力を加えられることによる。接地時に0.2を代入して、非接地時にΔtだけ減らしていく、これが0以上ならジャンプできると判定するだけの単純なコード。

　当たり判定は、移動する前に、未来の移動先で衝突判定を行い、衝突していたらそれに合わせて移動先を変えることで行っている。水平方向or縦方向どちらの衝突かの判定は、未来の位置と障害物が重なった部分の縦横の大小関係から。

　いずれにせよ、これでpygletはマスターしたな！（）

# Pygameと組み合わせる

　アクションゲームを作っていたらほぼ忘れかけていたが、本題はゲーム作りなんかではない。というわけで、先ほどのゲームと並行して、pygameでキー入力を表示できるようにしてみる。

## と、その前に。Pygameをインストールしなきゃ！

　```!pip install pygame```でインストール！　pyglet同様、コンソールでやるなら頭の```!```はいらない。

## Pygameの基本

　まず、左右の矢印キーの投下状態を色で示すpygameコードを作成。

　pygameのほうがpygletよりも簡単に書ける。ただ、今のところpygameよりもpygletのほうが多機能な印象だ。聞くところによるとpygletのほうが処理も速いらしい。pygameとpygletの比較は本題ではないので深堀はしないが。

```python:pygameExample.py
import pygame

pygame.init() #pygameの初期化
pygame.display.set_caption("pygame example") #ウィンドウタイトル
screen = pygame.display.set_mode((720, 600)) #ウィンドウの表示
white = (255,255,255) #白
whiteBlue = (200, 200, 255) #青白
gray = (100, 100, 100) #灰色

Flag = True
while Flag:
    screen.fill(white) #先に定義した色で塗りつぶす

    pygame.draw.rect(screen, (20, 20, 20),pygame.Rect(90, 90, 190, 70)) #黒い四角

    keys = pygame.key.get_pressed() #キー入力状態の取得
    pygame.draw.rect(screen, whiteBlue if keys[pygame.K_LEFT] else gray, #右矢印キーで色を変える
                      pygame.Rect(100, 100, 50, 50))
    
    pygame.draw.rect(screen, whiteBlue if keys[pygame.K_SPACE] else gray, #スペースキーで色を変える
                      pygame.Rect(160, 100, 50, 50))
    
    pygame.draw.rect(screen, whiteBlue if keys[pygame.K_RIGHT] else gray, #左矢印キーで色を変える
                      pygame.Rect(220, 100, 50, 50))
        
    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            Flag = False #×ボタンで閉じられたらループを抜ける
        if event.type == pygame.KEYDOWN:
            if event.key == pygame.K_ESCAPE:
                Flag = False #ESCキーでもループを抜ける

    pygame.display.flip() #描画したものを表示
    pygame.time.wait(25) #25msec待つ

pygame.quit() #pygameを終了
```

![ezgif-4-50266e2691.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3800150/be7084cc-215e-6558-8b10-7d84c1cb1f8c.gif)

　矢印キー（左右）とスペースキーの投下状態が表示される。

## がっちゃんこ（pyglet with pygame）

　いよいよpygameとpygletのフュージョン。

　作ったキー入力の投下状態を表示するpygameコードとpygletのアクションゲームをがっちゃんこ。まぜまぜしてみる。
　
　ループのベースはpygameのほうを使う。

```python:pygame_And_pyglet.py
import pyglet
from dataclasses import dataclass
import copy
import time
import pygame

#=============[pygame]========================
pygame.init() #pygameの初期化
pygame.display.set_caption("pygame & pyglet") #ウィンドウタイトル
screen = pygame.display.set_mode((720, 600)) #ウィンドウの表示
white = (255,255,255) #白
whiteBlue = (200, 200, 255) #青白
gray = (100, 100, 100) #灰色
#=============================================

config = pyglet.gl.Config(double_buffer=True)
#visible=Falseで作成してすぐにはウィンドウを表示しない
window = pyglet.window.Window(640, 480, visible=False, config=config, resizable=False)
window.set_caption('pyglet & pygame') #ウィンドウのタイトル

batch = pyglet.graphics.Batch() #バッチの定義

keys = pyglet.window.key.KeyStateHandler() #キー入力状態の判定用

def sign(a):
    return (a > 0) - (a < 0) #aが正なら1,負なら-1

@dataclass
class Object: #位置と大きさ用の構造体を設定
    x: float
    y: float
    width: float
    height: float

class wall(): #壁とかそういうやつ
    drawObj :pyglet.shapes.Rectangle
    shape :Object = Object(0, 0, 0, 0)
    collisionCountDown :float = 0.0 #衝突すると一定値が入り、衝突してない間は0まで時間とともに低下する
    
    def __init__(self,x,y,width,height):
        self.shape = Object(x, y, width, height) #きちんとインスタンス変数として設定すること
        self.drawObj = pyglet.shapes.Rectangle(self.shape.x-self.shape.width//2, self.shape.y-self.shape.height//2,
                                                self.shape.width, self.shape.height, color=(255, 255, 255), batch=batch)
    def collision(self, expectedState : Object):
        distanceConstrait = ((self.shape.width + expectedState.width) / 2, (self.shape.height + expectedState.height) / 2) #接近できる限界距離
        horizontalOverlap = abs(self.shape.x - expectedState.x) - distanceConstrait[0] #接近限界距離を下回っているか
        verticalOverlap = abs(self.shape.y - expectedState.y) - distanceConstrait[1]
        collisionDir :int = 2
        if verticalOverlap < 0 and horizontalOverlap < 0: #衝突
            if verticalOverlap < horizontalOverlap: #縦方向の重なりが大きいとき（負数なので不等号は逆）
                collisionDir = 0 #横方向の衝突
                expectedState.x = self.shape.x + distanceConstrait[0] * sign(expectedState.x - self.shape.x)
            else:
                collisionDir = 1 #縦方向の衝突
                expectedState.y = self.shape.y + distanceConstrait[1] * sign(expectedState.y - self.shape.y)
            self.collisionCountDown = 0.7 #衝突時にリセット
        return collisionDir, expectedState
    
    def update(self, dt):
        self.collisionCountDown = max(0.0, self.collisionCountDown - dt) #時間経過とともに減少

    def draw(self):
        self.drawObj.color = (255, 100, 100) if self.collisionCountDown > 0 else (255, 255, 255) #衝突カウントダウンがまだ残っている場合は赤くなる

class Player(): #自機
    drawObj :pyglet.shapes.Rectangle
    shape :Object = Object(0, 0, 0, 0)
    velocity :list[float] = [0.0, 0.0]
    Cv = [0.3, 0.1] #粘性抵抗値（速度比例）
    onObject :float = 0.0
    def __init__(self, x, y, width, height):
        self.shape = Object(x, y, width, height)
        self.drawObj = pyglet.shapes.Rectangle(self.shape.x-self.shape.width//2, self.shape.y-self.shape.height//2, 
                                                self.shape.width, self.shape.height, color=(150, 150, 255), batch=batch)
    def update(self, dt, walls : list[wall]):
        Pygamekeys = pygame.key.get_pressed() #[pygame] pygameのキー判定をorでいれて、どちらかがフォーカスを得ていれば入力を受け取れるようにする。
        window.push_handlers(keys)
        if keys[pyglet.window.key.LEFT] or Pygamekeys[pygame.K_LEFT]:
            self.velocity[0] -= 4000.0 * dt #左に力 (m*dv/dt = F より dv = (F/m)*dt)
        if keys[pyglet.window.key.RIGHT] or Pygamekeys[pygame.K_RIGHT]:
            self.velocity[0] += 4000.0 * dt #右に力
        if (keys[pyglet.window.key.SPACE] or Pygamekeys[pygame.K_SPACE]) and self.onObject > 0.0: #ジャンプ用のチャージがある限り加速
            self.velocity[1] += 12000.0 * dt
        self.velocity[1] -= 4000 * dt #重力
        self.velocity[0] = (1.0 - self.Cv[0]) * self.velocity[0]
        self.velocity[1] = (1.0 - self.Cv[1]) * self.velocity[1]

        expectedState = copy.deepcopy(self.shape) #いったんコピー
        expectedState.x += self.velocity[0] * dt #移動先で衝突判定は考える
        expectedState.y += self.velocity[1] * dt
        self.onObject = max(0.0, self.onObject - dt)
        for wall in walls:
            colDir, expectedState = wall.collision(expectedState) #壁ごとに衝突判定
            if colDir <= 1: #衝突していたら
                self.velocity[int(colDir)] = 0.0 #衝突方向の速度を0にする
                if colDir == 1 and wall.shape.y < self.shape.y:
                    self.onObject = 0.2 #乗っかっているとジャンプ用のチャージをフルに
        self.shape = expectedState #最終的な位置を代入
        self.shape.y = max(self.shape.width//2, self.shape.y) #一定高さより下には落ちない

    def draw(self):
        self.drawObj.x = self.shape.x - self.shape.width // 2
        self.drawObj.y = self.shape.y - self.shape.height // 2

dt = 0 #ループ毎のΔt
LoopFlag = True #メインループの終了判定用フラグ
action = 0 #キー入力による移動用

JIKI = Player(window.width//2,window.height//2,50,50) #自機の設定
wallList :list[wall] = [] #壁用の配列
wallList.append(wall(window.width//2,5,window.width,10)) #床の追加
wallList.append(wall(5,210,10,400)) #壁の追加
wallList.append(wall(window.width-5,210,10,400)) #壁の追加
wallList.append(wall(200,100,100,10)) #台の追加
wallList.append(wall(350,200,200,20))
wallList.append(wall(440,240,20,60))
wallList.append(wall(150,300,300,20))
wallList.append(wall(520,300,40,40))
wallList.append(wall(360,400,10,10))

@window.event
def on_close(): #ウィンドウの×ボタンがクリックされたときのイベント
    global LoopFlag #外の変数にアクセスするために必要
    LoopFlag = False

@window.event
def on_draw(): #windowのイベントとして描画処理を書く
    window.clear()
    batch.draw() #バッチでまとめて描画

@window.event
def on_key_press(symbol, modifiers): #押されたときのみイベントが発生する
    global LoopFlag
    if symbol == pyglet.window.key.ESCAPE:
        LoopFlag = False #ESCキーが押されたらループフラグを折ってループから抜ける。

window.set_visible() #ここでウィンドウを表示
while LoopFlag: #メインループ
    dt = pyglet.clock.tick() #前回tick()を呼び出してから経った時間（秒）を返す
    screen.fill(white) #[pygame] 先に定義した色で塗りつぶす

    JIKI.update(dt, wallList) #状態の更新
    JIKI.draw() #描画図形の定義の更新
    for eachWall in wallList:
        eachWall.update(dt)
        eachWall.draw()

    windowlist = list(pyglet.app.windows)
    for w in windowlist: #現在存在するwindow全てについて更新
        w.switch_to()
        w.dispatch_events()
        w.dispatch_event('on_draw') #実際の描画
        w.flip()

    #=============[pygame]========================
    pygame.draw.rect(screen, (20, 20, 20),pygame.Rect(90, 90, 190, 70)) #黒い四角

    #pygletのキー判定をorでいれて、どちらかがフォーカスを得ていれば入力を受け取れるようにする。
    Pygamekeys = pygame.key.get_pressed() #キー入力状態の取得
    pygame.draw.rect(screen, whiteBlue if (keys[pyglet.window.key.LEFT] or Pygamekeys[pygame.K_LEFT]) else gray, #右矢印キーで色を変える
                      pygame.Rect(100, 100, 50, 50))
    
    pygame.draw.rect(screen, whiteBlue if (keys[pyglet.window.key.SPACE] or Pygamekeys[pygame.K_SPACE]) else gray, #スペースキーで色を変える
                      pygame.Rect(160, 100, 50, 50))

    pygame.draw.rect(screen, whiteBlue if (keys[pyglet.window.key.RIGHT] or Pygamekeys[pygame.K_RIGHT]) else gray, #左矢印キーで色を変える
                      pygame.Rect(220, 100, 50, 50))
    
    if pygame.display.get_active():
        pygame.draw.rect(screen, (20, 20, 20),pygame.Rect(90, 170, 190, 70)) #黒い四角

    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            LoopFlag = False #×ボタンで閉じられたらループを抜ける
        if event.type == pygame.KEYDOWN:
            if event.key == pygame.K_ESCAPE:
                LoopFlag = False #ESCキーでもループを抜ける

    pygame.display.flip() #描画したものを表示
    pygame.time.wait(25) #25msec待つ
    #=============================================

window.close() #pygletを終了
pygame.quit() #pygameを終了
```

![ezgif-4-ca0e69b259.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3800150/7c88f9a2-9756-d84d-3bac-249367255d3b.gif)


>　***完璧な共存だ（ご満悦）***
 
　ただ、注意すべきは、キー入力を受け取れるのはフォーカスしているウィンドウのみであるということ。誤操作を防ぐ意味でこの仕様のほうが正しいものの、フォーカスされていないウィンドウは、もう片方のフォーカスされている方からデータをもらう必要がある。

　現在、ウィンドウがフォーカスされているかは取得できるので、適宜切り変えることもできる。しかし、上のコード同様、単純に両方のキー入力をもらって論理和（or）で判定取った方が早いだろう。

 ## OpenAIGymとやってみる

　本来、キー入力は余談であり、機械学習だとかの際に可視化用のもう一つのウィンドウが欲しい、という話がすべての始まりである。というわけで、[CarRacing](https://gymnasium.farama.org/environments/box2d/car_racing/)を動かしながら、キー入力と報酬を表示してみる。

　扱うのはCar Racing v2であるが、使用環境ではgymnasium[box2d]をインストールする際にエラーが発生した。少々手間取ったが、以下の方法で解決した。

:::note warn
普通に```pip install gymnasium[box2d]```でインストールしようとするとエラーが発生する。([参考](https://github.com/openai/gym/issues/3143))
どうやらswiftとwheel setup toolをインストールする必要があるようだ。
:::

### CarRacingをまずはインストールする

```terminal:
!pip install wheel setuptools pip --upgrade
!pip install swig
!pip install gymnasium[box2d]
```

　これでインストールできるはずである。再三触れるが、コンソールでやるなら頭の```!```は外すこと。

### CarRacingを動かす

```python:CarRacing.py
import gymnasium as gym
import pygame #キー入力用

env = gym.make('CarRacing-v2', continuous = True, render_mode="human")
print("Observation space: ", env.observation_space) #観測空間をprint
print("Action space: ", env.action_space) #行動空間をprint
s, info = env.reset()

Flag = True #メインループの終了判定用フラグ

while Flag: #メインループ
    action = [0.0, 0, 0] #continuous = Trueのとき、連続入力モード
    
    keys = pygame.key.get_pressed() #矢印キー入力で操作できるように
    if keys[pygame.K_LEFT]: #左
        action[0] -= 0.5
    if keys[pygame.K_RIGHT]: #右
        action[0] += 0.5
    action[1] = 1 if keys[pygame.K_UP] else 0 #アクセル
    action[2] = 1 if keys[pygame.K_DOWN] else 0 #ブレーキ

    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            Flag = False #×ボタンで閉じられたらループを抜ける
        if event.type == pygame.KEYDOWN:
            if event.key == pygame.K_ESCAPE:
                Flag = False #ESCキーでもループを抜ける

    # 行動を実行すると、環境の状態が更新される
    observation, reward, terminated, truncated, info = env.step(action)

    # ゲームが終了したら、環境を初期化して再開
    if terminated or truncated:
        observation, info = env.reset(options={"randomize": True})

env.close()
```

　キー入力でプレイできるが、結構難しい。

### pygletを接続

　ついにこの時が来た。上のCarRacingのコードにpygletを追加する。

　今回はpygletの書き方を少し変えて、ループ内で空のリストを作成、これに毎ループ、appendして描画する形をとった。こちらの方法はpygameの書き方にかなり近いやりかたとなる。pygameで作ったキー入力表示コードをpygletに移植する都合上、こちらの方がやりやすかった。

　また、新たにpygletのwindowの表示位置を調節している。視覚化が目的である以上、windowが重なって表示されると、並べるのが手間となる。
 
```python:CarRacing_with_pyglet.py
import gymnasium as gym
import pygame #キー入力用
import pyglet #pyglet君

Flag = True #メインループの終了判定用フラグ

#==========[pyglet]========
config = pyglet.gl.Config(double_buffer=True)
#visible=Falseで作成してすぐにはウィンドウを表示しない
window = pyglet.window.Window(640, 480, visible=False, config=config, resizable=False)
window.set_caption('pyglet') #ウィンドウのタイトル
pyglet.gl.glClearColor(1,1,1,1) #ウィンドウの背景 Red,Green,Blue,Alpha 0.0~1.0で指定
window.set_location(1080,980) #ウィンドウの位置を1080, 980の位置に取る。自身の画面に応じて変えること

batch = pyglet.graphics.Batch() #バッチの定義

whiteBlue = (200, 200, 255) #青白
gray = (100, 100, 100) #灰色 pygletに限らずpygameでも使える。共に色の形式は同じ。

@window.event
def on_close(): #ウィンドウの×ボタンがクリックされたときのイベント
    global Flag #外の変数にアクセスするために必要
    Flag = False #ループフラグを折ってループから抜ける。

@window.event
def on_draw(): #windowのイベントとして描画処理を書く
    window.clear()
    batch.draw() #バッチでまとめて描画

@window.event
def on_key_press(symbol, modifiers): #押されたときのみイベントが発生する
    global Flag
    if symbol == pyglet.window.key.ESCAPE:
        Flag = False #ESCキーが押されたらループフラグを折ってループから抜ける。

window.set_visible() #ここでウィンドウを表示
#==========================

env = gym.make('CarRacing-v2', continuous = True, render_mode="human")
print("Observation space: ", env.observation_space) #観測空間をprint
print("Action space: ", env.action_space) #行動空間をprint
s, info = env.reset()
score = 0.0 #スコア用

while Flag: #メインループ
    action = [0.0, 0, 0] #continuous = Trueのとき、連続入力モード
    
    keys = pygame.key.get_pressed() #矢印キー入力で操作できるように
    if keys[pygame.K_LEFT]: #左
        action[0] -= 0.5
    if keys[pygame.K_RIGHT]: #右
        action[0] += 0.5
    action[1] = 1 if keys[pygame.K_UP] else 0 #アクセル
    action[2] = 1 if keys[pygame.K_DOWN] else 0 #ブレーキ

    #==================[pyglet]======================
    shapesList = [] #図形登録用 appendしていけば、pygameのdrawのように書ける
    shapesList.append(pyglet.shapes.Rectangle(90, 90, 190, 130, color=(20,20,20), batch=batch)) #黒四角

    shapesList.append(pyglet.shapes.Rectangle(100, 100, 50, 50,
                        color=whiteBlue if keys[pygame.K_LEFT] else gray, batch=batch))
    
    shapesList.append(pyglet.shapes.Rectangle(160, 100, 50, 50,
                        color=whiteBlue if keys[pygame.K_DOWN] else gray, batch=batch))
    
    shapesList.append(pyglet.shapes.Rectangle(160, 160, 50, 50,
                        color=whiteBlue if keys[pygame.K_UP] else gray, batch=batch))

    shapesList.append(pyglet.shapes.Rectangle(220, 100, 50, 50,
                        color=whiteBlue if keys[pygame.K_RIGHT] else gray, batch=batch))
    
    windowlist = pyglet.app.windows
    for w in windowlist: #現在存在するwindow全てについて更新
        w.switch_to()
        w.dispatch_events()
        w.dispatch_event('on_draw')
        w.flip()
    #==================================================

    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            Flag = False #×ボタンで閉じられたらループを抜ける
        if event.type == pygame.KEYDOWN:
            if event.key == pygame.K_ESCAPE:
                Flag = False #ESCキーでもループを抜ける

    # 行動を実行すると、環境の状態が更新される
    observation, reward, terminated, truncated, info = env.step(action)

    score += reward #報酬を加算してスコアとする
    label = pyglet.text.Label('Score: ' + str(round(score,2)), #[pyglet] スコアの表示
                          font_name='Times New Roman',
                          font_size=32,
                          x=50, y=50,
                          color = (20, 20, 20, 255), #RGBA
                          anchor_x='left', anchor_y='center', batch=batch) #CarRacingの報酬

    # ゲームが終了したら、環境を初期化して再開
    if terminated or truncated:
        observation, info = env.reset(options={"randomize": True})
        score = 0.0 #スコアの初期化

env.close() #pygameのウィンドウを閉じる
window.close() #pygletのウィンドウを閉じる
```

![ezgif-4-ad3c32a6f6.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3800150/5bbc151a-b17d-785a-3222-24479acabe11.gif)

　やったぜ。

　下にpygletのウィンドウが表示されている。これで本来の目的「OpenAIGymを動かしながらの別枠での内部状態の可視化」は達成できたといってもいいだろう。機能面や描画面で、pygletがpygameよりも使い勝手等がよさそうなこともいい知見となった。今後、積極的にpygletを使っていくのもよさそうだ。

 # おまけ（Pygletのwindowを閉じるコード）
 　ジュピターノートブックでこれまでのコードは作業していたが、エラーを起こしたりするとpygletのwindowが取り残されたりする。さらに、pygletの場合、プログラムを動かすたびにwindowが増えていくことになる。

　下のコードはそれらのwindowをまとめて破棄するためのもの。ノートブックの最後のセルに置いておくと安心である。

```python:
windowlist = list(pyglet.app.windows) #ウィンドウを閉じた時点でサイズが変わりエラーが発生するので、リストに一旦コピーする
for window in windowlist:
    window.close()
```