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_())
PySide リソース一覧の取得
qrcファイルの中身がこんな感じだったら
<!DOCTYPE RCC><RCC version="1.0"> <qresource> <file>folder/hogehoge.txt</file> <file>folder/fugafuga.bin</file> <file>folder/piyopiyo.jpg</file> </qresource> </RCC>
こんな感じでpyに変換すると思うのですが
pyside-rcc -o resource.py -py3 resource.qrc
こんな感じで一覧を取得できます
from PySide import QtCore import resource for x in QtCore.QDir(':folder').entryList(): print(x)
実行結果
fugafuga.bin hogehoge.txt piyopiyo.jpg
アイコンとかまとめて作るときはこんな感じで
icons = { os.path.splitext(x)[0]: QtGui.QIcon(':/icon/{0}'.format(x)) for x in QtCore.QDir(':icon').entryList() }
QTreeWidgetItemでアイコン複数表示
HTMLを表示するdelegateを用意するのが手っ取り早い
コードはここらへんを参考に
PyQt版の回答をゴニョゴニョして作成
import sys from PySide import QtCore, QtGui class RichTextDelegate(QtGui.QStyledItemDelegate): def paint(self, painter, option, index): option_v4 = QtGui.QStyleOptionViewItemV4(option) self.initStyleOption(option_v4, index) doc = self.make_text(option_v4.text) option_v4.text = "" style = QtGui.QApplication.style() style.drawControl(QtGui.QStyle.CE_ItemViewItem, option_v4, painter) painter.save() rect = style.subElementRect(QtGui.QStyle.SE_ItemViewItemText, option_v4) painter.translate(rect.topLeft()) painter.setClipRect(rect.translated(-rect.topLeft())) context = QtGui.QAbstractTextDocumentLayout.PaintContext() if option_v4.state & QtGui.QStyle.State_Selected: context.palette.setColor( QtGui.QPalette.Text, option.palette.color(QtGui.QPalette.Active, QtGui.QPalette.HighlightedText)) doc.documentLayout().draw(painter, context) painter.restore() def sizeHint(self, option, index): option_v4 = QtGui.QStyleOptionViewItemV4(option) self.initStyleOption(option_v4, index) doc = self.make_text(option_v4.text) return QtCore.QSize(doc.idealWidth(), doc.size().height()) def make_text(self, text): doc = QtGui.QTextDocument() doc.setDocumentMargin(1) f = QtGui.QTreeWidgetItem().font(0) f.setPointSize(11) doc.setDefaultFont(f) doc.setHtml(text) return doc app = QtGui.QApplication(sys.argv) tree = QtGui.QTreeWidget() tree.setHeaderHidden(True) tree.setItemDelegate(RichTextDelegate()) hoge = QtGui.QTreeWidgetItem() hoge.setText(0, '<i>hoge</i> <img src="hoge.png"/><img src="hoge.png"/>') fuga = QtGui.QTreeWidgetItem() fuga.setText(0, '<b>fuga</b> <img src="hoge.png"/><img src="hoge.png"/><img src="hoge.png"/>') piyo = QtGui.QTreeWidgetItem() piyo.setText(0, '<font color="red">piyo</font>') hoge.addChild(fuga) hoge.addChild(piyo) tree.addTopLevelItem(hoge) tree.show() sys.exit(app.exec_())