新規ゲームへの対応手順
はじめに
新規ゲームに対応するためには、あらかじめ用意しているゲーム固有の実装である alfort
フォルダもしくは unity_demo
フォルダ内のソースコードを適切に書き換える必要があります。
そのための準備として、以下のことを行ってください。
alfort
フォルダもしくはunity_demo
フォルダのコピーし、フォルダ名を対象のゲーム名(もしくはプロジェクトコード)にリネームします。{新規ゲーム名}
フォルダ以下の各ソースコード内のimport
文でalfort
フォルダもしくはunity_demo
フォルダ以下のモジュールを参照している部分を{新規ゲーム名}
フォルダ以下のモジュールを参照するように置換します。data/conf
フォルダ以下にあるalfort.yaml
もしくはunity_demo.yaml
のコピーを作成し、{新規ゲーム名}.yaml
にリネームします。playthrough.py
の先頭でfrom unity_demo.custom_gui import CustomGUI
となっている部分をfrom {新規ゲーム名}.custom_gui import CustomGUI
に置換します。
以降で具体的に行うコードの書き換えについて説明します。
{新規ゲーム名}/custom_gui.py
ツールの GUI において、ゲーム固有の部分が実装されたコードです。
同コード内の class CustomGUI
は、ツールの GUI においてゲーム共通の部分が実装されている base/base_gui.py
の class BaseGUI
を継承しています。
playthrough.py
を実行すると CustomGUI
が初期化され、GUI の構築が行われます。
この時に構築される各モードの GUI は図のようになっています。
__init__()
CustomGUI
インスタンスの初期化関数です。
通しプレイでは、各お手本の記録開始時と記録終了時にゲーム側でセーブを行い、
その時に作成されるセーブデータをお手本を溜めておくフォルダにコピーしています。
そのために、ゲーム側のセーブデータが存在するパスや、セーブデータのファイル名をインスタンス変数に登録しておく必要があります。
セーブデータのファイル名が固定の場合
変数 self.save_path
を用意し、ゲームの exe がある場所から見たセーブデータの相対パスを代入してください。
セーブデータのファイル名が変わる場合
変数 self.search_key_save
を用意し、セーブデータを検索できるような正規表現文字列を代入してください。
もし正規表現でも検索できないような命名規則 (完全ランダムな文字列である場合など) である場合、適切な対応を行ってください。
サンプルゲームのセーブデータは、セーブ時刻によって名前が決定されるセーブデータ本体と、固定名称である SharedData
というファイルが対となって構成されています。よって、「ファイル名が固定の場合」と「ファイル名が変わる場合」双方の対応を行っています。
callback_remove()
GUI 上の「削除」ボタンが押された時に呼ばれる関数です。 例えば「demo_000」の「削除」ボタンが押された場合、0 番目の手本データ (csv) と、 この手本の記録開始時、および記録終了時のセーブデータが削除されます。
手本フォルダに保存されているセーブデータを削除する部分で、削除したいセーブデータのファイル名を適切に指定してください。
サンプルゲームにおける実装では、手本データ、記録開始時および終了時のセーブデータに加え、
記録開始時および終了時の SharedData
も削除するようになっています。
get_game_savedata_path()
ゲーム側に存在するセーブデータのパスを取得する関数です。 ゲームに合わせて適切なパスを返してください。
copy_savedata_for_demo()
ゲーム起動前に手本フォルダからゲームフォルダにロードに必要なセーブデータをコピーする関数です。
手本フォルダのセーブデータは、元のファイル名に特定の接頭語 (start_save_
や end_save_
) を追加したものとなっています。
文字列操作でこの接頭語を取り除いてから、ゲーム側のセーブデータがあるフォルダにコピーする処理を用意してください。
サンプルゲームではセーブデータのファイル名が毎回変わるので、ゲーム起動前に全てのセーブデータをコピーすることができますが、
セーブデータのファイル名が固定である場合、再生する手本を切り替える処理を行う alfort/custom_core.py
> load_next_savedata
に、
その時に必要なセーブデータだけをコピーする処理を書く必要があると考えられます。
このとき、ゲーム起動中にセーブデータの上書きが不可能な場合は、
おそらくゲーム側の仕様、もしくは通しプレイツール全体の設計を変えなければ対応できません。
callback_make_nextdemo()
GUI 上の「追加記録」ボタンが押された時に呼ばれる関数です。 次の手本を作成する際、直前の手本の終了地点に対応するセーブデータをロードします。 以下のことを実現する関数を定義してください。
- 手本フォルダ内の記録終了時のセーブデータ (
end_save_
で始まるファイル) の中から、 これから記録しようとしている手本の 1 つ前のものを探す - 適切な文字列操作でファイル名の接頭語 (
end_save_
) を取り除く - (まだゲーム側にコピーしていなければ) 上記で得られた文字列をファイル名としてゲーム側のセーブデータフォルダにコピーする
- (必要なら) 上記文字列をロード用のパッド操作を行うための関数に引数として渡す
callback_end_make_demo()
GUI 上の「記録終了」ボタンが押された時に呼ばれる関数です。 記録終了時にゲーム側で作成したセーブデータを手本フォルダにコピーします。
- サンプルゲームにおいては仕様上、ゲームクリア / ゲームオーバー画面ではスタートボタンのメニューを開くことができないため、セーブを行うことができません。 よって、「記録終了」ボタンが押された時点でこれらの画面になっていた場合、セーブデータを作る処理を呼び出さないようにしています。 他のゲームではこの処理が必要でない可能性が高いため、ゲームの仕様を調べた上で変更、もしくは削除してください。
- セーブデータの仕様に合わせて、以下の処理を適切に定義してください。
- 記録開始時に作られたセーブデータに
start_save_{手本番号}_
の接頭語をつけて手本フォルダにコピーする - ゲーム上でのセーブを行うための関数を呼び出し、記録終了時のセーブデータを作成する
- 上記で作られたセーブデータに
end_save_{手本番号}_
の接頭語をつけて手本フォルダにコピーする - SharedData のようなセーブデータに付随するファイルがあれば、それらも全て同様にして手本フォルダにコピーする
- 記録開始時に作られたセーブデータに
load_first_savedata()
再生リストで選択した手本のうち、一番最初のものをロードしてゲームを開始する関数です。
{新規ゲーム名}/custom_core.py
> goto_load()
の実装がセーブデータの仕様によって変わりうるため、適切に変更してください。
最後には必ず wait_order()
を呼んでください。
set_debug_function()
再生開始前に必要なデバッグ機能などの設定を行う関数です。
{新規ゲーム名}/custom_core.py
に定義している Order
を利用した一連の操作処理関数を呼んでください。
必要な機能の例として、無敵化、ランダムイベントの確定発生、一撃必殺などが挙げられます。
最後には必ず wait_order()
を呼んでください。
load_checked_demo()
GUI 上でチェックを入れて選択した手本を再生リストに追加する関数です。
- サンプルゲームではセーブデータのファイル名が毎回変わるため、
選択した手本に対応するセーブデータのパス、各セーブデータの接頭語を取り除いたファイル名 (元のファイル名)、
各セーブデータに対応する SharedData のパスをの 3 つを組にして、リスト
replay_savelist
に格納しています。 セーブデータのファイル名が固定である場合、このリストは不要なので削除してください。 - 同じく、ゲームに SharedData に相当するファイルが存在しない場合は、これに関連する部分を削除してください。
{新規ゲーム名}/custom_core.py
__init__()
CustomComThread
インスタンスの初期化関数です。
self.last_button
のリスト長を、手本データに操作内容を記録する必要のあるボタン数に合わせてください。# ゲーム固有のデバッグ機能に関する変数
以降に、Order
を利用した再生開始前のデバッグ機能の設定で必要な変数を適宜追加してください。
record_demo()
記録モードにおいて、毎フレームの記録内容 (人間のパッド操作内容、ゲーム内部データなど) をリストに蓄積しておく関数です。 記録終了時にここで蓄積された内容が csv 出力されます。
ゲームに合わせて必要な情報を
self.demo_data
に追加してください。 基本的に、以下の内容は全てのゲームで共通して記録する必要があると考えられます。- プレイヤー座標 (x, y, z)
- プレイヤーの向き (水平方向)
- カメラの角度 (水平方向、垂直方向)
- ゲームで割り振られている 全ての ボタンの入力値
- 現在メッセージ (会話中や看板を読んだ時などのウィンドウ) が表示されているか
- 表示中のメッセージ内容を区別するためのゲーム内部で割り振られた ID
その他、ゲームの自動攻略に必要なゲーム内部情報を追加で記録してください。 なお、事前にゲーム側からそのような情報を Python 側に送信できるようにしておく必要があります。 イベントスキップ時に行われる手本の行飛ばしが厄介になり得るため、 イベントの発生 / 発生中 / 終了に関わる内部データはできるだけ保存しておくと、イベントスキップ時の操作ルールを構築する際に役立つと思われます。
また、ゲームから送信されるデータの形式が bool 値の場合、そのまま保存すると
TRUE/FALSE
の文字列 (string) で書き込まれます。 これらは全て int に変換してから 書き込む必要があることに注意してください。サンプルゲームの仕様の関係から、現在いるレベルに応じて csv に記録する内容を変えています。 他のゲームでは必要ない処理である可能性があるので、仕様を確認した上で変更、および削除してください。
get_current_level()
現在の画面がタイトル画面 (開発用の起動画面なども含む) なのか、もしくはゲームプレイ画面なのかを返す関数です。
ゲームから Python に送信される情報の内容に合わせて、適切な条件式を組んで menu
か game
のどちらかを返してください。
他ツールでも同様の判定が必要とされるため、そちらで既に対応済みなら同じものを流用するだけで問題ありません。
record_event_log()
特定のイベントやアクションの終了を検知してログに記録する処理です。
この機能を使用するには、あらかじめゲームから Python に各種イベントの完了シグナルを送信できるようにする必要があります。
必要に応じて、受け取ったメッセージを self.logger.write_info()
関数を使用してログ出力してください。
search_latest_savedata()
サンプルゲームのセーブデータの仕様に対応するための専用関数です。詳細な説明はコードを参照してください。 他のゲームでは必要ない処理である可能性があるので、仕様を確認した上で変更、および削除してください。
copy_tmp_savedata()
再生に失敗した時に使われる復帰地点用の一時セーブデータを手本フォルダにコピーする関数です。 この関数はゲーム上でのセーブデータ作成が行われた直後に呼ばれます。 その時点で最新のセーブデータのパスを適切に取得し、これを手本フォルダにコピーしてください。
delete_tmp_savedata()
再生失敗した時のリトライに用いられるセーブデータを削除する関数です。
- サンプルゲームではセーブデータのファイル名が可変文字列であるために、再生を繰り返すとリトライ用のセーブデータが大量に作られてしまいます。 セーブデータの仕様によっては、そもそもファイルが増殖することはないので、この関数自体を削除してください。
- もしこの関数の機能が必要な場合は、手本フォルダから削除すべきセーブデータを正規表現で検索する必要があります。
tmp_list = glob.glob(self.demo_dir + "_tmp_recoverypoint_hoge")
のhoge
部分をセーブデータのファイル名の形式に合わせて適切に変更してください。
load_discrete_savedata()
現在の手本の終了座標と次の手本の開始座標が一致しない時、次の手本のセーブデータをロードしてから開始するための関数です。
self.goto_load()
関数の定義に応じて、引数を適切に変更してください。
セーブデータのファイル名が固定である場合、その時に必要なセーブデータを手本フォルダからコピーする必要があると考えられます。
retry_current_demo()
再生失敗判定となったとき、一時セーブデータを使用して現在の手本の再生を最初からやり直すための関数です。
self.goto_load()
関数の定義に応じて、引数を適切に変更してください。
セーブデータのファイル名が固定である場合、その時に必要なセーブデータを手本フォルダからコピーする必要があると考えられます。
load_next_savedata()
セーブデータをロードしてから手本の再生を行うための関数です。
ループ再生が指定されている時に、最後の手本から最初の手本まで戻したり、
特定の手本再生が連続で失敗した時に、その手本をスキップして次の手本から再開したりする時に用いられます。
self.goto_load()
関数の定義に応じて、引数を適切に変更してください。
セーブデータのファイル名が固定である場合、その時に必要なセーブデータを手本フォルダからコピーする必要があると考えられます。
order 関連
タイトル画面/ゲーム内メニュー/デバッグメニュー などのアウトゲーム的な操作 (主に記録/再生開始前後の設定に必要な操作) を行う関数群です。
# custom_order.py を利用した各種メニュー操作
とコメントが書かれている部分以降がこれに該当します。
後述の custom_order.py
で定義した細かな単位の操作を組み合わせ、特定のことを行うために必要な一連の操作を定義してください。
例としてサンプルゲームでは以下のものが用意されています。
- ゲームを最初から、もしくは作成済みのセーブデータから開始するためのパッド操作
- ゲーム内メニューでセーブを行うためのパッド操作
- 特定のデバッグ機能を ON/OFF するためのパッド操作
また、セーブデータのロードが毎回同じパッド操作で実現できる場合、
goto_load()
関数の引数 save_fname_no_ext
(読み込むべきセーブデータの引数なしファイル名) や、
これを受け取っている SelectSaveData()
関数の引数に対応する値が存在しないため、関数の引数や定義を適切に変更してください。
{新規ゲーム名}/custom_order.py
タイトル画面 / ゲーム内メニュー / デバッグメニュー などで必要な操作を細かな単位に分解したものが定義されています。
ゲームに合わせて適切なものを base_order.py
の Order
クラスを継承したクラスとして用意してください。
ここで必要とされるコードの変更は、ほとんどがゲームによって全く異なると考えられ、決まった手順を書くことはできません。
ゲームの仕様を調べ、それに合うように操作内容の適切なルール化を行ってください。
例としてサンプルゲームでは以下のものが用意されています。
- 特定の項目 (引数と一致する文字列) を選択する
- メニューの開閉を行う
- 指定したボタンを 1 回押す など
基本的な方針は、ゲーム仕様に合わせて以下の作業を行うことになります。
- 各関数のボタン操作 (どのボタンが決定、戻る、メニューオープンなどに対応しているのか) を変更する
- 各処理を行うか否かの判断材料となる
game_data
の参照部分 (if の条件に相当) を適切な情報に変更する - その他の
game_data
を参照している部分も適切な情報に変更する
ゲームによってはこれだけでは不十分である場合も考えられるので、既存の実装を参考に、必要な機能を追加してください。
Order
を継承しているクラスの多くは、Collision Checker や Map Scanner などと共通して使えるため、
他のツールで既に変更対応が済んでいれば流用するだけでよいです。
ただし、他のツールでは必要のない操作が本ツールでは必要となることも考えられるので注意してください。
{新規ゲーム名}/custom_agents.py
再生モードにおいて、手本を参照しながらプレイヤーの行動内容 (=パッド入力内容) を決定するためのコードです。
この実装はゲームによって 非常に大きく左右される ものであり、特に後述の step
関数については作業手順を一般化することが不可能です。
実際にゲームをプレーしたり、ゲームの仕様書を調べたりしながら、ゲームの攻略に必要な全ての行動を上手くルール化してください。
エージェントの行動アルゴリズムを解説した資料や、
コード内のコメントにもルール設計のヒントが書かれています。適宜参照してください。
パラメータの調整
後述の パラメータ設定関連 > {新規ゲーム名}.yaml において、 プレイヤーの移動速度に関するパラメータ はゲームの仕様を調べてあらかじめ調整を済ませてください。
load_demo()
記録した手本の csv を読み込み、再生時に扱いやすいようにデータを加工する関数です。
- csv から各値を読み込む
tmp_row = list(map(float, row[0:23]))
の部分で、 csv に含まれる値の数 (列数) に応じて index 範囲を適切に変更してください。 なお、csv の情報中に数値以外、つまり文字列が含まれる場合は float への変換ができないので、 列データを別途読み込んでtmp_row
に追加 (append
) してください。 add_index_to_dict
- csv 中に存在する全てのイベント開始行と終了行を対応させるための dict を作成するための関数です。 各イベントに固有の ID が割り振られているならそれを key として、value には開始行と終了行の set にするのがよいです。 これを利用して、再生時には dict の key に含まれる ID のイベントが発生し、現在参照している手本の行が value のイベント開始行 index と等しい場合、value のイベント終了行 index までスキップするという処理が走ります。 ただ、ゲームによってはこれでイベントのスキップが上手く行かない可能性もあるため、 custom_core.py > record_demo でも説明したように追加で手本に記録した値が存在すれば、それらもセットにして格納してください。
イベントスキップ時のdata_index飛ばしのために (略)
とコメントが書かれている部分は、for ループで手本を一行ずつ読み込んでいく過程で、 現在の行がイベントの開始/終了タイミングなのかどうかを判定している処理です。この判定ルールをゲームに合わせて適切に変更してください。
next_move_checkpoint_data()
手本のルートに従った移動以外を行わない状態 (コード上では self.step_state == "move_checkpoint"
) の時に、
ルート通りの移動を実現するために一定間隔で設定される目標座標を更新する関数です。
以下の説明を元に、ゲームに合わせて適切な目標座標設定ルールを記述してください。
- 手本中で移動以外の行動をしていない時は
demo_xxx.csv
のデータをconfig.checkpoint_interval
で指定したフレーム数おき、 つまりこのフレーム数と同じだけの行数おきに読み、その行に書かれている座標を目標座標に設定すればよいです。 - 移動以外の特殊な行動が挟まる (例: ジャンプ、会話、その他の記録時と座標を合わせて行う必要がある特殊行動) 時は、
上記の「
config.checkpoint_interval
で指定したフレーム数おきにデータを読む」ルールを無視し、 それらの行動が開始される瞬間の座標を目標座標に設定する必要があります。
この「ルール無視」はコード中のif (elif) ~ break
の部分に相当し、移動以外の特殊な行動 として定義する内容、 及びdemo_xxx.csv
の内容から いつそれらが開始したか を判定するルールをゲームに合わせてカスタマイズしてください。
step()
手本データと毎フレーム送信されるゲーム内情報を元に、実際にゲームに反映するパッド操作内容を決定している関数です。
- ステートマシンで行動を管理しており、
self.step_state
が現在の state を表します。 ゲームに応じて state の種類、ゲームから送信された情報から どのような条件を満たしたときに state を遷移させるか、 各 state における行動内容 をカスタマイズしてください。この関数から呼ばれている他の関数も適切に変更する必要があります。 - イベントスキップを表す state (
self.step_state == "message_skip"
) は 任意の state から飛び道具的に遷移する可能性があるので、ステートマシンを表す if 文の階層の中では最上位に記述しておくとよいです。 - ほとんどのゲームに共通して用いることができると考えられる state においても、
ゲームから送信されているデータを参照している部分 (
game_data["Alfort"]["hoge"]
など) は適切な値に変える必要があります。 また、ゲーム特有の細かな仕様の影響で、if 文の分岐条件を変更したり、例外処理を追加したりする必要があると考えられます。
lib/utils/assert_window_watcher.py
ゲームのウィンドウを常に監視し、ポップアップウィンドウが出現したら自動で閉じるためのコードです。
サンプルゲームでは現在は使用されていませんが、今後エラー状況の記録が必要になる場合を想定しサンプルとして実装してあります。 なお、以下で説明する変更を行っても動かない可能性が考えられるため、適切にカスタマイズしてください。
__set_foreground_handler
# ウィンドウのタイトル名が XXX から始まる
とコメントにある通り、if name.find('Alfort') >= 0:
の部分で監視対象となるゲームのウィンドウ名の先頭文字列を指定してください。
get_window_handle
handle = win32gui.FindWindow(32770, "ASSERT")
の部分で、第 2 引数を自動で閉じたいウィンドウの名前を指定してください。 また、第 1 引数に閉じたいウィンドウのウィンドウクラスを指定してください。備考ウィンドウ クラスについて - Win32 apps
https://learn.microsoft.com/ja-jp/windows/win32/winmsg/about-window-classes
パラメータ設定関連
data/conf/{新規ゲーム名}.yaml
各種パラメータを記述するためのファイルです。
# ===== ゲーム共通のパラメータ =====
とコメントが書かれた部分の項目は、全てのゲームに共通して必要なパラメータだと考えられます。
プレイヤーの行動に関連するパラメータについては ゲーム実装における プレイヤーの移動パラメータも調べつつ、適切な値を設定してください。
特定座標への到着判定にかかわるパラメータは、正確な移動が求められたり、移動速度が遅いほど値を小さくする必要があります。 逆に、各移動方法の単位時間あたりの移動量よりは大きな値にしなければなりません (1 フレーム当たりのプレイヤー移動量が 100 のとき、今の座標が [0, 0, 0]、目標座標が [50, 0, 0]、到着判定の許容誤差を 10 だった場合を考えてみてください)。また、できるだけ大きな値に設定した方が、通常の移動がスムーズになります。
# ===== ゲーム固有のパラメータ =====
とコメントが書かれた部分の項目は、ゲームごとの仕様に合わせて必要となるパラメータです。
必要に応じて追加、削除してください。
{新規ゲーム名}/custom_config.py
class Config
で {新規ゲーム名}.yaml
で使っているパラメータの名前を定義しています。
パラメータの追加や削除を行った場合は、ここの項目も調節してください。
なお、ここで定義したパラメータと {新規ゲーム名}.yaml
に記述しているパラメータの間に過不足がある場合は、エラーで止まる仕様になっています。