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_())
Raspberry Piでトイレにクラシック音楽を流す
人がきたら音楽を流す装置をトイレに設置しました(割と好評)
- ケースはRaspberry Piの箱に100均で買った折り紙を貼って作成
- 本体のUSBでBluetoothスピーカーを充電
必要なもの
Raspberry Piと人感センサーの接続
下記ページを確認しながらRaspberry Piと人感センサーを接続
- https://developer.microsoft.com/en-us/windows/iot/docs/pinmappingsrpi
- https://www.mpja.com/download/31227sc.pdf
対応するピン同士をジャンパーワイヤで繋ぐ
人感センサー | Raspberry Pi | |
---|---|---|
+Power | <---> | 5V PWR |
GND | <---> | GND |
High/Low Output | <---> | GPIO18 |
人感センサーで音を鳴らすプログラムの作成
import pygame, os, random, time, RPi.GPIO pygame.init() pygame.mixer.pre_init(44100, 16, 2, 1024 * 4) pygame.mixer.init() pygame.mixer.set_num_channels(8) screen = pygame.display.set_mode((320, 240)) shutdown = False clock = pygame.time.Clock() files = [] active = False last_activated = 0 pin = 18 RPi.GPIO.setmode(RPi.GPIO.BCM) RPi.GPIO.setup(pin, RPi.GPIO.IN) # 入力を検知したあとに何秒間流すか timeout = 30 # 音楽の再生 def play_random_track(): global files # 再生リストがなくなったらmp3ファイルを検索してシャッフル if not files: files = [f for f in os.listdir('./') if f.endswith('.mp3')] random.shuffle(files) # 再生リストから一番最初の音楽をとってきて鳴らす pygame.mixer.music.load(files.pop(0)) pygame.mixer.music.set_volume(1.0) pygame.mixer.music.play() # メインループ while not shutdown: # 入力フラグ active = False # OSイベント処理 for e in pygame.event.get(): # 閉じるボタンやESCキーが入力されたらプログラムを終了する if e.type == pygame.QUIT or (e.type == pygame.KEYDOWN and e.key == 27): shutdown = True break # テスト用:Enterキーで音を鳴らせるようにしておく elif e.type == pygame.KEYDOWN and e.key == 13: active = True continue # 人感センサーからの入力 if RPi.GPIO.input(pin) == RPi.GPIO.HIGH: active = True # 入力があったときの処理 if active: # 入力時間を更新する last_activated = time.time() # 再生してないときは次のトラックを再生 if not pygame.mixer.music.get_busy(): play_random_track() # 最後の入力から一定時間(timeout秒)が経過したら音楽をフェードアウトさせる if (time.time() - last_activated) > timeout: if pygame.mixer.music.get_busy(): pygame.mixer.music.fadeout(5000) pygame.display.flip() clock.tick(5) # 処理負荷を抑える(1秒間に5回ループをまわす) RPi.GPIO.cleanup() pygame.quit()
電源入れたら自動でプログラムが動くようにする
以前の記事と同じ手順で起動時にBluetoothスピーカー接続&今回用意したスクリプトを起動するように修正
pulseaudio -D sleep 5 # 適当にウェイト bluetoothctl << EOF power on connect FF:FF:FF:FF:FF:FF # BluetoothスピーカーのMACアドレス quit EOF sleep 5 # 適当にウェイト pacmd set-sink-volume 1 32767 # 音量調節(デバイス番号、音量~65565) cd otohime python3.4 otohime.py