Reskilling ❘ Data Science

ゲームプログラミング | 物理演算

#リスキリング #レジリエンス #ICT教育 #ITスキル #プログラミング

物理演算ゲーム
プログラミング

普段みなさんが遊んでいるゲームはどのようにして作られているのでしょうか。

3D空間を自由に動き回るゲームにはFPSやマインクラフトのようなビルドゲームなど様々あります。

これらのゲームには「物理演算」という、現実世界の物体の動きをゲーム上で再現する仕組みが使われています。

今回はこの「物理演算」を使ってゲーム制作をしてみましょう。

  • 2015年

    創業

  • 120名

    従業員数

  • 2.3M

    月間UU

  • 1,800戸

    管理戸数

0. 実際に動かしてみよう

今回作るのは

物理演算を使った、アングリーバード風の「トリとばしゲーム」です。

まずは実際にプレイしてみましょう▼

トリとばしゲームを通じて、

・HTML
・JavaScript
・変数
・配列
・繰り返し処理
・ライブラリ

などについて学んでいきましょう。

1. HTMLで土台を作る

1.1 HTMLについて確認しよう

今回のゲームは
・HTML
・CSS
・JavaScript
の3つを使って作っていきます。

HTMLというのはWebページを構成する、最も基本となる要素です。

例えば、今見ているこのサイトもHTMLで書かれています。

HTMLは「タグ」と呼ばれる仕組みで表現されています。

文字を表示したい場合は「p」というタグを使います。

他にも、表形式で表示したい場合は「table」「tr」「td」といったタグを上手く使います。

実際には以下のようにHTMLを書きます。

<p>これは文字です</p>

<table>
  <tr>
    <td>これは表です。</td>
    <td>これは表です。</td>
  </tr>
  <tr>
    <td>これは表です。</td>
    <td>これは表です。</td>
  </tr>
</table>

このように、実際に表示したい文字を「タグ」ではさんで使います。

挟んだ終わりの方のタグには「/」を付けます。

1.2 ゲーム用のHTMLをコピーしよう

では、今回のゲーム作りのための土台となるHTMLを見てみましょう。

書くのは難しいので、「コードエディタ」にコピーしましょう。

<!DOCTYPE html>
<html lang="ja">

<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Simple Physics tori</title>
  <!-- Pico.css CDN -->
  <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@picocss/pico@2/css/pico.min.css">
  <!-- Matter.js CDN -->
  <script src="https://cdnjs.cloudflare.com/ajax/libs/matter-js/0.19.0/matter.min.js"></script>

</head>

<body class="container">

  <header>
    <hgroup>
      <h1>tobutori</h1>
    </hgroup>
  </header>

  <main>
    <div id="game-container"></div>
    <button class="secondary" onclick="location.reload()">リセット</button>
  </main>

  <script src="https://cdn.jsdelivr.net/gh/takahiro-inoway/workshop@main/birdgame_1/workshop_1.js"></script>

  <script>
  </script>

  <script src="https://cdn.jsdelivr.net/gh/takahiro-inoway/workshop@main/birdgame_1/workshop_2.js"></script>

</body>

</html>

2. ゲームの背景を作る

2.1 JavaScriptを書いてみよう

HTMLができたので、これでWebページを作る準備はできました。

ではゲームを作っていきます。

ゲームは普通のWebサイトと違って、ただ文字を表示すればよいわけではありません。

物を動かしたり、色を付けたり、文字以外のものを表示したりと、やることがたくさんです。

もちろん今回のゲームも「物理演算」を使いますので、「重力」「慣性」「弾性力」「密度」など、

たくさんの実装をしなければなりません。

Webページでこのような「動き」のある処理をするには「JavaScript」を使います。

JavaScritpはHTMLの「Scriptタグ」の中に書きます。

先ほどコピーしたHTMLからScriptタグを見つけて、以下のように中身に処理を書いてみましょう。

※練習のためコピペせずに自分で入力してみましょう。

<script> 
    const width = 1200;
    const height = 1400;
</script>

JavaScritpはこのように書きます。

ちなみに今書いたコードは「変数」を使っているコードです。

今回のゲームのために、背景のサイズを決めるためのものです。

<script> 
 // 背景を描画する
    // 全体のサイズを指定
    const width = 1200;
    const height = 1400;
</script>

2.2 ライブラリを使おう

変数は便利ですが、変数を作ったからといって勝手に背景ができるわけではありません。

ではどうすればよいかというと「描画」する処理が必要になります。

今変数で、「1200×1400の背景を作るぞ」と決めている状態ですので、「実際に描くぞ」が必要なわけですね。

そして今回は、「描画」は「ライブラリ」を使って行っていきます。

「ライブラリ」とは、便利な機能をまとめたパックのようなものです。

例えば、どこかのとても賢い人が「物理演算の計算プログラムを思いついたぞ」と言い、実際にプログラムを作ったとします。

この人は太っ腹だったので、このプログラムを誰でも使えるようにしてあげよう、と思いました。

ですが、口で伝えても仕方ないので、プログラムそのものを共有できる仕組みがあればよい、と考えました。

これが「ライブラリ」です。

今回はどこかの誰かが作った「物理演算と描画のライブラリ」を使って、ゲームを完成させます。

それでは、JavaScriptに以下を追加していきましょう。

  <script>
    // 背景を描画する
    // 全体のサイズを指定
    const width = 1200;
    const height = 1400;

    // ライブラリでレンダリングの準備
    const render = Render.create({
      element: document.getElementById('game-container'),
      engine: engine,
      options: { width, height, wireframes: false, background: '#f4f4f4' }
    });

    // 世界(world)に全ての物体(地面、鳥、バネ、マウス制約、箱の束)を追加
    Composite.add(world, []);

  </script>

3. ハコを作ろう

3.1 ハコのデザインを考えよう

背景ができたら、次はハコを作ってみましょう。

完成形ではたくさんのハコが描画されていました。

まずはハコのデザインをきめなければなりませんね。

・ハコのサイズ
・面の色
・線の色
・線の太さ

を以下のように決めて書いてみましょう。

<script>
    // 背景を描画する
    // 全体のサイズを指定
    const width = 1200;
    const height = 1400;
    // ライブラリでレンダリングの準備
    const render = Render.create({
      element: document.getElementById('game-container'),
      engine: engine,
      options: { width, height, wireframes: false, background: '#f4f4f4' }
    });

    // ハコを描画する
    // 1. デザインを考える
    const size = 40;    // 箱のサイズ
    const men_color = '#fb8c00';
    const sen_color = '#ef6c00';
    const futosa = 1;

    // 世界(world)に全ての物体(地面、鳥、バネ、マウス制約、箱の束)を追加
    Composite.add(world, []);
  </script>

3.2 配列を使おう

デザインができたので、後はこのハコを並べればよいだけですね。

同じハコなので、一見簡単そうに見えますよね。

しかし「10個ハコを置きたい」と思ったら、実際に10個処理をかかなければなりません。

コンピュータは指示したようにしか動かないので、10個ほしいということをプログラム上でも明確に記述する必要があるんですね。

とはいえ、10個の処理を大量に書くのは大変です。

そこで今回は「配列」を使います。

配列とは、複数の要素を保持しておくことができるものです。

変数の複数バージョンみたいなものです。

配列は以下のように書きます。

const 配列名 = [1,2,3,4]

では実際に使ってみましょう。

    // 2. ハコの配置を考える(0が空白、1が箱)
    // 配列の上が実際の画面の上部、配列の下が画面の下部になります
    const layout = [
      [0, 0, 1, 0, 0], // 1段目(上)
    ];

3.3 ネストしよう

「ネスト」とは配列を「入れ子構造」にすることです。

簡単に言うと、配列の中に配列を入れる、ということです。

ゲームには座標があります。座標はタテとヨコなので、2次元データです。

ですが、通常の配列だと、1次元しか表現できません。

そこで、配列をネストすることで、2次元を表現します。

では実際に書いてみましょう。

    // 2. ハコの配置を考える(0が空白、1が箱)
    // 配列の上が実際の画面の上部、配列の下が画面の下部になります
    const layout = [
      [0, 0, 1, 0, 0], // 1段目(上)
      [0, 0, 1, 0, 0], // 2段目
      [0, 1, 1, 0, 0], // 3段目
      [0, 1, 1, 1, 0], // 4段目
      [1, 1, 1, 1, 1]  // 5段目(下)
    ];

3.4 ハコを描画しよう

ハコの配列は2次元で作れましたが、まだ画面に表示されませんね。

それは「描画」をまだ行っていないからです。

背景を作る時に、描画の処理を少し書いたと思います。

それと同じように書く必要があります。

配列を効率的に処理するには、「繰り返し処理」を使います。

なんと「繰り返し処理」もネストすることができます。

なので、ネストされた配列を、ネストした繰り返し処理で「描画」する、ということなんですね。

では実際に書いてみましょう。

  <script>
    // 背景を描画する
    // 全体のサイズを指定
    const width = 1200;
    const height = 1400;
    // ライブラリでレンダリングの準備
    const render = Render.create({
      element: document.getElementById('game-container'),
      engine: engine,
      options: { width, height, wireframes: false, background: '#f4f4f4' }
    });

    // ハコを描画する
    // 1. デザインを考える
    const size = 40;    // 箱のサイズ
    const men_color = '#fb8c00';
    const sen_color = '#ef6c00';
    const futosa = 1;
    // 2. ハコの配置を考える(0が空白、1が箱)
    // 配列の上が実際の画面の上部、配列の下が画面の下部になります
    const layout = [
      [0, 0, 1, 0, 0], // 1段目(上)
      [0, 0, 1, 0, 0], // 2段目
      [0, 1, 1, 0, 0], // 3段目
      [0, 1, 1, 1, 0], // 4段目
      [1, 1, 1, 1, 1]  // 5段目(下)
    ];
    // 3.配置する位置を決める
    const startX = 900; // 左端の基準座標
    const startY = 600; // 上端の基準座標
    // 4. レイアウト配列をループで回す
    const hako = [];
    layout.forEach((row, r) => {
      row.forEach((cell, c) => {
        // 値が 1 の場合のみオブジェクトを作成
        if (cell !== 1) return
        // ハコの配列に追加します
        hako.push(Bodies.rectangle(
          startX + c * size, startY + r * 45, size, size,
          { render: { fillStyle: men_color, strokeStyle: sen_color, lineWidth: futosa } }
        ));
      });
    });

    // 世界(world)に全ての物体(地面、鳥、バネ、マウス制約、箱の束)を追加
    Composite.add(world, [...hako]);

  </script>

最後の部分を

Composite.add(world, [...hako]);

にするのを忘れないようにしましょう。

4. 地面を作ろう

ここまでで、ゲームのための技術はほとんど紹介できました。

ここからは、実際にコードを見ながら、どんどん書き写す練習をしていきましょう。

コードを書く➡実行する➡コードを書く➡実行する

を繰り返すことで、着実に喪についていきますよ。

    // 地面を描画する
    const j_x = 600;
    const j_y = 1200;
    const j_haba = 1200;
    const j_takasa = 400;
    const j_color = '#333';
    // rectangle(x, y, width, height, [options]): 中心座標、幅、高さ、オプション
    const jimen = Bodies.rectangle(
      j_x, j_y, j_haba, j_takasa,
      { isStatic: true, render: { fillStyle: j_color } }
    );

    // 世界(world)に全ての物体(地面、鳥、バネ、マウス制約、箱の束)を追加
    Composite.add(world, [jimen, ...hako]);

これで、ハコが落ちずに地面に乗るようになりましたね。

5. トリを作ろう

次は、今回の主人公である「トリ」を作ります。

ですが、本物のトリの形を作るのは大変なので、今回は「丸」で表現していきましょう。

「丸」での表現を使うことは実際のゲーム開発でも行われています。

なぜかと言うと、ハコと違って中心から外側までの距離が一定なため「接触判定」に便利だからです。

    // トリを描画する
    const t_x = 200;
    const t_y = 850;
    const t_hankei = 15;
    const t_color = '#e53935';
    // circle(x, y, radius, [options]): 中心座標、半径、オプション(densityは密度)
    let tori = Bodies.circle(
      t_x, t_y, t_hankei,
      { density: 0.002, render: { fillStyle: t_color } }
    );

    // 世界(world)に全ての物体(地面、鳥、バネ、マウス制約、箱の束)を追加
    Composite.add(world, [jimen, tori, ...hako]);

落っこちてしまう、残念なトリですが、一応できたと思います。

6. バネを作ろう

トリをとばす方法を考えます。

トリを中心として、ひっぱってはなすと、とんでいく、という実装を考えます。

中心からどのくらいトリが離れているか?によって、とぶ力が変わるのが自然な気がします。

これは「ゴム鉄砲をとばす」のに似ていますね。つまり「弾性力」です。

今回はトリに「バネ」がくっついていると考え、実装していきましょう。

    // バネで接続する
    const bane = { x: 200, y: 850 };
    // Constraint.create(options): 2点間(または体)を繋ぐ制約。stiffnessは剛性/硬さ
    const b_color = '#333';
    const b_chikara = 0.05;
    const b_futosa = 2;

    // 世界(world)に全ての物体(地面、鳥、バネ、マウス制約、箱の束)を追加
    Composite.add(world, [jimen, tori, ...hako]);

これでバネができました。

7. トリとバネを合体させよう

さっき、バネを作ったのですが、実はまだトリとバネはくっついていません。

合体させるコードは以下なので、これも実装しましょう。

    // バネで接続する
    const bane = { x: 200, y: 850 };
    // Constraint.create(options): 2点間(または体)を繋ぐ制約。stiffnessは剛性/硬さ
    const b_color = '#333';
    const b_chikara = 0.05;
    const b_futosa = 2;
    //
    const gattai = Constraint.create({
      pointA: bane,
      bodyB: tori,
      stiffness: b_chikara,
      render: { strokeStyle: b_color, lineWidth: b_futosa }
    });

    // 世界(world)に全ての物体(地面、鳥、バネ、マウス制約、箱の束)を追加
    Composite.add(world, [jimen, tori, gattai, ...hako]);

これでトリとバネが合体しました。

トリを引っ張るとバネが伸びると思います。

8. トリを発射するイベントを作ろう

バネがトリと合体したのは良かったのですが、このままではトリが発射されません。

バネを伸ばした後、なんらかのきっかけで、バネとの合体を解除する必要がありますね。

このような、きっかけで発生する事象のことを「イベント」と言います。

例えば、クリックしたときに処理させたいなら「クリックイベント」、

カーソルが当たったときに処理させたいなら「フォーカスイベント」などと言います。

それでは、トリを発射するイベントを実装してみましょう。

    // マウスにイベントを設定する
    // Mouse.create(element): マウスイベントを取得する要素を指定
    const mouse = Mouse.create(render.canvas);
    // 発射フラグ用の変数を定義
    let isFiring = false;
    // MouseConstraint.create(engine, [options]): マウスで物体を操作するための制約
    const mouseConstraint = MouseConstraint.create(engine, {
      mouse: mouse,
      constraint: { render: { visible: false } }
    });
    // マウスのドラッグ終了(手を放した)時に、鳥を掴んでいれば発射フラグをON
    Events.on(mouseConstraint, 'enddrag', (e) => { if (e.body === tori) isFiring = true; });

    // エンジンの更新ごと(毎フレーム)に実行される処理
    Events.on(engine, 'afterUpdate', () => {
      // 発射中かつ鳥が発射点を越えたら、ゴムの制約を解除して自由に飛ばす
      if (isFiring && tori.position.x > bane.x + 10) {
        gattai.bodyB = null; // 接続を解除
        gattai.render.visible = false;
        isFiring = false;
      }
    });

    // 世界(world)に全ての物体(地面、鳥、バネ、マウス制約、箱の束)を追加
    Composite.add(world, [jimen, tori, gattai, mouseConstraint, ...hako]);

これで今回のゲームは完成になります。

正しくトリが発射されるか確認し、もし上手くうごかなければエラーを見て「デバック」してみましょう。

答えとなるコードはここまでのページを遡って確認できます。

9. 完成したコード

今回完成したコードは以下になります。

もし上手くデバックできなかった場合は、以下と見比べて自分のコードの間違いを探してみましょう。

<!DOCTYPE html>
<html lang="ja">

<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Simple Physics tori</title>
  <!-- Pico.css CDN -->
  <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@picocss/pico@2/css/pico.min.css">
  <!-- Matter.js CDN -->
  <script src="https://cdnjs.cloudflare.com/ajax/libs/matter-js/0.19.0/matter.min.js"></script>

</head>

<body class="container">

  <header>
    <hgroup>
      <h1>tobutori</h1>
    </hgroup>
  </header>

  <main>
    <div id="game-container"></div>
    <button class="secondary" onclick="location.reload()">リセット</button>
  </main>

  <script src="https://cdn.jsdelivr.net/gh/takahiro-inoway/workshop@main/birdgame_1/workshop_1.js"></script>

  <script>
    // 背景を描画する
    // 全体のサイズを指定
    const width = 1200;
    const height = 1400;
    // ライブラリでレンダリングの準備
    const render = Render.create({
      element: document.getElementById('game-container'),
      engine: engine,
      options: { width, height, wireframes: false, background: '#f4f4f4' }
    });

    // ハコを描画する
    // 1. デザインを考える
    const size = 40;    // 箱のサイズ
    const men_color = '#fb8c00';
    const sen_color = '#ef6c00';
    const futosa = 1;
    // 2. ハコの配置を考える(0が空白、1が箱)
    // 配列の上が実際の画面の上部、配列の下が画面の下部になります
    const layout = [
      [0, 0, 1, 0, 0], // 1段目(上)
      [0, 0, 1, 0, 0], // 2段目
      [0, 1, 1, 0, 0], // 3段目
      [0, 1, 1, 1, 0], // 4段目
      [1, 1, 1, 1, 1]  // 5段目(下)
    ];
    // 3.配置する位置を決める
    const startX = 900; // 左端の基準座標
    const startY = 600; // 上端の基準座標
    // 4. レイアウト配列をループで回す
    const hako = [];
    layout.forEach((row, r) => {
      row.forEach((cell, c) => {
        // 値が 1 の場合のみオブジェクトを作成
        if (cell !== 1) return
        // ハコの配列に追加します
        hako.push(Bodies.rectangle(
          startX + c * size, startY + r * 45, size, size,
          { render: { fillStyle: men_color, strokeStyle: sen_color, lineWidth: futosa } }
        ));
      });
    });

    // 地面を描画する
    const j_x = 600;
    const j_y = 1200;
    const j_haba = 1200;
    const j_takasa = 400;
    const j_color = '#333';
    // rectangle(x, y, width, height, [options]): 中心座標、幅、高さ、オプション
    const jimen = Bodies.rectangle(
      j_x, j_y, j_haba, j_takasa,
      { isStatic: true, render: { fillStyle: j_color } }
    );

    // トリを描画する
    const t_x = 200;
    const t_y = 850;
    const t_hankei = 15;
    const t_color = '#e53935';
    // circle(x, y, radius, [options]): 中心座標、半径、オプション(densityは密度)
    let tori = Bodies.circle(
      t_x, t_y, t_hankei,
      { density: 0.002, render: { fillStyle: t_color } }
    );

    // バネで接続する
    const bane = { x: 200, y: 850 };
    // Constraint.create(options): 2点間(または体)を繋ぐ制約。stiffnessは剛性/硬さ
    const b_color = '#333';
    const b_chikara = 0.05;
    const b_futosa = 2;
    //
    const gattai = Constraint.create({
      pointA: bane,
      bodyB: tori,
      stiffness: b_chikara,
      render: { strokeStyle: b_color, lineWidth: b_futosa }
    });

    // マウスにイベントを設定する
    // Mouse.create(element): マウスイベントを取得する要素を指定
    const mouse = Mouse.create(render.canvas);
    // 発射フラグ用の変数を定義
    let isFiring = false;
    // MouseConstraint.create(engine, [options]): マウスで物体を操作するための制約
    const mouseConstraint = MouseConstraint.create(engine, {
      mouse: mouse,
      constraint: { render: { visible: false } }
    });
    // マウスのドラッグ終了(手を放した)時に、鳥を掴んでいれば発射フラグをON
    Events.on(mouseConstraint, 'enddrag', (e) => { if (e.body === tori) isFiring = true; });

    // エンジンの更新ごと(毎フレーム)に実行される処理
    Events.on(engine, 'afterUpdate', () => {
      // 発射中かつ鳥が発射点を越えたら、ゴムの制約を解除して自由に飛ばす
      if (isFiring && tori.position.x > bane.x + 10) {
        gattai.bodyB = null; // 接続を解除
        gattai.render.visible = false;
        isFiring = false;
      }
    });

    // 世界(world)に全ての物体(地面、鳥、バネ、マウス制約、箱の束)を追加
    Composite.add(world, [jimen, tori, gattai, mouseConstraint, ...hako]);

  </script>

  <script src="https://cdn.jsdelivr.net/gh/takahiro-inoway/workshop@main/birdgame_1/workshop_2.js"></script>

</body>

</html>

10. アレンジしよう

ソースコードにはたくさんの変数がありましたね。

各変数はそれぞれ、大きさや色、太さなどを設定するためのものでした。

上記は自由に書き換えが可能ですね。

ぜひ、自分の好きなように作り替えて、オリジナルゲームにアレンジしてみましょう。

◆変更のオススメポイント

・ハコの色、大きさ、線の色、太さ

・トリの色、半径、密度

・地面の色

・背景の色

・★上級者向け★ハコの数、位置、配列の変更

・本講座の制作
CIT経営開発事務所

・監修
CIT経営開発事務所 代表
井上 隆寛(いのうえ・たかひろ)

IT・事業コンサルタント
IT・開発エンジニア
行政書士R6合格者未登録

大手システム開発会社にてSE兼Webデザイナーとして従事。2021年にコンサルタントとして独立し、企業に対するITコンサルティング・ソリューション導入支援事業を開始。2023年にはイベント企画・運営事業を新たに展開、2024年には行政書士試験に合格。現在はIT・AIコンサルティング、システム開発、エンターテイメントの3事業を柱に、企業の技術顧問や講師としてICT教育やプログラミング授業も手がける。

公開中のリスキリング体験授業・学習教材

リスキリングに役立つ学習教材や体験授業を公開しています。ご自身の学習にお役立てください。

  • 2015年

    創業

  • 120名

    従業員数

  • 2.3M

    月間UU

  • 1,800戸

    管理戸数