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

前回までの進捗
前回は、入力ミスをそのまま計算しないように、バリデーション処理を追加した。
ここまで機能を増やしてきたことで、最初は小さかった画面もかなり要素が増えてきた。
現在の画面には、
- 4人の持ち点入力
- 自分の席や目標順位の設定
- 難易度とランダム生成
- 必要なロン・ツモ点の表示
- 仮の終局結果入力
- 終局後の順位表示
などがある。
これらをすべてApp.tsxへ書いていくと、かなり長くなってしまう。
そこで今回は、画面を役割ごとのReactコンポーネントに分割した。
5つのコンポーネントに分ける
現在は、画面を次の5つに分けている。
ScoreInputSection
ConditionSection
RequiredPointsSection
ResultInputSection
ResultSummarySection
役割はそれぞれ、
ScoreInputSection
→ 現在の持ち点を入力する
ConditionSection
→ 自分の席・目標順位・本場・供託などを設定する
RequiredPointsSection
→ 目標達成に必要な点数を表示する
ResultInputSection
→ ロン・ツモ・流局の仮結果を入力する
ResultSummarySection
→ 終局後の点数・順位・目標達成結果を表示する
という形。
画面上でもカードごとに分かれていたので、その単位でコンポーネントにした。
App.tsxからコンポーネントを呼び出す
App.tsxでは、それぞれをimportする。
import ConditionSection
from "./components/ConditionSection";
import RequiredPointsSection
from "./components/RequiredPointsSection";
import ResultInputSection
from "./components/ResultInputSection";
import ResultSummarySection
from "./components/ResultSummarySection";
import ScoreInputSection
from "./components/ScoreInputSection";
そして画面を組み立てる部分では、
<ScoreInputSection ... />
<ConditionSection ... />
<RequiredPointsSection ... />
<ResultInputSection ... />
<ResultSummarySection ... />
という形で並べている。App.tsxに直接大量のHTMLを書くのではなく、
点数入力
↓
条件設定
↓
必要点数
↓
終局結果入力
↓
結果表示
という画面全体の構造が見えやすくなった。
値はpropsで渡す
コンポーネントを分けると、それぞれの画面で必要な値を渡す必要がある。
たとえば持ち点入力では、
<ScoreInputSection
scores={scores}
kyotaku={kyotaku}
onChangeScore={updateScore}
/>
としている。ScoreInputSection側では、受け取る値の型を定義する。
type ScoreInputSectionProps = {
scores: Scores;
kyotaku: number;
onChangeScore:
(seat: Seat, value: string) => void;
};
ここでは、
scores
→ 現在の4人分の点数
kyotaku
→ 現在の供託本数
onChangeScore
→ 点数が変更されたときに呼ぶ処理
を親コンポーネントから受け取っている。
state自体はApp側で使う
画面をコンポーネントに分けたからといって、それぞれで同じstateを作り直しているわけではない。
ゲーム全体で使う状態は、前回までに作ったuseGameStateから取得する。
const {
scores,
selfSeat,
goal,
difficulty,
honba,
kyotaku,
resultType,
winner,
loser,
// ...
} = useGameState();
それを、必要なコンポーネントへ渡していく。
たとえばConditionSectionには、
<ConditionSection
difficulty={difficulty}
selfSeat={selfSeat}
goal={goal}
honba={honba}
kyotaku={kyotaku}
onChangeDifficulty={setDifficulty}
onChangeSelfSeat={setSelfSeat}
onChangeGoal={setGoal}
onChangeHonba={setHonba}
onChangeKyotaku={setKyotaku}
onRandomize={randomizeCondition}
onReset={resetSavedState}
/>
という形で渡している。
条件設定に関係する値と処理だけをまとめて渡す。
表示だけするコンポーネントもある
すべてのコンポーネントが入力を持っているわけではない。RequiredPointsSectionは、計算結果を表示する役割が中心。
<RequiredPointsSection
currentSelfRank={currentSelfRank}
difficulty={difficulty}
hasInvalidInput={hasInvalidInput}
alreadyAchieved={alreadyAchieved}
minimumRequiredWinPoints={
minimumRequiredWinPoints
}
requiredRonList={requiredRonList}
requiredTsumo={requiredTsumo}
/>
必要なロン点やツモ点の計算そのものは、コンポーネントの中では行わない。
計算済みの結果を受け取って、
現在順位
最低和了点
各家からの必要ロン点
必要ツモ点
などを表示する。
画面側に計算処理をできるだけ持たせないようにした。
入力と計算と表示を分ける
ここまでの整理で、少しずつ役割が分かれてきた。
useGameState
→ 入力状態を管理する
useGameDerivedState
→ 入力値から結果を計算する
各Sectionコンポーネント
→ 入力欄や結果を表示する
App.tsx
→ それらをつなぐ
という構成。
最初からこの形を考えていたわけではなく、機能を追加してコードが増えたので後から分けていった。
小さいうちはApp.tsxだけでも特に困らなかったけれど、入力項目や計算結果が増えてくると、役割ごとに分かれている方が探しやすくなった。
コンポーネントを分ける単位
今回迷ったのが、「どこまで細かくコンポーネントにするか」だった。
たとえば、
点数入力欄1個
だけでもコンポーネントにはできる。
ただ、細かくしすぎるとファイル数やpropsの受け渡しが増える。
今回は、画面上のカード単位で、
現在点数
条件
必要点数
終局結果
結果まとめ
というまとまりがあったので、その単位で分けた。
今の規模ならこのくらいが扱いやすそう。
さらに同じUIを何度も使うようになったら、その部分だけ追加でコンポーネント化することもできる。
App.tsxもまだ小さくはない
コンポーネントへ分けたことでJSX部分は整理できたものの、App.tsxではまだかなり多くの値を受け渡している。
特にConditionSectionやResultInputSectionはpropsが多い。
なので、これで完全に整理できたというわけではない。
ただ、
どの値を管理するか
どの値を計算するか
どの画面で表示するか
が分かれただけでも、以前よりコードを追いやすくなった。
今後さらに画面が増えてきたら、その時点でまた構成を考えることになりそう。
今回できたこと
今回の進捗は以下のとおり。
- 画面を5つのコンポーネントへ分割した
- 持ち点入力を
ScoreInputSectionへ分けた - 条件設定を
ConditionSectionへ分けた - 必要点数表示を
RequiredPointsSectionへ分けた - 終局結果入力を
ResultInputSectionへ分けた - 終局結果表示を
ResultSummarySectionへ分けた - stateやイベント処理をpropsで渡す形にした
App.tsxを各処理と画面をつなぐ役割に寄せた
今回も、新しい計算機能そのものを追加したわけではない。
ただ、今後さらに機能を増やしていくために、画面側のコードを整理できた。
最初は一つのファイルに書いていたものが、機能追加とともに少しずつ分かれていくのを見ると、アプリ自体もだいぶ大きくなってきた感じがする。
リファクタリング地獄。牛一。
コメント