Three.jsで一人称視点(FPS)を作る方法【PointerLockControls】

FPSゲームの「マウスで視点を回し、WASDで歩く」操作は、Three.js公式アドオンの PointerLockControls を使うと簡単に作れます。今回はマウスロックのUIから、移動、重力とジャンプまでを実装します。

デモは画面をクリックするとマウスカーソルが消えてFPS操作になります(Escで解除)。WASDで移動、Spaceでジャンプです。

デモを別タブで開く

PointerLockとは

通常のマウスイベントは「カーソルが画面のどこにあるか」しか分かりません。FPSの視点操作に必要なのは「マウスがどれだけ動いたか」で、しかもカーソルが画面端に到達したら困ります。

そこでブラウザの Pointer Lock API の出番です。カーソルを消して、マウスの移動量だけを無限に取り続けられるようにします。Three.jsではこれをカメラ操作に繋ぎ込んだ PointerLockControls が用意されています。

import { PointerLockControls } from 'three/addons/controls/PointerLockControls.js';

const controls = new PointerLockControls(camera, document.body);

これだけで、ロック中はマウスに合わせてカメラが回るようになります。

ロック開始のUIはお約束がある

ブラウザのセキュリティ上、ポインターロックはユーザーのクリックなどの操作からしか開始できません。ページを開いた瞬間に勝手にロックすることはできないので、「クリックしてスタート」のオーバーレイを置くのが定番のUIです。

<div id="overlay">
  <p><strong>クリックしてスタート</strong><br>
  マウス: 視点移動 / WASD: 移動 / Space: ジャンプ</p>
</div>
const overlay = document.getElementById('overlay');

overlay.addEventListener('click', () => controls.lock());
controls.addEventListener('lock', () => (overlay.style.display = 'none'));
controls.addEventListener('unlock', () => (overlay.style.display = 'grid'));

Escキーでのロック解除はブラウザが勝手にやってくれるので、unlock イベントでオーバーレイを再表示すれば「ポーズ画面」の出来上がりです。

移動: moveForward / moveRight

PointerLockControlsには「視点の向き基準で移動する」ヘルパーが生えています。自前でカメラの向きからベクトルを計算しなくていいので楽です。

const keyStates = {};
document.addEventListener('keydown', (event) => (keyStates[event.code] = true));
document.addEventListener('keyup', (event) => (keyStates[event.code] = false));

const SPEED = 8;

function updatePlayer(deltaTime) {
  if (!controls.isLocked) return; // ロック中だけ動かす

  const forward = Number(!!keyStates['KeyW']) - Number(!!keyStates['KeyS']);
  const right = Number(!!keyStates['KeyD']) - Number(!!keyStates['KeyA']);

  controls.moveForward(forward * SPEED * deltaTime);
  controls.moveRight(right * SPEED * deltaTime);
}

moveForward は視点の向きの水平成分に沿って動くので、上を向いたまま前進しても空に飛んでいきません。ちゃんとFPSの歩行になります。

controls.isLocked のガードも大事です。これがないとポーズ中(オーバーレイ表示中)にもキャラが歩けてしまいます。

重力とジャンプは自前で足す

PointerLockControlsは視点と水平移動だけの担当なので、垂直方向は自分で書きます。といっても、速度に重力を足し込んでいくだけの単純な物理です。

const velocity = new THREE.Vector3();
const GRAVITY = 25;
const JUMP_POWER = 9;
const EYE_HEIGHT = 1.6; // 目線の高さ
let onFloor = true;

function updateVertical(deltaTime) {
  if (keyStates['Space'] && onFloor) {
    velocity.y = JUMP_POWER;
    onFloor = false;
  }

  velocity.y -= GRAVITY * deltaTime;
  camera.position.y += velocity.y * deltaTime;

  // 地面より下には落ちない
  if (camera.position.y < EYE_HEIGHT) {
    camera.position.y = EYE_HEIGHT;
    velocity.y = 0;
    onFloor = true;
  }
}

GRAVITY = 25 が現実(9.8)より強いのは意図的です。現実の重力どおりだとジャンプがふわふわして「月面感」が出るので、ゲームでは強めの重力+強めの初速でキビキビさせるのが定石です。このあたりは好みで調整してください。

壁や段差のある本格的な当たり判定

このデモの床は「y座標の下限」だけの簡易判定です。壁にぶつかる・箱に乗る・坂を登るをやりたくなったら、選択肢は2つあります。

  1. three.js公式の games_fps サンプル方式 — レベル全体をOctree(空間分割)に登録し、プレイヤーをカプセル形状として毎フレーム衝突判定する。公式examplesのgames_fpsがそのまま教科書になります。物理エンジン無しでここまでできる、というお手本です
  2. 物理エンジンに任せるRapierのkinematicボディ+KinematicCharacterControllerを使う。壁ずり・段差・坂の処理が組み込みであります

起伏のある地面を歩くだけなら、前回のRaycaster接地をFPS視点に組み合わせる手もあります(視点はPointerLock、足元はレイ)。

まとめ

  • FPS視点はPointerLockControlsで。ロック開始はユーザー操作からしかできないので「クリックしてスタート」UIが定番
  • 移動は moveForward / moveRight ヘルパーが視点基準でやってくれる
  • 重力・ジャンプは速度を自前で積分する(重力は現実より強めがゲームらしい)
  • 本格的な衝突はOctree+カプセル(公式games_fps)か物理エンジンへ