僕自身は龍が如くシリーズは、クロヒョウ2、極1、極2、0、3、4、5、6、0とやって、7はRPGだし主人公違うしなぁと思って、買うだけ買って後でやろうと積んでいたところ、CEDECのすごいテストの話を聞いて、(オリジナル版を積んでいたのに)インターナショナル版を買って始めてしまうぐらいインパクトがあり(そして積んでたのを後悔したぐらいよかった)ました。それ以降、維新極、7外伝、8は発売日に買ってプレイしてます。
こちらにその講演の詳細なレポートがこちらにあります。
https://www.famitsu.com/news/202009/11205564.html
その8の発売前に龍が如くスタジオの技術責任者の方がXのアカウントを開設して、C++のコードを投稿されていたのですが、それに対してエンプラ開発目線で意見しているようなツイートを見かけて、「いや、システムの特性全然違うから」と思い筆を取った次第です。
ユニットテストの限界
大学時代、アジャイルソフトウェア開発というかエクストリームプログラミング(XP)が日本に来た時に、僕はその本に熱狂しました。こんな開発がしてみたいと。そして自分が社会人になるころにはアジャイル開発できる会社が増えたらいいなと日本XPユーザーグループの運営委員に入ったり本を書いたり翻訳したりしたわけです。ですが、学生の身分では、実際お客さんのいる開発などはできなく、できることといえば、テストとリファクタリングのコーディング周りのプラクティスぐらいだったので、ユニットテスト周りで自分でテスティングフレームワークを作ったりしてました。
そんなときに、サークルの先輩から言われたことがひとことがありました。
「テストファーストプログラミングを見て見たけど、研究のプログラムには使えなさそうだよね」
先輩がやられていたのはニューラルネットワークか何かだったか詳細は忘れてしまいましたが、確かに、1つ1つのニューロンの動きはテストできたとしても、システム全体だと荒すぎます。精度が70%以上みたいなユニットテストはナンセンスです。あくまでも入力に対して出力もすぱっと決まる、小さいモジュールの集合体としてプログラムが作れる場合にしかテストファーストプログラミングは適用できません。そんでもって、内部のロジックを大幅に書き換えました! というときにはテストも結局捨てて作り直すことが多くなります。業務システムにありがちな「内部のロジックは変わってもインタフェースは保とう」という方針が取れるのはかなり限定的な状況といえます。
一方、テストファーストプログラミング、現代的な言葉で言うと(といってもこっちももう20年以上歴史がある)テスト駆動開発が対象としているのは業務システムです。業務システムは状態を持ちません。
「データベースがあるじゃないか」と思われるかもしれませんが、業務的なシステムからするとデータベースはあくまでも外部システムです。業務システムのユニットテストは、DB含め、観測可能な外部の状態を固定し(Arrange)、中のロジックを実行し(Act)、結果を検証する(Assertion)というのがとても浅いシステムということになります。データベースが外部の状態ということはシステム自身は状態を持たず、イミュータブルであるとみなせます。
Wikipediaより引用
ゲームのコードというのはどのようなものか
一方でゲームのコードというのはどのようなものかというと、エンプラ系のシステムと比較すると大きな特徴は2つあるかと思います
- データ駆動
- 積分
1つ目は、ゲーム開発で一番使うツールはExcelと言われるぐらい、データ中心ということです。エンプラ開発でもマスタデータはありますが、キャラクターの絵やモデル、アニメーション、音は専用のツールで作りますが、それをどのタイミングでどう使うか、ユーザーが探索するマップと倒す敵の組み合わせとか出現頻度、アニメーション、セリフなど、ユーザーが触れる体験のほとんどはデータで駆動されます。
プログラムだけ単体テストしてもあまり意味はなく、データとの組み合わせで作品ができてきます。データ側に不具合というのありえます。データ単体もバリデーションでチェックをしたりも行われたりはしますが、データとプログラムを組み合わせて初めて発現するエラーも当然あります。Rustとかの型チェックの強い言語を使って言語側だけ強化しても限界があります。
あとは積分ですね。プログラムはフレーム単位でちょっとずつ変数を加工していくのですが、どんなに単体テストで1フレームの処理を「正しいだろう」と検証しても、プレイしつづけるとおかしくなったりします。物理エンジンで、ぶつかり方によってすごい勢いでふっとんでいくのをゲームで実感したことがある人はいると思います。
フレーム単位の小さいテストを書いても全体は見えないし、10秒間600フレーム回した後にテスト、みたいなテストを書いても、結局問題の発見には程遠い品質の低いテストにしかなりません。問題発見に2時間回し続けるバグがあったとして、他の全部のテストケースのループ数を7200秒x60フレームにするのか、というとそんなことはしないですよね。自動テストの限界として、一度作られてパスしたテストは、その後新しいバグを発見することは少ないというものがあります。時間をかければかけるほどリターンは小さくなります。研究の単体テストがうまく書けないのと同じ感じかと思います。
結局、龍が如くのテストというのはソフトウェア的にみるとどんなものなのか?
公開された情報からの推測でしかないですが、あえて別の言い方をすると、「複数のテストケースを並列実行する耐久E2Eテスト」なんじゃないかと思います。ビルド周りとの連携や、チケット管理システムへの自動起票などCI/CDとの連携周りがここ最近では強化されていそうですが、コア部分を見てみると、おそらくC/C++時代にはよくお世話になった(ユーザーがMFCとかでよくはまったと思われる)ASSERTが活躍しているのではないかと思います。
スライドにもゼロ除算の例がありました。これは単純にクラッシュする例ですが、マップでは入れてはいけない建物の中にすり抜けて入ってしまった! とかはおそらく地面がないので奈落に落ちることになると思うのでZ座標がマップ中に存在する最低点よりも低いというASSERTにできると思います。変な状態を検知したらクラッシュするようなASSERTを大量に埋め込んだプログラムにしておくことで、1つの「歩く」というテストケースの中に、たくさんのテスト条件を同居させているということなのかな、と。
もちろん、「期待した目的が達成できたかどうか」というのを表現する上では大事です。「A地点に行け」といったら、数秒以内にその地点に付くはずだ、というものです。おそらくこれはPythonで書かれているというテストケースで、このPythonのテストコードとC++レイヤーのASSERTの組み合わせで、状態が積分されていく&データ駆動という、業務システムとは毛色の異なるシステムの検証が行われているのではないかと想像しています。
テスト技法は、爆発する入力の組み合わせを減らして少ないケース数で効率よくテストを行う手法と言い換えられます。境界値テストは、同じ結果になる範囲のテストケースをいくら増やしても利得は少ないよね、じゃあ減らそうとか。悪く言えば怒られない程度に手を抜く手法と言えます。
ゲームの状態の組み合わせ数は業務システムの比ではなく膨大なので、それに対応した方法になっているのではないかと思います。業務システムが扱うテストケース数なんて、それと比べたらたかが知れてますよね。
まとめ
システムの特性が大きく変われば求められるテストの性質も大きく変わります。ゲームにはゲームならではの事情はあります。基本的にはそのままこの手法を取り込む必要のない業務システムが多いのではと思います。
ただ、ASSERTを活用(しているというのは僕の予想でしかないのですが)というのはいろいろ応用できそうな気がしています、業務システムのステートフルなコンポーネントのテストとか、研究用のプログラムとか、そういうところに応用が効くのではないかと思います。数理最適化案件なんかはゲームとかとだいぶ近そうですね。AIとかもいけるかもしれん。例えば、キャッシュを持つシステムで、キャッシュがあたたまった状態で実行したときのパフォーマンスが期待値よりも上になるはず、みたいなコードは極めてステートフルといえます。HPをMAX100として、キャッシュヒットしたらHPが回復、ヒットしなかったらダメージを受けて、HPがゼロになったらエラー、みたいなのとか良さそうです。今でもスロークエリーでエラーログを出すシステムとかは多いと思いますが、同一ユーザーに対して連続で起きなければOKとか、遅さ加減を見て、すごく遅ければ一発KOとかそういうのもありな気がしますね。あんまり厳しい条件で1つでも出たらエラーとか出しすぎてもオオカミ少年になりそうですし、そういうちょっと踏ん張るテストケースは面白そうです。
龍が如くスタジオは、完全新作タイトルである7外伝を11月末、8を1月末と、通常あり得ない2ヶ月スパンで発売するという離れ技をやってのけたので、今年のCEDECではまたすごいテストの発表があるのではないかと期待しています。