今回もまたまた数当てゲームの改良です。
もうちょっとなので、我慢してください。
ファイルとは何か……。これの説明は別サイトに丸投げして、割愛します。
わからなければ、検索でもしてください。
というわけで、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_winx と ginfo(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, ','という形で使います。
今回は、記述力向上を狙って、先に、自分でスクリプトを書いてもらいます。
新たに使用する命令は、ありません。全部説明済みです。
完成したら次に進んでください。
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 *saveDatabGameEnd = 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 *saveDatabGameEnd = 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 追記分
ファイル編なのでついでに書いておきます。
他のサイトの掲示板に、しばしばこのような質問が寄せられます。
明らかに、ファイルパスに含まれる円記号が、二つずつなくてはいけないと思い込んでしまっている例です。
スクリプトの文字列の中では、「\\」と重ねないといけませんが、これは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 上大