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つあります。
- three.js公式の games_fps サンプル方式 — レベル全体をOctree(空間分割)に登録し、プレイヤーをカプセル形状として毎フレーム衝突判定する。公式examplesのgames_fpsがそのまま教科書になります。物理エンジン無しでここまでできる、というお手本です
- 物理エンジンに任せる — Rapierのkinematicボディ+
KinematicCharacterControllerを使う。壁ずり・段差・坂の処理が組み込みであります
起伏のある地面を歩くだけなら、前回のRaycaster接地をFPS視点に組み合わせる手もあります(視点はPointerLock、足元はレイ)。
まとめ
- FPS視点はPointerLockControlsで。ロック開始はユーザー操作からしかできないので「クリックしてスタート」UIが定番
- 移動は
moveForward/moveRightヘルパーが視点基準でやってくれる - 重力・ジャンプは速度を自前で積分する(重力は現実より強めがゲームらしい)
- 本格的な衝突はOctree+カプセル(公式games_fps)か物理エンジンへ