[ブラウザゲームの作り方] Number-Place(数独)#5「データ保存」

2023年5月24日

ゲーム プログラミング

eyecatch 操作系が安定してきたところで、入力した数値を保存して、ナンプレのゲームを途中で中断したり、ブラウザをリロードしても、ゲームの続きをプレイできるようにしたいと思います。 次のソースコードを上書き、又は新規作成して、環境を更新してください。

ソースコード

[更新] main.js

import { View } from './js/view.js' import { Input } from './js/input.js' import { Question } from './js/question.js' import { Data } from './js/data.js' import { Common } from './js/common.js' export const Main = { stage_id : 'NumberPlace', data_path : 'data/questions.json', save_name : 'mynt_number_place_save', clear_name : 'mynt_number_place_clear', interval_px : 10, question_num : 0, } function init(){ Main.data = new Data() Main.view = new View() Main.input = new Input() Main.question = new Question({ callback : (e => { Common.continue() }).bind(this) }) } switch(document.readyState){ case 'complete': case 'interactive': init() break default: window.addEventListener('DOMContetLoaded' , init) break }

[新規] js/data.js

import { Main } from '../main.js' import { Common } from './common.js' export class Data{ save(){ if(!Common.is_started){return} const data = { question_num : Main.question_num, input : Common.get_matrix_numbers(''), question : Common.get_matrix_numbers('lock'), } const json = JSON.stringify(data) window.localStorage.setItem(Main.save_name , json) } load(){ const json = window.localStorage.getItem(Main.save_name) if(!json){return null} return JSON.parse(json) } }

[新規] js/common.js

import { Main } from '../main.js' import { Element } from './element.js' export class Common{ static get is_started(){ if(Element.elm_button.getAttribute('data-status') === 'check'){ return true } else{ return false } } static start(){ Element.elm_button.setAttribute('data-status' , 'check') Main.question.new(Main.question_num) } static continue(){ const datas = Main.data.load() if(!datas){return} Main.question_num = datas.question_num this.start() this.put_number(datas.input) } static put_number(datas){ const tr_lists = Element.tr_lists if(!tr_lists || !tr_lists.length){return} for(let i=0; i<tr_lists.length; i++){ const td_lists = tr_lists[i].getElementsByTagName('td') for(let j=0; j<td_lists.length; j++){ const num = datas[i][j] if(!num){continue} td_lists[j].textContent = num } } } // type : lock or '' static get_matrix_numbers(type=''){ const tr_lists = Element.tr_lists if(!tr_lists || !tr_lists.length){return} const numbers = [] for(let i=0; i<tr_lists.length; i++){ numbers[i] = [] const td_lists = tr_lists[i].getElementsByTagName('td') for(let j=0; j<td_lists.length; j++){ numbers[i][j] = this.get_cell_status(td_lists[j] , type) } } return numbers } static get_cell_status(cell , type){ if(cell.getAttribute('data-status') === type){ return Number(cell.textContent || 0) } else{ return 0 } } }

[更新] js/input.js

import { Main } from '../main.js' import { Element } from './element.js' import { Common } from './common.js' export class Input{ constructor(){ this.set_event() } get next_num(){ if(!this.data){return null} const next_num = this.data.num + 1 return next_num > 9 ? 0 : next_num } set_event(){ const btn = Element.elm_button if(btn){ btn.addEventListener('click' , this.click_btn.bind(this)) } if(typeof window.ontouchstart !== 'undefined'){ Element.table.addEventListener('touchstart' , this.touchstart.bind(this)) Element.table.addEventListener('touchmove' , this.touchmove.bind(this)) Element.table.addEventListener('touchend' , this.mouseup.bind(this)) } else{ Element.table.addEventListener('mousedown' , this.mousedown.bind(this)) Element.table.addEventListener('mousemove' , this.mousemove.bind(this)) Element.table.addEventListener('mouseup' , this.mouseup.bind(this)) } } touchstart(e){ this.mousedown(e.touches[0]) } touchmove(e){ e.preventDefault() this.mousemove(e.touches[0]) } mousedown(e){ const cell = e.target.closest('td') if(!cell){return} if(cell.getAttribute('data-status') === 'lock'){return} this.data = { cell : cell, num : Number(cell.textContent || 0), pos : { x : e.pageX, y : e.pageY, }, } } mousemove(e){ if(!this.data){return} const size = Math.abs(e.pageX - this.data.pos.x) if(size < Main.interval_px){return} const num = this.pos2num(size) this.data.cell.textContent = num || '' this.data.num = num this.data.move_flg = true } mouseup(e){ if(!this.data){return} if(!this.data.move_flg){ this.data.num = this.next_num } this.data.cell.textContent = this.data.num || '' delete this.data Main.data.save() } // 移動距離を0~9の数値に変換する pos2num(pos){ const num = ~~(pos / Main.interval_px) return num > 9 ? num % 10 : num } click_btn(){ const status = Element.elm_button.getAttribute('data-status') switch(status){ case 'check': console.log('check') break case 'start': default: Common.start() Main.data.save() break } } }

[更新] view.js

import { Main } from '../main.js' import { Element } from './element.js' export class View{ constructor(){ if(!Element.table){return} this.set_stage() } set_stage(){ const table = document.createElement('table') Element.table.appendChild(table) this.set_row(table) } set_row(parent){ for(let i=0; i<9; i++){ const row = document.createElement('tr') parent.appendChild(row) this.set_cell(row) } } set_cell(parent){ for(let i=0; i<9; i++){ const cell = document.createElement('td') parent.appendChild(cell) } } }

[更新] question.js

import { Main } from '../main.js' import { Element } from './element.js' export class Question{ constructor(options){ this.options = options || {} this.load() } load(){ const xhr = new XMLHttpRequest() xhr.open('get' , Main.data_path , true) xhr.setRequestHeader('Content-Type', 'application/json'); xhr.onreadystatechange = (e => { if(xhr.readyState !== XMLHttpRequest.DONE){return} const status = xhr.status; if (status === 0 || (status >= 200 && status < 400) || e.target.response) { this.datas = JSON.parse(e.target.response) } this.finish() }).bind(this) xhr.send() } new(num){ Main.question_num = num || Main.question_num const data = this.datas[Main.question_num] if(!data || !data.data){return} this.put_numbers(data.data) } put_numbers(datas){ const tr_lists = Element.tr_lists if(!tr_lists || !tr_lists.length){return} for(let i=0; i<datas.length; i++){ const td_lists = tr_lists[i].getElementsByTagName('td') for(let j=0; j<datas[i].length; j++){ if(datas[i][j]){ td_lists[j].textContent = datas[i][j] td_lists[j].setAttribute('data-status' , 'lock') } else{ td_lists[j].textContent = '' td_lists[j].setAttribute('data-status' , '') } } } } finish(){ if(this.options.callback){ this.options.callback() } } }

解説とポイント

上記ソースコードで更新したページで文字を入力して、ブラウザのページをリロードしてみましょう。 入力した数値や、問題数値がすぐに現れていれば成功です。 もし、うまく表示出来ない人は、ブラウザキャッシュでモジュールが更新されていない可能性があるので、ブラウザのスーパーリロードをしてみてください。 GoogleChromeであれば、シフトを押しながらリロードボタンをクリックするか、デバッグコンソールを表示した状態で、controlを押しながら、リロードボタンを押して「キャッシュの消去とハード再読み込み」を選択してください。

今回のメインモジュール

今回の注目するモジュールは、data.jsです。 中を見ると、非常に簡単なコードで、saveとloadの処理を書いているだけです。 ただ、saveする時にどんなデータを保存するかというデータ作成は、common.jsに、get_matrix_numbers() という関数を作って対応しています。 localStorageの読み込みと書き込み処理は非常に簡単なので覚えておくとcookieよりも手軽に使えて便利ですよ。

get_matrix_numbers関数について

この関数では、tableタグのセルをfor文で1つずつ見ていって、データ登録した時のような、9x9のマトリクス2重配列データを作っています。 typeという値で、問題の数値と入力した数値の2種類を分類して、それぞれ保存するようにしています。 ちなみに、問題は、データに保存されているので、本来は問題のIDを保存しておけばいいだけなんですが、データの仕様が今後変わる可能性もあるので、必要情報は保存しておくことにしました。

保存タイミング

データを保存するイベントは、数値を入力したタイミングです。 Main.jsで最初にmain.dataという変数にdataクラスのインスタンスを保存して、いろいろな処理を引き継いでいく仕様にしています。 実際の入力処理である、input.jsの中の、72行目あたりに、mouseupのタイミングで、Main.data.save() という関数を実行しています。 これで、入力をするたびに、localStorageにデータを書くようにしています。 すでに書かれてあるデータがあれば、上書きをするという簡易な処理です。

データの読み込み

次に、ページを読み込んだ時(リロードした時)に、localStorageに保存してあるデータがあれば、それを表示するという処理を、Main.jsの、questionデータを読み込んだcallback関数内に書いています。 バケツリレー方式になっていますが、Commonクラスのcontinueという関数にデータをloadして、ゲームをスタートさせるようにしています。 数値をマス目に書き込む処理は、問題数値も、入力数値も、どれも同じ関数で対応できるように、Common.put_number(データ) という関数を作って対応しています。 当たり前ですが、プログラミングは、このように似たような処理は、同じ関数にして使うのが鉄則で、こうしてまとめていくことで、書き込むソースコードが少なくなります。 この辺の感覚は、教科書を見ても覚えられないので、たくさんコードを書いたり、人のコードを見て、自分なりのまとめ方を身に着けていくというのが良いと思います。

人気の投稿

このブログを検索

ごあいさつ

このWebサイトは、独自思考で我が道を行くユゲタの少し尖った思考のTechブログです。 毎日興味がどんどん切り替わるので、テーマはマルチになっています。 もしかしたらアイデアに困っている人の助けになるかもしれません。

ブログ アーカイブ