プログラミング

Web Development for Beginners を読む:レッスン15, 16, 17

投稿日:2020年12月17日 更新日:

web development

1. はじめに

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

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

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

2. Part1: イントロダクション

「継承」と「コンポジション」

ゲームの中で扱う各オブジェクトを表現する手法として、「継承」と「コンポジション」がある。それぞれの説明と違いが書いてある。

どちらを採用するかは好みの問題である。

プログラミングにおいて「継承」と「コンポジション」はよく比較される話であり、一般的には「継承」よりも「コンポジション」が好まれる印象がある。

Pub/Sub パターン

Pub/Sub パターンの利点は、動作のトリガーとなるイベントリスナーと、そのときに実行される処理とを疎結合にできることである。

//set up a message structure
const Messages = {
  HERO_MOVE_LEFT: 'HERO_MOVE_LEFT'
};
//invoke the eventEmitter you set up above
const eventEmitter = new EventEmitter();
//set up a hero
const hero = createHero(0,0);
//let the eventEmitter know to watch for messages pertaining to the hero moving left, and act on it
eventEmitter.on(Messages.HERO_MOVE_LEFT, () => {
  hero.move(5,0);
});

//set up the window to listen for the keyup event, specifically if the left arrow is hit, emit a message to move the hero left
window.addEventListener('keyup', (evt) => {
  if (evt.key === 'ArrowLeft') {
    eventEmitter.emit(Messages.HERO_MOVE_LEFT)
  }
});

このコードの場合、'keyup' というイベントが発生すると hero オブジェクトの位置が変更されるが、'keyup' に追加するイベントリスナーと hero オブジェクトはお互いを知らない。複数のイベントリスナーから Messages.HERO_MOVE_LEFT が呼び出されていたとしても、このメッセージによって起きる具体的な処理を変更する場合は、一箇所(eventEmitter.on() で渡して無名関数)の変更で済む。

3. Part 2: canvas にヒーローとモンスターを描く

<canvas> 要素にキャラクターを描く方法について説明している。

ソースコード:app.js

画像をセットした img 要素のオブジェクトを生成して返す関数

function loadTexture(path) {
  return new Promise((resolve) => {
    const img = new Image();
    img.src = path;
    img.onload = () => {
      resolve(img);
    };
  });
}

hero となる画像を描画するコード

canvas = document.getElementById('canvas');
ctx = canvas.getContext('2d');
//  load textures
const heroImg = await loadTexture('assets/player.png')
// draw black background
ctx.fillStyle = 'black';
ctx.fillRect(0, 0, canvas.width, canvas.height);
// draw hero
ctx.drawImage(heroImg, canvas.width/2 - 45, canvas.height - (canvas.height /4));

4. Part 3: 動きを追加する

このレッスンでは、「動き」を加えていく。このゲームでオブジェクトを表現する手法としては、「コンポジション」ではなく「継承」が採用されている。

ソースコード:app.js

(1) ヒーローと敵、その親となるクラスを定義する。

GameObject クラス

class GameObject {
  constructor(x, y) {
    this.x = x;
    this.y = y;
    this.dead = false;
    this.type = "";
    this.width = 0;
    this.height = 0;
    this.img = undefined;
  }

  draw(ctx) {
    ctx.drawImage(this.img, this.x, this.y, this.width, this.height);
  }
}

GameObject を継承して、ヒーロークラスを定義する。

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.speed という変数が用意されている。

GameObject を継承して、敵クラスを定義する。

class Enemy extends GameObject {
  constructor(x, y) {
    super(x, y);
    (this.width = 98), (this.height = 50);
    this.type = "Enemy";
    let id = setInterval(() => {
      if (this.y < canvas.height - this.height) {
        this.y += 5;
      } else {
        //console.log('Stopped at', this.y)
        clearInterval(id);
      }
    }, 300)
  }
}
  • 敵は、画面の下に向かって自動的に移動する。

(2) 'keydown' イベントに対するイベントリスナーを追加する。

let onKeyDown = function (e) {
  //console.log(e.keyCode);
  switch (e.keyCode) {
    case 37:
    case 39:
    case 38:
    case 40: // Arrow keys
    case 32: // Space key
      e.preventDefault();
      break;
    default:
      break; // do not block other keys
    }
};
window.addEventListener('keydown', onKeyDown);

(3) Pub/Sub パターンを実装する。

windowオブジェクトに対するイベントリスナーを追加する。

window.addEventListener("keyup", (evt) => {
  if (evt.key === "ArrowUp") {
    eventEmitter.emit(Messages.KEY_EVENT_UP);
  } else if (evt.key === "ArrowDown") {
    eventEmitter.emit(Messages.KEY_EVENT_DOWN);
  } else if (evt.key === "ArrowLeft") {
    eventEmitter.emit(Messages.KEY_EVENT_LEFT);
  } else if (evt.key === "ArrowRight") {
    eventEmitter.emit(Messages.KEY_EVENT_RIGHT);
  }
});

メッセージを登録・発行するための EventEmitterクラスを定義する。

class EventEmitter {
  constructor() {
    this.listeners = {};
  }

  on(message, listener) {
    if (!this.listeners[message]) {
      this.listeners[message] = [];
    }
    this.listeners[message].push(listener);
  }

  emit(message, payload = null) {
    if (this.listeners[message]) {
      this.listeners[message].forEach((l) => l(message, payload));
    }
  }
}

定数を追加して、EventEmitterクラスのオブジェクトを用意する。

const Messages = {
  KEY_EVENT_UP: "KEY_EVENT_UP",
  KEY_EVENT_DOWN: "KEY_EVENT_DOWN",
  KEY_EVENT_LEFT: "KEY_EVENT_LEFT",
  KEY_EVENT_RIGHT: "KEY_EVENT_RIGHT",
};

let heroImg, 
    enemyImg, 
    laserImg,
    canvas, ctx, 
    gameObjects = [], 
    hero, 
    eventEmitter = new EventEmitter();

ゲームを初期化する。

function initGame() {
  gameObjects = [];
  createEnemies();
  createHero();

  eventEmitter.on(Messages.KEY_EVENT_UP, () => {
    hero.y -= 5 ;
  })

  eventEmitter.on(Messages.KEY_EVENT_DOWN, () => {
    hero.y += 5;
  });

  eventEmitter.on(Messages.KEY_EVENT_LEFT, () => {
    hero.x -= 5;
  });

  eventEmitter.on(Messages.KEY_EVENT_RIGHT, () => {
    hero.x += 5;
  });
}

(4) ゲーム・ループを準備する。

window.onload = async () => {
  canvas = document.getElementById("canvas");
  ctx = canvas.getContext("2d");
  heroImg = await loadTexture("assets/player.png");
  enemyImg = await loadTexture("assets/enemyShip.png");
  laserImg = await loadTexture("assets/laserRed.png");

  initGame();
  let gameLoopId = setInterval(() => {
    ctx.clearRect(0, 0, canvas.width, canvas.height);
    ctx.fillStyle = "black";
    ctx.fillRect(0, 0, canvas.width, canvas.height);
    drawGameObjects(ctx);
  }, 100)
  
};

(5) ある一定の間隔で敵を動かすコードを追加する。

function createEnemies() {
  const MONSTER_TOTAL = 5;
  const MONSTER_WIDTH = MONSTER_TOTAL * 98;
  const START_X = (canvas.width - MONSTER_WIDTH) / 2;
  const STOP_X = START_X + MONSTER_WIDTH;

  for (let x = START_X; x < STOP_X; x += 98) {
    for (let y = 0; y < 50 * 5; y += 50) {
      const enemy = new Enemy(x, y);
      enemy.img = enemyImg;
      gameObjects.push(enemy);
    }
  }
}

同じように、createHero() 関数を定義する。

function createHero() {
  hero = new Hero(
    canvas.width / 2 - 45,
    canvas.height - canvas.height / 4
  );
  hero.img = heroImg;
  gameObjects.push(hero);
}

最後に、描画を開始するための drawGameObjects() 関数を定義する。

function drawGameObjects(ctx) {
  gameObjects.forEach(go => go.draw(ctx));
}

5. おわりに

私はゲーム・プログラミングについて詳しくはないのですが、かなり本格的な手法を使った内容なのではないでしょうか。

当然、元のサイトではもっと詳しく文章で説明されてはいますが、学習者は1つ1つのコードを丁寧に読み解いて理解することが求められます。

📂-プログラミング

執筆者:labo


comment

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

関連記事

C言語のポインタを理解するためのお勧め書籍を紹介します

例え話をしないC言語のポインタの説明 | 右や左の旦那様 C言語のポインタについての記事が、はてなブックマークのトップページに載っていました。 私は業務・趣味のどちらにおいても C言語は使いません。し …

Web

OpenID Connect の処理フロー

OpenID Connect の処理フローを図にしました。

no image

ウェブプログラミングの知識があるとできること(その1)

先日、あるブログを見ていたら最新の記事だけが表示されない仕組みになっていました。 ウェブプログラミングの知識があるとこんなことができますという例として、その仕組を調べた時の過程を紹介します。 目次きっ …

MySQL

MySQL に新たにデータベースと専用ユーザを追加するSQLステートメント

目次1. 本ページに記載する SQLステートメントを使用するシチュエーション2. 前提3. SQLステートメント1. データベースの作成2. ユーザーの作成3. このユーザーに、先ほど作成したデータベ …

no image

Riot.jsを使ってみた感想

Riot.js を使って、素数判定を行うページを作ってみた感想です。 ※ 以前は、ここにその素数判定を実装していたのですが、今はありません。 目次Riot.jsを使った感想良い点悪い点 Riot.js …