複天一流:どんな手を使ってでも問題解決を図るブログ

宮本武蔵の五輪書の教えに従い、どんな手を使ってでも問題解決を図るブログです(特に、科学、数学、工学の問題についてですが)

エルチカのためのシミュレータをpygameでつくる:一つ目エルチカ

これまでの経過

派手なエルチカモードの開発を計画しているが、実機でやる前にソフトウェアでシミュレーターを作ってからやってみようと思う。そこで、pythonpygameというパッケージを利用することにした。基本的な枠組みを理解し、アニメーションをpythonで制御するところまで前回はたどり着いた。

今回は、いよいよLEDをチカチカさせるところまでの挑戦だが、まずは結果から。

youtu.be

こんな感じのLEDシミュレータを7つ並べて7ビット2進数表現を光で行うのが最終目的である。7ビットというのはアスキーコードの仕様である。文章を文字列で扱い、それを2進数へ変換した後、LEDシミュレータに送り込んで「一文字」ずつ光らせる、という手順である。

pygameを使ったプログラミングの基礎

まずはpygameのパッケージを読み込む。

import pygame

これだけ書いて実行すると

% python3 led0.py

pygame 2.6.1 (SDL 2.28.4, Python 3.12.3)
Hello from the pygame community. https://www.pygame.org/contribute.html

というメッセージが出て、自動的に終了する。これがpygameへの第一歩である。

次に手続き(おまじない)として、初期化と処理の終了の宣言を書く。

import pygame

pygame.init()

pygame.quit()

どちらもpygameクラスに含まれるメンバー関数(pythonではメソッド関数、あるいはメソッドという場合が多いらしいが、python foundation のdocsではメソッドを主にしつつも、両方使っている)である。こちらのHPによると、init()は初期化に成功したpygameモジュールの数と、失敗した数をペアにして返値としているらしい。quit()はvoid型らしい。さっそく確認してみた。

import pygame

nReturn = pygame.init()
print(*nReturn)
pygame.quit()

実行したら"5 0"という表示が出た。5個初期化に成功したというが、一体何だろうか?(確認の仕方はまだ知らないので、今回はここで終わり...)

「ゲームプログラミング」の基本

ビデオゲーム」というのは、双方向通信が可能なインターアクティブなアニメーションのシステムである。逆に、この内容を持っているプログラムは「ビデオゲーム」だけとは限らない。いわゆるchat式AIなどに動画などによるレスポンスを組み込んだシステムや、アバターを介してコミュニケーションを行うネット上のバーチャル世界(メタバースなど)なども「インターアクティブアニメーションシステム」と言えるだろう。したがって、pygameで扱えるプログラムはゲームに限らないのだが、面倒臭いので、このようなタイプのプログラミングを一律に「ゲームプログラミング」と呼ぶことにしよう。

pygameの仕様について色々試行錯誤してみた結果、ゲームプログラミングというのは結局3つの要素から構成されていることがわかった。

  1. アニメーションのタイミングの管理
  2. 双方向イベントの管理
  3. 描画スクリーンの管理

アニメーションなので描いた絵が動き回るわけである。したがって、そのタイミングの管理が必要になる。次に、ゲームというのは、ビーム砲を連射したり、右に左に弾幕を避けたりするわけで、プレイヤーの操作とゲームの反応イベントの組み合わせ、あるいはゲーム中の発生イベントとそれに対する反応操作の組み合わせによって成立している。つまり、イベントという概念が重要になる。これもプログラムによって管理しなくてはならない。最後に、アニメーションというのは無数の静止画を少しずつ変化させて表示し直す(再表示)システムであるから、基本である画像表示を管理しなくてはならない。

これら3つの管理を統合的に行うと、ゲーム、つまり双方向にインターアクティブなアニメーションシステムが成立する。

pygameでも、これら3つの要素を組み合わせているので、その要素を過不足なく書き下した時初めて「システム」として動き出す。この観点から「最低限」のpygameプログラムを考えてみたら次のようになった。

import pygame
pygame.init()

screen = pygame.display.set_mode((WDT,HGH))
clock   = pygame.time.Clock()
event   = pygame.event.get()

pygame.quit()

(WDT,HGH)はゲームスクリーンのサイズである。具体的な数字を与えてから実行すると、一瞬スクリーンが表示され、そしてすぐさま終了する。エラーが出ないはずなので、これが「最低限」だと思われる。

フレームとフレームレートの設定

人間が視認できる時間間隔はどのくらいだろうか?Xeviousのプロゲーマーは除外するとして、大抵の場合は1秒程度ではないだろうか?そこで、上の「最低限プログラム」が1秒程度は継続するように修正を入れてみたい。

これはFPS、Frame per Second、によって管理する。フレームというのは静止画1枚分のことである。少しずつフレームの内容を変化させながら、次々に入れ替えるとアニメーションになる。この入れ替える間隔(タイミング)のことをFPSという。大抵のゲーム機では60が設定されているようだが、これは1秒間に60回フレームが変化するという意味である。かなりのスピードであるから、このFPSでのフレームの切り替えが見える人は、もはや「Xeviousのプロゲーマー」を超越しているのではないだろうか?

我々凡人はFPS=1、つまり1秒で1枚(1フレーム)を試してみよう。これなら、切り替えの変化に気づくはずである。また、これは1秒間同じフレームを表示し続ける、という意味になるので、上のプログラムが1秒間持続する、という意味にもなる。

import pygame

WDT=500; HGH=500; SCRsize=(WDT,HGH)
FPS=1

pygame.init()

screen = pygame.display.set_mode(SCRsize)
clock   = pygame.time.Clock()
event   = pygame.event.get()

clock.tick(FPS)
pygame.quit()

実行は、上のコードをpygame-min.pyと名付けたならば、

% python3 pygame-min.py

とやる。

先ほどは一瞬で消えてしまったゲームスクリーンが「ゆっくり」消えるのがわかるだろう。計測してみれば、おおよそ1秒間コンピュータのスクリーンに表示されているはずである。ためしにFPS=60に変更すると、一瞬で消えてしまうだろう。

clock.tick(FPS)という命令は、pygameクラスのtimeという属性を管理していることになる。プログラム中ではclockというオブジェクトとして具体化されている(インスタンスというのだろうか)。tickというのは時計の秒針が出す音(チクタクのチク)を意味する英語である。FPSを省略してprint(clock.tick())とやると、tick()とtick()の間の時間間隔を測定することができる。これは別の機会に試してみよう(今回は省略)。

スクリーンの変化の管理

ゲームはアニメーションなので、スクリーンに何か変化を持たせたい。そこで背景の色を変えてみる。今度はpygameクラスのdisplay属性を管理することになる。プログラム中ではscreenというインスタンスを通じてアクセスできる。変化を指定しても、その変化を画面に「反映」させなくては、変化を視認することができない。この「再描画」の処理はscreenというインスタンスではなく、display属性全体にかけなくてはならないようで、次のようにcodingしないとうまくいかなかった。

import pygame

WDT=500; HGH=500; SCRsize=(WDT,HGH)
FPS=1

pygame.init()

screen = pygame.display.set_mode(SCRsize)
clock   = pygame.time.Clock()
event   = pygame.event.get()

screen.fill("green"); pygame.diplay.flip()
clock.tick(FPS)
pygame.quit()

イベントの管理

最後はイベントの管理である。イベントは、get()メソッドで読み取るまでのすべてのイベントがメモリに積み重なっていく。あまり時間をかけすぎるとメモリーに負担がかかるので、適当なタイミングでリフレッシュした方がいいだろう。FPS=1というのが、ほぼ目安になると思う(FPS=10分とかにすると、膨大な量のイベントがキャッシュに溜まりかねない)。

プログラムが起動した直後に基礎的なイベントがたくさん記録され、「シーケンス」型で保管される(配列、あるいはリストのようなもの)。調べてみたらその数は12であった。どんなイベントがあるか表示させてみた。

import pygame

WDT=500; HGH=500; SCRsize=(WDT,HGH)
FPS=1

pygame.init()

screen = pygame.display.set_mode(SCRsize)
clock   = pygame.time.Clock()
event   = pygame.event.get()

screen.fill("green"); pygame.diplay.flip()
clock.tick(FPS)
print(event, len(event))

pygame.quit()

実行結果は次のとおり。

% time python3 pygame-min.py

pygame 2.6.1 (SDL 2.28.4, Python 3.12.3)
Hello from the pygame community. https://www.pygame.org/contribute.html

[<Event(4352-AudioDeviceAdded {'which': 0, 'iscapture': 1})>, <Event(4352-AudioDeviceAdded {'which': 1, 'iscapture': 1})>, <Event(4352-AudioDeviceAdded {'which': 0, 'iscapture': 0})>, <Event(4352-AudioDeviceAdded {'which': 1, 'iscapture': 0})>, <Event(32774-WindowShown {'window': None})>, <Event(32770-VideoExpose {})>, <Event(32776-WindowExposed {'window': None})>, <Event(32768-ActiveEvent {'gain': 1, 'state': 2})>, <Event(32785-WindowFocusGained {'window': None})>] 9

python3 pygame-min.py  0.12s user 0.08s system 11% cpu 1.746 total

実行時間は1秒と少し。この「少し」の部分はpythonシステム自体の起動に要する時間などである。したがって、FPS=0.5としても少数以下の部分はあまり変わらないはずである。実際に実験してみると、予想通り2.746となった。

記録されたイベントは9個であり、Audioシステム接続のイベントやビデオ関係のもの、そしてウィンドウ表示関連のものが記録されている。

次に、自分自身のイベントを表示させてみる。例として、ウィンドウに付属している「停止ボタン」(Windowsの場合は右上の「閉じるボタン」(赤いXボタン)、macOSの場合は左上の赤丸ボタン)を押して、そのイベントの記録の表示にチャレンジしてみよう。

FPS=1の場合、マウスポインターをウィンドウ角に移動させ、ボタンを選んでクリックするまでに5秒くらいは必要なので、forを用いて5回の「フレーム」を作ることにする。

import pygame

WDT=500; HGH=500; SCRsize=(WDT,HGH)
FPS=1

pygame.init()

screen = pygame.display.set_mode(SCRsize)
clock   = pygame.time.Clock()

for i in range(5):
    event   = pygame.event.get()
    screen.fill("green"); pygame.display.flip()
    clock.tick(FPS)
    print(event, len(event))

pygame.quit()

実行時間は5.763秒。予想通りである。実行結果の表示は次のとおり。

0 [] 0
1 [<Event(32768-ActiveEvent {'gain': 1, 'state': 1})>, <Event(32783-WindowEnter {'window': None})>, <Event(1024-MouseMotion {'pos': (86, 29), 'rel': (0, 0), 'buttons': (0, 0, 0), 'touch': False, 'window': None})>] 3
2 [<Event(1024-MouseMotion {'pos': (17, 0), 'rel': (-69, -40), 'buttons': (0, 0, 0), 'touch': False, 'window': None})>, <Event(32768-ActiveEvent {'gain': 0, 'state': 1})>, <Event(32784-WindowLeave {'window': None})>, <Event(32787-WindowClose {'window': None})>, <Event(256-Quit {})>] 5
3 [] 0
4 [] 0

実行してから最初のフレーム(0フレーム)には何のイベントも記録されていない。私が反応しきれなかったのである。第1フレームでマウスの動きが記録されている。そして第2フレームで256-Quit、つまり「閉じるボタン」が押されたことが記録された。私の反射神経だと実行してからボタンを押すまでに3秒が必要であることがわかった(笑)。

このようにイベントの記録を分析しながら、次にどういう動き、アニメーションをさせるかプログラムしていくのが「ゲームプログラミング」である。シューティングゲームなどではFPS=1のような「たるい」タイミングではイライラが募ることであろう。一方、オセロゲームのようなものであれば、FPS=1にした方がいいのかもしれない。コンピュータとしてはなるべく早く処理したいので、FPSは大きい方が嬉しいはずだが、人間の動きが追いついてくるまで「待たねば」ならないのである。ゲームというのは、実に人間よりの、コンピュータには気の毒なプログラムなのである....。

(つづく)