1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

PygameとPygletを同時に動かす(&初めてのPyglet備忘録)

Last updated at Posted at 2024-06-13

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

目次

いや何の意味があるねん

 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と共有してもらわないと困るのだ。

 pygameとpygletの同居生活が最終目的だからね。

 というわけで、pygletのドキュメントを見たりしながら、まずは馴れ親しんだ(?)ループ型の奴を組んでみる。

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

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

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

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

バッチを使う(図形の描画)

 先ほどは説明を省いていたが、簡単な図形に関してはpyglet.shapesを使って描画する。

 パフォーマンスのためには、いちいちdraw()を呼ばずに、まとめて描画したほうがよいとのこと。そのためのBatchクラスが存在している。(Batched Rendering

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

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

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), width=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

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

Appendについて

 余談だが、上のコードのshapesList.appendについて、大量に行う場合はAppender = shapesList.appendのようにしてメソッドを変数に入れて使うとよい。(参考
 この差が意外と馬鹿にならない。3万個shapeを生成しようが、描画は1msec程度で済む。一方で、その数のappendを行うために200msecかかっていたりするため、描画速度について気にするよりも先に、描画前の演算部分の速度が問題となるだろう。

コーヒーブレイク:OpenGL方式の描画

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

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

screenshot.9.jpg

 この通り、上のコードをwindow.set_captionとかの初期化の項に続けて加えておけば、ウィンドウの背景色(正確にはclearするときの塗りつぶし色)を設定できる。このglClearColorはOpenGLの関数である。

 同様に、描画についても、OpenGLにおけるGL_LINE_LOOPとかGL_TRIANGLESとかそういうやつらを使って描画できるという。(参考
 GL_LINE_LOOPってなんだよ、って人はOpenGLについての解説を見るとよい。

 参考先の公式ドキュメント読むと......

#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のコードの書き方はわかったので、アクションゲーム的なものを試しに作ってみよう。プレイヤーの操作する自機と、登れる障害物をクラスで作る。これに衝突判定を実装して、描画する。

 スプライトを使えば画像を動かせるし、アニメーションもできるようだが、私は横着して図形のままでいく。

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

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

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

 いずれにせよ、これでpygletはマスターしたな!()

Pygameと組み合わせる

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

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

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

Pygameの基本

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

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

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

 矢印キー(左右)とスペースキーの投下状態が表示される。......使い道はバッグ・クロージャー以下だな。

がっちゃんこ(pyglet with pygame)

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

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

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

 完璧な共存だ(ご満悦)

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

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

OpenAIGymとやってみる

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

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

普通にpip install gymnasium[box2d]でインストールしようとするとエラーが発生する。(参考)
どうやらswiftとwheel setup toolをインストールする必要があるようだ。

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

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

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

CarRacingを動かす

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が重なって表示されると、並べるのが手間となる。

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

 やったぜ。

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

おまけ(Pygletのwindowを閉じるコード)

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

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

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

 

1
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?