HSP3Dish.jsでセーブ
目次
はじめに
この記事では、HSP3.7β9 HSP3Dish.js の内容について記載しています。 今後のバージョンアップで変更される可能性がある点に注意してください。
セーブ機能がほしい
ゲームやアプリには、保存機能はやっぱりほしいですよね。 ゲームでセーブできないのは不便です。
もちろんHSP3にもデータを保存/読込する命令が用意されています。(bsave
/bload
, notesave
/noteload
, bmpsave
/picload
)
これらの命令を使うと、Windowsの場合データはファイルに保存されて簡単に利用できます。
ところがHSP3Dish.jsを使用した場合は、ファイルへの出力ができません。 HSP3Dish.jsはブラウザのJavaScriptで動作しています。 ウェブブラウザで動くJavaScriptは、基本的に閲覧者側のファイルシステムにアクセスすることはできません。 またサーバー側にファイルを置くことも簡単にはできません。(File APIの話は、今回はしません。)
しかしHSP3Dish.jsでも保存機能は必要です。 というわけで、ウェブアプリでのデータの保存方法、HSP3Dish.jsで保存機能を実装する方法について調べてみました。
ウェブアプリでのデータ保存方法
ウェブブラウザでデータ保存が出来そうな方法をいくつか調べてみました。 多少の間違いはあるかもしれないので、正確な情報はご自身で確認してください。
Cookie
Webサイト内でユーザーが入力した情報などを保存するために使ったりするものです。次のような特徴があります。
- 保存場所:ブラウザ内(デバイスのストレージ)
- 容量:最大 4KB
- 保存できるのは文字列のみ
- サーバー側からも参照できる
- 有効期限があり、時間経過などで削除される。
IndexedDB
Webブラウザに実装されているデータを保存するデータベースの機能です。 IndexedDB API の機能で、ブラウザを閉じてもデータは消えません。 有効期限なしでデータを保存し、JavaScript によるクリア、もしくは、ブラウザーに保存したデータのクリアによってのみクリアされます。 次のような特徴があります。
- 保存場所:ブラウザ内(デバイスのストレージ)
- 容量:ストレージの空き容量の10%
- JavaScriptだけで読み書きが出来る。だいぶめんどくさい。
- テキスト以外にもバイナリデータを保存できる
- 同一ドメイン内ならデータを共有できる。ドメインが異なるとアクセスできない。
- オフラインでも読み書きできる。
localStorage
Webブラウザが管理するデータの保存領域です。 Web Storage APIの一部で、ブラウザを閉じてもデータは消えません。 有効期限なしでデータを保存し、JavaScript によるクリア、もしくは、ブラウザーキャッシュ/ローカルに保存したデータのクリアによってのみクリアされます。 次のような特徴があります。
- 保存場所:ブラウザ内(デバイスのストレージ)
- 容量:最大 5MB
- JavaScriptだけで読み書きが出来る。簡単。
- テキストしか保存できない。
- 同一ドメイン内ならデータを共有できる。ドメインが異なるとアクセスできない。
- オフラインでも読み書きできる。
サーバー上のDB(データベース)
サーバー上にDBとAPIサーバーまたはCGIを置く。 ブラウザ側からAPIを実行して、サーバー上のDBにデータを保存します。 掲示板やゲームのスコアランキングなどで見られる昔からある方法です。 サーバー側での準備が必要なので大変。データはサーバー上に保存されるので消えたり改ざんされる心配がない。 次のような特徴があります。
- 保存場所:サーバー上のDB
- 読み書きにはDB、APIやCGIを置けるサーバーと、JavaScriptが必要。すごく大変。
- 保存時と違うデバイスを使っていても、保存したデータを利用できる。
- ランキングなどのように、ユーザーデータを公開可能。
Firebase
Google が提供するモバイルや Web アプリケーションの開発プラットフォーム。 BaaSと呼ばれるサービスの一種。データベース管理やファイルの蓄積、ユーザー認証などのよく使う機能が提供されています。 サーバー上にDBを置く方法を、使いやすくしてくれているサービスです。 次のような特徴があります。
- 保存場所:Firebase(Googleのサーバー)
- 容量:プランによる(無料版あり)
- Googleアカウントが必要。
- 個人開発の小規模なものなら無料枠でも十分かも。
- プッシュ通知機能を付ける際にも利用できる。
- 保存時と違うデバイスを使っていても、保存したデータを利用できる。
- ランキングなどのように、ユーザーデータを公開可能。
保存方法の比較
いっぱい書いてあってなんだか分からないので、表にまとめてみます。
Cookie | IndexedDB | localStorage | Firebaseなど | |
---|---|---|---|---|
容量制限 | 4KB | ストレージの10% | 5MB | サーバー側で管理(プランによる) |
データ形式 | 文字列 | オブジェクト | 文字列 | だいたいなんでも |
有効期限 | 有効期限の設定あり | 手動で消すまで | 手動で消すまで | サーバー側で管理 |
データアクセスの範囲 | 同一ドメイン内 | 同一ドメイン内 | 同一ドメイン内 | インターネット経由でどこからでも |
保存する用途の例 | 入力したログインIDなど | ゲームのセーブデータ、大量のアイテムデータ | アプリの設定、ゲーム進行状況 | スコアランキング |
Cookieはゲームのセーブに向いてないですね。一番便利そうなのはFirebaseですが、HSP3の外でやる作業が多そうです。HSP3側での実装もJavaScriptの準備が多くて大変そうです。 ローカルに保存される方法の中では、IndexedDBは容量も大きくデータの種類も選ばないので使い勝手がよさそうです。
調べてみるとHSP3.7β9のHSP3Dish.jsでは、IndexedDBとlocalStorageが使用できるそうです。というわけで、ここからはこの2つのHSP3Dish.jsでの実装をやってみます。
IndexedDB
HSP3Dish.jsではマニュアルには記述はありませんが、 実装されている機能からの推測としてIndexedDBへの対応が検討されているようです。
HSP3Dish.jsで保存機能を実装している方々からの情報を頼りにやり方を調べてみました。 マニュアル記載外の機能なので、真似する場合は自己責任でお願いします。
IndexedDB:作例と設置方法
こちらが作例です。 作例:./dish/save01.html
HSP3
; スマホ向けHSP3Dishのサンプル
; 保存のテスト
; indexedDb
#include "hsp3dish.as"
strVar = ""
intVar = 0
dblVar = 0.0
dim arrVar, 100
buf_note = ""
msg = ""
objsize 200
strObjStr = "文字列" : input strObjStr, , , 128 : idObjStr = stat
strObjInt = "1234" : input strObjInt, , , 128 : idObjInt = stat
strObjDbl = "3.14" : input strObjDbl, , , 128 : idObjDbl = stat
strObjArr = "10, 20, 30" : input strObjArr, , , 128 : idObjArr = stat
button gosub "保存", *l_save_HSP_SYNC_DIR
button gosub "読込", *l_load_HSP_SYNC_DIR
button gosub "削除", *l_clear_HSP_SYNC_DIR
button gosub "コピー", *l_copy_HSP_SYNC_DIR
button gosub "note保存", *l_save_note
button gosub "note読込", *l_load_note
button gosub "dir", *l_dir
redraw 1
; メインループ
*main
redraw 1 : await 16 : redraw 0 : color 255, 255, 255 : boxf : color : pos 0,0
; note
pos 0,260
mes "" + buf_note
mes "" + msg
goto *main
stop
; HSP_SYNC_DIR 保存
*l_save_HSP_SYNC_DIR
; 入力ボックス内の文字列をそれぞれのデータ型に変換
strVar = strObjStr ; 文字列
intVar = int(strObjInt) ; 整数
dblVar = double(strObjDbl) ; 実数
split strObjArr, ",", b ; 整数配列
n = stat
if n > 0 {
dim arrVar, n
repeat n
arrVar(cnt) = int(b(cnt))
loop
} else {
dim arrVar, 1
}
; 保存
bsave "/save/strVar", strVar
bsave "/save/intVar", intVar
bsave "/save/dblVar", dblVar
bsave "/save/arrVar", arrVar
devcontrol "syncfs"
return
; HSP_SYNC_DIR 読み込み
*l_load_HSP_SYNC_DIR
fname = "/save/strVar"
exist fname
if strsize > 0 {
strVar = ""
bload fname, strVar
objprm idObjStr, strVar
}
fname = "/save/intVar"
exist fname
if strsize > 0 {
intVar = 0
bload fname, intVar
objprm idObjInt, "" + intVar
}
fname = "/save/dblVar"
exist fname
if strsize > 0 {
dblVar = 0.0
bload fname, dblVar
objprm idObjDbl, "" + dblVar
}
fname = "/save/arrVar"
exist fname
if strsize > 0 {
arrVar = 0,0,0
bload fname, arrVar
s = strf("%d, %d, %d", arrVar(0), arrVar(1), arrVar(2))
objprm idObjArr, s
}
redraw 1
return
; HSP_SYNC_DIR DB削除
*l_clear_HSP_SYNC_DIR
; #Error 12 --> File I/O error
fname = "/save/strVar"
exist fname
if strsize > 0 {
strVar = ""
delete fname
objprm idObjStr, strVar
}
devcontrol "syncfs"
; #Error 12 --> File I/O error
; delete "/save"
return
; HSP_SYNC_DIR コピー
*l_copy_HSP_SYNC_DIR
bcopy "/save/strVar", "/save/strVar_copy"
bcopy "/save/intVar", "/save/intVar_copy"
bcopy "/save/dblVar", "/save/dblVar_copy"
bcopy "/save/arrVar", "/save/arrVar_copy"
devcontrol "syncfs"
return
; note保存
*l_save_note
notesel buf_note
noteadd "note系命令のテスト"
noteadd "行の追加テスト"
notesave "/save/hsp_note"
devcontrol "syncfs"
buf_note = "notesave...ok"
return
; note読み込み
*l_load_note
buf_note = ""
fname = "/save/hsp_note"
exist fname
if strsize > 0 {
notesel buf_note
noteload fname
}
return
; dirlist のテスト
*l_dir
s = ""
msg = ""
dirlist s, "*"
msg = s
return
HSPスクリプトエディタのメニューから ツール>HSP3Dish / Cソース変換 をクリックして、HSP3Dish helperを起動します。
「変換」を押して、HSP3Dish.jsに変換します。
出てきたhtmlをテキストエディタで開いて、ENV.HSP_LIMIT_STEPを探します。 次の行にENV.HSP_SYNC_DIRを追加します。
JavaScript
・・・
ENV.HSP_LIMIT_STEP = "15000";//ブラウザに処理を返すまでの実行ステップ数
ENV.HSP_SYNC_DIR = '/save';
HSP3Dish.js、.data、.hmtlをサーバーにアップロードすれば完成です。
IndexedDB:使い方
アプリの使い方です。
保存 | 入力ボックスの内容をIndexedDBに保存します。 |
読込 | IndexedDBの内容を読み込んで、入力ボックスに表示します。 |
削除 | IndexedDBに保存したデータを削除します。機能しません。 |
コピー | IndexedDBに保存したデータをコピーしてIndexedDBに保存します。 |
note保存 | note系命令でテキストを保存します。 |
note読込 | note系命令でテキストを読み込みます。 |
dir | dirlist命令のテスト。 |
これだけでは何が起きているか把握が難しいので、デベロッパーツールで動きを確認します。
- デベロッパーツールのアプリケーションを開きます。
- 左側に表示されたツリーのIndexedDBを開きます。
- 閲覧中のサイトのドメインで使用しているIndexedDBを見ることができます。
- /saveがHSP3が作成したデータベース名、FILE_DATAがオブジェクトストアです。
- データを保存するとオブジェクトストア内に"/save/strVar"などのキーと値が出てきます。
出てこない場合は、右クリックして「更新」をクリックしてください。
ストレージ
▼indexedDB
└myDatabease データベース名
└myObjectStore オブジェクトストア
キーと値が対となって保存されている。
IndexedDB:解説 保存
JavaScript
ENV.HSP_SYNC_DIR = '/save';
htmlにHSP_SYNC_DIR
を追加することで、IndexedDBの利用が可能になるようです。
引数として渡したデータベース名「/save」でデータベースが作成されます。
最初の一文字目は「/(スラッシュ)」である必要がありますが、「save」の部分はスラッシュを含まなければ何でもいいようです。(IndexedDBの仕様では日本語やスペース、数字や記号が使えます。HSP3側が対応しているかはわかりません。)
オブジェクトストア「FILE_DATA」は変更できません。
bsave "/save/strVar", strVar
devcontrol "syncfs"
HSP3からのデータ保存はbsave
で行います。ファイル名の代わりに「/save/strVar」をキーとして渡すと変数の内容を保存できます。
「/save」の部分がデータベース名です。「データベース名/重複しない名前」という感じで指定します。
「/save」フォルダに「strVar」というファイル名で保存する、というイメージで使えるようにしてあるようです。
データベース名(/saveの部分)はwebアプリごとに独自の名称を使用した方がよさそうです。 IndexedDBは同一ドメイン内で共有しています。 同一ドメイン内に複数のアプリを置いていて、データベース名を同じにしているとキー名が競合してしまう可能性があります。 アプリごとにデータを管理するには、アプリごとにデータベース名を変えておくと安全です。
最後に、bsave
で書き込んだらdevcontrol "syncfs"
を実行します。意味は分かりません。だって掲示板に書いてあったんだもん!
(きっと更新した内容をデータベースに反映するとかそういうやつ。)
IndexedDB:解説 読み込み
HSP3
exist "/save/strVar"
if strsize > 0 {
strVar = ""
bload "/save/strVar", strVar
objprm idObjStr, strVar
}
exist
とstrsize
でデータベースにキーが村債することを確認します。
ファイルの有無を確認するのと同じ要領です。
データの読み込みは、bload
で行います。ファイル名としてキー名を指定します。
読み込んだ後は、Windows環境での利用時と同じように使用することができます。
IndexedDB:解説 削除、コピー
作成したデータベース、オブジェクトストア、キーは削除する方法が用意されていません。
delete
命令ではエラーが出て削除することができません。今後のバージョンで対応されるといいですね。
IndexedDBの削除を行う場合、同じキー名をサイズゼロで上書きするか、JavaScriptで削除するスクリプトを用意する必要があります。 またIndexedDBは、サイトのキャッシュを削除しても消えません。手動で消す場合は、デベロッパーツールを使って削除する必要があります。
データの削除はできませんが、保存したデータはbcopy
命令でコピーが可能です。説明の必要もないくらい簡単です。
IndexedDB:解説 note系命令
note系命令での読み書きも可能です。注意点はbsave
/bload
と同じです。
; 読み込み
notesel buf_note
noteadd "note系命令のテスト"
noteadd "行の追加テスト"
notesave "/save/hsp_note"
devcontrol "syncfs"
HSP3
; 書き込み
buf_note = ""
exist "/save/hsp_note"
if strsize > 0 {
notesel buf_note
noteload "/save/hsp_note"
}
説明いらないぐらい簡単です。
localStorage
HSP3Dish.jsでは、exec
命令でJavaScriptが実行可能です。
この機能を使ってlocalStorageの読み書きに対応させることが可能です。
localStorageは使用できる容量が5MBと小さく、テキストしか保存できません。 またドメイン内で共有され階層構造も持たないため、アプリ間でキーの競合が発生しやすい状況です。 使用する場合は、ドメイン何の全アプリ共通の設定などを保存させるような使い方がいいと思います。 サイズが大きいものも避けた方がいいです。
localStorage:作例と設置方法
こちらが作例です。 作例:./dish/save02.html
HSP3
; スマホ向けHSP3Dishのサンプル
; 保存のテスト
; localstorage
#include "hsp3dish.as"
#module
; JavaScript エスケープ処理
#defcfunc escapeJS str p_intxt
res_txt = p_intxt
strrep res_txt, "\\", "\\\\" ; \ → \\
strrep res_txt, "'" , "\\'" ; ' → \'
strrep res_txt, "\"" , "\\\"" ; " → \"
strrep res_txt, "/" , "\\/" ; / → \/
strrep res_txt, "<" , "\\x3c" ; < → \x3c
strrep res_txt, ">" , "\\x3e" ; > → \x3e
strrep res_txt, "\n" , "\\n" ; \n → \\n
strrep res_txt, "\r" , "\\r" ; \r → \\r
return res_txt
; JavaScript エスケープ処理を戻す
#defcfunc unescapeJS str p_intxt
res_txt = p_intxt
strrep res_txt, "\\'", "'" ; \' → '
strrep res_txt, "\\\"", "\"" ; \" → "
strrep res_txt, "\\/", "/" ; \/ → /
strrep res_txt, "\\x3c", "<" ; \x3c → <
strrep res_txt, "\\x3e", ">" ; \x3e → >
strrep res_txt, "\\n" , "\n" ; \\n → \n
strrep res_txt, "\\r" , "\r" ; \\r → \r
strrep res_txt, "\\\\", "\\" ; \\ → \
return res_txt
#global
; キー
keyName = "HSP3_key"
; 読み込み用バッファ
sdim strbuf, 128
; 保存するテキスト
strObj = "alert('Hello !')"
; 確認用
exec_text = ""
msg = ""
; GUI
objsize 200
input strObj, , , 256 : idObjStr = stat
button gosub "保存", *l_save
button gosub "読込", *l_load
button gosub "削除", *l_clear
button gosub "実行", *l_run
redraw 1
; メインループ
*main
redraw 1 : await 16 : redraw 0 : color 255, 255, 255 : boxf : color : pos 0,0
pos 0, 130
mes exec_text
mes msg
goto *main
stop
; 保存
; 入力された文字列をそのまま使うと危険なので、エスケープ文字に変換してから保存作業をする。
*l_save
; localStorage.setItem('キー', '保存する文字列');
exec_text = "localStorage.setItem('" + keyName + "', '" + escapeJS(strObj) + "');"
exec exec_text
msg = "save:\n" + strObj
return
; 読み込み
; JavaScript側からHSP3側の受け取り用変数に値を書き込む。
; エスケープ文字に変換してあるので元に戻す。
*l_load
ptrStrbuf = varptr(strbuf)
sizeStrbuf = varsize(strbuf)
; {
; const ptr = HSP側の変数のポインタ;
; const str = localStorage.getItem('キー');
; if(!!str){
; stringToUTF8Array(str, Module.HEAP8, ptr, HSP側の変数のバッファサイズ);
; }
; }
exec_text = "{"
exec_text+= "const ptr = " + ptrStrbuf + ";"
exec_text+= "const str = localStorage.getItem('" + keyName + "');"
exec_text+= "if(!!str){
exec_text+= " stringToUTF8Array(str, Module.HEAP8, ptr, " + sizeStrbuf + ");"
exec_text+= "}"
exec_text+= "}"
exec exec_text
strbuf = unescapeJS(strbuf)
objprm idObjStr, strbuf
msg = "load:\n" + strbuf
return
; 削除
*l_clear
strbuf = ""
objprm idObjStr, ""
; localStorage.removeItem('キー');
exec_text = "localStorage.removeItem('" + keyName + "');"
exec exec_text
msg = "delete !"
return
; 実行
; 入力された文字列をそのまま実行
*l_run
exec_text = strObj
exec exec_text
msg = "run !"
return
HSP3Dish helperで作成した、HSP3Dish.js、.data、.hmtlをサーバーにアップロードすれば完成です。
localStorage:使い方
アプリの使い方です。
保存 | 入力ボックスの内容をlocalStorageに保存します。 |
読込 | localStorageの内容を読み込んで、入力ボックスに表示します。 |
削除 | localStorageに保存したデータを削除します。機能しません。 |
実行 | 入力ボックスの内容をそのまま実行します。 |
これだけでは何が起きているか把握が難しいので、デベロッパーツールで動きを確認します。
- デベロッパーツールのアプリケーションを開きます。
- 左側に表示されたツリーのローカル ストレージを開きます。
- 閲覧中のサイトのドメインで使用しているローカル ストレージを見ることができます。
- webアプリを置いているドメイン「https://~」を開いてください。
- 保存されているキーと値の一覧が表示されます。
ストレージ
▼ローカル ストレージ
└htts://example.com
キーと値が対となって保存されている。
localStorage:解説 保存
exec
命令で次のようなJavaScriptを実行するだけで文字列を保存できます。
localStorage.setItem('キー', '保存する文字列');
HSP3側では、保存する文字列に「'(シングルクォーテーション)」などが入っていると予期しない動作を起こす可能性があるので、 保存する文字列の一部をJavaScriptのエスケープ文字に変換しています。
キーには記号や日本語なども使用できます。 しかし予想外のトラブルを避けたいなら、英数字とアンダースコアのみを使用した方がよさそうです。
localStorage:解説 読み込み
exec
命令で次のようなJavaScriptを実行すると、localStorageから文字列を読み込めます。
{
const ptr = HSP側の変数のポインタ;
const str = localStorage.getItem('キー');
if(!!str){
stringToUTF8Array(str, Module.HEAP8, ptr, HSP側の変数のバッファサイズ);
}
}
localStorageで読み込んだ文字列をJavaScript側からHSP3側の変数に書き込む作業を行っています。
HSP3側の変数の場所を示すためにポインタを利用しています。
stringToUTF8Array
はHSP3Dish.js内の関数です。
HSP3では次のような実装を行っています。
ptrStrbuf = varptr(strbuf)
sizeStrbuf = varsize(strbuf)
exec_text = "{"
exec_text+= "const ptr = " + ptrStrbuf + ";"
exec_text+= "const str = localStorage.getItem('" + keyName + "');"
exec_text+= "if(!!str){
exec_text+= " stringToUTF8Array(str, Module.HEAP8, ptr, " + sizeStrbuf + ");"
exec_text+= "}"
exec_text+= "}"
exec exec_text
strbuf = unescapeJS(strbuf)
varptr
でポインタの取得、varsize
で文字列変数サイズの取得を行っています。
sizeStrbuf
には、書き込み先変数(strbuf
)のサイズ以下を指定すればいいので、必ずvarsize
が必要というわけではありません。今回は例として毎回サイズを調べるようにしてみました。
最後にJavaScript側から受け取った文字列は、一部がJavaScriptのエスケープ文字になっているのでunescapeJS
で元に戻しています。
localStorage:解説 削除
exec
命令で次のようなJavaScriptを実行するだけで削除できます。
localStorage.removeItem('キー');
localStorage:注意点
HSP3のスクリプトだけで実装できるので、モジュール化すれば使いやすそう。 ただし理解しないまま使用するとトラブルの原因となってしまいそうです。 例えば以下のようなものが考えられます。
- 安易に使ってしまい、容量制限に到達してしまう。
- 複数のアプリを作った際に、気づかないうちにキーが競合してしまう。
- 同一ドメインを他のひとが作ったアプリと共用している場合、キーの競合や容量制限でトラブルを起こす。
IndexedDB、localStorage
IndexedDBを使ってみるとファイルへの保存と変わらない感覚で利用できるので、とても使いやすいと感じました。 データの削除さえ出来れば、正式な運用が見えてくるかもですね。 その削除機能もJavaScriptで書いてしまえば、実装できてしまいます。
localStorageは容量制限と競合のリスクを考えると、ゲームのセーブには使わない方がよさそうです。 ドメイン内全体に関する情報(サイト内のwebアプリの利用実績、アプリの音量など)の保存などに使うとよさそうです。
HSP3.7β9のHSP3Dishには、初心者向けの誰でも使える保存機能は実装されていません。 JavaScriptを駆使すれば使えるという感じです。 実装をミスると、ユーザーのブラウザにデータを残してしまう可能性があります。 JavaScriptが分からない方は、安易にマネしないようお願いします。
execで出来ること
localStorageの実装方法を使うと、HSP3とJavaScriptで文字列を受け渡しすることができます。 これはつまりJavaScriptでしか使えない機能もHSP3から間接的に使用できるということです。 様々な情報が取得できる可能性があります。(以下は取得できそうな情報の例)
- 加速度、重力加速度、回転速度。
- デバイスの方位、傾き角度。
- 緯度、経度、高度、移動速度、移動方向。
- バッテリーの充電状態、残量。
また、デバイスを振動させたり、音声認識や音声合成も使えるかもしれません。 作る予定はないですが、面白そうです。 HSP3のモジュールで実装できそうなのもいいですね。