2017/10/05

Canvasに表示したオブジェクトをキーボード操作で移動させる方法

Canvas+JavaScriptでゲームをつくろうとしている。もちろん自作ゲームエンジンで!
その第一歩として、Canvasに表示したオブジェクト(Player)をキーボード操作で移動させる方法をまとめる。



ゲームエンジンの基本は無限ループ


ゲームの基本は無限ループなので、以下のような流れで処理していく。
  1. Canvasをクリアする(白紙に戻す)
  2. キーボード操作などを受け付る
  3. オブジェクトの座標や画像を変更する
  4. Canvasに描画する
  5. 1に戻ってループする

このループをfps30なら1秒間に30回、fps60なら1秒間に60回繰り返す。
ループ内でキーボード操作(keydownkeypresskeyupなど)を取得し、オブジェクトの座標を変更することで、移動させることができる。


オブジェクトをキーボード操作で動かす


今回はES2015から追加されたClass構文をつかう。オブジェクトをクラスとして扱うことで、座標やサイズなどをカプセル化でき、Canvasに描画する際に簡単にできるようになるからだ。


ゲームエンジンのコアの実装


/**
 * ゲームエンジン
 */
class Game {
  /**
   * ゲームインスタンスを作成する
   * @param {Object} opt オプション
   * @param {HTMLCanvasElement} opt.canvas Canvas要素
   * @param {Object} opt.assets アセット(画像ファイルなど)
   * @param {Number} opt.fps フレームレート
   */
  constructor(opt) {
    this._canvas = opt.canvas;
    // 描画用コンテキスト
    this._ctx = this._canvas.getContext('2d');
    
    // アセット(画像ファイルなど)
    this._assets = opt.assets;
    this._loadedAssets = {};
    
    // 描画速度(フレームレート)
    this._fps = opt.fps || 30;

    // 無限ループ用のtimer(start・stop時に使用)
    this._timer;

    // Canvasに描画するオブジェクトリスト
    this._items = [];

    // キーボード入力を保持する
    this._keyboard = '';
    this._setEventListener();
  }
  /**
   * ゲームを開始する
   */
  async start() {
    await this._loadAssets();
    this._timer = setInterval(() => {
      this._render();
    }, 1000 / this._fps);
  }
  /**
   * ゲームを一時停止する
   */
  stop() {
    clearInterval(this._timer);
  }
  /**
   * アセットを読み込む
   */
  async _loadAssets() {
    const promises = Object.keys(this._assets).map(asset => {
      return new Promise((resolve, reject) => {
        const img = new Image();
        img.onload = () => { resolve(); }
        img.onerror = err => { reject(err); }
        img.src = this._assets[asset];
        this._loadedAssets[asset] = img;
      });
    });

    return Promise.all(promises);
  }
  /**
   * Canvasをクリアし、オブジェクトを再描画する
   */
  _render() {
    // Canvasをクリアする
    this._ctx.clearRect(0, 0, this._canvas.clientWidth, this._canvas.clientHeight);

    this._items.forEach(a => {
      // オブジェクトを再描画する
      a.draw(this._ctx, this._loadedAssets);
      // オブジェクトの状態を変更する
      a.update(this._keyboard);
    })
  }
  /**
   * キーボード操作を受け付ける
   */
  _setEventListener() {
    window.addEventListener('keydown', e => { this._keyboard = e.key });
    window.addEventListener('keyup', e => { this._keyboard = '' });
  }
  /**
   * 描画リストにオブジェクトを追加する
   * @param {Object} item 描画するオブジェクトインスタンス
   */
  add(item) {
    this._items.push(item);
  }
  /**
   * 描画リストから対象のオブジェクトを削除する
   * @param {Object} item 削除したいオブジェクトのインスタンス
   */
  remove(item) {
    const idx = this._items.find(a => a === item);
    this._items.splice(idx, 1);
  }
}
ゲームエンジン(コア部分)は、コード内のコメントのとおり。

ループ処理はsetIntervalを使い、1000ms/fps間隔で実行している。
ただしループ内の処理が大きくなった場合にsetIntervalは危険だ。それは処理が完了する前に次の処理が実行される可能性があるためだ。
もし処理が大きくなるようだったらsetTimeoutを検討したほうが良い。


キーボードで操作するために、_setEventListenerで、keydownとkeyupのイベントを追加している。
keydownで_keyboardプロパティにkeyコードを設定。keyupでクリア。
そして、_render内のItem.updateに引数として渡している。

updateの引数として渡された_keyboardプロパティは、次に紹介するPlayerクラスのupdateメソッドで受け取り、キーコードにより座標を変更している。


Playerオブジェクトのクラス


/**
 * Playerクラス
 */
class Player {
  /**
   * Playerインスタンスを作成する
   * @param {Objeect} opt オプション
   * @param {Number} opt.x 初期位置(X座標)
   * @param {Number} opt.y 初期位置(Y座標)
   * @param {Number} opt.w オブジェクトサイズ(幅)
   * @param {Number} opt.h オブジェクトサイズ(高さ)
   * @param {String} opt.src アセットのキー
   */
  constructor(opt = {}) {
    this.x = opt.x || 0;
    this.y = opt.y || 0;
    this.w = opt.w || 64;
    this.h = opt.h || 64;
    this.src = opt.src;
  }
  /**
   * オブジェクトを描画する
   * @param {CanvasRenderingContext2D} ctx 2Dコンテキスト
   * @param {Object} assets アセット
   */
  draw(ctx, assets) {
    if (assets[this.src]) {
      ctx.drawImage(assets[this.src], this.x, this.y, this.w, this.h);
    } else {
      ctx.fillRect(this.x, this.y, this.w, this.h);
    }
  }
  /**
   * オブジェクトの状態を変更する
   * @param {Object} keyboard キーコード
   */
  update(keyboard) {
    // ここでキーボード操作により座標を変更する
    switch (keyboard) {
      case 'ArrowUp':
        this.y -= 5;
        break;
      case 'ArrowDown':
        this.y += 5;
        break;
      case 'ArrowLeft':
        this.x -= 5;
        break;
      case 'ArrowRight':
        this.x += 5;
        break;
    }
  }
}
Game#_render内のItem#updateで渡されたキーコードを、Player#updateで受け取り、矢印の向きにあわせて座標を変更している。

ここでキーボードの操作など関係なく座標を変更すれば、オブジェクトが自動で動いてくれる。


メイン処理


/**
 * Main処理
 */
function main() {
  const assets = {
    player: 'https://avatars3.githubusercontent.com/u/5305599'
  };
  const game = new Game({
    canvas: document.getElementById('app'),
    assets: assets,
    fps: 30
  });
  
  const player = new Player({
    src: 'player'
  });
  game.add(player);
  
  document.getElementById('start').addEventListener('click', () => game.start());
  document.getElementById('stop').addEventListener('click', () => game.stop());
}
main();

assetsにCanvasに表示したい画像を定義しておく。
そしてGameクラスに渡すことで、ゲーム開始前にPromise.allでロードしている。
Image.srcにURLを指定してもその画像のロードが終わるまではctx.drawImageで描画できないため、あらかじめロードしキャッシュしておくためだ。

あとは、Start/Stopボタンにゲーム開始/終了のイベントを登録し完成!



完成





以上


written by @bc_rikko

0 件のコメント :

コメントを投稿