アドベンチャー

この章ではテキストを読み進めていく形式のアドベンチャーゲームを作ってみましょうか…。

もう少し説明するとホラー系のノベルや恋愛シミュレーション(笑)が当てはまるジャンルです。

アナタはコレ系のゲームをしたことがありますか?

このテのゲームを作るソフトも存在しますが一から自分の手で組んでみても面白いかもしれません。

TIPSにテキストから定義してある命令とパラメータの読み込み方を以前載せました。

ソレの改良(と呼べるかわかりませんが)とモジュールの画像エフェクト命令を利用したいと思います。

まずはサンプルで使用する命令を決めておきましょうか…。

・まず学校等ステージとなる背景画像を表示する命令「stage
・キャラクターを表示させる命令「charon」と、消去する命令「charoff
・場面切り替え時にウィンドウ内をフェードアウトする命令「dark
・画面を揺らす命令「quake
・キャラクターの表情を変化させる命令「status
・ユーザーに入力させる命令「input
・ゲーム終了する(エンディングに行く)命令「end

形として恋愛シミュレーションのような感じで、「とある学生が転校した所から」というシチュエーションで。

使用する画像…というかこの章のスクリプトまで全てが入ったものをお渡しします。コチラからどうぞ。

それではスクリプトの部分をモジュールから入りましょうか…。

#module "adventure"
#deffunc retmes val
	mref p1,24
	mref bmscr,67
	fsize=bmscr.32/2
	strlen len,p1
	if len*fsize+csrx < winx : mes p1 : return
	mx=csrx
	my=csry
	repeat len
		peek tmp,p1,cnt
		if tmp > 128&(tmp < 160)|(tmp > 223) : tmp.1=2 : fsize=fsize*2 : else : tmp.1=1
		if mx+fsize > 620 : mx=30 : my+=bmscr.32+10
		if tmp=13 : tmp.1=2 : fsize=fsize*2 : msg="\n" : else : strmid msg,p1,cnt,tmp.1
		pos mx,my : mes msg
		if tmp=13 : mx=30 : my+=bmscr.32+10 : else : mx+=fsize
		if tmp.1=2 : fsize=fsize/2 : tmp=cnt+2 : continue tmp
	loop
	return

#deffunc quake int,int,int,int,int
	mref p1,0
	mref p2,1
	mref p3,2
	mref p4,3
	mref p5,4
	direction=0
	tmp=0
	pos 0,0
	repeat p5*2
		redraw 0
		direction=cnt\2
		if p1=0 : gcopy p2,tmp,0,p3-5,p4-5 : if direction : tmp-=5 : else : tmp+=5
		if p1=1 : gcopy p2,0,tmp,p3-5,p4-5 : if direction : tmp-=5 : else : tmp+=5
		redraw
		wait 1
	loop
	gcopy p2,0,0,p3,p4
	return
#global

retmesはテキストが端まで来たら自動改行させる命令です。

パラメータとして文字列の入った変数を渡します。

内部で自動的に位置指定をしてますので表示位置には気を使わなくて結構です。

quakeは画面を揺らす命令ですね。

パラメータ1で寄らす方向を決めます。0で横、1で縦揺れ。

パラメータ2で揺らす画像(ウィンドウ)を指定します。

パラメータ3で画像の横幅を。

パラメータ4で画像の高さを指定します。

パラメータ5で一揺れの回数を指定します。

サンプルではこれらのモジュールを「adventure.as」として読み込んでいます。

それではサンプルの心臓部に入りましょうか。

#include "adventure.as"
#define WX 640 ; ウィンドウXサイズ
#define WY 480 ; ウィンドウYサイズ
#define ZOOM 3 ; 拡大率
#define CHARCTERS 4 ; 登場する人間の数
#define FACEX 48 ; 表示し始めるX位置
#define PMAX 3 ; 一度に登場できる最大人数
	sdim scenario,2048 ; シナリオデータ
	sdim back,16,3 ; 背景
	sdim myname,12 ; 自分の名前
	sdim name,4,CHARCTERS ; 登場人物の名前
	sdim msg,256 ; メッセージ用の変数
	sdim command,8 ; 命令名が入る
	sdim tmp,128,3 ; 一時変数
	sdim update,2 ; メッセージエリアの背景更新用フラグ
	dim tmpnum,2 ; 数値型用一時変数
	dim status,CHARCTERS ; 登場人物の現在の状態
	dim facey,CHARCTERS ; 登場人物の顔位置
	dim key,1 ; キー判定
	dim num,1 ; 現在登場している人数
	dim position,PMAX ; それぞれの立ち位置に誰が立っているか
	dim posx,PMAX,PMAX ; ソレぞれの立ち位置
	dim param1,1 ; 第一パラメータ用
	dim param2,1 ; 第二パラメータ用
	;------------- ココからシナリオデータ --------------
	back="gate.jpg","classroom.jpg","passage.jpg"
	name="A子","B子","A男","B男"
	posx.0.0=-64*ZOOM+WX/2 ; 1人
	posx.0.1=-64*ZOOM-250+WX/2,-64*ZOOM+250+WX/2 ; 2人
	posx.0.2=-64*ZOOM-400+WX/2,-64*ZOOM+WX/2,-64*ZOOM+400+WX/2 ; 3人
	facey=37*ZOOM,33*ZOOM,23*ZOOM,18*ZOOM
	scenario ="stage\"0\"\n"
	scenario+="msg\"俺は先週引っ越してきたという設定になっている。\"\n"
	scenario+="msg\"今日からこの新しい学校に通うことになるというのが定番だ。\"\n"
	scenario+="wait\"10\"\n"
	scenario+="msg\"【主人公】中に入らないと話が進まないので入るとするか。\"\n"
	scenario+="wait\"100\"\n"
	scenario+="quake\"1,1\"\n"
	scenario+="charon\"2\"\n"
	scenario+="status\"2,2\"\n"
	scenario+="msg\"【&name2;】いてっ!ぶつかってくるなゴルァ!前見て歩きやがれ!\"\n"
	scenario+="wait\"10\"\n"
	scenario+="msg\"来て早々ガラの悪そうな奴に出会ってしまった…。\"\n"
	scenario+="msg\"構っているとロクなことがない。ココは逃げるに限るな。\"\n"
	scenario+="wait\"50\"\n"
	scenario+="status\"2,0\"\n"
	scenario+="msg\"【&name2;】あ、この野郎。逃げるな!謝れっ!\"\n"
	scenario+="wait\"10\"\n"
	scenario+="dark\"200\"\n"
	scenario+="stage\"1\"\n"
	scenario+="msg\"【担任】…というわけで彼は今日からこのクラスの一員の…えっと名前何だったかな?\"\n"
	scenario+="wait\"10\"\n"
	scenario+="input\"&myname;\"\n"
	scenario+="msg\"【&myname;】&myname;です。\"\n"
	scenario+="wait\"100\"\n"
	scenario+="msg\"【担任】そう&myname;君だ。&myname;君の席はアソコだ。\"\n"
	scenario+="msg\"それではコレで朝のHRを終了する。\"\n"
	scenario+="msg\"1時間目始まるまで各自授業準備しておきなさい。\"\n"
	scenario+="wait\"50\"\n"
	scenario+="msg\"【&myname;】テメェが覚えておけっつーの(ボソ\"\n"
	scenario+="wait\"10\"\n"
	scenario+="dark\"200\"\n"
	scenario+="stage\"1\"\n"
	scenario+="charon\"1\"\n"
	scenario+="status\"1,3\"\n"
	scenario+="msg\"【&name1;】どうもはじめまして。私&name1;。ヨロシクね。\"\n"
	scenario+="wait\"100\"\n"
	scenario+="msg\"【&myname;】あ、あぁヨロシク…。\"\n"
	scenario+="wait\"100\"\n"
	scenario+="msg\"【&myname;】あ…。\"\n"
	scenario+="wait\"10\"\n"
	scenario+="charon\"2\"\n"
	scenario+="status\"2,5\"\n"
	scenario+="msg\"【&name2;】ん?\"\n"
	scenario+="msg\"【&myname;】…。\"\n"
	scenario+="wait\"50\"\n"
	scenario+="msg\"【&name1;】!!\"\n"
	scenario+="status\"2,2\"\n"
	scenario+="status\"1,1\"\n"
	scenario+="wait\"100\"\n"
	scenario+="charoff\"1\"\n"
	scenario+="status\"2,0\"\n"
	scenario+="msg\"【&name2;】テメェ!\"\n"
	scenario+="wait\"10\"\n"
	scenario+="quake\"0,3\"\n"
	scenario+="dark\"200\"\n"
	scenario+="msg\"そして月日は流れ、転校して1年が経った…(早\"\n"
	scenario+="wait\"10\"\n"
	scenario+="stage\"2\"\n"
	scenario+="charon\"0\"\n"
	scenario+="status\"0,3\"\n"
	scenario+="charon\"3\"\n"
	scenario+="status\"3,3\"\n"
	scenario+="msg\"【&name3;】…だよなぁ〜(藁\"\n"
	scenario+="msg\"【&name0;】そうよねぇ〜アハハw\"\n"
	scenario+="wait\"100\"\n"
	scenario+="msg\"【&myname;】そうだったんだ…って何の話? つーかその前に誰なんだよっお前らは!\"\n"
	scenario+="wait\"100\"\n"
	scenario+="msg\"【&name0;・&name3;】…さぁ…。\"\n"
	scenario+="status\"0,4\"\n"
	scenario+="status\"3,4\"\n"
	scenario+="wait\"10\"\n"
	scenario+="dark\"100\"\n"
	scenario+="end\"0\"\n"
	;------------------- ココまで ----------------------
	buffer 3
	picload "adv.bmp"
	bufx=winx
	bufy=winy
	buffer 2,bufx*ZOOM,bufy*ZOOM+100
	gzoom bufx*ZOOM,bufy*ZOOM,3,0,0,bufx,bufy
	color 200,200,255 : boxf 0,bufy*ZOOM,600,bufy*ZOOM+100
	buffer 4,600,100

はい。ココまでが準備ですね。

シナリオは外部ファイルにして読み込ませたほうがいろいろ便利です。

やり方は前章までと同じですので説明いたしません。

シナリオデータscenarioをよく見ると変数が入っていますね。

ココで言っているのはHSPの変数ではなく文字列に埋め込んでいる変数のことですよ?

具体的には「&myname;」「&name0;」「&name1;」「&name2;」「&name3;」の4つのテキストです。

HTMLタグの特殊文字と同じような感じにしてみました(^^;

コレを後のメイン処理で展開しています。

始めにZOOMという拡大率(倍数)が定義されていますが、

コレは人物画像の拡大率(倍数)です。300%にしています。

なんでこんなことをするかって言うと、他のゲーム等でも当てはまることなのですが、

無駄に細かい画像を使っても、その画像ファイルがハードディスクを圧迫するだけだからです。

3倍までするとこの章の初めにある完成図のように少しカクカクした画像になってしまいますが、

1.5〜2倍くらいなら大丈夫でしょう。

自分のパソコンだけで使うのはいいとして他人にプレイしてもらう時はそういう面まで気を配りましょう。

 

それでは続きましてメインの処理に入っていきましょう。

TIPSと同じ要領で一行を1データとして読み込み処理をしていきます。

*main
	gsel 0
	font "MS 明朝",18,1
	gmode 2
	notesel scenario
	notemax max
	repeat max ; シナリオデータの最後まで1行ずつ読み込む
		noteget tmp,cnt
		getstr command,tmp,0,'"' ; 命令名取出し
		getstr tmp,tmp,strsize,'"' ; パラメータ取り出し
		if command="msg" { ; メッセージエリアに表示させる命令
			repeat
				tmpnum.1=0 ; 変数を名前に変換するのに必要なフラグ変数
				instr tmpnum,tmp,"&myname;",0
				if tmpnum ! -1 {
					tmp.2 = myname : tmpnum.1=8
				} else {
					instr tmpnum,tmp,"&name0;",0
					if tmpnum > -1 : tmp.2 = name.0 : tmpnum.1 = 7
					instr tmpnum,tmp,"&name1;",0
					if tmpnum > -1 : tmp.2 = name.1 : tmpnum.1 = 7
					instr tmpnum,tmp,"&name2;",0
					if tmpnum > -1 : tmp.2 = name.2 : tmpnum.1 = 7
					instr tmpnum,tmp,"&name3;",0
					if tmpnum > -1 : tmp.2 = name.3 : tmpnum.1 = 7
				}
				if tmpnum.1 = 0 : break
				strmid tmp.1,tmp,0,tmpnum
				strmid tmp,tmp,tmpnum+tmpnum.1,128
				tmp=""+tmp.1+""+tmp.2+""+tmp
			loop
			msg+=tmp+"\n"
		}
		if command="wait" { ; ウェイト(クリックするまでストップさせる)命令
			int tmp
			if msg!="" { ; 表示させるメッセージがある場合
				redraw 0
				gmode 4,,,200 ; 背景を少し透過させてメッセージエリアを表示
				pos 20,WY-120 : gcopy 2,0,bufy*ZOOM,600,100
				gmode 2
				pos 30,WY-110 : retmes msg ; メッセージを実際に表示させるのはウェイトさせた時
				msg=""
				redraw
			}
			repeat
				stick key,,1
				if key&256 : break ; クリックされるまでループし続ける
				wait 1
			loop
			wait tmp
			pos 20,WY-120 : gcopy 4,0,0,600,100 ; メッセージエリアをクリアする
		}
		if command="input" { ; ユーザーに入力させる命令
			color 192,192,192 : boxf : color ,,255
			if tmp="&myname;" { ; コレしかないのだが...
				pos 100,100 : mes "アナタの名前を決めてください(最大半角11文字)"
				pos 200,200 : input myname,100
				pos 350,190 : gcopy 2,ZOOM*224,ZOOM*128,ZOOM*32,ZOOM*16 ; ボタン
				repeat
					stick key,,1
					if key&256 {
						tmp=mousex
						tmp.1=mousey
						if myname ! "" {
							if 350 < tmp & (ZOOM * 32 + 350 > tmp) {
								if 190 < tmp.1 & (ZOOM * 16 + 190 > tmp.1) : break
							}
						}
					}
					wait 1
				loop
				>pos 350,190 : gcopy 2,ZOOM*224,ZOOM*144,ZOOM*32,ZOOM*16 ; ボタン凹
				repeat 1
					stick key,256,1
					if key&256 : wait 1 : continue 0
				loop
				pos 350,190 : gcopy 2,ZOOM*224,ZOOM*128,ZOOM*32,ZOOM*16 ; ボタン凸
				clrobj
				pos 0,0 : gcopy 3,0,0,WX,WY
				color
			}
		}
		if command="quake" { ; ウィンドウ全体(ウィンドウ内)を揺らす命令
			getstr param1,tmp,0,',' ; 揺らす方向を読み込む(0=横,1=縦)
			getstr param2,tmp,strsize ; 揺らす回数を読み込む
			int param1
			int param2
			quake param1,3,WX,WY,param2 ; 揺らす
		}
		if command="status" { ; キャラクター表情変更命令
			getstr param1,tmp,0,',' ; キャラクタID
			getstr param2,tmp,strsize ; キャラクタの表情
			int param1
			int param2
			status.param1=param2 ; キャラの状態を更新
			redraw 0
			tmpnum=num-1
			repeat num
				tmp=position.cnt
				pos posx.cnt.tmpnum + FACEX,facey.tmp
				gcopy 2, ZOOM * 64 * tmp + FACEX, facey.tmp - 50, ZOOM * 32, ZOOM * 32
				pos posx.cnt.tmpnum + FACEX,facey.tmp
				gcopy 2, ZOOM * status.tmp * 32, ZOOM * 128, ZOOM * 32, ZOOM * 32
			loop
			redraw
		}
		if command="stage" { ; 背景画像読み込み命令
			int tmp
			buffer 3
			picload back.tmp
			gsel 0
			redraw 0
			pos 0,0 : gcopy 3,0,0,WX,WY
			redraw
			repeat PMAX
				position.cnt=-1 ; キャラクタを全て消去
			loop
			num=0 ; 人数も0に戻しておく
			update="1" ; メッセージエリアの背景記憶フラグ
		}
		if command="charon" { ; キャラクター表示命令
			int tmp
			position.num=tmp ; 誰を表示するか
			redraw 0
			pos 0,0 : gcopy 3,0,0,WX,WY
			repeat num+1
				pos posx.cnt.num, 50
				gcopy 2, ZOOM * 64 * position.cnt, 0, ZOOM * 64, ZOOM * 128 ; キャラクタ
			loop
			redraw
			num+
			update="1"
		}
		if command="charoff" { ; キャラクター消去命令
			int tmp
			redraw 0
			pos 0,0 : gcopy 3,0,0,WX,WY
			repeat num
				if position.cnt=tmp {
					tmp=cnt
					repeat num-cnt-1,tmp
						tmpnum=cnt+1
						position.cnt=position.tmpnum
					loop
					num-
				}
				break
			loop
			tmpnum=num-1
			repeat num
				pos posx.cnt.tmpnum, 50
				gcopy 2, ZOOM * 64 * position.cnt, 0, ZOOM * 64, ZOOM * 128 ; 残りキャラクタ
			loop
			redraw
			update="1"
		}
		if command="dark" { ; ウィンドウを暗くする命令
			int tmp
			repeat 32,1
				redraw 0
				color : boxf
				gmode 4,,,-cnt*8+256
				pos 0,0 : gcopy 3,0,0,WX,WY ; 徐々に暗くする
				redraw
				wait 1
			loop
			update="1"
			gmode 2
			wait tmp
		}
		if command="end" : break ; ゲーム終了(ループを抜ける)命令
		if update="1" { ; テキストエリア背景更新
			gsel 4
			pos 0,0 : gcopy 0,20,WY-120,600,100 ; メッセージエリアの背景を更新
			gsel 0
			update="0"
		}
	loop
	color : boxf : color 255
	font "MS 明朝",400
	pos 120,40 : mes "終"
	stop

なんか非常に見難くなってしまいましたね(^^;

スクリプト内で新しく作成した命令たちをモジュールにした方がよかったですね…。

流れとしてこういう風に作っていけばアドベンチャーゲームになるよ、という程度とお考えください(^^;

必要だけど細かい処理は省いていたり簡略化したりしています。

例えば「表示テキストがエリアサイズより大きくなったらテキストエリアのページを切り替える処理」や、

「自分の名前を入力するところなどゲームに似合わないinputを使ったり」しています。

また「ゲームの進行を分岐させる話」がなかったり「セーブ・ロード機能」も付いていません。

ココでは必要最低限な「キャラが同画面内に入ったら配置変更」したり、

「キャラクターの表情の変更」といった基本部分のみ取り入れています。

細かい点については今後にアドベンチャーゲーム作成ツールを説明する講座が出るようであれば説明します。

ごちゃごちゃして難しそうに感じる方もいると思いますが、比較的簡単なので一度トライしてみてください。

まぁシナリオを考えるのが非常に面倒だとは思いますが…(^^;

今になってよく考えてみると、この章ではじめて背景に画像を使用するようですね…。

 

- 謝辞 -

今回サンプルで使用した下記の背景画像は「CHIC STUDIO(活動停止)」様の素材を使用させていただきました。