JavaScript
HTML5
CSS3
codepen

【Web開発初心者向け】codepenで作るFlappy Bird

More than 1 year has passed since last update.

この記事はWeb開発の経験がない方に向けて作成した講義用の資料です。

用意するもの

  • codepenのアカウント
  • ブラウザ

codepenは登録済みであることを前提に話を進めます。

製作するもの

今回はFlappy Birdといわれるゲームを自作していきます。
クリックやタップで鳥が飛び上がり壁の隙間をすり抜けていくゲームです。

デモはこちら

製作の流れ

  1. 必要な画像の準備
  2. 鳥が落ちるようにする
  3. クリックで飛び上がるようにする
  4. 壁を用意する
  5. 壁を動かす
  6. 鳥と壁が衝突したかを判定する
  7. ゲームにスタートボタンをつける

これでなんとかゲームとして遊べるレベルになります。
では早速codepenにアクセスして製作を開始しましょう。

作り方

必要な画像の準備

自分の製作物としてネット上に公開されるものにたいしては著作権に留意しなければなりませんね。
今回私はいらすとやから素材をお借りしました。
このゲームで必要となる画像の素材としては以下のものになります。

  • 背景

ただし、背景と壁に関してはパターンで使えるようなテクスチャ画像を用意してください。
用意ができたらひとまず見えるようにして確認してみましょう。

背景を表示する

まずは背景画像を適用します。
これはCSSの編集のみで対応できます。

CSS
body {
  background-image: url("背景画像のURL");
}

これで背景が用意した画像で埋め尽くされるようになりましたでしょうか。
背景画像は並べて表示されるためパターン画像にしていないと画像と画像の境目が気になりますね。

鳥を表示する

次は鳥を表示します。
HTMLにimgタグを追加しCSSでは画面の大きさによって画像の大きさが変化するように高さを画面全体の高さに比例するように設定します。
さらに鳥の画像はあとで動かすことになるので位置を絶対位置にしておきます。

HTML
<img id="bird" src="鳥の画像のURL">
CSS(追加分)
#bird {
  height: 5%;
  position: absolute;
  left: 10%;
}

壁を表示する

壁はdivタグにenemyというクラスを割り振ることで表示していきます。
クラスを使用することで複数のタグにたいして同じCSSを適用することが可能になります。
まずは画面の中央辺りに仮で壁を用意します。

HTML(追加分)
<diiv class="enemy"></div>
CSS(追加分)
.enemy {
  background-image: url("壁の画像のURL");
  width: 10%;
  height: 100%;
  position: absolute;
  top: 50%;
  left: 50%;
}

壁も背景画像と同様に繰り返して使用されるためパターン画像になっていないと境目が気になりますね。
enemyというクラスを割り振ると今は必ず中央になってしまいますが、CSS内のleftとtopの値を操作することで壁を動かしてみます。
50%を40%や60%に変えてどのように動くか確認してみて下さい。

鳥が落ちるようにする

では次に鳥が落ちていくのに必要な処理を書いていきます。
今回は機能が多いのでJavaScriptのFunctionを利用して細かく機能ごとに分けていきます。

初期化のための関数init

まずこの処理で必要な変数は以下の通りになります。

  • 鳥を表すimgタグ
  • 画面全体の高さ
  • 鳥にかかる加速度
  • 鳥が落ちている今の速度
  • 鳥のいる高さ

これらをの変数を用意するための関数をinitという名前で用意します。
initという関数の機能は"初期化"とします。

JS
function init() {
  bird = document.getElementById("bird");
  maxY = window.innerHeight - bird.height;
  ay = 0.4;
  vy = 0;
  y = 0;
  bird.style.top = y + "px";
  start();
}

最後の2行で鳥の位置を初期化し次で作成するゲーム全体の処理を行う関数startを呼んでいます。

ゲーム全体の処理を行う関数start

この関数では鳥が落下したり壁との衝突判定を開始するなど、ゲーム全体の流れを制御します。
moveBirdは後で作成する鳥を飛ばすための関数です。

JS(追加分)
function start(){
  moveBirdInterval = setInterval(moveBird, 20);
}

ゲームが終了した時の処理を行う関数end

この関数では鳥が落下したり壁との衝突判定を開始するなど、ゲーム全体の流れを制御します。

JS(追加分)
function end(){
  clearInterval(moveBirdInterval);
}

鳥を動かす関数moveBird

鳥が落ちる部分を関数moveBirdとして定義します。
鳥の落下に関しては簡単に自由落下の計算を行います。

JS(追加分)
function moveBird() {
  vy += ay;
  y += vy;
  if (y < 0) {
    y = 0;
  } else if (y > maxY) {
    end();
  }
  bird.style.top = y + "px";
}

この関数では鳥を動かすと同時に天井にぶつかったときにそのままめり込まない処理と地面に落ちたときに繰り返しの処理を止める関数endを呼び出すようにしています。

initとstartを呼び出す

このままでは関数を用意しただけで呼び出されないため呼びます。
現在JSは以下のようになっているはずです。

JS
function init(){
  /* 省略 */
}

function start(){
  /* 省略 */
}

function end(){
  /* 省略 */
}

function moveBird(){
  /* 省略 */
}

JSが読み込まれたときにinitを実行したいためまず最初にinitを実行するように追記します。

JS
init();

function init(){
  /* 省略 */
}

function start(){
  /* 省略 */
}

function end(){
  /* 省略 */
}

function moveBird(){
  /* 省略 */
}

これで鳥が勝手に落ちていくようになっているはずです。

クリックで飛び上がるようにする

つぎにクリックで飛び上がるようにしていきます。

飛び上がるための関数fly

まずは関数を用意します。
単純に速度を上向きに変更します。
topという属性は画面の上端からの距離を示しているため下向きに正となる座標系になっていることに気をつけましょう。

JS(追加分)
function fly() {
  vy = -10;
}

そしてこれを関数initの中でクリックと結びつけます。

JS(一部抜粋)
function init(){
  document.addEventListener("click", fly);

  /* 省略 */
}

これでクリックしたときに鳥が飛び上がるようになりました。

壁を用意する

壁を示すdivタグはもう作ってありますね。

壁を生成する関数genEnemy

壁を示すタグを自動で生成するような関数を用意します。
生成する際に壁の隙間がランダムに変わるようにMath.random()を使用します。
Math.random()は0から1までのランダムな数を返してくるため、ここではそれを利用して20から80までの乱数posを生成します。
そして壁は隙間を作るため上の壁と下の壁をそれぞれ生成した乱数posをもとに位置を決めています。
次に壁のleftmaxXにすることで壁の左側の端を画面の右側に合わせます。
insertBeforeという関数で用意したdivタグをHTMLの先頭に追加しています。
HTMLの先頭に追加していくのは表示したときに壁が一番後ろに表示されるようにするためです。

JS(追加分)
function genEnemy() {
  var pos = 20 + Math.random() * 60;
  var enemyTop = document.createElement("div");
  var enemyBottom = document.createElement("div");
  enemyTop.className = "enemy";
  enemyBottom.className = "enemy";
  enemyTop.style.bottom = pos + 10 + "%";
  enemyBottom.style.top = (100 - pos) + 10 + "%";
  enemyTop.style.left = maxX + "px";
  enemyBottom.style.left = maxX + "px";
  document.body.insertBefore(enemyTop, document.body.firstChild);
  document.body.insertBefore(enemyBottom, document.body.firstChild);
}

まずこの関数ではmaxXという画面の幅を保持する変数を用意する必要があるため関数initでmaxXを用意します。

JS(一部抜粋)
function init(){
    maxX = window.innerWidth;

  /* 省略 */
}

次にこの関数を関数startで2秒ごとに呼び出すようにします。

JS(一部抜粋)
function start() {
  moveBirdInterval = setInterval(moveBird, 20); // この行はすでに書いてあるはずです
  genEnemyInterval = setInterval(genEnemy, 2000);
}

次に関数endで繰り返しを止めるようにします。

JS(一部抜粋)
function end() {
  clearInterval(moveBirdInterval); // この行はすでに書いてあるはずです
  clearInterval(genEnemyInterval);
}

最後に最初にHTMLに記述した壁を示すdivタグを消してHTMLを以下と同じ構成にします。

HTML
<img id="bird" src="鳥の画像のURL">

これで一応自動的に生成はされるはずですが壁が動いていないので確認できませんね。
動かしていきましょう。

壁を動かす

壁にはenemyというクラスが割り振られているため、それらのすべてに対して一定時間ごとに少しずつ左にずらしていきます。

壁を動かす関数moveEnemy

それでは関数を用意します。
この関数ではまず初めにenemyというクラスのタグたちを取得します。
そしてそのすべてに対してleftの値を10ずつ減らすような関数になります。
ただし、画面の外に出てしまった場合には削除するようにしています。

JS(追加分)
function moveEnemy() {
  var enemies = document.getElementsByClassName("enemy");
  for (var i = 0; i < enemies.length; i++) {
    var left = parseInt(enemies[i].style.left.replace(/px/, ""));
    left -= 10;
    if (left < -enemies[i].getBoundingClientRect().width) {
      enemies[i].remove();
    } else {
      enemies[i].style.left = left + "px";
    }
  }
}

そしてこれを関数startで呼び出します。

JS(一部抜粋)
function start() {
  moveBirdInterval = setInterval(moveBird, 20); // この行はすでに書いてあるはずです
  genEnemyInterval = setInterval(genEnemy, 2000); // この行はすでに書いてあるはずです
  moveEnemyInterval = setInterval(moveEnemy, 20);
}

次に関数endで繰り返しを止めるようにします。

JS(一部抜粋)
function end() {
  clearInterval(moveBirdInterval); // この行はすでに書いてあるはずです
  clearInterval(genEnemyInterval); // この行はすでに書いてあるはずです
  clearInterval(moveEnemyInterval);
}

背景を動かす関数moveBackground

壁だけが動いているので背景も動かします。

JS(追加分)
function moveBackground(){
  bgX -= 10;
  document.body.style.backgroundPosition = bgX + "px";
}

この関数では背景の位置を保持する変数bgXが必要となるので関数initで初期化します。

JS(一部抜粋)
function init(){
  bgX = 0;

  /* 省略 */
}

次に関数startで呼び出します。

JS(一部抜粋)
function start() {
  moveBirdInterval = setInterval(moveBird, 20); // この行はすでに書いてあるはずです
  genEnemyInterval = setInterval(genEnemy, 2000); // この行はすでに書いてあるはずです
  moveEnemyInterval = setInterval(moveEnemy, 20); // この行はすでに書いてあるはずです
  moveBackgroundInterval = setInterval(moveBackground, 20);
}

次に関数endで繰り返しを止めるようにします。

JS(一部抜粋)
function end() {
  clearInterval(moveBirdInterval); // この行はすでに書いてあるはずです
  clearInterval(genEnemyInterval); // この行はすでに書いてあるはずです
  clearInterval(moveEnemyInterval); // この行はすでに書いてあるはずです
  clearInterval(moveBackgroundInterval);
}

これで背景も壁と一緒に動くようになりました。
鳥も飛び上がるようになりいよいよゲームらしくなってきました。

鳥と壁が衝突したかを判定する

今のままでは壁に当たっても何にもならないので衝突判定を行います。

衝突判定を行う関数checkCollision

まずは関数を用意します。
moveEnemyと同じようにenemyというクラスのついたタグを取得しそれぞれに対して鳥と重なっているかを判定しています。
重なっている場合は関数endを呼び出します。
重なっている判定は少しややこしいかもしれないですがじっくり読むと単純であることに気が付かれると思います。
if文が二重に使われています。
一つ目では横方向の重なりを
二つ目では縦方向の重なりを
判定しています。

JS(追加分)
function checkCollision() {
  var enemies = document.getElementsByClassName("enemy");
  var birdRect = bird.getBoundingClientRect();
  for (var i = 0; i < enemies.length; i++) {
    var enemyRect = enemies[i].getBoundingClientRect();
    if (enemyRect.left < birdRect.right && birdRect.left < enemyRect.right) {
      if (enemyRect.top < birdRect.bottom && birdRect.top < enemyRect.bottom) {
        end();
      }
    }
  }
}

そして衝突の判定は鳥を動かすたびに行うため関数moveBirdの最後に関数checkCollisionを呼び出します。

JS(一部抜粋)
function moveBird() {
  /* 省略 */

  checkCollision();
}

これで壁にぶつかると止まるようになりました。
ここまで来るといよいよゲームらしくなってきましたね。

ゲームにスタートボタンをつける

さて、これではゲームオーバーになるたびにページをリロードしなければもう一度遊ぶことができません。
スタートボタンを設置してゲームオーバーしてもリトライできるようにしましょう。

画像を用意する

スタートボタンは画像を用います。
適当な画像を用意しましょう。
デモでは他と同様にいらすとやからお借りしました。

ボタンを表示する

imgタグを用います。
HTMLの最後に追記しましょう。
クリックされたときに関数initを実行するように設定します。

HTML
<img id="bird" src="鳥の画像のURL"> // この行はすでに書いてあるはずです
<img id="startBtn" onclick="init()" src="スタートボタンの画像のURL">

次にCSSで画面の中央に配置します。
上下左右からの距離を0にして余った部分は余白(margin)として設定します。

CSS(追加分)
#startBtn {
  width: 80%;
  position: absolute;
  top: 0;
  left: 0;
  right: 0;
  bottom: 0;
  margin: auto;
}

関数initでボタンを非表示にする

遊んでいるときは邪魔なのでボタンを隠します。

JS(一部抜粋)
function init(){
  document.getElemtntById("startBtn").style.display = "none";

  /* 省略 */
}

関数endでボタンを表示する

終わったら表示します。

JS(一部抜粋)
function end() {
  document.getElemtntById("startBtn").style.display = "block";
  clearInterval(moveBirdInterval); // この行はすでに書いてあるはずです
  clearInterval(genEnemyInterval); // この行はすでに書いてあるはずです
  clearInterval(moveEnemyInterval); // この行はすでに書いてあるはずです
}

最後に

最後まで読んでいただきありがとうございます。
デモではさらにここにいくつかの機能を盛り込んでいます。
独自の機能をいろいろつけて楽しんでいただけたらと思います。