Pro Yearly is on sale from $80 to $50! »

Python による大規模ゲーム開発環境 ~Cyllista Game Engine 開発事例~

510ec964f5d26c2724c883fd7b671e3d?s=47 Cygames
September 02, 2020

Python による大規模ゲーム開発環境 ~Cyllista Game Engine 開発事例~

2020/09/02 CEDEC2020

510ec964f5d26c2724c883fd7b671e3d?s=128

Cygames

September 02, 2020
Tweet

Transcript

  1. 1/91

  2. 自己紹介 • 株式会社 Cygames Cyllista Game Engine シニアゲームエンジニア 沖 幸太朗

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

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

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

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

  7. Python とは 7/91

  8. Python 8/91 • 汎用の高水準プログラミング言語。 関数型,オブジェクト指向,動的型付けなどの 特徴を持つ。 – https://www.python.org/ • 本体部分は必要最小限の機能のみ。

    パッケージという形で様々な機能が提供される。 • 世界で広く使われているプログラミング言語の1つ。 – IEEE Spectrum で3年連続1位 (2017-2019)
  9. Python の強み1 -動的(対話型)- 9/91 • コンパイル・リンクせずにすぐに動作テストが可能。 • 対話モード(REPL)を利用することにより,関数の評価などを気軽に行える。

  10. Python の強み2 -充実したパッケージ- 10/91 • 2020年8月20日現在,257,375 種類のパッケージが提供されている。 – 最近ではディープラーニング系が多く提供されている。 •

    サードパーティー製のパッケージは pypi からインストールできる。 – https://pypi.org/ pip install (package_name)
  11. Python の IDE (統合開発環境) 11/91 • PyCharm が非常に便利 – https://www.jetbrains.com/ja-jp/pycharm/

    – コード補完・検査やリアルタイムでのエラー指摘など,非常に優秀 – コード検索も非常に高速 – 有償版では… • 細かな負荷計測 • Perforce 連携 • プロセスアタッチによるデバッグ etc... • このようなツールを使っている人も – Visual Studio Code – Vim
  12. Python の弱点を知る 12/91 • Python は非常に便利なプログラミング言語。 • だけど弱点も存在する。 弱点を知ることで, Python

    を効率良く使いこなす!
  13. Python の弱点1 -速度の遅さ- 13/91 • Python では単純な処理でも負荷がかかることがある。 • 特に大量の計算が発生する処理が遅い。 –

    動的型付けのため,単純な計算でも型チェックが入ってしまう。 • 大量の計算を行うのであれば NumPy パッケージが有効。 • 複雑な処理を行う場合は pyd が有効。
  14. NumPy 14/91 • 数値計算を高速に行うためのパッケージ。 – ディープラーニング系では必須なパッケージ。 • 多次元配列 (ndarray) –

    四則演算 – 行列積 – 統計量 (平均・分散・標準偏差・最大値・最小値など) etc... pip install numpy
  15. 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)]
  16. 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)
  17. pyd 対応 17/91 • pyd とは Python モジュールとして 利用できる DLL

    のこと。 – C 言語でコーディングできるので高速。 – Python.h をインクルード。 • https://docs.python.org/ja/3/extending/ • 右のコードで作った sample.pyd は Python コードからインポート可能。 – 加算処理を行う add() を提供。 #include <Python.h> 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
  18. pyd 対応 (モジュール定義) 18/91 • モジュール名を name としたときに, PyInit_name という名前の初期化関数を

    用意する必要がある。 • PyModuleDef 構造体でモジュールの 定義を設定する。 #include <Python.h> 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); }
  19. pyd 対応 (関数定義) 19/91 • Python コードからアクセスできる関数に ついて,入力として2つの PyObject*, 出力として

    PyObject* を返す関数を定義。 • 右の例では,args から引数をパースし, その値を実処理 (add()) に渡し, 結果を PyObject* に変換して返している。 #include <Python.h> 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); }
  20. pyd 対応 20/91 • …とにかく難易度が高い! – 2つの変数を加算するだけの単純な処理で これだけのコードを書かなければならない。 – PyObject

    の扱いが難しい。 • 参照カウントを自分で操作する必要があり, 誤るとクラッシュの原因となる。 pybind11 を使う!
  21. 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/
  22. pybind11 22/91 #include <Python.h> 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 <pybind11/pybind11.h> int add(int x, int y) { return x + y; } PYBIND11_MODULE(sample, m) { m.doc() = "Sample Module"; m.def("add", &add); }
  23. Python の弱点2 -並列処理に弱い- 23/91 • Python には GIL (Global Interpreter

    Lock) が存在する – 排他ロック。ロックを持つスレッドのみ実行可能。 ロックを持っていないスレッドはロックが開放されるまで待たされる。 – async などを利用することで,ファイルI/O 等との並列は可能ではあるが, 原則実行されているのは1スレッドのみ。 • multiprocessing モジュールでプロセス分離 – プロセス間は Pipe 通信もしくは共有メモリが利用できる。 (共有メモリは Python 3.8 以降のみ対応)
  24. 高速化事例 -ジョブシステム- 24/91 • 同時に複数の Python のコードを実行したい。 – (例) Python

    で書かれたコンバータで,大量のアセットを並列にコンバートしたい。 • Python 上で動くジョブシステムを作ってみた。 – 1つ以上のジョブをジョブシステムに投入すると,ジョブエグゼキュータが 並列に処理していく。
  25. ジョブシステムフロー 25/91 常駐プロセスとして,1つのExecutor Server を立てる。

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

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

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

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

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

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

  32. ジョブシステムフロー 32/91 Executor Thread から Executor Process にジョブを渡し,Executor Process 内で

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

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

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

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

  37. ジョブシステム サンプルコード 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
  38. ジョブシステム サンプルコード 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()
  39. ジョブシステム サンプルコード 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
  40. ジョブ内からのジョブ実行 40/91 • ジョブの中から複数のジョブを実行したいときがある。 – (例) アセットのコンバート中に,他の複数のアセットのデータ取得のためにコンバート が必要になったとき。 – 全ての

    Executor Process 内で上記の状況になると,デッドロック状態になってしまう。 • Executor Process の数を自動的に増減できるようにする。 – ジョブ内からジョブを投入しようとしたとき,一定時間以上 Executor Process の空き が見つからなければ,Executor Process を自動的に増加させてそれを利用する。 – 増加させた Executor Process は不要になったタイミングで破棄する。
  41. Cyllista Game Engine とは 41/91

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

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

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

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

    • Python に関する情報が溢れているため,調査コストが低い。
  48. 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’)
  49. ツール配布方法 49/91 • ツールの配布は Perforce で行っている。 – ツール利用者は Python のソースコードを

    sync するだけで更新が適用される。
  50. ランタイム(実機)との連携 50/91

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

    – またやり直しだ… 否! クラッシュしても編集内容が 消えないようにする!
  52. システム構成図 52/91

  53. サーバー 53/91 • 独立したプロセス。 – Python で作成。 • 編集状態を所持。 –

    Undo/Redo 等の情報も。 • クライアントから送信された変更内容 を他の全てのクライアントに リアルタイムに通知。 サーバープロセスは強固に。 安定性を最優先とする!
  54. クライアント 54/91 • ランタイムやエディタに含まれる。 – ランタイム : C++ – エディタ

    : Python • ランタイム / エディタ共にクラッシュ することが多々あるが,サーバーに 再接続したときにサーバーが情報を 保持しているので,それを復元する。
  55. サーバー・クライアント通信 55/91 • サーバー・クライアント間は TCP で通信する。 – コンソール機とも通信する必要があるため,Pipe 通信は使えない。 –

    パケットロスさせないように,UDP ではなく TCP。 • Python 上で TCP 通信を組む場合は,socket / select モジュールで 実装すると良い。
  56. サーバー・クライアント通信 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()
  57. 外部ツールとの連携 57/91

  58. 外部ツールにおける Python 58/91 • Python で操作することができる外部ツール – Shotgun – Wwise

    • Python が中に組み込まれている外部ツール – Maya – Houdini
  59. Python で操作する外部ツール 59/91 • pip を利用して各ツール用に用意されたパッケージをインストールする。 – Shotgun – Wwise

    pip install git+https://github.com/shotgunsoftware/python-api.git pip install waapi
  60. 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)
  61. 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 上で直接利用したいけれど,出来ない!
  62. 解決方法 62/91 • Python 2系と3系を跨ぐ場合は,subprocess モジュール等で 別のプロセスを立ててしまうのが良い。 – exe や

    bat を起動する。 – 起動引数や一時ファイルで情報を渡す。
  63. GUI 開発について 63/91

  64. 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
  65. 最小限コード 65/91 from PySide2 import QtWidgets if __name__ == '__main__':

    application = QtWidgets.QApplication(sys.argv) label = QtWidgets.QLabel('Hello World') label.show() application.exec_()
  66. Qt for Python の良い点 66/91 • 基本的な GUI コンポーネントや各種システムが用意されているため, 少ない手数で扱える。

    – メッセージングシステム – ドッキングシステム – ショートカットシステム • Python で書けるので効率が良い。 – コンパイルする必要がないので,GUI の調整⇔動作確認が非常に速い。 – 豊富なパッケージと組み合わせて利用することが出来る。 • Maya 等の DCC ツールでも採用されているため,ノウハウの共有が出来る。 – DCC ツールは Python 2系なので,コードの共有は難しいかもしれないが…
  67. Qt for Python のあまり嬉しくない点 67/91 • ドキュメントが不足している。 – 引数の型が書かれていないため,C++ 用のドキュメントで確認する必要がある。

    • クラッシュしたときにデバッグがしづらい。 – クラッシュの原因が Qt 内部だった場合に,原因の特定が非常に難しい。 • Qt for Python ならではの不具合が存在する。 – 特に困ったのが @QtCore.Slot デコレータを設定すると,特定の条件下でメモリ破壊が 発生することがあるという不具合。 • https://bugreports.qt.io/browse/PYSIDE-249 – Cyllista Game Engine では @QtCore.Slot デコレータは使用禁止に。
  68. GUI 事例 -グラフビューワ- 68/91 • エフェクトグラフやアセット依存関係図などで使用。 • Qt Graphics View

    Framework を利用。 – 長方形や曲線など,様々な 2D アイテムを制御することが出来る。アイテムはクリック 等の各種イベントを扱うことが出来,マウスの動きも追跡することが出来る。
  69. GUI 事例 -ビューポート- 69/91 • ランタイムの画面を ウィジェット内に埋め込んでいる。 • 別プロセスのウィンドウを容易に ウィジェットに埋め込み可能。

    • マウスイベントの扱いは要注意。 – 別プロセスのウィンドウ上のマウスイベントは埋め込み先に通知されない。 – 別プロセス上でコンテキストメニュー等を表示したい場合は nativeEvent() で Windows のメッセージを拾って制御する。 window_wrapper = QtGui.QWindow.fromWinId(hwnd) window_widget = QtWidgets.QWidget.createWindowContainer(window_wrapper) self.layout().addWidget(window_widget)
  70. GUI 事例 -アセットエクスプローラ- 70/91 • アセット専用のエクスプローラ。 各エディタ上で使用する。 • QAbstractItemModel を継承して,

    アセット情報管理に最適化して実装。 – 数万アセット表示可能。 – サムネイル表示,詳細表示などの様々な 表示形式に対応。 • サムネイルの動画再生にも対応。 – アニメーションデータなどは動画で確認可能。
  71. 安定したツール開発環境 71/91

  72. テスト駆動開発(TDD) 72/91 • ランタイム同様にツールについても TDD を導入。 • TDD とは –

    プログラムに必要な各機能について,最初にテストを書き, そのテストが動作する必要最低限な実装をとりあえず行った後, コードを洗練させる,という短い工程を繰り返すスタイル – https://ja.wikipedia.org/wiki/テスト駆動開発
  73. テストツール 73/91 • unittest – Python 標準モジュール • nose –

    継承不要 – プラグイン機能 • pytest – fixture デコレータでテストで利用する リソースをテストごとに定義できる。 – プラグイン機能 – unittest/nose 用のテストを そのまま流用可能。 pip install nose pip install pytest
  74. テストツール 74/91 • テストカバレッジ分析も coverage パッケージで可能。 – PyCharm を使うことで簡単に分析できる。 pip

    install coverage
  75. テスト環境 75/91 • ローカル – CLI ツールで即時テスト可能。 • CI –

    公開されているテストを Jenkins 上で 巡回テストする。 – 全てのテストが通った場合に 安定版と認定する。
  76. python.exe のクラッシュ追跡 76/91 • pyd (Qt for Python や自作の pyd)

    を利用すると,python.exe が クラッシュすることがある。 – Python ソースコードのみを扱っている場合はクラッシュすることはまず無い。 • 主なクラッシュ原因例 – GIL を取得していない状態で PyObject を操作した。 – バッファオーバーランによるメモリ破壊。 – PyObject の参照カウントが正しく設定されておらず,参照カウントが 0 になった PyObject に対して書き込み処理を行った。
  77. クラッシュレポート 77/91 • python.exe がクラッシュしたときに 原因の調査が出来るように対応。 – dmpファイルを必ず出力する設定にする。 – dmpファイルの出力先ディレクトリを監視し,

    dmpファイルの出力が検知されたら, クラッシュレポートダイアログを立ち上げる。 – デバッグシンボル (pdb) もツールと合わせて 配布しておくことで,コールスタックも表示できる。 – 状況などを書き込んでもらって送信してもらう。
  78. ソースコード自動フォーマット 78/91 • PEP8 準拠 – 自動整形やソースコードチェックに使用するモジュール群。 • PEP8 準拠のソースコードになっていない場合は

    Perforce に submit 出来ないようにしている。 – Perforce のトリガー機能を利用。 pip install autoflake pip install autopep8_ pip install flake8___
  79. ドキュメント生成 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: """
  80. 型アノテーション 80/91 • Python は動的型付け。 – とても便利だが,これが理由で不具合も多々発生。 • 想定していない型の変数が入り込んできて,不正な挙動を起こす 等。

    • 実行されて初めてエラーに気付く。 • 型アノテーションを利用する。 – 変数や関数の返り値などに型ヒントを付ける。 – mypy を利用すれば,間違った型を検知することが出来る。 • あくまで静的解析による検知だけで,実行時にエラーになるわけではないので注意。 • http://www.mypy-lang.org/ pip install mypy
  81. 型アノテーション 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"
  82. 型アノテーション 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 等。
  83. その他事例 83/91

  84. ツールアップデータ 84/91 • ツールを更新するためのアップデータも Python で出来ている。 – 自分自身も更新する必要があるので厄介… • アップデータは

    PyInstaller で exe 化している。 – https://www.pyinstaller.org/ – exe 化しているので,アップデータのソースコードが更新対象に入っていても問題なし。 pip install PyInstaller
  85. ツールアップデータ 85/91 • Python 上で Perforce を扱うには p4python パッケージが便利。 –

    https://www.perforce.com/manuals/p4python pip install p4python
  86. 自動リロード 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
  87. 自動リロード 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
  88. まとめ 88/91

  89. 利便性の高いツールを作るには… 89/91 • ツール開発者の効率を上げる! – Python は対話型のため,開発イテレーションが速い。 – 技術関連の情報も溢れているので,調査コストが低い。 –

    独自のモジュール化することで,汎用的に使い回すことが出来る。 • 多種多様な技術を積極的に取り入れる! – サードパーティー製のパッケージがすぐに試用できる。 – Qt for Python を活用して GUI 開発。 – 外部ツール用のパッケージをインストールすれば,簡単に連携可能。 • ツールの配布を容易にする! – Perforce で Python のソースコードを配布するだけで OK。
  90. まとめ 90/91 • Python を使えば何でも出来る。 • Python の強みと弱みを理解すれば,さらにクオリティが上がる。 • ツールについてもランタイム同様に開発環境を整えよう。

    Python によって 最高のツールが作れる!
  91. 最高のツールで最高のゲームを