mieki256's diary



2017/04/24(月) [n年前の日記]

#1 [python] pysdl2について調べていたり

pygame_sdl2 についてアレコレ調べていたけど、どうも筋が悪い気がしてきたので、pysdl2について調べ始めたり。

pysdl2てのは…。Pythonから、SDL2というマルチメディア向けのライブラリを呼び出して使えるライブラリ。要するにゲームが作れるライブラリ。

pysdl2の概要を一応メモ。 :

公式ドキュメントによると、pysdl2は2つのパッケージで構成されているそうで。
  • sdl2。SDL2のAPI(機能?)と1:1で対応してる。
  • sdl2.ext。sdl2だけでは各機能が低レベル過ぎて記述が面倒臭くなるので、もう少し簡単に書けるようにsdl2を拡張する。

更に、「全部 Python で実装してある」とも書いてあった。Cで書いてあるわけじゃないから、例えば sdl2.ext を極力使ったほうが処理時間が短くなる、というわけでもないらしい。

チュートリアルを勉強。 :

とりあえず、公式ドキュメントのチュートリアルから ―― hello world から始めたり。まずはウインドウを表示する。できれば画像も表示する。

_Hello World - PySDL2 0.9.5 documentation

公式ドキュメントのソレは sdl2.ext を利用した版になっている。日本語コメントをガシガシ追加しつつ写経。

もし、このスクリプトを動かしたい人が居たら…。動作には画像が必要なので、 _hello.png をDLして、res というフォルダを作ってその中に入れといてもらえれば、と。

_helloworld_pysdl2_ext.py
u"""
PySDL2 (pysdl2.ext) のテスト.

ウインドウを表示して画像(スプライト)を表示

動作確認環境:
Windows10 x64 + Python 2.7.13 32bit + PySDL2 0.9.5
"""

import sdl2.ext

# sdl2.ext.Resource() で、リソース(画像等)の格納場所を指定できる
# res という名前のフォルダを作って、中に画像(hello.png)を入れた
RESOURCES = sdl2.ext.Resources(__file__, "res")

# PySDL2 の初期化
sdl2.ext.init()

# ウインドウを作成。タイトル文字列とウインドウサイズを指定
window = sdl2.ext.Window("Hello World", size=(640, 480))

# 作成したウインドウを表示
window.show()

# スプライトを作成。hello.png を読み込んで渡す
factory = sdl2.ext.SpriteFactory(sdl2.ext.SOFTWARE)
sprite = factory.from_image(RESOURCES.get_path("hello.png"))

# スプライトの表示位置を指定
sprite.position = 24, 24

# スプライトを描画
spriterenderer = factory.create_sprite_render_system(window)
spriterenderer.render(sprite)

# イベントループ。閉じるボタンが押されるまで待つ(ループする)
processor = sdl2.ext.TestEventProcessor()
processor.run(window)

# 終了処理。今まで確保したアレコレを破棄する
sdl2.ext.quit()

以下で実行。
python helloworld_pysdl2_ext.py
helloworld_pysdl2_ext_ss.png

ウインドウが表示されて、画像も表示された。また、閉じるボタンをクリックすれば、ウインドウを閉じることができた。

ところで、ソースを眺めると…。

「sdl2.ext.SpriteFactory()って何だ?」
「factory.create_sprite_render_system()って何?」
「sdl2.ext.TestEventProcessor()って何なの? 何をしてくれるの?」

という気分になりませんかね。自分はなりました。

sdl2だけで書いてみる。 :

sdl2.extを使わずに、sdl2だけで書いてみたり。pysdl2をインストールしたフォルダ内の、examples\sdl2hello.py が参考になるかと。

ちなみに、公式のサンプルはbmpファイルを読み込んでいたので sdl2 だけで処理ができていたのだけど。欲を出して(?)、うっかり png を読み込もうとしたら…。pngの読み込みには、SDL2_image.dll が必要になるのですな…。

_helloworld_pysdl2.py
u"""
PySDL2 のテスト.

ウインドウを表示して画像(スプライト)を表示
sdl2.ext は使わず、sdl2 を使って処理をしてみる。
ソフトウェア的に画像転送してるので処理としては遅いらしい。

png画像の読み込みには sdl2.sdlimage が利用できるらしい?

動作確認環境:
Windows10 x64 + Python 2.7.13 32bit + PySDL2 0.9.5
"""

import sdl2
import sdl2.sdlimage
import ctypes
import os


# PySDL2 の初期化
sdl2.SDL_Init(sdl2.SDL_INIT_VIDEO)

# ウインドウを作成して表示。
# タイトル文字列、表示位置、ウインドウサイズを指定している
window = sdl2.SDL_CreateWindow(b"Hello World",
                               sdl2.SDL_WINDOWPOS_CENTERED,
                               sdl2.SDL_WINDOWPOS_CENTERED,
                               640, 480,
                               sdl2.SDL_WINDOW_SHOWN)

# ウインドウのサーフェイスを取得
windowsurface = sdl2.SDL_GetWindowSurface(window)

# 画像ファイルのパスを取得
filepath = os.path.join(os.path.dirname(os.path.abspath(__file__)),
                        "res", "hello.png")

# 画像を読み込み
# image = sdl2.SDL_LoadBMP(filepath.encode("utf-8"))
image = sdl2.sdlimage.IMG_Load(filepath.encode("utf-8"))

# 画像サイズを取得してみる
# ctypes を利用しているため、
# image には SDL_Surface ではなく LP_SDL_Surface が入ってる
# 故に image.w では取得できないが image.contents.w にすれば取得できる
w = image.contents.w
h = image.contents.h
print("w, h = %d, %d" % (w, h))

# 画像の転送先領域を指定
x = 24
y = 36
w = image.contents.w
h = image.contents.h
dst_rect = sdl2.rect.SDL_Rect(x, y, w, h)

# 画像を、ウインドウサーフェイスに転送
sdl2.SDL_BlitSurface(image, None, windowsurface, dst_rect)

# ウインドウサーフェイスを更新
sdl2.SDL_UpdateWindowSurface(window)

# RGBサーフェイスを解放
sdl2.SDL_FreeSurface(image)

# イベントループ。閉じるボタンが押されるまで待つ(ループする)
event = sdl2.SDL_Event()
running = True
while running:
    while sdl2.SDL_PollEvent(ctypes.byref(event)) != 0:
        if event.type == sdl2.SDL_QUIT:
            running = False
            break
    sdl2.SDL_Delay(10)

# 終了処理。今まで確保したアレコレを破棄する
sdl2.SDL_DestroyWindow(window)
sdl2.SDL_Quit()

実行してみる。
python helloworld_pysdl2.py
helloworld_pysdl2_ss.png

画像が表示された。

sdl2.ext を使った版と比べると、sdl2 だけで書いた版は、やっぱり面倒臭い感が。画像をサーフェイスに読み込むだけで一体何行書くんだよ、みたいな。

かといって、そこ(画像の読み込みと描画)を「sdl2.ext.SpriteFactory()」でまとめられても、ちと困るよなと…。

こっちは画像をサーフェイスに読み込みたいわけで、スプライトが欲しいわけではないのですよ。なのにどうしてそこでスプライト云々のクラスが登場するのかと。しかもそのスプライトって一体何ができるスプライトなのか、そこもわかんねーよ、みたいな。まとめ過ぎ・包み過ぎだろうと。

印象。 :

pysdl2 も pysdl2 で、ちと筋が悪いような気もしてきたり。

sdl2は、SDL2の各機能を呼び出すことだけを目的にして作ってあるので、これはこれで問題無いのだけど。

sdl2.extは、個人的にどうもしっくりこない…。と言うのも、ゲームプログラムのロジック部分まで無頓着にまとめてしまっている上に、各クラスやメソッドが中で何をしてくれそうなのかよく分からなくて。

結果、謎の呪文だらけのブラックボックス、それも、sdl2.extの作者さんが想定した範囲内でしか使い道がないのであろう、頭が固いガチガチなライブラリになってしまっている気配が…。いやまあ、せっかくPythonで書いてあるのだから、どんどんソースを見て何をしてるか理解すればいいんだろうけど。逆に言うと、ソース見なきゃ役割が分からない、ってのもどうなんだと。

おまじない満載の「これしかできません」的ブラックボックスを使いたいなら、それこそUnityでも使ったほうが…。ウンザリするほどおまじないを暗記させられるけど、その分リターンも期待できそうだよなと。

しかし、Unity 利用時のようなゲーム映像が得られるならともかく、Python + SDL2 程度で、そこでしか通用しないおまじないを覚えさせてみてどうするんだと。コレの使い方を覚えたところで、他で使い道が無い。汎用性が無い。発展が期待できない。使い方を勉強することの旨味が少ない…。 *1

さりとて、sdl2.ext をスルーして sdl2 だけで書こうとすると、やっぱりアレコレ面倒臭いと感じてしまうのも間違いなくて。いやまあ、PythonからSDL2を呼べるだけでも有難いことだけど、でもやっぱりまどろっこしい。画像を一つ出すだけなのにどうしてこんなに行数が必要に、みたいなところが。

もっともこのあたり、DXRuby を普段使わせてもらっているから感じてしまうのだろうけど…。DXRubyに触れてなかったら「この手のライブラリはこんなもんだ」とうっかり思い込んでいたかもしれず。

それにしても…。DXRubyに比べると、SDL や SDL2 を呼び出す系のライブラリは、えてしてラップの仕方・包み方がおかしいところがあるなと。「何故ここを包まない?」と「どうしてここまで包むんだ?」が時々出現してしまう、そんな印象が。(※ 感想には個人差があります。) *2

pongのチュートリアルも勉強。 :

一応、pong game のチュートリアルも写経したので、もったいないから貼っときます。

_The Pong Game - PySDL2 0.9.5 documentation

_pong_pysdl2_6.py
u"""
PySDL2 のテスト.

ポンゲーム。

動作確認環境:
Windows10 x64 + Python 2.7.13 32bit + PySDL2 0.9.5
"""

# sdl2 の全機能にアクセスしたいので、sdl2 もインポートする

import sys
import sdl2
import sdl2.ext

# 白色を定義
WHITE = sdl2.ext.Color(255, 255, 255)


class SoftwareRenderer(sdl2.ext.SoftwareSpriteRenderSystem):

    u"""ソフトウェアレンダラークラス."""

    def __init__(self, window):
        u"""コンストラクタ."""
        super(SoftwareRenderer, self).__init__(window)

    def render(self, components):
        u"""描画処理."""
        # 画面全体を黒く塗りつぶしてから、本来の描画処理を行うようにしている
        sdl2.ext.fill(self.surface, sdl2.ext.Color(0, 0, 0))
        super(SoftwareRenderer, self).render(components)


class MovementSystem(sdl2.ext.Applicator):

    u"""各スプライトの移動処理を担当するクラス."""

    def __init__(self, minx, miny, maxx, maxy):
        u"""コンストラクタ."""
        super(MovementSystem, self).__init__()
        self.componenttypes = Velocity, sdl2.ext.Sprite
        self.minx = minx
        self.miny = miny
        self.maxx = maxx
        self.maxy = maxy

    def process(self, world, componentsets):
        u"""スプライトの移動処理."""
        for velocity, sprite in componentsets:
            # 速度を持っているスプライトに対して、
            # 画面内から飛び出さないように位置を変更する
            swidth, sheight = sprite.size
            sprite.x += velocity.vx
            sprite.y += velocity.vy

            sprite.x = max(self.minx, sprite.x)
            sprite.y = max(self.miny, sprite.y)

            pmaxx = sprite.x + swidth
            pmaxy = sprite.y + sheight
            if pmaxx > self.maxx:
                sprite.x = self.maxx - swidth
            if pmaxy > self.maxy:
                sprite.y = self.maxy - sheight


class CollisionSystem(sdl2.ext.Applicator):

    u"""アタリ処理クラス."""

    def __init__(self, minx, miny, maxx, maxy):
        u"""コンストラクタ."""
        super(CollisionSystem, self).__init__()
        self.componenttypes = Velocity, sdl2.ext.Sprite
        self.ball = None
        self.minx = minx
        self.miny = miny
        self.maxx = maxx
        self.maxy = maxy

    def _overlap(self, item):
        u"""ボールがプレイヤー(パドル)と当たってるか調べて返す."""
        pos, sprite = item
        if sprite == self.ball.sprite:
            # 見ている相手がボールなのでアタリ判定はしない
            return False

        # 相手の範囲とボールの範囲を取得
        left, top, right, bottom = sprite.area
        bleft, btop, bright, bbottom = self.ball.sprite.area

        return (bleft < right and bright > left and
                btop < bottom and bbottom > top)

    def process(self, world, componentsets):
        u"""アタリ判定処理."""
        # ボールと当たってるスプライトを返す
        collitems = [comp for comp in componentsets if self._overlap(comp)]
        if collitems:
            # 何かに(プレイヤーに)当たったらボールのx速度を反転する
            self.ball.velocity.vx = -self.ball.velocity.vx

            # 当たった位置でy速度を変更
            sprite = collitems[0][1]
            ballcentery = self.ball.sprite.y + self.ball.sprite.size[1] // 2
            halfheight = sprite.size[1] // 2
            stepsize = halfheight // 10
            degrees = 0.7
            paddlecentery = sprite.y + halfheight
            if ballcentery < paddlecentery:
                factor = (paddlecentery - ballcentery) // stepsize
                self.ball.velocity.vy = -int(round(factor * degrees))
            elif ballcentery > paddlecentery:
                factor = (ballcentery - paddlecentery) // stepsize
                self.ball.velocity.vy = int(round(factor * degrees))
            else:
                self.ball.velocity.vy = - self.ball.velocity.vy

        # ボールが上下の壁に当たったらボールのy速度を反転
        if (self.ball.sprite.y <= self.miny or
                self.ball.sprite.y + self.ball.sprite.size[1] >= self.maxy):
            self.ball.velocity.vy = - self.ball.velocity.vy

        if (self.ball.sprite.x <= self.minx or
                self.ball.sprite.x + self.ball.sprite.size[0] >= self.maxx):
            self.ball.velocity.vx = - self.ball.velocity.vx


class Velocity(object):

    u"""速度保持クラス.

    コレを持っているクラスは速度があるので、位置が変化するものとして扱われる。
    """

    def __init__(self):
        u"""コンストラクタ."""
        super(Velocity, self).__init__()
        self.vx = 0
        self.vy = 0


class TrackingAIController(sdl2.ext.Applicator):

    u"""敵の思考ルーチン."""

    def __init__(self, miny, maxy):
        u"""コンストラクタ."""
        super(TrackingAIController, self).__init__()
        self.componenttypes = PlayerData, Velocity, sdl2.ext.Sprite
        self.miny = miny
        self.maxy = maxy
        self.ball = None

    def process(self, world, componentsets):
        u"""思考処理."""
        for pdata, vel, sprite in componentsets:
            if not pdata.ai:
                continue

            centery = sprite.y + sprite.size[1] // 2
            if self.ball.velocity.vx < 0:
                # ball is moving away from the AI
                if centery < self.maxy // 2:
                    vel.vy = 3
                elif centery > self.maxy // 2:
                    vel.vy = -3
                else:
                    vel.vy = 0
            else:
                bcentery = self.ball.sprite.y + self.ball.sprite.size[1] // 2
                if bcentery < centery:
                    vel.vy = -3
                elif bcentery > centery:
                    vel.vy = 3
                else:
                    vel.vy = 0


class PlayerData(object):

    u"""パドルががCPUかプレイヤーかを保持するクラス."""

    def __init__(self):
        u"""コンストラクタ."""
        super(PlayerData, self).__init__()
        self.ai = False


class Player(sdl2.ext.Entity):

    u"""プレイヤー(パドル)クラス."""

    def __init__(self, world, sprite, posx=0, posy=0, ai=False):
        u"""コンストラクタ."""
        self.sprite = sprite
        self.sprite.position = posx, posy  # スプライトの位置を設定
        self.velocity = Velocity()         # 速度を持たせる
        self.playerdata = PlayerData()
        self.playerdata.ai = ai            # プレイヤーにするか、CPUにするかを指定


class Ball(sdl2.ext.Entity):

    u"""ボールクラス."""

    def __init__(self, world, sprite, posx=0, posy=0):
        u"""コンストラクタ."""
        self.sprite = sprite
        self.sprite.position = posx, posy
        self.velocity = Velocity()


def run():
    u"""メイン処理."""
    # PySDL2 の初期化
    sdl2.ext.init()

    # ウインドウ作成と表示
    window = sdl2.ext.Window("The Pong Game", size=(800, 600))
    window.show()

    # ワールド作成
    world = sdl2.ext.World()

    # 移動処理担当クラスを生成。左上と右下の座標を渡している
    movement = MovementSystem(0, 0, 800, 600)

    # アタリ判定処理クラスを生成。左上と右下の座標を渡している
    collision = CollisionSystem(0, 0, 800, 600)

    # ソフトウェアレンダラーを生成
    spriterenderer = SoftwareRenderer(window)

    # 敵AI制御クラスを生成
    aicontroller = TrackingAIController(0, 600)

    # 敵AI制御クラス、移動処理担当クラス、
    # アタリ判定クラス、レンダラーをワールドに追加
    world.add_system(aicontroller)
    world.add_system(movement)
    world.add_system(collision)
    world.add_system(spriterenderer)

    # 白色のスプライトを3つ作成
    factory = sdl2.ext.SpriteFactory(sdl2.ext.SOFTWARE)
    sp_paddle1 = factory.from_color(WHITE, size=(20, 100))  # プレイヤー1
    sp_paddle2 = factory.from_color(WHITE, size=(20, 100))  # プレイヤー2
    sp_ball = factory.from_color(WHITE, size=(20, 20))   # ボール

    # プレイヤーを2つ生成
    player1 = Player(world, sp_paddle1, 0, 250)
    player2 = Player(world, sp_paddle2, 780, 250, True)

    # ボールを生成
    ball = Ball(world, sp_ball, 390, 290)
    ball.velocity.vx = -3

    collision.ball = ball
    aicontroller.ball = ball

    running = True     # ループ管理用フラグ

    # メインループ
    while running:
        events = sdl2.ext.get_events()  # イベント取得
        for event in events:
            if event.type == sdl2.SDL_QUIT:
                # 閉じるボタンが押されたらメインループを終了させる
                running = False
                break
            if event.type == sdl2.SDL_KEYDOWN:
                # キーが押された
                if event.key.keysym.sym == sdl2.SDLK_UP:
                    # 上キーだった
                    player1.velocity.vy = -3
                elif event.key.keysym.sym == sdl2.SDLK_DOWN:
                    # 下キーだった
                    player1.velocity.vy = 3
            elif event.type == sdl2.SDL_KEYUP:
                # キーが離された
                if event.key.keysym.sym in (sdl2.SDLK_UP, sdl2.SDLK_DOWN):
                    # 上キーか下キーだった
                    player1.velocity.vy = 0

        # 時間待ち
        sdl2.SDL_Delay(10)

        # ワールドの処理
        world.process()

        # ウインドウのグラフィックバッファを更新
        # window.refresh()

    sdl2.ext.quit()  # SDL2関係の終了処理

    return 0         # 終了コードを返す

if __name__ == "__main__":
    # このスクリプトが単体で呼ばれた時に、ここが処理される
    ret = run()    # メイン処理呼び出し
    sys.exit(ret)  # pythonスクリプトを終了

実行。
python pong_pysdl2_6.py
pong_pysdl2_6_ss.png

これもソースを眺めると…。

「sdl2.ext.World()って何だよ?」
「sdl2.ext.Applicator って何?」

という気分に。やっぱり筋が悪い気が…。いやまあ、「だったら sdl2 だけ使って書いてりゃいいだろ!」って言われるのは分かってますけど。

チュートリアルなのに、ソフトウェアレンダラーを使ってる気配がするのも、なんだか…。だったら pygame でいいじゃん…。SDL2の旨味って、ハードウェア描画が使いやすくなったことじゃなかったのか…。ライブラリの売りを自らスポイルしていくチュートリアルってのも、意味が分からん…。

*1: 例えば、HTMLタグを覚えれば何かしら応用が利くけど、ホームページビルダーの使い方を覚えてもそこから発展しないじゃん、みたいな話かも。
*2: とは言え自分もそのへん全然言えないですけど。クラスを一つ作るたびに「このメソッドの引数はよくない…が、もっとヨサゲなまとめ方が思いつかない…」と悩んだりするわけで。

以上です。

過去ログ表示

Prev - 2017/04 - Next
1
2 3 4 5 6 7 8
9 10 11 12 13 14 15
16 17 18 19 20 21 22
23 24 25 26 27 28 29
30

カテゴリで表示

検索機能は Namazu for hns で提供されています。(詳細指定/ヘルプ


注意: 現在使用の日記自動生成システムは Version 2.19.6 です。
公開されている日記自動生成システムは Version 2.19.5 です。

Powered by hns-2.19.6, HyperNikkiSystem Project