0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Reactで神経衰弱アプリ作ってみた(コンポーネント作成編)

Last updated at Posted at 2024-12-23

企画概要

今アツいReactを使って、みんなでアプリを作ってみよう!という思いから始まった神経衰弱アプリ作成企画…

全6回のうち、今回は第5回のコンポーネント作成編です!

関連記事は以下です
興味のある方は是非シリーズで読んでみてください

コンポーネント作成編

アプリのメイン機能であるカードをめくって一致するか判定するロジックなどは完成しました。

ここからはゲームらしくするために以下の機能を作成します!

  • 残り時間のカウントダウン
  • ゲームオーバーまたはクリアのモーダル表示(おまけで「もう一度遊ぶ」機能)

カウントダウンタイマー機能

まずはカウントダウンタイマーの実装からです。

といっても、ここでしたことは参考サイトのコードを真似して書いてみただけです。

setIntervalでカウントダウンしていってますね。

業務の合間にやってる開発なので時短は大事です。

src\hooks\useCountDownTimer.ts
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 };
src\pages\game.tsx (タイマー部分のみ抜粋)

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」と表示するモーダルを実装します。

こちらも基本は参考サイトを見ながらモーダルを作成しました!

まずはMaterialUIreact-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;
image.png
初期表示
image.png
「モーダル開く」を押すとモーダルが表示されます(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;
image.png
ゲーム残り時間が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;
image.png
見た目はゲームオーバーとほぼ変わらず。こちらはクリアしたことで残り時間が0に更新されている

「もう一回」ボタン

ついでに、「もう一回」ボタンを押したときにゲームの状態を初期化して再度神経衰弱が出来るようにしてみました。

カウントダウンタイマーの初期値を再設定して、カードの生成関数とペアになったカードの配列を初期化しています。

 // 「もう一回」ボタン押下時の処理
  const retryGames = () => {
    setCountTime(TIMEOUT);
    setMatchedCards([]);
    setCards(generateCards());
  };
image.gif
「もう一回」ボタンを押下すると再度ゲームが始まる

これでモーダルが完成しました!

時間制限リトライが出来るようになったことでだいぶゲームらしくなったのではないでしょうか?!

参考

完成したソースコード

src\pages\game.tsx

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;
0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?