弾幕ゲームから学ぶPointer Lock API

UI開発者 板垣

本記事ではJavaScriptで作成された簡単な弾幕ゲームを基に、Pointer Lock APIやその周辺技術の紹介をいたします。

Pointer Lock APIとは

一言で言ってしまえば「マウスポインターの動きを簡単に奪取できるAPI」です。
マウスポインターの動きを奪取するとどのようなことができるのか、代表的な例として以下の2点があげられます。

  • 標準のマウスポインターを非表示にして、div要素やcanvas要素で描画したオリジナルのアイコンをマウスポインターとして扱う
  • マウスポインターがブラウザの枠外に出ている状態でマウスイベントにアクセスする

このような特性を生かすことで、マウスの動きに合わせたコンテンツを簡単に作成できます。
特に、マウスポインターの位置を必要とするブラウザゲームなどと相性が良いとされてます。

説明の基となる弾幕ゲーム

※本デモはPCブラウザでご覧ください。

ゲームの仕様

  • 開始ボタンを押下するとマウスポインターが消えてゲームが開始される
  • ゲーム開始後は、マウス操作でゲーム画面内にある青丸が操作可能となる
  • 赤丸に当たった場合はゲームオーバーとなる
  • 時間経過によってスコアは増幅していく

Pointer Lock APIおよび、その周辺技術が関わっている処理の解説

本章にて、Pointer Lock APIおよび、その周辺技術が関わっている処理について解説します。

ページ表示時

2020年11月時点では、MDNをはじめとする、Pointer Lock APIの紹介記事の多くで、以下のようにDOMを拡張する形でPointer Lock APIの初期化処理をしています。

canvas.requestPointerLock = canvas.requestPointerLock || canvas.mozRequestPointerLock;
document.exitPointerLock = document.exitPointerLock || document.mozExitPointerLock;

ですが、すでにPointer Lock APIの機能の半数はベンダープレフィックス付けることなく、多くのブラウザで利用できるようになっております。 そのため、今回は前述した初期処理の説明は割愛いたします。

開始ボタン押下時

const gameStart = () => {
    ...

    player = new Player(PLAYER_COLOR, PLAYER_INITIAL_X, PLAYER_INITIAL_Y, PLAYER_RADIUS);
    Enemy.init(enemy, ENEMY_DENSITY, ENEMY_MIN_SPEED, ENEMY_MAX_SPEED, ENEMY_MIN_RADIUS, ENEMY_MAX_RADIUS);

    canvas.requestPointerLock();
    player.startControl();
    score.count();
    renderer.render(renderFn);

    ...
};

開始ボタンを押下すると上記のgameStart関数が実行されます。
この処理の中にcanvas.requestPointerLockというメソッドがありますが、これはPointer Lock APIの機能の1つです。 このメソッドを実行すると、マウスポインターが画面上から見えなくなり、マウスポインターの動きを奪取できるようになります。

マウスポインターの移動時

control(e) {
        const movementX = e.movementX;
        const movementY = e.movementY;
        this.x += movementX;
        this.y += movementY;

        const overflowLeft = this.x + this.radius < 0;
        if (overflowLeft) {
            this.x = (canvas.width + this.radius);
        }
        const overflowRight = this.x - this.radius > canvas.width;
        if (overflowRight) {
            this.x = -this.radius;
        }
        const overflowTop = this.y + this.radius < 0;
        if (overflowTop) {
            this.y = (canvas.height + this.radius);
        }
        const overflowBottom = this.y - this.radius > canvas.height;
        if (overflowBottom) {
            this.y = -this.radius;
        }
    }
    startControl() {
        document.addEventListener('mousemove', this.boundControlMethods);
    }

ゲーム開始ボタンを押下後は、startControlメソッドが実行され、documentオブジェクトに対してmousemoveイベントが登録されて、イベントが検知されると最終的にcontrolメソッドが実行されるようになっています。
このcontrolメソッド内にあるe.movementXe.movementYはPointer Lock APIが紹介される際にしばしば登場する周辺技術で、これら2つのプロパティからは「マウスポインターの移動した量」を取得できます。

前述したElement.requestPointerLockを使用するとマウスポインターの位置が固定されてしまうので、MouseEvent.pageXMouseEvent.screenXなどのプロパティからは固定値しか取得できなくなってしまいます。
そのため、Element.requestPointerLockを使用してポインターの位置を制御する場合は、MouseEvent.movementXMouseEvent.movementYを利用する必要があります。

本ゲームではこれら2つのプロパティを活用して、マウスポインターが枠外へ出たときに対面する位置へマウスポインターを移動させ、マウスポインターが行方不明とならないように制御しています。

ゲームオーバー時

const gameEnd = (e) => {
    if (!e || !document.pointerLockElement) {
        app.classList.add('is-end');
        player.stopControl();
        score.abort();
        document.exitPointerLock();
        cancelAnimationFrame(renderer.frame);
    }
};

startBtn.addEventListener('click', gameStart);
document.addEventListener('pointerlockchange', gameEnd);

上記のコードブロックの中にはPointer Lock APIに関連した処理が3つ書かれています。

1つはpointerlockchangeイベントです。
このイベントは前出のElement.requestPointerLockや、後述するdocument.exitPointerLockなどが実行されて、ポインターロックに変化があると発火するイベントです。
本ゲームでは「開始ボタンを押下したとき」、「青丸が赤丸へ当たったとき」、「ユーザーがエスケープキーを押下したとき」に、Element.requestPointerLockもしくはdocument.exitPointerLockが実行されて、このイベントが発火するようになっています。
ちなみに、「青丸が赤丸へ当たったとき」には、gameStart関数内にあるCharacter.hitの第3引数に指定されたコールバック関数が実行されます。
今回は第3引数にgameEnd関数が指定されているため、結果的にdocument.exitPointerLockが実行されてpointerlockchangeイベントが発火しています。

続いて、gameEnd関数内のif文に書かれているdocument.pointerLockElementですが、これはポインターロックをした要素を取得するために使用します。
本ゲームの場合だと、canvas要素からElement.requestPointerLockを実行して、ポインターロックをしたため、document.pointerLockElementの中にはcanvas要素が入っていることになります。

最後に、document.exitPointerLockですが、このメソッドを実行することでポインターロックを終了できます。
また、ポインターロックは、document.exitPointerLockを使わずともエスケープキーを押下することで、ユーザーの任意のタイミングで終了できます。

おわりに

Pointer Lock APIはゲーム制作に興味がある私にとって、とてもそそられるAPIでした。
今後は簡易的なゲームだけではなく、FPSなどの3Dゲームも制作していければと考えています。

ちなみに、今回制作した弾幕ゲームを私自身プレイしてみましたが、最高スコアが8以上伸びませんでした......。
私よりも高スコアを出せた方がいらっしゃいましたらSNSで発信ください。そのときには心より拍手をお送りいたします。