チーム内に多数のテスターがいる場合でも、各レベルの詳細な統計情報を収集することは(そして現代のゲームには数百もあります)単純に非現実的です。 明らかに、テストプロセスは何らかの形で自動化する必要があります。
以下は、インディーマッチ3ゲームでこれをどのように行ったかについてのストーリーです。 (注意-フットクロス!)
問題の声明
レベルテストを自動化するために、次の要件を満たす「ボット」をプログラムすることにしました。
ゲームエンジンへのバインド -ボットは、標準ゲーム中に関与するコードの同じブロックを「使用」する必要があります。 この場合、ボットと実際のプレイヤーの両方が同じゲームをプレイすることを確認できます(つまり、彼らは同じロジック、メカニズム、バグなどに対処します)。
スケーラビリティ -ボットは、既存のレベルだけでなく、将来作成できるレベルもテストできる必要があります(将来、新しいタイプのチップ、セル、敵などをゲームに追加できる場合) 。 この要件は、以前の要件とほぼ重複しています。
パフォーマンス -1つのレベルで統計を収集するには、少なくとも1000回、数百のレベルで「再生」する必要があります。これは、ボットが1つのレベルの分析に1日かからずに十分な速度で統計を再生および収集する必要があることを意味します。
信頼性 -ボットの動きは、平均的なプレーヤーの動きにほぼ近いはずです。
最初の3つのポイントは自明ですが、ボットを「人間的に」プレイさせるにはどうすればよいですか?
ボット戦略。 最初のアプローチ
プロジェクトの作業を開始する前に、さまざまなmatch3ゲームで約1000レベルのさまざまな難易度を経験しました(全体で2〜3ゲームも経験しました)。 そして、この経験を考慮して、レベルを作成する次のアプローチに焦点を合わせることにしました。
この決定は究極の真実であると主張するものではなく、欠陥がないわけではありませんが、上記の要件を満たすボットの構築に役立ちました。
良いレベルは最適な戦略を見つけるための一種のタスクであるという事実から進めることにしました。レベルを作成するレベルデザイナーはアクションの最適なシーケンスを「推測」し、プレイヤーはそれを推測する必要があります。 彼が正しく推測すれば、レベルは比較的簡単です。 プレイヤーが「故障」している場合、彼は絶望的な状況にある可能性が非常に高くなります。
たとえば、「乗算」チップがあるレベルが与えられていると仮定します(隣のチップのグループを収集しない場合、次のプレイヤーが移動する前に、隣接するセルの1つをキャプチャします)。 レベルの目標は、ボードの一番下の列にある特定のチップを下げることです。 私たちのゲームは海の冒険に捧げられています(そう、また!)。したがって、ダイバーを底まで下げる必要があり、サンゴは繁殖チップとして機能します。
プレイヤーが最初からサンゴを破壊するのに時間をかけず、代わりにダイバーをすぐに下げようとすると、サンゴが成長してレベルが通過できなくなる可能性が高くなります。 したがって、この場合、レベル設計者が「考えた」戦略は次のように言えます。「できるだけ早くサンゴを破壊し、ダイバーを下げてください」。
したがって、レベルデザイナー側とプレイヤー側の両方からレベルを渡す戦略は、さまざまな戦術的なアクション(以下では単に戦術と呼びます)間の優先順位付けにあります。 これらの戦術のセットは、このジャンルのすべてのゲームでほぼ同じです。
- できるだけ多くのチップを集めます(ポイント)
- 特定の種類のチップを収集する
- 「干渉する」チップ/セルを破壊する
- ボーナスチップを作成する
- 「ボス」を攻撃する
- 特定のチップを失望させる
- などなど...
各レベルにこれらの優先順位を設定する機能は、Excelベースのレベルエディターに追加しました。
レベルデザイナーは、レベルを作成し、ボットの戦術的な優先順位を設定します。生成されたXMLファイルには、レベルとテスト方法の両方に関する必要な情報がすべて含まれます。 図でわかるように、次の優先順位付けがレベルを解決することになっています。
- サンゴの破壊(ANTI-CORAL)
- ボーナスの作成(COLLECT BONUS)
- ダイバーの降下(DIVERS)
- 最も価値のある動きの選択(最大ポイント)
レベルをテストするとき、ボットはこれらの優先順位を考慮して、適切な動きを選択する必要があります。 戦略を決定した後、ボットをプログラムすることはすでに難しくありません。
ボットアルゴリズム
ボットは、次の単純なアルゴリズムに従って動作します。
- XMLファイルからレベルの優先順位リストをダウンロードします
- 現在可能なすべての動きを配列に保存する
- リストから次の優先順位を取ります。
- 現在の優先度に対応する配列内の移動があるかどうかを確認します
- ある場合は、最適な動きを選択します。
- そうでない場合は、GOTO 3。
- 移動が完了したら、GOTO 2
移動が戦術的な優先順位に対応するかどうかを判断するタスクについては、ここでは詳しく説明しません。 簡単に言えば、考えられる一連の動きをコンパイルするとき、各動きのパラメーター(収集されたチップの数、チップの種類、ボーナスチップが「作成」されたか、獲得されたポイント数など)が保存され、これらのパラメーターのコンプライアンスがチェックされます優先順位。
ボットが指定された回数だけレベルを「勝ち取った」後、結果の統計を表示します。レベルが何回勝ったか、平均して何回の動きをしたか、何点獲得したかなどです。 この情報に基づいて、複雑さのレベルが目的のバランスに対応しているかどうかをすでに判断できます。 ここでは、たとえば、上の写真に示されているレベルごとのプリントアウトの一部:
レベル統計:
=====================================
ストローク制限: 30
実行回数: 1000
損失率 : 63 % ( 630ラン)
失われたレベルの平均完了: 61 %
受賞時の平均ウェイ数: 24
------------------------------------
メガネ:
-------------------
最小アカウント: 2380
最大アカウント: 7480
-------------------
------------------------------------
実行統計:
-------------------
0 。 失う( 87.0 %完了)
1 失う( 12.0 %完了)
2 。 失う( 87.0 %完了)
3 。 失う( 87.0 %完了)
4 。 失う( 87.0 %完了)
5 。 勝利(レベルは26のステップで完了しました。2つ星を組み立てました。 )
6 。 失う( 75.0 %完了)
7 。 勝利(レベルは26のステップで完了しました。3つ星を組み立てました。 )
...
63%のケースでレベルが失われていることがわかります。 これはこのレベルの定量的評価であり、ゲーム内のレベルの順序のバランスを取り、決定する際に既に信頼できます。
そして、プレイヤーがそのように行動すると誰が言ったのですか?
上記では、プレーヤーが次の動きを選択し、この位置で利用可能な動きを分析し、現時点で最も優先度の高い戦術に最適なものを選択することを提案しました。 そして、この仮定に基づいて、ボットのロジックを設定します。
ただし、ここには2つの仮定があります。
- 本物のプレーヤーは、ボード上のすべての動きを分析することはまずありません。むしろ、最初に見つかった多かれ少なかれ妥当な動きを選択します。 そして、たとえボード上のすべての動きを考慮しようとしても、彼はまだ良い動きに気付かない可能性があります。
- プレイヤーは常に正確に決定して優先順位を付けることができるとはほど遠い-これには、特定のゲームとその仕組みに関する十分な知識が必要です。つまり、特定の動きごとに最適なアクションを期待することはできません。 (これはささいな問題ではありませんが、プレイヤーは自分の過ちから学び、最終的にレベルを渡すために何が重要で、何が待つことができるかを理解します)。
これらの仮定に基づいて構築されたボットは、平均としてではなく、ゲームのすべての微妙さに精通した「最適な」プレーヤーとしてプレイすることがわかります。 また、このボットによって収集された統計情報に頼ることはできません。 どうする?
ボット戦略。 二次近似
明らかに、ボットのアクションが「理想的」ではなくなるような何らかの補正係数を導入する必要があります。 ボットの動きの一部を単純にランダムにするために、単純なオプションにとどまることにしました。
ランダムな動きの数を決定する係数は、レベル設計者が目標に基づいて再度設定します。 この係数を「戦略からの逸脱」と呼びました-このレベルに0.2を設定します。
0.2の偏差は、0.2の確率(つまり、20%の場合)で、ボットがボード上のランダムな動きを選択することを意味します。 そのような偏差でレベルの統計がどのように変化したかを見てみましょう(偏差がゼロのときに以前の統計が計算されました-つまり、ボットは与えられた優先度に完全に正確に従いました):
レベル統計:
=====================================
ストローク制限: 30
実行回数: 1000
損失率 : 78 % ( 780ラン)
失われたレベルの平均完了: 56 %
受賞時の平均ウェイ数: 24
------------------------------------
メガネ:
-------------------
最小アカウント: 2130
最大アカウント: 7390
-------------------
...
失われたレベルの割合は、15%増加すると予想されました(63から78)。 失われたレベルの平均的な完成度(ダイバーの底まで下がる割合)も予想どおりに低下しました。 しかし、勝ったときの平均的な動きの数は変わっていません。
統計では、このレベルは非常に複雑であることが示されています。30の動きのうち24をよく考えて(30 * 0.2 = 6の動きは「無駄」になります)、この場合でもプレイヤーは78%のケースで負けます。
問題は残っています-このレベルの係数0.2はどこから来たのですか? 他のレベルのオッズは何ですか? レベル設計者の裁量でそれを残すことにしました。
この係数の意味は非常に単純です:「プレイヤーがこのレベルで行えるラッシュムーブの回数」。 ゲームの初期段階で単純なレベルが必要な場合、これはどのプレーヤーでも簡単に渡すことができます。0.9または1に等しいこの係数でレベルのバランスをとることができます。 、最適な戦略からのわずかな偏差またはゼロ偏差でバランス調整を実行できます。
性能
そして最後に、仕事の速さについて一言。
ボットをゲームの「エンジン」の一部にしました-設定されたフラグに応じて、プログラムはプレイヤーからの動きを待つか、動きがボットによって行われます。
ボットの最初のバージョンはかなり遅いことが判明しました-テストモードですべてのグラフィック効果がオフになり、チップがセル間を瞬時に移動したにもかかわらず、30回の移動でレベルの1000回の実行を完了するには1時間以上かかりました。
ゲームロジック全体がレンダリングサイクルに関連付けられており、テスターの作業を高速化するために1秒あたり60フレームに制限されているため、垂直同期をオフにしてFPSを「リリース」することにしました。 この方法でLibGDXフレームワークを使用します(誰かに役立つかもしれません):
cfg。 vSyncEnabled = false ;
cfg。 foregroundFPS = 0 ;
cfg。 backgroundFPS = 0 ;
新しい LwjglApplication ( 新しい YourApp ( ) 、cfg ) ;
その後、ボットは1秒あたり約1000フレームの速度ですぐに実行されました! ほとんどのレベルでは、これで5分未満でレベルを1000回「実行」できます。 正直に言って、もっと早くしたいのですが、すでにこれで作業できます。
ここで、ボットのビデオを見ることができます(残念ながら、ビデオを記録すると、FPSは約120に低下します)。
まとめ
その結果、ゲームエンジンに関連付けられたボットがあります-別のテスターコードを維持する必要はありません。
将来、新しいメカニックがゲームに追加された場合、ボットはそれらをテストするように簡単に教えることができます-レベルエディターに新しい戦術の識別子を追加し、動きを分析するときにコードに追加パラメーターを入力するだけです。
もちろん、最も難しいのは、ボットの行動が平均的なプレーヤーの行動とどれだけ一致するかを評価することです。 しかし、これは自動テスターを構築するためのあらゆるアプローチの弱点になるため、生きている人(特に対象読者)のテストをキャンセルする人はいません。