2011/06/17
KAICHO: s_naray[at]yahoo[dot]co[dot]jp
※SPAM防止のため捻ってある

スクリプタのためのノベルゲームテスト技法

■はじめに

本文書は、ノベルゲームを作る上で、スクリプタが実施すべきテストについて 述べる。対象は「システム」(=プログラム)であり、立ち絵とかシナリオ分岐とかの テスト方法にはあんま触っていないことに注意。

これを実施したからといって、バグがモノスゴ減って安定したゲームが ばんばん量産できるようになるわけではないけれど、これを実践しないと 間違いなくバギーでトゥーバッドでユーアーベリーノーなゲームを 量産することになっちゃう、といういつもどおりの微妙な体(てい)で。

なお、語句は一般のソフトウェアテストのそれと統一されてないことがあるのを 予め断っておく。ニュアンスが伝わればイインダヨ!

■何故テスト(デバッグ)が必要なのか?

案外気にしてない人が多いので一応書いておく。というかこれ気にせずにテスト しちゃダメだと思う。

もちろん、テストにより不具合を事前に叩き出し、これを修正するのが直接的な目的。 しかし、もっと高い視点から見よう。さすれば見えてくる、主な理由は以下の二つ。

バグバグで進行すら困難なゲームを遊びたいって人は居ない。それでも 遊びたいと思うまでに魅力的なシナリオがあれば別だが、仮にそうだとしても、 じゃぁシナリオがおなじレベルでバグバグなゲームと安定してるゲームと どっちプレイしたいかと聞かれれば前者を選ぶ人はいないだろう。 ユーザがシナリオに集中するためには、システムは空気のように動き、そ の存在を感じさせないことが大事。バグなんか言語道断だ。

バギーなゲームを作り上げてしまったとしよう。インターネット全盛の昨今、 「XXってゲームはバグ多くってさー…」という情報は、本当にあっという間に 広まる。さすればこれを見て購入を躊躇うユーザが増加することは避けられない。 当然、売り上げが落ちる。「リリース直後に修正パッチを出したからO.K.」ではない。 世の中には、パッチの当て方が判らなかったり、 そもそもそういうもので後から品質改善ができるということすら知らない人もいる。 パッチリリースの手間とコスト、テスト工数もバカにはならない。 だから、最初からカンペキを目指さなければならないのだ。

もっと言えば、コトは更に重篤な場合だってある。酷いバグを残したまま 製品をリリースした場合、 「XXってゲームの○○ってスクリプタは大バグ残したままリリースしやがった」 という事実が残る。アナタの職業的な評判はがた落ち、下手すると死活問題に 発展しかねない。

更に更に厳しいことを言えば、周囲への悪影響だって甚大だ。バグ出した 本人だけが迷惑を被るなら自業自得だが、「△△ってメーカーのゲームは いつもバグが多い」なんてことがマコトシヤカに囁かれるようになると、 メーカー・ブランド存亡の危機にすら発展しうる。 それもこれも、全てはキミがちゃんとテストしなかったからだ! 一体どうしてくれるんだッ!

…とか言いながら、一応フォローを。我輩、 前工程(シナリオ、演出、絵、サウンドなど)が遅れに遅れた挙句、諸般の事情で 締め切りを延期することができず、 十分なテスト時間が取れなくて酷いバグが沢山残ったゲームをリリースしちゃった例を 知っている。ので、バグがあることが必ずしもスクリプタが悪いとイコールなわけでは ないことは「我輩は」理解できる。ユーザに理解してもらうのはムリだけどね。

スクリプタにできることは、不具合のないシステムを組み上げることだ。 シナリオはシナリオライタが作る。デザインはデザイナーが作る。 他の部分に残念なところがあったとしても、少なくともスクリプトが残念なことに ならないようにしよう。小指の先ほどの大きさのスクリプタプライドにかけて。

そしてもうひとつ、「全てのプログラムにはバグがある」という、 悲観的だが否定しきれない現実も忘れずに。「ま、動くだろ」と安易に考えた 時点で負けは確定する。負けるな!

■テストの勘所

何のためにテストをするのかと問われれば、不具合をなくすため、と答えるのが 世の定め。でも、じゃぁどうやってテストするのかと問われて論理的な回答が 出来る人は少ないように思う。知っとけ!テストとはロジカルに実行して初めて 意味があるものなのだよ?

というところで、以下にテストの勘所を示す。

■実際のテスト方法

ここからは、じゃあ実際テストする時どうやっていくべきか、を具体的に述べる。 正直、こういうテストは結構面倒だ。面倒なんだけど、それを乗り越えて実施する ことで、一つ上の品質が手に入るのだ、と思って頂ければ幸いであるよ。

テストは、「単体・モジュールテスト」、「プログラムテスト」、「トータルテスト」 の三段階に分けて行う。最初は細部から、徐々に全体をテストする、ような形であると 理解していただければ幸い。

  1. 最初にすること

    以下は必ず最初に実行すること!これだけでずいぶん能率が上がる。是非。

    1. 起動時に --debug オプションを付けてデバッグモードで起動する
    2. デバッグコンソールを表示しておく

  2. 単体・モジュールテスト

    まず、最小分割単位でテストを行う。KAGなら「他のマクロをを呼び出さないマクロ」、 TJS2なら「他の自作関数を呼び出さない関数」が最小分割単位だ。 他のマクロや関数と関わっているようなものは、こういうシンプルな最小部品の テストがすべて終わってからテストを開始すること。そうしないと、 直していくうちにどんどん修正箇所と再テスト箇所が増加してハマっていく。

    例えば、ここでは、以下のようなTJS関数をテストすることを考える。 この関数は、「指定した画像ファイルが存在するかどうかを調査して、true/falseを 返す。ただし、画像ファイルは拡張子を指定する必要はなく(指定してもよい)、 拡張子 png/tlg/jpg/bmp については自動的に付加してチェックする」もの。 なんでこんなものが必要なのかは、この次のマクロを見れば判るから とりあえずここでは述べない。

    [iscript]
    
    // 指定した画像(拡張子あってもなくてもいい)が存在するかどうか
    function existGraph(g)
    {
            var q = Storages.isExistentStorage;
            return q(g) || q(g+'.png') || q(g+'.tlg') || q(g+'.jpg') || q(g+'.bmp');
    }
    // "abc.png" と "abc.png.tlg" が存在したら区別つかんがな、とか、
    // 指定されたファイルがテキストファイルだったらどうすんの、という指摘はガン無視
    
    [endscript]
    

    テストの肝は、簡単に言えばその部品中の すべてのコード、分岐が網羅的に実行されるように 入力を選んでその部品を呼び出し、出力が予想と同じかどうかを確認すること。 上のexistGraph()関数なら、拡張子なし・拡張子あり(.png、.tlg、 .jpg、.bmp)それぞれについて、ファイルが存在する・しない場合に、正しく 結果が返ってくるかをチェックすることになる。

    テスト前に、簡単でいいので、なにをテストし、どういう結果が期待されているか、 という表を書いて欲しい。

    呼び出し方ファイル有無予想される返り値
    existGraph("abc")画像ファイル"abc*"が存在しないfalse
    existGraph("abc")画像ファイル"abc"が存在するtrue
    existGraph("abc")画像ファイル"abc.png"が存在するtrue
    existGraph("abc")画像ファイル"abc.tlg"が存在するtrue
    existGraph("abc")画像ファイル"abc.jpg"が存在するtrue
    existGraph("abc")画像ファイル"abc.bmp"が存在するtrue

    で、あとは 画像ファイルの有無をこの表に従って変更しながら、TJS レベルで existGraph("abc") を呼び出しまくる。計6回。そのくらいはさくさくやろう。 これで、existGraph()が意図したとおりに動くかどうか、全ての分岐を舐めて 確認できたはずだ。

    それが終わったら、次にこの関数を利用する関数・マクロををテストする。 ここでは、KAGマクロ[image_load]を対象としよう。このマクロは、 「画像ファイルが存在していたら指定されたレイヤに読み込み、そうでなければ 白画面に画像ファイル名を表示」する。ゲーム作り始めのころ、素材が揃わない 時にもエラーなく実行できて便利。

    ; 指定された画像を指定されたレイヤに読み込む。ファイルが無い場合は白地に
    ; ファイル名文字列を表示する
    ; [image_load layer=0(def) page=fore(def) left=0(def) top=0(def) storage=]
    [macro name="image_load"]
    ; デフォルト値設定
    [eval exp="mp.layer = 0"      cond="mp.layer   === void"]
    [eval exp="mp.page  = 'fore'" cond="mp.page    === void"]
    [eval exp="mp.left  = 0"      cond="mp.left    === void"]
    [eval exp="mp.top   = 0"      cond="mp.top     === void"]
    [layopt *]
    [if exp="existGraph(mp.storage)"]
    	; ファイルが存在した場合
            [image *]
    [else]
    	; ファイルが存在しなかった場合
    	; トータルテストの際は、ここを以下一行に変更することを勧める
    	; [eval exp="System.inform('エラー、画像ファイルがありません: ' + mp.storage)"]
    	[image * storage="white"]
    	[ptext * text=%storage x=32 y=32 size=32]
    [endif]
    [endmacro]
    

    さて、上のマクロをテストするにはどうしたらいいだろう。「全部舐める」ように 入力を決め、想定した出力になるかどうかを確認すればいいのだから、 そういう形で入出力を決めるべき。ここでは、existGraph()関数は 「カンペキに動くはず」と考えてよい。上で既にテスト済みだからこそできる判断。

    呼び出し方ファイル有無予想される動作
    [image_load storage=abc layer=0 page=fore]画像ファイル"abc.*"が存在しないlayer=0 page=foreに白地に"abc"が表示される
    [image_load storage=abc]画像ファイル"abc.png"が存在するlayer=0 page=fore "abc.png"が表示される
    [image_load storage=abc layer=1 top=100 left=100]画像ファイル"abc.png"が存在するlayer=1 page=fore top=100 left=100 に"abc.png"が表示される

    このくらいのマクロにバグが入るわけないじゃん、という方が居るかもしれない。 そういう方のために、上のマクロにはあえてバグを残しておいた。 二番目のテストでエラーになる。しかし、結構深いお話なので、 コード見て判る人は多分居ないだろう。試して、エラーメッセージ読んで、 エラーの意味をかなり考えて、それで初めてバグの存在が判るようになるはずだ。 テストの重要性を身をもって知るがよいわ!

    以上を繰り返すことで、関数・マクロの小部品が、意図した通りに動作することが 確定する。確定させないままこの先に進んではいけないよ?

  3. プログラムテスト

    ある程度モジュールテストが終わると、それらを組み合わせて大きな機能単位ごとの テストができるようになる。これをプログラムテストという。ゲームで言えば、 例えば CGモード、タイトル画面、スタッフロール、ロード・セーブ画面など、 単体でほぼ完結する機能ごとにテストを実施する。これらのテストも、基本的には 単体・モジュールテストと同じ、「全パスチェックし、入力・出力が正しいか 確認する」のがテストスタンスだ。ただ、ここまで大きくなってくると、それぞれに 全く異なるテストマトリックスが必要になる。ここでは共通したテスト項目を 書いておこう。エラーが起こりやすい例を織り交ぜてある。

    メモリリークを探すのはかなり難しい。吉里吉里は画像を読み込む時にそれを キャッシュするので、見かけ上のメモリ使用量は増加しがちだからだ。 デバッグウィンドウで「System.doCompact()」を実行すると不要なメモリを 開放してくれるので、適宜これを実行してメモリ量が増加していないか確認すると よい。…これでも正確じゃないんだけど。一応、吉里吉里の コマンドラインオプションに --memusage=low とか -gclim=0 とか指定するとメモリ使わないようにはなるが、 これらもカンペキじゃないんだよね…目に見えて遅くなっちゃうし。

    表示レイヤー数が増加しているかどうかは必ずチェックすべきだ。システムボタンを 表示してるゲームではありがちなんだが、よーく調べてみると「以前のシステム ボタンを消去せずに次のシステムボタンを表示してた」なんてことがあって、 (同じ場所に重ねて表示されるので)見かけ上は全く変わらないのに、 調べてみるとトランジションのたびにボタン数が増えていた、なんてことが よくある。こういうのは、一つチェックスクリプトを書いておくことをお勧めする。 我輩は拙作TJSFunctions の中に、printLayers() という関数を用意した。これを使えば、現在 layerがどのように表示されているかがパツイチでわかる。今週のオススメ。

  4. トータルテスト

    ゲームスクリプトが全て完成し、分割して作っていたモジュール・プログラムを全て テストし終わったら、全てを組み合わせて、いよいよ最終テストに入る。 残ったバグはここで全て叩き出すことを念頭におきつつ、代表的なテスト方法を 以下に列挙する。

    autoでゲームクリアできるか、skipでゲームクリアできるか、は、ゲーム中に ゲームを停止させるような不具合がないことを確認するために実施する。あと、 autoが快適か、skipが快適か、どこか遅いところはないか、を確認することも 兼ねている。当然、立ち絵や背景の指定抜け、画像不一致などもあわせて チェックする。これはもう必須テストと言っても過言ではない。

    「高速自動マウスクリックツール」は我輩のオススメ。スクリプトが大きくなれば なるほど、タイミングの問題で不具合が発生する可能性が高くなる。 短期間での多重クリックは、裏ではスキップを呼び、ロックや排他処理が原因で 発生する不具合を叩き出す有効なツールとなる。我輩も、「ユーザ先では 時々起こるけど、自分のところではどうしても再現できなかった問題」を こういうツールを使って再現させたことが何度かある。そんなに時間は かからない(そもそも高速スキップされちゃうから)ので、是非一度試して 頂きたい。 我輩はFortuna を使わせてもらっている。Home/EndでOn/Offが切り替えられるのがとても便利。

    「再起動なくゲームを複数回クリアする」ことの目的は、メモリリークの発見。 クリアし終えたら、Windowsのタスクマネージャを起動して、krkr.eXe(または アナタのゲームのexe名)のメモリ使用量をチェックする(System.doComact()を 忘れずに)。どのくらいになるかは ケースバイケースだが、起動直後、1ループ目、2ループ目と比較して、 明らかにメモリ使用量が増加しているようなら、 どこかにメモリリークがあるかもしれない、と疑うべきだ。 通常は多くてもゲーム全体で500MBを超えるメモリが消費されることはない。 起動時と今との差分が1GBを超えるようなら、よっぽど凝ったゲームで無い限り それはメモリリークを含んでいる可能性が極めて高い。死ぬ気で原因を探ること。 タスクマネージャとにらめっこしながら、「何をしたらメモリ使用量が劇的に 増加するか」をチェックし、そのキッカケを発見したら、次はその動作を 細分化し、どこが原因かを探っていく。最終的には全マクロの一行ごとに[l]を 入れていくくらいの地道な作業だが、絶対に必要な作業だ。

    同時に、プレーし続けていくと徐々に操作感が悪く(反応が鈍く、重く) なっていかないかもチェックする。メモリリークなどが無い限りはそうでもないが、 前述のようにレイヤの消去忘れがあったりすると、扱うレイヤ数が増加し、 だんだん重くなることは十分ありうる。

    以上のような、とにかく色々やってみて、何度もループ回してみて、それで 問題が起こるかどうか、というのを探すのがこのテストの目的だ。ユーザは びっくりするような操作をする。それを読み、予め対応しておくのが カッコいいスクリプタ。

■それ以外のテスト

■四方山

■だからテストには時間が必要なんだ!

甘いソフトハウスだったり、締め切りが近づいたりすると、真っ先に削られて しまうのがテスト時間。なぜだ!不具合を修正するための最後の砦だというのに! 感覚では、テキスト1MBあたり、最低でも一週間、できれば二週間はテスト期間は 必要だと思う。そうでないと、十分に不具合を抽出できない。是非テスト期間を 削らないで頂きたい。お願い、ディレクターの中の人。

■おわりに

如何だったろうか。なんてめんどくさいと思ったろうか。その通りだが、 アナタはそういう世界に肩までどっぷりであることを知るべきだ。是非面倒っぷりに 負けずにまじめにテストし、不具合をどんどん退治して欲しい。それが須らく アナタの評価に繋がる(…はず…だと思う…気がする…かもしれない)。

こういうテストの概念は全てのソフトウェアに共通する。だから、本当は、 ソフトウェア工学の「テスト」部分の工学書を是非読んで欲しい。 ホワイトボックステスト・ブラックボックステストなど、なんや難しげな 用語は一杯出てくるけれども、プログラマを自称するならそのコンセプトは 掴んでおくべきだ。…ああ、 Wikipediaのソフトウェアテスト項が結構端的に書いてあっていいかもしれんね。

ちなみに、業務用のプログラムを書いたことがある人は知ってると思うが、 いわゆる世のソフトウェアエンジニアが実施するテストは、 基本は同じなれど、上で紹介したものより はるかに複雑で精緻だ。「プログラムソースコード一行ごとに10テスト必要」と 呼ばれる程にテスト項目は多く、ちょっと大きいプログラムだと電話帳何冊分もの テストマトリックスが作成される。そこまでテストしてるのに製品にはバグが残る、 ということは、つまりそれだけソフトウェアにはバグが入り込みやすいということだ。 スクリプタもこれを肝に銘じ、テストの重要さをゆめゆめ忘るることなかれ。