企画概要
今アツいReactを使って、みんなでアプリを作ってみよう!という思いから始まった神経衰弱アプリ作成企画…
全6回のうち、今回は第5回のコンポーネント作成編です!
関連記事は以下です
興味のある方は是非シリーズで読んでみてください
コンポーネント作成編
アプリのメイン機能であるカードをめくって一致するか判定するロジックなどは完成しました。
ここからはゲームらしくするために以下の機能を作成します!
- 残り時間のカウントダウン
- ゲームオーバーまたはクリアのモーダル表示(おまけで「もう一度遊ぶ」機能)
カウントダウンタイマー機能
まずはカウントダウンタイマーの実装からです。
といっても、ここでしたことは参考サイトのコードを真似して書いてみただけです。
setIntervalでカウントダウンしていってますね。
業務の合間にやってる開発なので時短は大事です。
import { useEffect } from "react";
const useCountDownTimer = (
countTime: number | null,
setCountTime: (arg0: number) => void
) => {
useEffect(() => {
const countDownInterval = setInterval(() => {
if (countTime === 0) {
clearInterval(countDownInterval);
}
if (countTime && countTime > 0) {
setCountTime(countTime - 1);
}
}, 1000);
return () => {
clearInterval(countDownInterval);
};
}, [countTime]);
};
export { useCountDownTimer };
import React, { useState } from "react";
import { useCountDownTimer } from "../hooks/useCountDownTimer";
// タイマーの時間
const TIMEOUT = 60;
const Board: React.FC = () => {
// タイマーの設定
const [countTime, setCountTime] = useState<number>(TIMEOUT);
useCountDownTimer(countTime, setCountTime);
return (
<div>
<p
style={{
display: "flex",
justifyContent: "center",
}}
>
ゲーム残り時間:
{countTime % 60}秒{" "}
</p>
</div>
);
};
export default Board;
参考
モーダル表示機能 + 「もう一度遊ぶ」機能
さて、次はタイマーが0になったときに「GameOver」、カードが全部そろったときに「Clear」と表示するモーダルを実装します。
こちらも基本は参考サイトを見ながらモーダルを作成しました!
まずはMaterialUIとreact-modalのライブラリを導入します。便利なものはすぐに使いましょう。時短大事(2回目)
npm install @mui/material @emotion/react @emotion/styled
npm install --save react-modal
必要なライブラリがインストールできたら、とりあえずモーダルを表示してみます。
import { Button } from "@mui/material";
import React, { useState } from "react";
import Modal from "react-modal";
const Board: React.FC = () => {
const [modalIsOpen, setModalIsOpen] = useState(false);
return (
<div>
<Button
variant="contained"
color="primary"
onClick={() => {
setModalIsOpen(true);
}}
>
モーダル開く
</Button>
<Modal isOpen={modalIsOpen}>モーダルだよ</Modal>
</div>
);
};
export default Board;
初期表示 |
「モーダル開く」を押すとモーダルが表示されます(styleを設定していないので見た目が崩れていますが一旦無視) |
ゲームオーバーモーダル
モーダルが表示できたのでタイマーが0になったときにゲームオーバーのモーダルを出します。
import React, { useEffect, useState } from "react";
import Modal from "react-modal";
import { Button } from "@mui/material";
// タイマーの時間
const TIMEOUT = 60;
const Board: React.FC = () => {
// タイマーの設定
const [countTime, setCountTime] = useState<number>(TIMEOUT);
useCountDownTimer(countTime, setCountTime);
// モーダルのstyle
const customStyles = {
content: {
top: "20%",
left: "50%",
right: "auto",
bottom: "auto",
transform: "translate(-50%, -50%)",
},
};
// モーダルの表示設定
const [gameOverModalIsOpen, setGameOverModalIsOpen] = useState(false);
useEffect(() => {
if (countTime === 0) {
setGameOverModalIsOpen(true);
return;
}
setGameOverModalIsOpen(false);
}, [countTime]);
return (
<div>
<p
style={{
display: "flex",
justifyContent: "center",
}}
>
ゲーム残り時間:
{countTime % 60}秒{" "}
</p>
<Modal isOpen={gameOverModalIsOpen} style={customStyles}>
<h2>Game Over!!!</h2>
<Button
variant="outlined"
color="inherit"
onClick={() => {
setGameOverModalIsOpen(false);
}}
>
閉じる
</Button>
</Modal>
</div>
);
};
export default Board;
ゲーム残り時間が0秒なのでゲームオーバー |
クリアモーダル
続いて、クリアモーダルも表示します。
こちらは表示されているカードがすべてペアになったら表示されるように設定します。
import React, { useEffect, useState } from "react";
import Card from "../components/Card";
import Modal from "react-modal";
import { Button } from "@mui/material";
// カードの枚数
const NUMBEROFCARDS = 24;
// カードの生成関数
const generateCards = (): CardData[] => {
const values = ["A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L"];
const cards = values.flatMap((value) => [
{ id: Math.random(), value },
{ id: Math.random(), value },
]);
return cards.sort(() => Math.random() - 0.5);
};
const Board: React.FC = () => {
const [cards, setCards] = useState<CardData[]>(generateCards()); //cardsに、generateCardsで生成した内容を設定
const [flippedCards, setFlippedCards] = useState<number[]>([]);
const [matchedCards, setMatchedCards] = useState<number[]>([]);
// モーダルのstyle
const customStyles = {
content: {
top: "20%",
left: "50%",
right: "auto",
bottom: "auto",
transform: "translate(-50%, -50%)",
},
};
// モーダルの表示設定
const [gameOverModalIsOpen, setGameOverModalIsOpen] = useState(false);
const [clearModalIsOpen, setClearModalIsOpen] = useState(false);
useEffect(() => {
if (matchedCards.length === NUMBEROFCARDS) {
setClearModalIsOpen(true);
setCountTime(0);
return;
}
if (countTime === 0) {
setGameOverModalIsOpen(true);
return;
}
setClearModalIsOpen(false);
setGameOverModalIsOpen(false);
}, [countTime]);
return (
<div>
<Modal isOpen={clearModalIsOpen} style={customStyles}>
<h2>Game Clear !</h2>
<Button
variant="outlined"
color="inherit"
onClick={() => {
setClearModalIsOpen(false);
}}
>
閉じる
</Button>
</Modal>
<Modal isOpen={gameOverModalIsOpen} style={customStyles}>
<h2>Game Over!!!</h2>
<Button
variant="outlined"
color="inherit"
onClick={() => {
setGameOverModalIsOpen(false);
}}
>
閉じる
</Button>
</Modal>
</div>
);
};
export default Board;
見た目はゲームオーバーとほぼ変わらず。こちらはクリアしたことで残り時間が0に更新されている |
「もう一回」ボタン
ついでに、「もう一回」ボタンを押したときにゲームの状態を初期化して再度神経衰弱が出来るようにしてみました。
カウントダウンタイマーの初期値を再設定して、カードの生成関数とペアになったカードの配列を初期化しています。
// 「もう一回」ボタン押下時の処理
const retryGames = () => {
setCountTime(TIMEOUT);
setMatchedCards([]);
setCards(generateCards());
};
「もう一回」ボタンを押下すると再度ゲームが始まる |
これでモーダルが完成しました!
時間制限とリトライが出来るようになったことでだいぶゲームらしくなったのではないでしょうか?!
参考
完成したソースコード
import { Link } from "react-router-dom";
import React, { useEffect, useState } from "react";
import Card from "../components/Card";
import { useCountDownTimer } from "../hooks/useCountDownTimer";
import Modal from "react-modal";
import { Button } from "@mui/material";
// カードのデータ型
interface CardData {
id: number;
value: string;
}
// カードの枚数
const NUMBEROFCARDS = 24;
// タイマーの時間
const TIMEOUT = 60;
// カードの生成関数
const generateCards = (): CardData[] => {
const values = ["A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L"];
const cards = values.flatMap((value) => [
{ id: Math.random(), value },
{ id: Math.random(), value },
]);
return cards.sort(() => Math.random() - 0.5);
};
const Board: React.FC = () => {
const [cards, setCards] = useState<CardData[]>(generateCards()); //cardsに、generateCardsで生成した内容を設定
const [flippedCards, setFlippedCards] = useState<number[]>([]);
const [matchedCards, setMatchedCards] = useState<number[]>([]);
// モーダルのstyle
const customStyles = {
content: {
top: "20%",
left: "50%",
right: "auto",
bottom: "auto",
transform: "translate(-50%, -50%)",
},
};
// タイマーの設定
const [countTime, setCountTime] = useState<number>(TIMEOUT);
useCountDownTimer(countTime, setCountTime);
// モーダルの表示設定
const [gameOverModalIsOpen, setGameOverModalIsOpen] = useState(false);
const [clearModalIsOpen, setClearModalIsOpen] = useState(false);
useEffect(() => {
if (matchedCards.length === NUMBEROFCARDS) {
setClearModalIsOpen(true);
setCountTime(0);
return;
}
if (countTime === 0) {
setGameOverModalIsOpen(true);
return;
}
setClearModalIsOpen(false);
setGameOverModalIsOpen(false);
}, [countTime]);
// 「もう一回」ボタン押下時の処理
const retryGames = () => {
setCountTime(TIMEOUT);
setMatchedCards([]);
setCards(generateCards());
};
const handleCardClick = (id: number) => {
//揃い済みのカード、めくったカードをクリックしたら何も処理をせず返す。
if (
flippedCards.length === 2 ||
matchedCards.includes(id) ||
flippedCards.includes(id)
) {
return;
}
//めくられたカードのidをFlippedCardsの要素に追加する。
//1枚目、2枚目の要素がnewFlippedCardsに追加される。
const newFlippedCards = [...flippedCards, id];
setFlippedCards(newFlippedCards);
//2枚めくられたら以下の判定処理を実施
if (newFlippedCards.length === 2) {
const [firstId, secondId] = newFlippedCards;
//カードの配列のなかから、1枚目、2枚目のカードIDと一致するカード情報を検索
const firstCard = cards.find((card) => card.id === firstId);
const secondCard = cards.find((card) => card.id === secondId);
//1枚目と2枚目のカードの内容が一致するか確認。?を使いnull対策。
//一致したら、matchedCardsにfirstId,secondIDを追加
if (firstCard?.value === secondCard?.value) {
setMatchedCards([...matchedCards, firstId, secondId]);
}
//1秒後にsetFlippedCardsを実行し、flippedCards状態が空になり、すべてのカードが裏返しに戻る
setTimeout(() => setFlippedCards([]), 1000);
}
};
// カードを並べるためのレイアウトを設定
const rows = [];
for (let i = 0; i < 3; i++) {
rows.push(
<div key={i} style={{ display: "flex", justifyContent: "center" }}>
{cards.slice(i * 8, (i + 1) * 8).map((card) => (
<Card
key={card.id}
id={card.id}
value={card.value}
// 以下評価結果がtrueだと裏になる。
isFlipped={
flippedCards.includes(card.id) || matchedCards.includes(card.id)
}
onClick={handleCardClick}
/>
))}
</div>
);
}
return (
<div
style={{
marginTop: "50px",
}}
>
<p
style={{
display: "flex",
justifyContent: "center",
}}
>
ゲーム残り時間:
{countTime % 60}秒{" "}
</p>
{rows}
<div
style={{
marginTop: "50px",
display: "flex",
justifyContent: "center",
}}
>
<Link to="/">
{" "}
<Button variant="outlined" sx={{ justifyContent: "center" }}>
HOMEへ戻る
</Button>
</Link>
</div>
<Modal isOpen={clearModalIsOpen} style={customStyles}>
<h2>Game Clear !</h2>
<Button
variant="outlined"
color="primary"
onClick={() => {
setClearModalIsOpen(false);
retryGames();
}}
>
もう一回
</Button>{" "}
<Button
variant="outlined"
color="inherit"
onClick={() => {
setClearModalIsOpen(false);
}}
>
閉じる
</Button>
</Modal>
<Modal isOpen={gameOverModalIsOpen} style={customStyles}>
<h2>Game Over!!!</h2>
<Button
variant="outlined"
color="primary"
onClick={() => {
setGameOverModalIsOpen(false);
retryGames();
}}
>
もう一回
</Button>{" "}
<Button
variant="outlined"
color="inherit"
onClick={() => {
setGameOverModalIsOpen(false);
}}
>
閉じる
</Button>
</Modal>
</div>
);
};
export default Board;