1
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?

【JavaScript】スロットマシン制作で学んだ class構文・タイマー処理・判定ロジック

1
Posted at

タイトルなし1.gif

1. はじめに

ドットインストールのJavaScript講座でスロットマシンを作りました。今回の実装では class 構文・再帰的な setTimeout・絵柄の一致判定ロジックなど、これまでの講座の中でも特に盛りだくさんの内容でした。この記事では、実装を通じて理解したことを整理します。

2. 完成コード

今回作成したスロットマシンの全コードです。HTML・CSS・JavaScriptの3ファイルで構成されています。

2.1 HTML

シンプルな構成です。<main> の中身はJavaScriptで動的に生成するため、HTMLにはほとんど何も書きません。

<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="utf-8">
  <title>Slot Machine</title>
  <link rel="stylesheet" href="css/styles.css">
</head>
<body>
  <main></main>
  <div id="spin">SPIN</div>
  <script src="js/main.js"></script>
</body>
</html>

2.2 CSS

body {
  background: #bdc3c7;
  font-size: 16px;
  font-weight: bold;
  font-family: Arial, sans-serif;
}
main {
  width: 300px;
  background: #ecf0f1;
  padding: 20px;
  border: 4px solid #fff;
  border-radius: 12px;
  margin: 16px auto;
  display: flex;
  justify-content: space-between;
}
.panel img {
  width: 90px;
  height: 110px;
  margin-bottom: 4px;
}
.stop {
  cursor: pointer;
  width: 90px;
  height: 32px;
  background: #ef454a;
  box-shadow: 0 4px 0 #d1483e;
  border-radius: 16px;
  line-height: 32px;
  text-align: center;
  font-size: 14px;
  color: #fff;
  user-select: none;
}
#spin {
  cursor: pointer;
  width: 280px;
  height: 36px;
  background: #3498db;
  box-shadow: 0 4px 0 #2880b9;
  border-radius: 18px;
  line-height: 36px;
  text-align: center;
  color: #fff;
  user-select: none;
  margin: 0 auto;
}
.unmatched {
  opacity: 0.5;
}
.inactive {
  opacity: 0.5;
}

2.3 JavaScript

今回の実装の中心です。Panel クラスにパネル1枚分のDOM生成・アニメーション・判定ロジックをまとめています。

'use strict';
{
  class Panel {

    // パネル1枚分のDOM要素を生成してmainに追加する
    constructor() {
      const section = document.createElement('section');
      section.classList.add('panel');
      this.img = document.createElement('img');
      this.img.src = this.getRandomImage();
      this.timeoutId = undefined;
      this.stop = document.createElement('div');
      this.stop.textContent = 'STOP';
      this.stop.classList.add('stop', 'inactive');

      // STOPボタンが押されたときの処理
      this.stop.addEventListener('click', () => {
        if (this.stop.classList.contains('inactive')) {
          return;
        }
        this.stop.classList.add('inactive');

        // タイマーを止めて、残りパネル数をカウントダウンする
        clearTimeout(this.timeoutId);
        panelsLeft--;

        // 全パネルが止まったら判定を実行してSPINを再度有効にする
        if (panelsLeft === 0) {
          checkResult();
          spin.classList.remove('inactive');
          panelsLeft = 3;
        }
      });
      section.appendChild(this.img);
      section.appendChild(this.stop);
      const main = document.querySelector('main');
      main.appendChild(section);
    }

    // ランダムに画像パスを1つ返す
    getRandomImage() {
      const images = [
        'img/seven.png',
        'img/bell.png',
        'img/cherry.png',
      ];
      return images[Math.floor(Math.random() * images.length)];
    }

    // 50msごとに画像を切り替えて回転アニメーションを再帰的に繰り返す
    spin() {
      this.img.src = this.getRandomImage();
      this.timeoutId = setTimeout(() => {
        this.spin();
      }, 50);
    }

    // 自分の絵柄がp1・p2どちらとも一致しない場合にtrueを返す
    isUnmatched(p1, p2) {
      return this.img.src !== p1.img.src && this.img.src !== p2.img.src;
    }

    // 揃っていないパネルの画像を半透明にする
    unmatch() {
      this.img.classList.add('unmatched');
    }

    // 次のゲームに備えてパネルを初期状態に戻す
    activate() {
      this.img.classList.remove('unmatched');
      this.stop.classList.remove('inactive');
    }
  }

  // 3枚の絵柄を比較して揃っていないパネルを薄く表示する
  function checkResult() {
    if (panels[0].isUnmatched(panels[1], panels[2])) {
      panels[0].unmatch();
    }
    if (panels[1].isUnmatched(panels[0], panels[2])) {
      panels[1].unmatch();
    }
    if (panels[2].isUnmatched(panels[0], panels[1])) {
      panels[2].unmatch();
    }
  }

  // パネルを3枚生成する(生成と同時にDOMへの追加も行われる)
  const panels = [
    new Panel(),
    new Panel(),
    new Panel(),
  ];

  // 残りSTOP数を管理するカウンター
  let panelsLeft = 3;

  const spin = document.getElementById('spin');

  // SPINボタンが押されたら全パネルを活性化して回転を開始する
  spin.addEventListener('click', () => {
    if (spin.classList.contains('inactive')) {
      return;
    }
    spin.classList.add('inactive');
    panels.forEach(panel => {
      panel.activate();
      panel.spin();
    });
  });
}

3. 実装を通じて理解したこと

この実装で特に学びになったポイントを5つに絞って整理します。

3.1 class でパネルを管理する設計

1枚のパネルに必要な要素(画像・STOPボタン・タイマーID)をすべて class の中にまとめる設計が、今回の核心でした。

constructor() の中で <section><img><div> をそれぞれ生成し、DOMに追加しています。

パネルを3つ作るときは、インスタンスを3つ生成するだけで済みます。

const panels = [
  new Panel(),
  new Panel(),
  new Panel(),
];

「要素の生成・イベント登録・DOMへの追加をすべて constructor() 内でやる」という書き方は、クラスを使う利点がよく出ているみたいです。

3.2 setTimeout の再帰呼び出しでアニメーションを実現する

画像をパラパラ切り替えるアニメーションは、setTimeout を再帰的に呼び出すことで実現しています。

spin() {
  this.img.src = this.getRandomImage();
  // 50ms後に自分自身をもう一度呼び出す
  this.timeoutId = setTimeout(() => {
    this.spin();
  }, 50);
}

ポイントは、setTimeout の戻り値(タイマーID)を this.timeoutId に保存していることです。STOPボタンが押されたとき、clearTimeout(this.timeoutId) でタイマーを止めています。

タイマーIDを保存しておかないと、後から clearTimeout() で止めることができません。「止める手段を確保してから動かす」という発想が大切みたいです。

3.3 「揃っていないもの」を探す判定ロジック

3枚の絵柄が揃っているかの判定は、isUnmatched() メソッドで行っています。

「揃っているものを探す」ではなく「揃っていないものを探す」という発想で書かれているのが特徴的と理解しました。

isUnmatched(p1, p2) {
  // 自分の絵柄がp1とも違い、p2とも違うなら「揃っていない」
  return this.img.src !== p1.img.src && this.img.src !== p2.img.src;
}

checkResult() 関数では、3枚それぞれに対して isUnmatched() を呼び、揃っていないパネルだけ unmatch() で薄く表示します。

function checkResult() {
  if (panels[0].isUnmatched(panels[1], panels[2])) {
    panels[0].unmatch();
  }
  if (panels[1].isUnmatched(panels[0], panels[2])) {
    panels[1].unmatch();
  }
  if (panels[2].isUnmatched(panels[0], panels[1])) {
    panels[2].unmatch();
  }
}

3枚すべてバラバラの場合は3枚とも薄くなり、2枚が同じ場合は残り1枚だけが薄くなります。3枚すべて同じ(大当たり)のときは isUnmatched() が全部 false を返すため、何も薄くならないみたいです。

3.4 panelsLeft でSTOP完了数を管理する

3つのSTOPボタンがすべて押されたタイミングで判定を実行するため、panelsLeft という変数でカウントダウンしています。

let panelsLeft = 3;

STOPが押されるたびに panelsLeft-- し、0 になったら checkResult() を呼び出します。判定後は panelsLeft = 3 でリセットします。

this.stop.addEventListener('click', () => {
  if (this.stop.classList.contains('inactive')) {
    return;
  }
  this.stop.classList.add('inactive');

  // タイマーを止めて、残りパネル数をカウントダウンする
  clearTimeout(this.timeoutId);
  panelsLeft--;

  // 全パネルが止まったら判定を実行してSPINを再度有効にする
  if (panelsLeft === 0) {
    checkResult();
    spin.classList.remove('inactive');
    panelsLeft = 3;
  }
});

3.5 inactive クラスでボタンの有効・無効を制御する

SPINボタンとSTOPボタンの有効・無効は、inactive クラスの付け外しで管理しています。

CSSで .inactiveopacity: 0.5 を設定しておくことで、見た目を薄くしています。

// ボタンを非活性にする
this.stop.classList.add('inactive');

// ボタンを活性化する
this.stop.classList.remove('inactive');

まとめ

今回のスロットマシン実装で理解できたことをまとめます。

  • class を使うと、1つのパネルに必要な要素・状態・ロジックをひとまとめにできるみたいです
  • setTimeout の再帰呼び出しでアニメーションを実現し、戻り値の timeoutId を保存しておくことで clearTimeout() で止められると理解しました
  • 「揃っていないものを探す」という isUnmatched() の発想は、判定ロジックをシンプルに書く工夫のようです
  • panelsLeft のようなカウンター変数で「全員がアクションを完了したか」を管理するパターンは、複数要素を扱う場面で使えそうと感じました
1
0
1

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
1
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?