[Labyrinthe Noir]>[Top]>[こども工作教室]>

「ゲームブック」のソースと解説

ゲームブックとは、元々は書籍でファンタジー系の物語を読み進めると選択肢が書かれており、その選択によって次に読むべきページが変わるというものです。
そのため、物語の主人公となって行動を選択することで進むページが決められ、物語に変化が現れます。
そこにキャラクターのステータスを設定し、サイコロでランダム性を入れてゲーム性をより今のビデオゲームに近づけることができます。

実際に作るための物語は自分で用意してください。

基本構成

準備するファイルについて

ゲームの物語はHTMLファイル1つ1つに分割して書きます。1つのファイルから次のファイルへとリンクやボタンで飛びます。これらをゲームファイルと呼びましょう。
ゲームファイルの中にもJavaScriptを組みます。これは選択肢を選んだとき、個別のイベントを処理します。

次に、各ゲームファイルで共有するJavaScriptを書いたファイルがあります。これをスクリプトファイルと呼びます。プレイヤーのステータス表示やステータスの計算、ボタンを押したときの基本動作を処理しています。

あとは、CSSファイルがあります。これもゲームファイルで共有してデザインを統一します。タグやクラスを割り当てたタグについて、同じデザインを適用します。

種類(用途) ファイル名 内容
ゲームファイル gb00.html 導入ページ
gb01.html 第1話(基本3択)
gb02.html 第2話(基本4択)
gb03.html 第3話(ダメージ計算)
gb04.html 第4話(サイコロ処理)
gb05.html 基本パターン
gb99.html エンディング
gb_dead.html ゲームオーバー
スクリプトファイル gb_script.js JavaScriptのファイル
CSSファイル gb_base.css 文字の大きさなどを決める

上記の全てのファイルを保存してください。

Windowsならリンクの上で右クリック(Macの場合、Ctrlキーを押しながら、リンクをクリック)をするとショートカットメニューが表示されますので、「名前を付けてリンク先を保存」を選びます。
保存先には、ドキュメントフォルダ内に作業用のフォルダを作っておくと良いでしょう。
全てのファイルはテキストのUTF-8で保存されています。

ゲームファイルについて

ゲームファイルは、単純な番号を付けています。
もちろん、順番に飛ぶ必要はありませんが、URLやソースを見ることができるため隠す必要もないでしょう。

ファイルの頭に「gb」を共通で付けて、2桁の番号を連続で付けています。
最初のページが「gb00.html」、その次のページが「gb01.html」、ゴールのページは「gb99.html」としています。
また、それらとは別にゲームオーバーになったときのために「gb_dead.html」もあります。

スクリプトファイルについて

スクリプトファイルは「gb_script.js」を使っています。

ページ間で、ユーザーのデータを共有するためにcookieを利用しています。ページを読み込むとcookieも読み込まれ、ページを移動する直前にcookieを保存するようになっています。

スクリプトの一部は変更しない方が良い部分もありますので、変更が可能なところを中心に解説して行きます。
ゲームファイルの解説に合わせて必要な部分を解説します。

CSSファイルについて

CSSファイルは「gb_base.css」です。

<body>タグには幅を400pxで固定しています。背景や文字色も設定できます。

<h1><h2>タグには文字の色を指定しています。

準備しておくこと

舞台とストーリー

ストーリーを作る際に、一番重要なのはその世界感です。
簡単に言うと、どこを舞台にして物語が進行するのかが重要です。

例えば、巨大な森の中であったり、お城であったり、街の中だったり、迷路の中だったり、世界中であったり、宇宙であっても構いません。
移動する空間がゲームブックの舞台となり、ページを移動することで景色が変わり、物語が進みます。

ストーリーの分岐方法

ストーリーの分岐をどうするか、最初に基本的なパターンを決めましょう。途中で変更はいくらでもできます。

大きく分けると3つのパターンが考えられます。1つは、分岐がほとんどない一本道タイプ。次に、分岐をしてもすぐに戻る寄り道タイプ。最後は、分岐がどんどん増える枝分かれタイプです。

導入部からエンディングまで3話構成と短くして、それぞれのパターンを比べてみましょう。

  ストーリー構成 特徴
(A)一本道タイプ

導入部 → 第1話 → 第2話 → 第3話 → エンディング

一つのストーリーを追いかけることで、重厚なストーリーを作りやすい。
選択肢のイベントだけでストーリーに変化を与えるだけで、大きな変化がないため流れは単調になる。
(B)寄り道タイプ
導入部 → 第1話A → 第2話 → 第3話A → エンディング
→ 第1話B → 第3話B
基本のストーリーを1つにして、選択肢によってストーリーに変化を与える。
選択肢だけでなく、分岐でもストーリーを作れるので、柔軟性が生まれる。
(C)枝分かれタイプ
導入部 → 第1話A → 第2話A → 第3話A → エンディング
→ 第3話B
→ 第2話B → 第3話C
→ 第3話C
→ 第1話B → 第2話C → 第3話E
→ 第3話F
→ 第2話D → 第3話G
→ 第3話H
ストーリーがどんどん分岐するため、進み方によって大きくストーリーが変わる。
分岐が増えることで、楽しみ方も増える反面、ページ数が増えて収拾が付かなくなる。
ストーリーに変化を付けるためにより多くの文章を作る必要があるが、実際に使うページは偏りが起きる。
検証作業も複雑になる。

適度に分岐してストーリーに変化を持たせることができる(B)パターンはとても理想的です。
しかし、最初から物語を二重に作るのは大変です。まずは、(A)パターンで流れを作ってから、慣れてきたところで(B)パターンにしても良いでしょう。

(C)パターンでは、途中の物語も多くなります。枝分かれが多くなると話を1つにまとめるのも難しくなります。エンディングも1つにならないかもしれません。

また、ストーリーの流れを一方向でなく、戻ったり、飛んだりすることも可能です。自由な発想を活かすことができます。

ステータスと戦闘システム

キャラクター作りに重要なのがステータスです。そして、それはゲームの戦闘システムとも関連しています。

例えば、敵が登場して闘う必要があるとき、勝ち負けの判定やゲームオーバーの判定が必要になります。また、経験値やレベルアップもあります。
もちろん、それらの要素がなく物語だけを進めることもできます。偶然性やゲーム性をどう織り込んでいくか、これが基本システムを作ることになります。そのシステムに必要なのがステータスなのです。

標準ではステータスは次のように設定しています。

ステータス名 名前 体力 攻撃力 防御力 経験値 レベル
初期値 あなた 10 1 1 0 1

これらは配列関数Playerで管理し、名前のステータスならば、「Player["名前"]」で呼び出すことができます。

ステータスの増減に関しては、直接配列変数Playerに数値を増減することができます。
経験値に関しては10以上になる毎にレベルアップ処理を行いますので、専用のユーザー関数getExp()で処理します。

また、戦闘ダメージ計算も専用のユーザー関数getDamege()を使って処理します。
この戦闘のシステムでは、敵の攻撃力と体力を使い、先にプレイヤーから攻撃を行います。プレイヤーの攻撃力から相手の体力(防御力)を引いた残りがダメージとなり、敵の体力を下げます。ここで敵の体力が0以下になると勝利となります。次に、敵の攻撃となり、敵の攻撃力からプレイヤーの防御力を引いてダメージとして体力から引きます。ここでプレイヤーの体力が0以下になると敗北です。そして、まだ体力が残っていたら、またプレイヤーの攻撃となります。
ユーザー関数getDamage()では、敵の防御力が0になるまでにプレイヤーが受ける総ダメージを返します。
あとは、それらをそのまま体力(Player["体力"])から引くのか、ランダムで軽減措置をするなど増減を行ってから処理することもできます。

ステータスは必ず使わなければならない訳ではありません。不要な場合は、Player["名前"]か、配列変数stDefのステータス初期値を空欄「""」にすることで表示しなくなります。

初期値の変更

ステータスの初期値はスクリプトファイルで設定されていますので、変更したい場合はスクリプトファイルの該当箇所を変更します。

参照:gb_script.js

変更可能な基本設定部分を見てみましょう。

//基本設定
//ステータス名
stName = new Array("名前","体力","攻撃力","防御力","経験値","レベル");
//ステータス初期値
stDef = new Array("あなた",10,1,1,0,1);
//外部へ戻るページ
pageBack = "http://www.shurey.com/";
//体力がなくなったときのページ
pageDead = "gb_dead.html";
//cookie名
cname = "gamebook";

配列変数stNameを使って、ステータス名を決めています。これはステータス値を呼び出す配列変数Playerの要素名と連動しています。

配列変数stDefには、ステータスの初期値が入っています。ステータス名の順序と対応させる必要があります。文字列は""で囲い、数値は囲いません。

変数pageBackにゲームを止めて戻る場合のページをしています。URLをそのまま記述するか、相対的に見たパス名やファイル名を指定します。

変数pageDeadには、ゲームオーバーになった場合に表示するファイル名を指定します。

変数cnameはステータスを保存するcookie名となり、通常は変更しません。ただし、別のゲームブックを作る場合はステータスが混同してしまうのでcookie名の変更が必要です。

ステータス名を変更する場合、それによりステータスの役割が変わりますので、ステータスのリセットやレベルアップの手順も変更が必要になります。
ユーザー関数restartStatus()やgetExp()などステータス名を使っている部分を参照してください。

ゲームファイル(1) 導入部

参照:gb00.html

まず最初にプレイヤーに見てもらうのが導入ページです。参照ページのソースを見ながら、説明を読みましょう。
参照ページのソースは、ブラウザで開いてからソースを表示するか、メモ帳などのテキストエディタで保存したゲームファイルを開いてください。編集作業を行う場合は必ず、テキストエディタが必要です。

ここでは、導入となるストーリーと、次へ進むボタン、ステータスのリセットボタン、元のサイト(ページ)に戻るためのボタンを用意します。

ゲームファイルの基本構成は、ページのタイトル、ステータス表示、ストーリー、イベントボタン、スクリプトとなっています。導入ページではページタイトルがゲームのタイトルになります。

<body>
<h1>GameBook</h1>
<p><div id="field_status"></div></p> <p>ストーリー</p> <p class="selbtn"><input type="button" onClick="goN()" value="精霊を振り払い森に入る"></p>
<p class="selbtn"><input type="button" onClick="goR()" value="ステータスをリセットする"></p>
<p class="selbtn"><input type="button" onClick="gotoBack()" value="森に入らず帰る"></p>
<script type="text/javascript" src="gb_script.js"></script>
<script type="text/javascript">
スクリプト </script> </body>

<h1>タグがゲームタイトルです。次のゲームファイルからは<h2>タグを使ってページタイトルを表示します。違いは文字の大きさです。(CSSファイルで変更できます。)

<div id="field_status"></div>が、ステータスの表示部分です。数値のステータスのみ出力されます。

<p>タグは、段落を作るタグです。ストーリーを書く場合はこのタグだけでできます。段落を分ける場合は、別の<p>タグを用意します。

<input type="button">タグはボタンを表します。ここでイベント処理を行います。
この部分の<p>タグにはclassが割り当てられていますが、これはボタンを左右の中央に配置するためです。ボタンには、表示の文章(value)と押した時(onClick)に処理するユーザー関数が指定されています。
ボタンには3つのユーザー関数が指定されています。それに対応したものがスクリプトに書かれています。

<script>タグは2つあります。
1つ目のタグでは、gb_script.jsを読み込んでステータスの保存や読込などの基本的な処理を行っています。また、用意されたスクリプトをユーザー関数として利用できるようにもなります。
2つ目のタグには、イベントボタンに対応したユーザー関数を独自に記述します。

スクリプト

書式:gotoPage(ページ名) //ページ名 = ページのファイル名(拡張子省略)

それでは3つのユーザー関数を順番に見てみましょう。

1つ目のイベントボタンを押すとユーザー関数goN()が実行されます。

function goN() {
	//次のページへ進む
	gotoPage("gb01");
}

ここでは単に次のページへ移動するという処理が行われます。
ユーザー関数gotoPage()が移動する処理です。そこに引数としてファイル名を与えています。このときファイルの拡張子(.html)は省略しています。

次は2つ目のユーザー関数goR()です。

function goR() {
	//ステータスのリセット
	resetStatus();
	//再読込
	gotoPage("gb00");
}

ステータスをリセットするためのユーザー関数resetStatus()を呼びだし、その後でこの同じページを読み直しています。
このリセットはゲームをクリアした後やゲームオーバーの後に、このページに戻ってきたときにステータスを継続するかリセットするか選ぶためのものです。
保存されたステータスの値も初期値に戻ります。

3つ目のユーザー関数gotoBack()はスクリプト内にはありません。これはgb_script.jsの中にあるためです。初期設定で指定したページに戻ります。

ゲームファイル(2) 基本的な分岐処理

参照:gb01.html / gb02.html

ここからがゲームブックの内容になってきます。まずはストーリーの分岐をせずに第1話に飛んできます。

まずはgb01.htmlのソースを確認してください。ページ構成は導入部とほぼ同じです。ページタイトルやストーリー、イベントボタンを作り替えます。
gb02.htmlについては、この章を読み終えた後でソースを確認してください。選択肢が4つに増えているだけで同じような内容です。

では、gb01.htmlのイベントボタンを見てみましょう。
ここではストーリーに合わせて3つの選択肢が用意されています。

<p class="selbtn"><input type="button" value="北の草むら" onClick="goN()"></p>
<p class="selbtn"><input type="button" value="西の獣道" onClick="goW()"> <input type="button" value="東の獣道" onClick="goE()"></p>

ボタンを押すとそれぞれにイベントが発生し、次のページへ移動します。

スクリプト

3つのユーザー関数の内容を見てみましょう。
どれも、基本的な構造になっています。メッセージを表示し、ステータスを増減し、ページを移動します。

まずはユーザー関数goN()です。

function goN() {
	alert("しばらくすると視界が開けた。と、思ったら窪地に足を滑らせて後ろに倒れてしまった。\n\n体力 -1");
	
	Player["体力"]--;
 	
	gotoPage("gb02");
}

関数alert()を使ってメッセージを表示しています。このとき、ステータスの変動を予告しています。「\n」は改行を表します。

配列変数Playerはステータスの値を表しています。ここでは体力を1つ下げています。

最後にページを移動しています。

次はユーザー関数goW()を見てみます。

function goW() {
alert("獣道を辿って行くと、道はなだらかに続いていた。\n\n経験値 +1");
Player["経験値"]++;
gotoPage("gb02");
}

メッセージの表示とページの移動はユーザー関数goN()と同じです。

ここでは経験値を1つ増やしています。

次はユーザー関数goE()を見てみましょう。

function goE() {
	alert("獣道を辿っていたが、いつの間にか道がなくなっていた。戻る道も見えずしばらくさまよった。\n\n体力 -2 / 経験値 +1");

	Player["体力"] -= 2;
	Player["経験値"]++;

	gotoPage("gb02");
}

今度は、体力を2つ減らして、経験値を1つ増やしています。

ステータスの変更

ここでステータスの変更方法をまとめておきます。

ステータスの値は配列変数Playerに入っていますので、要素名にステータス名を入れて値を取り出したり書き換えたりします。

名前のように文字列の場合の書き換え方は次のようにします。

名前を書き換える Player["名前"] = "新しい名前"
ステータスを表示させない Player["名前"] = ""

メッセージに名前を表示する場合は次のようにします。

alert(Player["名前"] + "が獣道を辿って行くと、道はなだらかに続いていた。\n\n経験値 +1");

文字列は「+」で繋がれますので計算式のようになります。

数値のステータスを増減する場合は次のように式を書きます。省略式は代入式を省略したもので、結果は同じです。

  代入式 省略式(1) 省略式(2)
体力を1つ増やす Player["体力"] = Player["体力"] + 1 Player["体力"]++ Player["体力"] += 1
体力を2つ増やす Player["体力"] = Player["体力"] + 2   Player["体力"] += 2
体力を1つ減らす Player["体力"] = Player["体力"] - 1 Player["体力"]-- Player["体力"] -= 1
体力を2つ減らす Player["体力"] = Player["体力"] - 2   Player["体力"] -= 2

数値を1つ増減する場合だけ特別な省略式(1)があります。同じ変数の値を増減するのが省略式(2)です。
代入式では、右辺を自由に作ることができます。

レベルについては経験値が10以上になった場合に増えますので、直接増やしても意味がありません。
ページの移動時に経験値をチェックして、レベルアップしていたらメッセージを表示します。

名前をストーリー内に表示する

名前をストーリーに表示したい時は、変数を使えません。
HTML上では<span class="PL"><span>を使って表示させることができます。

<p><span class="PL">あなた<span>は森の中に入っていった。</p>

タグの間に文字を入れて置くとタグの位置が分かりやすくなります。

この処理はgb_script.js内のユーザー関数setPLName()で実行しています。

ゲームファイル(3) 戦闘のダメージ処理

書式:getDamage(pw,th) // pw = 敵の攻撃力 / th = 敵の体力(防御力)

参照:gb03.html

イベントで敵と遭遇し、戦闘を行う場合の処理を解説します。gb03.htmlのソースを見てください。

ストーリーとイベントボタンの間に敵の明示を入れています。

<p class="enemy">灰色の狼(3/3)</p>

<p>タグにenemyクラスを割り当てています。
敵の表示は、名前と攻撃力、体力(防御力)としています。

選択肢は4択ですが、戦闘に関する部分だけ確認してみましょう。
選択肢のボタンは次のようになっています。

<p class="selbtn"><input type="button" value="木の枝を取って狼を追い払う" onClick="goN()"></p>

ユーザー関数はgoN()です。

function goN() {
getDamage(3,3);
if (damage > 0) {
alert ("狼も" + Player["名前"] + "を敵とみなしたようだ。格闘の末、なんとか追い払ったが、傷を負ってしまった。\n\n体力 -" + damage + " / 経験値 +" + damage);
Player["体力"] -= damage;
Player["経験値"] += damage;
} else {
alert ("狼も" + Player["名前"] + "を敵とみなしたようだ。格闘の末、なんとか追い払った。\n\n経験値 +3");
Player["経験値"] += 3;
}
gotoPage("gb04");
}

戦闘のダメージを決めるためユーザー関数getDamage()を実行しています。ここで、表示した敵の攻撃力と体力(防御力)の値を送っています。
ユーザー関数getDamage()は、プレイヤーがその敵を倒すまでに受けるダメージを算出して、グローバル変数damageに代入します。

if文では変数damageを確認し、0ならばダメージなし、それを超えるならダメージありとして処理を分岐しています。

ダメージ計算では、プレイヤーと敵が交互に攻撃する様子をシミュレーションし、敵を倒すまでに受けたダメージを計算しています。

ゲームファイル(4) ダイスの処理

書式:getDice(num) // num = 出目の上限

参照:gb04.html

ダイスの処理では、最大値を指定したサイコロでランダムに数字を決めます。これは乱数処理とも言います。
乱数は、その時々に偶然性を作りたい場合に利用します。

ダイスの処理は4番目の選択肢からユーザー関数goS()の中で使っています。

function goS() {
//運試し
getDice(3);
if (dice == 1) {
alert ("2つのバッグを持ち上げようとしたら、地面に大きな穴が現れた。バッグはその中へ落ちたが、なんとか落ちずに済んだ。\n\n経験値 +3");
Player["経験値"] += 3;
} else {
alert ("2つのバッグを持ち上げようとしたら、足下がぬかるんだ泥になっていた。腰まで落ちてしまい、ずぶ濡れになった。\n\n体力 -3 / 経験値 +3");
Player["体力"] -= 3;
Player["経験値"] += 3;
}
gotoPage("gb05");
}

ダイスの処理としてユーザー関数getDice()を実行しています。引数は出目の最大値で、1~最大値までの乱数をグローバル変数diceに代入します。

上記の場合、1~3までの数字のうちどれか1つが変数diceに入っています。

if文では、変数diceが1の場合とそれ以外の場合で処理を分岐しています。
これにより、真の場合の確率は1/3、偽の場合は2/3となります。

このように確率の異なるイベントを発生させる場合にダイスを使用します。

例えば、おみくじのように、大吉は10%、中吉は30%、吉は40%、凶は20%というように確率を分けます。
このような3つ以上の分岐は次のようにします。

getDice(10);

if (dice == 1) {
	alert("大吉");
} else if (dice < 5) {
	alert("中吉");
} else if (dice < 9) {
	alert("吉");
} else {
	alert("凶");
}

4つの分岐だからということで、getDice(4)としてはいけません。
サイコロの目は確率の配分に応じて必要になります。それぞれの確率を分数に直すと1/10、3/10、4/10、2/10です。分母は10なので、getDice(10)とします。

if文は、最初に大吉の分だけ真にします。確率が1/10なので、変数diceが1のときだけ大吉にします。
次はelse文とif文が一緒になっています。今度は中吉の場合です。変数diceが2~4のとき、確率が3/10となります。正確には「if (dice => 2 && dice =< 4)」と書くことで2以上4以下と表現できます。上記ではこれを簡略化しています。

if (dice < 5) はどのように簡略化されているのでしょうか。
まず、5未満ということは整数では4以下と同じです。
次に2以上という表現は完全に省略されています。これは、最初のif文で1の場合を除外(大吉になる)しているため、次に来るのは2以上の場合しかないからです。

吉の場合は、変数diceが5~8のとき、凶の場合は、9~10のときです。

ゲームファイル(5) ゲームオーバー

体力が0以下になると、ゲームオーバーになる処理が必要です。

体力の値をページの移動時、ユーザー関数gotoPage()の中でチェックして、体力が0以下なら専用のページに飛ばします。

ゲーム画面

参照:gb_dead.html

ゲームオーバー専用のページ、gb_dead.htmlのソースを見てください。

ストーリー部はゲームオーバーと分かる内容にします。

イベントボタンは、やり直しとゲームの終了の2つです。

<p class="selbtn"><input type="button" onClick="goR()" value="再度挑戦する"></p>
<p class="selbtn"><input type="button" onClick="gotoBack()" value="あなたの家に戻る"></p>

「再度挑戦する」場合は、ペナルティを受けて導入画面から始められます。
「戻る」を選ぶと、ゲームのページから出ます。導入画面の「森に入らず帰る」と同じです。

スクリプト

「再度挑戦する」を選ぶと、ユーザー関数goR()を実行します。

function goR() {
restartStatus();
gotoPage("gb00");
}

ユーザー関数restartStatus()では、ステータスにペナルティを与えています。体力を10に戻し、各ステータスを1つ下げます。
その後、導入ページに戻ります。

ゲームファイル(6) エンディング

ストーリーの完結となるのがエンディングです。

今回はエンディングは1つですが、複数のエンディングを用意することもできます。

ゲーム画面

参照:gb99.html

gb99.htmlのソースを見てください。

ストーリーは、物語を終わりをしっかりと描きます。

イベントボタンはゲームオーバーのページと同じようになっています。

<p class="selbtn"><input type="button" onClick="goR()" value="森の入口に戻る"></p>
<p class="selbtn"><input type="button" onClick="goH()" value="森を後にする"></p>

「森の入口に戻る」場合、ステータスは体力だけ10に戻して、他のステータスはそのまま引き継ぎます。
クリアしたご褒美に、手軽にもう一度遊んでもらって、別の選択肢を試して物語の変化を楽しんでもらうことができます。

スクリプト

「森の入口に戻る」場合のユーザー関数goR()は次のようになっています。

function goR() {
//体力初期値
Player[1] = 10;

gotoPage("df00");
}

ユーザー関数のまとめ

最後にユーザー関数をまとめておきます。

ゲームファイルで使うユーザー関数

ユーザー関数名 解説 含まれるユーザー関数
gotoPage(page) レベルアップと体力のチェックをしてページを移動する setExp()
saveStatus()
gotoBack() ステータスを初期化して、ホームページに戻る defaultStatus()
saveStatus()
getDamage(pw,th) 敵の攻撃力(pw)と体力(th)からダメージを計算し変数damageに入れる  
getDice(num) 0~numまでの乱数を発生させて変数diceに入れる  

スクリプトファイルで使うユーザー関数

ユーザー関数名 解説 含まれるユーザー関数
setStatus() ステータスを表示(ルートで実行)  
setPLName() プレイヤー名を表示(ルートで実行)  
saveStatus() cookieの保存  
defaultStatus() ステータスの初期設定  
resetStatus() ステータスの初期化 defaultStatus()
saveStatus()
restartStatus() ゲームオーバー時の再スタート処理 saveStatus()
setExp() レベルアップのチェック  

戻る