#33 あー…あのDVDの待機画面のやつもう一度見たいなー…

どうも、まりなです。
あー…あれ、見たいなぁ…DVDプレーヤーのDVD入れてない時に出る待機画面のあれ。もう一度だけでいいから見たいなぁ…

↑これです。もしかしてピンとこない世代ももういる?こわ。

でももう時は令和、DVDはアウトオブデート、時代はBlu-rayです。実際DVDプレイヤーなんてもう捨ててしまいました。でも見たいなぁ…(上のYouTubeの動画見とけって?そんなこと言ったらこの記事はここで終わりだ)

「ないものは作ろう」が工学部マインド(?)です。今回はpygameというライブラリを使って例の待機画面っぽいものを作りたいと思います。今回は作成過程を紹介しながらプログラミングする時に私が考えていることなどを書いていこうと思います。高校までプログラミング経験ほぼ0だった私ですが大学に入りはや3年が経ちまあまあプログラミングもできるようになったので初心者講座的なものをやってみようという試みです。

作ろう

前述の通りpygameというpythonでゲームなどが作れるライブラリを使って作っていきます。が、私はこのライブラリについて詳しいわけではなく存在を知っていた程度です。初めての言語やライブラリ、フレームワークに触れる時、私はまずチュートリアルやサンプルコードを探します。日本語のものがあるとなお良いです。「pygame サンプルコード」などでググって良さげなサンプルコードを探してみます。

今回はこちらのサイトを参考にさせていただきました。
まずはサンプルコード"pygame_hello_world.py"をコピペしてそのままうごかしてみます。

GUIが開いてHello Worldと書かれました。まずはこの文字を"DVD"に変えてみようと思います。ウィンドウのタイトルも適当に変えてみようと思います。

pygame.display.set_caption("DVD Taiki")
text_surface = font.render("DVD", True, (0, 0, 255))

それぞれ11行目、16行目を書き換えることで変えられそうだったのでこんな感じに書いてみました。実行してみます。

それぞれ書き変わりましたね。次は文字を動かしてみます。文字の位置(座標)を変数として設けます。

    x = 100
    y = 100
    main_surface.blit(text_surface, (x, y))

位置が定数になっていたmain_surface.blitの第二引数を変数x, yを使うように書き換え、

vx = 4
vx = -4

x方向y方向の速度も変数として設けます。そしてメインループの中で位置を更新するようにすれば文字が動いてくれそうです。

x = x + vx
y = y + vy

main_surface.blit(text_surface, (x, y))

実行してみます。

あれ、動きません。調べると文字の位置を変えた後画面を更新しないといけないみたいです。メインループの中で更新するようにします。

x = x + vx
y = y + vy

main_surface.blit(text_surface, (x, y))

pygame.display.update()

これで動くはず、実行してみます。

思ってたんと違うのができてしまいました…(でもなんかこれはこれで懐かしさありますね。こういうのもありましたよね?え、誰に聞いてる?)

原因は明らかで一度描画した文字を消していないので次々にDVDが量産されてしまっているんですね。メインループ内で画面の初期化を行うようにします。次のDVDを書く時は一度画面を真っ白にしてから書くようにします。

# メイン画面の色設定(引数:RGB)
main_surface.fill((220, 220, 220))

画面の初期化はサンプルコードではループの外にこんな風に書いてあったのでこれを参考にします。白色をRGBで指定していますが複数箇所で使う場合これが何色なのかわかりにくくなってしまうことを危惧し「白色」を定数として設けておくことにしました。ある筋からの情報によると白って200色あるらしいですからね。このプログラムにおける「白色」はこれだと定めておくのは大事そうです。

    # メイン画面の色設定(引数:RGB)
    white = (220, 220, 220)
    main_surface.fill(white)

ループ内にも画面の初期化を書き加えます。

        # init the main surface
        main_surface.fill(white)

        # move
        x = x + vx
        y = y + vy

        # depict the text
        main_surface.blit(text_surface, (x, y))

        # update the display
        pygame.display.update()

ループ内はこんな感じになりました。適宜#でコメントアウトをつけて後でコードを見たときに何をやってるかわかりやすくするのは大事ですね。

さあ実行してみます。今度こそうまく行くはず。

ちゃんと文字が動いてますね。でもこれでは文字はどっか行ったままです。跳ね返らないと意味がありません。跳ね返って跳ね返るたび色が変わるから趣があるのです。ということで文字に衝突判定を設けます。

要は画面の枠に文字が当たったら速度に-1をかけてやればいいのです。(大丈夫?これわかる?理系じゃない人にはピンときにくい?)
画面横にあたったらvxを-1倍、画面上下にあたったらvyを-1倍する処理をループ内に書きます。

    # メイン画面(Surface)初期化(横, 縦)
    main_surface = pygame.display.set_mode((300, 300))

    # メイン画面(Surface)初期化(横, 縦)
    width = 300
    height = 300
    main_surface = pygame.display.set_mode((width, height))

ここでさっきの白の件と同様、画面の縦幅と横幅を定数として設けておきます。これらを使って、

        # collision decision
        if x <= 0 or x >= width:
            vx *= -1
        if y <= 0 or y >= height:
            vy *= -1

衝突判定はこう書けます。さらに文字の色も衝突のたびに変わるようにします。色はランダムに変わるようにしたいのでRGBをそれぞれ乱数で決めれば良さそうです。

def RandomColor():
    R = rd.randrange(0, 255)
    G = rd.randrange(0, 255)
    B = rd.randrange(0, 255)
    return (R, G, B)

ランダムで色を決めるRandomColorという関数を作りました。複数箇所で行う操作は関数化しましょう。

        # collision decision
        if x <= 0 or x >= width:
            vx *= -1
            text_color = RandomColor()
            text_surface = font.render("DVD", True, text_color)
        if y <= 0 or y >= height:
            vy *= -1
            text_color = RandomColor()
            text_surface = font.render("DVD", True, text_color)

衝突判定の部分に色を変える処理も書き加えました。これで実行してみます。

かなりあれっぽくなりました。しかし勘の良い方はお気づきかもしれませんが問題があります。
衝突判定を書いたのに文字が画面外にはみ出してしまっていますね。これは衝突判定の書き方が厳密にはまちがっているからです。x, yは文字の左上の座標なので今の書き方では文字の左上が画面枠に当たったときに衝突判定を行うようになってしまっています。

実際には文字の右端が右壁に当たった時や下端が下壁にあたったときに衝突判定しなければなりません。これを判定するためには文字の幅や高さの値が必要になるのですが調べるとpygameでは.get_width()や.get_height()といったメソッドで幅や高さを取得できるようです。

    # set the text width and height
    text_width = text_surface.get_width()
    text_height = text_surface.get_height()

ということで文字の幅や高さを取得し、

        # collision decision
        if x <= 0 or x + text_width >= width:
            vx *= -1
            text_color = RandomColor()
            text_surface = font.render("DVD", True, text_color)
        if y <= 0 or y + text_height >= height:
            vy *= -1
            text_color = RandomColor()
            text_surface = font.render("DVD", True, text_color)

衝突判定をこう書きかえました。
もうひとつ解決したい問題があります。今の書き方では文字の軌道がずっと同じままです。軌道がすこしずつ変わっていくから趣があるんですよね。ということで衝突したとき速度を単純に-1倍するだけでなくちょっとだけ変動させることにします。

        # collision decision
        if x <= 0 or x + text_width >= width:
            vx *= -1
            vx += rd.uniform(-1, 1)
            text_color = RandomColor()
            text_surface = font.render("DVD", True, text_color)
        if y <= 0 or y + text_height >= height:
            vy *= -1
            vy += rd.uniform(-1, 1)
            text_color = RandomColor()
            text_surface = font.render("DVD", True, text_color)

-1から1の乱数を速度に加えることでブレを生み軌道を変化させます。

あと画面や文字の大きさとかも調整して最終的に完成したものがこちらです。

満足いくものができました。これでずっとDVDの待機画面のやつを見ていられます。

おわり

いかがだったでしょうか(なにが?)。皆さまのお役に立てば幸いです(なにの?)。それでは、さようなら(???)。

ソースコード

import sys
import pygame
import random as rd

def RandomColor():
    R = rd.randrange(0, 255)
    G = rd.randrange(0, 255)
    B = rd.randrange(0, 255)
    return (R, G, B)

def main():
    # init pygame
    pygame.init()
    # init the display
    width = 600
    height = 600
    main_surface = pygame.display.set_mode((width, height))
    # set the title of main display
    pygame.display.set_caption("DVD Taiki")
    # generate the font object(value:(font type, font size))
    # if font type is "None", it's set as the default font of pygame
    font = pygame.font.Font(None, 60)
    # generate the text surface object(value:text, antialias: bool, (R, G, B))
    text_color = RandomColor()
    text_surface = font.render("DVD", True, text_color)
    # set the text width and height
    text_width = text_surface.get_width()
    text_height = text_surface.get_height()
    # set the color of main display(value:(R,G,B))
    white = (220, 220, 220)
    main_surface.fill(white)
    # set the text on the main display(value:(surface, coordinate))
    x = rd.randrange(0, width - text_width)
    y = rd.randrange(0, height - text_height)
    main_surface.blit(text_surface, (x, y))
    # update the main display
    pygame.display.update()
    # set the clock object
    clock = pygame.time.Clock()
    # set initial velocity
    vx = 4
    vy = -4

    # flag for loop
    going = True
    # main loop
    while going:
        # get a event
        for event in pygame.event.get():
            # if event type is quit, set the flag False
            if event.type == pygame.QUIT:
                going = False

        # init main surface
        main_surface.fill(white)
        # move
        x += vx
        y += vy

        # collision decision
        if x <= 0 or x + text_width >= width:
            vx *= -1
            vx += rd.uniform(-1, 1)
            text_color = RandomColor()
            text_surface = font.render("DVD", True, text_color)
        if y <= 0 or y + text_height >= height:
            vy *= -1
            vy += rd.uniform(-1, 1)
            text_color = RandomColor()
            text_surface = font.render("DVD", True, text_color)

        # depict the text
        main_surface.blit(text_surface, (x, y))
        # update the display
        pygame.display.update()

        # set the frame rate
        clock.tick(30)

    # quit
    pygame.quit()
    sys.exit()

if __name__ == '__main__':
    main()

この記事が気に入ったらサポートをしてみませんか?