プログラミング

Web Development for Beginners を読む:レッスン18, 19, 20

投稿日:

web development

1. はじめに

Web Development for Beginners の3つのレッスンをやっていきます。

スペースゲーム(シューティングゲーム)を作成するレッスンの後半にあたります。

※ 本記事は、「個人的なメモ」という意味合いが強いです。

2. Part 4: レーザーと衝突検知を追加する

ソースコード:app.js

(1) 衝突検知のための「オブジェクトの四角形表現を返す関数」を用意する

rectFromGameObject() {
  return {
    top: this.y,
    left: this.x,
    bottom: this.y + this.height,
    right: this.x + this.width,
  };
}

(2) 衝突検知の判定関数を追加する

引数で渡された2つの四角形に重なりがあるか判定する。

function intersectRect(r1, r2) {
  return !(
    r2.left > r1.right ||
    r2.right < r1.left ||
    r2.top > r1.bottom ||
    r2.bottom < r1.top
  );
}

(3) レーザー発射機能を追加する

(i) キー・イベントのメッセージを追加する

KEY_EVENT_SPACE: "KEY_EVENT_SPACE",
COLLISION_ENEMY_LASER: "COLLISION_ENEMY_LASER",
COLLISION_ENEMY_HERO: "COLLISION_ENEMY_HERO",

(ii) スペース・キーを処理する。

  } else if(evt.keyCode === 32) {
    eventEmitter.emit(Messages.KEY_EVENT_SPACE);
  }

(iii) リスナーを追加する。

スペース・キーが押された場合のリスナー

eventEmitter.on(Messages.KEY_EVENT_SPACE, () => {
 if (hero.canFire()) {
   hero.fire();
 }

敵がレーザーと衝突した場合のリスナー

eventEmitter.on(Messages.COLLISION_ENEMY_LASER, (_, { first, second }) => {
  first.dead = true;
  second.dead = true;
})

(iv) レーザーを動かす。

  class Laser extends GameObject {
  constructor(x, y) {
    super(x,y);
    (this.width = 9), (this.height = 33);
    this.type = 'Laser';
    this.img = laserImg;
    let id = setInterval(() => {
      if (this.y > 0) {
        this.y -= 15;
      } else {
        this.dead = true;
        clearInterval(id);
      }
    }, 100)
  }
}

(v) 衝突を処理する。

function updateGameObjects() {
  const enemies = gameObjects.filter(go => go.type === 'Enemy');
  const lasers = gameObjects.filter((go) => go.type === "Laser");
  // laser hit something
  lasers.forEach((l) => {
    enemies.forEach((m) => {
      if (intersectRect(l.rectFromGameObject(), m.rectFromGameObject())) {
        eventEmitter.emit(Messages.COLLISION_ENEMY_LASER, {
          first: l,
          second: m,
        });
      }
    });
  });

  gameObjects = gameObjects.filter(go => !go.dead);
} 

(vi) クールダウンのクラスを定義する。

class Hero extends GameObject {
 constructor(x, y) {
   super(x, y);
   (this.width = 99), (this.height = 75);
   this.type = "Hero";
   this.speed = { x: 0, y: 0 };
   this.cooldown = 0;
 }
 fire() {
   gameObjects.push(new Laser(this.x + 45, this.y - 10));
   this.cooldown = 500;

   let id = setInterval(() => {
     if (this.cooldown > 0) {
       this.cooldown -= 100;
     } else {
       clearInterval(id);
     }
   }, 200);
 }
 canFire() {
   return this.cooldown === 0;
 }
}

3. Part 5: スコアとライフの処理

ソースコード:app.js

(1) 必要なアセットを solution/assets/ フォルダーから your-work フォルダーにコピーします。

lifeImg = await loadTexture("assets/life.png");

(2) lifeImg をアセットのリストに追加します。

let heroImg, 
... 
lifeImg, 
... 
eventEmitter = new EventEmitter();

(3) 変数を追加します。

(4) updateGameObjects() 関数を拡張します。

enemies.forEach(enemy => { 
  const heroRect = hero.rectFromGameObject(); 
  if (intersectRect(heroRect, enemy.rectFromGameObject())) { 
    eventEmitter.emit(Messages.COLLISION_ENEMY_HERO, { enemy }); 
  } 
})

(5) life と points を追加します。

(i) 変数を初期化します。

this.life = 3;
this.points = 0;

(ii) 画面に変数を描画します。

function drawLife() { 
  // TODO, 35, 27 
  const START_POS = canvas.width - 180; 
  for(let i=0; i < hero.life; i++ ) { 
    ctx.drawImage( 
      lifeImg,  
      START_POS + (45 * (i+1) ),  
      canvas.height - 37); 
  } 
} 
function drawPoints() { 
  ctx.font = "30px Arial"; 
  ctx.fillStyle = "red"; 
  ctx.textAlign = "left"; 
  drawText("Points: " + hero.points, 10, canvas.height-20); 
} 
function drawText(message, x, y) { 
  ctx.fillText(message, x, y); 
}

(iii) ゲームループにメソッドを追加します。

drawPoints();
drawLife();

(6) ゲームルールを実装します。

(i) ヒーローと敵の衝突ごとに、ライフを差し引きます。

decrementLife() { 
  this.life--; 
  if (this.life === 0) { 
    this.dead = true; 
  } 
}

(ii) 敵に当たるレーザーごとに、ゲームのスコアを100ポイント増やします。

incrementPoints() { 
    this.points += 100; 
}

これらの関数を衝突イベントエミッターに追加します。

eventEmitter.on(Messages.COLLISION_ENEMY_LASER, (_, { first, second }) => { 
   first.dead = true; 
   second.dead = true; 
   hero.incrementPoints(); 
}) 
eventEmitter.on(Messages.COLLISION_ENEMY_HERO, (_, { enemy }) => { 
   enemy.dead = true; 
   hero.decrementLife(); 
});

4. Part 6: 終了と再開

ソースコード:app.js

(1) 終了状態を追跡します。

function isHeroDead() { 
  return hero.life <= 0; 
} 
function isEnemiesDead() { 
  const enemies = gameObjects.filter((go) => go.type === "Enemy" && !go.dead); 
  return enemies.length === 0; 
}

(2) メッセージハンドラにロジックを追加します。

eventEmitter.on(Messages.COLLISION_ENEMY_LASER, (_, { first, second }) => { 
    first.dead = true; 
    second.dead = true; 
    hero.incrementPoints(); 
    if (isEnemiesDead()) { 
      eventEmitter.emit(Messages.GAME_END_WIN); 
    } 
}); 
eventEmitter.on(Messages.COLLISION_ENEMY_HERO, (_, { enemy }) => { 
    enemy.dead = true; 
    hero.decrementLife(); 
    if (isHeroDead())  { 
      eventEmitter.emit(Messages.GAME_END_LOSS); 
      return; // loss before victory 
    } 
    if (isEnemiesDead()) { 
      eventEmitter.emit(Messages.GAME_END_WIN); 
    } 
}); 
eventEmitter.on(Messages.GAME_END_WIN, () => { 
    endGame(true); 
}); 
   
eventEmitter.on(Messages.GAME_END_LOSS, () => { 
  endGame(false); 
});

(3) 新しいメッセージタイプを追加します。

GAME_END_LOSS: "GAME_END_LOSS", 
GAME_END_WIN: "GAME_END_WIN",

(4) 選択したボタンを押すだけでゲームを再開するコードコードを追加します。

(i) Enterキーの押下をリッスンします。

else if(evt.key === "Enter") { 
    eventEmitter.emit(Messages.KEY_EVENT_ENTER); 
}

(ii) 再起動メッセージを追加します。

KEY_EVENT_ENTER: "KEY_EVENT_ENTER",

(5) ゲームルールを実装します。

(i) プレイヤーの勝利条件。

a. まず、displayMessage() 関数を作成します。

function displayMessage(message, color = "red") { 
  ctx.font = "30px Arial"; 
  ctx.fillStyle = color; 
  ctx.textAlign = "center"; 
  ctx.fillText(message, canvas.width / 2, canvas.height / 2); 
}

b.  endGame() 関数を作成します。

function endGame(win) { 
  clearInterval(gameLoopId); 
  // set a delay so we are sure any paints have finished 
  setTimeout(() => { 
    ctx.clearRect(0, 0, canvas.width, canvas.height); 
    ctx.fillStyle = "black"; 
    ctx.fillRect(0, 0, canvas.width, canvas.height); 
    if (win) { 
      displayMessage( 
        "Victory!!! Pew Pew... - Press [Enter] to start a new game Captain Pew Pew", 
        "green" 
      ); 
    } else { 
      displayMessage( 
        "You died !!! Press [Enter] to start a new game Captain Pew Pew" 
      ); 
    } 
  }, 200)   
}

(ii) ロジックを再起動します。

a. resetGame() 関数を作成します。

function resetGame() { 
  if (gameLoopId) { 
    clearInterval(gameLoopId); 
    eventEmitter.clear(); 
    initGame(); 
    gameLoopId = setInterval(() => { 
      ctx.clearRect(0, 0, canvas.width, canvas.height); 
      ctx.fillStyle = "black"; 
      ctx.fillRect(0, 0, canvas.width, canvas.height); 
      drawPoints(); 
      drawLife(); 
      updateGameObjects(); 
      drawGameObjects(ctx); 
    }, 100); 
  } 
}

(iii) initGame() でゲームをリセットすために、eventEmitterにリスナーを追加します。

eventEmitter.on(Messages.KEY_EVENT_ENTER, () => { 
  resetGame(); 
});

(iv) EventEmitter に clear() 関数を追加します。

clear() { 
  this.listeners = {}; 
}

5. まとめ

スペースゲームの開発において、ベースとなった手法は以下の2点でした。

  1. クラスと継承でゲームオブジェクトを表す
  2. Pub/Subパターン (EventEmitterクラス) を使って動作を実行する

その上でゲームとして具体的に必要な、

  • キャラクターを動かす
  • 衝突検知
  • スコア
  • ライフ

といった機能を順番に追加していきました。

実践的な内容なので、今後ゲームを作る予定のある人にとってはかなり参考になるのではないかと思います。

📂-プログラミング

執筆者:labo


comment

メールアドレスが公開されることはありません。 が付いている欄は必須項目です

関連記事

Web Programming

サーバーからブラウザを通じてデスクトップ通知する方法(Push API を利用)

Push API を使ってサーバーからブラウザにメッセージを送る方法について説明しています。

Web Programming

Service Worker の状態変化を検証するためのウェブページを作りました

Service Worker の状態変化を検証するためのウェブページを作りました。

CSS Flexible Box Layout Module のサンプルページを作りました

CSS Flexible Box Layout Module のサンプルページを作りました。 目次1. スクリーンショット2. デモページ3. 内容4. ソースコード5. 参考情報 1. スクリーンシ …

web development

Web Development for Beginners を読む:レッスン1

目次1. はじめに2. Web Development for Beginners の進め方3. レッスン1「Introduction to Programming Languages and Too …

webpack 3 を使ったウェブページ開発手順

webpack 3 を使って、簡単なウェブページを開発する手順を紹介します。あくまで一つの例です。 1つ1つ細かい説明はできていませんが、「だいたいこんなふうにして作ることができますよ」ということが伝 …