【Cocos Creator 3.8】FreeCameraの実装:アタッチするだけでWASD移動・マウスドラッグ回転ができる自由カメラスクリプト
このガイドでは、FreeCamera という汎用コンポーネントを実装します。
カメラノードにこのスクリプトをアタッチするだけで、WASDで平行移動・マウス右ドラッグで視点回転・マウスホイールで前後移動 といった「フリーカメラ」操作が可能になります。
シーン全体を見回しながらレベルデザインを確認したり、デバッグ用カメラとして利用するのに便利です。
コンポーネントの設計方針
機能要件の整理
- このコンポーネントは Camera コンポーネントを持つノード にアタッチして使用する。
- WASD キーによるカメラ移動(ローカル座標系での前後左右移動)。
- Shift キーで移動速度を一時的にブースト。
- マウス右ボタンを押しながらドラッグでカメラの向きを変更(Yaw/Pitch)。
- マウスホイールで前後移動(ズームではなく、カメラ自体を前後へ移動)。
- インスペクタから移動速度・回転感度・ピッチ角の制限などを調整可能にする。
- 他のカスタムスクリプトには一切依存せず、このスクリプト単体で完結させる。
- 必須の標準コンポーネント(Camera)が見つからない場合はエラーログを出す。
入力仕様
デフォルトのキー割り当ては以下のようにします。
- W / S:前進 / 後退
- A / D:左移動 / 右移動
- Q / E:下降 / 上昇(上下移動)
- Shift:押している間だけ移動速度をブースト
- マウス右ボタン + ドラッグ:カメラの向きを回転
- マウスホイール:カメラの前後移動(視線方向に沿って移動)
インスペクタで設定可能なプロパティ
FreeCamera コンポーネントでインスペクタから編集可能にするプロパティの一覧と役割です。
- moveSpeed (number)
- 基本の移動速度(単位:1秒あたりの距離)。
- WASD/QE およびホイール移動に適用される基準速度。
- 例: 5〜20 程度が扱いやすい。
- boostMultiplier (number)
- Shift キーを押している間の速度倍率。
- moveSpeed × boostMultiplier が実際の速度になる。
- 例: 3〜10 など。
- mouseLookSensitivity (number)
- マウス右ドラッグでの回転感度。
- 値が大きいほど少ないマウス移動で大きく回転する。
- wheelMoveFactor (number)
- マウスホイール 1 スクロールあたりの移動係数。
- moveSpeed × wheelMoveFactor × スクロール量 が前後移動量。
- invertY (boolean)
- マウスのY軸操作を反転するかどうか。
- オンにすると「マウスを上に動かすとカメラが下を向く」挙動になる。
- minPitch (number)
- ピッチ角(上下回転)の下限(度数)。
- 例: -89 で真上を向きすぎて反転するのを防ぐ。
- maxPitch (number)
- ピッチ角(上下回転)の上限(度数)。
- 例: 89 で真下を向きすぎて反転するのを防ぐ。
- enableCursorLock (boolean)
- マウス右ボタンを押したときにカーソルロック(Pointer Lock API)を試みるかどうか。
- Webビルド時に、よりFPSゲームらしい操作感になる。
- エディタ内プレビューではブラウザ環境と挙動が異なる場合あり。
- activeOnStart (boolean)
- シーン開始時に FreeCamera を有効化するかどうか。
- オフの場合、他のスクリプトから
enabled = trueにすることで後から有効化できる。
TypeScriptコードの実装
以下が FreeCamera コンポーネントの完全な実装コードです。
import { _decorator, Component, Node, Camera, input, Input, EventKeyboard, EventMouse, EventMouseType, EventTouch, KeyCode, Vec3, math, systemEvent, SystemEventType, sys } from 'cc';
const { ccclass, property } = _decorator;
/**
* FreeCamera
* - カメラノードにアタッチして使用する「自由カメラ」コンポーネント。
* - WASD/QE で移動、Shift で速度ブースト、右クリックドラッグで視点回転、
* ホイールで前後移動を行う。
* - 他のカスタムスクリプトには依存しない。
*/
@ccclass('FreeCamera')
export class FreeCamera extends Component {
@property({
tooltip: '基本の移動速度(1秒あたりの距離)。WASD/QE とホイール移動に適用されます。',
})
public moveSpeed: number = 10;
@property({
tooltip: 'Shift キーを押している間の速度倍率(moveSpeed に掛け算されます)。',
})
public boostMultiplier: number = 5;
@property({
tooltip: 'マウス右ドラッグによる視点回転の感度(値が大きいほど敏感)。',
})
public mouseLookSensitivity: number = 0.2;
@property({
tooltip: 'マウスホイール 1 スクロールあたりの移動係数(moveSpeed に掛け算されます)。',
})
public wheelMoveFactor: number = 1.0;
@property({
tooltip: 'マウスY軸の操作を反転するかどうか(ONで「上に動かすと下を見る」挙動)。',
})
public invertY: boolean = false;
@property({
tooltip: 'ピッチ角(上下回転)の最小値(度)。-89 などにして真上を向きすぎないようにします。',
})
public minPitch: number = -89;
@property({
tooltip: 'ピッチ角(上下回転)の最大値(度)。89 などにして真下を向きすぎないようにします。',
})
public maxPitch: number = 89;
@property({
tooltip: '右クリック時にマウスカーソルのロック(Pointer Lock)を試みるかどうか(Web向け)。',
})
public enableCursorLock: boolean = false;
@property({
tooltip: 'シーン開始時に FreeCamera を有効化するかどうか。',
})
public activeOnStart: boolean = true;
// 内部状態
private _camera: Camera | null = null;
// キー入力の状態管理
private _moveForward: boolean = false;
private _moveBackward: boolean = false;
private _moveLeft: boolean = false;
private _moveRight: boolean = false;
private _moveUp: boolean = false;
private _moveDown: boolean = false;
private _boost: boolean = false;
// マウスルック状態
private _isRightMouseDown: boolean = false;
// オイラー角での向き管理(度数法)
private _yaw: number = 0; // 左右回転(Y軸)
private _pitch: number = 0; // 上下回転(X軸)
onLoad() {
// カメラ取得(必須コンポーネント)
this._camera = this.getComponent(Camera);
if (!this._camera) {
console.error('[FreeCamera] Camera コンポーネントが見つかりません。このスクリプトは Camera を持つノードにアタッチしてください。');
}
// 現在の回転をオイラー角に変換して初期値として保持
const euler = this.node.eulerAngles;
this._yaw = euler.y;
this._pitch = euler.x;
// 入力イベント登録
this._registerInput();
// activeOnStart が false の場合、最初は無効化しておく
this.enabled = this.activeOnStart;
}
onDestroy() {
this._unregisterInput();
}
start() {
// 特に初期化は onLoad で完了しているが、
// 将来的に開始時処理が必要になった場合のためにメソッドを残しておく。
}
/**
* 毎フレームの更新処理:
* - キー入力状態から移動方向を決定し、移動を行う
* - マウスルックによる回転は EventMouse のコールバック内で更新
*/
update(deltaTime: number) {
if (!this.enabled) {
return;
}
if (!this._camera) {
return;
}
// 移動方向ベクトル(ローカル空間)
const moveDir = new Vec3(0, 0, 0);
if (this._moveForward) {
moveDir.z -= 1;
}
if (this._moveBackward) {
moveDir.z += 1;
}
if (this._moveLeft) {
moveDir.x -= 1;
}
if (this._moveRight) {
moveDir.x += 1;
}
if (this._moveUp) {
moveDir.y += 1;
}
if (this._moveDown) {
moveDir.y -= 1;
}
if (!moveDir.equals(Vec3.ZERO)) {
// 正規化して速度を一定にする
moveDir.normalize();
// 実際の速度を計算(Shift でブースト)
let speed = this.moveSpeed;
if (this._boost) {
speed *= this.boostMultiplier;
}
// ローカル空間の移動をワールド空間に変換して適用
const moveWorld = new Vec3();
// このノードのローカル→ワールド方向変換(回転のみ)
Vec3.transformQuat(moveWorld, moveDir, this.node.rotation);
moveWorld.multiplyScalar(speed * deltaTime);
const currentPos = this.node.position;
const newPos = new Vec3(
currentPos.x + moveWorld.x,
currentPos.y + moveWorld.y,
currentPos.z + moveWorld.z,
);
this.node.setPosition(newPos);
}
}
// ===== 入力イベント登録・解除 =====
private _registerInput() {
// キーボード
input.on(Input.EventType.KEY_DOWN, this._onKeyDown, this);
input.on(Input.EventType.KEY_UP, this._onKeyUp, this);
// マウス
input.on(Input.EventType.MOUSE_DOWN, this._onMouseDown, this);
input.on(Input.EventType.MOUSE_UP, this._onMouseUp, this);
input.on(Input.EventType.MOUSE_MOVE, this._onMouseMove, this);
input.on(Input.EventType.MOUSE_WHEEL, this._onMouseWheel, this);
// タッチ(モバイル向けの簡易対応:1本指ドラッグで視点回転)
input.on(Input.EventType.TOUCH_START, this._onTouchStart, this);
input.on(Input.EventType.TOUCH_MOVE, this._onTouchMove, this);
input.on(Input.EventType.TOUCH_END, this._onTouchEnd, this);
input.on(Input.EventType.TOUCH_CANCEL, this._onTouchEnd, this);
}
private _unregisterInput() {
input.off(Input.EventType.KEY_DOWN, this._onKeyDown, this);
input.off(Input.EventType.KEY_UP, this._onKeyUp, this);
input.off(Input.EventType.MOUSE_DOWN, this._onMouseDown, this);
input.off(Input.EventType.MOUSE_UP, this._onMouseUp, this);
input.off(Input.EventType.MOUSE_MOVE, this._onMouseMove, this);
input.off(Input.EventType.MOUSE_WHEEL, this._onMouseWheel, this);
input.off(Input.EventType.TOUCH_START, this._onTouchStart, this);
input.off(Input.EventType.TOUCH_MOVE, this._onTouchMove, this);
input.off(Input.EventType.TOUCH_END, this._onTouchEnd, this);
input.off(Input.EventType.TOUCH_CANCEL, this._onTouchEnd, this);
}
// ===== キーボード入力処理 =====
private _onKeyDown(event: EventKeyboard) {
switch (event.keyCode) {
case KeyCode.KEY_W:
this._moveForward = true;
break;
case KeyCode.KEY_S:
this._moveBackward = true;
break;
case KeyCode.KEY_A:
this._moveLeft = true;
break;
case KeyCode.KEY_D:
this._moveRight = true;
break;
case KeyCode.KEY_Q:
this._moveDown = true;
break;
case KeyCode.KEY_E:
this._moveUp = true;
break;
case KeyCode.SHIFT_LEFT:
case KeyCode.SHIFT_RIGHT:
this._boost = true;
break;
}
}
private _onKeyUp(event: EventKeyboard) {
switch (event.keyCode) {
case KeyCode.KEY_W:
this._moveForward = false;
break;
case KeyCode.KEY_S:
this._moveBackward = false;
break;
case KeyCode.KEY_A:
this._moveLeft = false;
break;
case KeyCode.KEY_D:
this._moveRight = false;
break;
case KeyCode.KEY_Q:
this._moveDown = false;
break;
case KeyCode.KEY_E:
this._moveUp = false;
break;
case KeyCode.SHIFT_LEFT:
case KeyCode.SHIFT_RIGHT:
this._boost = false;
break;
}
}
// ===== マウス入力処理 =====
private _onMouseDown(event: EventMouse) {
if (event.getButton() === EventMouse.BUTTON_RIGHT) {
this._isRightMouseDown = true;
if (this.enableCursorLock && sys.isBrowser && document.body.requestPointerLock) {
// Pointer Lock API を試みる(対応ブラウザのみ)
try {
document.body.requestPointerLock();
} catch (e) {
console.warn('[FreeCamera] Pointer Lock の取得に失敗しました:', e);
}
}
}
}
private _onMouseUp(event: EventMouse) {
if (event.getButton() === EventMouse.BUTTON_RIGHT) {
this._isRightMouseDown = false;
if (this.enableCursorLock && sys.isBrowser && (document as any).exitPointerLock) {
try {
(document as any).exitPointerLock();
} catch (e) {
console.warn('[FreeCamera] Pointer Lock の解除に失敗しました:', e);
}
}
}
}
private _onMouseMove(event: EventMouse) {
if (!this.enabled) {
return;
}
if (!this._isRightMouseDown) {
return;
}
const deltaX = event.getDeltaX();
const deltaY = event.getDeltaY();
// Yaw(左右)はマウスX移動、Pitch(上下)はマウスY移動
this._yaw -= deltaX * this.mouseLookSensitivity;
const invert = this.invertY ? -1 : 1;
this._pitch -= deltaY * this.mouseLookSensitivity * invert;
// ピッチ角を制限
this._pitch = math.clamp(this._pitch, this.minPitch, this.maxPitch);
this._applyRotation();
}
private _onMouseWheel(event: EventMouse) {
if (!this.enabled) {
return;
}
if (!this._camera) {
return;
}
const scrollY = event.getScrollY(); // 上スクロールで正、下スクロールで負(環境による)
if (scrollY === 0) {
return;
}
let speed = this.moveSpeed * this.wheelMoveFactor;
if (this._boost) {
speed *= this.boostMultiplier;
}
// スクロール方向に応じて前後移動
// 多くの環境では「上スクロール = 正の値」なので、前進を負方向にしておく
const dir = scrollY > 0 ? -1 : 1;
const moveLocal = new Vec3(0, 0, dir);
const moveWorld = new Vec3();
Vec3.transformQuat(moveWorld, moveLocal, this.node.rotation);
moveWorld.multiplyScalar(speed * 0.02); // ホイールはフレームレートに依存しないので係数を小さく
const currentPos = this.node.position;
const newPos = new Vec3(
currentPos.x + moveWorld.x,
currentPos.y + moveWorld.y,
currentPos.z + moveWorld.z,
);
this.node.setPosition(newPos);
}
// ===== タッチ入力(簡易マウスルック) =====
private _touchLastX: number = 0;
private _touchLastY: number = 0;
private _isTouching: boolean = false;
private _onTouchStart(event: EventTouch) {
const loc = event.getLocation();
this._touchLastX = loc.x;
this._touchLastY = loc.y;
this._isTouching = true;
}
private _onTouchMove(event: EventTouch) {
if (!this.enabled) {
return;
}
if (!this._isTouching) {
return;
}
const loc = event.getLocation();
const deltaX = loc.x - this._touchLastX;
const deltaY = loc.y - this._touchLastY;
this._touchLastX = loc.x;
this._touchLastY = loc.y;
this._yaw -= deltaX * this.mouseLookSensitivity * 0.5;
const invert = this.invertY ? -1 : 1;
this._pitch -= deltaY * this.mouseLookSensitivity * invert * 0.5;
this._pitch = math.clamp(this._pitch, this.minPitch, this.maxPitch);
this._applyRotation();
}
private _onTouchEnd(event: EventTouch) {
this._isTouching = false;
}
// ===== 回転適用 =====
private _applyRotation() {
// X: pitch, Y: yaw, Z: roll(0固定)
this.node.setRotationFromEuler(this._pitch, this._yaw, 0);
}
}
主要メソッドの解説
- onLoad()
- 同じノードにアタッチされた
Cameraコンポーネントを取得し、見つからない場合はconsole.errorを出力。 - ノードの現在の回転をオイラー角に変換し、
_yaw/_pitchの初期値として保持。 - キーボード・マウス・タッチの入力イベントを登録。
activeOnStartに応じてthis.enabledを設定。
- 同じノードにアタッチされた
- update(deltaTime)
- キー入力状態(
_moveForwardなど)から移動方向ベクトルを構築。 - 正規化した後、
moveSpeedとboostMultiplierから実速度を計算。 - ノードの回転を使ってローカル移動方向をワールド方向に変換し、位置を更新。
- キー入力状態(
- _onMouseMove()
- 右クリック押下中のみ処理。
- マウスの移動量から
_yaw(左右)と_pitch(上下)を更新。 minPitch/maxPitchで上下角度を制限し、_applyRotation()でノードの回転に反映。
- _onMouseWheel()
- スクロール量に応じて視線方向に前後移動。
- フレームレートに依存しないため、内部的に小さい係数を掛けて移動量を調整。
- _applyRotation()
setRotationFromEuler(pitch, yaw, 0)でカメラの向きを反映するヘルパー。
使用手順と動作確認
1. スクリプトファイルの作成
- Assets パネルで右クリックします。
Create → TypeScriptを選択します。- ファイル名を
FreeCamera.tsに変更します。 - 作成された
FreeCamera.tsをダブルクリックしてエディタ(VSCode など)で開き、上記の TypeScript コードを丸ごと貼り付けて保存します。
2. カメラノードの用意
すでにシーンに Camera がある場合はそれを使っても構いません。ここでは新規作成の手順を示します。
- Hierarchy パネルで右クリックします。
Create → 3D Object → Cameraを選択します。- 作成された Camera ノードを
FreeCameraなど分かりやすい名前に変更します(任意)。
この Camera ノードには自動的に Camera コンポーネントが付与されます。
もし何らかの理由で Camera コンポーネントを削除してしまった場合は、必ず Inspector の Add Component → Rendering → Camera から追加してください。
3. FreeCamera コンポーネントをアタッチ
- Hierarchy で先ほどの Camera ノードを選択します。
- Inspector パネル下部の
Add Componentボタンをクリックします。 Customカテゴリ内にあるFreeCameraを選択して追加します。
Inspector に FreeCamera のプロパティが表示されます。
4. プロパティの設定例
まずは扱いやすい値として、以下のように設定してみてください。
- moveSpeed:
15 - boostMultiplier:
5 - mouseLookSensitivity:
0.2 - wheelMoveFactor:
1.0 - invertY: お好みで ON/OFF
- minPitch:
-89 - maxPitch:
89 - enableCursorLock:
- エディタ内プレビュー:OFF 推奨(ブラウザでの挙動確認時のみ ON でもよい)
- Web ビルドで FPS ライクな操作にしたい場合は ON
- activeOnStart:
true(シーン開始時からすぐ操作したい場合)
5. シーンのテスト用オブジェクトを配置
カメラの動きを確認しやすくするため、適当なオブジェクトをシーンに置いておきます。
- Hierarchy で右クリック →
Create → 3D Object → Boxを選択します。 - 同様に Plane や他のオブジェクトをいくつか配置して、空間の広がりが分かるようにします。
- Camera ノードを選択し、Position を
(0, 2, 10)、Rotation を(0, 180, 0)など適当な値に設定しておきます。
6. 再生して動作確認
- エディタ右上の
▶(Play)ボタンを押して Game ビューで再生します。 - Game ビューをクリックしてフォーカスを当てます。
- W / A / S / D / Q / E キーでカメラが移動することを確認します。
- Shift を押しながら移動して、速度が上がることを確認します。
- マウス右ボタンを押しながらドラッグして、カメラの向きが変わることを確認します。
- マウスホイールを回して、カメラが前後に移動することを確認します。
Web ビルド(ブラウザ)でテストする場合は、enableCursorLock を ON にしておくと、右クリック時にマウスカーソルが隠れ、より FPS ゲームのような操作感になります(ブラウザのセキュリティ仕様により、ユーザー操作をトリガーにしたときのみ有効になります)。
まとめ
この FreeCamera コンポーネントは、カメラノードにアタッチするだけで 以下のようなメリットを提供します。
- レベルデザイン中にステージ全体を素早く見回せる「デバッグカメラ」として利用できる。
- ゲーム内のフリーカメラモードやリプレイビューアなどに、そのまままたは少しの改造で流用できる。
- 他のカスタムスクリプトに一切依存していないため、どのプロジェクトにも簡単に持ち込んで再利用できる。
- 移動速度・回転感度・ピッチ制限・カーソルロックなどをインスペクタから調整でき、用途に応じたチューニングが容易。
例えば、ゲーム本編では通常の追従カメラを使い、デバッグビルド時だけ FreeCamera を有効化して自由にシーンを飛び回る、といった運用も可能です。
本記事のコードをベースに、特定キーでの有効/無効切り替え や 境界エリア内に移動を制限 するなど、プロジェクトに合わせた拡張も簡単に行えます。
この FreeCamera をプロジェクトの「標準デバッグカメラ」として組み込んでおけば、シーン確認や調整作業の効率が大きく向上するはずです。




