QAプロセスでPlayable!を使用してみた

# 初めに

今回は社内プロジェクトでPlayable!を用いたコリジョンチェックを行っていきます。
手順は公式サイトの「コリジョンチェックができるようになるまで」に基本的な部分は準拠します。

https://docs.playable.qa/getting-started/integration-practice

今回実装するプロジェクトはUnrealEngineを用いたまだ非公開プロジェクトのため画像等は載せられないですが、所感がどのようなものなのかが伝わるといいなと思っています。





# Playable!について

「Playable!」は、AIQVE ONE 株式会社が提供する、ゲーム QA に特化した自動テストツールセットです。 AI 技術を活用し、より高度なゲーム QA・デバッグ・テスト体験を実現することを目標に作成されているものになります。

https://docs.playable.qa/





# プロジェクト導入時に躓きやすい点

<details>
<summary>導入時全般</summary>

1. **Pythonをコマンドラインから実行する**
Pythonをコマンドライン上から実行するには相対パスの場合はコマンドライン上の現在のディレクトリから実行したいpythonファイルのパスを指定することでpythonファイルを実行することができます。
絶対パスの場合はどのディレクトリからでも同じなので問題ありません。
**※ただし、パス内にスペースが含まれる場合はダブルクォート(”)で左右を閉じてパス文字列として認識させないとダメです**
        
```python
python 実行したいpythonファイルのパス
```
        
2. **VSCodeからPythonのツールを実行する際にインタープリターの変え忘れ**
右下からインタープリター（Pythonの仮想環境）を変えておかないと意味がない

3. **各パッケージフォルダ内のPythonファイルに記述されているコードのパッケージインポート箇所のインポートディレクトリをAlfortからTemplate(任意の名称)への変更**
正直なところ言うと最初から変更後のパッケージを配布してほしいのが本音、一個一個やるのが煩わしい箇所な上**ミスると動きません**
Ctrl + Shift + Fなどで全検索を行い一括で変更してもよいのかもしれませんが自分は行っていないためこの方法で行った際の動作不良は自己責任でお願いします。

</details>



<details>
<summary>ゲームプロジェクトの作成と自動テスト対応</summary>

1. **ソースコードからビルドが必要なプラグインの導入**
これはビルドが通りそうで通らないエラーが出てもごり押しで行けるという点が躓く、エラーだが動くためう～んこのといった感じ

2. **BPでのDebugCameraDataToJson関数の作成時の凡ミス**
DebugCameraDataToJson関数を実装する際にJsonのデータを構築するのだがその際にJsonデータの構築ノードのFieldパラメータの設定を忘れてしまいがち、忘れてしまうと実行ファイルのビルドに成功していても実行時にクラッシュするしビルド前のUEエディタ上でも実行時クラッシュするためわかりづらい

</details>



<details>
<summary>Map Scannerによるマップスキャン</summary>

1. **ツールのコード変更の箇所について**
ここもミスしないでください
ミスするとバグった際に発見するのがめんどくさくなりがちです。
慎重にコードの変更を行ってください
2. **現状サイトに載っていませんがPythonファイルの変更が必須の箇所があります**

<details>
<summary>settings.yamlファイルの読み込みエンコードの変更</summary>

1. map_scanner_server.pyのGUIクラスの__init__メソッド
            
```python
# 変更前
with open("./settings.yaml") as file:
```
        
```python
# 変更後
with open("./settings.yaml", encoding="utf-8") as file:
```
                
2. map_scanner_searcher.pyのGUIクラスの__init__メソッド
                
```python
# 変更前
with open("./settings.yaml") as file:
```
                
```python
# 変更後
with open("./settings.yaml", encoding="utf-8") as file:
```
                
3. server_global.pyのsave_map_dataメソッド
                
```python
# 変更前
with open("./settings.yaml") as file:
```
                
```python
# 変更後
with open("./settings.yaml", encoding="utf-8") as file:
```

</details>



<details>
<summary>map_scanner_server.pyのGUIクラス内のupdate_server_uiメソッドの変更</summary>
            
```python
# 変更前
def update_server_ui(self):
    dpg.configure_item("client listbox", items=self.connected_clients)
    try:
        dpg.configure_item("task listbox", items=self.client_task_buffer[self.selected_client])
    except:
        dpg.configure_item("task listbox", items=[])
```


            
```python
# 変更後

def update_server_ui(self):
    dpg.configure_item("client listbox", items=self.connected_clients)
    try:
        # 変更前：dpg.configure_item("task listbox", items=self.client_task_buffer[self.selected_client])
        dpg.configure_item("task listbox", items=self.client_task_buffer[str(self.selected_client)])
    except:
        dpg.configure_item("task listbox", items=[])
```

</details>



3. **マップスキャンを行う際サイト通りに環境構築手順を進めている場合の注意点**
サイト通りに進めている場合はサーバー側から起動と実行を行ってください
何も変更されていない状態ではクライアントは一度起動後サーバーへの接続に失敗すると再接続する機能などが存在しないため再度クライアントの立ち上げを行う必要があり面倒です
その為サーバーのプログラムを実行し、サーバー側の起動後にクライアントから接続を行ってください
4. **スキャン時の注意点**
スキャンを行う際クライアントは複数起動することができますが、タスクマネージャーを開いた状態でCPU・メモリ・GPUの状態を見ながら1クライアントずつゆっくり起動してください。
連打してしまうと複数一気に起動が走ってしまいます、また、複数起動してしまった場合ハイスペックPCでも最悪フリーズしてしまうほどの処理負荷がかかりますので注意してください。
それ以外にも処理負荷が高すぎるとクライアントスレッドの数が多くてもリソース不足に陥りリソースの解放待ちがボトルネックになり**逆に計算が遅くなる場合がある**ため気を付けてください。
おすすめはCPU処理負荷が90%未満です。Pythonの処理が複数実行されGPUリソースよりもCPUのリソースを多く消費するためCPUのリソースを目安にするとよいです。
※ハイエンドのゲームを複数立ち上げているのと同じなので重いです。
5. **スキャン結果を用いたマップ経路探索の事前計算**
map_scanner_searcher.pyを実行しGUI上の探索マップに自身が設定したプロジェクト名を設定してください、サイト内では「template」とされている部分です
6. **ScanResultViewerの使用時の注意点**
MapScannerによるマップスキャンの項目の最後の確認手順として使用するScanResultViewerですが
現時点で複数のバグがありとても面倒なためバグとそのバグの対処法を明記しておきます。
        1. ツール起動時のSettingの項目のCellSizeですが基本的には触らないでも大丈夫です
        このパラメータはマップスキャナーによるマップスキャン時に使用されているパラメータと同じ値でないとバグって実行できません
            - 詳細
                
                マップスキャナーはマップスキャン時に
                MapScannerのPythonフォルダ直下のsettings.yamlファイルを参照してスキャン設定を読み込んでいるのですが、このスキャン設定のうちの「fineness_size」がScanResultViewerのSettingにある「CellSize」と元の数値以上になっていないとバグります。
                ダメな例：fineness_size = 40, CellSize = 39
                いい例：fineness_size = 40, CellSize = 40
                いい例：fineness_size = 40, CellSize = 41
                
                この2つのパラメータはスキャン時に作成されるマップの粒度（ボクセルの大きさ）を指定しており
                
        2. ツール実行時に存在するチェックボックスですが、一度消すと基本的に表示が元に戻りません
        再度元の表示が見たい場合は一度ツールを停止し再起動してください

</details>



<details>
<summary>Collision Checkerによるコリジョンチェック</summary>

1. map_param_calc.pyを用いた座標変換係数の計算における座標軸について
ツール上では床面となる座標平面の画像に対して座標変換係数を行うので使うツールに合わせてください
UnrealEngineの場合はXYの値をそのまま使ってもらって構いません
2. map_param_calc.pyを用いて座標変換係数の導出を行ったらちゃんとメモ帳などに保存して下さい
ツールを停止すると同じパラメータを導出するのはほぼ不可能です（再度開きなおした場合再度マップの地点指定からやり直しとなり、少しでも違う場所を設定した場合小数点以下単位で変化が顕著に出るためです）
3. コリジョンファイルの生成時のコマンドについて
ここも他のコマンドと同じようにディレクトリの記載の際にスペースが存在する場合はダブルクォート(”)で括ってください
また、このコマンドで作成したファイルは使用した「collision_completed_目標点の最短配置間隔の数値」のファイルを使用してください
4. コリジョンチェックの実行時のコード修正箇所について
「svrun_client.py」は修正必須です。

<details>
<summary>変更後</summary>
            
```json
#  Created by Yushi Matsuda
#  Copyright (c) 2022 Morikatron Inc. All rights reserved.
        
import argparse
import copy
import datetime
import keyboard
import os
import subprocess
import sys
            
from typing import Tuple
from utils import assert_window_watcher
from utils.util_modules import read_yaml
            
import svrun_client_logic
            
def default_filename_callback() -> Tuple[str, str]:
    """アサート発生時のスクリーンショットのファイル名を指定するコールバック
            
    Returns:
    Tuple[str, str]: アサートメッセージのスクリーンショットの保存先, ゲームのスクリーンショットの保存先
    """
    base_dir = "screen_shot"
    if not os.path.exists(base_dir):
        os.makedirs(base_dir, exist_ok=True)
            
        now = datetime.datetime.now()
        assert_image_file = f"assert_{now.strftime('%Y%m%d_%H%M%S')}.png"
        game_image_file = f"game_{now.strftime('%Y%m%d_%H%M%S')}.png"
        return os.path.join(base_dir, assert_image_file), os.path.join(base_dir, game_image_file)
            
            # _/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/
            #   スタートポイント
            # _/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/
            if __name__ == "__main__":
                parser = argparse.ArgumentParser(formatter_class=argparse.ArgumentDefaultsHelpFormatter)
                # for multi_process params
                parser.add_argument('-n', '--num', default=1, help='num of test process', type=int)
                # for test params
                parser.add_argument('--id', default=0, help='unique id', type=int)
                parser.add_argument('--waitsec', default=0, help='wait sec', type=float)
                parser.add_argument('--logfolder', default='./result/', help='log path', type=str)
                parser.add_argument('-d', '--datafolder', required=True, help='path to datafolder', type=str)
                # for env params
                parser.add_argument('--envresx', default=320, help='environment resolution size x', type=int)
                parser.add_argument('--envresy', default=180, help='environment resolution size y', type=int)
                parser.add_argument('--envaddr', default='127.0.0.1', help='COMBridge address', type=str)
                parser.add_argument('--envport', default=8000, help='COMBridge port', type=int)
                # for server-client
                parser.add_argument('--svaddr', default='127.0.0.1', help='data server address', type=str)
                parser.add_argument('--svport', default=8050, help='data server port', type=int)
                args = parser.parse_args()
            
                yaml_data = read_yaml(os.path.join(args.datafolder, "_map_list.yaml"))
                envpath = yaml_data["path_params"]["env_path"]
            
                # アサートメッセージを自動で閉じる
                watcher = assert_window_watcher.AssertWindowWatcher(default_filename_callback)
                watcher.start()
            
                print('********** 実行引数 **********')
                for k, v in args._get_kwargs():
                    print("{0}={1}".format(k, v))
            
                for id in range(args.num):
                    proc_args = copy.deepcopy(args)
                    proc_args.id = id
                    proc_args.waitsec = 20 * id
                    proc_args.envport = args.envport + id
                    option = ''
                    for k, v in proc_args._get_kwargs():
                        option += ' --{0}={1}'.format(k, v)
                    print(f'start "{id}/{args.num}" python svrun_client_logic.py')
                    command_line_args = f'--envpath "{envpath}"' + option
                    print(f"args = {command_line_args}")
                    p = subprocess.run(f'start "{id}/{args.num}" python svrun_client_logic.py {command_line_args}', shell=True)
            
                while True:
                    if keyboard.is_pressed("ctrl+c"):
                        watcher.stop()
                        sys.exit()
```

</details>
            
5. コリジョンチェック実行時の注意点

<details>
<summary>サーバー側</summary>
            
サーバーの起動コマンドは以下のようになっています。
            
```json
python svrun_server.py -n 最大同時接続数 -m テストマップ名 -d マップスキャンデータのフォルダ
```
            
この際マップスキャンデータのフォルダにcollision_compleated.jsonが入っている必要があります。
このcollision_compleated.jsonは元々collision_compleated_目標点の最短配置間隔.jsonだったものですが、ファイル名を変更しないとエラーになるので変更してください。

</details>

<details>
<summary>クライアント側</summary>
            
サーバー側と同じくマップスキャンデータのフォルダ内にcollision_compleated.jsonが必要がある

</details>

6. コリジョンチェック後のビューワーによる確認時の注意点
    
ビューワー実行時に必要なデータのコピー時に「マップID_reach_log.json」ファイルを「reach_log.json」ファイルにリネームすることを忘れないで下さい。

</details>




# プロジェクトに導入してみた感想

今回のプロジェクトで自動テスト（Playthrough）、当たり判定チェック（Collision Check）の機能を使用しました。

導入の際躓くことはありましたが、サポートも丁寧に対応して頂けたため導入することができました。

今のところPythonのソースコードを直接変更することが導入手順に含まれていますが、導入時のコード変更時に注意すれば問題なく導入できるかと思います。





# プロジェクトで使ってみた感想

今回使用したプロジェクトでは広範囲なマップを使用していたため人力によるコリジョンチェックでは時間がかかってしまうところにコリジョンチェックを行い実際のプレイ時のコリジョンを視覚化することで広範囲マップのコリジョンの安定性を担保することができたと思います。

現在開発途中のツールではありますが、自動テスト、当たり判定チェックなど人力でやるとコストのかかってしまう作業をこのツール一つで全て行えるのはとても便利で良いものだと思いました。





# 最後に

導入時こそ少し難しい箇所が存在しますが、環境の導入さえ終わってしまえばほぼ面倒な作業を行わずかつ実際のゲーム環境で自動テストや当たり判定のチェックが自動で行えるのでとてもいいツールだと思います。

これからのますます使い勝手はよくなっていくものと思うのでまた他の機会でも使ってみたいと思いました。

今回使用した機能以外にもQA用の様々な機能があるようなので気になった方は公式サイトを除いていただくのが良いかと思います

https://playable.qa/