Help us understand the problem. What is going on with this article?

【初心者向け】じゃんけんゲームを作りながら覚えるHTML&JavaScript

More than 1 year has passed since last update.

この記事では、HTML, CSS, JavaScriptの基礎文法を学んで次のステップに行きたいなと思っている方をターゲットに記事を書いていきます。

プログラミングに限らず、新しいものを身につけたいと思ったときには座学だけではなく、トライ&エラーを繰り返して実際に何かモノを作ると一気に成長します。

例えば次のようなものを作ってみると良いでしょう。

  1. Todoアプリを作る
  2. 簡単なゲームを作る
  3. ホームページを作る

今回作るもの

今回は上記の内2つ目に上げた「簡単なゲームを作る」を取り上げてJavaScriptの学習を目的とした記事となります。

「HTML、JavaScript、1枚の画像」のみを使ってじゃんけんゲームを作りながら一緒にHTMLのcanvasとJavaScriptを勉強していきましょう。

完成形とソースコード

完成形は次の画像になります。

janken-anim.gif

「じゃんけんアプリ」にアクセスすれば実際にジャンケンを楽しんでいただくことも出来ます。

ソースコードはこちらのレポジトリから確認できます。

じゃんけんゲームを作ることで学べること

今回使っている技術は次のとおりです。

  • 画像
    • スプライト画像について
  • HTML
    • canvas要素について
  • JavaScript
    • 即時関数
    • カプセル化(変数・関数のプライベート化)
    • DOMの基本操作(特定要素の取得・クリックイベントの処理)
    • canvasの操作方法
    • 画像の一部を切り抜く方法
    • setTimeoutと再帰関数を使ったアニメーションの実装方法
    • アニメーションフレームの概念

じゃんけんゲームのようなシンプルなゲームを作るだけでもこれだけ多くのことが学ぶことが出来ます。

それでは実際にソースコードを見ながら解説していきます。

この記事を読み終えた後、もしくは読みながら、同じようなじゃんけんゲームをご自身の手を動かして作ってみることをオススメします。

最初にも述べたとおり、技術を身につけるためには座学だけではなく、自分でも手を動かしてトライ&エラーを繰り返すことが必要になるためです。

それでは次の順番でそれぞれのファイルについて解説していきます。

  1. 1枚の画像ファイル
  2. HTML
  3. JavaScript

今回使用する3ファイルの説明

1枚の画像ファイル

今回使う1枚画像は次のように「グー・チョキ・パー」を1つにまとめた「スプライト画像」というものになります。

sprite.png

スプライト画像とは、複数の画像を1枚の画像にまとめた画像のことです。

1枚の画像にグーチョキパーの3つを含めないで、それぞれ「グーの画像ファイル」「チョキの画像ファイル」「パーの画像ファイル」のように3つの画像を用意して使うことも可能です。

しかし、今回1枚のスプライト画像を使った理由はなぜでしょうか?
1枚のスプライト画像を選択した理由は次のとおりです。

  1. 画像URLのリクエスト回数が3回から1回に減らせる
  2. canvasから取得できる「context」の機能を使うことで、画像の好きな範囲を切り取ることが出来る

最初の方で貼り付けたアニメーション画像は、このスプライト画像の一部を切り取って順番にグー・チョキー・パーと表示しているだけです。

HTML

今回使用するじゃんけんゲームのソースコードでは必要最低限のHTMLの実装となっています。

次のサンプルコードを確認してみましょう。

index.html
<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <title>じゃんけんアプリ</title>
</head>
<body>
    <canvas
        id="screen"
        width="400"
        height="400"
        style="border:1px solid #000000;"></canvas>
    <div>
        <button id="rock" value="1">グー</button>
        <button id="scissors" value="2">チョキ</button>
        <button id="paper" value="0">パー</button>
        <button id="restart">再開</button>
    </div>
    <script src="./main.js"></script>
</body>
</html>

今回のHTMLでどんなことを実装しているか大きく分けると次のようになります。

  • canvas要素に直接width, height, style属性を使って、じゃんけんゲームで使う画像の表示エリアを確保している(CSSを使っていない)
  • グー・チョキ・パーの選択をするためのボタンと、ゲーム再開するためのボタンを設置
  • JavaScriptファイルの読み込み

今回スタイルを当てているのはcanvas要素だけで、大きなアプリケーションを作っているわけではないので、CSSファイルを使わず直接style属性を使ってcanvasの枠線を用意しました。

おそらくHTML、JavaScriptを学び始めて次の段階にいこうと思っている方は、あまりcanvas要素に馴染みがないのではないでしょうか?

canvas要素について簡単に説明すると、図形・画像の描画や、ゲームなどのアニメーション実装をしやすくするためにHTML5から新しく用意された要素です。

canvasについてのより詳しい詳細はMDNのドキュメントページを参考にすると良いでしょう。

じゃんけんゲーム以外にもWebページでゲームを作るときはcanvas要素を使うことが一般的でしょう。

今回はじゃんけんゲームの作り方を紹介しながら、JavaScriptを覚えるのが目的ですが、canvasを使って作れるゲーム例として、過去に僕がJavaScriptの勉強目的で作ったゲームのリンクを以下に貼ります。

このようにcanvasを使うことで様々なゲームを作ることが可能です。

JavaScript

それでは最後にJavaScriptのコードを見てみましょう。

main.js
// 先頭に文字列で「'use strict';」とすることで、潜在的なバグを減らすことが出来ます。
// 詳細は以下のサイトの記事を参考
//
// - MDN Strict モード
//   - https://developer.mozilla.org/ja/docs/Web/JavaScript/Strict_mode
// - 【JavaScript入門】strictモードの使い方を徹底解説!
//   - https://www.sejuku.net/blog/58342
'use strict';

// JavaScriptは関数スコープのため
// 変数や関数を外から見えなくするために(カプセル化・プライベート化)
// 即時関数でスコープを閉じながら、関数内の処理をすぐに実行している。
(() => {
  // 手の形を数で表現。それぞれの数値はHTMLのbuttonタグ内のvalue属性で定義している。
  // value="1" => グー
  // value="2" => チョキ
  // value="0" => パー
  const HAND_FORMS = [
    0, // パー
    1, // グー
    2  // チョキ
  ];

  // images/sprite.png(グーチョキパーの画像)を切り取って使う際に
  // それぞれの手のx座標を指定している。
  const HAND_X = [
    0,   // グー
    380, // チョキ
    750  // パー
  ];
  // images/sprite.png(グーチョキパーの画像)を切り取って使う際に
  // それぞれの手のwidth(横幅)を指定している。
  const HAND_WIDTH = [
    360, // グー
    340, // チョキ
    430  // パー
  ];
  // ↑
  // 例: それぞれの手の形の画像を切り出したいときは
  // - グー : x軸が0pxから横幅360px切り出した範囲
  // - チョキ : x軸が380pxから横幅340px切り出した範囲
  // - パー : x軸が750pxから横幅430px切り出した範囲


  const IMAGE_PATH = './images/sprite.png';
  // 1秒間で60コマ(フレーム)のアニメーションを行う
  // ここの値が大きいほど手の切り替わりスピードが早くなる
  // 例:
  // - FPSの値が1: 1秒に1回手が切り替わる
  // - FPSの値が10: 1秒に10回手が切り替わる
  // - FPSの値が60: 1秒に60回手が切り替わる
  const FPS = 10;

  // loop関数内で呼び出しているdraw関数の実行をするかしないかを切り分けているフラグ
  // グー・チョキ・パーのいずれかのボタンが押された時にtrueになる。(onClick関数を参照)
  let isPause = false;

  // draw関数が実行されるたびに1増える(インクリメント)
  // currentFrameの値を剰余算演算子(%)を使い出たあまりを使うことで、
  // 表示される手の形を決める。
  // 今回の場合は手の形は3つ(HAND_FORMS.length)なので
  // 値は必ず0, 1, 2のいずれかとなる。
  // 例:
  // currentFrameが30のとき: 30 % 3 => 0 => HAND_FORMS[0] => グー
  // currentFrameが31のとき: 30 % 3 => 1 => HAND_FORMS[1] => チョキ
  // currentFrameが32のとき: 30 % 3 => 2 => HAND_FORMS[2] => パー
  let currentFrame = 0;

  /**
   * 実際にアニメーションを開始させる処理
   */
  function main() {
    const canvas = document.getElementById('screen');
    const context = canvas.getContext('2d');
    const imageObj = new Image();
    currentFrame = 0;

    // 画像('./images/sprite.png')の読み込みが完了したら、
    // loop関数の無限ループを実行する。
    imageObj.onload = function () {
      function loop() {
        if (!isPause) {
          draw(canvas, context, imageObj, currentFrame++);
        }

        // 指定した時間が経過したらloop関数を呼び出す。
        // 関数自身を呼び出す関数のことを再帰関数という。
        //
        // 例: FPSの値に応じてloop関数が実行される時間間隔が変わる
        // FPSが60 => 1000/60 => 16.666 => 0.016秒後にloop関数を実行 => 0.016秒毎に1回手が切り替わる
        // FPSが10 => 1000/10 => 100 => 0.1秒後にloop関数を実行 => 0.1秒毎に1回手が切り替わる
        // FPSが1 => 1000/1 => 1000 => 1秒後にloop関数を実行 => 1秒毎に1回手が切り替わる
        setTimeout(loop, 1000 / FPS);
      }
      loop();
    };
    imageObj.src = IMAGE_PATH;
  }

  /**
   * グー・チョキ・パー画像('./images/sprite.png')から特定の手の形を切り取る
   * @param {*} canvas HTMLのcanvas要素
   * @param {*} context canvasから取得した値。この値を使うことでcanvasに画像や図形を描画することが出来る
   * @param {*} imageObject 画像データ。
   * @param {*} frame 現在のフレーム数(コマ数)。フレーム % HAND_FORMS.lengthによって0(グー), 1(チョキ), 2(パー)を決める
   */
  function draw(canvas, context, imageObject, frame) {
    // HTML5から導入されたcanvasをJavaScriptを使って画像の切り替えを行っている。
    // - Canvas API: https://developer.mozilla.org/ja/docs/Web/HTML/Canvas
    // - CanvasRenderingContext2D: https://developer.mozilla.org/ja/docs/Web/API/CanvasRenderingContext2D

    // Canvasをまっさらな状態にする。(クリアする)
    // クリアをしなかった場合、以前に描画した画像がcanvas上に残ったままになってしまう。
    context.clearRect(0, 0, canvas.width, canvas.height);

    // 0: グー, 1, チョキ, 2: パー
    const handIndex = frame % HAND_FORMS.length;
    const sx = HAND_X[handIndex];
    const swidth = HAND_WIDTH[handIndex];

    // 画像のx座標(sx)と指定した手の横幅(swidth)を使って、
    // グー・チョキ・パー画像('./images/sprite.png')から特定の手の形を切り取る
    context.drawImage(
      imageObject,
      sx,
      0,
      swidth,
      imageObject.height,
      0,
      0,
      swidth,
      canvas.height
    );
  }

  /**
   * ボタンクリック時の処理の定義をまとめて行う関数
   */
  function setButtonAction() {
    const rock = document.getElementById('rock');
    const scissors = document.getElementById('scissors');
    const paper = document.getElementById('paper');
    const restart = document.getElementById('restart');

    // グー・チョキ・パーのいずれかをクリックした時に呼ばれる。
    function onClick(event) {
      // 自分の手と相手の手の値を取得する。
      const myHandType = parseInt(event.target.value, 10);
      const enemyHandType = parseInt(currentFrame % HAND_FORMS.length, 10);

      // isPauseフラグをtrueにすることでloop関数内で呼び出している
      // draw関数が実行されなくなる。
      isPause = true;

      // 自分の手の値と相手の値をjudge関数に渡して勝敗を確認する。
      judge(myHandType, enemyHandType);
    }

    // グー・チョキ・パーボタンを押したときの処理をonClick関数で共通化
    rock.addEventListener('click', onClick);
    scissors.addEventListener('click', onClick);
    paper.addEventListener('click', onClick);

    // 再開ボタンを押したとき、ブラウザをリロードする
    // https://developer.mozilla.org/en-US/docs/Web/API/Location/reload
    restart.addEventListener('click', function () {
      window.location.reload();
    });
  }

  // 自分の手の値(0~2のいずれか)と相手の手の値(0~2のいずれか)を使って計算を行い
  // 値に応じて勝ち・負け・引き分けを判断して、アラートに結果を表示する。
  function judge(myHandType, enemyHandType) {
    // 0: 引き分け, 1: 負け, 2: 勝ち
    // じゃんけんの勝敗判定のアルゴリズム: https://qiita.com/mpyw/items/3ffaac0f1b4a7713c869
    const result = (myHandType - Math.abs(enemyHandType) + 3) % HAND_FORMS.length;

    if (result === 0) {
      alert('引き分けです!');
    } else if (result === 1) {
      alert('あなたの負けです!');
    } else {
      alert('あなたの勝ちです!');
    }
  }

  // ボタンクリック時の処理の定義を行ってから、アニメーションを開始する
  setButtonAction();
  main();
})();

上記のコードを見ると長く感じるかもしれませんが、コードの半分は何を行っているか細かく説明したコメントになるため、実際は100行ほどのコードになります。

実際にそれぞれの行で何を行っているかはコメントを読んでいただけたらと思います。
そのかわりに、このJavaScriptコードでざっくり何をしているかの流れを説明したいと思います。

コードの流れとしては大きく分けると次の6通りです。

  1. 'use strict'; を先頭で宣言して、コーディングルールを厳格化する。(詳しくはMDNのドキュメントを参照してください)
  2. グローバル汚染を防ぐために即時関数内で実際の処理を実装している(Qiitaの記事「中上級者になるためのJavaScript【知識編】」がとても参考になります。(2549イイね(2018/9/11 現在)))
  3. main関数より手前で定義している変数に、ゲームで使う固定の値を定義している
    • HAND_FORMS: 自分の選択した手を数値にしたときの値を表現(HTMLのグーチョキパーのvalue値を参照)
    • HAND_X: スプライト画像画像のそれぞれの手を切り抜く際の視点となるx座標を定義している
    • HAND_WIDTH: HAND_Xで定義した始点から、それぞれの手を切り抜く際の横幅を定義している
    • IMAGE_PATH: スプライト画像画像の相対パス
    • FPS: アニメーションスピードの設定をしている。ここの値が大きくなるほどアニメーションが早くなり、小さくなるほど遅くなる。
    • isPause: グー・チョキ・パーのいずれかを選択した後にアニメーションさせないためのフラグ
    • currentFrame: 無限ループを使ってアニメーションを行っており、現在のループが何回目かを記録。「ループ回数 % 3」で0(グー), 1(チョキ), 2(パー)の値が取得できて、表示する手を切り替える事ができる。
  4. 必要な機能ごと関数を定義している
    • main関数: 画像の読み込みが完了したら無限ループでdraw関数を実行してアニメーション処理を行っている。
    • draw関数: currentFrameの値に応じて手を切り替えている
    • setButtonAction関数: DOMの取得やイベント処理の定義を行っている
    • judge関数: ユーザーがグー・チョキ・パーのいずれかをクリックしたら勝敗判定を行う
  5. 定義されているsetButtonAction関数を実行して、ボタンをクリックしたときの処理を有効にする
  6. main関数を呼び出してアニメーションを開始する。(ゲーム開始)

実際に手を動かして学習する

冒頭でも書いたように今回の記事はHTML, CSS, JavaScriptの基礎文法を学んで次のステップを学びたい人向けの記事です。

そのため、自分で作業を進められるようにコード上に細かくヒントを散りばめました。
例えば次のようなものがヒントに当たります。

  • 即時関数
  • グローバル汚染を防ぐカプセル化の方法
  • canvasを使ってゲームを作ることが出来る
  • スプライト画像を切り抜いて手を切り替えている
  • canvasと無限ループを使うことでアニメーションさせることが出来る
  • じゃんけんの勝敗判定のアルゴリズム

もう一度こちらでも、今回使ったソースコードのレポジトリを共有します。

今回紹介したじゃんけんゲームが、次のステップのためのJavaScript学習にお役に立てたら幸いです。
わからない点があればお気軽に質問してください^^

あらためて、今回使ったファイルは次の通りで、ライブラリも全く使っていない純粋なブラウザJavaScriptゲームとなります。

  • 1枚の画像
  • 20行ほどのHTML
  • 100行ほどのJavaScript(コメントを除いたとき)

ライブラリを使わなくてもゲームが作れるというのを体験していただけたらと思います。

最後に

途中でも紹介した以下のゲームについてもコードが見れるので、もし興味があればそれぞれのコードを参考にテトリスやその他のゲーム作りにもチャレンジしてみてください。

実際に僕自身上記ゲームを作ったことでJavaScriptスキルがだいぶ上がりました。
なので、同じように上記ゲームを作りあげたらだいぶ成長すると思います^^

今回はWebフロントエンド技術を使って簡単ゲームの作り方について説明しましたが、その他にもWeb開発に関する知識を共有しているブログがあるので、

Webフロントエンドだけでなく、「バックエンドも勉強したいな」「エンジニアではないけどWeb技術について知りたいな」という方は是非以下のリンク先にアクセスしてみてください^^

tsuyopon_xyz
【Webエンジニアを目指している方へ】 Webのフロントエンド・バックエンドスキルの基礎を学べる学習コンテンツを多数公開しています! → https://tsuyopon.xyz   MENTAでも教えています!(未経験からWebエンジニア転職に成功した人も輩出) →https://menta.work/plan/677
https://tsuyopon.xyz/
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away