Slide 1

Slide 1 text

1/91

Slide 2

Slide 2 text

自己紹介 • 株式会社 Cygames Cyllista Game Engine シニアゲームエンジニア 沖 幸太朗 • 大手ゲーム開発会社でコンシューマゲーム開発に従事し, リードプログラマおよびクリエイティブディレクターとしてチームを牽引。 2016年より株式会社 Cygames に所属し, 内製ゲームエンジン「Cyllista Game Engine」の開発サブリーダーとして, アセットシステム・サウンドシステムを中心に開発を進めている。 2/91

Slide 3

Slide 3 text

はじめに 3/91

Slide 4

Slide 4 text

ゲームエンジンの目標 4/91 最高のゲームエンジン ハードウェアが 最高のパフォーマンスを 出せる ゲーム開発者が 最高のパフォーマンスを 出せる

Slide 5

Slide 5 text

ゲームエンジンの目標 5/91 ゲーム開発者が最高のパフォーマンスを出せる イテレーションを速く回す! ツールの利便性を上げる! そのためには…

Slide 6

Slide 6 text

利便性の高いツールを作るには… 6/91 • ツール開発者の効率を上げる! • 多種多様な技術を積極的に取り入れる! • ツールの配布を容易にする!

Slide 7

Slide 7 text

Python とは 7/91

Slide 8

Slide 8 text

Python 8/91 • 汎用の高水準プログラミング言語。 関数型,オブジェクト指向,動的型付けなどの 特徴を持つ。 – https://www.python.org/ • 本体部分は必要最小限の機能のみ。 パッケージという形で様々な機能が提供される。 • 世界で広く使われているプログラミング言語の1つ。 – IEEE Spectrum で3年連続1位 (2017-2019)

Slide 9

Slide 9 text

Python の強み1 -動的(対話型)- 9/91 • コンパイル・リンクせずにすぐに動作テストが可能。 • 対話モード(REPL)を利用することにより,関数の評価などを気軽に行える。

Slide 10

Slide 10 text

Python の強み2 -充実したパッケージ- 10/91 • 2020年8月20日現在,257,375 種類のパッケージが提供されている。 – 最近ではディープラーニング系が多く提供されている。 • サードパーティー製のパッケージは pypi からインストールできる。 – https://pypi.org/ pip install (package_name)

Slide 11

Slide 11 text

Python の IDE (統合開発環境) 11/91 • PyCharm が非常に便利 – https://www.jetbrains.com/ja-jp/pycharm/ – コード補完・検査やリアルタイムでのエラー指摘など,非常に優秀 – コード検索も非常に高速 – 有償版では… • 細かな負荷計測 • Perforce 連携 • プロセスアタッチによるデバッグ etc... • このようなツールを使っている人も – Visual Studio Code – Vim

Slide 12

Slide 12 text

Python の弱点を知る 12/91 • Python は非常に便利なプログラミング言語。 • だけど弱点も存在する。 弱点を知ることで, Python を効率良く使いこなす!

Slide 13

Slide 13 text

Python の弱点1 -速度の遅さ- 13/91 • Python では単純な処理でも負荷がかかることがある。 • 特に大量の計算が発生する処理が遅い。 – 動的型付けのため,単純な計算でも型チェックが入ってしまう。 • 大量の計算を行うのであれば NumPy パッケージが有効。 • 複雑な処理を行う場合は pyd が有効。

Slide 14

Slide 14 text

NumPy 14/91 • 数値計算を高速に行うためのパッケージ。 – ディープラーニング系では必須なパッケージ。 • 多次元配列 (ndarray) – 四則演算 – 行列積 – 統計量 (平均・分散・標準偏差・最大値・最小値など) etc... pip install numpy

Slide 15

Slide 15 text

NumPy (行列積演算の時間比較) 15/91 • 例えば N 次元行列同士の行列積を出してみる。 – a と b の行列積を c に代入する。 import random a = [[random.random()] * N for i in range(N)] b = [[random.random()] * N for i in range(N)] c = [[0] * N for i in range(N)]

Slide 16

Slide 16 text

NumPy (行列積演算の時間比較) 16/91 • NumPy を使わない場合 N=100 : 0.2[s] N=200 : 1.8[s] • NumPy を使う場合 N=100 : 0.003[s] N=200 : 0.007[s] for i in range(N): for j in range(N): for k in range(N): c[i][j] = a[i][k] * b[k][j] import numpy as np c = np.dot(a, b)

Slide 17

Slide 17 text

pyd 対応 17/91 • pyd とは Python モジュールとして 利用できる DLL のこと。 – C 言語でコーディングできるので高速。 – Python.h をインクルード。 • https://docs.python.org/ja/3/extending/ • 右のコードで作った sample.pyd は Python コードからインポート可能。 – 加算処理を行う add() を提供。 #include int add(int x, int y) { return x + y; } PyObject* pythonAdd(PyObject* self, PyObject* args) { int x, y, g; if (!PyArg_ParseTuple(args, "ii", &x, &y)) { return nullptr; } else { g = add(x, y); return Py_BuildValue("i", g); } } static PyMethodDef pythonMethods[] = { {"add", pythonAdd, METH_VARARGS}, {NULL}, }; static PyModuleDef pythonModule = { PyModuleDef_HEAD_INIT, "sample", "Sample Module", 0, pythonMethods, }; PyMODINIT_FUNC PyInit_sample() { return PyModule_Create(&pythonModule); } import sample sum = sample.add(1, 2) # sum = 3

Slide 18

Slide 18 text

pyd 対応 (モジュール定義) 18/91 • モジュール名を name としたときに, PyInit_name という名前の初期化関数を 用意する必要がある。 • PyModuleDef 構造体でモジュールの 定義を設定する。 #include int add(int x, int y) { return x + y; } PyObject* pythonAdd(PyObject* self, PyObject* args) { int x, y, g; if (!PyArg_ParseTuple(args, "ii", &x, &y)) { return nullptr; } else { g = add(x, y); return Py_BuildValue("i", g); } } static PyMethodDef pythonMethods[] = { {"add", pythonAdd, METH_VARARGS}, {NULL}, }; static PyModuleDef pythonModule = { PyModuleDef_HEAD_INIT, "sample", "Sample Module", 0, pythonMethods, }; PyMODINIT_FUNC PyInit_sample() { return PyModule_Create(&pythonModule); }

Slide 19

Slide 19 text

pyd 対応 (関数定義) 19/91 • Python コードからアクセスできる関数に ついて,入力として2つの PyObject*, 出力として PyObject* を返す関数を定義。 • 右の例では,args から引数をパースし, その値を実処理 (add()) に渡し, 結果を PyObject* に変換して返している。 #include int add(int x, int y) { return x + y; } PyObject* pythonAdd(PyObject* self, PyObject* args) { int x, y, g; if (!PyArg_ParseTuple(args, "ii", &x, &y)) { return nullptr; } else { g = add(x, y); return Py_BuildValue("i", g); } } static PyMethodDef pythonMethods[] = { {"add", pythonAdd, METH_VARARGS}, {NULL}, }; static PyModuleDef pythonModule = { PyModuleDef_HEAD_INIT, "sample", "Sample Module", 0, pythonMethods, }; PyMODINIT_FUNC PyInit_sample() { return PyModule_Create(&pythonModule); }

Slide 20

Slide 20 text

pyd 対応 20/91 • …とにかく難易度が高い! – 2つの変数を加算するだけの単純な処理で これだけのコードを書かなければならない。 – PyObject の扱いが難しい。 • 参照カウントを自分で操作する必要があり, 誤るとクラッシュの原因となる。 pybind11 を使う!

Slide 21

Slide 21 text

pybind11 21/91 • 直感的に pyd の C コードが書ける。 – C の関数を直接指定すれば,勝手に Python コード用の定義を作ってくれる。 – PyObject の参照カウントを自動的に制御してくれる。 – C と Python の型を自動変換してくれる。 • str ⇔ std::string, std::string_view • list ⇔ std::vector • set ⇔ std::unordered_set • dict ⇔ std::unordered_map • C++11 以上が必要 • https://pybind11.readthedocs.io/en/stable/

Slide 22

Slide 22 text

pybind11 22/91 #include int add(int x, int y) { return x + y; } PyObject* pythonAdd(PyObject* self, PyObject* args) { int x, y, g; if (!PyArg_ParseTuple(args, "ii", &x, &y)) { return nullptr; } else { g = add(x, y); return Py_BuildValue("i", g); } } static PyMethodDef pythonMethods[] = { {"add", pythonAdd, METH_VARARGS}, {NULL}, }; static PyModuleDef pythonModule = { PyModuleDef_HEAD_INIT, "sample", "Sample Module", 0, pythonMethods, }; PyMODINIT_FUNC PyInit_sample() { return PyModule_Create(&pythonModule); } #include int add(int x, int y) { return x + y; } PYBIND11_MODULE(sample, m) { m.doc() = "Sample Module"; m.def("add", &add); }

Slide 23

Slide 23 text

Python の弱点2 -並列処理に弱い- 23/91 • Python には GIL (Global Interpreter Lock) が存在する – 排他ロック。ロックを持つスレッドのみ実行可能。 ロックを持っていないスレッドはロックが開放されるまで待たされる。 – async などを利用することで,ファイルI/O 等との並列は可能ではあるが, 原則実行されているのは1スレッドのみ。 • multiprocessing モジュールでプロセス分離 – プロセス間は Pipe 通信もしくは共有メモリが利用できる。 (共有メモリは Python 3.8 以降のみ対応)

Slide 24

Slide 24 text

高速化事例 -ジョブシステム- 24/91 • 同時に複数の Python のコードを実行したい。 – (例) Python で書かれたコンバータで,大量のアセットを並列にコンバートしたい。 • Python 上で動くジョブシステムを作ってみた。 – 1つ以上のジョブをジョブシステムに投入すると,ジョブエグゼキュータが 並列に処理していく。

Slide 25

Slide 25 text

ジョブシステムフロー 25/91 常駐プロセスとして,1つのExecutor Server を立てる。

Slide 26

Slide 26 text

ジョブシステムフロー 26/91 Executor Server 内に並列処理するための スレッドとして,Executor Thread を 立てる。

Slide 27

Slide 27 text

ジョブシステムフロー 27/91 Executor Thread が Executor Process を1つずつ立ち上げる。

Slide 28

Slide 28 text

ジョブシステムフロー 28/91 様々なツールプロセスが自由に立ち上がる。

Slide 29

Slide 29 text

ジョブシステムフロー 29/91 ツールのプロセスから Executor Server に ジョブを投入するために,Executor Client をツールプロセス内に作成して接続する。

Slide 30

Slide 30 text

ジョブシステムフロー 30/91 各ツール内でジョブを作成して投入する。 一度に投入するジョブの数は自由。

Slide 31

Slide 31 text

ジョブシステムフロー 31/91 Executor Server にジョブが渡ると, 空いている Executor Thread にジョブが 投入される。

Slide 32

Slide 32 text

ジョブシステムフロー 32/91 Executor Thread から Executor Process にジョブを渡し,Executor Process 内で ジョブの実行が開始される。

Slide 33

Slide 33 text

ジョブシステムフロー 33/91 プロセスが別々なので,ジョブの実行は並列 に行われる。

Slide 34

Slide 34 text

ジョブシステムフロー 34/91 ジョブの実行結果が Executor Process か ら Executor Thread に戻される。

Slide 35

Slide 35 text

ジョブシステムフロー 35/91 投入元のツールプロセス内の Executor Client にジョブの実行結果が戻される。

Slide 36

Slide 36 text

ジョブシステムフロー 36/91 ジョブの実行を終えたら,Executor 内の ジョブの情報はクリアされて,次のジョブが 投入されるのを待つ。

Slide 37

Slide 37 text

ジョブシステム サンプルコード 37/91 Job クラス ・ジョブの基底クラス。 ・do_job 関数をオーバーライド してジョブの実行内容を書く。 ・ジョブ実行時に渡す情報は JobContext クラスを利用する。 class JobStatus(object): UNEXECUTED = 0 # SUCCEEDED = 1 # ( ) FAILED = 2 # ( ) class JobContext(object): def __init__(self, *args) -> None: # ... class Job(object): def __init__(self) -> None: self._status: int = JobStatus.UNEXECUTED @property def status(self) -> int: return self._status def do_job(self, context: JobContext) -> bool: # raise NotImplementedError

Slide 38

Slide 38 text

ジョブシステム サンプルコード 38/91 Executor Process ・multiprocessing.Queue を 利用してサーバーとの送受信を 行っている。 ・Job インスタンスが受信されたら それを実行し,結果を送信する。 class ExecutorProcess(object): def __init__(self, s2c_queue: multiprocessing.Queue, c2s_queue: multiprocessing.Queue) -> None: self._s2c_queue: multiprocessing.Queue = s2c_queue self._c2s_queue: multiprocessing.Queue = c2s_queue def run(self) -> None: while True: try: job = self._s2c_queue.get() self._exec_job(job) except (EOFError, BrokenPipeError): break except Exception as e: print(str(e)) def _exec_job(self, job: Job) -> None: context = JobContext(...) try: result = job.do_job(context) except Exception: print(str(e)) result = False job.status = JobStatus.SUCCEEDED if result else JobStatus.FAILED self._c2s_queue.put(job) def _process_main(s2c_queue: multiprocessing.Queue, c2s_queue: multiprocessing.Queue) -> None: executor = ExecutorProcess(s2c_queue, c2s_queue) executor.run()

Slide 39

Slide 39 text

ジョブシステム サンプルコード 39/91 Executor Thread ・multiprocessing.Process で Executor Process を立ち上げる。 その際に送受信用の Queue を 渡している。 ・Job がキューされたらコピーした Job を送信する。 ・スレッドのループで受信を待ち, 結果を受け取る。 class ExecutorThread(threading.Thread): def __init__(self) -> None: self._s2c_queue: multiprocessing.Queue = multiprocessing.Queue() self._c2s_queue: multiprocessing.Queue = multiprocessing.Queue() self._process: Optional[multiprocessing.Process] = None self._ready: bool = False self._result_job: Optional[Job] = None def run(self) -> None: self._process = multiprocessing.Process(target=_process_main, args=(self._s2c_queue, self._c2s_queue,), daemon=True) self._process.start() self._ready = True while True: try: job = self._c2s_queue.get() self._done_job(job) except Exception as e: print(str(e)) def queue_job(self, job: Job) -> None: self._result_job = None if not self._ready: return self._ready = False self._s2c_queue.put(copy.deepcopy(job)) def release_job(self) -> None: if self._ready: return self._ready = True self._result_job = None def _done_job(self, job: Job) -> None: self._result_job = job

Slide 40

Slide 40 text

ジョブ内からのジョブ実行 40/91 • ジョブの中から複数のジョブを実行したいときがある。 – (例) アセットのコンバート中に,他の複数のアセットのデータ取得のためにコンバート が必要になったとき。 – 全ての Executor Process 内で上記の状況になると,デッドロック状態になってしまう。 • Executor Process の数を自動的に増減できるようにする。 – ジョブ内からジョブを投入しようとしたとき,一定時間以上 Executor Process の空き が見つからなければ,Executor Process を自動的に増加させてそれを利用する。 – 増加させた Executor Process は不要になったタイミングで破棄する。

Slide 41

Slide 41 text

Cyllista Game Engine とは 41/91

Slide 42

Slide 42 text

No content

Slide 43

Slide 43 text

No content

Slide 44

Slide 44 text

Cyllista Game Engine 内製統合型エンジン ランタイム + エディタ 44/91 ハイエンドコンソール向け

Slide 45

Slide 45 text

プログラミング言語の統一 45/91 Cyllista Game Engine の ツール関連は全て Python で 書いています。

Slide 46

Slide 46 text

46/91 プログラミング言語の統一

Slide 47

Slide 47 text

Python に統一した理由 47/91 • 言語を統一することにより,学習コストを下げる。 • 対話型なので,開発イテレーションが速い。 • サードパーティー製のパッケージが簡単に導入できるため, 多種多様な技術をすぐに試用することが出来る。 • Python に関する情報が溢れているため,調査コストが低い。

Slide 48

Slide 48 text

cy モジュール 48/91 • Cyllista Game Engine の Python ソースコードは, ツール起動用のコード以外は全て cy モジュールとして登録。 • cy モジュール例 – cy.asset : アセットシステム – cy.ed : GUI コード – cy.log : ログ – cy.perforce : Perforce 制御 – cy.util : ユーティリティ from cy import log log.info(’Information’)

Slide 49

Slide 49 text

ツール配布方法 49/91 • ツールの配布は Perforce で行っている。 – ツール利用者は Python のソースコードを sync するだけで更新が適用される。

Slide 50

Slide 50 text

ランタイム(実機)との連携 50/91

Slide 51

Slide 51 text

エディタ 51/91 • エディタからランタイムアプリケーションが起動。 – ランタイムの画面を使って,編集などを行う。 • ランタイムはよくクラッシュする! – ランタイムがクラッシュしたら,編集したものが消えてしまう… – またやり直しだ… 否! クラッシュしても編集内容が 消えないようにする!

Slide 52

Slide 52 text

システム構成図 52/91

Slide 53

Slide 53 text

サーバー 53/91 • 独立したプロセス。 – Python で作成。 • 編集状態を所持。 – Undo/Redo 等の情報も。 • クライアントから送信された変更内容 を他の全てのクライアントに リアルタイムに通知。 サーバープロセスは強固に。 安定性を最優先とする!

Slide 54

Slide 54 text

クライアント 54/91 • ランタイムやエディタに含まれる。 – ランタイム : C++ – エディタ : Python • ランタイム / エディタ共にクラッシュ することが多々あるが,サーバーに 再接続したときにサーバーが情報を 保持しているので,それを復元する。

Slide 55

Slide 55 text

サーバー・クライアント通信 55/91 • サーバー・クライアント間は TCP で通信する。 – コンソール機とも通信する必要があるため,Pipe 通信は使えない。 – パケットロスさせないように,UDP ではなく TCP。 • Python 上で TCP 通信を組む場合は,socket / select モジュールで 実装すると良い。

Slide 56

Slide 56 text

サーバー・クライアント通信 56/91 サーバースレッド クライアントスレッド import socket import select _LISTEN_BACKLOG = 10 _SELECT_TIMEOUT = 0.1 server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) server_socket.bind(('', self._port)) server_socket.listen(_LISTEN_BACKLOG) sockets = {server_socket} while self._running: read_sockets = select.select(sockets, [], [], _SELECT_TIMEOUT)[0] for s in read_sockets: if s is server_socket: connection, address = s.accept() sockets.add(connection) # ... else: # ... for s in sockets: s.shutdown(socket.SHUT_RDWR) s.close() import socket _SOCKET_TIMEOUT = 0.1 client_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) client_socket.settimeout(_SOCKET_TIMEOUT) client_socket.connect((hostname, portnumber)) while self._running: try: # ... except EnvironmentError: continue except Exception: break client_socket.shutdown(socket.SHUT_RDWR) client_socket.close()

Slide 57

Slide 57 text

外部ツールとの連携 57/91

Slide 58

Slide 58 text

外部ツールにおける Python 58/91 • Python で操作することができる外部ツール – Shotgun – Wwise • Python が中に組み込まれている外部ツール – Maya – Houdini

Slide 59

Slide 59 text

Python で操作する外部ツール 59/91 • pip を利用して各ツール用に用意されたパッケージをインストールする。 – Shotgun – Wwise pip install git+https://github.com/shotgunsoftware/python-api.git pip install waapi

Slide 60

Slide 60 text

Python が組み込まれている外部ツール 60/91 • Maya – Version.2.7 • Houdini – Version.2.7 • 最新の Python – Version.3.8 • Cyllista Game Engine – Version.3.7 Python 2系 Python 3系 (EOL)

Slide 61

Slide 61 text

Python 2系 と 3系 61/91 • 2系 と 3系 は言語仕様がいろいろ異なる。 – 2系 のソースコードは 3系 の Python で実行できないことがある。 • print 構文の違い。 • 文字列の扱いの違い。 etc... – 2to3 を利用することによって,2系 のソースコードを 3系 仕様に変換可能。 • https://docs.python.org/ja/3/library/2to3.html Cyllista Game Engine の cy モジュールを Maya 上で直接利用したいけれど,出来ない!

Slide 62

Slide 62 text

解決方法 62/91 • Python 2系と3系を跨ぐ場合は,subprocess モジュール等で 別のプロセスを立ててしまうのが良い。 – exe や bat を起動する。 – 起動引数や一時ファイルで情報を渡す。

Slide 63

Slide 63 text

GUI 開発について 63/91

Slide 64

Slide 64 text

Qt for Python (PySide2) 64/91 • Qt とは – クロスプラットフォームアプリケーションフレームワーク – https://www.qt.io/jp • Qt for Python とは – Qt の Python バインディング (Qt 公式) – https://doc.qt.io/qtforpython – Maya や Houdini にも導入されている pip install PySide2

Slide 65

Slide 65 text

最小限コード 65/91 from PySide2 import QtWidgets if __name__ == '__main__': application = QtWidgets.QApplication(sys.argv) label = QtWidgets.QLabel('Hello World') label.show() application.exec_()

Slide 66

Slide 66 text

Qt for Python の良い点 66/91 • 基本的な GUI コンポーネントや各種システムが用意されているため, 少ない手数で扱える。 – メッセージングシステム – ドッキングシステム – ショートカットシステム • Python で書けるので効率が良い。 – コンパイルする必要がないので,GUI の調整⇔動作確認が非常に速い。 – 豊富なパッケージと組み合わせて利用することが出来る。 • Maya 等の DCC ツールでも採用されているため,ノウハウの共有が出来る。 – DCC ツールは Python 2系なので,コードの共有は難しいかもしれないが…

Slide 67

Slide 67 text

Qt for Python のあまり嬉しくない点 67/91 • ドキュメントが不足している。 – 引数の型が書かれていないため,C++ 用のドキュメントで確認する必要がある。 • クラッシュしたときにデバッグがしづらい。 – クラッシュの原因が Qt 内部だった場合に,原因の特定が非常に難しい。 • Qt for Python ならではの不具合が存在する。 – 特に困ったのが @QtCore.Slot デコレータを設定すると,特定の条件下でメモリ破壊が 発生することがあるという不具合。 • https://bugreports.qt.io/browse/PYSIDE-249 – Cyllista Game Engine では @QtCore.Slot デコレータは使用禁止に。

Slide 68

Slide 68 text

GUI 事例 -グラフビューワ- 68/91 • エフェクトグラフやアセット依存関係図などで使用。 • Qt Graphics View Framework を利用。 – 長方形や曲線など,様々な 2D アイテムを制御することが出来る。アイテムはクリック 等の各種イベントを扱うことが出来,マウスの動きも追跡することが出来る。

Slide 69

Slide 69 text

GUI 事例 -ビューポート- 69/91 • ランタイムの画面を ウィジェット内に埋め込んでいる。 • 別プロセスのウィンドウを容易に ウィジェットに埋め込み可能。 • マウスイベントの扱いは要注意。 – 別プロセスのウィンドウ上のマウスイベントは埋め込み先に通知されない。 – 別プロセス上でコンテキストメニュー等を表示したい場合は nativeEvent() で Windows のメッセージを拾って制御する。 window_wrapper = QtGui.QWindow.fromWinId(hwnd) window_widget = QtWidgets.QWidget.createWindowContainer(window_wrapper) self.layout().addWidget(window_widget)

Slide 70

Slide 70 text

GUI 事例 -アセットエクスプローラ- 70/91 • アセット専用のエクスプローラ。 各エディタ上で使用する。 • QAbstractItemModel を継承して, アセット情報管理に最適化して実装。 – 数万アセット表示可能。 – サムネイル表示,詳細表示などの様々な 表示形式に対応。 • サムネイルの動画再生にも対応。 – アニメーションデータなどは動画で確認可能。

Slide 71

Slide 71 text

安定したツール開発環境 71/91

Slide 72

Slide 72 text

テスト駆動開発(TDD) 72/91 • ランタイム同様にツールについても TDD を導入。 • TDD とは – プログラムに必要な各機能について,最初にテストを書き, そのテストが動作する必要最低限な実装をとりあえず行った後, コードを洗練させる,という短い工程を繰り返すスタイル – https://ja.wikipedia.org/wiki/テスト駆動開発

Slide 73

Slide 73 text

テストツール 73/91 • unittest – Python 標準モジュール • nose – 継承不要 – プラグイン機能 • pytest – fixture デコレータでテストで利用する リソースをテストごとに定義できる。 – プラグイン機能 – unittest/nose 用のテストを そのまま流用可能。 pip install nose pip install pytest

Slide 74

Slide 74 text

テストツール 74/91 • テストカバレッジ分析も coverage パッケージで可能。 – PyCharm を使うことで簡単に分析できる。 pip install coverage

Slide 75

Slide 75 text

テスト環境 75/91 • ローカル – CLI ツールで即時テスト可能。 • CI – 公開されているテストを Jenkins 上で 巡回テストする。 – 全てのテストが通った場合に 安定版と認定する。

Slide 76

Slide 76 text

python.exe のクラッシュ追跡 76/91 • pyd (Qt for Python や自作の pyd) を利用すると,python.exe が クラッシュすることがある。 – Python ソースコードのみを扱っている場合はクラッシュすることはまず無い。 • 主なクラッシュ原因例 – GIL を取得していない状態で PyObject を操作した。 – バッファオーバーランによるメモリ破壊。 – PyObject の参照カウントが正しく設定されておらず,参照カウントが 0 になった PyObject に対して書き込み処理を行った。

Slide 77

Slide 77 text

クラッシュレポート 77/91 • python.exe がクラッシュしたときに 原因の調査が出来るように対応。 – dmpファイルを必ず出力する設定にする。 – dmpファイルの出力先ディレクトリを監視し, dmpファイルの出力が検知されたら, クラッシュレポートダイアログを立ち上げる。 – デバッグシンボル (pdb) もツールと合わせて 配布しておくことで,コールスタックも表示できる。 – 状況などを書き込んでもらって送信してもらう。

Slide 78

Slide 78 text

ソースコード自動フォーマット 78/91 • PEP8 準拠 – 自動整形やソースコードチェックに使用するモジュール群。 • PEP8 準拠のソースコードになっていない場合は Perforce に submit 出来ないようにしている。 – Perforce のトリガー機能を利用。 pip install autoflake pip install autopep8_ pip install flake8___

Slide 79

Slide 79 text

ドキュメント生成 79/91 • Sphinx を利用。 – docstring 規格に合わせて関数ごとにドキュメントを書き,Sphinx を実行すれば Web 等で参照可能なページが出来上がる。 – Cyllista Game Engine 用にカスタマイズして,目次などを見やすく改善。 pip install Sphinx def info(text: str, inspect_depth: int = 0, once: bool = False) -> None: """ :param text: :param inspect_depth: :param once: """

Slide 80

Slide 80 text

型アノテーション 80/91 • Python は動的型付け。 – とても便利だが,これが理由で不具合も多々発生。 • 想定していない型の変数が入り込んできて,不正な挙動を起こす 等。 • 実行されて初めてエラーに気付く。 • 型アノテーションを利用する。 – 変数や関数の返り値などに型ヒントを付ける。 – mypy を利用すれば,間違った型を検知することが出来る。 • あくまで静的解析による検知だけで,実行時にエラーになるわけではないので注意。 • http://www.mypy-lang.org/ pip install mypy

Slide 81

Slide 81 text

型アノテーション 81/91 • 例えば a と b という2つの変数を加算した値を返す関数の場合 – 両方 int が入力されれば問題ないけれど,int と str が入力されたら例外が発生する。 – 引数と返り値に型ヒントを付ける。 – mypy を実行すると事前にエラーとして検知できる。 def add_int(a, b): return a + b add_int(1, 2) # 3 add_int(1, 'a') # TypeError def add_int(a: int, b: int) -> int: return a + b error: Argument 2 to "add_int" has incompatible type "str"; expected "int"

Slide 82

Slide 82 text

型アノテーション 82/91 • typing モジュールを利用することで,様々な型ヒント設定が可能。 – typing.Union[X, Y] • X または Y を表す。 – typing.Optional[X] • X または None を表す。typing.Union[X, None]同等。 – typing.Sequence • 整数インデックスで要素アクセスできるものを表す。str,list,tuple 等。 – typing.Iterable • for ループで扱えるものを表す。str,list,tuple,dict,set 等。

Slide 83

Slide 83 text

その他事例 83/91

Slide 84

Slide 84 text

ツールアップデータ 84/91 • ツールを更新するためのアップデータも Python で出来ている。 – 自分自身も更新する必要があるので厄介… • アップデータは PyInstaller で exe 化している。 – https://www.pyinstaller.org/ – exe 化しているので,アップデータのソースコードが更新対象に入っていても問題なし。 pip install PyInstaller

Slide 85

Slide 85 text

ツールアップデータ 85/91 • Python 上で Perforce を扱うには p4python パッケージが便利。 – https://www.perforce.com/manuals/p4python pip install p4python

Slide 86

Slide 86 text

自動リロード 86/91 • ソースコードの修正を即時反映させるために,モジュールのリロード処理を 自動的に行うようにする。 – importlib.reload() • 通常の import でインポートしたモジュールに対して利用。 – __loader__.exec_module() • 下サンプルのように importlib モジュールを利用してインポートしたモジュールに対して利用。 def load_python_module(module_name: str, module_path: str): import importlib.machinery import importlib.util module_loader = importlib.machinery.SourceFileLoader(module_name, module_path) module_spec = importlib.util.spec_from_loader(module_name, module_loader) module_instance = importlib.util.module_from_spec(module_spec) module_spec.loader.exec_module(module_instance) return module_instance

Slide 87

Slide 87 text

自動リロード 87/91 • 依存しているモジュールも走査し,変更が検知されたら自動リロードを行う。 def get_imported_module_list(module_instance, include_system: bool = False, module_list: Set = None): import inspect if not hasattr(module_instance, '__file__'): return module_list if module_list is None: module_list = {module_instance} member_infos = inspect.getmembers(module_instance, lambda m: inspect.ismodule(m) or inspect.isclass(m) or inspect.ismethod(m)) for info in member_infos: # Module Instance depend_module: Optional[ModuleType] = inspect.getmodule(info[1]) if depend_module is None: continue # if depend_module in module_list: continue # if not include_system: module_file = getattr(depend_module, '__file__', None) if module_file is None: continue if 'python37-64' in module_file: continue # import module_list.add(depend_module) module_list = get_imported_module_list(depend_module, include_system, module_list) return module_list

Slide 88

Slide 88 text

まとめ 88/91

Slide 89

Slide 89 text

利便性の高いツールを作るには… 89/91 • ツール開発者の効率を上げる! – Python は対話型のため,開発イテレーションが速い。 – 技術関連の情報も溢れているので,調査コストが低い。 – 独自のモジュール化することで,汎用的に使い回すことが出来る。 • 多種多様な技術を積極的に取り入れる! – サードパーティー製のパッケージがすぐに試用できる。 – Qt for Python を活用して GUI 開発。 – 外部ツール用のパッケージをインストールすれば,簡単に連携可能。 • ツールの配布を容易にする! – Perforce で Python のソースコードを配布するだけで OK。

Slide 90

Slide 90 text

まとめ 90/91 • Python を使えば何でも出来る。 • Python の強みと弱みを理解すれば,さらにクオリティが上がる。 • ツールについてもランタイム同様に開発環境を整えよう。 Python によって 最高のツールが作れる!

Slide 91

Slide 91 text

最高のツールで最高のゲームを