Home -> HSP講座 -> 初級編 No.12

記憶するためのもの

今回もまたまた数当てゲームの改良です。
もうちょっとなので、我慢してください。


ファイルとは?

ファイルとは何か……。これの説明は別サイトに丸投げして、割愛します。
わからなければ、検索でもしてください。


まずは読み込み

というわけで、HSP からファイルを操作する方法を覚えましょう。
まずは「読み込み」作業です。

「読み込む」は、英語で LOAD。
使う命令は bload です。

a.txt というファイルを適当に用意して、HSP の作業フォルダにおいてください。
+参照:ない場合 → ダウンロード(対象をファイルに保存)

// bload でファイルを読み込む
// 長くなるので、wID_Main とかは省略

*main
	sdim buf, 65535
	fileName = "a.txt"	// ファイル名を指定します
	
	gosub *Load		// ファイル読み込み作業
	gosub *Output		// 出力作業
	
	stop
	
*Load
	exist fileName			// ファイルサイズを確認
	if ( strsize < 0 ) {
		dialog "ファイルがありません!", 1, "Error"
		return
	}
	
	fileSize = strsize
	
	if ( fileSize > 65535 ) {	// ファイルサイズが大きすぎる場合
		fileSize = 65535	// 65535 に調整
	}
	
	// ファイルを読み込む
	bload fileName, buf, fileSize
	return
	
*Output
	mesbox buf, ginfo_winx, ginfo_winy - 20
	return

簡単ですね。HSPならではの簡潔さです。

exist 命令は、ファイルが存在するかどうかを確認する命令です。
ついでにファイルのサイズも取得できます。
exist 命令を使うと、strsize というシステム変数にファイルサイズが代入されるんですが、
「システム変数って何?」は後述です。
変数の一種だと考えてください。
ちなみに、ファイルが存在しなかったらが格納されます。

dialog 命令は、名前そのまま「ダイアログを表示」します。
モード 1 は警告と [OK] ボタンです。

bload 命令は、さっき話した、ファイルを読み込む命令です。
第二パラメータ(p2)に変数を指定して、それのなかにファイルの内容を代入します。
p3 は読み込むサイズです。exist命令で取得したファイルサイズを指定したので、
すべて読み出します。

ginfo_winx, ginfo_winy はそれぞれ、ウィンドウの大きさを表します。
(正確には、クライアント領域の広さです。)
これらは「マクロ」といって、別のものに置き換えられます。
ginfo_winx は、「ginfo(12)」に置き換えられます。
HSPは、スクリプトの中にあるマクロを元の文字列に置き換えてから、そのスクリプトを処理します。
つまり、僕がginfo_winxと書いても、後で ginfo(12) に書き換えられてしまうということです。
だから、ginfo_winxginfo(12) は同じものです。

じゃあなんで直接 ginfo(12) と書かないかというと、可読性(読みやすさ)のためです。
ginfo_winx だったら、「Window のX => ウィンドウの横幅」だとわかります。
でも 12 じゃ、なにかわかりません。
見やすさ向上のために、あえてマクロを使っているのです。


次は書き込み

読み込みができたら、次は書き込みです。
こっちも非常に簡単です。

だいたい見当がつくでしょうけど……
「読み込む」は英語で LOAD → 命令名は bload
「書き込む」は英語で SAVE → 命令名は bsave

// bsave でファイルに書き込む

*main
	sdim buf, 320
	fileName = "b.txt"	// ファイル名を指定します
	
	gosub *Save		// ファイル読み込み作業
	gosub *Output		// 出力作業
	
	stop
	
*Save
	exist fileName			// ファイルサイズを確認
	if ( strsize >= 0 ) {
		dialog "ファイルがすでに存在します!", 1, "Error"
		return
	}
	
	// ファイルに書き込む内容
	buf = {"
		\tプログラ広場
		\t
		\tプログラミング関係のサイトです。
		\t管理人が作成したソフトやモジュールを公開中!
		\tHSP や AB の講座もやっています。
		\t一度見に来てね。
		\t最終更新:
	"}
	
	// ファイルを作成する
	bsave fileName, buf, strlen(buf)
	return
	
*Output
	mesbox buf, ginfo_winx, ginfo_winy - 20
	return

一応、安全対策もしておきました。
既存のファイルに上書きしたら、有害サイトになってしまうので^^;。

さて、内容はさっきのサンプルとほぼ同じなので、解説は省略です。
(さっきも省略したのはいうまでもない。)

bsave 命令は、ちょうど bload 命令のです。
「ワンキー・ヘルプ(F1)」で十分でしょう。


テキストファイルとバイナリファイル

簡単に、2つの違いを書いておきましょう。

テキストファイルというのは、その名の通り文章が書いてあります。
これはほぼすべての「テキストエディタ」で開くことができ、人間が見てもわかります。
また、一文字一文字を表すためのルールが決まっています。
機械はすべての情報を数値で記憶するので、文字を数値に変換するルールが必要です。
このようなルールを「エンコーディング(encoding)といいます。
ルールに則って文字から変換された数値を「文字コード」と呼びます。

バイナリファイルは、2進数(binary)で書かれたファイルです。
これは、人間が見ても意味不明です。
大抵の場合、専用のソフトでしか開けませんし、内容の意味も、そのソフトの作者ぐらいしか知りません。
内容に共通のルールはありません。

ちなみに、文字・文章も突き詰めて言えば2進数で保存されているので、テキストファイルもバイナリファイルの一種です。
一般的に、テキストファイルとバイナリファイルは分けて考えられます。


データを保存する

ゲームといえば「セーブ」です。
今までのデータを、次するときにも使えるように、セーブするのが普通でしょう。

というわけで、データを保存してみましょう。
数当てゲームで、クリアの割合を記録してみます。

ファイルの形式は CSV です。CSV というのは、値を半角カンマ , で区切る形式です。
かなり一般的なので、これを使ってみましょう。
ファイルには、「ゲーム回数」と「クリア回数」を書き込みます。

CSV 形式の文字列を操作するには、getstr 命令を使います。

	getstr str 変数, CSV の変数, index, ','
という形で使います。
index には、初めは 0、二回目からは strsize と書きます。
第一パラメータ(p1)の変数は、sdim命令で初期化しておきます。

今回は、記述力向上を狙って、先に、自分でスクリプトを書いてもらいます。
新たに使用する命令は、ありません。全部説明済みです。

完成したら次に進んでください。
goto *script

*script
数当てゲーム3のスクリプトに、
次のサブルーチンを追加してください。
拡張性の高さも、構造化の利点です。

// データ記録
*saveData
	sdim data
	sdim scntGames
	sdim scntClears
	
	exist "nhg.txt"				// ファイルサイズを取得する
	if ( strsize < 0 ) {
		bsave "nhg.txt", data, 0	// 空のファイルを作る
	}
	bload "nhg.txt", data, strsize		// 読み込む
	
	// 情報を取り出す
	getstr scntGames,  data, 0, ','		// ゲーム回数
	getstr scntClears, data, strsize	// クリア回数
	
	// ゲーム回数を増加
	scntGames = str(int(scntGames) + 1)	// +1 する
	
	// クリア回数を増加
	//! *Clear の時は bWin に真を、 *TimeUp の時は bWin に偽(0) を代入するように変更
	if ( bWin ) {
		scntClears = str(int(scntClears) + 1)
	}
	
	// 保存する
	data = scntGames +","+ scntClears
	bsave "nhg.txt", data, strlen(data)
	
	return
※一部だけ。ゲーム終了時にgosubでここにジャンプする。

サブルーチンを追加しただけでは意味がないので、他のラベルも若干修正します。
*Clear*TimeUp を、次のように変更します。

// 正解した
*Clear
	color 0, 0, 255
	font msgothic, 50, 1
	mes "正解!お見事!!"
	
	dialog "正解です!\nあなたは "+ nAnsCount +"回で正解しました", 0, "おめでとう"
	
	// 画面をリセット
	gosub *ChangeScreenToGame
	
	// 問題変更
	nAnsCount = 0
	gosub *CreateQuestion
	
// 記録 bWin = 1 // 0 以外の値 gosub *saveData
bGameEnd = 1 // ゲームが終了したことを表す return
// 時間切れ
*TimeUp
	color 255, 0, 0
	font msgothic, 60, 1
	mes "タイムアップ!!"
	
	dialog "時間切れです。答えは "+ question +"でした。", 0, "残念"
	
	// 画面をリセット
	gosub *ChangeScreenToGame
	
	// 問題変更
	nAnsCount = 0
	gosub *CreateQuestion
	
// 記録 bWin = 0 // 0 gosub *saveData
bGameEnd = 1 return

太字のところを追加しただけです。
gosub ジャンプの他、
正解した場合の *Clear ラベルでは、bWin に真を、
失敗した場合の *TimeUp ラベルでは偽を代入する文を追加しています。

これで実行すると、ちゃんと記録をしてくれます。
ここを見る前に作ったスクリプトが動かなかったら、なぜ動かないのかじっくり考えてみてください。


構成設定ファイルを使う

さてと。CSV 形式のファイルを使う方法を説明しましたが、見てわかるとおり結構めんどくさいですよね?
実はもっと簡単に記録する方法があります。
構成設定 (INI) ファイルを使う方法です。

INI というのは、以前マイクロソフト社(Windowsを作っている会社)が提供していた形式です。
※今は推奨されていませんが、使う人は非常に多いので、あまり気にしなくても大丈夫。
実際はただのテキストファイルです。
CSV のように、書き方(書式)が決まっています。

[セクション名1]
キー1=値1
キー2=値2
[セクション名2]
…(省略)
; この行はコメント

セクション・キー・値の三つを使って記録します。
見ると、キー1つと値1つが、= につながれているのがわかります。
ということは、キーを指定すれば、値を取り出せる!

セクションは、キーの入れ物です。
キーがそこら中に散らばるとまとまり感がないので、
セクションで関連するキーを纏めます。

要するに、セクション名とキー名を指定して、値を取り出したり、変更したりするのです。
なんだかめんどくさそうですけど、やってみましょう。

*main
	sdim val1
	sdim val2
	sdim val3
	
	filename = "__data_file__.ini"	// 設定ファイル名( 安全のため、変な名前にする )
	section  = "default"		// セクション名。今は何でもいい
	
	screen 0, 180, 130, 2
	
	pos 10, 13 : mes "1"
	pos 30, 10 : input val1, 120, 25 : oID_input1 = stat
	pos 10, 43 : mes "2"
	pos 30, 40 : input val2, 120, 25 : oID_input2 = stat
	pos 10, 73 : mes "3"
	pos 30, 70 : input val3, 120, 25 : oID_input3 = stat
	
	pos 10, 100 : button gosub "save", *save
	pos 80, 100 : button gosub "load", *load
	
	gsel 0, 1
	stop
	
// 保存ルーチン
*save
	sdim data, 320		// 大きめに確保
	
	// ※完全に上書きなので、読み込まなくていい
	
	// データを作成
	data  = "["+ section +"]\n"	// セクションは大括弧 [] で囲む
	data += "val1="+ val1 +"\n"
	data += "val2="+ val2 +"\n"
	data += "val3="+ val3 +"\n"
	
	// 保存
	bsave filename, data, strlen(data)
	
	dialog "セーブしました!"
	
	return
	
// 読み込みルーチン
*load
	exist filename
	if ( strsize < 0 ) {
		dialog "設定ファイルがありません!", 1, "Error"
		return
	}
	
	sdim data, strsize + 1		// ファイルサイズより大きく確保 (必須)
	bload filename, data, strsize	// 全部読み込む
	
	// セクションを探す
	index = instr(data, 0, "["+ section +"]\n")
	if ( index < 0 ) {
		dialog "セクション "+ section +" がありません!", 1, "Error"
		return
	}
	
	index += strlen("["+ section +"]\n")	// 検索開始位置を変更
	
	// キーを探す
	n = instr(data, index, "val1=")
	if ( n < 0 ) { goto *Error_NoKey }		// エラー
	getstr val1, data, index + n + strlen("val1=")	// 改行まで取り出す
	
	n = instr(data, index, "val2=")
	if ( n < 0 ) { goto *Error_NoKey }		// エラー
	getstr val2, data, index + n + strlen("val2=")	// 改行まで取り出す
	
	n = instr(data, index, "val3=")
	if ( n < 0 ) { goto *Error_NoKey }		// エラー
	getstr val3, data, index + n + strlen("val3=")	// 改行まで取り出す
	
	// 入力ボックスの値を変える
	objprm oID_input1, val1
	objprm oID_input2, val2
	objprm oID_input3, val3
	
	dialog "ロードしました!"
	
	return
	
*Error_Nokey
	dialog "キーがありません!", 1, "Error"
	return

長ぇ!!!
長い、長すぎます。
これは酷い……。

と、そのまえに、新出命令・関数の説明です。
instr()関数は、文字列の中から文字列を検索します。
p1 に検索したい文字列型の変数を指定します。
p3 の文字列があった場合、それへの「インデックス」を返します。
なければ、負数(-1)を返します。
p2 には、検索開始オフセットというのを指定します。

この、インデックス値は、先頭の文字を 0 として、一文字進むごとに 1 づつ増えていく数値です。
たとえば、「Hello.」という文字列では、「o」のインデックスは 4 です。
一番左にカーソルを合わせて、右に動くごとに +1 されます。
これは、文字列に含まれる文字の位置を表すときに使われます。

文字インデックスの図解
※文字列"Hey, John"とインデックス
この図で、文字列の上がインデックス値です。インデックス値が指す文字は、数字の右下です。
右端の「×(ばつ印)」は、インデックス値が存在しないことを表します。右下に文字が無いので、当然ですね。
※右端の塗りつぶされた文字は NULL 終端文字と言いますが、気にしなくて結構です。

ちなみに、instr()が返すインデックス値は、発見した文字列の位置から、
p2の値を引いた値です。

サンプルの変数 index は、セクション [section] の次の行の先頭を指しています。
また、(n + index) は、検索したキーの最初の文字を指しています。
(n + index + キーの長さ + '='の長さ) で、値の最初の文字を指します。
この辺わかりにくいので、慣れるまで苦労するかも。(僕だけ?)

そして本題。INI が予想外に便利じゃないという話でした。
普通に文字列操作をするなら、こんな感じでめんどくさいですが、
マイクロソフト社が一時期推奨していた」だけあって、
もっと簡単にする方法があります!!

でもその方法を直接使うと、ある程度知識が必要なので、
簡略化したバージョンを僕が作っておきました。
+参照:ini module

使い方は、コメントに書いてあるとおりです。
WriteIniで書き込み、GetIniで読み込み。

このモジュールは、「Module」みたいなフォルダを作って、そこに保存しておいてください。意外と役に立ちます。
"ini.as"のように、汎用的に使える、独立したプログラムをモジュール(module)といいます。どうでもいいですが……。

では、これを使ってさっきのサンプルを書き直します。

#include "ini.as"

*main
	sdim val1
	sdim val2
	sdim val3
	
	// INI ファイル名はフルパス!
	filename = "./__data_file__.ini"	// 設定ファイル名( 安全のため、変な名前にする )
	section  = "default"			// セクション名。今は何でもいい
	
	SetIniName filename		// ini ファイル名を設定する
	
	screen 0, 180, 130, 2
	
	pos 10, 13 : mes "1"
	pos 30, 10 : input val1, 120, 25 : oID_input1 = stat
	pos 10, 43 : mes "2"
	pos 30, 40 : input val2, 120, 25 : oID_input2 = stat
	pos 10, 73 : mes "3"
	pos 30, 70 : input val3, 120, 25 : oID_input3 = stat
	
	pos 10, 100 : button gosub "save", *save
	pos 80, 100 : button gosub "load", *load
	
	gsel 0, 1
	stop
	
// 保存ルーチン
*save
	// ※完全に上書きなので、読み込まなくていい
	
	// データを保存
	WriteIni section, "val1", val1	// セクションは自動的に作成される
	WriteIni section, "val2", val2
	WriteIni section, "val3", val3
	
	dialog "セーブしました!"
	
	return
	
// 読み込みルーチン
*load
	exist filename
	if ( strsize < 0 ) {
		dialog "設定ファイルがありません!", 1, "Error"
		return
	}
	
	// 値を読み込む
	GetIni section, "val1", val1, 64, "err"	// p4 は読み出す最大文字数
	GetIni section, "val2", val2	// p5 は、エラーの時に返す文字列
	GetIni section, "val3", val3	// p6 は、INIファイル名。SetINIname したので指定しなくていい。
	
	// 入力ボックスの値を変える
	objprm oID_input1, val1
	objprm oID_input2, val2
	objprm oID_input3, val3
	
	dialog "ロードしました!"
	
	return

配列を使えばもっと楽になる。次回をお楽しみに。

#include疑似命令は、指定したスクリプト・ファイルを連結します。
ここでは、さっきのモジュール(ini.as)を連結しています。
フォルダの中のファイルは、「フォルダ名/ファイル名」とします。(パス)
連結というとなんだかすごいことをしているみたいですが、ただ単に、#include を、そのファイルの内容に置き換えているだけです。

// ファイル名:test1.hsp

	mes "test1.hsp の命令です"

// ファイル名:test2.hsp

#include "test1.hsp"	// 連結!

	mes "test2.hsp の命令です。"

// サンプル3

// ファイル名:test2.hsp

// ファイル名:test1.hsp

	mes "test1.hsp の命令です"

	mes "test2.hsp の命令です。"

面倒ですが、上2つを指定したファイル名で保存して、test2.hsp を実行してみてください。
これは、サンプル3と全く同じ動作をします。
この #include もですが、
# から始まる命令は、どれもプリプロセッサ命令です。大事なので、覚えておいてください。
※ # は シャープ ♯ ではなく ナンバーサイン #。

さっきの INI を使うサンプルに戻りましょう。
実は、filename に代入するファイル名を若干変更しています。
ファイル名の前に、"./" を付けました。
これは、「相対パス」というのですが、ファイルの話は別のところで説明するので省略します。
INI ファイルのファイル名には、./ を付けるようにしてください。(じゃないと失敗する)


ファイルパスの¥記号

※2009 02/02 追記分

ファイル編なのでついでに書いておきます。

他のサイトの掲示板に、しばしばこのような質問が寄せられます。

現在ランチャーソフトを作成しています。
ソフトのファイルパスを dialog 命令で取得し、保存したいと思っているのですが、
refstrが返すパスは、円記号¥が1つしか付いていません。
円記号が二重になった(¥¥)形式に変えるにはどうすればいいですか?

明らかに、ファイルパスに含まれる円記号が、二つずつなくてはいけないと思い込んでしまっている例です。

スクリプトの文字列の中では、「\\」と重ねないといけませんが、これはHSPでの特殊なルールに過ぎません。
+参照:エスケープシーケンス

// 円記号の数
	
	mes "'\\' 1つだけ"
	mes "D:\D_MyDocuments\DProgramFiles\hsp31\sampview.exe"
	mes
	mes "'\\' 2つ重ね"
	mes "D:\\D_MyDocuments\\DProgramFiles\\hsp31\\sampview.exe"
	mes
	mes "'\\' 4つ重ね"
	mes "D:\\\\D_MyDocuments\\\\DProgramFiles\\\\hsp31\\\\sampview.exe"
	mes
	
	string = "D:\\D_MyDocuments"	// 2つ書きました
	
	input string, 200, 25			// \ 1つしか表示されていません
	button gosub "出力", *OnBtn_Output
	
	stop
	
*OnBtn_Output
	mes string
	return

おわりに — 朗報

長文、おつかれさまでした。
この回をもって、数当てゲームの開発は終了です。
これ以上は、各自で拡張してください。
( たとえば、保存した成績を表示させる機能とか。 )

HSP自体の勉強はまだ続きます。
では、また次回。


by 上大

第十一章へ   第十三章へ