2009/11/13
KAICHO: s_naray[at]yahoo[dot]co[dot]jp
※SPAM防止のため捻ってある

スクリプタのためのノベルゲームシステムの作り方

■はじめに

本文書では、吉里吉里/KAGを題材に、スクリプタの方々を対象として、ノベルゲームの システムの作り方について述べる。

正直に言えば、書きたいのは「作り方」なんて大それたものではなくて、 以下で述べる「『システム』の目的」を達成するために、 『何をどう用意したら効率的か』を、 経験則を絡めながら備忘録的にメモしたものだと思って頂ければ幸いであるよ。 とはいえ、我輩が今まで作成してきたノベルゲームでは全て同じ構成を採用しており、 特に大きな不具合も出ていないことから、そこそこ汎用性はあると考えている。 というかそう信じたい。

現実社会で、 ダメプログラマのダメプログラムに仕込まれたダメバグに いつも悩まされているためか、 やたらと愚痴っぽい文章になっていることにはご容赦頂きたい。

■『システム』とは何か

『システム』とは、目的を効率よく達成するための手段を提供する「系」である。 従って、ノベルゲームにおける『システム』は、 ノベルゲームを効率よく製作・完成させる「系」であり、 それ以外であってはならない。 この定義はよく覚えておく必要がある。システムを設計する場合、 この定義に合致しない設計思想を導入してはならない。簡単だけど忘れがち。

■『システム』の目的

ノベルゲームはシナリオテキストだけで作られるわけではない。立ち絵、表情、背景、 メッセージウィンドウ、パラメータウィンドウ、BGM、SEなどなど、さまざまな 素材を、スクリプトでつなぎ合わせて作られている。 とはいえ、ただつなぎ合わせるだけなら、 スクリプトを書く上でノウハウなど要らないし、システムなんてものも不要だ。 スクリプト中に、単純に絵や音を順次表示・再生していくようにベタ書きすればいい。

しかし、世の多くのノベルゲームはそうなっていない。何故か?それがすなわち、 『システム』を作る理由である。システムを作ることにより、 以下の三点が実現できる。

判りやすい例を示そう。メッセージ枠の表示と消去について考えてみる。 通常メッセージ枠の大きさや表示位置は決まっている。だったら、表示のたびに

(snip)

; メッセージ枠を表示
[backlay]
[position layer=message0 page=back visible=true frame="メッセージ枠" left=24 top=427 width=580 height=171 marginl=59 margint=12 marginr=42 marginb=16]
[trans method=crossfade time=200][wt]
[current layer=message0 page=fore]

(キャラクタ表示、セリフ表示など)

; メッセージ枠を消去
[backlay]
[position layer=message0 page=back frame="" color=0 opacity=0]
[trans method=crossfade time=200][wt]

(別キャラクタ表示)

; メッセージ枠を表示
[backlay]
[position layer=message0 page=back visible=true frame="メッセージ枠" left=24 top=427 width=580 height=171 marginl=59 margint=12 marginr=42 marginb=16]
[trans method=crossfade time=200][wt]
[current layer=message0 page=fore]

(更にシナリオは続く)

…などとスクリプト中に書くのは無駄の無駄無駄だし、コピー&ペーストしたとしても 間違いも入り込みやすい。 それよりは、上の一連のスクリプトをマクロ化する方がいい。 [メッセージ枠表示]及び[メッセージ枠消去]というマクロを定義し、

(snip)

[メッセージ枠表示]

(キャラクタ表示、セリフ表示など)

[メッセージ枠消去]

(別キャラクタ表示)

[メッセージ枠表示]

(更にシナリオは続く)

のようにマクロを使った方が、タイプ量は減るし、後から見てわかりやすくなるし、 バグも混入しにくくなくなるし、 メッセージウィンドウの大きさや場所を変更した場合にも、 一箇所(マクロ定義の中)を修正すればゲーム中全てに反映できて メンテナンス性も飛躍的に向上する。生産性が向上するいいことずくめ。

モノスゴい演出が画面上でぐりんぐりん動く…のは、実はシステムの目的ではない。 システムとは須らく地味なものである。 『いかに使う側(ここでは上位のスクリプト)が楽になるようにするか』、 これがシステム化への第一歩だ。 「こういうタグが吉里吉里/KAGにあればいいのに」と思ったなら、 それをマクロ化することを考えてみよう。 それが即ち、『使う側に優しいシステム』的な発想だ。

■「設計」と「コーディング」と「デバッグ」

ところで、スクリプト・プログラムを作る工程は、大きく分けて「設計」 「コーディング」「デバッグ」の三つあることをご存知だろうか。実際にはこれらが 細分化されたり、保守フェーズがあったりするけれど、ざっくり分けてこの三つ。 スクリプト・プログラムを作る上で、これらの作業順を明示しておきたい。 実際に作ったことある人にはアタリマエなので、ご存知の方は多いと思うけれども、 まぁ一応。

本当はデバッグの仕方にも、テストクライテリア決めてー、チェックリスト書いてー、 モジュールテストしてー、プログラムテストしてー、トータルテストしてー、 のような方法論はあるけれど、今回はそこまで書かない。とりあえず、 『デバッグも下層から実施する』ことだけ知っておけばいい…んじゃ…ないか…な?

■システムを作り始める前に

まずは、設計が必要だ。設計とは、難しく言えば、要求分析し、 基本仕様・機能仕様・詳細仕様を纏めることだ。もっと簡単に言えば、 「何を実現したいのか」、「そのためには何を決めておかなければならないか」、 「最終的にどのような仕様にするか」 を明確にすること。我輩がいつも使っているシステムを例に、記述してみよう。

  1. 何を実現したいのか
    ノベルゲームを効率的に作れるようにしたい。これは上の「目的」と同じ。 もう少し具体的には、シナリオテキストから実際のスクリプトの生成を簡単にしたい。 これにはいくつか条件があるが、要するに シナリオテキストと実際のスクリプト間で差分が少なくなればいいだろう。

    …などが守れれば、実現できると思ってみる。

  2. そのためには何を決めておかなければならないか
    ゲームを構成する要素を一度ばらばらに分解し、 それらについて、単純化・抽象化を進め、 目的の動作をさせるためにどのような機能が必要かを列挙する必要がある。

  3. 最終的にどのような仕様にするか
    上述2.に列挙された機能それぞれについて、 どのような関数・マクロで実装するかを明確にし、 インターフェース部分のプロファイル宣言をバッチリ決めておく。

これらのうち、1.は大体もう判ったので、以下では2.以降を考えていく。 まずは、ノベルゲームを構成する要素をばらばらに分解してみよう。

■ノベルゲームシステムを構成する要素

ノベルゲームを構成するものを、要素に分解するとどのようになるだろうか。 百論あるとは思うが、この文書では、大きく「絵」と「音」に分け、 それを更に以下のように分解してみた。

大項目中項目小項目備考
背景 最背景最も背面の背景
前景キャラクタ立ち絵よりも前に表示される背景
キャラクタ立ち絵一画面に複数表示でき、画面上では横に並ぶものとする。一キャラに複数種類の立ち絵を許容する
表情差分微笑、怒り、憮然、破顔など
漫符赤面、汗、#、!、?など
システムテキスト メッセージウィンドウ地の文やセリフを表示するウィンドウ
システムボタンセーブ・ロード・早送りなどの画面上に表示されたボタン
その他(画面上の日付窓など)画面デザイン毎に異なる
BGM-いわゆるBGM
SE-いわゆる効果音
キャラボイス-いわゆるキャラクタボイス
その他システム画面
  • サークルロゴ
  • タイトル画面
  • ロード・セーブ画面
  • コンフィグ画面
  • おまけ画面(CGギャラリー・BGMプレイヤーなど)
-ゲーム毎に全く異なり、ほぼ毎回新規作成のため、今回は考慮しない

このような要素に分解したら、中項目ごとに(小項目ごとでもいいが、 それだと細か過ぎる気がする)別々の細分化した管理システムを作り、 最後に全てを組み合わせて全体システムを構築することを考える。 即ち、中項目ごとにほぼ独立した管理システムを構築することとなる。 従って、これから構築する管理システムは、 上記ピンクで示した六つの中項目に「全体統括システム」を加えた、 以下の七つに分類される。

  1. 背景管理システム
  2. キャラクタ管理システム
  3. テキスト管理システム
  4. BGM管理システム
  5. SE管理システム
  6. キャラボイス管理システム
  7. 全体統括システム
殆どの場合、テキスト管理システム、及び全体統括システムを除いた残りの五つの 管理システムは、多くのゲームで共通であり、容易に流用可能である。 逆に言えば、その五システムを一度作成しておけば、 残り二システムを変更するだけで、他のノベルゲームへ即適用可能となり、 システム作成手間が大きく削減できる。

これらを階層的にあらわすと、こんなカンジになるだろう。 緑部分が今回「システム」と呼ぶ部分で、 シナリオスクリプトからは全てこの「システム」を通して 吉里吉里を操作するようにする。

ユーザ
シナリオスクリプト
全体統括システム
背景管理システム キャラクタ管理システム テキスト管理システム BGM管理システム SE管理システム キャラボイス管理システム
吉里吉里(KAG/TJS)

■管理システムごとに仕様を決める

大体どういう管理システムを作るかが決まったら、 管理システム毎にもう少し具体的な仕様を決める。 仕様が決まらないうちにスクリプトを作り出してはならない。 よほどプログラムに慣れた人で無い限り、ソラで書いたものが最終的にカッチリ 組み合わさってうまく動くことは決してない。どのようなものを作るかについて、 必ず文書を書くこと。

以下、例として、上の六つの管理システムのうち、 画像関係三システムと全体統括システムについて、 どのようなものを作るかを具体的に決めてみる。 シナリオとにらめっこしながら欲しい機能を列挙し、 管理システムのどこでそれを実装するかを分類すれば、自ずと表ができるはずだ。 以下に簡単な例を示す。

管理システム実装すべき機能機能詳細
背景管理システム背景表示背景を表示・非表示する
画面遷移[trans]タグを実行するラッパマクロ
キャラクタ管理システムキャラ表示キャラクタを画面上にフェード表示する
キャラ消去キャラクタを画面からフェード消去する
キャラ移動キャラクタの立ち位置を変更する
全キャラ消去画面上のキャラクタを全て消去する
テキスト管理システムメッセージ枠表示メッセージ枠をフェード表示する
メッセージ枠消去メッセージ枠をフェード消去する
改行メッセージウインドウ内でテキストを改行する
改ページメッセージウインドウ内でテキスト改ページする
全体統括システム場面切替場面を切り替える。表示されていた全キャラクタを消去し、メッセージ枠も消してから場面を切り替え、メッセージ枠を再び表示する
全フェード背景・キャラクタ・テキストを全て同時にフェードする
キャラ表示・消去キャラクタを表示・消去する。表示前には自動的にメッセージ枠を消し、表示後には自動的にメッセージ枠を再表示する

『実装すべき機能』で述べた機能は、 全て1マクロで実行できるように纏めるのが望ましい。 そうすることで、シナリオスクリプトのタイプ量が劇的に減少し、 後から読んでも判りやすいスクリプトになるからだ。

ということを前提に、以下、もう少し詳しい仕様を決めていく。 なお、ここで述べる要求仕様・マクロ仕様はそれぞれあくまで例であり、 異様に非常に簡単になっている。 実際にはもっともっと複雑で厳密な仕様定義が必要となることはご了承頂きたい。

  1. 全体仕様

  2. 背景管理システム
    要求仕様
    背景を表示する。消去には特別のマクロは用意せず、 clear.pngという透明画像をあらかじめ用意しておき、 これを指定することで消去を実現する。 無指定で1000msのcrossfadeで表示し、表示時間は指定できるようにする。 crossfadeはボタンクリックなどでスキップ可能にする。

    マクロ仕様
    今回は、背景は一枚しか使用しないので、layer=base を背景専用とする。 テキスト・キャラクタを含めた[trans]を統合的に実行する [画面遷移]マクロも用意する。 遷移=falseで、[backlay]と[trans]を省略する。 これは、背景・キャラクタ・テキストの同時transを実行するための前準備に使う。

    [背景表示 画像=<背景画像> 時間=<指定>|1000 遷移=<false>|true]
    [画面遷移 時間=<指定>|1000]
    

  3. キャラクタ管理システム
    要求仕様
    キャラクタ立ち絵を表示・消去する。立ち絵サイズは300x480であり、 画面の中央・右・左の三つの位置に表示することができる。 無指定で300msのcrossfadeで表示し、表示時間は指定できるようにする。 crossfadeはボタンクリックなどでスキップ可能にする。 layer=などは指定しなくても、自動的に空きレイヤーを探して使用する。

    マクロ仕様
    キャラクタは最大三人表示するため、layer=[012]のいずれかを使用する。 空きレイヤーを自動的に探して使用するため、引数にlayer=は存在しない。 背景管理システムと同様、遷移=falseで、[backlay]と[trans]を省略する。

    [キャラ表示 画像=<キャラ画像> 位置=<左|中央|右> 時間=<指定>|300 遷移=<false>|true]
    [キャラ消去 位置=<左|中央|右> 時間=<指定>|300 遷移=<false>|true]
    [キャラ移動 from=<左|中央|右> to=<左|中央|右> 時間=<指定>|300 遷移=<false>|true]
    [全キャラ消去 時間=<指定>|300 遷移=<false>|true]
    

  4. テキスト管理システム
    要求仕様
    背景・立ち絵以外の、画面上に定型的に存在するオブジェクトを 操作する。メッセージウィンドウ、パラメータ、日付表示、 キャラクタ名表示窓など。 無指定で200msのcrossfadeで表示し、表示時間は指定できるようにする。 crossfadeはボタンクリックなどでスキップ可能にする。 改行、改ページはそれぞれひとつのマクロとして定義し、 改ページではセーブできる。

    マクロ仕様
    今回は、メッセージウィンドウだけを表示・非表示にするマクロを作成する。 テキストレイヤにはlayer=message0を使用する。 改行、改ページはそれぞれ[nl](newline)、[np](newpage)というマクロで実装し、 [np]内には[label](どこでもセーブプラグイン)を指定して、 [np]部分でセーブを可能にする。 背景管理システムと同様、遷移=falseで、[backlay]と[trans]を省略する。

    [メッセージ枠表示 遷移=<false>|true]
    [メッセージ枠消去 遷移=<false>|true]
    [nl]
    [np]
    

  5. 全体統括システム
    要求仕様
    背景・キャラクタ・テキストの三つを統括して管理・処理する。 場面変更・三つ同時のフェードアウトなどの動作を実装する。 実際のスクリプトからは、極力このシステムを通して画面を操作する。

    マクロ仕様
    場面を切り替えるマクロ、画面全体をフェードするマクロ、 統合的にキャラクタを表示・消去するマクロを用意する。 これらは、上で定義してきた個別のマクロとは違い、 ゲーム画面上での遷移を考慮して、 背景・キャラクタ・テキストの三つを統合的に取り扱う。 [場面切替]マクロは、 全キャラクタ消去→ウィンドウ枠消去→背景表示→ウィンドウ枠表示 の一連の処理を行う。 [キャラ]マクロは、画像を指定すればキャラクタを ウィンドウ枠消去→キャラクタ表示→ウインドウ枠表示 の順で処理しつつ表示し、画像が指定していなければ、キャラクタを ウィンドウ枠消去→キャラクタ消去→ウインドウ枠表示 の順で処理しつつ消去する。

    シナリオスクリプトの殆どの動作は、このセクションに書かれたマクロだけで できるように、厳選しかつ高機能なマクロを定義すること。

    [場面切替 画像=背景画像 遷移=<false>|true 時間=<指定>|1000]
    [全フェード 画像=背景画像 遷移=<false>|true 時間=<指定>|1000]
    [キャラ 画像=<キャラ画像> 位置=<左|中|右> 時間=<指定>|300]
    

■実装例

上記を実装した例を作成したので、 ダウンロードして試してみて欲しい。 吉里吉里の実行バイナリは入っていないので、 data ディレクトリが存在するディレクトリに krkr.exe(多分Ver2.30) をコピーして実行して欲しい。 first.ks を見て、シナリオスクリプトはここまで単純化できることを 知って頂ければ幸いであるよ。

■アップデートパッチについて(お小言編)

リリース後は、ノベルゲームといえどメンテナンスモードに入る。 メンテナンスモードとは、「何も無い限り何もしない」モードだ。とはいえ、 致命的なバグが発見されたり、どうしても新機能を追加したいのであれば この限りではない。そういう場合、アップデートパッチを提供することになる。

しかし、だ。 いきなり否定してしまって恐縮だが、 本来、パッチなんてものは提供すべきではないことを知らねばならない。 これは『不具合を修正するな』と言っているわけではない。 そもそも最初から不具合を作りこまないようにせよ、と言っていると思って欲しい。

一般ゲームも含め、世の供給側の人々は、安易にパッチリリースに頼りすぎている。 何故最初からカンペキを目指さないのか、 何故目の前にある既知のバグを全て取り去る前にリリースしてしまうのか。 特に同人では締め切りは好きなだけ延ばせるのだから、 『冬がダメなら次の夏で』くらいの気持ちで、 既知の不具合が全て解消するまで、リリースを伸ばすべきだと思う。 どうしても取れないバグは、アナタの技術不足が原因だ。 それをユーザに押し付けるべきではない。

製品というものは世に出た瞬間から『製品』である。 全てをWebで、しかも無償で提供するので無い限り、 『製品』は変わらないからこそ『製品』なのであって、 パッチという形での機能・シナリオの修正・追加は極力すべきではない (ただし、 ネットワーク経由で接続されることが大前提になっているならこの限りではない)。 これには、本質的には以下のような理由がある。

パッチのリリース可否は、 以上の「ユーザに負わせるリスク」を熟考した上で判断することを強くお勧めする。 ユーザから要求があれば、アップデートが終了した完全版メディアを、 (元メディアと交換などの条件で)無償配布するくらいの覚悟で臨んで欲しい。

もうひとつ。スクリプタ・プログラマの評価は、以下の二つで絶対的に決まる。

  1. ユーザからの(デザインを除く)システムに対する不平不満の数と質
  2. 作りこんだバグの数と質

ここには、バグの修正速度や修正の正確性は含まれない。 スクリプタ・プログラマは、バグを作りこんだ時点で圧倒的に負けだからだ。 バグは判明したら直せばいい、システムは後から改善すればいい、 と思っている人は、即刻スクリプタ・プログラマを降りて欲しい。 常に最善を目指す努力をしない人間は、製作現場には必要ない。 否、周囲の足を引っ張りかねないから、居ない方がいい。

自信と、それに見合う実力を持って仕事をしよう。

■アップデートパッチについて(実践編)

アップデートパッチの提供動機には、以下の四つが挙げられる。

  1. 修正:致命的なバグが発見され、修正する必要がある
  2. 追加:どうしても新機能(CGモードなど)を追加したい
  3. 修正:気に入らなかったシナリオを修正したい
  4. 追加:新たなシナリオを追加したい

このように、パッチ提供動機は、大きく『修正』と『追加』のどちらかに 分類される。それぞれについて見ていこう。

  1. 修正する場合の注意点

    致命的なバグが発見された場合、もちろんそれを修正する必要がある。 実は修正には「勘所」がある。しかし、世のスクリプタ・プログラマはそれを 知らなすぎ、安易に色々やりすぎだと思う。 ソフトウェア工学を一から全て学べとは言わないが、その基礎は知った上で スクリプタ・プログラマを名乗るようになって欲しいと切に願う。
    口うるさいことは十分に承知しながらも、以下にそれらを列挙する。

    我々はスクリプタ・プログラマだ。論理的思考を持って、論理的世界で、 論理的に間違いのないスクリプト・プログラムを作り上げる、それが仕事だ。 問題を目の前にして、自分が解決できないから「バグではない」などと言い放つのは 全く無知蒙昧であり、恥を知れ、と言いたい。ああ言いたいともさ! 現実世界で我輩を苦しめる舶来の「自称プログラマ」なヤツラにな!(涙)

  2. 追加する場合の注意点

    一刀両断的に正直かつ明確に言えば、「追加」が必要になる最大の理由は、 そのノベルゲームに対する最初の計画が不十分だったからだ。 作るための時間が足りなかった?後から追加したいことが増えた? 何故それを最初から予定に組み込めなかったのか。 次回作のために、再発防止策をひねり出しておくことを強くお勧めする。
    以上、お小言終り。

    『追加』が必要と考えるなら、後述するセーブデータの共通化のためにも、 一番最初、頒布開始の「前」から追加のための布石を打っておくことをお勧めする。 具体的には、「ここには追加シナリオを入れる」という場所に、最初から 追加シナリオを入れた場合のスクリプトをコメントで書いておく。

    (snip)
    [cm]
    
    ; Ver 1.10 からの追加シナリオ挿入予定位置
    ;[if exp="f.game_version >= 1.10"]
    ;	[call storage="追加シナリオ.ks" target="*追加シナリオ01"] 
    ;[endif]
    
    *scenario_no2|次の日
    (snip)
    

    シナリオを追加する際にはこのコメントを外し、"追加シナリオ.ks"を用意すればよい。 これにより、元スクリプトを殆ど修正することなく、 シナリオを追加することができる。 これは、 後述する「パッチ適用前後でのセーブデータの互換性」を保つためにも 必要なテクニックである。そうはいっても実際には最初からこういうコメントを 入れておくのは難しいので、 とりあえず「このあたりにシナリオ入るかもよ」という印と、 その規模に見合った空行を入れておくことをお勧めする。

最後に、「変更」という作業に対してもっと慎重になろう、ということも追記したい。 「変更する」ということは、「新たなミスが入りこむ可能性がある」ということだ。 安易に変更して、新しいバグを入れ込んじゃう人のなんと多いことか。 変更に対し臆病になってはいけないが、もっと慎重になることはいいことだ。 是非慎重になって欲しい。そして、ユーザの足を引っ張らないスクリプタ・ プログラマになって欲しい。

■アップデートパッチについて(互換性編)

ノベルゲームでは、パッチを適用することで、 セーブデータの互換性が無くなることがしばしばある。これは 吉里吉里/KAGだけでなく、他のAVG作成システムにも多かれ少なかれ存在する アキレス腱である。

より具体的には、吉里吉里/KAGでは、 パッチ適用後にパッチ適用前のセーブデータをロードして続きを遊ぶと、 以下のようなエラーメッセージが出ることがある。

エラーが発生しました
ファイル : first.ks   行 : ###
タグ : 不明 ( ← エラーの発生した前後のタグを示している場合もあります )
シナリオファイルに変更があったため return の戻り先位置を特定できません
スクリプトで例外が発生しました
シナリオファイル xxxxxxx.ks 内にラベル *xxxx が見つかりません

これが出ると、ユーザにはもう何もできない。 回避のためには、該当の(古い)セーブデータを消し、 新たに最初からゲームを始めなければならない。 何故このエラーが出るのかは製作者にしかわからないので、 ユーザは黙ってそれに従うしかない。 大作であるほど、また、ストーリーが佳境に入り、ラストに近づいていればいるほど、 セーブデータの互換性問題は、ユーザのモチベーションを著しく削ぐ重大な問題だ。 ユーザ志向を謳うなら、セーブデータの互換性は常に保つ努力をすべきだ。

セーブデータの互換性が保てない原因は二つある。

  1. 古いセーブデータでは、 パッチで追加変更した関数・変数・マクロ・ラベルなどが参照できないため
  2. 古いセーブデータが[call]タグの先でセーブされており、[call]の呼び出し元 位置付近に変更が入ったため

どのようにスクリプトを作成すればこれらを回避することができるか、 以下に注意点等を見ていこう。

●ラベルに関する注意点

データをセーブした際、吉里吉里/KAGは以下のような動作を行う。

一方、データをロードした場合は、吉里吉里/KAGは以下のような動作を行う。

実は結構簡単だ。 このとき注意すべきは、パッチ適用前後で、 既存の「スクリプトファイル名」及び「セーブラベル名」は 変更してはならないということだ。 例えば、パッチの前後で以下のような変更を実施してしまうと、 これは互換性の面から「アウト」となる。

パッチ前パッチ後
*start|はじまり
えへへへ
[p][cm]

*life|生きるって
生きてるって、いいね
[p][cm]

*death|死ぬって
死ぬって悲しいね
[p][cm]
*start|はじまり
えへへへ
[p][cm]

*new_life|生きるって    ← 過去のデータはここで再開できない
生きてるって、いいね
[p][cm]

*death|死ぬって
死ぬって悲しいね
[p][cm]

これは例えば、"*|"のようなラベル名が自動生成されるセーブポイントの場合でも 同じ、というかより制約が厳しい。"*|"が減ると、自動生成されていた ラベル(XXXXXX:2 や XXXXXX:3 など)が減るため、「最後のセーブポイント」で セーブされていたデータが再開できなくなる。 しかも、再開する時に、ラベル名の位置が変わってしまうため、 別の位置から再開される場合がある。

パッチ前パッチ後
*start|はじまり
えへへへ
[p][cm]

*|生きるって
生きてるって、いいね
[p][cm]

*|死ぬって
死ぬって悲しいね
[p][cm]

*next|次のシナリオ
*start|はじまり
えへへへ
[p][cm]

; *|生きるって   ← こうすると、ラベル *start:2 がなくなる
生きてるって、いいね
[p][cm]

*|死ぬって        ← ここが新しい *start:1 になる
死ぬって悲しいね
[p][cm]

*next|次のシナリオ

纏めると、『ラベルの名前や数は不用意に変更しないこと』ということに尽きる。 より具体的には、以下の条件を守ればよい。

実際にスクリプトを書いてみると、これらの条件はかなり厳しいことが分かる。 最初にスクリプトを書く段階から、後のパッチのことを考えておくことをお勧めする。

●新しいパッチで関数・変数・マクロ・ラベルなどを追加した場合

そもそも、パッチでは関数・変数・マクロ・ラベルなどを一切追加しなければよい。 これで確実にセーブデータの互換問題の半分はクリアできる。 このとき、セーブデータは、パッチ前後で互換性を保つことができる。
しかし、シナリオ追加などで、どうしても変数などを追加する必要がある場合、 一つの手段として、セーブデータにゲームのバージョン情報を含んでセーブしておき、 それを利用してバージョンごとにシナリオの動作を変える、という方法がある。 ゲーム先頭で

[eval exp="f.game_version = 1.00"]

と定義(sf.game_version ではダメな理由は分かる…よね?)しておき、 その後、パッチを適用するたびにバージョンを上げる。 そして、変数が追加になる場合は、シナリオ中で、この変数の値を見て、 バージョン毎に挙動を変更するようにすればよい。 例えば、「f.semute(効果音をミュートするフラグ)」が パッチバージョン1.10から 追加になった場合、効果音を演奏するマクロ[se_play]を、以下のように変更する。

Ver 1.09 までVer 1.10 から
[macro name="se_play"]
[se_opt *]
[playse storage=%storage buf=%buf loop=%loop|false]
[se_wait buf=%buf canskip=%canskip cond="!mp.loop"]
[endmacro]
[macro name="se_play"]
[se_opt *]
[if exp="f.game_version < 1.10 || !f.semute"]
[playse storage=%storage buf=%buf loop=%loop|false]
[se_wait buf=%buf canskip=%canskip cond="!mp.loop"]
[endif]
[endmacro]

こうすることで、新しいパッチ中では、 バージョン1.10未満のセーブデータでは単純に f.semute 変数は無視され、 古いセーブデータでも最後までプレイ可能になる。 即ち、パッチ適用前後でセーブデータの混在が可能となり、互換性が保たれる。

●古いセーブデータが[call]の先でセーブされているものの場合

最も考慮が難しいのがこれ。 吉里吉里では、[call]タグを使用すると、 [return]した時に戻る場所を覚えておくために、 セーブデータ中に[call]元の位置を記録する。 より具体的には、以下の三つを記録する。

  1. [call]が存在するシナリオファイル名
  2. [call]行の行番号
  3. [call]行全体

従って、パッチの前後で[call]が存在する行がスクリプト中で移動したり、 その行に追記したりした場合、セーブデータの互換性は失われ、 古いセーブデータをロードして実行しようとすると、前述のエラーが表示される。 これを防ぐためには、以下の条件を守る必要がある。

厄介なのは、広く使われている「どこでもセーブプラグイン」が、 [call]を利用して任意の場所でのセーブを実現していることだ。 従って、どこでもセーブプラグインを利用すると、 基本的に[label]を含む行 (これは最終的に[label]を呼ぶマクロを含む行も該当する) の位置を変更したり、削除したりすることができなくなる。 以下の条件を守ることで、なんとかセーブデータの互換を保つことはできる。

シナリオを追加する場合、パッチバージョンによってシナリオの矛盾が出ぬように 前述のf.game_versionを活用することもお忘れなく。

具体的には、以下のように変更する。バージョン1.1へのアップデート前と後で、 シナリオファイル「今日追加.ks」を追加実行することになった場合を例示している。

アップデート前アップデート後
; 改ページマクロ[plc]定義
[macro name="plc"]
[p][label][cm]
[endmacro]

(snip)

*start|はじまり
今日はいい天気です。
[plc]

でも昨日は雨でした。
[plc]

[call storage="一昨日.ks"]

おしまい。
[s]
; 改ページマクロ[plc]定義
[macro name="plc"]
[p][label][cm]
[endmacro]

(snip)

*start|はじまり
今日はいい天気です。
[plc]
[call storage="今日追加.ks" cond="f.game_version >= 1.1"]
でも昨日は雨でした。
[plc]

[call storage="一昨日.ks"]

おしまい。
[s]

変更箇所を一行に押し込め、前後で[label]を含む行の行番号に変更はない。 このようにすれば、セーブデータの互換性を保つことは可能である。 本当は上の例では 「[call]先でセーブ可能ラベルを新たに定義しないこと」という条件が付くが、 それはもっと前に述べているので、ここでは説明を割愛する。

■おまけ:スクリプト中に日本語を書くということ

本職プログラマには多いのだが、 「(実際に表示されるメッセージを除き、)ソースコード中に日本語を書くなんて カッコ悪い、英語書け英語」と思っている人がいる。 データファイルは、全て記号と番号で正規的に命名されていないと 気がすまない人もいる。 まぁ、仕事のプログラムを書く場合はそうなのかもしれない。

しかし、我輩はあえて、ノベルゲームのスクリプト中では、日本語を推奨したい。 理由は至極簡単、『そのほうが早く正確に読めるから』。 英語で書かれたコメントを、 その裏に潜む真の意味まで酌んで読むことができる人など何人居るだろう。 作者が非ネイティブであれば、元の文章が本当に正しい英語なのかどうかすら怪しい。 ファイル名についても、記号で書かれたファイル名を、 対応表を元にどんな内容であるか探し当てる作業が、 どれほど作業能率を下げるだろう。そういうことを考えると、 判りやすい言語を使って判りやすく記述されていた方が、管理は格段に楽になる。

それに、万一自分がそのゲームに関われない状態になった場合、 ゲーム製作は自分の手を離れ、 アナタが作ったスクリプトは別の人の管轄下に入るかもしれない。 その時、スクリプトが英語と記号まみれだったとしたら、 引き継いだ人はそのスクリプトを完全に理解できるだろうか。絶対ムリだ。 我輩も何度か引き継いだことがあるが、死ぬような思いだった。 もうあんな無駄な苦労はしたくない。

だから、日本語。日本語でコメントを書き、日本語ファイル名を取り扱い、 日本語で命名されたマクロを使う。後から読んで判りやすいじゃないか。

比較してみよう、以下のスクリプト、やりすぎてあざといのは承知の上で、 右と左はどちらがわかりやすい?

スクリプト in 英語スクリプト in 日本語
; Written in English
[erase_message_window]
; akane & minatsu must be displayed simultaneously.
[disp_char name="akane" graph="ak0002 ak0010 ak0200" position="right"]
[disp_char name="minatsu" graph="mn0001 mn0020" position="left"]
[disp_message_window]
[speech name="akane"]
英語で書いてあると何がなんだか判りません。
[np]
; erase message_window once here
[erase_message_window]
; display akihito slowly, in 2000ms
[disp_char name="akihito" graph="ah0001 ah0030" position="center" time=2000]
[disp_message_window]
; 日本語で書いた場合
[メッセージ枠消去]
; あかねと美夏は同時に表示すること
[キャラ表示 名前="あかね" 画像="ワンピ 笑顔 赤面" 位置="右"]
[キャラ表示 名前="美夏"   画像="和服 微笑" 位置="左"]
[メッセージ枠表示]
[セリフ 名前=あかね]
日本語の方が判りやすい…と思うんだけど。
[np]
; 一度メッセージ枠を消すこと
[メッセージ枠消去]
; アキヒトは2000msでゆっくり表示する
[キャラ表示 名前="アキヒト" 画像="着流し 爆笑" 位置="中央" time=2000]
[メッセージ枠表示]

幸い、吉里吉里/KAGでは、フツーに日本語が使える。 マクロとしても使えるし、サブルーチン名としても、ラベルとしても使える。 使えるなら、便利に使おう。もちろんあざとくならない程度に、ね。 もちろん、製作者の趣味趣向の一つだと言われればそれまでだが、 「わかりやすく書く」ことの一つの有効な手段だと思うのだ。

嘲笑を込めて「日本語ベーシックみたいだー」とかノタマウ人は、 既にそれが死語になっていることに気づくべし。

■おわりに

この文書では、主に以下の三点について述べた。

  1. 体系的なシステム設計の重要性
  2. バグ修正時の心得
  3. アップデートパッチ間でのセーブデータの互換性

かなり小うるさいことも書いてきたが、どうだろうか。 『そんな面倒なことやってらんねぇ』とか 『楽しく組めればそれでいいじゃん』とか思ったかもしれない。 それは半分正しく、半分間違っている。 同人であれ作品を世に出すということは、 即ち不特定多数のユーザにプレイしてもらうということだ。 このとき、ユーザに不快感を与えてはならない。 スクリプタにとって、『不快感を与えない』とは即ち、 バグの抑制が必要であることを意味する (でもなぜかレスポンス・パフォーマンスの向上が問われることはあまりない)。 早く、確実にゲームを作り上げること、 バグの発生を防止しつつ発生可能性を下げること、 万一発生した場合に再発を防止すること。 これらの指標として、上の三点を述べたと思いねぇ。

そして、スクリプタ・プログラマの究極の目的は、「ユーザにゲームを楽しんで もらうこと」であることを忘れないこと。スゴい技術は手段であって目的ではない。 それを忘れた時、アナタはまごうかたなく「ヘンなシステム」を 作ってしまうことだろう。そもそも我々は日陰者なのだ。 「あのゲームのシステムはスゴいよね」ではなく、 「あのゲーム面白かったよね」と言ってもらえることを目標にしよう。 ゲームの感想ばかりで、システムについての話題がこれっぽっちも出てこないこと、 それは実はスクリプタ・プログラマにとっての最高の褒め言葉だと思う。 それでいいじゃないか。

以上の偏向文書が、少しでもスクリプタ諸兄の役に立つことができれば幸いであるよ。