Upgrade to Pro — share decks privately, control downloads, hide ads and more …

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

Cygames
September 02, 2020

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

2020/09/02 CEDEC2020

Cygames

September 02, 2020
Tweet

More Decks by Cygames

Other Decks in Technology

Transcript

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

    View full-size slide

  2. はじめに
    3/91

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  6. Python とは
    7/91

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  14. 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)]

    View full-size slide

  15. 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)

    View full-size slide

  16. 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

    View full-size slide

  17. 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);
    }

    View full-size slide

  18. 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);
    }

    View full-size slide

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

    View full-size slide

  20. 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/

    View full-size slide

  21. 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);
    }

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  36. ジョブシステム サンプルコード
    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

    View full-size slide

  37. ジョブシステム サンプルコード
    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()

    View full-size slide

  38. ジョブシステム サンプルコード
    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

    View full-size slide

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

    View full-size slide

  40. Cyllista Game Engine とは
    41/91

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  45. 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’)

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  49. システム構成図
    52/91

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  53. サーバー・クライアント通信
    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()

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  57. 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)

    View full-size slide

  58. 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 上で直接利用したいけれど,出来ない!

    View full-size slide

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

    View full-size slide

  60. GUI 開発について
    63/91

    View full-size slide

  61. 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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  76. ドキュメント生成
    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:
    """

    View full-size slide

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

    View full-size slide

  78. 型アノテーション
    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"

    View full-size slide

  79. 型アノテーション
    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 等。

    View full-size slide

  80. その他事例
    83/91

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  83. 自動リロード
    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

    View full-size slide

  84. 自動リロード
    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

    View full-size slide

  85. まとめ
    88/91

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide