14
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

FORKAdvent Calendar 2020

Day 18

Reactで神経衰弱を作ってみた

Last updated at Posted at 2020-12-17

卒業できるかな?

See the Pen RwGKbRJ by kotani (@kotani) on CodePen.

はじめに

最近注目されているReactについて記事を書こうと思います。
Reactで検索してみると、既に入門的な情報が転がっていますが、それらを読んだだけでは
なかなかイメージがつきにくいこともあり、Reactを使って自分で作ってみることにしました。
Reactの公式ページには三目並べのゲームがチュートリアルで紹介されてましたが、
私は神経衰弱を作ってみることで、Reactを体験してみようと思います。

基本的な知識

Reactとは「ユーザインターフェース構築のための JavaScript ライブラリ」だそうです。

コンポーネントと呼ばれる小さい部品を組み合わせることでアプリケーションを構築します。
その1つ1つのコンポーネントは状態(State)を持っており、外部から渡された情報 props
に従ってその状態を変化させることで、そのコンポーネントの挙動の制御を行います。

コンポーネント同士を疎結合にすることで、再利用したり管理しやすくなり、
どちらかというと大規模アプリケーション開発に向いていると言われてます。
なので、アニメーションといった実装には向いてないと思います。

そのほかにも、仮想DOMという仕組みによって高速描画を行ったり、
JSXの記法でJavaScriptの中にHTML似の構文を使える技術も取り入れています。

シングルページアプリケーション(SPA)開発にも適しており、iPhoneアプリや
androidアプリのように、ReactNativeフレームワークを使うことで、どちらの環境でも
動くアプリ開発もできるようです。

神経衰弱の概要

ゲームのイメージは、下記の通りです。

  • 5種類の絵柄が書かれた2対のカード合計10枚が伏せてあります。
  • スタートボタンをクリックするとカウントダウンタイマーが作動しゲーム開始となります。
  • 任意のカード1枚目をクリックすると、カードが反転し絵柄が表示されます。
  • 続けて別の2枚目のカードをクリックすると、同様に反転し絵柄が表示されます。
  • 2枚のカードの絵柄が揃ったら、カードの色をピンク色に変更してそのままにします。
  • 2枚のカードの絵柄が揃わなかったら、1秒程度後に2枚とも元の状態へと伏せます。
  • 全てのカードの絵柄が揃った場合は「おめでとう!」のメッセージが表示されます。
  • 全てのカードの絵柄を揃えることができないままタイムアウトになってしまった場合は
    「ゲームオーバー」のメッセージを表示させます。
  • カードが揃ったかどうかの判定結果は、下の方に「あたり」「はずれ」のメッセージを表示させます。

このような内容のゲームをReactで実装してみます。

準備

Reactを使った本格的な開発は、create-react-app という環境構築ツールを使うようですが、
ここでは、下記jsを外部から取り込むだけで使えるようになるお手軽な方法で実装します。

<script src="https://unpkg.com/react@17/umd/react.development.js" crossorigin></script>
<script src="https://unpkg.com/react-dom@17/umd/react-dom.development.js" crossorigin></script>
<script src="https://unpkg.com/babel-standalone@6/babel.min.js"></script>

HTMLの body 終了タグの直前に上記3行を記述して、scriptタグにtype="text/babel" 属性を追加します。JSX構文はまだ今のブラウザには対応していないので、Babelというツールを使って後方互換バージョンへ変換することで使えるようになっています。また、カードの画像を1個用意して、10枚分のカードが並んだ状態にCSS調整しておきます。

コンポーネントの説明

ここでは、コンポーネントの少し具体的な内容をコードと共に紹介していきます。

1 構成

下記のように、3つのコンポーネントで構成されています。

①Gameコンポーネント(②の親コンポーネント)
      |
②Tableコンポーネント(③の親コンポーネント)
      |
③Cardコンポーネント

2 Gameコンポーネント

下記は、divタグのところにReactコンポーネントを組み込む処理の記述になります。

<div id="root"></div>

React.Component クラスを継承したGameクラスを定義し、render()メソッドの中でGameコンポーネントを記述します。Gameコンポーネントの中にTableコンポーネントが含まれていることが分かるかと思います。

class Game extends React.Component {
    render() {
        return (
            <div className="game">
                <div className="main">
                    <Table />
                </div>
            </div>
        );
    }
}
ReactDOM.render(
    <Game />,
    document.getElementById('root')
);

3 Tableコンポーネント1(初期化)

コードが長いので分割します。TableコンポーネントもReact.Componentを継承します。
constructorの中では、getInitialState()をコールして初期化した各種状態のプロパティを持つオブジェクトをstateに格納します。shuffleは、毎回絵柄の並びが入れ替わるように配列内部をランダムに置換します。


class Table extends React.Component {
    constructor(props) {
        super(props);
        this.state = this.getInitialState();
    }
    getInitialState = () => {
        let arr = Array("", "", "", "", "", "", "", "", "", "");
        let sts = Array(10).fill(0);
        arr = this.shuffle(arr);
        return {
            cards: arr,
            status: sts,
            ready: -1,
            message: '',
            count: 15,
            timer: null,
            title:'',
            run:false,
            overlay: 'overlay'
        }
    }
    shuffle = ([...array]) => {
        for (let i = array.length - 1; i >= 0; i--) {
            const j = Math.floor(Math.random() * (i + 1));
            [array[i], array[j]] = [array[j], array[i]];
        }
        return array;
    }

    :
    :

}

4 Tableコンポーネント2(click時処理)

handleClickは、カードをクリックした時に呼ばれるメソッドです。
i はカードのindexであり、クリックされたカードに紐づいた数値(0~9)が渡されます。
stateで持っている状態は下記の通りです。

  • cards :絵柄 ("♡"や"♧"など)の配列
  • status : カードの状態を表す配列 0(初期値)、1(クリックされ絵柄が表示されている状態)
    2(2枚の絵柄が揃った状態)、3(2枚の絵柄が異なった状態)
  • ready : クリックされたカードの状態 -1(初期値)、0~9(クリック1枚目のindex)
    -2(「はずれ」が表示された状態)
  • message : 判定後のメッセージ "" または "あたり" または "はずれ"
  • count : カウントダウンの数値(1づつ減算、残り:xx秒 のところに表示される)
  • timer : setInterval()の戻り値を格納
  • title : 画面中央に表示する文言 "" または "おめでとうございます。卒業です!" または "ゲームオーバー"
  • run : ゲームの状態 true(ゲーム実行中)、false(ゲーム停止中)
  • overlay : オーバーレイのクラス指定 "" または "overlay" または "overlay overlay-end"

readyの値を見て、1枚目がクリックされたのか?2枚目がクリックされたのか?の判定に使用しています。

handleClick(i) {
    const sts = this.state.status.slice();
    //未選択以外をクリックされた時は無視
    if (this.state.status[i] != 0) {
        return;
    }
    //ゲームスタートしてなければ無視
    let run = this.state.run;
    if (!run){
        return;
    }
    let ready = -1;
    let message = "";
    let title = "";
    let overlay = "";
    if (this.state.ready == -2) {
        return;
    } else if (this.state.ready == -1) {
        //1枚目をクリックした時の処理
        sts[i] = 1;
        ready = i;
    } else if (this.state.ready != i) {
        //2枚目をクリックした時の処理
        sts[i] = 1;
        //2枚揃ったかどうかを判定
        if (this.state.cards[this.state.ready] == this.state.cards[i]) {
            //2枚揃った!
            message = "あたり";
            sts[this.state.ready] = 2;
            sts[i] = 2;
            if (!this.isFinish(sts)) {
                setTimeout(() => {
                    this.cardClear();
                }, 800);
            } else {
                message = '';
                run = false;
                title = "おめでとうございます。卒業です!";
                overlay = 'overlay overlay-end';
                clearInterval(this.state.timer);
            }
        } else {
            //揃ってなかった!
            message = "はずれ";
            ready = -2;
            sts[this.state.ready] = 3;
            sts[i] = 3;
            //少し経過後に元に戻す
            const rollbacksts = this.state.status.slice();
            rollbacksts[this.state.ready] = 0;
            rollbacksts[i] = 0;
            setTimeout(() => {
                this.cardClear();
                this.cardReset(rollbacksts);
            }, 800);
        }
    }
    this.setState({
        status: sts,
        ready: ready,
        message: message,
        run: run,
        title: title,
        overlay: overlay
    });
}

5 Tableコンポーネント3(ゲーム開始)

gameStartは、スタートボタンが押された場合に呼ばれます。
runを参照して、既にゲーム実行中であれば何もせずにリターンして、
そうでない場合はsetInterval()を使ってカウントダウンタイマーを起動します。

gameStart= () => {
    if(this.state.run){
        return;
    }
    this.setState(this.getInitialState());
    const timer = setInterval(() => this.countDown(), 1000);
    this.setState({
        timer: timer,
        run: true,
        overlay: ''
    });
}

6 Tableコンポーネント4(その他)

カードの状態を元の伏せた状態に戻します。

cardReset = (sts) => {
    this.setState({
        status: sts,
        ready: -1
    });
}

わずかな時間「あたり」「はずれ」のメッセージを表示させたあとは表示削除します。

cardClear = () => {
    this.setState({
        message: ""
    });
}

全てのカードが揃ったかどうかを判定します。stsの値で2以外のものが1つでも存在したら、
まだ伏せてあるカードがあると判定します。

isFinish = (sts) => {
    let flg = true;
    for (let i = 0; i < sts.length; i++) {
        if (sts[i] != 2) {
            flg = false;
            break;
        }
    }
    return flg;
}

タイマーによって1秒ごとに呼ばれるメソッドです。
countの値を1づつ減らしていき、0になったらゲームオーバーと判定します。

countDown = () => {
    let nextCount = this.state.count-1;
    if(nextCount < 1) {
        this.setState({
            message: '',
            count: 0,
            run: false,
            title: 'ゲームオーバー',
            overlay: 'overlay overlay-end'
        });
        clearInterval(this.state.timer);
    } else {
        this.setState({
            count: nextCount
        });
    }
}

カードの並びを構成するHTMLをJSXで定義します。

renderCard(i) {
    return (
        <Card key={i}
            number={this.state.cards[i]}
            ready={this.state.status[i]}
            onClick={() => this.handleClick(i)}
        />
    );
}
render() {
    const cards = [];
    for (let i = 0; i < 10; i++) {
        cards.push(this.renderCard(i));
    }
    return (
        <div>
            <button className="start-button" onClick={this.gameStart}>スタート</button>
            <div className="count-number">残り:{this.state.count}</div>
            <div className="table">
                {cards}
            </div>
            <div className="status">{this.state.message}</div>
            <div className={this.state.overlay}><p className="title">{this.state.title}</p></div>
        </div>
    );
}

7 Cardコンポーネント

関数型コンポーネントとして定義しています。
propsで渡されたreadyの値に従ってカードのclassName属性を設定します。(「裏」「表」「当たり」)

function Card(props) {
    let cardStyle = 'card card-ura';
    let numStyle = 'omote';
    switch(props.ready){
        case 1:
            numStyle = 'ura';
            break;
        case 2:
            numStyle = "ura atari";
            break;
        case 3:
            numStyle = "ura hazure";
            break;
        default:
            cardStyle = 'card card-omote';
            break;
    }
    return (
        <button className={cardStyle} onClick={props.onClick}>
            <div className={numStyle}><span>{props.number}</span></div>
        </button>
    );
}

最後に

Reactは、少々学習コストが高く感じられました。経験の浅い方が、これにいきなり入るのは
ちょっとハードルが高いように思います。上手に設計し構築することが出来るようになれば、
大規模なアプリでも耐えうる実力が十分ありそうなライブラリだということがわかりました。

コンポーネント間のデータの受け渡しはpropsを通じて行われますが、コンポーネントの
数が増えたり親子関係が深くなると、親から子へ通知する処理やコールバック関数が増えてしまい、
複雑化するようです。

そこで、React-Reduxという状態(state)管理ライブラリをうまく活用することで、
コンポーネント間の情報の受け渡しや状態管理をより簡単に実現することができます。

この神経衰弱は少しstate管理が煩雑になっているようなので、React-Reduxで実装した場合、
どのような書き方に変わるのか?どのような効果があるのか?を次回試してみようと思います。


:christmas_tree: FORK Advent Calendar 2020
:arrow_left: 17日目 Reactアプリケーション開発入門 @sy12345
:arrow_right: 19日目 Vue.jsのSSGフレームワークGridsomeはすごいぞ!! @Kodak_tmo

14
2
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
14
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?