プログラミング

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

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

関連記事

no image

プログラミングができると便利である実例

バスにジャストで乗りこむ技術|こんぴゅ|note

JavaScript でスロットマシーンを作ってみました

JavaScript でスロットマシーンを作ってみました。 目次1. スクリーンショット2. デモページ3. 内容4. ソースコード5. 参考情報 1. スクリーンショット スクリーンショット 2. …

Web Programming

ソフトウェアにおける日付・時刻フォーマット

目次ソフトウェアにおける日付・時刻フォーマット参考情報ISO 8601RFC 5322RFC 7231Common Log Format ソフトウェアにおける日付・時刻フォーマット ソフトウェア(特に …

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

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

Redux

Redux の非同期処理サンプルページを作りました

Redux で非同期処理を行うサンプルページを作りました。 目次1. スクリーンショット2. デモページ3. 動作4. ソースコード5. 参考情報 1. スクリーンショット スクリーンショット 2. …