Pythonゲームプログラミング #3 アニメーション
gist消してしまったので
代わりにこのリポジトリを参考にしてください
#0 環境構築
#1 メインループ
#2 画像表示
#3 アニメーション
#4 キー入力
#5 サウンド
#6 衝突判定
#7 ステートマシン
#8 マップチップ
#9 スクロール
アニメーション
pygameを使ったゲームプログラミングについて説明していきます。
- Windows10
- Python3.6.1
- pygame1.9.3
今回は画像をアニメーションさせてみます。
アニメーションの仕組み
アニメーションは、パラパラ漫画のように異なる画像を少しずつ表示することによって、絵が動いているように見せています。
ゲームでは、歩きアニメーションなどの繰り返しがあるものと、爆発のように繰り返しのないものがあります。
画像の大きさや並びに法則性を持たせておけば、複雑なクラスを作成しなくても、簡単にアニメーションをおこなうことができます。
サンプルコード
今回はこのような画像を用意してみました。
実行結果
解説
表示する画像の座標指定
今回はキャラクターの画像を32ピクセルごとに横に並べているので、変数が1つあればパターンを切り替えることができます。
例えば左から3番目の画像を表示したい場合は下記のように書きます。
i = 2 # n番目 - 1 screen.blit(img_char, (0, 0), (32 * i, 0, 32, 32))
フレームカウント
10行目と35行目
frame = 0 # フレーム frame += 1
アニメーションをおこなうには今何フレーム目なのかをカウントしておく必要があります。メインループの最後でフレーム数をインクリメントします。
ループあり
24行目
loop_anim_index = int(frame / 3) % 5
フレームごとに変数を 0, 1, 2, 0, 1, 2 ... と変化させたい場合は「フレーム÷変化の長さ」の余剰を求めます。
x = frame % 3 # 0, 1, 2 を繰り返す
アニメーションの速度が速すぎる場合は、フレーム数を割って調整します。
x = int(frame / 2) % 3 # 2フレームごとに切り替わる
ループなし
28行目
anim_index = min(int(frame / 15), 5)
アニメーションをループさせたくない場合はmin関数などを使って、指定した数より大きくならないようにしておきます。
Pythonゲームプログラミング #2 画像表示
gist消してしまったので
代わりにこのリポジトリを参考にしてください
#0 環境構築
#1 メインループ
#2 画像表示
#3 アニメーション
#4 キー入力
#5 サウンド
#6 衝突判定
#7 ステートマシン
#8 マップチップ
#9 スクロール
画像表示
pygameを使ったゲームプログラミングについて説明していきます。
- Windows10
- Python3.6.1
- pygame1.9.3
今回は画像を表示してみます。英語が苦でない方はpygame公式ドキュメントのTutorialを読んだほうが良いでしょう。
画像の読み込みと表示
まずは画像ファイルを適当に用意します。pngなどで透過情報を付けておくとpygameでも透過してくれます。
とりあえず1キャラクターにつき幅32ピクセル、高さ32ピクセルで描いてみました。これを前回作成したmain.pyと同じフォルダに保存します。
main.pyを書き換えて画像を表示するプログラムを実行してみます
こんな感じで星とキノコが動けばOKです
解説
8行目
img_char = pygame.image.load('test.png') # 画像ファイルの読み込み
画像ファイルを読み込むとSurfaceクラスのインスタンスが作成されます。Surfaceクラスはpygameで画像を扱うためのクラスです。Surfaceクラスについての詳しい説明はドキュメントを読んでみてください。
24行目
screen.blit(img_char, (0, 0)) # 画像全体を表示
screenの座標(0, 0)にimg_charの画像全体をコピーしています。screenもSurfaceクラスです。
25行目
screen.blit(img_char, (x, 100), (64, 0, 32, 32)) # 画像の一部を表示
screenの座標(x, 100)にimg_charの座標(64, 0)から幅32px高さ32px分だけコピーしています。
27~34行目
# 画像の一部を回転させて表示 temp = pygame.Surface((32, 32), pygame.SRCALPHA) temp.blit(img_char, (0, 0), (32, 0, 32, 32)) temp = pygame.transform.rotate(temp, a) # 回転すると画像サイズが変化するので手動で中央寄せ offset_x = (32 - temp.get_width()) / 2 offset_y = (32 - temp.get_height()) / 2 screen.blit(temp, (160 + offset_x, 200 + offset_y))
pygame.transform関数を使って回転する星を表示してみました。img_char全体を回転させてから必要な部分を切り出すのは難しいので↓
あらかじめ表示したい部分を切り抜いてから回転させます。
Surfaceを作成するときにpygame.SRCALPHAフラグを指定すると透過情報付きのSurfaceを作成することができます。また、pygame.transformの他の関数を使えば拡大縮小や反転も可能です。詳しくはドキュメントを読んでみてください。
Pythonゲームプログラミング #1 メインループ
gist消してしまったので
代わりにこのリポジトリを参考にしてください
#0 環境構築
#1 メインループ
#2 画像表示
#3 アニメーション
#4 キー入力
#5 サウンド
#6 衝突判定
#7 ステートマシン
#8 マップチップ
#9 スクロール
メインループ
pygameを使ったゲームプログラミングについて説明していきます。
- Windows10
- Python3.6.1
- pygame1.9.3
今回はメインループを実装してみます。英語が苦でない方はpygame公式ドキュメントのTutorialを読んだほうが良いでしょう。
リアルタイムゲームのプログラミング
一般的なGUIアプリケーションはイベント駆動型。つまりボタン等をクリックしたときに何らかの処理がはじめて実行されるというものがほとんどです。
これに対してアクションゲームなどでは、プレイヤーが何も操作しなくてもゲーム全体の状況はどんどん変化します。
アニメーションは異なる絵をパラパラ漫画のように切り替えてものを動かしたりしますが、同じようにゲームでも、キャラクターの移動処理などを何度も繰り返しながらゲームの状況を変化させていきます。
この1コマ1コマのことを「フレーム」と呼び、メインループとはフレームの処理を繰り返すためのループになります。
PyCharmでプロジェクトを作る
前回インストールしたPyCharmを起動します
初回起動時に出てくるウインドウの「Create New Project」ボタンを押すか
メニューの「File -> New Project」からプロジェクトが作成できます
とりあえず「c:\pygame」フォルダ内に「mygame」プロジェクトを作成します
プロジェクトパネルのmygameを右クリックしてソースコードを追加します
これでプログラムを書く準備ができました
プログラムを書いて実行してみる
作成したmain.pyに次のプログラムを書いて実行してみます
書き終わったら「Run -> Debug...」でプログラムをデバッグ実行します
実行するソースコードを聞かれるのでmain.pyを選択します
青い画面のウインドウが表示されればOKです
その他の処理について補足
pygameの細かい仕様等については公式ドキュメント見てください。
フレームレート
フレームレートとはフレーム処理が1秒間に何回実行されるのかをあらわします。
ゲームスピードが固定されていないとキャラクターの移動速度などが変化してしまうなどの不都合がおこります。これを回避するためにフレームレートを一定に保ったり、キャラクターの移動速度や加速度をフレームレートに応じて変化させるといった処理が必要になります。
clock.tick(60)
本プログラムでは、Clockクラスを使用してフレームレートを固定しています。tick(60)は「前回の呼び出しから1/60秒経過するまで待つ」という意図になります。
イベント処理
アプリケーションは基本的にOS上で動作していて、OSからの命令(ウインドウの処理等)もアプリケーション側で正しく処理する必要があります。
for event in pygame.event.get(): if event.type == pygame.QUIT: end_game = True
アプリケーションの終了命令がきたらゲーム終了フラグをTrueにしています。このイベント処理をしないと×ボタン等に反応しなくなります。
バッファフリップ
ダブルバッファリングでは2つの画面を切り替えて処理することで、片方をウインドウに表示している間、プログラムは表示されていないほうの画面に描画処理をおこなうという処理を実現しています。
pygame.display.flip()
ダブルバッファリングが有効になっている場合、ウインドウに表示するバッファを切り替える処理を呼び出す必要があります。
※pygameではデフォルトでダブルバッファリングが有効になっているようです。
Pythonゲームプログラミング #0 環境構築
#0 環境構築
#1 メインループ
#2 画像表示
#3 アニメーション
#4 キー入力
#5 サウンド
#6 衝突判定
#7 ステートマシン
#8 マップチップ
#9 スクロール
環境構築
pygameを使ったゲームプログラミングについて説明していきます。
- Windows10
- Python3.6.1
- pygame1.9.3
python3をインストール
まずはPythonの公式サイトからpythonのインストーラをダウンロードします。画像のように Downloads -> Python 3.x.x を選択します。
ダウンロードが完了したらインストーラを実行し「Add Python 3.x to PATH」にチェックをいれて「Install Now」を押してください。画像は64bit版になっていますが32bit版でもたぶん大丈夫です。
インストールが終わったらpythonが正しくインストールできたか確認します。「Winキー + R」で「ファイル名を指定して実行」ウインドウを表示し、「cmd」と入力してEnterキーを押します。
コマンドプロンプトが起動するので「python -V」と打ちEnterキーを押します。Vは大文字です。インストールしたバージョンが表示されれば正常にインストールされています。
pygameをインストール
コマンドプロンプトに「pip install pygame」と打ちEnterキーを押します。
pygameのインストールが終わったら、もうコマンドプロンプトは使いませんので、「exit」と入力して終了します。
PyCharmをインストール
IDEは何でもいいのですがPyCharmがおすすめです。PyCharmの公式サイトからCommunity版をダウンロードします。
インストーラの指示に従ってインストールを完了させます。
インストールが正常に完了していれば「Winキー + S」の検索メニューで「pycharm」と入力すれば、プログラムが検索できるはずです。
PySide: Asynchronously Text Filtering
QThreadを使った非同期テキストフィルタ
ほしいもの
- 別スレッドで実行されるテキストフィルタ
- 一定時間ごとにフィルタリング済みのデータを返す
- 途中でキャンセルもしたい
コード
from PySide import QtCore, QtGui import datetime import time class FilteringThread(QtCore.QThread): completed = QtCore.Signal() def __init__(self): super().__init__() self._data = [] # フィルタリングする前のデータリスト self._filter = '' # 部分一致キーワード self._queue = [] # フィルタ済みのデータ self._lastFlushed = datetime.datetime.now() # 前回データを流した時間 self._callback = None # フィルタ済みデータを受け取る関数 self._complete = True # 全体の処理が完了したか判定する self._canceled = False # 途中キャンセルがリクエストされたか def setFilter(self, text): # フィルタの設定 self._filter = text def setData(self, data): # データの設定 self._data = data def setCallback(self, func): # データ受取関数の設定 self._callback = func def cancel(self): # キャンセルのリクエスト self._canceled = True def isCompleted(self): # 終了判定 # isFinishedがいい感じで動かない return self._complete def run(self): # スレッドから実行される関数 self._complete = False self._canceled = False if self._filter: for d in self._data: if self._canceled: break time.sleep(0.001) # フィルタリングに時間がかかってるふり if self._filter in d: # フィルタを追加したらqueueにいれる self._queue.append(d) # 一定時間経過してたら流す now = datetime.datetime.now() if (now - self._lastFlushed).total_seconds() >= 1.0: self._callback(self._queue) self._queue.clear() self._lastFlushed = now # 最後に残ったデータを流して終了 self._callback(self._queue) self._queue.clear() self.completed.emit() self._complete = True self.exit() # 実験用データ DATA = [str(i) for i in range(0, 1000)] # 必要なインスタンスを作る app = QtGui.QApplication([]) listview = QtGui.QListWidget() filtertext = QtGui.QLineEdit() thread = FilteringThread() message = QtGui.QLabel('') # フィルタ変更時の処理 def textChanged(text): while not thread.isCompleted(): thread.cancel() time.sleep(0.1) listview.clear() if text == '': return thread.setFilter(text) thread.start() message.setText('') # フィルタ項目取得時 def flushed(queue): for i in queue: listview.addItem(QtGui.QListWidgetItem(i)) # 開始時 def started(): message.setText('filteringなう...') # 終了時 def completed(): message.setText('{0} matching records'.format(listview.count())) # 設定 listview.setUniformItemSizes(True) # ちょっぴり速くなるよ listview.setSortingEnabled(False) # もしソートするならflushedで一回だけやる filtertext.textChanged.connect(textChanged) thread.setData(DATA) thread.setCallback(flushed) thread.started.connect(started) thread.completed.connect(completed) # 実行 widget = QtGui.QWidget() widget.setLayout(QtGui.QVBoxLayout()) layout = widget.layout() layout.addWidget(filtertext) layout.addWidget(listview) layout.addWidget(message) widget.show() app.exec_()
表示するデータがあまりに多くなると今度はListWidgetのclearやらpaintやらで時間がかかってしまうようになるっぽい。これはどうしたものか…
PySide: Save and Restore QDockWidgets
Dockの生成削除込みでQSettings保存
適当に調べた
- QSettingsを使用してWidgetの位置やサイズを保存できる
- 復元する前にWidgetを生成してobjectNameを設定しておく必要がある
- DockWidgetを追加・削除できるプログラムではその状況も保存する必要がある
- QMainWindow::restoreDockWidget関数はコレジャナイ感
- あっ、もしかしてDockWidgetの復元は自分でやらないといけない?
必要な処理
初期化時に設定ファイルがあったら前回の内容を復元
- 前回生成されていたDockWidgetの復元
- restoreGeometry
- restoreState
終了時に設定ファイルを保存
- 全部のDockWidgetの型とobjectName(とりあえず毎回上書き)を保存
- saveGeometry
- saveState
コードこんな感じ
class MainWindow(QtGui.QMainWindow): def __init__(self): # 復元 if os.path.exists(path_to_ini): s = QtCore.QSettings(path_to_ini, QtCore.QSettings.IniFormat) self._restoreDockWidgets(s.value('dockwidgets')) self.restoreGeometry(s.value('geometry')) self.restoreState(s.value('state')) def closeEvent(self, event): # 保存 s = QtCore.QSettings(path_to_ini, QtCore.QSettings.IniFormat) s.setValue('dockwidgets', self._saveDockWidgets()) s.setValue('geometry', self.saveGeometry()) s.setValue('state', self.saveState()) super().closeEvent(event) def createDockWidget(self, widget_type) -> QtGui.QDockWidget: # 渡されたWidgetをDockにいれて返す dock = QtGui.QDockWidget() self.addDockWidget(QtCore.Qt.LeftDockWidgetArea, dock) dock.setWidget(widget_type()) dock.setWindowTitle(dock.widget().windowTitle()) dock.setFloating(True) return dock def _saveDockWidgets(self): # DockWidgetにユニークな名前を付けて型名と一緒に保存 # 実際に保存する型名はDockWidgetに設定されたWidgetのもの a = [x for x in self.children() if isinstance(x, QtGui.QDockWidget)] for x in a: x.setObjectName(str(uuid.uuid1())) # 保存されるデータは "型名:名前,型名:名前..." になる return ','.join( [type(x.widget()).__name__ + ':' + x.objectName() for x in a]) def _restoreDockWidgets(self, value): # 設定ファイルから型と名前を取得してDockWidgetを復元 # ここではwidgetsモジュール内にDock化される型をまとめている前提 for t, name in [x.split(':') for x in value.split(',')]: if hasattr(widgets, t): d = self.createDockWidget(getattr(widgets, t)) d.setObjectName(name)
他に良い方法知ってたら教えてください
PySide: Suppress the clicked signal when emitting a contextMenuRequested signal
右クリックでコンテキストメニューを出すときに一緒にclickedシグナルが発信されると困るケースがあった。mousePressEventとmouseReleaseEventを継承して左クリックのみに反応するようにしてこれを回避した
from PySide import QtGui, QtCore import sys class CustomListWidget(QtGui.QListWidget): def __init__(self): super().__init__() self.setContextMenuPolicy(QtCore.Qt.CustomContextMenu) self.clicked.connect(lambda: print('clicked')) self.customContextMenuRequested.connect(self._showContextMenu) self.addItem(QtGui.QListWidgetItem('hoge')) self.addItem(QtGui.QListWidgetItem('fuga')) self.addItem(QtGui.QListWidgetItem('piyo')) def mousePressEvent(self, event): # 左クリックのときだけ反応させる if event.button() == QtCore.Qt.LeftButton: super().mousePressEvent(event) def mouseReleaseEvent(self, event): # 選択状態のアイテムにはReleaseEventのフックも必要 # 左クリックのときだけ反応させる if event.button() == QtCore.Qt.LeftButton: super().mouseReleaseEvent(event) def _showContextMenu(self, pos): item = self.itemAt(pos) if not item: return menu = QtGui.QMenu() menu.addAction(QtGui.QAction(item.text(), self)) menu.exec_(self.mapToGlobal(pos)) app = QtGui.QApplication(sys.argv) w = CustomListWidget() w.show() sys.exit(app.exec_())