ハイハイスクールアドベンチャー_web版
差分
このページの2つのバージョン間の差分を表示します。
| 両方とも前のリビジョン前のリビジョン次のリビジョン | 前のリビジョン | ||
| ハイハイスクールアドベンチャー_web版 [2024/02/14 04:44] – [データベースとか] araki | ハイハイスクールアドベンチャー_web版 [2025/10/20 02:00] (現在) – [PWA] araki | ||
|---|---|---|---|
| 行 38: | 行 38: | ||
| [[https:// | [[https:// | ||
| + | PWA対応なので、インストールして遊んでくれてもいいんだよ! | ||
| + | |||
| + | {{:: | ||
| + | |||
| + | インストールはアドレスバーの右端にあるボタン、またはブラウザのメニューから。 | ||
| + | |||
| + | {{:: | ||
| + | |||
| + | いうまでもないが、画面真ん中あたりの「どうする?」の隣にあるテキストボックスにコマンドを入れて enter である。 | ||
| ===== あれこれ ===== | ===== あれこれ ===== | ||
| 行 57: | 行 66: | ||
| じゃあ、メモリ上に置いといて、書き込みは処理が終わってまとめてやればいいんじゃないかということになるが、画像を RGBA形式で保持していると、結局読み出しが遅いのだ。 | じゃあ、メモリ上に置いといて、書き込みは処理が終わってまとめてやればいいんじゃないかということになるが、画像を RGBA形式で保持していると、結局読み出しが遅いのだ。 | ||
| - | そもそもそんなに色数使わないので、8bitのインデックスを作ってペイントバッファとしてそれを使えばいいんじゃないかということに気づいてそのようにした。 | + | そもそもそんなに色数使わないので、8bitのインデックスを作ってペイントバッファとしてそれを使えばいいんじゃないかということに気づいて((気づきが遅い!))そのようにした。 |
| なお、当初は、普通に一枚の canvasでやりくりしてたし、速度的には十分だったが、下に書くように、画面の表示領域をネイティブのサイズより大きくしたかったので、画面の canvas要素へ拡大転送するための canvasを作成して、ネイティブサイズ((256x152ピクセル))の画像はそこに描画されている。 | なお、当初は、普通に一枚の canvasでやりくりしてたし、速度的には十分だったが、下に書くように、画面の表示領域をネイティブのサイズより大きくしたかったので、画面の canvas要素へ拡大転送するための canvasを作成して、ネイティブサイズ((256x152ピクセル))の画像はそこに描画されている。 | ||
| 行 70: | 行 79: | ||
| ウェブページ上に 512x304((要するに倍のサイズ))で canvasを用意して、プログラム中ではネイティブサイズのダミーの canvas に書き込みを行い、倍サイズの表示領域に拡大転送することでこれを実現している。 | ウェブページ上に 512x304((要するに倍のサイズ))で canvasを用意して、プログラム中ではネイティブサイズのダミーの canvas に書き込みを行い、倍サイズの表示領域に拡大転送することでこれを実現している。 | ||
| - | < | + | < |
| this.zctx = document.getElementById(" | this.zctx = document.getElementById(" | ||
| this.zctx.imageSmoothingEnabled = true; | this.zctx.imageSmoothingEnabled = true; | ||
| 行 91: | 行 100: | ||
| 2011年ごろの JavaScript によるオブジェクト指向プログラミングというと、クラスも何もなくて、普通の変数の宣言を利用してやっていたのが実態であった。 | 2011年ごろの JavaScript によるオブジェクト指向プログラミングというと、クラスも何もなくて、普通の変数の宣言を利用してやっていたのが実態であった。 | ||
| - | < | + | < |
| var Klass = function() | var Klass = function() | ||
| { | { | ||
| 行 113: | 行 122: | ||
| 基本的にデータファイルも、CGIで描画させた画像もこの枠組みを使ってやりとりしていたので、これも大きかった。 | 基本的にデータファイルも、CGIで描画させた画像もこの枠組みを使ってやりとりしていたので、これも大きかった。 | ||
| - | < | + | < |
| var req = new XMLHttpRequest(); | var req = new XMLHttpRequest(); | ||
| req.open(' | req.open(' | ||
| 行 127: | 行 136: | ||
| これはラムダ式を使うのが今風なんだと。 | これはラムダ式を使うのが今風なんだと。 | ||
| - | < | + | < |
| fetch(' | fetch(' | ||
| var buf = new Uint8Array(abuf); | var buf = new Uint8Array(abuf); | ||
| 行 139: | 行 148: | ||
| 全部をひとまとまりでやろうとするとうまく動かない。 | 全部をひとまとまりでやろうとするとうまく動かない。 | ||
| - | < | + | < |
| fetch(' | fetch(' | ||
| if (res.ok) | if (res.ok) | ||
| 行 169: | 行 178: | ||
| Save/ | Save/ | ||
| + | |||
| + | === トラブルシュート === | ||
| + | |||
| + | |||
| + | 原因はよくわからないがDBが壊れることがあるようである。 | ||
| + | そんなときは、Chromeの系譜なら F12を押して DevTool を呼び出し、" | ||
| + | |||
| + | 勿論データは失われてしまうが、以後、load/ | ||
| + | |||
| + | {{:: | ||
| ==== PWA ==== | ==== PWA ==== | ||
| 行 178: | 行 197: | ||
| 基本的には、manifestファイルと呼ばれる JSON形式のファイルと、service workerとよばれる JavaScriptの二本立てでできている。 | 基本的には、manifestファイルと呼ばれる JSON形式のファイルと、service workerとよばれる JavaScriptの二本立てでできている。 | ||
| あと、あまり触れられていないけれど、インストールされたアプリの顔となるアイコンに適切なサイズのものがないとインストールボタンが表示されないので、これも重要な要素である。((知らねーよ。そんなことどこの解説ページにも書いてなかったんだから!)) | あと、あまり触れられていないけれど、インストールされたアプリの顔となるアイコンに適切なサイズのものがないとインストールボタンが表示されないので、これも重要な要素である。((知らねーよ。そんなことどこの解説ページにも書いてなかったんだから!)) | ||
| + | |||
| + | === Manifest === | ||
| + | |||
| + | PWAに不可欠なもので、アプリケーションとしてのウェブページの詳細を記述したものになる。 | ||
| + | 形式はJSON形式で、manifest.json とか xxx.webmanifest とかといった名前であることが多いようである。 | ||
| + | |||
| + | マニフェストファイルは、HTMLファイルの linkタグを使って次のようにマッピングする。 | ||
| + | |||
| + | <code html> | ||
| + | <link rel=" | ||
| + | </ | ||
| + | |||
| + | そして、マニフェストファイルそのものの中には、とりあえず以下のような内容を記述する。 | ||
| + | もっと詳細な仕様を記述できるが、詳しくは[[https:// | ||
| + | |||
| + | <code css> | ||
| + | { | ||
| + | " | ||
| + | " | ||
| + | " | ||
| + | " | ||
| + | { | ||
| + | " | ||
| + | " | ||
| + | " | ||
| + | }, | ||
| + | { | ||
| + | " | ||
| + | " | ||
| + | " | ||
| + | } | ||
| + | ], | ||
| + | " | ||
| + | " | ||
| + | " | ||
| + | } | ||
| + | </ | ||
| + | |||
| + | === Service Worker (JavaScript) === | ||
| + | |||
| + | PWAを構成するもうひとつの要素が Service Workerになる。 | ||
| + | JavaScriptで、Service Workerを登録してやる必要がある。 | ||
| + | |||
| + | 色々、凝った挙動などをさせることもできるようだが、とりあえず、アプリになればいい、というような向きには、最低限の内容でよさそうである。 | ||
| + | |||
| + | なお、キャッシュアイテムについては、相対パスでも絶対パスでもフルのURIでも構わないが、取得できない((404とか403のエラーを返す))ようなものがあると、インストールに失敗するので必ずすべてのアイテムがアクセス可能なことを確認すること。 | ||
| + | |||
| + | Service WorkerもHTMLファイルから登録してやる必要がある。 | ||
| + | |||
| + | <code javascript> | ||
| + | if (' | ||
| + | { | ||
| + | navigator.serviceWorker.addEventListener(' | ||
| + | if (event.data && event.data.type == ' | ||
| + | const shouldReload = confirm(' | ||
| + | if (shouldReload) | ||
| + | { | ||
| + | window.location.reload(); | ||
| + | } | ||
| + | } | ||
| + | }); | ||
| + | navigator.serviceWorker.register(' | ||
| + | } | ||
| + | </ | ||
| + | |||
| + | Service Worker本体((ここでは js/ | ||
| + | |||
| + | <code javascript> | ||
| + | // cache | ||
| + | var CACHE_NAME = ' | ||
| + | var urlsToCache = [ | ||
| + | ' | ||
| + | ' | ||
| + | ' | ||
| + | ' | ||
| + | ' | ||
| + | ' | ||
| + | ' | ||
| + | ' | ||
| + | ' | ||
| + | ' | ||
| + | ' | ||
| + | ' | ||
| + | ' | ||
| + | ' | ||
| + | ' | ||
| + | ' | ||
| + | ' | ||
| + | ' | ||
| + | ' | ||
| + | ' | ||
| + | ' | ||
| + | ]; | ||
| + | |||
| + | // install | ||
| + | self.addEventListener(' | ||
| + | event.waitUntil( | ||
| + | caches.open(CACHE_NAME) | ||
| + | .then((cache) => cache.addAll(urlsToCache)) | ||
| + | ) | ||
| + | ); | ||
| + | |||
| + | // refresh | ||
| + | self.addEventListener(' | ||
| + | event.respondWith( | ||
| + | caches.match(event.request) | ||
| + | .then((res) => { | ||
| + | return res ? res : fetch(event.request); | ||
| + | }) | ||
| + | ); | ||
| + | }); | ||
| + | |||
| + | // update | ||
| + | self.addEventListener(' | ||
| + | const cacheWhitelist = []; // 現在有効なキャッシュ名 | ||
| + | |||
| + | event.waitUntil( | ||
| + | caches.keys().then((cacheNames) => { | ||
| + | return Promise.all( | ||
| + | cacheNames.map((cacheName) => { | ||
| + | // 現在のバージョンに含まれていない(古い)キャッシュを削除 | ||
| + | if (cacheWhitelist.indexOf(cacheName) === -1) { | ||
| + | console.log(' | ||
| + | return caches.delete(cacheName); | ||
| + | } | ||
| + | }) | ||
| + | ); | ||
| + | }) | ||
| + | // すべてのキャッシュ削除が完了したら、クライアントへの制御を引き継ぐ | ||
| + | .then(() => self.clients.claim()) | ||
| + | ); | ||
| + | | ||
| + | event.waitUntil( | ||
| + | // すべてのクライアントに更新完了を通知 | ||
| + | self.clients.claim().then(() => { | ||
| + | self.clients.matchAll().then((clients) => { | ||
| + | clients.forEach(client => { | ||
| + | client.postMessage({ type: ' | ||
| + | }); | ||
| + | }); | ||
| + | }) | ||
| + | ); | ||
| + | }); | ||
| + | </ | ||
| + | |||
| + | 要はキャッシュを登録しているだけである。 | ||
| + | これでオフラインでも使えるようになるらしい((そのように作られていれば))が、やってみていないのでわからない。 | ||
| + | |||
| + | なお、キャッシュされるオブジェクトの指定は、sw.jsのある場所からの相対パスまたは絶対パス、URIなどで、これを間違うとエラーをはいてキャッシュに失敗する。 | ||
| + | |||
| + | ブラウザによってはsw.jsがエラーを出しているとインストールボタンを表示してくれない厳格なやつもいたりするので、よく確認してほしい。 | ||
| + | |||
| + | === トラブルシュート === | ||
| + | |||
| + | Manifest周りに問題がありそうなときは、Chromeの系譜であれば F12で DevToolを呼び出し、Applicationのタブから Manifestを選んで、エラーや Warningがないか、そもそも読み込まれているのかを確認するといい。 | ||
| + | 上記のアイコンの問題もここに「デスクトップだったら144x144以上のサイズのアイコンがないとだめ」って出てたので問題を認識・解決できた。 | ||
| + | |||
| + | {{:: | ||
| + | |||
| + | ==== 音とか ==== | ||
| + | |||
| + | PalmOS、Android、そして Qtでは音がある場面では音が鳴るようになっている。 | ||
| + | Web版を再公開したときに、「誰か解けたのか?」というようなことをつぶやくと「校歌は歌った」というような返事があった。 | ||
| + | そういえばWeb版では音のことをすっかり失念していた。 | ||
| + | |||
| + | HTML5ではaudioコンポーネントがあり、JavaScriptに Audioクラスが実装されている。 | ||
| + | そしてほとんどすべてのモダンなブラウザでMP3形式の音源を鳴らすことができる。 | ||
| + | |||
| + | === 音を鳴らせ === | ||
| + | |||
| + | HTMLで audio 要素を配置してもいいのだが、別に画面上に何かがいる必要はないので、適宜動的に確保する方向で実装する。 | ||
| + | 動的に確保された Audio のインスタンスは再生が終わったらガベージコレクションの対象になるって書いてあるので、音を鳴らすときに確保して、鳴らしたら勝手に消えることを期待する。 | ||
| + | |||
| + | <code javascript> | ||
| + | var m = new Audio(); | ||
| + | m.src = this.base_uri + "/ | ||
| + | m.load(); | ||
| + | m.volume = 1.0; | ||
| + | m.loop = false; | ||
| + | m.play(); | ||
| + | </ | ||
| + | |||
| + | ボリュームは1.0が100%になる。 | ||
| + | |||
| + | === 音を鳴らすな === | ||
| + | |||
| + | 多分、音鳴ったら困る環境の人も少なくないだろう。 | ||
| + | いきなり、ハイハイスクールの校歌が流れたらびっくりするだろうし、大体が、この校歌結構長尺で鳴り出すと止まらないのだ。 | ||
| + | |||
| + | なので、ミュート機能はつけておいた方がいいと判断した。 | ||
| + | まあ、PCなり、スマホなりでミュートできるだろうけれど。 | ||
| + | どっちかというと、ミュートというよりデフォルトでミュートしておいて、鳴らしたい人だけ解除するイメージだ。 | ||
| + | |||
| + | まあ、単純にチェックボックスを一個置いておけばいいのだが、今時のインターフェイスとしてはちょっとカッコ悪い。 | ||
| + | そもそもこの昭和レトロなアドベンチャーゲームに何をいっているんだという向きもあるだろうが、せっかくなので、今風のスライドスイッチにしておきたい。 | ||
| + | |||
| + | Safariなら、そういう要素がサポートされているらしいが、多くのブラウザでは未サポート((2024年2月現在))なので、別の方法でやることにする。 | ||
| + | |||
| + | 世の中には優れた人が多くいて、既にCSSを駆使して、そういうものをチェックボックスに付加してくれているので、まるっとそれをいただくことにする。 | ||
| + | |||
| + | <code html> | ||
| + | <label for=" | ||
| + | <div class=" | ||
| + | <input type=" | ||
| + | <div class=" | ||
| + | <div class=" | ||
| + | </ | ||
| + | <span class=" | ||
| + | </ | ||
| + | </ | ||
| + | |||
| + | チェックボックスがスイッチの本体だが、チェックボックスを消して、代わりに circleと baseでスイッチを構成する。 | ||
| + | この部分はすべてCSSに記述されている。 | ||
| + | |||
| + | <code css> | ||
| + | .switch { | ||
| + | position: relative; | ||
| + | } | ||
| + | |||
| + | .switch_label { | ||
| + | display: flex; | ||
| + | align-items: | ||
| + | } | ||
| + | |||
| + | .switch input[type=' | ||
| + | position: absolute; | ||
| + | width: 0; | ||
| + | height: 0; | ||
| + | } | ||
| + | |||
| + | .switch .base { | ||
| + | width: 28px; | ||
| + | border-radius: | ||
| + | height: 16px; | ||
| + | background-color: | ||
| + | } | ||
| + | |||
| + | .switch .circle { | ||
| + | position: absolute; | ||
| + | top: 2px; | ||
| + | left: 2px; | ||
| + | width: 12px; | ||
| + | height: 12px; | ||
| + | border-radius: | ||
| + | background-color: | ||
| + | transition: | ||
| + | } | ||
| + | |||
| + | .title { | ||
| + | margin-left: | ||
| + | } | ||
| + | |||
| + | .switch input: | ||
| + | background-color: | ||
| + | transition: | ||
| + | } | ||
| + | |||
| + | .switch input: | ||
| + | transform: translateX(100%); | ||
| + | background-color: | ||
| + | } | ||
| + | </ | ||
| + | |||
| + | なお、実際に使用しているスタイルシートは、レスポンシブデザインっぽく、px使わないでサイズを指定しています。 | ||
| + | |||
| + | ==== 設定 ==== | ||
| + | |||
| + | 基本的には、特に設定項目はなかったのだが、音を鳴らすことにしたら、Qt版同様に、音を鳴らすのかどうかを覚えておいた方がいいような気がしてきた。 | ||
| + | |||
| + | まあ、毎回 Mute ONにしてしまうでもいいような気もしたが、将来何かの設定を追加したときに、それも覚えておいた方がいいような気がしたので、保存できるようにした。 | ||
| + | |||
| + | ゲームデータ保存用objectStoreはIndexedDBに作ってあったので、バージョンを2にして、設定用のobjectStoreも作るようにした。 | ||
| + | DBのアップグレードとかしないと思ってたが、いきなりすることになるとは先見の明がなさ過ぎた。 | ||
| + | |||
| + | <code javascript> | ||
| + | const req = indexedDB.open(Database.DBNAME, | ||
| + | req.onupgradeneeded = (event) => | ||
| + | { | ||
| + | const db = event.target.result; | ||
| + | const oldver = event.oldVersion; | ||
| + | const newver = event.newVersion; | ||
| + | const migrations = { | ||
| + | " | ||
| + | db.createObjectStore(Database.STORENAME, | ||
| + | }, | ||
| + | " | ||
| + | db.createObjectStore(Database.PREFSTORE, | ||
| + | } | ||
| + | } | ||
| + | for (var v = oldver + 1 ; v <= newver ; v++) | ||
| + | { | ||
| + | if (migrations[v]) migrations[v](); | ||
| + | } | ||
| + | }; | ||
| + | </ | ||
| + | |||
| + | バージョン更新のやり方は、どこかで見かけたのを参考にした。 | ||
| + | |||
| + | ==== テーマ ==== | ||
| + | |||
| + | 今どきのアプリとしては、システムのテーマに追随しないわけにはいかない。 | ||
| + | |||
| + | テーマへの追随に関しては、システムがダークモードだったら、body要素に class=' | ||
| + | これを動的に行うコードを追加し、初期化時にチェック、またシステム設定が変更された場合には検出し追随するようにイベントリスナーを登録して対応する。 | ||
| + | |||
| + | <code javascript> | ||
| + | applySystemTheme(e) | ||
| + | { | ||
| + | if (e.matches) | ||
| + | { | ||
| + | document.body.classList.add(' | ||
| + | } | ||
| + | else | ||
| + | { | ||
| + | document.body.classList.remove(' | ||
| + | } | ||
| + | } | ||
| + | | ||
| + | this._darkModeQuery = window.matchMedia(' | ||
| + | this.applySystemTheme(this._darkModeQuery); | ||
| + | this._darkModeQuery.addListener(this.applySystemTheme); | ||
| + | </ | ||
ハイハイスクールアドベンチャー_web版.1707885891.txt.gz · 最終更新: by araki
