novelsphere.js Document

Chapter 7 進行を制御しよう

Section 1 選択肢を作る

選択肢を作るには?

選択肢は、多くのノベルゲームにとってなくてはならないものです。novelsphere.jsのノベルには選択肢を作ることができますので、さっそく実装してみましょう。

ラベルを立てる

選択肢を作るには、まずラベルを用意する必要があります。ラベルとは、シナリオ中の場所を指定する目印のようなもので、この目印をシナリオ中に書き込むことを「ラベルを立てる」といいます。

ここでは、ノベルの冒頭に「森の中で話をする」か「部屋の中で話をする」かを選ばせる選択肢を表示し、選ばれた結果によって表示される背景が変わるというシナリオを書いてみることにします。

「main.ks」を書き換えて以下の状態とした上で、このシナリオを動かすために必要な「bg2.jpg」を素材集からプロジェクトフォルダの中の「data」フォルダ内、「bgimage」フォルダにコピーしておきます。

準備が終わったら、ビルド・再生してみてください。

**main.ks

; ========================================
; 本編
; ========================================
@playbgm storage=madoromi
@current layer=message0 page=fore
@メッセージ枠表示

__[link target=*mori]森の中で話をする[endlink][r]
[link target=*heya]部屋の中で話をする[endlink]
@s

*mori
@cm
@image storage=bg1 layer=base page=fore
@jump target=*goryu

*heya
@cm
@image storage=bg2 layer=base page=fore
@jump target=*goryu

*goryu__
@ボワッと表示 file=migiri1

; ----- キャラがぴょこんと跳ねる -----
[move layer=0 page=fore o2_path=(400,90,255,100) time=250][wm]
[move layer=0 page=fore o2_path=(400,120,255,100) time=250][wm]

はじめまして![l][r]

; ----- キャラがおじぎする -----
[move layer=0 page=fore o2_path=(400,140,192,100,10) time=300
accel=-3][wm]
[move layer=0 page=fore o2_path=(400,120,255,100,0) time=300 accel=3][wm]

行末クリック待ちしました。[x]
改ページクリック待ちしました。
今度は字体を変えてみます。[r]
こんなふうに、[font color=0xff0000]文字を赤くしたり、[font face=serif]明朝体にしたり、[font size=24 color=0x0000ff]小さくしたりできます。[x]

@ボワッと表示 file=event imagelayer=3 transtime=500 x=0 y=0
イベント画です![x]

@ボワッと消去 imagelayer=3 transtime=500

それではまたお会いしましょう![x]
@ボワッと消去 imagelayer=3

半角アスタリスク(*)から始まる行はラベルと見なされます。この行は、タグでもテキストでもコメントでもない、特殊な行です。

ここでは、*mori、*heya、*goryuという3つのラベルを立てました。

ラベルは、ただ立てただけでは何も起こりません。選択肢を設置するには、次の[link]タグ、[endlink]タグを使う必要があります。

リンクを設置する ── [link][endlink]

[link]タグと[endlink]タグで囲われたテキストはリンクとなり、クリックすると特定の場所へジャンプさせることができます。

[link]タグにはtarget属性でジャンプ先のラベルを指定します。ちなみにstorage属性を指定すれば、クリックしたときにどこか別のラベルにジャンプするのではなく、別のシナリオファイルにジャンプさせることもできます。target属性とstorage属性のどちらかは必須です(両方指定することもできます)。

ここでは、メッセージ枠を表示した直後に、「森の中で話をする」というテキストと「部屋の中で話をする」というテキストを表示し、リンクとしています。これらのテキストをクリックすると、ノベルの進行はそれぞれ*mori、*heyaの箇所へジャンプします。

ノベルの進行を止める ── [s]

[link]タグで選択肢を設置した後は、[s]タグでノベルの進行を止める必要があります。そうしないと、せっかく選択肢を表示しても、プレイヤーにそれを選ばせる前にどんどんノベルが先に進んでいってしまいます。

シナリオの進行をジャンプさせる ── [jump]

ここでは、前章でも少しだけ登場した[jump]タグを使います。

[jump]タグには、target属性とstorage属性のどちらか、または両方を指定することができます。target属性には、ジャンプしたい先のラベル名を指定します。storage属性には、ジャンプしたい先のシナリオファイル名を指定します。target属性だけを指定すると、同じシナリオファイル中のそのラベル名の箇所へジャンプし、storage属性を指定すると、指定されたシナリオファイルの冒頭へとジャンプします。

「森の中で話をする」と「部屋の中で話をする」のどちらかをクリックすると、それぞれジャンプ先のラベルの位置にノベルの進行が分岐しますが、一度分岐したシナリオをまた1つの流れにまとめたいということもあると思います。

そのような時には、まとめたい箇所にラベルを立てて、分岐したシナリオの末尾でそのラベルにジャンプするような[jump]タグを書きます。

Section 2 変数を使う

変数とは?

変数とは「ある選択肢でどれを選択したか?」「1回以上このノベルをクリアしたことはあるか?」といった、そのノベルの状態を保存しておくメモのようなものです。

例えば前節のシナリオでは、ノベルの冒頭で「森の中と部屋の中、どちらで話をするか」という選択肢を表示しましたが、このときプレイヤーがどちらを選んだかによって結末が変わるようなシナリオを書きたいとしましょう。しかし今のままでは「プレイヤーがどちらを選んだか」という情報はどこにも控えられていないので、いざ最後のシーンに至ったとき、そのプレイヤーがどちらを選んだのかを判別することができません。このような情報を控えておく場所が変数なのです。

変数の宣言を行う ── [eval]

先ほどは、ラベルによって一旦シナリオを分岐させ、その後また合流させるシナリオを書きました。それぞれの分岐の箇所に1行ずつシナリオを書き加え、以下のようにしてみてください。

*mori
__@eval o2_exp="tf.basho='mori'"__
@cm
@image storage=bg1 layer=base page=fore
@jump target=*goryu

*heya
__@eval o2_exp="tf.basho='heya'"__
@cm
@image storage=bg2 layer=base page=fore
@jump target=*goryu

[eval]という新しいタグが使われていますが、このタグによって、変数にノベルの状態をメモすることを実現しています。順を追って理解していきましょう。

変数には名前がある

先ほど変数はメモのようなものだと書きましたが、そのメモには1つ1つ名前がつけられています。このシナリオでは、tf.bashoという名前の変数を使っています。tf.bashoという名前の変数に「プレイヤーがどちらを選んだか」という情報を書き込んでいるのです。

変数には自由に名前を付けることができますが、2つだけルールがあります。1つ目は「名前に記号を使ってはいけないこと」、2つ目は「名前の最初にはtf.をつけること」です(実はtf.以外をつけるべき局面もあるのですが、これについては後述します)。

変数には数字や文字列を書き込んでおくことができる

変数に書き込んでおく「もの」のことを値といいます。値にはいろいろな種類がありますが、ひとまず数字と文字列を覚えておけばよいでしょう。

ちなみにこのシナリオでは、tf.bashoという名前の変数に対し、1つ目の[eval]タグではmoriという値を、2つ目の[eval]タグではheyaという文字列を書き込んでいます。

文字列を書き込むときに忘れてはいけないのが、その文字列をシングルクォーテーション(')で括ることです。逆に、数字を書き込むときにはシングルクォーテーションで括ってはいけません。

ちなみに今回のシナリオでは行いませんでしたが、一度値を書き込んだ変数の中身は、何度でも書き換えることができます。

変数を扱うときには[eval]タグを使う

これまで私たちはタグを使った言語を学んできましたが、novelsphere.js自体はJavaScriptというプログラミング言語で作られています。[eval]タグはそのJavaScriptを直接操作する命令といえます。

[eval]タグのo2_exp属性に値を指定すると、その値がそのままJavaScriptとして実行されます。

JavaScriptと聞くと何やら難しそうな感じがしますが、今は変数を取り扱うための機構とだけ考えても差し支えありません。

変数に値を書き込むときには半角イコール(=)を使う

今回のシナリオのうち1つ目の[eval]タグのo2_exp属性の値は以下のとおりでした。

tf.basho='mori'

tf.bashoという「変数の名前」が左、moriという「変数に書き込みたい値」が右に書かれ、間が半角イコール(=)で結ばれています。今回、書き込みたい値は数字ではなく文字列なので、'mori'というふうにシングルクォーテーションで括られています。

これが変数に値を書き込むときの書き方です。

変数の中身に応じて展開を変える ── [if][endif]

せっかく変数に値を保存しても、それを使わなければ意味がありません。

「main.ks」の最後のほうを書き換え、以下の状態にしてみてください。

**main.ks

; ========================================
; 本編
; ========================================
@playbgm storage=madoromi
@current layer=message0 page=fore
@メッセージ枠表示

[link target=*mori]森の中で話をする[endlink][r]
[link target=*heya]部屋の中で話をする[endlink]
@s

*mori
@eval o2_exp="tf.basho='mori'"
@cm
@image storage=bg1 layer=base page=fore
@jump target=*goryu

*heya
@eval o2_exp="tf.basho='heya'"
@cm
@image storage=bg2 layer=base page=fore
@jump target=*goryu

*goryu
@ボワッと表示 file=migiri1

; ----- キャラがぴょこんと跳ねる -----
[move layer=0 page=fore o2_path=(400,90,255,100) time=250][wm]
[move layer=0 page=fore o2_path=(400,120,255,100) time=250][wm]

はじめまして![l][r]

; ----- キャラがおじぎする -----
[move layer=0 page=fore o2_path=(400,140,192,100,10) time=300
accel=-3][wm]
[move layer=0 page=fore o2_path=(400,120,255,100,0) time=300 accel=3][wm]

行末クリック待ちしました。[x]
改ページクリック待ちしました。
今度は字体を変えてみます。[r]
こんなふうに、[font color=0xff0000]文字を赤くしたり、[font face=serif]明朝体にしたり、[font size=24 color=0x0000ff]小さくしたりできます。[x]

@ボワッと表示 file=event imagelayer=3 transtime=500 x=0 y=0
イベント画です![x]

@ボワッと消去 imagelayer=3 transtime=500

それではまたお会いしましょう![x]

__[if o2_exp="tf.basho=='mori'"]
	次は部屋の中でお会いしましょう![x]
[endif]__

@ボワッと消去 imagelayer=3

先に答えからお話ししてしまうと、この箇所は、もしtf.bashoという変数の中身がmoriだった場合は「次は部屋の中でお会いしましょう!」と表示しろ、という意味になります。それでは、各タグの書き方を覚えていきましょう。

[if]タグと[endif]タグでシナリオを囲う

変数の中身に応じてノベルの展開を変える(条件分岐する)ときの基本は、展開を変えたい箇所を[if]〜[endif]タグで囲うことです。この2つのタグは必ずセットで使われます。[if]タグのo2_exp属性には、JavaScriptの文を指定することができます。ちなみに日本語では、ifは「もし○○なら」という意味です。

変数の値を調べるときは半角イコール2つ(==)を使う

tf.basho=='mori'

今回書いた[if]タグのo2_exp属性の値は上の通りですが、これはtf.moriという変数の中身がmoriという文字列であるか否かを調べるときに使うJavaScriptの文で、「tf.bashoの中身はmoriという文字列か?」という疑問文のようなものだと考えてください。

[if]タグは、o2_exp属性に書かれた疑問文が正解であるときにのみ、[endif]タグまでのシナリオを実行しますので、この場合はtf.bashoという変数の中身がmoriである場合に限って、「次は部屋の中でお会いしましょう!」というテキストが表示されることになります。

複雑な条件判定を行う ── [elsif][else]

[if]〜[endif]の間では[elsif]タグを使うことができる

先ほど書き足したシナリオを、さらに以下のように書き換えてみましょう。

[if o2_exp="tf.basho=='mori'"]
	次は部屋の中でお会いしましょう![x]
[elsif o2_exp="tf.basho=='heya'"]
	次は森の中でお会いしましょう![x]
[endif]

もし必要があれば、[if]〜[endif]の間のシナリオには、[elsif]タグを使うことができます。elsifは「さもなければもし○○なら」という意味で、このタグにもo2_exp属性を指定することができます。

上のシナリオでは、tf.basho変数の中身がmoriだった場合には「次は部屋の中でお会いしましょう!」というテキストが表示されますが、もしmoriではなくheyaだった場合には「次は森の中でお会いしましょう!」というテキストが表示されます。

[if]〜[endif]の間では[else]タグを使うことができる

先ほどの箇所は、さらに以下のように書くこともできます。

[if o2_exp="tf.basho=='mori'"]
	次は部屋の中でお会いしましょう![x]
[else]
	次は森の中でお会いしましょう![x]
[endif]

先ほど[elsif]タグだった部分が、[else]タグに置き換えられています。elseは日本語では「さもなければ」という意味です。このタグにはo2_exp属性を指定することはできません。

実は上のシナリオは、1つ前の[elsif]タグを使ったシナリオとほぼ同じ意味を持ちます。上のシナリオでは、tf.basho変数の中身がmoriだった場合には「次は部屋の中でお会いしましょう!」というテキストが表示されますが、もしmoriでなかった場合には、無条件で「次は森の中でお会いしましょう!」が表示されます。

もし変数の中身がmoriではなかったとき、「moriでないならheyaか?」というふうに改めてその中身を確かめることをしていないのが1つ前のシナリオとの違いですが、今回のノベルではtf.mori変数の中身がmoriかheya以外であることはありえないので、2つのシナリオは実質的に同じ意味になるというわけです。

条件分岐のまとめ

まとめると、条件分岐に使うタグの使い方は以下の通りとなります。

あくまでも基本は[if]タグと[endif]タグのセットで、[elsif]タグや[else]タグは必要に応じて使う、と覚えておきましょう。

条件分岐に関わる4つのタグの使い方

Section 3 セーブ機能を実装する

セーブの考え方

novelsphere.jsのノベルにはセーブ機能を実装することができます。

ここでは、右クリックまたは二本指タップでその時点の状態を記録できる、簡単なセーブ機能を実装してみましょう。なお、ロード機能の実装は次章で行います。

novelsphere.jsのセーブ機能には若干癖がありますが、やるべきことは次の2つです。

セーブしたい情報を準備する ── [o2_savestat]

セーブ機能を使ってノベルの状態を保存するには、まずはその「保存したい情報」をあらかじめ準備しておかねばなりません。

保存したい情報を準備しておくには[o2_savestat]タグを使用します。

「first.ks」を開いて、[x]マクロの定義を以下のように書き換えてください。

@macro name=x
	[p][cm]
	[o2_savestat]
@endmacro

この状態でノベルをビルド・再生しても何も起こっていないように見えますが、実際には[x]マクロが実行されるたびに、セーブのための情報が準備されています。

右クリック時の動作を定義する ── [rclick][return]

今回は、右クリック(スマートフォンなどからプレイしている場合には二本指タップ)だけでセーブが行われる簡単なセーブ機能を実装しますが、そのためには実際に右クリックが行われた時の動作を決めておかねばなりません。

何も設定を変えていない状態では、右クリックには「メッセージ枠が消える」という動作が割り当てられていますが、これを好みのものに変更することができます。この変更は[rclick]タグで行います。

「first.ks」の末尾に、以下のシナリオを追加してください。

@rclick call=true storage=rclick.ks

そして新しく「rclick.ks」というシナリオファイルを作ります。中身は以下のようにして、他のシナリオファイルと同じく、プロジェクトフォルダ内「data」フォルダの中の「scenario」フォルダに保存します。

**rclick.js

; ========================================
; 右クリックサブルーチン
; ========================================
@eval o2_exp="alert('右クリックされました')"
@return

保存し終わったら、ビルド・再生してみましょう。右クリックすると、「右クリックされました」というダイアログが表示されるようになりましたか?

今回は本編が始まる前にあたる「first.ks」に[rclick]タグを書き込みましたので、それ以降に右クリックが行われた時の動作は、全てこの[rclick]タグで指定したとおりのものとなります。

call・jump・storage・targetの各属性の使い方

[rclick]タグのcall属性にtrueを指定すると、右クリックされたときに、storage属性とtarget属性で指定した位置にノベルの進行が一時的に飛ぶようにすることができます。元の場所に戻ってくる前提で、ノベルの進行を一時的に別の場所へ飛ばすことをサブルーチンジャンプといいます。

storage属性とtarget属性のどちらかは省略可能です(これは84ページで学んだ[jump]タグの使いかたと一緒です)。今回はstorage属性にrclick.ksを指定しましたので、これ以降に右クリックされた時には、ノベルの進行がrclick.ksの先頭にサブルーチンジャンプします。

サブルーチンジャンプ先のシナリオには、必ず[return]タグがなければなりません。[return]タグに行き当たると、サブルーチンジャンプは終了し、ノベルの進行は元の場所(今回の場合は「main.ks」)に戻ります。

なお、今回は出番がありませんでしたが、[rclick]タグにはjump属性を指定することもできます。jump属性にtrueを指定すると、storage属性とtarget属性で指定した位置にノベルの進行が飛びます。しかしjump属性を使った場合、サブルーチンジャンプではなく本当にノベルの進行が飛んでしまうので、「最終的には本編など、元の場所に戻ってくること」を前提とすることが多い右クリックの設定には、あまり使う機会はないでしょう。

普通のジャンプとサブルーチンジャンプの違い


[eval]タグを使ってダイアログを出す

今回、動作確認のために[eval]タグを少し特殊な方法で使いました。

[eval]タグのo2_exp属性に、alert('○○○')のような値を指定すると、好きなメッセージが書かれたダイアログを出すことができます。

前節では[eval]タグを変数に値を書き込むためのタグとして扱いましたが、実はこのタグはo2_exp属性で指定した文をJavaScriptとして実行するためのタグであり、変数に値を書き込む以外にも多彩な使い道があります。JavaScriptの使い道はあまりにも幅広く、複雑な反面、使いこなせば恐ろしく便利なものです。しかしそれを説明するには、この本とは別にもう1冊分の説明が必要でしょう。

この本ではJavaScriptの詳しい説明は省きますが、動作確認のためにダイアログを出すのは案外便利ですので、豆知識として覚えておくといいでしょう。


準備した情報を実際にセーブする ── [save]

これで下準備が整いました。それでは実際に、「右クリックされたらセーブする」部分を実装しましょう。

先ほど作った「rclick.ks」の中身を、以下のように書き換えてください。

**rclick.js

; ========================================
; 右クリックサブルーチン
; ========================================
@save place=0
@eval o2_exp="alert('セーブしました!')"
@return

これだけで、右クリックしたときにセーブが行われるようになりました。

place属性にはセーブを保存する場所を指定します。これはセーブスロットのようなもので、ここでは0番のセーブスロットにノベルの状態をセーブしています。

しかし、ただセーブするだけでは意味がありません。セーブしたデータを読み込んでノベルを途中から再開するためのロード機能を実装する必要があります。ロード機能は、この次の章で実装していきます。