オーラス麻雀ゲーム開発日記構想から現在までの開発記録を、公開順にまとめています。

前回までの進捗
前回は、ロン・ツモ・流局の仮結果を入力し、終局後の点数や順位を確認できるようにした。
これで、オーラスの点数状況を入力して、必要なアガリ点や仮の結果を試せるようになった。
ただし、ページを再読み込みすると、入力した内容がすべて初期状態に戻ってしまう。
点数や席、目標順位などを毎回入力し直すのは面倒なので、今回は入力内容をブラウザに自動保存することにした。
localStorageを使って保存する
今回は、ブラウザに用意されているlocalStorageを使う。localStorageへ保存したデータは、ページを閉じたり再読み込みしたりしても残る。
サーバーやデータベースを用意する必要がなく、同じブラウザで使う簡単な保存機能なら比較的手軽に実装できる。
今回のアプリでは、次のような情報を保存する。
- 4人の持ち点
- 自分の席
- 目標順位
- 難易度
- 本場
- 供託
- 終局方法
- 和了者
- 放銃者
- ロン点
- ツモ時の支払い
- 流局時のテンパイ者
保存するデータの形は、SavedGameStateという型にまとめた。
type SavedGameState = {
scores: Scores;
selfSeat: Seat;
goal: Goal;
difficulty: Difficulty;
honba: number;
kyotaku: number;
resultType: ResultType;
winner: Seat;
loser: Seat;
ronPoints: number;
dealerPayment: number;
childPayment: number;
tenpaiSeats: Seat[];
};
保存する項目を型として定義しておくことで、保存時に必要な値が抜けていないか確認しやすくなる。
オブジェクトを文字列に変換する
localStorageへは、オブジェクトをそのまま保存できない。
そこで、JSON.stringifyを使って文字列へ変換する。
export function saveGameState(
state: SavedGameState,
): void {
localStorage.setItem(
storageKey,
JSON.stringify(state),
);
}
setItemの第1引数には保存先を識別するキーを、第2引数には保存する文字列を渡す。
保存されたデータは、ブラウザ上では次のような文字列になる。
{
"selfSeat": "east",
"goal": "top",
"honba": 1,
"kyotaku": 2
}
実際には、点数や仮の終局結果なども含めたデータが保存される。
保存データを読み込む
ページを開いたときは、localStorage.getItemを使って保存済みのデータを取得する。
const savedText =
localStorage.getItem(storageKey);
まだ一度も保存されていない場合は、nullが返ってくる。
保存データが見つかった場合は、JSON.parseを使って文字列からオブジェクトへ戻す。
const parsed = JSON.parse(
savedText,
) as SavedGameState;
stringify:保存。データを文字列にするparse:読み込み。文字列をデータへ戻す
という関係らしい。
保存に失敗してもアプリを止めない
保存データが壊れていたり、何らかの理由でブラウザへ書き込めなかったりする可能性もある。
そのため、読み込みと保存の処理をtry...catchで囲んだ。
try {
const savedText =
localStorage.getItem(storageKey);
if (savedText === null) {
return null;
}
return JSON.parse(
savedText,
) as SavedGameState;
} catch (error) {
console.error(
"保存データの読み込みに失敗しました。",
error,
);
return null;
}
読み込みに失敗した場合は、保存データがないものとしてnullを返す。
保存機能の問題でアプリ全体が表示されなくなるより、初期状態で使える方がよさそう。
初回表示時に保存データを使う
保存データの読み込みは、useGameStateの最初で行う。
const savedState = useMemo(
() => loadSavedGameState(),
[],
);
その後、各状態の初期値を決めるときに、保存データがあればそちらを使う。
const [goal, setGoal] =
useState<Goal>(
savedState?.goal ??
initialGameState.goal,
);
この場合、
- 保存された目標順位があれば使う
- なければ初期値を使う
という流れになる。??は、左側がnullまたはundefinedの場合に、右側の値を使う演算子。
保存データの有無によって分岐を書くより、短くまとめられる。
同じ形で、点数、自分の席、本場、供託、終局方法なども復元する。
入力が変わるたびに自動保存する
保存ボタンを用意する方法もあるけれど、今回は入力内容が変わったら自動的に保存する形にした。
ReactのuseEffectを使い、保存対象の値が変更されたときに保存処理を実行する。
useEffect(() => {
const stateToSave: SavedGameState = {
scores,
selfSeat,
goal,
difficulty,
honba,
kyotaku,
resultType,
winner,
loser,
ronPoints,
dealerPayment,
childPayment,
tenpaiSeats,
};
saveGameState(stateToSave);
}, [
scores,
selfSeat,
goal,
difficulty,
honba,
kyotaku,
resultType,
winner,
loser,
ronPoints,
dealerPayment,
childPayment,
tenpaiSeats,
]);
配列の中に書いた値のどれかが変わると、保存処理がもう一度実行される。
点数を変更したときも、目標順位を変更したときも、特別な保存操作は必要ない。
利用者から見ると、入力した時点で自動的に保存される。
保存ボタンの押し忘れがなくなる反面、保存対象を追加したときは、この配列への追加も忘れないようにする必要がありそう。
初期状態へ戻す機能も追加する
自動保存されるようになると、間違った入力もそのまま残る。
そこで、保存内容を消して初期状態へ戻す処理も用意した。
function resetSavedState(): void {
clearSavedGameState();
setScores({
...initialGameState.scores,
});
setSelfSeat(
initialGameState.selfSeat,
);
setGoal(
initialGameState.goal,
);
// ほかの状態も初期値へ戻す
}
clearSavedGameStateでは、localStorage.removeItemを使って保存データを削除する。
export function clearSavedGameState(): void {
localStorage.removeItem(storageKey);
}
その後、Reactで管理している各状態も初期値へ戻す。
状態を初期化すると自動保存の処理が再び動くので、最終的には初期状態の内容が保存される。
これで、ページを再読み込みしても初期状態から始められる。
保存処理を別ファイルに分ける
保存、読み込み、削除の処理は、storage.tsというファイルへまとめた。
storage.ts
├ 保存データを読み込む
├ 現在の状態を保存する
└ 保存データを削除する
Reactの状態管理を行うファイルにlocalStorageの処理をすべて書くこともできる。
ただ、保存方法を後から変更する可能性もあるので、ブラウザへの保存処理は分けておいた。
画面や状態管理側では、
saveGameState(state);
のように呼び出すだけで済む。
保存先を変更するときも、主にstorage.tsを修正すればよくなるはず。
今回できたこと
今回の進捗は以下のとおり。
- 保存する入力内容を型としてまとめた
- 入力内容をJSON文字列へ変換した
localStorageへデータを保存した- ページ表示時に保存データを読み込んだ
- 保存データがない場合は初期値を使うようにした
- 入力内容が変わるたびに自動保存した
- 保存データの読み書きに失敗した場合の処理を追加した
- 初期状態へ戻す機能を追加した
- 保存処理を別ファイルへ分離した
これで、ページを再読み込みしても、入力した点数や条件が残るようになった。
見た目の変化は小さいけれど、毎回入力し直す必要がなくなり、かなり使いやすくなったと思う。
ここまでで、点数入力から必要点数の表示、仮の終局結果の判定、入力内容の保存まで一通り動くようになった。
次は、条件を毎回手入力しなくても試せるように、難易度に応じたオーラス条件をランダム生成する機能についてまとめたい。
難易度自体はすでにある(SaveGameStateの項目にある)けど、まだ仮置き状態。
牛一。
コメント