ビューワープログラム
1. 概要
ビューワープログラムは json 出力されたコリジョンチェックの結果ファイルを読み込み、
その結果を GUI 上、並びにゲーム上に可視化するためのツールを構成するものです。
ゲームバイナリとビューワープログラム間の通信は Game-Python Bridge を介して行っています。
GUI 作成にはDearPyGUI
を利用し、ビューワープログラムは MVC モデルで作成しています。
関連ファイル
ビューワープログラムに関連するファイルは以下です。
Python
├── COMBridge +------------------+ Game-Python Bridge の Python モジュールディレクトリ
├── alfort +---------------------+ Alfort 関連ディレクトリ
│ ├── alfort_env_worker.py +---+ サブスレッド上で動作する Unreal Engine 通信モジュール
│ └── debug_launcher.py +------+ タイトル画面 / デバッグ機能等の操作モジュール群
├── game_modules +---------------+ ゲーム非依存ディレクトリ
│ ├── game_modules.py +--------+ ゲーム操作に関連するモジュール群
│ └── state_machines.py +------+ ステートマシンモジュール群
├── res +------------------------+ 素材データディレクトリ
├── result_viewer.py +-----------+ ビューワーツールプログラム
├── utils +----------------------+ 便利モジュールディレクトリ
│ ├── common_func.py +---------+ 普遍的に使用される関数群
│ └── logger.py +--------------+ ログ出力モジュール群
└── wrappers +-------------------+ ゲーム通信ディレクトリ
└── ue4_wrapper.py +---------+ Unreal Engine 通信モジュール
2. プログラムの詳細
MVC モデルを参考にビューワープログラムを作成しており、
ビューワープログラムではMain_Controller
クラス、Main_View
クラス、Main_Model
クラスの 3 クラス間で情報をやり取りすることでシステムを提供しています。
プログラム解析や拡張を行う場合、まずは下記の引用元を参考にして MVC モデルの概要を把握することを推奨します。
MVC モデル
役割ごとに Model, View, Controller に分割してコーディングを行うモデル
それぞれの役割は,
- Model --> システムの中でビジネスロジックを担当する
- View --> 表示や入出力といった処理をする
- Controller --> ユーザーの入力に基づき,Model と View を制御する
エントリーポイントでは Main_Controller
クラスのインスタンスを生成し、
クラス内部で他 2 つのクラスインスタンスを生成後に各種セットアップと GUI 描画処理を実行しています。
# result_viewer.py
# エントリーポイント (簡略コード)
if __name__ == '__main__':
# コントローラーを生成
controller = Main_Controller()
# セットアップ
controller.setup()
# GUI表示
controller.show()
セットアップ関数内ではモデルMain_Model
とコントローラーMain_Controller
を生成し、セットアップを実行します。
# result_viewer.py
# エントリーポイント (簡略コード)
class Main_Controller:
def setup(self):
# DearPyGUI 初期化
dpg.create_context()
# モデル生成
self.model = Main_Model()
# ビュー生成
self.view = Main_View(self)
# DearPyGUI 内部の値レジストリにツールで参照書き込みする値を事前登録
self.model.registry_value()
# ツール専用のセーブデータがあれば事前読み込み
self.model.load_data()
# DearPyGUI を利用して UI を構築
self.view.setup()
# 読み込んだセーブデータの値に応じて諸々処理を実行
self.view.check_loaded_data()
# コントローラー自身のセットアップ処理
self._setup_myself()
GUI 操作全般をMain_View
クラスに移譲しています。
そのため GUI 関連のセットアップについてもself.view.setup()
関数内部で実行されます。
本プログラムでは GUI 描画にDearPyGUIを利用しており、
2023 年 3 月末時点ではバージョン1.7.1
を前提としています。
# result_viewer.py
# DearPyGUI を利用して UI 構築 (簡略コード)
class Main_View:
def setup(self):
# デスクトップ解像度を取得
monitor_info = GetMonitorInfo(MonitorFromPoint((0, 0)))
work_area = monitor_info.get("Work")
self.screen_width = work_area[2]
self.screen_height = work_area[3]
# DearPyGUI に日本語表示可能なフォントを登録
self.register_font(self.__font_size)
# GUI ウィンドウを作成. 画面の右下に生成されるように設定
dpg.create_viewport(title='Morikatron CollisionTest Tool', width=self.viewport_width, height=self.viewport_height,
x_pos=self.screen_width - (30 + self.viewport_width), y_pos=self.screen_height - (30 + self.viewport_height))
# メニューバーを設定
with dpg.viewport_menu_bar():
with dpg.menu(label="バージョン情報"):
self.make_menu_version()
# GUI ウィンドウ内に内部ウィンドウを生成し、「ゲーム設定」タブと「結果確認」タブの表示設定
with dpg.window(label="リザルトビューワー", tag="Result_View", no_move=True, no_close=True, pos=[0, 25], no_scrollbar=False, width=330, height=485):
with dpg.tab_bar(label="tb"):
with dpg.tab(label="ゲーム設定"):
self.make_game_tab()
with dpg.tab(label="結果確認", tag="result__tab"):
self.make_result_tab()
ボタン押下、スライド操作、ファイル選択等の全ユーザー操作に対してコールバックを設定し、
全てのユーザー操作からcontroller.callback_event(key, args)
関数が呼び出される仕組みになっています。
以下に「ゲーム設定」タブの表示プログラムを例として示します。
# result_viewer.py
# DearPyGUI を利用して UI 構築 (簡略コード)
class Main_View:
def make_game_tab(self):
dpg.add_text("ゲームの実行ファイル")
with dpg.group(horizontal=True):
dpg.add_input_text(tag="game__text_exefile", source="game__val_exefile", readonly=True, width=170)
dpg.add_button(label="exeファイルを選択", tag="game__select_exefile", callback=lambda: self.__ctrl.callback_event("game__select_exefile", ["game__val_exefile"]))
dpg.add_text("テスト結果フォルダ")
with dpg.group(horizontal=True):
dpg.add_input_text(tag="game__text_resultfolder", source="game__val_resultfolder", readonly=True, width=170)
dpg.add_button(label="フォルダを選択", tag="game__select_resultfolder", callback=lambda: self.__ctrl.callback_event("game__select_resultfolder", ["game__val_resultfolder"]))
dpg.add_text("マップ選択")
with dpg.group(horizontal=True):
dpg.add_listbox(tag="game__select_map", source="game__val_mapname", items=self.__ctrl.model.get_map_list(), num_items=len(self.__ctrl.model.get_map_list()))
self.__make_line(height=10)
dpg.add_checkbox(label="コリジョン抜け検出のためのデバッグ表示", tag="game__flag_draw_raycast", default_value=True)
with dpg.group(horizontal=True):
dpg.add_button(label="起動", tag="game__run", enabled=False, width=315, height=50, callback=lambda: self.__ctrl.callback_event("game__run"))
# result_viewer.py
# イベントコールバック関数 (簡略コード)
class Main_Controller:
def callback_event(self, key, args=None):
# ゲーム設定タブ関連キー
if "game__" in key:
if key == "game__select_exefile":
# 選択ボタンを押されたら、View モジュールでファイル選択ウィンドウを開き、選択後に Model モジュールで設定を保存
self.view.select_exefile(lambda path: self.model.save_settings())
if key == "game__select_resultfolder":
# 選択ボタンが押されたら、View モジュールでフォルダ選択ウィンドウを開き、選択後に Model モジュールで選択パス文字列の保存
self.view.select_resultfolder(lambda path: self.model.set_dpg_path(args[0], path))
if key == "game__run":
# Model モジュールで結果ファイルとコリジョンファイルを読み込み
self.model.read_resultfile()
self.model.read_collisionfile()
# View モジュールで結果確認タブを自動的に開く
self.view.open_result_tab()
# Model モジュールで対象マップ用セーブファイルをゲーム読み込み用フォルダへコピー
self.model.copy_savefile(folderpath, map_keyword)
# Model モジュールでゲームを起動
self.model.run_exe(is_draw_raycast=is_draw_raycast)
# View モジュールでマップウィンドウを開く
self.view.make_map_window()
if key == "game__quit":
# Modelモジュールでゲームを終了
self.model.quit_exe()
# 結果確認タブ関連キー
if "result__" in key:
pass # 省略
# マップ表示関連キー
if "map__" in key:
pass # 省略
# 落下点一覧ウィンドウ関連キー
if "dropwindow__" in key:
pass # 省略
# 怪しい箇所一覧ウィンドウ関連キー
if "lackcolwindow__" in key:
pass # 省略